图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


Encoder编码器-Decoder解码器框架 + Attention注意力机制

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part1

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part2

Pytorch:解码器端的Attention注意力机制、seq2seq模型架构实现英译法任务

BahdanauAttention注意力机制、LuongAttention注意力机制

BahdanauAttention注意力机制:基于seq2seq的西班牙语到英语的机器翻译任务、解码器端的Attention注意力机制、seq2seq模型架构

图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制

注意力机制、bmm运算

注意力机制 SENet、CBAM

机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)

基于Seq2Seq的中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制)

基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)

注意:这一文章“基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)”
	该文章实现的Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
	而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
	所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。

图片的描述生成任务

学习目标

  • 了解关于图片的描述生成任务和MSCOCO数据集.
  • 掌握使用迁移学习实现图片的描述生成过程.

任务说明

  • 以一张图片为输入, 使用模型帮助我们生成针对图片内容的描述, 描述将会以文本的形式展现, 即输出为一段与图片有关的文字。这样的任务将适用于很多实际场景中, 比如直播间的聊天机器人需要针对主播某一时刻的截图进行评论, 来合理的进行与主播互动, 增加直播室热度, 提升用户留存率.

数据集说明

  • 数据集名称: MS-COCO
  • 数据下载地址: http://cocodataset.org/#download
  • 数据文件分为两部分:
    • 标注文件: annotations/captions_train2014.json
    • 图片文件: train2014/xxxx.jpg
  • 标注文件captions_train2014.json预览
{"info": {"description": "COCO 2014 Dataset","url": "http://cocodataset.org","version": "1.0","year": 2014,"contributor": "COCO Consortium","date_created": "2017/09/01"},
"images": [{"license": 5,"file_name": "COCO_train2014_000000057870.jpg","coco_url": "http://images.cocodataset.org/train2014/COCO_train2014_000000057870.jpg","height": 480,"width": 640,"date_captured": "2013-11-14 16:28:13","flickr_url": "http://farm4.staticflickr.com/3153/2970773875_164f0c0b83_z.jpg","id": 57870}, ...]
"annotations": [{"image_id": 318556,"id": 48,"caption": "A very clean and well decor
ated empty bathroom"},{"image_id": 116100,"id": 67,"caption": "A panoramic view of a kit
chen and all of its appliances."}, ...]
}
  • 标注文件分析:
    • 标注文件captions_train2014.json中存在三个键, 分别是:”info”, “images”, “annotations”, 代表”数据集信息”, “图片详情”, “图片标注描述详情”, 其中”annotations”是我们用到的, “annotations”的值是一个列表, 包含所有的图片对应的描述信息, 每个图片的描述信息是一个字典形式, 包含”image_id”, “id”, “caption”三个键, 代表对应的图片id, 描述信息的唯一标识(同一张图片可能存在多个描述信息), 描述信息的具体内容
  • 图片文件train2014/xxxx.jpg预览
COCO_train2014_000000218579.jpg      COCO_train2014_000000509321.jpg
COCO_train2014_000000218580.jpg      COCO_train2014_000000509339.jpg
COCO_train2014_000000218589.jpg      COCO_train2014_000000509350.jpg
COCO_train2014_000000218599.jpg      COCO_train2014_000000509358.jpg
COCO_train2014_000000218601.jpg      COCO_train2014_000000509365.jpg
...
  • 图片文件分析:
    • 所有的文件格式为jpg, 图片名称由数据集名称COCO_train2014以及图片id:000000218579组成, 对应标注文件的中描述信息”image_id”。图片总数为85000张, 每张图片至少在标注文件中存在5条描述信息.

使用迁移学习实现图片的描述生成过程

  • 第一步: 导入必备的工具包并下载MS-COCO数据集.
  • 第二步: 限制训练集的大小以保证在可控时间内完成训练.
  • 第三步: 使用InceptionV3预训练模型处理图片训练集数据.
  • 第四步: 对图片描述的文本进行处理.
  • 第五步: 划分训练与验证数据集并使用tf.data封装.
  • 第六步: 构建微调模型并选取优化方法和损失函数.
  • 第七步: 构建训练函数并进行训练.
  • 第八步: 构建评估函数并进行评估.

第一步: 导入必备的工具包并下载MS-COCO数据集

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
# 打印tensorflow版本
print("Tensorflow Version:", tf.__version__)

# 导入matplotlib进行损失曲线的绘制
import matplotlib.pyplot as plt

# 导入sklearn中的相关工具以便进行训练集与验证集划分
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle

# 导入一些必备的处理工具包
import re
import numpy as np
import os
import time
import json
from glob import glob
from PIL import Image
import pickle


# 下载图片的标注文件
# 定义标注文件的存储文件目录
annotation_folder = '/annotations/'

# 如果不存在该文件目录
if not os.path.exists(os.path.abspath('.') + annotation_folder):
    # 使用tf.keras工具中get_file方法下载图片的标注文件 
    # 'captions.zip'是下载的文件名, cache_subdir表示文件缓存路径
    # origin表示文件下载地址, extract表示是否对文件进行解压缩
    # 进行解压缩后, 获得压缩包的地址annotation_zip
    annotation_zip = tf.keras.utils.get_file('captions.zip',
                                          cache_subdir=os.path.abspath('.'),
                                          origin = 'http://images.cocodataset.org/annotations/annotations_trainval2014.zip',
                                          extract = True)
    # 获得解压缩的标注文件地址 
    annotation_file = os.path.dirname(annotation_zip)+'/annotations/captions_train2014.json'
    # 文件解压后, 删除压缩包
    os.remove(annotation_zip)

# 下载图片文件
# 定义图片文件的存储文件目录
image_folder = '/train2014/'

# 如果不存在该文件目录
if not os.path.exists(os.path.abspath('.') + image_folder):
    # 过程和下载标注文件相同
    image_zip = tf.keras.utils.get_file('train2014.zip',
                                      cache_subdir=os.path.abspath('.'),
                                      origin = 'http://images.cocodataset.org/zips/train2014.zip',
                                      extract = True)
    # 定义图片文件路径
    PATH = os.path.dirname(image_zip) + image_folder
    # 删除压缩包
    os.remove(image_zip)
  • 输出效果
Tensorflow Version: 2.1.0-rc2
Downloading data from http://images.cocodataset.org/annotations/annotations_trainval2014.zip
252878848/252872794 [==============================] - 16s 0us/step
Downloading data from http://images.cocodataset.org/zips/train2014.zip
 6301122560/13510573713 [============>.................] - ETA: 7:05

下载后的文件详情请参考2.1 使用迁移学习进行图片的描述生成任务下的相关数据集.

第二步: 限制训练集的大小以保证在可控时间内完成训练

  • 限制训练集大小的目标:
    • 为了加快训练速度,将使用30,000个训练子集来训练模型。如果你的硬件资源足够充分,也可以选择使用更多数据来提高模型质量。
# 将标注的json文件加载到内存
with open(annotation_file, 'r') as f:
    annotations = json.load(f)

# 定义存储图片和对应描述的列表
all_captions = []
all_mg_name_vector= []

# 循环遍历标注的json文件中的键'annotations'
for annot in annotations['annotations']:
    # 将每一个caption(描述)加上开始和结束标记
    caption = '<start> ' + annot['caption'] + ' <end>'
    # 再取对应的image_id
    image_id = annot['image_id']
    # 对应图片文件的图片全路径
    full_coco_image_path = PATH + 'COCO_train2014_' + '%012d.jpg' % (image_id)
    # 将图片全路径装进列表中
    all_img_name_vector.append(full_coco_image_path)
    # 将对应的描述装进列表中
    all_captions.append(caption)

# 使用shuffle方法打乱数据集中的数据顺序
train_captions, img_name_vector = shuffle(all_captions,
                                          all_img_name_vector,
                                          random_state=1)

# 选取30000条作为使用数据
num_examples = 30000
train_captions = train_captions[:num_examples]
img_name_vector = img_name_vector[:num_examples]

# 打印使用数据数量和数据原本的数量
print(len(train_captions), len(all_captions))
  • 输出效果
# 共有数据414113条, 只选取30000条使用
(30000,414113)

第三步: 使用InceptionV3预训练模型处理图片训练集数据

  • 使用InceptionV3中的预处理方法对图像进行处理,将像素缩放至[-1, 1], 以便之后迁移InceptionV3模型
# 创建一个函数load_image来处理原生图片 

def load_image(image_path):
    """以原生图片路径image_path为参数, 返回处理后的图片和图片路径"""
    # 读取原生图片路径 
    img = tf.io.read_file(image_path)
    # 对图片进行图片格式的解码, 颜色通道为3
    img = tf.image.decode_jpeg(img, channels=3)
    # 统一图片尺寸为299x299
    img = tf.image.resize(img, (299, 299))
    # 调用keras.applications.inception_v3中的preprocess_input方法对统一尺寸后的图片进行处理
    img = tf.keras.applications.inception_v3.preprocess_input(img)
    # 返回处理后的图片和对应的图片地址
    return img, image_path
  • 调用
image_path = "./train2014/COCO_train2014_000000520749.jpg"
img, image_path = load_image(image_path)
  • 输出效果:
# 图片地址:
./train2014/COCO_train2014_000000520749.jpg

# 图片效果:

  • 初始化InceptionV3模型并加载预训练的Imagenet权重:
# 使用tf.keras.applications.InceptionV3并加载imagenet权重, 不包括模型的输出头
image_model = tf.keras.applications.InceptionV3(include_top=False,
                                                weights='imagenet')
# 将预训练模型的输入作为特征提取模型的输入
new_input = image_model.input

# 将预训练模型的最后一层的输出部分作为特征提取模型的输出
hidden_layer = image_model.layers[-1].output

# 根据输入和输出构建特征提取模型
image_features_extract_model = tf.keras.Model(new_input, hidden_layer)
  • 调用
print(image_features_extract_model)
  • 输出效果
# keras模型对象
<tensorflow.python.keras.engine.training.Model object at 0x7f2a4074fa10>
  • 使用模型对特征进行提取
# 将之前选取的30000条数据进行去重并排序作为特征提取对象
encode_train = sorted(set(img_name_vector))

# 将encode_train列表创建基于tensor的tf数据集, 方便之后对数据集对象进行操作 
image_dataset = tf.data.Dataset.from_tensor_slices(encode_train)

# 根据硬件资源本身的情况,对数据集进行并行数据处理(使用load_image进行处理), 并将16个数据合并成1个批次
image_dataset = image_dataset.map(
  load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE).batch(16)

# 遍历image_dataset
for img, path in image_dataset:
    # 使用之前构建的特征提取模型对每一批图片进行特征提取, 得到批次特征
    batch_features = image_features_extract_model(img)
    # 将四维(图片本身3维+批次数1维)的批次特征转化成三维
    batch_features = tf.reshape(batch_features,
                              (batch_features.shape[0], -1, batch_features.shape[3]))

    # 防止硬件内存无法满足要求,将batch图片特征存储在对应的路径下
    for bf, p in zip(batch_features, path):
        # 得到特征的路径
        path_of_feature = p.numpy().decode("utf-8")
        # 使用numpy进行存储
        np.save(path_of_feature, bf.numpy())
  • 输出效果:
    • 在./train2014/路径下出现以下.npy结尾的文件
COCO_train2014_000000218579.jpg.npy      COCO_train2014_000000509321.jpg.npy
COCO_train2014_000000218580.jpg.npy      COCO_train2014_000000509339.jpg.npy
COCO_train2014_000000218589.jpg.npy      COCO_train2014_000000509350.jpg.npy
COCO_train2014_000000218599.jpg.npy      COCO_train2014_000000509358.jpg.npy
COCO_train2014_000000218601.jpg.npy      COCO_train2014_000000509365.jpg.npy

第四步: 对图片描述的文本进行处理

  • 选取最常出现的前5000个词汇进行数值映射:
# 最常出现的词汇个数
top_k = 5000

# 使用tf.keras.preprocessing.text.Tokenizer方法实例化数值映射器, 其中超出部分的词汇使用<unk>表示
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
                                                  oov_token="<unk>",
                                                  filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~ ')

# 使用数值映射器拟合train_captions(用于训练的描述文本)
tokenizer.fit_on_texts(train_captions)

# 数值映射器是从1开始不断映射的, 因此可以将0作为英文词汇的空白分割符
# 这里使用<pad>表示英文词汇的空白分割符
tokenizer.word_index['<pad>'] = 0
tokenizer.index_word[0] = '<pad>'

# 最后作用于描述文本得到对应的数值映射结果
train_seqs = tokenizer.texts_to_sequences(train_captions)
print("train_seqs:", train_seqs)
  • 输出效果
[[3, 2, 184, 185, 14, 7, 2, 154, 8, 2, 66, 127, 4], 
 [3, 2, 13, 26, 17, 2, 471, 10, 2, 472, 320, 473, 12, 2, 234, 4], 
 [3, 18, 474, 57, 235, 2, 83, 321, 4], 
 [3, 25, 109, 475, 322, 84, 476, 477, 7, 478, 4], 
 [3, 2, 15, 40, 14, 7, 38, 5, 2, 23, 236, 4], 
 [3, 2, 13, 237, 41, 71, 17, 2, 186, 128, 4], 
 [3, 48, 129, 479, 238, 155, 480, 110, 6, 44, 4], 
 ...
 [3, 2, 49, 11, 14, 7, 2, 239, 24, 12, 2, 130, 323, 4], 
 [3, 187, 481, 8, 48, 482, 483, 24, 12, 188, 58, 4], 
 [3, 2, 131, 50, 189, 5, 2, 67, 37, 10, 2, 52, 484, 4]]
  • 为了保证输入满足要求, 需要对数值映射结果进行最大长度补齐:
# 使用tf.keras.preprocessing.sequence.pad_sequences进行补齐, 参数'post'代表使用0在序列前面补齐
cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
print("cap_vector:", cap_vector)
  • 输出效果
cap_vector: [[  3   2 184 ...   0   0   0]
 [  3   2  13 ...   0   0   0]
 [  3  18 474 ...   0   0   0]
 ...
 [  3 284 220 ...   0   0   0]
 [  3   2   1 ...   0   0   0]
 [  3  48  19 ...   0   0   0]]
  • 获取图片描述文本的最大长度, 将在之后的步骤中使用:
def calc_max_length(tensor):
    """计算最大长度的函数"""
    return max(len(t) for t in tensor)

# 获取训练集数据映射后的最大长度
max_length = calc_max_length(train_seqs)
print("max_length:", max_length)
  • 输出效果:
# 根据使用的样本数量,最大长度可能发生变化
max_length: 28

第五步: 划分训练与验证数据集并使用tf.data封装

  • 划分训练与验证数据集:
# 使用train_test_split方法对数据集进行划分,训练集占80%,验证集占20%
img_name_train, img_name_val, cap_train, cap_val = train_test_split(img_name_vector,
                                                                    cap_vector,
                                                                    test_size=0.2,
                                                                    random_state=0)

# 打印对应的数量
print(len(img_name_train), len(cap_train), len(img_name_val), len(cap_val))
  • 输出效果
(24000, 24000, 6000, 6000)
  • 创建一个tf.data数据集准备用于训练:
# 设定训练过程的超参数

# 参数更新的批次数量
BATCH_SIZE = 64

# 数据打乱时的缓存区大小,缓存区越大结果混乱程度越高
BUFFER_SIZE = 1000

# 对描述文本进行嵌入的维度大小
embedding_dim = 256

# 联合嵌入特征的维度大小(文本嵌入的维度+图片编码后的维度)
units = 512

# 不重复的词汇总数
vocab_size = top_k + 1

# 完成一轮数据训练的步数
num_steps = len(img_name_train) // BATCH_SIZE

# 以下两个参数的值由InceptionV3模型的输出形状决定
# InceptionV3模型的输出形状为(8, 8, 2048)即(64, 2048) 
# 对应attention_features_shape和features_shape 
attention_features_shape = 64
features_shape = 2048

# 使用tf.data.Dataset.from_tensor_slices方法构建tf.data数据集, 便于之后使用
dataset = tf.data.Dataset.from_tensor_slices((img_name_train, cap_train))

# 加载之前存储的numpy图片文件
def map_func(img_name, cap):
    # 使用np.load加载npy文件到内存
    img_tensor = np.load(img_name.decode('utf-8')+'.npy')
    # 返回对应的图片张量和对应的描述
    return img_tensor, cap


# 使用dataset的map方法并行调用map_func函数, 将数据集加载到内存中
dataset = dataset.map(lambda item1, item2: tf.numpy_function(
          map_func, [item1, item2], [tf.float32, tf.int32]),
          num_parallel_calls=tf.data.experimental.AUTOTUNE)

# 将数据集成批次的进行打乱
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

# 根据当前硬件的资源情况,会在模型训练同时预取数据到内存中, 加快训练速度
dataset = dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
  • 输出效果
# 预取数据集对象
<PrefetchDataset shapes: (<unknown>, <unknown>), types: (tf.float32, tf.int32)>

第六步: 构建微调模型并选取优化方法和损失函数

  • 构建注意力机制的类:
    • 注意力机制的计算规则遵循以下公式:

  • 构建注意力机制类的伪代码:
# 这里使用Bahdanau 注意力机制

1, score = FC(tanh(FC(EO) + FC(H)))
2, attention weights = softmax(score, axis = 1).
# 解释: Softmax 默认被应用于最后一个轴,但是这里我们想将它应用于第一个轴, 
# 因为分数 (score) 的形状是 (批大小,最大长度,隐层大小),最大长度 (max_length) 是输入的长度。
# 因为我们想为每个输入长度分配一个权重,所以softmax应该用在这个轴上。
3, context vector = sum(attention weights * EO, axis = 1) 
# 解释: 选择第一个轴的原因同上.
4, embedding output = 解码器输入 X 通过一个嵌入层
5, merged vector = concat(embedding output, context vector)

符号代表:
FC: 全连接层
EO: 编码器输出
H: 隐藏层状态
X: 解码器输入
  • 构建注意力机制类:
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, hidden):
        """
        description: 具体计算函数
        :param features: 编码器的输出
        :param hidden: 解码器的隐层输出
        return: 通过注意力机制处理后的结果context_vector和注意力权重attention_weights
        """
        # 为hidden扩展一个维度(batch_size, hidden_size) --> (batch_size, 1, hidden_size)
        hidden_with_time_axis = tf.expand_dims(hidden, 1)

        # 根据公式计算注意力得分, 输出score的形状为: (batch_size, 64, hidden_size)
        score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))

        # 根据公式计算注意力权重, 输出attention_weights形状为: (batch_size, 64, 1)
        attention_weights = tf.nn.softmax(self.V(score), axis=1)

        # 最后根据公式获得注意力机制处理后的结果context_vector
        # context_vector的形状为: (batch_size, hidden_size)
        context_vector = attention_weights * features
        context_vector = tf.reduce_sum(context_vector, axis=1)
        return context_vector, attention_weights
  • 构建CNN编码器:
    • 称作CNN编码器主要是因为之前使用InceptionV3进行图片处理, 编码器内部只有一个全连接层构成.
