Python 迁移学习实用指南:6~11(3)https://developer.aliyun.com/article/1426855
此处,α
和β
是用于控制内容和风格成分对整体损失的影响的权重。 此描述可以进一步简化,并表示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qlqlLObC-1681567330251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/1f77aa7c-844e-4579-b42b-f5febbdfa09b.png)]
在这里,我们可以根据前面的公式定义以下组件:
dist
是规范函数; 例如,L2 规范距离style(...)
是用于为参考风格和生成的图像计算风格表示的函数content(...)
是一个函数,可为原始内容和生成的图像计算内容的表示形式I[c]
,I[s]
和I[g]
,并分别生成图像
因此,最小化此损失会导致风格(I[g]
)接近风格(I[s]
),以及内容(I[g]
)接近内容(I[c]
)。 这有助于我们达成有效的风格转换所需的规定。 我们将尝试最小化的损失函数包括三个部分: 即将讨论的内容损失,风格损失和总变化损失。 关键思想或目标是保留原始目标图像的内容,同时在目标图像上叠加或采用参考图像的风格。 此外,在神经风格转换的背景下,您应该记住以下几点:
- 风格可以定义为参考图像中存在的调色板,特定图案和纹理
- 内容可以定义为原始目标图像的整体结构和更高级别的组件
到目前为止,我们知道深度学习对于计算机视觉的真正威力在于利用诸如深层卷积神经网络(CNN)模型之类的模型,这些模型可用于在构建这些损失函数时提取正确的图像表示。 在本章中,我们将使用迁移学习的原理来构建用于神经风格迁移的系统,以提取最佳特征。 在前面的章节中,我们已经讨论了与计算机视觉相关的任务的预训练模型。 在本章中,我们将再次使用流行的 VGG-16 模型作为特征提取器。 执行神经风格转换的主要步骤如下所示:
- 利用 VGG-16 帮助计算风格,内容和生成图像的层激活
- 使用这些激活来定义前面提到的特定损失函数
- 最后,使用梯度下降来最大程度地减少总损耗
如果您想更深入地研究神经风格转换背后的核心原理和理论概念,建议您阅读以下文章:
A Neural Algorithm of Artistic Style, by Leon A. Gatys, Alexander S. Ecker, and Matthias Bethge (https://arxiv.org/abs/1508.06576)
Perceptual Losses for Real-Time Style Transfer and Super-Resolution, by Justin Johnson, Alexandre Alahi, and Li Fei-Fei (https://arxiv.org/abs/1603.08155)
图像预处理方法
在这种情况下,实现此类网络的第一步也是最重要的一步是对数据或图像进行预处理。 以下代码段显示了一些用于对图像进行大小和通道调整的快速工具:
import numpy as np from keras.applications import vgg16 from keras.preprocessing.image import load_img, img_to_array def preprocess_image(image_path, height=None, width=None): height = 400 if not height else height width = width if width else int(width * height / height) img = load_img(image_path, target_size=(height, width)) img = img_to_array(img) img = np.expand_dims(img, axis=0) img = vgg16.preprocess_input(img) return img def deprocess_image(x): # Remove zero-center by mean pixel x[:, :, 0] += 103.939 x[:, :, 1] += 116.779 x[:, :, 2] += 123.68 # 'BGR'->'RGB' x = x[:, :, ::-1] x = np.clip(x, 0, 255).astype('uint8') return x
当我们要编写自定义损失函数和操作例程时,我们将需要定义某些占位符。 请记住,keras
是一个利用张量操作后端(例如tensorflow
,theano
和CNTK
)执行繁重工作的高级库。 因此,这些占位符提供了高级抽象来与基础张量对象一起使用。 以下代码段为风格,内容和生成的图像以及神经网络的输入张量准备了占位符:
from keras import backend as K # This is the path to the image you want to transform. TARGET_IMG = 'lotr.jpg' # This is the path to the style image. REFERENCE_STYLE_IMG = 'pattern1.jpg' width, height = load_img(TARGET_IMG).size img_height = 480 img_width = int(width * img_height / height) target_image = K.constant(preprocess_image(TARGET_IMG, height=img_height, width=img_width)) style_image = K.constant(preprocess_image(REFERENCE_STYLE_IMG, height=img_height, width=img_width)) # Placeholder for our generated image generated_image = K.placeholder((1, img_height, img_width, 3)) # Combine the 3 images into a single batch input_tensor = K.concatenate([target_image, style_image, generated_image], axis=0)
我们将像前几章一样加载预训练的 VGG-16 模型。 也就是说,没有顶部的全连接层。 唯一的区别是我们将为模型输入提供输入张量的大小尺寸。 以下代码段有助于我们构建预训练模型:
model = vgg16.VGG16(input_tensor=input_tensor, weights='imagenet', include_top=False)
构建损失函数
如背景小节所述,神经风格迁移的问题围绕内容和风格的损失函数。 在本小节中,我们将讨论和定义所需的损失函数。
内容损失
在任何基于 CNN 的模型中,来自顶层的激活都包含更多的全局和抽象信息(例如,诸如人脸之类的高级结构),而底层将包含局部信息(例如,诸如眼睛,鼻子, 边缘和角落)。 我们希望利用 CNN 的顶层来捕获图像内容的正确表示。 因此,对于内容损失,考虑到我们将使用预训练的 VGG-16 模型,我们可以将损失函数定义为通过计算得出的顶层激活(给出特征表示)之间的 L2 范数(缩放和平方的欧几里得距离)。 目标图像,以及在生成的图像上计算的同一层的激活。 假设我们通常从 CNN 的顶层获得与图像内容相关的特征表示,则预期生成的图像看起来与基本目标图像相似。 以下代码段显示了计算内容损失的函数:
def content_loss(base, combination): return K.sum(K.square(combination - base))
风格损失
关于神经风格迁移的原始论文,《一种由神经科学风格的神经算法》,由 Gatys 等人撰写。利用 CNN 中的多个卷积层(而不是一个)来从参考风格图像中提取有意义的风格和表示,捕获与外观或风格有关的信息。 不论图像内容如何,在所有空间尺度上都可以工作。风格表示可计算 CNN 不同层中不同特征之间的相关性。
忠于原始论文,我们将利用 Gram 矩阵并在由卷积层生成的特征表示上进行计算。 Gram 矩阵计算在任何给定的卷积层中生成的特征图之间的内积。 内积项与相应特征集的协方差成正比,因此可以捕获趋于一起激活的层的特征之间的相关性。 这些特征相关性有助于捕获特定空间比例的图案的相关汇总统计信息,这些统计信息与风格,纹理和外观相对应,而不与图像中存在的组件和对象相对应。
因此,风格损失定义为参考风格的 Gram 矩阵与生成的图像之间的差异的按比例缩放的 Frobenius 范数(矩阵上的欧几里得范数)。 最小化此损失有助于确保参考风格图像中不同空间比例下找到的纹理在生成的图像中相似。 因此,以下代码段基于 Gram 矩阵计算定义了风格损失函数:
def style_loss(style, combination, height, width): def build_gram_matrix(x): features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1))) gram_matrix = K.dot(features, K.transpose(features)) return gram_matrix S = build_gram_matrix(style) C = build_gram_matrix(combination) channels = 3 size = height * width return K.sum(K.square(S - C))/(4\. * (channels ** 2) * (size ** 2))
总变化损失
据观察,仅减少风格和内容损失的优化会导致高度像素化和嘈杂的输出。 为了解决这个问题,引入了总变化损失。 总变化损失与正则化损失相似。 引入此方法是为了确保生成的图像中的空间连续性和平滑性,以避免产生嘈杂的像素化结果。 在函数中的定义如下:
def total_variation_loss(x): a = K.square( x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :]) b = K.square( x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :]) return K.sum(K.pow(a + b, 1.25))
总损失函数
在定义了用于神经风格传递的整体损失函数的组成部分之后,下一步就是将这些构造块缝合在一起。 由于内容和风格信息是由 CNN 在网络中的不同深度捕获的,因此我们需要针对每种损失类型在适当的层上应用和计算损失。 我们将对卷积层进行 1 到 5 层的风格损失,并为每一层设置适当的权重。
这是构建整体损失函数的代码片段:
# weights for the weighted average loss function content_weight = 0.05 total_variation_weight = 1e-4 content_layer = 'block4_conv2' style_layers = ['block1_conv2', 'block2_conv2', 'block3_conv3','block4_conv3', 'block5_conv3'] style_weights = [0.1, 0.15, 0.2, 0.25, 0.3] # initialize total loss loss = K.variable(0.) # add content loss layer_features = layers[content_layer] target_image_features = layer_features[0, :, :, :] combination_features = layer_features[2, :, :, :] loss += content_weight * content_loss(target_image_features, combination_features) # add style loss for layer_name, sw in zip(style_layers, style_weights): layer_features = layers[layer_name] style_reference_features = layer_features[1, :, :, :] combination_features = layer_features[2, :, :, :] sl = style_loss(style_reference_features, combination_features, height=img_height, width=img_width) loss += (sl*sw) # add total variation loss loss += total_variation_weight * total_variation_loss(generated_image)
构造自定义优化器
目的是在优化算法的帮助下迭代地使总损失最小化。 Gatys 等人的论文中,使用 L-BFGS 算法进行了优化,该算法是基于准牛顿法的一种优化算法,通常用于解决非线性优化问题和参数估计。 该方法通常比标准梯度下降收敛更快。
SciPy 在scipy.optimize.fmin_l_bfgs_b()
中提供了一个实现。 但是,局限性包括该函数仅适用于平面一维向量,这与我们正在处理的三维图像矩阵不同,并且损失函数和梯度的值需要作为两个单独的函数传递。 我们基于模式构建一个Evaluator
类,然后由keras
创建者 FrançoisChollet 创建,以一次计算损失和梯度值,而不是独立和单独的计算。 这将在首次调用时返回损耗值,并将缓存下一次调用的梯度。 因此,这将比独立计算两者更为有效。 以下代码段定义了Evaluator
类:
class Evaluator(object): def __init__(self, height=None, width=None): self.loss_value = None self.grads_values = None self.height = height self.width = width def loss(self, x): assert self.loss_value is None x = x.reshape((1, self.height, self.width, 3)) outs = fetch_loss_and_grads([x]) loss_value = outs[0] grad_values = outs[1].flatten().astype('float64') self.loss_value = loss_value self.grad_values = grad_values return self.loss_value def grads(self, x): assert self.loss_value is not None grad_values = np.copy(self.grad_values) self.loss_value = None self.grad_values = None return grad_values evaluator = Evaluator(height=img_height, width=img_width)
风格迁移实战
难题的最后一步是使用所有构建块并在操作中执行风格转换! 可以从数据目录中获取艺术/风格和内容图像,以供参考。 以下代码片段概述了如何评估损耗和梯度。 我们还按规律的间隔/迭代(5
,10
等)写回输出,以了解神经风格迁移的过程如何在经过一定的迭代次数后考虑的图像转换图像,如以下代码段所示:
from scipy.optimize import fmin_l_bfgs_b from scipy.misc import imsave from imageio import imwrite import time result_prefix = 'st_res_'+TARGET_IMG.split('.')[0] iterations = 20 # Run scipy-based optimization (L-BFGS) over the pixels of the # generated image # so as to minimize the neural style loss. # This is our initial state: the target image. # Note that `scipy.optimize.fmin_l_bfgs_b` can only process flat # vectors. x = preprocess_image(TARGET_IMG, height=img_height, width=img_width) x = x.flatten() for i in range(iterations): print('Start of iteration', (i+1)) start_time = time.time() x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20) print('Current loss value:', min_val) if (i+1) % 5 == 0 or i == 0: # Save current generated image only every 5 iterations img = x.copy().reshape((img_height, img_width, 3)) img = deprocess_image(img) fname = result_prefix + '_iter%d.png' %(i+1) imwrite(fname, img) print('Image saved as', fname) end_time = time.time() print('Iteration %d completed in %ds' % (i+1, end_time - start_time))
到现在为止,必须非常明显的是,神经风格转换是一项计算量巨大的任务。 对于所考虑的图像集,在具有 8GB RAM 的 Intel i5 CPU 上,每次迭代花费了 500-1,000 秒(尽管在 i7 或 Xeon 处理器上要快得多!)。 以下代码段显示了我们在 AWS 的 p2.x 实例上使用 GPU 所获得的加速,每次迭代仅需 25 秒! 以下代码片段还显示了一些迭代的输出。 我们打印每次迭代的损失和时间,并在每五次迭代后保存生成的图像:
Start of iteration 1 Current loss value: 10028529000.0 Image saved as st_res_lotr_iter1.png Iteration 1 completed in 28s Start of iteration 2 Current loss value: 5671338500.0 Iteration 2 completed in 24s Start of iteration 3 Current loss value: 4681865700.0 Iteration 3 completed in 25s Start of iteration 4 Current loss value: 4249350400.0 . . . Start of iteration 20 Current loss value: 3458219000.0 Image saved as st_res_lotr_iter20.png Iteration 20 completed in 25s
现在,您将学习神经风格迁移模型如何考虑内容图像的风格迁移。 请记住,我们在某些迭代之后为每对风格和内容图像执行了检查点输出。 我们利用matplotlib
和skimage
加载并了解我们系统执行的风格转换魔术!
我们将非常受欢迎的《指环王》电影中的以下图像用作我们的内容图像,并将基于花卉图案的精美艺术品用作我们的风格图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNcqvAgc-1681567330251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/27afcdb0-5ba3-4a05-8278-8ed5112261a8.png)]
在以下代码段中,我们将在各种迭代之后加载生成的风格化图像:
from skimage import io from glob import glob from matplotlib import pyplot as plt %matplotlib inline content_image = io.imread('lotr.jpg') style_image = io.imread('pattern1.jpg') iter1 = io.imread('st_res_lotr_iter1.png') iter5 = io.imread('st_res_lotr_iter5.png') iter10 = io.imread('st_res_lotr_iter10.png') iter15 = io.imread('st_res_lotr_iter15.png') iter20 = io.imread('st_res_lotr_iter20.png') fig = plt.figure(figsize = (15, 15)) ax1 = fig.add_subplot(6,3, 1) ax1.imshow(content_image) t1 = ax1.set_title('Original') gen_images = [iter1,iter5, iter10, iter15, iter20] for i, img in enumerate(gen_images): ax1 = fig.add_subplot(6,3,i+1) ax1.imshow(content_image) t1 = ax1.set_title('Iteration {}'.format(i+5)) plt.tight_layout() fig.subplots_adjust(top=0.95) t = fig.suptitle('LOTR Scene after Style Transfer')
以下是显示原始图像和每五次迭代后生成的风格图像的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdBYzRpV-1681567330252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/7bb09bf1-7394-43ba-b4a0-a17eee6abe1c.png)]
以下是高分辨率的最终风格图像。 您可以清楚地看到花卉图案的纹理和风格是如何在原始《指环王》电影图像中慢慢传播的,并赋予了其良好的复古外观:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyDVbpUo-1681567330252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/58ca16d2-f5bc-48dc-8e18-44aa17afaa25.png)]
让我们再举一个风格迁移示例。 下图包含我们的内容图像,即来自黑豹的著名虚构的城市瓦卡达。 风格图片是梵高非常受欢迎的画作《星空》! 我们将在风格传递系统中将它们用作输入图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fbcm2b1M-1681567330252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/006dc7f4-1cc1-458d-a29d-7229ed1b7c9d.png)]
以下是高分辨率的最终风格图像,显示在下面的图像中。 您可以清楚地看到风格绘画中的纹理,边缘,颜色和图案如何传播到城市内容图像中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwor5gYa-1681567330252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/6be810e1-6824-4697-a3f4-46705e71d681.png)]
天空和架构物采用了与您在绘画中可以观察到的非常相似的形式,但是内容图像的整体结构得以保留。 令人着迷,不是吗? 现在用您自己感兴趣的图像尝试一下!
总结
本章介绍了深度学习领域中一种非常新颖的技术,它利用了深度学习的力量来创造艺术! 确实,数据科学既是一门艺术,也是正确使用数据的科学,而创新则是推动这一发展的事物。 我们介绍了神经风格迁移的核心概念,如何使用有效的损失函数来表示和表达问题,以及如何利用迁移学习的力量和像 VGG-16 这样的预训练模型来提取正确的特征表示。
计算机视觉领域不断发展,深度学习与迁移学习相结合为创新和构建新颖的应用打开了大门。 本章中的示例应帮助您了解该领域的广泛新颖性,并使您能够走出去并尝试新技术,模型和方法来构建诸如神经风格转换的系统! 随之而来的是有关图像标题和着色的更有趣,更复杂的案例研究。 敬请关注!
十、自动图像字幕生成器
在前面的章节中,我们研究了一些案例研究,这些案例研究将迁移学习应用于计算机视觉以及自然语言处理(NLP)中的问题。 但是,这些都是它们各自特定领域中的问题。 在本章中,我们将专注于构建将这两个流行领域(计算机视觉和 NLP)结合在一起的智能系统。 更具体地说,我们将专注于构建与机器翻译相结合的对象识别系统,以构建自动图像字幕生成器。
图像字幕的想法并不是什么新鲜事物。 通常,存在于各种媒体资源(例如书籍,论文或社交媒体)中的任何图像通常都需要加上适当的文本说明,以获取更好的含义和上下文。 使这项任务变得艰巨的是,图像标题通常是由一个或多个句子组成的自由流动的自然语言。 因此,由于用于图像标题的文本数据的非结构化性质,这不是传统的图像分类问题。
可以通过结合使用计算机视觉领域专家的预训练模型(例如视觉几何组(VGG)和 Inception )以及序列模型(例如循环神经网络(RNN)或长短期记忆(LSTM)),以生成单词序列以形成图片说明的适当单词。 在本章中,我们将探索一种有趣的方法来构建自动图像字幕或场景识别系统。
我们将涵盖构建此系统的以下主要方面,该系统由深度学习和迁移学习提供支持:
- 了解图像字幕
- 制定目标
- 了解数据
- 自动图像字幕的方法
- 使用迁移学习的图像特征提取
- 为我们的字幕建立词汇表
- 构建图像标题数据集生成器
- 建立我们的图像字幕编解码器深度学习模型
- 训练我们的图像字幕深度学习模型
- 自动图像字幕实战
我们将涵盖计算机视觉和 NLP 的基本概念,以构建我们的自动图像标题生成器。 我们将深入研究适合的深度学习架构,并结合迁移学习,以在流行且易于使用的图像数据集之上实现该系统。 我们还将展示如何在新的照片和场景上构建和测试我们的自动图像标题生成器。 您可以在 GitHub 存储库中的Chapter 11
文件夹中快速阅读本章的代码。 可以根据需要参考本章。 我们还将在那里发布一些奖金示例。
了解图像字幕
到目前为止,您应该了解图像字幕的意义和含义。 该任务可以简单地定义为为任何图像编写和记录自由流动的自然文本描述。 通常用于描述图像中的各种场景或事件。 这也通常称为场景识别。 让我们看下面的例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rF1iU42y-1681567330252)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/08624886-c3d1-4202-a1a6-9121e0ab4c19.jpg)]
看着这个场景,合适的标题或描述是什么? 以下是对场景的所有有效描述:
- 越野摩托车手在山上
- 一个家伙在山上的空中的自行车上
- 一辆越野车车手正在一条肮脏的道路上快速移动
- 骑自行车的人在空中骑黑摩托车
您会看到所有这些标题都是有效的并且相似,但是使用不同的词来传达相同的含义。 这就是为什么自动生成图像标题并非易事的原因。
实际上,流行论文《展示和演讲:神经图像字幕生成器》(Vinyals 及其合作者,2015 年)描述了图像字幕,从中我们汲取了构建此系统的灵感:
自动描述图像的内容是连接计算机视觉和自然语言处理的人工智能的基本问题。
对于一个人来说,只需瞥一眼照片或图像几秒钟就足以生成基于自然语言的字幕。 但是,由于大多数计算机视觉问题都集中在识别和分类问题上,因此使人工智能(AI)执行此任务极具挑战性。 就复杂性而言,这是有关核心计算机视觉问题的一些主要任务:
- 图像分类和识别:这涉及经典的有监督学习问题,其中主要目标是基于几个预定义的类类别(通常称为类标签)将图像分配给特定类别 。 流行的 ImageNet 竞赛就是这样一项任务。
- 图像标注:稍微复杂一点的任务,我们尝试使用图像中各个实体的描述来标注图像。 通常,这涉及图像中特定部分或区域的类别,甚至是基于自然语言的文本描述。
- 图像标题或场景识别:我们尝试使用准确的基于自然语言的文本描述来描述图像的另一项复杂任务。 这是本章重点关注的领域。
图像字幕的任务不是什么新鲜事。 已有多种利用技术的现有方法,例如将图像中各个实体的文本描述缝合在一起以形成描述,甚至使用基于模板的文本生成技术。 但是,对于该任务,使用深度学习是一种更强大,更有效的方法。
制定目标
我们实际案例研究的主要目标是图像字幕或场景识别。 在一定程度上,这是一个监督学习问题,而不是传统的分类问题。 在这里,我们将处理一个称为Flickr8K
的图像数据集,其中包含图像或场景的样本以及描述它们的相应自然语言标题。 这个想法是建立一个可以从这些图像中学习并自动开始为图像添加字幕的系统。
如前所述,传统的图像分类系统通常将图像分类或分类为预定义的类。 在前面的章节中,我们已经构建了这样的系统。 但是,图像字幕系统的输出通常是形成自然语言文本描述的单词序列; 这比传统的监督分类系统更加困难。
我们仍将监督模型训练的性质,因为我们将必须基于训练图像数据及其相应的字幕说明来构建模型。 但是,建立模型的方法会略有不同。 我们将利用迁移学习和深度学习中的概念照常构建此系统。 更具体地说,我们将结合使用深层卷积神经网络(DCNNs)和顺序模型。
了解数据
让我们看一下将用于构建模型的数据。 为简单起见,我们将使用Flickr8K
数据集。 该数据集包括从流行的图像共享网站 Flickr 获得的图像。 要下载数据集,可以通过填写以下伊利诺伊大学计算机科学系的表格来请求它,您应该在电子邮件中获取下载链接。
要查看与每个图像有关的详细信息,可以访问其网站,其中讨论了每个图像及其图像。 源,以及每个图像的五个基于文本的标题。 通常,任何样本图像都将具有类似于以下内容的标题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5zLeniL-1681567330253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/37049eb5-b79c-43bf-b8b1-fceb07a6697c.png)]
您可以清楚地看到图像及其相应的标题。 很明显,所有标题都试图描述相同的图像或场景,但是它可能专注于图像的特定和不同方面,这使自动化成为一项艰巨的任务。 我们还建议读者查看《将图像描述作为排名任务:数据,模型和评估指标》(Micah Hodosh 等人,IJCAI 2015)。
单击下载链接时,将获得两个文件:
Flickr8k_Dataset.zip
:所有原始图像和照片的 1 GB ZIP 存档Flickr8k_text.zip
:3 MB 的 ZIP 存档,其中包含照片的所有自然语言文本说明,这些文本说明为标题
Flickr_8k.devImages.txt
,lickr_8k.trainImages.txt
和Flickr_8k.testImages.txt
文件分别包含 6,000、1,000 和 1,000 个图像的文件名。 我们将合并dev
和train
图像,以构建包含 7,000 张图像的训练数据集,并使用包含 1,000 张图像的测试数据集进行评估。 每个图像都有五个不同但相似的标题,可在Flickr8k.token.txt
文件中找到。
自动图像字幕的方法
现在,我们将讨论构建自动图像字幕系统的方法。 正如我之前提到的,我们的方法将利用基于深度神经网络的方法以及将学习迁移到图像字幕的方法。 这得益于流行论文《Show and Tell:神经图像字幕生成器》(Oriol Vinyals 等人,2015)。 我们将在概念上概述我们的方法,然后将其转换为将用于构建自动图像字幕系统的实用方法。 让我们开始吧!
概念方法
成功的图像字幕系统需要一种将给定图像转换为单词序列的方法。 为了从图像中提取正确和相关的特征,我们可以利用 DCNN,再结合循环神经网络模型(例如 RNN 或 LSTM),我们可以在给定源图像的情况下,构建混合生成模型以开始生成单词序列作为标题。
因此,从概念上讲,这个想法是建立一个混合模型,该模型可以将源图像I
作为输入,并可以进行训练以使可能性最大, P(S|I)
,这样S
是单词序列的输出,这是我们的目标输出,可以由S = {S [1], S[2], ..., S[n]}
表示,这样每个单词S[w]
都来自给定的词典,这就是我们的词汇。 该标题S
应该能够对输入图像给出恰当的描述。
神经机器翻译是构建这样一个系统的绝佳灵感。 通常在语言模型中用于语言翻译,模型架构涉及使用 RNN 或 LSTM 构建的编码器-解码器架构。 通常,编码器涉及一个 LSTM 模型,该模型从源语言中读取输入语句并将其转换为密集的定长向量。 然后将其用作解码器 LSTM 模型的初始隐藏状态,最终以目标语言生成输出语句。
对于图像字幕,我们将利用类似的策略,其中处理输入的编码器将利用 DCNN 模型,因为我们的源数据是图像。 到目前为止,我们已经看到了基于 CNN 的模型在从图像中进行有效且丰富的特征提取的优势。 因此,源图像数据将转换为密集数字固定长度向量。 通常,利用迁移学习方法的预训练模型将是最有效的方法。 此向量将用作我们的解码器 LSTM 模型的输入,该模型将生成字幕说明(如单词序列)。 从原始论文中汲取灵感,可以用数学方式表示要最大化的目标,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfwFqOSa-1681567330253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/4e95a622-a125-4518-9d81-f0de283fe145.png)]
在此,Θ
表示模型参数,I
表示输入图像,S
是其相应的由单词序列组成的标题描述。 考虑到长度为N
的字幕说明,表示总共N
个字,我们可以对{S[0], S[1], ...,S[N]}
使用链式规则,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O3YZjmrD-1681567330253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/ea7fa58f-ba10-4924-a55c-5875ee0b307b.png)]
因此,在模型训练期间,我们有一对(I, S)
图像标题作为输入,其思想是针对上一个方程式优化对数概率的总和,使用有效算法(例如随机梯度下降)来完整训练数据。 考虑到前面公式的 RHS 中的项序列,基于 RNN 的模型是合适的选择,这样,直到t-1
的可变单词数依次由存储状态h[t]
表示。 根据先前的t-1
状态和输入对(图像和下一个字)x[t]
,使用以下命令在每个步骤中按以下步骤更新此内容: 非线性函数f(...)
:
h[t+1] = f(h[t], x[t])
通常, x[t]
代表我们的图像特征和文本,它们是我们的输入。 对于图像特征,我们利用了前面提到的 DCNN。 对于函数f
,我们选择使用 LSTM,因为它们在处理消失和探索梯度等问题方面非常有效,这已在本书的初始章节中进行了讨论。 考虑到 LSTM 存储器块的简要介绍,让我们参考《Show and Tell》研究论文中的下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSrThq61-1681567330253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/78f9f35a-5341-47c2-8e0d-2759083c43a7.png)]
存储块包含 LSTM 单元c
,该单元由输入,输出和忘记门控制。 单元c
将根据输入对每个时间步的知识进行编码,直到先前的时间步为止。 如果门是 1 或 0 ,则这三个门是可以相乘的层,以保持或拒绝来自门控层的值。 循环连接在上图中以蓝色显示。 我们通常在模型中有多个 LSTM,并且在时间t-1
的输出m[t-1]
在时间被馈送到下一个 LSTM。因此,使用以下三个时间,将在时间t-1
处的输出m[t-1]
反馈到存储块。 我们前面讨论过的门。 实际的单元格值也使用“忘记门”反馈。 通常将时间t
处的存储器输出m[t]
输出到 softmax 以预测下一个单词。
这通常是从输出门o[t]
和当前单元状态c[t]
获得的。 下图中描述了其中的一些定义和操作以及必要的方程式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3CaiJBu-1681567330253)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/dc8b0dfb-4929-42ec-ab41-196bf28b0bbe.png)]
在这里,⊙
是乘积运算符,尤其用于当前的门状态和值。 W
矩阵是网络中的可训练参数。 这些门有助于解决诸如爆炸和消失梯度的问题。 网络中的非线性是由我们的常规 S 型σ
和双曲正切h
函数引入的。 如前所述,内存输出m[t]
被馈送到 softmax 以预测下一个单词,其中输出是所有单词上的概率分布。
因此,基于此知识,您可以考虑基于 LSTM 的序列模型需要与必要的词嵌入层和基于 CNN 的模型结合,以从源图像生成密集特征。 因此,LSTM 模型的目标是根据预测的所有先前单词以及输入图像(由我们先前的p(S[t] | I, S[0], S[1], ..., S[t-1])
。 为了简化 LSTM 中的循环连接,我们可以以展开形式来表示它,其中我们代表一系列 LSTM,它们共享下图所示的相同参数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5i79pXIr-1681567330254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/25803aa5-92dd-4e45-9711-a1ef2878d940.png)]
从上图可以明显看出,基于展开的 LSTM 架构,循环连接由蓝色水平箭头表示,并已转换为前馈连接。 同样,很明显,在时间t-1
处 LSTM 的输出m[t-1]
在时间t
被馈送到下一个 LSTM。 将源输入图像视为I
,将字幕视为S = {S[0], S[1], ..., S[N]}
,下图描述了先前描述的展开架构中涉及的主要操作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yn4SOOWl-1681567330254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/f4d3d071-bd12-4d75-99c3-f51d640b329d.png)]
在这里,标题中的每个文本单词都由单热门向量S[t]
表示,因此其尺寸等于我们词汇量(唯一的单词)。 另外要注意的一点是,我们为S[0]
设置了特殊的标记或分隔符,分别由和
S[N]
表示,我们用来表示字幕的开头和结尾。 这有助于 LSTM 理解何时完全生成了字幕。
输入图像I
输入到我们的 DCNN 模型中,该模型生成密集特征向量,并将基于嵌入层的单词转换为密集单词嵌入W[eps]
。 因此,要最小化的整体损失函数是每个步骤右词的对数似然比,如以下等式所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-66Ejk1I2-1681567330254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/e6a2071b-06ad-4962-91d1-2a66bf08c6e0.png)]
因此,在模型训练期间,考虑模型中的所有参数(包括 DCNN,LSTM 和嵌入),可以将这种损失最小化。 现在让我们看一下如何将其付诸实践。
实用的实践方法
现在我们知道了可用于构建成功的图像字幕生成器的基本概念和理论,下面让我们看一下需要动手实践来解决此问题的主要构建块。 基于图像字幕的主要操作,要构建模型,我们将需要以下主要组件:
- 图像特征提取器 — 带迁移学习的 DCNN 模型
- 文本字幕生成器 - 使用 LSTM 的基于序列的语言模型
- 编解码器模型
在为字幕生成系统实现它们之前,让我们简要介绍一下这三个组件。
图像特征提取器 – 使用迁移学习的 DCNN 模型
我们系统的主要输入之一是源图像或照片。 我们都知道,机器学习(ML)或深度学习模型不能仅使用原始图像。 我们需要进行一些处理,还需要从图像中提取相关特征,然后将这些特征用于识别和分类等任务。
图像特征提取器本质上应该接收输入图像,从中提取丰富的层次特征表示,并以固定长度的密集向量的形式表示输出。 我们已经看到了 DCNN 在处理计算机视觉任务方面的强大功能。 在这里,我们将通过使用预训练的 VGG-16 模型作为特征提取器来从所有图像中提取瓶颈特征,从而充分利用迁移学习的力量。 就像快速刷新一样,下图显示了 VGG-16 模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLuU3mbU-1681567330254)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/9c6d795e-28b3-449a-9a67-10405cbce1e1.png)]
为了进行特征提取,我们将删除模型的顶部,即 softmax 层,并使用其余的层从输入图像中获取密集的特征向量。 这通常是编码过程的一部分,输出被馈送到产生字幕的解码器中。
文本字幕生成器 – 使用 LSTM 的基于序列的语言模型
如果传统的基于序列的语言模型知道序列中已经存在的先前单词,则它将预测下一个可能的单词。 对于我们的图像字幕问题,如上一节所述,基于 DCNN 模型的特征和字幕序列中已经生成的单词,LSTM 模型应该能够在每个时间步长预测我们字幕中的下一个可能单词 。
嵌入层用于为字幕数据字典或词汇表中的每个唯一单词生成单词嵌入,通常将其作为 LSTM 模型(解码器的一部分)的输入,来根据图像特征和先前的词序在我们的字幕中生成下一个可能的单词。 想法是最终生成一系列单词,这些单词一起在描述输入图像时最有意义。
编解码器架构
这是将前面两个组件联系在一起的模型架构。 它最初是在神经机器翻译方面取得的巨大成功,通常您将一种语言的单词输入编码器,而解码器则输出另一种语言的单词。 好处是,使用单个端到端架构,您可以连接这两个组件并解决问题,而不必尝试构建单独的和断开的模型来解决一个问题。
DCNN 模型通常形成编码器,该编码器将源输入图像编码为固定长度的密集向量,然后由基于 LSTM 的序列模型将其解码为单词序列,从而为我们提供了所需的标题。 同样,如前所述,必须训练该模型以使给定输入图像的字幕文本的可能性最大化。 为了进行改进,您可以考虑将详细信息添加到此模型中,作为将来范围的一部分。
现在,让我们使用这种方法来实现我们的自动图像标题生成器。
使用迁移学习的图像特征提取
我们模型的第一步是利用预训练的 DCNN 模型,使用迁移学习的原理从源图像中提取正确的特征。 为简单起见,我们不会对 VGG-16 模型进行微调或将其连接到模型的其余部分。 我们将事先从所有图像中提取瓶颈特征,以加快以后的训练速度,因为使用多个 LSTM 构建序列模型即使在 GPU 上也需要大量的训练时间,我们很快就会看到。
首先,我们将从源数据集中的Flickr8k_text
文件夹中加载所有源图像文件名及其相应的标题。 同样,我们将把dev
和train
数据集图像组合在一起,正如我们之前提到的:
import pandas as pd import numpy as np # read train image file names with open('../Flickr8k_text/Flickr_8k.trainImages.txt','r') as tr_imgs: train_imgs = tr_imgs.read().splitlines() # read dev image file names with open('../Flickr8k_text/Flickr_8k.devImages.txt','r') as dv_imgs: dev_imgs = dv_imgs.read().splitlines() # read test image file names with open('../Flickr8k_text/Flickr_8k.testImages.txt','r') as ts_imgs: test_imgs = ts_imgs.read().splitlines() # read image captions with open('../Flickr8k_text/Flickr8k.token.txt','r') as img_tkns: captions = img_tkns.read().splitlines() # combine dev and train image names into one set train_imgs = train_imgs + dev_imgs
现在我们已经整理好输入图像的文件名并加载了相应的标题,我们需要构建一个基于字典的映射,该映射将源图像及其对应的标题映射在一起。 正如我们前面提到的,一个图像由五个不同的人字幕,因此,我们将为每个图像列出五个字幕。 下面的代码可以帮助我们做到这一点:
from collections import defaultdict caption_map = defaultdict(list) # store five captions in a list for each image for record in captions: record = record.split('\t') img_name = record[0][:-2] img_caption = record[1].strip() caption_map[img_name].append(img_caption)
我们稍后将在构建数据集进行训练和测试时利用它。 现在让我们集中讨论特征提取。 在提取图像特征之前,我们需要将原始输入图像预处理为正确的大小,并根据将要使用的模型缩放像素值。 以下代码将帮助我们进行必要的图像预处理步骤:
from keras.preprocessing import image from keras.applications.vgg16 import preprocess_input as preprocess_vgg16_input def process_image2arr(path, img_dims=(224, 224)): img = image.load_img(path, target_size=img_dims) img_arr = image.img_to_array(img) img_arr = np.expand_dims(img_arr, axis=0) img_arr = preprocess_vgg16_input(img_arr) return img_arr
我们还需要加载预训练的 VGG-16 模型以利用迁移学习。 这是通过以下代码片段实现的:
from keras.applications import vgg16 from keras.models import Model vgg_model = vgg16.VGG16(include_top=True, weights='imagenet', input_shape=(224, 224, 3)) vgg_model.layers.pop() output = vgg_model.layers[-1].output vgg_model = Model(vgg_model.input, output) vgg_model.trainable = False vgg_model.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ ... ... block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ flatten (Flatten) (None, 25088) 0 _________________________________________________________________ fc1 (Dense) (None, 4096) 102764544 _________________________________________________________________ fc2 (Dense) (None, 4096) 16781312 ================================================================= Total params: 134,260,544 Trainable params: 0 Non-trainable params: 134,260,544 _________________________________________________________________
很明显,我们删除了 softmax 层,并使模型不可训练,因为我们只对从输入图像中提取密集的特征向量感兴趣。 现在,我们将构建一个利用我们的工具函数并帮助从输入图像中提取正确特征的函数:
def extract_tl_features_vgg(model, image_file_name, image_dir='../Flickr8k_imgs/'): pr_img = process_image2arr(image_dir+image_file_name) tl_features = model.predict(pr_img) tl_features = np.reshape(tl_features, tl_features.shape[1]) return tl_features
现在,我们通过提取图像特征并构建训练和测试数据集来对所有先前的函数和预先训练的模型进行测试:
img_tl_featureset = dict() train_img_names = [] train_img_captions = [] test_img_names = [] test_img_captions = [] for img in train_imgs: img_tl_featureset[img] = extract_tl_features_vgg(model=vgg_model, image_file_name=img) for caption in caption_map[img]: train_img_names.append(img) train_img_captions.append(caption) for img in test_imgs: img_tl_featureset[img] = extract_tl_features_vgg(model=vgg_model, image_file_name=img) for caption in caption_map[img]: test_img_names.append(img) test_img_captions.append(caption) train_dataset = pd.DataFrame({'image': train_img_names, 'caption': train_img_captions}) test_dataset = pd.DataFrame({'image': test_img_names, 'caption': test_img_captions}) print('Train Dataset Size:', len(train_dataset), '\tTest Dataset Size:', len(test_dataset)) Train Dataset Size: 35000 Test Dataset Size: 5000
我们还可以通过使用以下代码来查看训练数据集的外观:
train_dataset.head(10)
前面代码的输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BO4oNsus-1681567330255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/20a72719-13fb-4588-858b-e498eed2b063.png)]
显然,每个输入图像都有五个标题,并且将其保留在数据集中。 现在,我们将这些数据集的记录和从迁移学习中学到的图像特征保存到磁盘上,以便我们可以在模型训练期间轻松地将其加载到内存中,而不必每次运行模型时都提取这些特征:
# save dataset records train_dataset = train_dataset[['image', 'caption']] test_dataset = test_dataset[['image', 'caption']] train_dataset.to_csv('image_train_dataset.tsv', sep='\t', index=False) test_dataset.to_csv('image_test_dataset.tsv', sep='\t', index=False) # save transfer learning image features from sklearn.externals import joblib joblib.dump(img_tl_featureset, 'transfer_learn_img_features.pkl') ['transfer_learn_img_features.pkl']
另外,如果需要,您可以使用以下代码段进行一些初始检查来验证图像特征的外观:
[(key, value.shape) for key, value in img_tl_featureset.items()][:5] [('3079787482_0757e9d167.jpg', (4096,)), ('3284955091_59317073f0.jpg', (4096,)), ('1795151944_d69b82f942.jpg', (4096,)), ('3532192208_64b069d05d.jpg', (4096,)), ('454709143_9c513f095c.jpg', (4096,))] [(k, np.round(v, 3)) for k, v in img_tl_featureset.items()][:5] [('3079787482_0757e9d167.jpg', array([0., 0., 0., ..., 0., 0., 0.], dtype=float32)), ('3284955091_59317073f0.jpg', array([0.615, 0\. , 0.653, ..., 0\. , 1.559, 2.614], dtype=float32)), ('1795151944_d69b82f942.jpg', array([0\. , 0\. , 0\. , ..., 0\. , 0\. , 0.538], dtype=float32)), ('3532192208_64b069d05d.jpg', array([0\. , 0\. , 0\. , ..., 0\. , 0\. , 2.293], dtype=float32)), ('454709143_9c513f095c.jpg', array([0\. , 0\. , 0.131, ..., 0.833, 4.263, 0\. ], dtype=float32))]
我们将在建模的下一部分中使用这些特征。
为我们的字幕建立词汇表
下一步涉及对字幕数据进行一些预处理,并为字幕构建词汇表或元数据字典。 我们首先读取训练数据集记录并编写一个函数来预处理文本标题:
train_df = pd.read_csv('image_train_dataset.tsv', delimiter='\t') total_samples = train_df.shape[0] total_samples 35000 # function to pre-process text captions def preprocess_captions(caption_list): pc = [] for caption in caption_list: caption = caption.strip().lower() caption = caption.replace('.', '').replace(',', '').replace("'", "").replace('"', '') caption = caption.replace('&','and').replace('(','').replace(')', '').replace('-', ' ') caption = ' '.join(caption.split()) caption = '<START> '+caption+' <END>' pc.append(caption) return pc
现在,我们将对字幕进行预处理,并为词汇建立一些基本的元数据,包括用于将唯一的单词转换为数字表示的工具,反之亦然:
# pre-process caption data train_captions = train_df.caption.tolist() processed_train_captions = preprocess_captions(train_captions) tc_tokens = [caption.split() for caption in processed_train_captions] tc_tokens_length = [len(tokenized_caption) for tokenized_caption in tc_tokens] # build vocabulary metadata from collections import Counter tc_words = [word.strip() for word_list in tc_tokens for word in word_list] unique_words = list(set(tc_words)) token_counter = Counter(unique_words) word_to_index = {item[0]: index+1 for index, item in enumerate(dict(token_counter).items())} word_to_index['<PAD>'] = 0 index_to_word = {index: word for word, index in word_to_index.items()} vocab_size = len(word_to_index) max_caption_size = np.max(tc_tokens_length)
重要的是要确保将词汇表元数据保存到磁盘上,以便将来在任何时候都可以将其重新用于模型训练和预测。 否则,如果我们重新生成词汇表,则很有可能已使用其他版本的词汇表来训练模型,其中单词到数字的映射可能有所不同。 这将给我们带来错误的结果,并且我们将浪费宝贵的时间:
from sklearn.externals import joblib vocab_metadata = dict() vocab_metadata['word2index'] = word_to_index vocab_metadata['index2word'] = index_to_word vocab_metadata['max_caption_size'] = max_caption_size vocab_metadata['vocab_size'] = vocab_size joblib.dump(vocab_metadata, 'vocabulary_metadata.pkl') ['vocabulary_metadata.pkl']
如果需要,您可以使用以下代码片段检查词汇元数据的内容,还可以查看常规预处理的文本标题对于其中一张图像的外观:
# check vocabulary metadata {k: v if type(v) is not dict else list(v.items())[:5] for k, v in vocab_metadata.items()} {'index2word': [(0, '<PAD>'), (1, 'nearby'), (2, 'flooded'), (3, 'fundraising'), (4, 'snowboarder')], 'max_caption_size': 39, 'vocab_size': 7927, 'word2index': [('reflections', 4122), ('flakes', 1829), ('flexing', 7684), ('scaling', 1057), ('pretend', 6788)]} # check pre-processed caption processed_train_captions[0] '<START> a black dog is running after a white dog in the snow <END>'
在构建数据生成器函数时,我们将在不久的将来利用它,它将用作模型训练期间深度学习模型的输入。
构建图像标题数据集生成器
在消耗大量数据的任何复杂深度学习系统中,最重要的步骤之一就是构建高效的数据集生成器。 这在我们的系统中非常重要,尤其是因为我们将处理图像和文本数据。 除此之外,我们将处理序列模型,在训练过程中,我们必须多次将相同数据传递给我们的模型。 将列表中的所有数据解压缩后,预先构建数据集将是解决此问题的最无效的方法。 因此,我们将为我们的系统利用生成器的力量。
首先,我们将使用以下代码加载从迁移学习中学到的图像特征以及词汇元数据:
from sklearn.externals import joblib tl_img_feature_map = joblib.load('transfer_learn_img_features.pkl') vocab_metadata = joblib.load('vocabulary_metadata.pkl') train_img_names = train_df.image.tolist() train_img_features = [tl_img_feature_map[img_name] for img_name in train_img_names] train_img_features = np.array(train_img_features) word_to_index = vocab_metadata['word2index'] index_to_word = vocab_metadata['index2word'] max_caption_size = vocab_metadata['max_caption_size'] vocab_size = vocab_metadata['vocab_size'] train_img_features.shape (35000, 4096)
我们可以看到有 35,000 张图像,其中每张图像都有大小为 4,096 的密集特征向量表示。 现在的想法是构建一个模型数据集生成器,该生成器将生成(输入,输出)对。 对于我们的输入,我们将使用转换为密集特征向量的源图像以及相应的图像标题,在每个时间步添加一个单词。 对应的输出将是对应输入图像和标题的相同标题的下一个单词(必须预测)。 下图使此方法更加清晰:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YmEaKeLd-1681567330255)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/8439d2a4-1315-4146-9eb0-59f0841cbaeb.png)]
基于此架构,很明显,对于同一图像,在每个时间步上,我们都传递相同的特征向量,并保持每次添加一个单词的标题,同时传递下一个要预测的单词作为相应的输出来训练我们的模型。 以下函数将帮助我们实现这一点,我们利用 Python 生成器进行延迟加载并提高内存效率:
from keras.preprocessing import sequence def dataset_generator(processed_captions, transfer_learnt_features, vocab_size, max_caption_size, batch_size=32): partial_caption_set = [] next_word_seq_set = [] img_feature_set = [] batch_count = 0 batch_num = 0 while True: for index, caption in enumerate(processed_captions): img_features = transfer_learnt_features[index] for cap_idx in range(len(caption.split()) - 1): partial_caption = [word_to_index[word] for word in caption.split()[:cap_idx+1]] partial_caption_set.append(partial_caption) next_word_seq = np.zeros(vocab_size) next_word_seq[word_to_index [caption.split()[cap_idx+1]]] = 1 next_word_seq_set.append(next_word_seq) img_feature_set.append(img_features) batch_count+=1 if batch_count >= batch_size: batch_num += 1 img_feature_set = np.array(img_feature_set) partial_caption_set = sequence.pad_sequences( sequences=partial_caption_set, maxlen=max_caption_size, padding='post') next_word_seq_set = np.array(next_word_seq_set) yield [[img_feature_set, partial_caption_set], next_word_seq_set] batch_count = 0 partial_caption_set = [] next_word_seq_set = [] img_feature_set = []
让我们尝试了解此函数的真正作用! 尽管我们确实在上图中描绘了一个不错的视觉效果,但现在我们将使用以下代码为10
的批量大小生成示例数据:
MAX_CAPTION_SIZE = max_caption_size VOCABULARY_SIZE = vocab_size BATCH_SIZE = 10 print('Vocab size:', VOCABULARY_SIZE) print('Max caption size:', MAX_CAPTION_SIZE) print('Test Batch size:', BATCH_SIZE) d = dataset_generator(processed_captions=processed_train_captions, transfer_learnt_features=train_img_features, vocab_size=VOCABULARY_SIZE, max_caption_size=MAX_CAPTION_SIZE, batch_size=BATCH_SIZE) d = list(d) img_features, partial_captions = d[0][0] next_word = d[0][1] Vocab size: 7927 Max caption size: 39 Test Batch size: 10
现在,我们可以使用以下代码从数据生成器函数验证返回的数据集的维数:
img_features.shape, partial_captions.shape, next_word.shape ((10, 4096), (10, 39), (10, 7927))
很明显,我们的图像特征本质上是每个向量中 4,096 个特征的密集向量。 在字幕的每个时间步都对同一图像重复相同的特征向量。 字幕生成的向量的大小为MAX_CAPTION_SIZE
,即39
。 下一个单词通常以单次编码的方式返回,这对于用作 softmax 层的输入非常有用,以检查模型是否预测了正确的单词。 以下代码向我们展示了图像特征向量如何查找输入图像的10
批量大小:
np.round(img_features, 3) array([[0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ], [0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ], [0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ], ..., [0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ], [0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ], [0\. , 0\. , 1.704, ..., 0\. , 0\. , 0\. ]], dtype=float32)
如前所述,在批量数据生成过程中的每个时间步都重复了相同的图像特征向量。 我们可以检查在输入给模型的每个时间步骤中标题的形成方式。 为了简单起见,我们仅显示前 11 个单词:
# display raw caption tokens at each time-step print(np.array([partial_caption[:11] for partial_caption in partial_captions])) [[6917 0 0 0 0 0 0 0 0 0 0] [6917 2578 0 0 0 0 0 0 0 0 0] [6917 2578 7371 0 0 0 0 0 0 0 0] [6917 2578 7371 3519 0 0 0 0 0 0 0] [6917 2578 7371 3519 3113 0 0 0 0 0 0] [6917 2578 7371 3519 3113 6720 0 0 0 0 0] [6917 2578 7371 3519 3113 6720 7 0 0 0 0] [6917 2578 7371 3519 3113 6720 7 2578 0 0 0] [6917 2578 7371 3519 3113 6720 7 2578 1076 0 0] [6917 2578 7371 3519 3113 6720 7 2578 1076 3519 0]] # display actual caption tokens at each time-step print(np.array([[index_to_word[word] for word in cap][:11] for cap in partial_captions])) [['<START>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' 'running' '<PAD>' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' 'running' 'after' '<PAD>' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' 'running' 'after' 'a' '<PAD>' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' 'running' 'after' 'a' 'white' '<PAD>' '<PAD>'] ['<START>' 'a' 'black' 'dog' 'is' 'running' 'after' 'a' 'white' 'dog' '<PAD>']]
我们可以清楚地看到在符号后的每个步骤中如何将一个单词添加到输入标题,这表示文本标题的开始。 现在让我们看一下对应的下一单词生成输出(通常是根据两个输入预测的下一单词):
next_word array([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]) print('Next word positions:', np.nonzero(next_word)[1]) print('Next words:', [index_to_word[word] for word in np.nonzero(next_word)[1]]) Next word positions: [2578 7371 3519 3113 6720 7 2578 1076 3519 5070] Next words: ['a', 'black', 'dog', 'is', 'running', 'after', 'a', 'white', 'dog', 'in']
很清楚,下一个单词通常基于输入字幕中每个时间步的单词顺序指向字幕中的下一个正确单词。 这些数据将在训练期间的每个周期馈入我们的模型。
Python 迁移学习实用指南:6~11(5)https://developer.aliyun.com/article/1426857