[Serie de aprendizaje profundo (6)]: Serie RNN (4): modelo seq2seq con mecanismo de atención y combate real (2): agregue una descripción del contenido a las imágenes

Este es principalmente el combate real de la seq2seq basada en la atención anterior. La imagen se describe a través de seq2seq. No digas tonterías, comencemos.


Tabla de contenido

Uno, el conjunto de datos

1.1, lea la etiqueta de texto y almacene

1.2. Convierta la imagen en datos de características y almacénela

1.3. Preprocesamiento, filtrado de datos de texto, creación de un diccionario, alineación y vectorización

1.4, crea un conjunto de datos

2. Construcción del modelo seq2seq basado en la atención

2.1, codificador

2.2. Construir un mecanismo de atención tipo Bahdanau

2.3, decodificador

Tres, entrene el modelo seq2seq en el gráfico dinámico

3.1, definir el modelo

3.2, entrenamiento de modelos

4. Predicción y uso de modelos

Seis, resumen


Uno, el conjunto de datos

El conjunto de datos COCO es un conjunto de datos competente publicado por Microsoft que se puede utilizar para el reconocimiento de imágenes. Las imágenes se interceptan principalmente de escenas complejas. Aquí usamos el conjunto de datos COCO2014 para agregar una descripción a la imagen, la dirección de descarga del conjunto de datos COCO:

http://cocodataset.org/#download

 El conjunto de entrenamiento incluye principalmente 82783 imágenes en el conjunto de entrenamiento, 40504 en el conjunto de validación y 40775 en el conjunto de prueba, que se dividen en 80 categorías. También está equipado con anotaciones rectangulares para la detección de objetivos, anotaciones de dispersión para la segmentación semántica, anotaciones basadas en puntos clave del cuerpo humano y anotaciones descriptivas para imágenes.

Debido a que hay demasiados conjuntos de datos de COCO2014, solo se seleccionan 300 imágenes para la demostración de capacitación aquí. Los pasos de implementación específicos son los siguientes:

  • Leer etiqueta de texto y almacenar
  • Convierta imágenes en datos de características y almacénelas
  • Procesar previamente el filtro de datos de texto, crear un diccionario, alinear y vectorizar
  • Crear conjunto de datos

Se puede ver en los pasos anteriores que necesitamos extraer las características de la imagen y convertirlas en datos en formato .npy, y usar el vector de características como los datos de entrada del codificador. Aquí usamos el modelo Reset50 definido para extraer características, por lo que también necesitamos Descargar modelos de preentrenamiento relacionados:

resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5

Aquí proporcionamos el modelo de entrenamiento previo ResNet50 entrenado y los datos de entrenamiento después de que se hayan extraído las funciones para una fácil descarga. La dirección específica es:

Enlace: https://pan.baidu.com/s/13ne5HuovZKvqvPHwNVrpvw Código de extracción: deea

1.1, lea la etiqueta de texto y almacene

Use train_caption e img_filenames para almacenar el nombre del archivo y su etiqueta de descripción. Aquí necesitamos agregar las etiquetas <start> y <end> al principio y al final de la etiqueta de descripción de la imagen para facilitar el entrenamiento posterior.
  

with open(annotation_file, 'r') as f:  # 加载标注文件
        annotations = json.load(f)

    train_caption = []  # 存储图片对应的标题
    img_filenames = []  # 存储图片的路径

    # 获取全部文件及对应的标注文本
    for annot in annotations['annotations']:
        caption = '<start> ' + annot['caption'] + ' <end>'
        img_id = annot['image_id']

        full_coco_image_path = 'COCO_train2014_' + "%012d.jpg" % (img_id)

        img_filenames.append(full_coco_image_path)
        train_caption.append(caption)

        if len(train_caption) >= num_example:
            break

1.2. Convierta la imagen en datos de características y almacénela