class CNN_Encoder(tf.keras.Model):
    def __init__(self, embedding_dim):
        super(CNN_Encoder, self).__init__()
        # 实例化一个全连接层
        self.fc = tf.keras.layers.Dense(embedding_dim)

    def call(self, x):
        # 使用全连接层
        x = self.fc(x)
        # 激活函数使用relu函数
        x = tf.nn.relu(x)
        return x
  • 调用
encoder = CNN_Encoder(embedding_dim)
print("encoder:", encoder)
  • 输出效果
encoder: <__main__.CNN_Encoder object at 0x13efb7da0>
  • 构建RNN解码器:
    • 这里RNN是指GRU, 同时在解码器中使用注意力机制.
class RNN_Decoder(tf.keras.Model):
  def __init__(self, embedding_dim, units, vocab_size):
      super(RNN_Decoder, self).__init__()
      # 传入联合嵌入特征的维度
      self.units = units
      # 实例化一个embedding层
      self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
      # 实例化一个gru层
      # return_sequences=True代表返回GRU序列模型的每个时间步的输出(每个输出做连接操作)
      # return_state=True代表除了返回输出外,还需要返回最后一个隐层状态
      # recurrent_initializer='glorot_uniform'即循环状态矩阵的初始化方式为均匀分布
      self.gru = tf.keras.layers.GRU(self.units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_initializer='glorot_uniform')
      # 实例化两个全连接层
      self.fc1 = tf.keras.layers.Dense(self.units)
      self.fc2 = tf.keras.layers.Dense(vocab_size)
      # 实例化注意力机制
      self.attention = BahdanauAttention(self.units)

  def call(self, x, features, hidden):
      # 首先使用注意力计算规则获得features和hidden的注意力结果
      context_vector, attention_weights = self.attention(features, hidden)

      # 输入通过embedding 层, 得到的输出形状: (batch_size, 1, embedding_dim)
      x = self.embedding(x)

      # 连接x和注意力结果, 获得新的输出x,形状为: (batch_size, 1, embedding_dim + hidden_size)
      x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

      # 将x输入到gru层
      output, state = self.gru(x)

      # 将x输入到全连接层, 输出形状: (batch_size, max_length, hidden_size)
      x = self.fc1(output)

      # 改变x形状以便输入到第二个全连接层, 输出形状为: (batch_size * max_length, hidden_size)
      x = tf.reshape(x, (-1, x.shape[2]))

      # 将x输入到第二个全连接层, 输出形状为: (batch_size * max_length, vocab)
      x = self.fc2(x)
      # 返回解码结果, gru隐层状态, 和注意力权重
      return x, state, attention_weights

  def reset_state(self, batch_size):
      # 初始化gru隐层状态的权重张量为全0张量
      return tf.zeros((batch_size, self.units))
  • 调用:
