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”时, 明亮的方块集中在冲浪板附近.注意力机制与人类在识别事物方面具有高度一致性.