Al entrenar el modelo, cada iteración del modelo necesita convertir imágenes en características y luego realizar cálculos, lo que hace que el programa requiera muchas operaciones repetitivas. Aquí, la imagen puede convertirse directamente en un vector de características y almacenarse en el enlace de preprocesamiento, lo que puede ahorrar muchas operaciones repetitivas.

Aquí, usamos ResNet50 como el modelo básico para la extracción de características, y tomamos la característica de la segunda capa de la derivada del modelo como la entrada de todo el modelo. La forma de entrada es: (batch_size, 49, 2048). El proceso de implementación específico es el siguiente:
 

def make_numpy_feature(numpyPATH, img_filename, PATH, weights=RESNET50_WEIGHTS):
    '''
    将图片转化为特征数据
    :param numpyPATH:存储提取好特征后的存储文件夹
    :param img_filename:图片文件列表
    :param PATH: 图片所在的文件夹
    :param weights: ResNet50模型的权重
    :return:
    '''
    if os.path.exists(numpyPATH):  # 去除已有文件夹
        shutil.rmtree(numpyPATH, ignore_errors=True)

    os.mkdir(numpyPATH)  # 新建文件夹

    size = [224, 224]  # 设置输出尺寸
    batch_size = 10  # 批量大小

    def load_image(image_path):
        '''输入图片的预处理'''
        img = tf.compat.v1.read_file(PATH + image_path)
        img = tf.image.decode_jpeg(img, channels=3)
        img = tf.image.resize(img, size)
        # 使用Reset模型的统一预处理
        img = tf.keras.applications.resnet50.preprocess_input(img)
        return img, image_path

    # 创建ResNet模型
    image_model = ResNet50(weights=weights, include_top=False)
    new_input = image_model.input
    # 获取ResNet导数第二层(池化前的卷积结果)
    hidden_layer = image_model.layers[-2].output
    image_feature_extract_model = tf.keras.Model(new_input, hidden_layer)
    image_feature_extract_model.summary()

    # 对文件目录去重
    encode_train = sorted(set(img_filename))

    # 图片数据集
    image_dataset = tf.data.Dataset.from_tensor_slices(encode_train).map(load_image).batch(batch_size)

    for img, path in image_dataset:
        batch_feature = image_feature_extract_model(img)
        print(batch_feature.shape)
        batch_feature = tf.reshape(batch_feature, (batch_feature.shape[0], -1, batch_feature.shape[3]))
        print(batch_feature.shape)
        for bf, p in zip(batch_feature, path):
            path_of_feature = p.numpy().decode('utf-8')
            np.save(numpyPATH + path_of_feature, bf.numpy())

1.3. Preprocesamiento, filtrado de datos de texto, creación de un diccionario, alineación y vectorización

Después de preprocesar los datos de la imagen COCO2014, también es necesario preprocesar cada etiqueta de texto. Los pasos específicos son:

  1. Texto de filtro: eliminar caracteres no válidos
  2. Construya un diccionario: genere diccionarios hacia adelante y hacia atrás
  3. Operaciones de alineación y texto vectorizado: vectorizar el texto de acuerdo con el diccionario y alinear el texto de acuerdo con la longitud especificada

La implementación del código específico es la siguiente:

ef text_preprocessing(train_caption, max_vocab_size):
    '''
    文本标签预处理:(1)文本过滤;(2)建立字典;(3)向量化文本以及文本对齐
    :param train_caption: 文本标签数据集
    :param max_vocab_size: 限制最大字典的大小
    :return:
    '''
    # 文本过滤,去除无效字符
    tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=max_vocab_size,
                                                      oov_token="<unk>",
                                                      filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~ ')
    tokenizer.fit_on_texts(train_caption)

    # 建立字典,构造正反向字典
    tokenizer.word_index = {key: value for key, value in tokenizer.word_index.items() if value <= max_vocab_size}
    # 向字典中加入<unk>字符
    tokenizer.word_index[tokenizer.oov_token] = max_vocab_size + 1
    # 向字典中加入<pad>字符
    tokenizer.word_index['<pad>'] = 0

    index_word = {value: key for key, value in tokenizer.word_index.items()}

    # 向量化文本和对齐操作,将文本按照字典的数字进行项向量化处理,
    # 并按照指定长度进行对齐操作(多余的截调,不足的进行补零)
    train_seqs = tokenizer.texts_to_sequences(train_caption)
    cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
    max_length = len(cap_vector[0])  # 标签最大长度

    return cap_vector, max_length, tokenizer.word_index, index_word