decoder = RNN_Decoder(embedding_dim, units, vocab_size)
print("decoder:", decoder)
  • 输出效果
decoder: <__main__.RNN_Decoder object at 0x150e5de10>
  • 选取优化方法和损失函数:
# 选取Adam优化方法
optimizer = tf.keras.optimizers.Adam()

# 损失基本计算方法为稀疏类别交叉熵损失
# from_logits=True代表是否将预测结果预期为非 0/1 的值进行保留
# 理论来讲二分类最终的结果应该只有0/1,函数将自动将其变为0/1,from_logits=True后,值不会被改变
# reduction='none',接下来我们将自定义损失函数,reduction必须设置为None,
# 我们可以将它看作是自定义损失函数的识别属性
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True, reduction='none')

# 因为每次生成的结果都是局部结果,要和真实结果进行比较需要对真实结果进行遮掩
# 等效于对损失计算结果进行掩码
def loss_function(real, pred):
    """自定义损失函数,参数为预测结果pred和真实结果real"""
    # 使用tf.math.equal方法对real和0进行对比
    # 对结果再进行逻辑非操作生成掩码张量mask
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    # 使用基本计算方法计算损失
    loss_ = loss_object(real, pred)
    # 将mask进行类型转换,使其能够进行后续操作
    mask = tf.cast(mask, dtype=loss_.dtype)
    # 将loss_与mask相乘即对loss_进行掩码
    loss_ *= mask
    # 计算loss_张量所有元素的均值
    return tf.reduce_mean(loss_)

