图片的描述生成任务

学习目标

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

图片描述生成 - 图2


任务说明

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

数据集说明

  • 数据集名称: 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


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

  • 限制训练集大小的目标:
    • 为了加快训练速度,将使用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 = ‘ ‘ + annot[‘caption’] + ‘
# 再取对应的imageid
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

图片效果:
图片描述生成 - 图3


  • 初始化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模型对象


  • 使用模型对特征进行提取


# 将之前选取的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]))

  1. # 防止硬件内存无法满足要求,将batch图片特征存储在对应的路径下<br /> for bf, p in zip(batch_features, path):<br /> # 得到特征的路径<br /> path_of_feature = p.numpy().decode("utf-8")<br /> # 使用numpy进行存储<br /> 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方法实例化数值映射器, 其中超出部分的词汇使用表示
tokenizer = tf.keras.preprocessing.text.Tokenizer(numwords=top_k,
oov_token=”“,
filters=’!”#$%&()*+.,-/:;=?@[]^
`{|}~ ‘)

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

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

最后作用于描述文本得到对应的数值映射结果
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)


  • 输出效果:


# 预取数据集对象
, ), types: (tf.float32, tf.int32)>


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

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

图片描述生成 - 图4图片描述生成 - 图5

  • 构建注意力机制类的伪代码:


# 这里使用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)

  1. def call(self, features, hidden):<br /> """<br /> description: 具体计算函数<br /> :param features: 编码器的输出<br /> :param hidden: 解码器的隐层输出<br /> return: 通过注意力机制处理后的结果context_vector和注意力权重attention_weights<br /> """<br /> # 为hidden扩展一个维度(batch_size, hidden_size) --> (batch_size, 1, hidden_size)<br /> hidden_with_time_axis = tf.expand_dims(hidden, 1)
  2. # 根据公式计算注意力得分, 输出score的形状为: (batch_size, 64, hidden_size)<br /> score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))
  3. # 根据公式计算注意力权重, 输出attention_weights形状为: (batch_size, 64, 1)<br /> attention_weights = tf.nn.softmax(self.V(score), axis=1)
  4. # 最后根据公式获得注意力机制处理后的结果context_vector<br /> # context_vector的形状为: (batch_size, hidden_size)<br /> context_vector = attention_weights * features<br /> context_vector = tf.reduce_sum(context_vector, axis=1)<br /> return context_vector, attention_weights

  • 构建CNN编码器:
    • 称作CNN编码器主要是因为之前使用InceptionV3进行图片处理, 编码器内部只有一个全连接层构成.


class CNNEncoder(tf.keras.Model):
def init(self, embeddingdim):
super(CNN_Encoder, self).__init
()
# 实例化一个全连接层
self.fc = tf.keras.layers.Dense(embedding_dim)

  1. def call(self, x):<br /> # 使用全连接层<br /> x = self.fc(x)<br /> # 激活函数使用relu函数<br /> x = tf.nn.relu(x)<br /> return x

  • 调用:


encoder = CNN_Encoder(embedding_dim)
print(“encoder:”, encoder)


  • 输出效果:


encoder: <__main__.CNN_Encoder object at 0x13efb7da0>


  • 构建RNN解码器:
    • 这里RNN是指GRU, 同时在解码器中使用注意力机制.


class RNNDecoder(tf.keras.Model):
def init(self, embeddingdim, 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)

  1. # 输入通过embedding 层, 得到的输出形状: (batch_size, 1, embedding_dim)<br /> x = self.embedding(x)
  2. # 连接x和注意力结果, 获得新的输出x,形状为: (batch_size, 1, embedding_dim + hidden_size)<br /> x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
  3. # 将x输入到gru层<br /> output, state = self.gru(x)
  4. # 将x输入到全连接层, 输出形状: (batch_size, max_length, hidden_size)<br /> x = self.fc1(output)
  5. # 改变x形状以便输入到第二个全连接层, 输出形状为: (batch_size * max_length, hidden_size)<br /> x = tf.reshape(x, (-1, x.shape[2]))
  6. # 将x输入到第二个全连接层, 输出形状为: (batch_size * max_length, vocab)<br /> x = self.fc2(x)<br /> # 返回解码结果, gru隐层状态, 和注意力权重<br /> 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 lossfunction(real, pred):
“””自定义损失函数,参数为预测结果pred和真实结果real”””
# 使用tf.math.equal方法对real和0进行对比
# 对结果再进行逻辑非操作生成掩码张量mask
mask = tf.math.logical_not(tf.math.equal(real, 0))
# 使用基本计算方法计算损失
loss
= lossobject(real, pred)
# 将mask进行类型转换,使其能够进行后续操作
mask = tf.cast(mask, dtype=loss
.dtype)
# 将loss与mask相乘即对loss进行掩码
loss *= mask
# 计算loss
张量所有元素的均值
return tf.reducemean(loss)


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

  • 构建训练函数:


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

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

  1. # 初始化解码器的隐含状态张量<br /> hidden = decoder.reset_state(batch_size=target.shape[0])
  2. # 定义解码器的第一个文本描述输入(即起始符<start>对应的张量) <br /> dec_input = tf.expand_dims([tokenizer.word_index['<start>']] * target.shape[0], 1)
  3. # 开启一个用于梯度记录的上下文管理器<br /> with tf.GradientTape() as tape:<br /> # 使用编码器处理输入的图片张量<br /> features = encoder(img_tensor)<br /> # 开始使用解码器循环解码, 解码长度为target.shape[1]即文本描述张量的最大长度<br /> for i in range(1, target.shape[1]):<br /> # 使用解码器获得第一个预测值和隐含张量<br /> predictions, hidden, _ = decoder(dec_input, features, hidden)<br /> # 计算该解码过程的损失<br /> loss += loss_function(target[:, i], predictions)<br /> # 接下来这里使用了teacher_forcing来定义下一次解码的输入<br /> # 关于teacher_forcing请查看下方定义和作用<br /> dec_input = tf.expand_dims(target[:, i], 1)
  4. # 全部循环解码完成后, 计算句子粒度的平均损失<br /> average_loss = (loss / int(target.shape[1]))<br /> # 获得整个模型训练的参数变量<br /> trainable_variables = encoder.trainable_variables + decoder.trainable_variables<br /> # 使用梯度管理器对象对参数变量求解梯度<br /> gradients = tape.gradient(loss, trainable_variables)<br /> # 根据梯度更新参数<br /> optimizer.apply_gradients(zip(gradients, trainable_variables))<br /> # 返回句子粒度的平均损失<br /> 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

  1. # 绘制轮数平均损失<br /> loss_plot.append(total_loss / num_steps)<br /> # 打印轮数, 对应的平均损失<br /> print ('Epoch {} Loss {:.6f}'.format(epoch + 1,<br /> total_loss/num_steps))<br /> # 打印每轮的耗时<br /> 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()

  • 输出效果:

图片描述生成 - 图6


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

  • 构建评估函数:


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[‘‘]], 0)

  1. # 初始化图片描述的文本结果列表<br /> result = []<br /> # 根据解码器结果生成最终的文本结果 <br /> for i in range(max_length):<br /> # 使用解码器获得每次的输出张量 <br /> predictions, hidden, attention_weights = decoder(dec_input, features, hidden)<br /> # 根据每次获得的注意力权重填充用于制图的注意力张量<br /> attention_plot[i] = tf.reshape(attention_weights, (-1, )).numpy()<br /> # 从解码器得到的预测概率分布predictions中s随机按概率大小选择索引作为predicted_id<br /> predicted_id = tf.random.categorical(predictions, 1)[0][0].numpy()<br /> # 根据数值映射器和predicted_id获得对应单词(文本)并装入结果列表中<br /> result.append(tokenizer.index_word[predicted_id])<br /> # 判断预测字符是否的终止符<end><br /> if tokenizer.index_word[predicted_id] == '<end>':<br /> # 返回结果列表和用于制图的注意力张量<br /> return result, attention_plot<br /> # 如果不是终止符, 则将本次的结果扩展维度作为下次解码器的输出<br /> dec_input = tf.expand_dims([predicted_id], 0)
  2. # 根据预测结果的真实长度对attention_plot进行切片, 去除多余的为0的部分<br /> attention_plot = attention_plot[:len(result), :]<br /> # 返回结果列表和切片后的注意力张量<br /> return result, attention_plot

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

  1. # 创建一个10x10的画板<br /> fig = plt.figure(figsize=(10, 10))<br /> # 获得图片描述文本结果长度<br /> len_result = len(result)<br /> # 循环结果列表长度<br /> for l in range(len_result):<br /> # 将每个结果对应的注意力张量变成8x8的张量<br /> temp_att = np.resize(attention_plot[l], (8, 8))<br /> # 创建大小为结果列表长度一半的子图画布<br /> ax = fig.add_subplot(len_result//2, len_result//2, l+1)<br /> # 设置子图画布的title<br /> ax.set_title(result[l])<br /> # 在子图画布上显示原图片<br /> img = ax.imshow(temp_image)<br /> # 在子图画布上显示注意力的灰度块 <br /> ax.imshow(temp_att, cmap='gray', alpha=0.6, extent=img.get_extent())
  2. # 调整子图位置, 填充整个画布<br /> plt.tight_layout()<br /> # 图像显示<br /> 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: a snowboarder sits in the snow at the base of a tall mountain
Prediction Caption: a person is with a small white hat sitting at the air while skis in the snow with skis is skiing slope


  • 使用一张图片进行模型预测:


# 任意选择一张图片
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
图片描述生成 - 图7


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

图片描述生成 - 图8