1.4, crea un conjunto de datos

La interfaz tf.data.Dataset se usa aquí para combinar el archivo de características y el vector de texto para generar un conjunto de datos para preparar el modelo de entrenamiento. La implementación específica es la siguiente:

def load_data(annotation_file, PATH, numpyPATH,
              num_example=NUM_EXAMPLE, max_vocab_size=MAX_VOCAB_SIZE):
    '''
    对数据集进行预处理并加载数据集
    :param annotation_file: 训练数据的标注文件
    :param PATH: 图片数据集
    :param numpyPATH: 将图片提取特后的存储的位置
    :param num_example: 这里选择其中300个样本数据(注意为了方便训练演示,你也可以训练全部数据集)
    :param max_vocab_size: 限定字典的最大长度
    :return:
    '''
    with open(annotation_file, 'r') as f:  # 加载标注文件
        annotations = json.load(f)

    train_caption = []  # 存储图片对应的标题
    img_filenames = []  # 存储图片的路径

    # 获取全部文件及对应的标注文本
    for annot in annotations['annotations']:
        caption = '<start> ' + annot['caption'] + ' <end>'
        img_id = annot['image_id']

        full_coco_image_path = 'COCO_train2014_' + "%012d.jpg" % (img_id)

        img_filenames.append(full_coco_image_path)
        train_caption.append(caption)

        if len(train_caption) >= num_example:
            break

    # 将图片转化为特征数据,并进行存储
    if os.path.exists(numpyPATH):
        make_numpy_feature(numpyPATH, img_filenames, PATH)

    # 文本数据的预处理
    cap_vector, max_length, word_index, index_word = text_preprocessing(train_caption, max_vocab_size)

    # 将数据拆分为训练集和测试集
    img_name_train, img_name_val, cap_train, cap_val = \
        train_test_split(img_filenames, cap_vector, test_size=0.2, random_state=0)

    return img_name_train, cap_train, img_name_val, cap_val, max_length, word_index, index_word


def dataset(annotation_file, PATH, numpyPATH, batch_size):
    '''
    创建数据集
    :param instances_file: 训练数据
    :param annotation_file: 训练数据的标注文件
    :param PATH: 图片数据集
    :param numpyPATH: 将图片提取特后的存储的位置
    :param batch_size: 数据集的batch size
    :return:
    '''
    img_name_train, cap_train, img_name_val, cap_val, max_length, word_index, index_word = \
        load_data(annotation_file, PATH, numpyPATH)

    def map_func(img_name, cap):
        #         print("===========================================",numpyPATH+str(img_name.numpy()).split("\'")[1]+'.npy')
        img_tensor = np.load(numpyPATH + str(img_name.numpy()).split("\'")[1] + '.npy')
        return img_tensor, cap

    train_dataset = tf.data.Dataset.from_tensor_slices((img_name_train, cap_train))

    train_dataset = train_dataset.map(lambda item1, item2: tf.py_function(
        map_func, [item1, item2], [tf.float32, tf.int32]), num_parallel_calls=8)

    train_dataset = train_dataset.shuffle(1000).batch(batch_size).prefetch(1)

    return train_dataset, img_name_val, cap_val, max_length, word_index, index_word

2. Construcción del modelo seq2seq basado en la atención

2.1, codificador

El codificador aquí es relativamente simple, con una capa de enlace completo. La función de la capa de enlace completo es principalmente transformar los datos originales específicos de la imagen para que la dimensión específica de la imagen sea la misma que la de la capa de inserción. El resultado final la forma es (tamaño_lote, 49, incrustación) . La implementación del código específico es la siguiente:

# 编码器模型
class DNN_Encoder(tf.keras.Model):
    def __init__(self, embedding_dim):
        super(DNN_Encoder, self).__init__()
        # keras的全连接支持多维输入。仅对最后一维进行处理
        self.fc = tf.keras.layers.Dense(embedding_dim)  # (batch_size, 49, embedding_dim)

    def call(self, inputs, training=None, mask=None):
        x = self.fc(inputs)
        x = tf.keras.layers.Activation('relu')(x)

        return x

2.2. Construir un mecanismo de atención tipo Bahdanau

class BahdanauAttention(tf.keras.Model):
    def __init__(self, units):
        super(BahdanauAttention, self).__init__()
        self.W1 = tf.keras.layers.Dense(units)
        self.W2 = tf.keras.layers.Dense(units)
        self.V = tf.keras.layers.Dense(1)

    def call(self, features,  # features形状(batch_size, 49, embedding_dim)
             hidden):  # hidden(batch_size, hidden_size)

        hidden_with_time_axis = tf.expand_dims(hidden, 1)  # (batch_size, 1, hidden_size)

        score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))  # (batch_size, 49, hidden_size)

        attention_weights = tf.nn.softmax(self.V(score), axis=1)  # (batch_size, 49, 1)

        context_vector = attention_weights * features  # (batch_size, 49, hidden_size)
        context_vector = tf.reduce_sum(context_vector, axis=1)  # (batch_size,  hidden_size)

        return context_vector, attention_weights

2.3, decodificador

Los principales pasos del decodificador son:

  1. Utilice el mecanismo de atención para procesar las características de salida del codificador y la capa oculta de salida del decodificador en el momento anterior (tenga en cuenta que si es t = 1, la capa oculta necesaria en este momento es la capa oculta que inicializamos)
  2. Utilice GRU para construir el modelo RNN
  3. Utilice la capa de enlace completo para procesar la salida para obtener el resultado de salida
def gru(units):
    if tf.test.is_gpu_available():
        return tf.keras.layers.CuDNNGRU(units,
                                        return_sequences=True,
                                        return_state=True,
                                        recurrent_initializer='glorot_uniform')
    else:
        return tf.keras.layers.GRU(units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_activation='sigmoid',
                                   recurrent_initializer='glorot_uniform')

# 解码器模型
class RNN_Decoder(tf.keras.Model):
    def __init__(self, embedding_dim, units, vocab_size):
        super(RNN_Decoder, self).__init__()
        self.units = units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = gru(self.units)
        self.fc1 = tf.keras.layers.Dense(self.units)
        self.fc2 = tf.keras.layers.Dense(vocab_size)

        self.attention = BahdanauAttention(self.units)

    def call(self, inputs, features, hidden, training=None, mask=None):
        # 返回注意力特征向量和注意力权重
        context_vector, attention_weights = self.attention(features, hidden)

        x = self.embedding(inputs)

        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)  # (batch_size, 1, embedding_dim + hidden_size)
        output, state = self.gru(x)  # 使用循环网络gru进行处理

        x = self.fc1(output)  # (batch_size, max_length, hidden_size)
        x = tf.reshape(x, (-1, x.shape[2]))  # (batch_size * max_length, hidden_size)

        x = self.fc2(x)  # (batch_size * max_length, vocab)

        return x, state, attention_weights

    def reset_state(self, batch_size):
        return tf.zeros((batch_size, self.units))

Tres, entrene el modelo seq2seq en el gráfico dinámico

3.1, definir el modelo

Aquí necesitamos definir la función de pérdida del modelo, el optimizador, la creación del modelo y otras operaciones. La implementación específica es la siguiente:

# 加载数据
batch_size = 20
annotation_file = r'annotations/captions_train2014.json'
PATH = r"train2014/"
numpyPATH = './numpyfeature/'
dataset, img_name_val, cap_val, max_length, word_index, index_word = dataset(annotation_file, PATH, numpyPATH,
                                                                             batch_size)