第七步: 构建训练函数并进行训练

  • 构建训练函数
# 因为之后要绘制损失曲线, 定义一个用于存放每轮平均损失的列表
loss_plot = []

@tf.function # 该装饰器使该函数自动编译张量图, 使其可以直接执行 
def train_step(img_tensor, target):
    # 设定初始损失为0
    loss = 0

    # 初始化解码器的隐含状态张量
    hidden = decoder.reset_state(batch_size=target.shape[0])

    # 定义解码器的第一个文本描述输入(即起始符<start>对应的张量)    
    dec_input = tf.expand_dims([tokenizer.word_index['<start>']] * target.shape[0], 1)

    # 开启一个用于梯度记录的上下文管理器
    with tf.GradientTape() as tape:
        # 使用编码器处理输入的图片张量
        features = encoder(img_tensor)
        # 开始使用解码器循环解码, 解码长度为target.shape[1]即文本描述张量的最大长度
        for i in range(1, target.shape[1]):
            # 使用解码器获得第一个预测值和隐含张量
            predictions, hidden, _ = decoder(dec_input, features, hidden)
            # 计算该解码过程的损失
            loss += loss_function(target[:, i], predictions)
            # 接下来这里使用了teacher_forcing来定义下一次解码的输入
            # 关于teacher_forcing请查看下方定义和作用
            dec_input = tf.expand_dims(target[:, i], 1)

    # 全部循环解码完成后, 计算句子粒度的平均损失
    average_loss = (loss / int(target.shape[1]))
    # 获得整个模型训练的参数变量
    trainable_variables = encoder.trainable_variables + decoder.trainable_variables
    # 使用梯度管理器对象对参数变量求解梯度
    gradients = tape.gradient(loss, trainable_variables)
    # 根据梯度更新参数
    optimizer.apply_gradients(zip(gradients, trainable_variables))
    # 返回句子粒度的平均损失
    return average_loss
  • 什么是teacher_forcing?

    • 它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing.
  • teacher_forcing的作用:

    • 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大.
    • teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳.
  • 进行训练并打印日志:
# 设定训练轮数
EPOCHS = 20

# 循环轮数训练
for epoch in range(0, EPOCHS):
    # 获得每轮训练的开始时间
    start = time.time()
    # 初始化轮数总损失为0
    total_loss = 0
    # 循环数据集中的每个批次进行训练
    for (batch, (img_tensor, target)) in enumerate(dataset):
        # 调用train_step函数获得批次总损失和批次平均损失
        t_loss = train_step(img_tensor, target)
        # 将批次平均损失相加获得轮数总损失
        total_loss += t_loss

    # 绘制轮数平均损失
    loss_plot.append(total_loss / num_steps)
    # 打印轮数, 对应的平均损失
    print ('Epoch {} Loss {:.6f}'.format(epoch + 1,
                                         total_loss/num_steps))
    # 打印每轮的耗时
    print ('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
  • 输出效果
Epoch 1 Loss 1.053660
Time taken for 1 epoch 102.81588959693909 sec
Epoch 2 Loss 0.803199
Time taken for 1 epoch 46.122520208358765 sec
Epoch 3 Loss 0.729249
Time taken for 1 epoch 45.95720458030701 sec
Epoch 4 Loss 0.682262
Time taken for 1 epoch 46.03855228424072 sec
Epoch 5 Loss 0.645122
Time taken for 1 epoch 46.359169721603394 sec
Epoch 6 Loss 0.616254
Time taken for 1 epoch 45.84763479232788 sec
Epoch 7 Loss 0.582275
Time taken for 1 epoch 46.07718873023987 sec
Epoch 8 Loss 0.550876
Time taken for 1 epoch 46.32008242607117 sec
Epoch 9 Loss 0.520402
Time taken for 1 epoch 46.090750217437744 sec
Epoch 10 Loss 0.489396
Time taken for 1 epoch 46.069819688797 sec
Epoch 11 Loss 0.460302
Time taken for 1 epoch 46.13562488555908 sec
Epoch 12 Loss 0.431713
Time taken for 1 epoch 45.62839698791504 sec
Epoch 13 Loss 0.402241
Time taken for 1 epoch 45.647090673446655 sec
Epoch 14 Loss 0.377377
Time taken for 1 epoch 45.79609179496765 sec
Epoch 15 Loss 0.350675
Time taken for 1 epoch 45.3898491859436 sec
Epoch 16 Loss 0.324569
Time taken for 1 epoch 45.74031972885132 sec
Epoch 17 Loss 0.305316
Time taken for 1 epoch 44.66712689399719 sec
Epoch 18 Loss 0.283276
Time taken for 1 epoch 45.17093324661255 sec
Epoch 19 Loss 0.263147
Time taken for 1 epoch 45.49183177947998 sec
Epoch 20 Loss 0.246605
Time taken for 1 epoch 44.986790895462036 sec
  • 绘制损失曲线:
# 绘制损失曲线
plt.plot(loss_plot)

# 定义x轴,y轴,和图标名称
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Plot')
plt.show()
  • 输出效果

第八步: 构建评估函数并进行评估

  • 构建评估函数:
def evaluate(image):
    """评估函数, 以一张图片为输入"""
    # 初始化用于制图的注意力张量, 为全0张量
    attention_plot = np.zeros((max_length, attention_features_shape))
    # 初始化隐层张量
    hidden = decoder.reset_state(batch_size=1)
    # 使用load_image进行图片初始处理, 并扩展一个维度
    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)
    # 初始化解码器的输入张量
    dec_input = tf.expand_dims([tokenizer.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()
        # 从解码器得到的预测概率分布predictions中s随机按概率大小选择索引作为predicted_id
        predicted_id = tf.random.categorical(predictions, 1)[0][0].numpy()
        # 根据数值映射器和predicted_id获得对应单词(文本)并装入结果列表中
        result.append(tokenizer.index_word[predicted_id])
        # 判断预测字符是否的终止符<end>
        if tokenizer.index_word[predicted_id] == '<end>':
            # 返回结果列表和用于制图的注意力张量
            return result, attention_plot
        # 如果不是终止符, 则将本次的结果扩展维度作为下次解码器的输出
        dec_input = tf.expand_dims([predicted_id], 0)

    # 根据预测结果的真实长度对attention_plot进行切片, 去除多余的为0的部分
    attention_plot = attention_plot[:len(result), :]
    # 返回结果列表和切片后的注意力张量
    return result, attention_plot

def plot_attention(image, result, attention_plot):
    """注意力可视化函数"""
    # 获得numpy格式的图片表示 
    temp_image = np.array(Image.open(image))

    # 创建一个10x10的画板
    fig = plt.figure(figsize=(10, 10))
    # 获得图片描述文本结果长度
    len_result = len(result)
    # 循环结果列表长度
    for l in range(len_result):
        # 将每个结果对应的注意力张量变成8x8的张量
        temp_att = np.resize(attention_plot[l], (8, 8))
        # 创建大小为结果列表长度一半的子图画布
        ax = fig.add_subplot(len_result//2, len_result//2, l+1)
        # 设置子图画布的title
        ax.set_title(result[l])
        # 在子图画布上显示原图片
        img = ax.imshow(temp_image)
        # 在子图画布上显示注意力的灰度块 
        ax.imshow(temp_att, cmap='gray', alpha=0.6, extent=img.get_extent())

    # 调整子图位置, 填充整个画布
    plt.tight_layout()
    # 图像显示
    plt.show()
  • 调用:
# 在验证集上进行调用
# 随机在[0, len(img_name_val)]区间产生一个随机数
rid = np.random.randint(0, len(img_name_val))
# 根据随机数获得对应的图片
image = img_name_val[rid]
# 获得图片对应描述文本
real_caption = ' '.join([tokenizer.index_word[i] for i in cap_val[rid] if i not in [0]])
# 调用评估函数获得结果和制图的注意力张量
result, attention_plot = evaluate(image)
# 打印真实描述和预测描述进行对比
print ('Real Caption:', real_caption)
print ('Prediction Caption:', ' '.join(result))
  • 输出效果
# 可以多次运行获得结果(代码将随机选择不同的图片生成描述)
Real Caption: <start> a snowboarder sits in the snow at the base of a tall mountain <end>
Prediction Caption: a person is with a small white hat sitting at the air while skis in the snow with skis is skiing <unk> slope <end>
  • 使用一张图片进行模型预测:
# 任意选择一张图片
image_url = 'https://tensorflow.org/images/surf.jpg'
# 取图片的扩展名.jpg
image_extension = image_url[-4:]
# 将图片下载到本地
image_path = tf.keras.utils.get_file('image'+image_extension,
                                     origin=image_url)
# 调用评估函数获得结果和制图的注意力张量
result, attention_plot = evaluate(image_path)
# 打印预测结果
print ('Prediction Caption:', ' '.join(result))
# 绘制注意力子图
plot_attention(image_path, result, attention_plot)
# 查看原图片
Image.open(image_path)
  • 输出效果
Downloading data from https://tensorflow.org/images/surf.jpg
65536/64400 [==============================] - 0s 3us/step
Prediction Caption: a person is sitting down to surfboard in no to their surf <end>

  • 注意力分析:
    • 灰度子图中越明亮的部分说明在生成描述单词时被利用的信息越多(越被注意), 如生成单词”person”时, 明亮的方块基本在人脸附近, 而生成”surfboard”时, 明亮的方块集中在冲浪板附近.注意力机制与人类在识别事物方面具有高度一致性.

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值