# 模型搭建
embedding_dim = 256
units = 512
vocab_size = len(word_index)  # 字典大小

# 图片特征(47, 2048)
features_shape = 2048
attention_features_shape = 49

# 创建模型对象字典
model_objects = {
    'encoder': DNN_Encoder(embedding_dim),
    'decoder': RNN_Decoder(embedding_dim, units, vocab_size),
    'optimizer': tf.train.AdamOptimizer(),
    'step_counter': tf.train.get_or_create_global_step(),
}

checkpoint_prefix = os.path.join("mytfemodel/", 'ckpt')
checkpoint = tf.train.Checkpoint(**model_objects)
latest_cpkt = tf.train.latest_checkpoint("mytfemodel/")
if latest_cpkt:
    print('Using latest checkpoint at ' + latest_cpkt)
    checkpoint.restore(latest_cpkt)


def loss_mask(real, pred):
    '''使用Softmax屏蔽计算损失'''
    mask = 1 - np.equal(real, 0)  # 批次中被补0的序列不参与计算loss
    loss_ = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=real, logits=pred) * mask
    return tf.reduce_mean(loss_)


def all_loss(encoder, decoder, img_tensor, target):
    loss = 0
    hidden = decoder.reset_state(batch_size=target.shape[0])

    dec_input = tf.expand_dims([word_index['<start>']] * target.shape[0], 1)
    feature = encoder(img_tensor)  # (batch_size,49,256)

    for i in range(1, target.shape[1]):
        predictions, hidden, _ = decoder(dec_input, feature, hidden)
        loss += loss_mask(target[:, i], predictions)

        dec_input = tf.expand_dims(target[:, i], 1)
    return loss


grad = tfe.implicit_gradients(all_loss)

3.2, entrenamiento de modelos

El entrenamiento en gráficos dinámicos implica principalmente los siguientes pasos:

  1. Definir la función de entrenamiento de un solo paso
  2. Para iteración de bucle utilizando la función de entrenamiento de un solo paso
  3. Guarde la pérdida durante el entrenamiento y muéstrela

La implementación específica es la siguiente:

grad = tfe.implicit_gradients(all_loss)


# 实现单步训练过程
def train_one_epoch(encoder, decoder, optimizer, step_counter, dataset, epoch):
    total_loss = 0
    for (step, (img_tensor, target)) in enumerate(dataset):
        loss = 0

        optimizer.apply_gradients(grad(encoder, decoder, img_tensor, target), step_counter)
        loss = all_loss(encoder, decoder, img_tensor, target)

        total_loss += (loss / int(target.shape[1]))
        if step % 5 == 0:
            print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
                                                         step,
                                                         loss.numpy() / int(target.shape[1])))
    print("step", step)
    return total_loss / (step + 1)


# 训练模型
loss_plot = []
EPOCHS = 50

for epoch in range(EPOCHS):
    start = time.time()
    total_loss = train_one_epoch(dataset=dataset, epoch=epoch, **model_objects)  # 训练一次

    loss_plot.append(total_loss)  # 保存loss

    print('Epoch {} Loss {:.6f}'.format(epoch + 1, total_loss))
    checkpoint.save(checkpoint_prefix)
    print('Train time for epoch #%d (step %d): %f' %
          (checkpoint.save_counter.numpy(), checkpoint.step_counter.numpy(), time.time() - start))

#
plt.plot(loss_plot)
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Plot')
plt.show()

El proceso de formación es el siguiente:

La curva de pérdida es:

4. Predicción y uso de modelos

Aquí usamos la técnica de optimización del muestreo de distribución polinomial que discutimos anteriormente. Los pasos principales de la predicción del modelo son los siguientes:

  1. Construya ResNet50 como la capa de extracción de características de la imagen
  2. Ingrese las características extraídas en el codificador
  3. Pase el resultado del codificador y el estado intermedio al decodificador
  4. Decodificar cíclicamente el marco seq2seq y generar información de texto a su vez
  5. Utilice el muestreo de distribución polinomial para obtener el texto de la secuencia actual del resultado del decodificador
  6. Siga los pasos 4 y 5 para procesar en un bucle hasta que la salida sea <end> o se exceda el mensaje de texto más largo

Y la implementación del código específico es la siguiente:

def evaluate(encoder, decoder, optimizer, step_counter, image):
    attention_plot = np.zeros((max_length, attention_features_shape))

    hidden = decoder.reset_state(batch_size=1)
    size = [224, 224]

    def load_image(image_path):
        img = tf.read_file(PATH + image_path)
        img = tf.image.decode_jpeg(img, channels=3)
        img = tf.image.resize(img, size)
        img = tf.keras.applications.resnet50.preprocess_input(img)
        return img, image_path

    from tensorflow.python.keras.applications.resnet import ResNet50

    image_model = ResNet50(weights='resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5'
                           , include_top=False)  # 创建ResNet网络

    new_input = image_model.input
    hidden_layer = image_model.layers[-2].output
    image_features_extract_model = tf.keras.Model(new_input, hidden_layer)

    temp_input = tf.expand_dims(load_image(image)[0], 0)
    img_tensor_val = image_features_extract_model(temp_input)
    img_tensor_val = tf.reshape(img_tensor_val, (img_tensor_val.shape[0], -1, img_tensor_val.shape[3]))

    features = encoder(img_tensor_val)

    #    print(step_counter.numpy())
    dec_input = tf.expand_dims([word_index['<start>']], 0)
    result = []

    for i in range(max_length):
        predictions, hidden, attention_weights = decoder(dec_input, features, hidden)

        attention_plot[i] = tf.reshape(attention_weights, (-1,)).numpy()

        print(predictions.get_shape())

        predicted_id = tf.multinomial(predictions, num_samples=1)[0][0].numpy()
        result.append(index_word[predicted_id])

        print(predicted_id)

        if index_word[predicted_id] == '<end>':
            return result, attention_plot

        dec_input = tf.expand_dims([predicted_id], 0)

    attention_plot = attention_plot[:len(result), :]
    return result, attention_plot


def plot_attention(image, result, attention_plot):
    temp_image = np.array(Image.open(PATH + image))

    fig = plt.figure(figsize=(10, 10))

    len_result = len(result)
    for l in range(len_result):
        # print(len(attention_plot[l]),attention_plot[l])
        temp_att = np.resize(attention_plot[l], (7, 7))
        ax = fig.add_subplot(len_result // 2, len_result // 2 + len_result % 2, l + 1)
        ax.set_title(result[l])
        img = ax.imshow(temp_image)
        ax.imshow(temp_att, cmap='gray', alpha=0.4, extent=img.get_extent())

    plt.tight_layout()
    plt.show()


# captions on the validation set
rid = np.random.randint(0, len(img_name_val))

image = img_name_val[rid]
real_caption = ' '.join([index_word[i] for i in cap_val[rid] if i not in [0]])
result, attention_plot = evaluate(image=image, **model_objects)

print('Real Caption:', real_caption)
print('Prediction Caption:', ' '.join(result))
plot_attention(image, result, attention_plot)
## opening the image
img = Image.open(PATH + img_name_val[rid])
plt.imshow(img)
plt.axis('off')
plt.show()

El resultado final:

El resultado de la salida visual es:

La imagen original es:

Por supuesto, el código completo se puede encontrar en mi github, la dirección es: https://github.com/kingqiuol/learning_tensorflow/tree/master/nlp/seq2seq

Seis, resumen

La práctica del modelo Seq2Seq termina aquí, preste atención para descubrir algunas de las formas de los datos y los detalles de cómo calcular, para que podamos tener una comprensión más profunda del modelo Seq2Seq basado en el mecanismo de atención.

Supongo que te gusta

Origin blog.csdn.net/wxplol/article/details/104448914
Recomendado
Clasificación