原文:Hands-On Transfer Learning with Python
译者:飞龙
本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。
不要担心自己的形象,只关心如何实现目标。——《原则》,生活原则 2.3.c
六、图像识别与分类
知识投资永远是最大的利益。
——本杰明·富兰克林
在计算机视觉的保护下,图像识别是一个活跃的跨学科研究领域。 顾名思义,图像或对象识别是识别图像或视频序列中的对象的任务。 传统上,该领域利用数学和计算机辅助建模以及对象设计方面的进步。 这些年来,已经开发了一些手工标注的数据集,以测试和评估图像识别系统。 我们现在称它们为传统技术,一直统治着整个场景,并且不断地改进这项任务,直到最近。 2012 年,深度学习参加了 ImageNet 竞赛,为快速改善和进步计算机视觉和深度学习技术打开了闸门。
在本章中,我们将从深度学习(尤其是迁移学习)的角度介绍图像识别和分类的概念。 本章将涵盖以下方面:
- 深度学习图像分类简介
- 基准数据集
- 最新的深度图像分类模型
- 图像分类和迁移学习用例
本章从本书的第三部分开始。 在本书的这一部分中,我们将涵盖涉及前两部分中讨论的概念和技术的案例研究。 这些用例将呈现现实世界的主题/研究领域,并帮助您了解如何在不同的环境中利用迁移学习。 您可以在 GitHub 存储库中的Chapter 6
文件夹中快速阅读本章的代码。 可以根据需要参考本章。
基于深度学习的图像分类
卷积神经网络(CNN)是这项深度学习革命的核心,旨在改善图像分类任务。 CNN 是处理图像数据的专用神经网络。 作为快速补充,CNN 可以通过它们共享的权重架构帮助我们推断出位移和空间不变特征,并且基本上是前馈网络的变体。 在第 3 章“了解深度学习架构”和第 5 章中,我们已经详细介绍了 CNN 的基础知识。 在继续进行之前,我们鼓励读者快速复习以更好地理解。 下图展示了运行中的典型 CNN:
典型的 CNN,来自这个页面
神经网络最早出现在 2011 年的图像分类竞赛中。受 GPU 训练的网络开始赢得竞赛。 在 2012 年,深层的 CNN 在 ImageNet 图像分类任务上的表现从以前的最好水平提高到 83%,这是世界上第一次注意到。 结果令人惊讶,足以引起全球关注,并有助于通过深度学习解决用例的泛滥。
基准数据集
图像分类,或就此而言,任何分类任务,本质上都是监督学习任务。 受监督的任务通过可用的基础训练集了解不同的类别。
即使 CNN 是共享权重的优化前馈网络,但要在深层 ConvNet 中训练的参数数量可能仍然很大。 这就是为什么需要大量的训练才能获得表现更好的网络的原因之一。 幸运的是,全球研究小组一直在努力收集,手工标注和众包不同的数据集。 这些数据集可用于对不同算法的表现进行基准测试,以及识别不同比赛中的获胜者。
以下是图像分类领域中广泛接受的基准数据集的简要列表:
- ImageNet:这是一个具有黄金标准的可视数据集,具有超过 1400 万个带有手工标注的高分辨率彩色图像,涵盖了 20,000 个类别。 它是 2009 年由普林斯顿大学计算机科学系设计用于视觉对象识别任务的。此后,此数据集(在其 1000 个非重叠类的修整版本中)已用作 ImageNet 大规模视觉识别挑战的基础。
- 8000 万个微小图像数据集:顾名思义,该 MIT 数据集包含从互联网收集的 8000 万个图像,并标记了 75,000 多个不同的非抽象英语名词。 该数据集还为其他广泛使用的数据集(包括 CIFAR 数据集)奠定了基础。
- CIFAR-10:由加拿大高级研究所开发,CIFAR-10 是机器学习(ML)研究中使用最广泛的数据集之一。 该数据集包含 60,000 张横跨 10 个非重叠类的低分辨率图像。
- CIFAR-100:来自同一研究组,该数据集包含 60,000 张图像,均匀分布在 100 个不同的类别中。
- 上下文中的公共对象:上下文中的公共对象(COCO)是用于对象标识,分段和字幕的大型可视数据库。 该数据集包含超过 200,000 张跨越不同类别的标记图像。
- 开放图像:这可能是最大的可用标注数据集之一。 该数据集的版本 4 包含超过 900 万个带标注的图像。
- Caltech 101 和 Caltech 256:这些数据集包含分别跨越 101 和 256 个类别的带标注的图像。 加州理工学院 101 包含约 9,000 张图像,而加州理工学院 256 包含近 30,000 张图像。
- 斯坦福犬数据集:这是一个有趣的数据集,特定于不同的犬种。 它包含 20,000 个彩色图像,涵盖 120 个不同的犬种。
- MNIST:MNIST 是有史以来最著名的视觉数据集之一,已成为 ML 爱好者的事实上的 HelloWorld 数据集。 它包含超过 60,000 个手工标记的数字(零到九个数字)。
上面的列表仅是冰山一角。 还有许多其他数据集可以捕获世界的不同方面。 准备这些数据集是一个痛苦且耗时的过程,但是这些数据集使深度学习在当前形式下如此成功。 鼓励读者详细研究这些和其他此类数据集,以了解它们背后的细微差别以及这些数据集为我们解决的挑战。 在本章和后续章节中,我们将利用其中的一些数据集来理解迁移学习的概念。
最新的深度图像分类模型
多年来,深度学习已引起了广泛的关注和炒作。 不足为奇的是,在全球范围内以深度学习为中心的知名竞赛,会议和期刊上共享了大量研究工作。 尤其是图像分类架构已经引起人们的关注,这几年来,定期进行迭代改进一直是人们关注的重点。 让我们快速了解一些表现最佳,最流行的最新深度图像分类架构:
- AlexNet:这是可以归功于打开闸门的网络。 由深度学习的先驱之一 Geoffrey Hinton 和团队设计,该网络将前五名的错误率降低到了 15.3%。 它也是最早利用 GPU 加快学习过程的架构之一。
- VGG-16:牛津大学视觉几何小组的网络是表现最好的架构之一,广泛用于对其他设计进行基准测试。 VGG-16 采用了一个简单的架构,该架构是基于
3 x 3
个卷积层(一个 16 层)相互堆叠,然后是一个最大池化层,以实现强大的表现。 该模型由稍微更复杂的模型 VGG19 继承。 - Inception:也称为 GoogleNet ,该网络是在 ImageNet 大规模视觉识别挑战赛(ILSVRC)在 2014 年实现了前五名的错误率为 6.67%。 它是最早实现接近人类表现的架构之一。 该网络背后的新颖之处在于使用了起始层,该起始层包括了在同一级别将不同大小的内核连接在一起的过程。
- ResNet:由 Microsoft Research Asia 引入,残差网络(ResNet)是一种新颖的架构,利用批量规范化和跳过连接来实现仅仅 3.57% 的前五位的错误率。 它比诸如 VGG 之类的简单架构要深很多(152 层)并且要复杂得多。
- MobileNet:尽管大多数架构都在竞争中胜过其他架构,但每个新的复杂网络都需要更多的计算能力和数据资源。 MobileNet 偏离了此类架构,并被设计为适用于移动和嵌入式系统。 该网络利用了一种新颖的思想,即使用深度可分离卷积来减少训练网络所需的参数总数。
我们提供了基于深度学习的图像分类空间中一些最新架构的快速概述和概述。 有关详细讨论,读者可以查看第 3 章,“了解深度学习架构”中的“卷积神经网络”部分。
图像分类与迁移学习
到目前为止,我们已经讨论了什么是图像分类。 在本节中,我们将通过构建自己的分类器来弄清手。 在本章的较早部分中,我们简要提到了著名的基准测试数据集,包括 CIFAR-10 和 Stanford Dogs 数据集,我们将在接下来的部分中重点介绍这些数据集。 我们还将利用预先训练的模型来了解我们如何利用迁移学习来改进我们的模型。
CIFAR-10
CIFAR-10 是深度学习领域中使用最广泛的图像数据集之一。 由加拿大高级研究所准备,这是一个相当不错的数据集。 该数据集的主要优点是它包含 10 个非重叠类别的平衡分布。 图像的分辨率和尺寸较低,因此可以将数据集用于在较小的内存占用量系统上进行训练。
建立图像分类器
CIFAR-10 是少数可用的平衡数据集之一。 它的整体大小为 60,000 张图像。 以下代码段加载 CIFAR-10 数据集,并设置训练变量和测试变量:
# load CIFAR dataset
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
数据集中的图像分辨率很低,有时甚至很难贴上标签。 IPython 笔记本CIFAR-10_CNN_Classifier.ipynb
中提供了本节中共享的代码。
我们已经讨论了 CNN 以及如何针对视觉数据集对其进行优化。 CNN 遵循权重分配原则以减少参数数量; 从头开始开发它们不仅需要强大的深度学习技能,还需要巨大的基础架构需求。 牢记这一点,从头开始开发 CNN 并测试我们的技能将很有趣。
以下代码片段展示了使用 Keras 构建的非常简单的 CNN,它只有五层(两层卷积,一层最大池化,一层密集和一层最终 softmax 层):
model = Sequential()
model.add(Conv2D(16, kernel_size=(3, 3),
activation='relu',
input_shape=INPUT_SHAPE))
model.add(Conv2D(32, (3,3), padding='same',
kernel_regularizer=regularizers.l2(WEIGHT_DECAY),
activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(NUM_CLASSES, activation='softmax'))
为了提高总体泛化表现,该模型还包含BatchNormalization
层和DropOut
层。 这些层有助于我们控制过拟合,还可以防止网络存储数据集本身。
我们仅用 25 个周期来运行模型,以在验证集上达到约 65% 的准确率。 以下屏幕快照显示了训练后的模型的输出预测:
来自基于 CNN 的 CIFAR-10 分类器的预测
尽管绝不是最先进的结果,但结果足够不错。 读者应该记住,这个 CNN 只是为了展示手头的巨大潜力,我们鼓励您尝试在同一行上进行实验。
迁移知识
由于本章和书着重于迁移学习,因此让我们快速进行利用和迁移所学信息的实际任务。 在上一节中,我们讨论了不同的最新 CNN 架构。 现在,让我们利用在 ImageNet 上训练的 VGG-16 模型对 CIFAR-10 数据集中的图像进行分类。 该部分的代码在 IPython 笔记本CIFAR10_VGG16_Transfer_Learning_Classifier.ipynb
中可用。
ImageNet 是一个庞大的视觉数据集,具有 20,000 多个不同类别。 另一方面,CIFAR-10 仅限于 10 个非重叠类别。 像 VGG-16 这样的强大网络需要巨大的计算能力和时间来训练,以达到比人类更好的表现。 这将迁移学习带入了画面。 由于我们大多数人都无法访问无限的计算,因此我们可以在两种不同的设置下利用这些网络:
- 使用经过预训练的最新网络作为特征提取器。 这是通过删除顶部分类层并使用倒数第二层的输出来完成的。
- 在新数据集上微调最新的网络。
我们将利用 VGG-16 作为特征提取器,并在其之上构建自定义分类器。 以下代码段加载并准备了 CIFAR-10 数据集以供使用:
# extract data
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
#split train into train and validation sets
X_train, X_val, y_train, y_val = train_test_split(X_train,
y_train,
test_size=0.15,
stratify=np.array
(y_train),
random_state=42)
# perform one hot encoding
Y_train = np_utils.to_categorical(y_train, NUM_CLASSES)
Y_val = np_utils.to_categorical(y_val, NUM_CLASSES)
Y_test = np_utils.to_categorical(y_test, NUM_CLASSES)
# Scale up images to 48x48
X_train = np.array([sp.misc.imresize(x,
(48, 48)) for x in X_train])
X_val = np.array([sp.misc.imresize(x,
(48, 48)) for x in X_val])
X_test = np.array([sp.misc.imresize(x,
(48, 48)) for x in X_test])
前面的代码片段不仅将训练数据集拆分为训练集和验证集,还将目标变量转换为单热编码形式。 我们还将图像尺寸从32 x 32
调整为48 x 48
,以符合 VGG-16 输入要求。 训练,验证和测试数据集准备就绪后,我们可以朝着准备分类器的方向努力。
以下代码段显示了我们如何轻松地在现有模型之上附加新层。 因为我们的目标是仅训练分类层,所以通过将可训练参数设置为False
冻结其余层。 这使我们即使在功能较弱的基础架构上也可以利用现有架构,并将学到的权重从一个域转移到另一个域:
base_model = vgg.VGG16(weights='imagenet',
include_top=False,
input_shape=(48, 48, 3))
# Extract the last layer from third block of vgg16 model
last = base_model.get_layer('block3_pool').output
# Add classification layers on top of it
x = GlobalAveragePooling2D()(last)
x= BatchNormalization()(x)
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dropout(0.6)(x)
pred = Dense(NUM_CLASSES, activation='softmax')(x)
model = Model(base_model.input, pred)
for layer in base_model.layers:
layer.trainable = False
我们有基本的成分。 整个流水线中剩下的最后一块是数据扩充。 整个数据集仅包含 60,000 张图像; 数据扩充非常方便,可以为手头的样本集添加某些变化。 这些变体使网络能够学习比其他方法更通用的功能。 以下代码段利用ImageDataGenerator()
实用工具准备训练和验证增强对象:
# prepare data augmentation configuration
train_datagen = ImageDataGenerator(rescale=1\. / 255,
horizontal_flip=False)
train_datagen.fit(X_train)
train_generator = train_datagen.flow(X_train,
Y_train,
batch_size=BATCH_SIZE)
val_datagen = ImageDataGenerator(rescale=1\. / 255,
horizontal_flip=False)
val_datagen.fit(X_val)
val_generator = val_datagen.flow(X_val,
Y_val,
batch_size=BATCH_SIZE)
现在让我们训练模型几个周期并衡量其表现。 以下代码段调用fit_generator()
函数将新添加的层训练到模型中:
train_steps_per_epoch = X_train.shape[0] // BATCH_SIZE
val_steps_per_epoch = X_val.shape[0] // BATCH_SIZE
history = model.fit_generator(train_generator,
steps_per_epoch=train_steps_per_epoch,
validation_data=val_generator,
validation_steps=val_steps_per_epoch,
epochs=EPOCHS,
verbose=1)
fit_generator()
返回的历史对象包含有关每个周期的详细信息。 我们利用这些来绘制模型在精度和损失方面的整体表现。 结果如下所示:
训练验证表现
如我们所见,与从头开始开发的模型相比,迁移学习帮助我们在整体表现上实现了惊人的提升。 这项改进利用了训练有素的 VGG-16 权重将学习到的特征转移到该域。 读者可以使用相同的工具plot_predictions()
可视化随机样本上的分类结果,如以下屏幕截图所示:
使用基于 VGG-16 的迁移学习构建的分类器的预测结果
神经网络是相当复杂的学习机器,很难调试和优化。 尽管有许多可用的技术,但需要经验来微调网络。 在当前情况下,使用像 VGG-16 这样的深层 CNN 可能对如此小尺寸的图像来说是过大的杀伤力,但它显示了巨大的潜力。 明智地使用它!
这是迁移学习的快速简单的应用,其中我们利用了像 VGG-16 这样令人惊讶的复杂深度 CNN 来准备 CIFAR-10 分类器。 鼓励读者不仅尝试定制分类器的不同配置,而且尝试甚至使用不同的预训练网络来理解所涉及的复杂性。
狗品种鉴定数据集
在上一节中,我们利用低分辨率图像数据集将图像分类为 10 个非重叠类别。 这绝非易事,但我们以最小的努力获得了不错的表现。
现在让我们升级游戏,并使图像分类的任务更加令人兴奋。 在本节中,我们将专注于细粒度图像分类的任务。 与常规图像分类任务不同,细粒度图像分类是指识别更高级别类别中不同子类别的任务。
为了更好地理解此任务,我们将围绕斯坦福犬数据集进行讨论。 顾名思义,该数据集包含不同犬种的图像。 在这种情况下,任务是识别每种犬种。 因此,高级概念是狗本身,而任务是正确分类不同的子概念或子类(在这种情况下为品种)。 该数据集包含来自 ImageNet 数据集的 20,000 个带有标签的图像,该图像集由 120 个不同的犬种组成。 为了便于讨论,我们将利用 Kaggle 提供的数据集。 该数据集位于以下链接中。
让我们开始构建狗分类器的任务。 但是,在实际模型之前,让我们对数据集本身进行快速探索性分析,以更好地理解。
探索性分析
我们不能足够强调理解底层数据集的重要性。 在当前情况下,我们正在处理一个视觉数据集,该数据集由 10,000 个样本组成,分布在 120 个类别(狗的品种)中。 读者可以在名为dog_breed_eda.ipynb
的 IPython 笔记本中参考与探索性分析有关的所有步骤。
由于这是一个可视数据集,因此我们首先将数据集中的一些样本可视化。 有多种方法可以在 Python 中提取和可视化图像数据。 我们将依靠 SciPy 和与 matplotlib 相关的工具来做到这一点。 以下代码片段导入所需的库:
In [1]: import os
...: import scipy as sp
...: import numpy as np
...: import pandas as pd
...:
...: import PIL
...: import scipy.ndimage as spi
...:
...: import matplotlib.pyplot as plt
...: import seaborn as sns
...:
...:np.random.seed(42)
由于数据集很大,因此我们准备了几个工具来加载随机的图像批量并显示选定的批量。 工具的标题为load_batch()
和plot_batch()
; 这些细节在 IPython 笔记本中可用。 以下代码段绘制了随机批次以供参考:
In [7]:batch_df = load_batch(dataset_df,
...: batch_size=36)
In [8]:plot_batch(batch_df, grid_width=6, grid_height=6
...: ,im_scale_x=64, im_scale_y=64)
生成的输出如下网格所示:
狗品种识别数据集的样本图像
从前面的网格中,我们可以看到在分辨率,照明,缩放级别等方面存在很多变化,并且图像不仅包含一只狗,而且还包含其他狗和周围环境。 项目。 我们还需要了解图像尺寸的差异。 使用以下代码段,我们生成了一个散点图来理解它们:
In [12]: plt.plot(file_dimension_list[:, 0],
file_dimension_list[:, 1], "ro")
...: plt.title("Image sizes")
...: plt.xlabel("width")
...: plt.ylabel("height")
生成的散点图如下所示。 我们可以清楚地看到最大图像数量在500 x 500
尺寸之内,但是形状确实有所不同:
图像大小的散点图
还需要检查狗的品种分布,以了解我们正在处理什么。 由于我们具有标记的数据集,因此我们可以轻松地进行检查。 以下代码段使用pandas
绘制品种分布:
In [13]: fig = plt.figure(figsize = (12,5))
...:
...: ax1 = fig.add_subplot(1,2, 1)
...:dataset_df.breed.value_counts().tail().plot('bar',
...: ax=ax1,color='gray',
...: title="Breeds with Lowest Counts")
...:
...: ax2 = fig.add_subplot(1,2, 2)
...:dataset_df.breed.value_counts().head().plot('bar',
...: ax=ax2,color='black',
...: title="Breeds with Highest Counts")
数据集没有被平均分割; 与其他品种相比,某些品种的样本更多。 从下图中的图可以明显看出:
最高和最低犬种样本数
这样的数据集需要彻底的探索。 我们已经在本节中介绍了一些探索性步骤。 进一步的步骤在引用的 IPython 笔记本中列出/执行。 鼓励读者逐步了解图像大小调整的影响,不同层如何检测不同特征,灰度等。
数据准备
探索性分析有助于我们更好地了解手头的数据集。 下一个任务是为数据集构建一个实际的分类器。 众所周知,对于任何分类问题,第一步都是将数据集分为训练集和验证集。 由于我们正在使用 Keras,因此我们将从其工具中获取帮助以帮助准备我们的数据集。 以下代码段展示了将原始数据集组织为训练集和验证集的过程:
# Prepare column to store image path
data_labels['image_path'] = data_labels.apply(
lambda row: (train_folder +
row["id"] + ".jpg" ),
axis=1)
# load image data as arrays of defined size
train_data = np.array([img_to_array(load_img(img, target_size=(299,
299)))
for img in data_labels['image_path'].values.tolist()
]).astype('float32')
# split data into train and test
x_train, x_test, y_train, y_test = train_test_split(train_data,
target_labels,
test_size=0.3,
stratify=np.array(target_labels),
random_state=42)
# split train dataset into train and validation sets
x_train, x_val, y_train, y_val = train_test_split(x_train,
y_train,
test_size=0.15,
stratify=
np.array(y_train),
random_state=42)
前面显示的第一步是准备标签dataframe
中的派生列以保存实际图像路径。 然后,我们继续简单地将数据集分为训练,验证和测试数据集。 下一步是在将标签输入模型之前,将标签快速转换为一次性编码形式。 以下代码段准备了目标变量的一键编码形式:
y_train_ohe = pd.get_dummies(y_train.reset_index(
drop=True)
).as_matrix()
y_val_ohe = pd.get_dummies(y_val.reset_index(
drop=True)
).as_matrix()
y_test_ohe = pd.get_dummies(y_test.reset_index(
drop=True)
).as_matrix()
众所周知,深度学习算法需要大量数据。 在这种情况下,即使我们总共有 10,000 张图像,每个类别的计数也不是很大。 为了对此进行改进,我们执行增强。 简单来说,数据增强是利用策展的数据集通过生成现有数据点的变体来扩大自身的过程。 在这种情况下,我们利用keras
中的ImageDataGenerator
来扩充训练和验证数据集,如以下代码片段所示:
# Create train generator.
train_datagen = ImageDataGenerator(rescale=1./255,
rotation_range=30,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip = 'true')
train_generator = train_datagen.flow(x_train,
y_train_ohe,
shuffle=False,
batch_size=BATCH_SIZE,
seed=1)
# Prepare Validation data augmentation
val_datagen = ImageDataGenerator(rescale = 1./255)
val_generator = train_datagen.flow(x_val,
y_val_ohe,
shuffle=False,
batch_size=BATCH_SIZE,
seed=1)
现在我们已经准备好数据,下一步是准备实际的分类器。
使用迁移学习的狗分类器
现在我们的数据集已经准备好,让我们开始建模过程。 我们已经知道如何从头开始构建深度的卷积网络。 我们也了解达到良好表现所需的微调数量。 对于此任务,我们将利用迁移学习的概念。
预训练模型是开始进行迁移学习任务所需的基本特征。 如前几章所述,可以通过在当前任务上微调预训练网络的权重,或将预训练模型用作特征提取器,来利用迁移学习。
在这种用例中,我们将集中精力利用预训练的模型作为特征提取器。 众所周知,深度学习模型基本上是神经元相互连接的层的堆叠,最后一层充当分类器。 这种架构使深度神经网络能够捕获网络中不同级别的不同特征。 因此,我们可以利用此属性将它们用作特征提取器。 通过删除最后一层或使用倒数第二层的输出,可以做到这一点。 然后,将倒数第二层的输出馈送到其他一组层,然后是分类层。 以下代码段展示了基于 InceptionV3 预训练模型的特征提取,并堆叠了其他层以准备分类器:
# Get the InceptionV3 model so we can do transfer learning
base_inception = InceptionV3(weights='imagenet',
include_top = False,
input_shape=(299, 299, 3))
# Add a global spatial average pooling layer
out = base_inception.output
out = GlobalAveragePooling2D()(out)
out = Dense(512, activation='relu')(out)
out = Dense(512, activation='relu')(out)
total_classes = y_train_ohe.shape[1]
predictions = Dense(total_classes,
activation='softmax')(out)
如前面的代码片段所示, Keras 提供了用于处理许多预训练模型的简单工具,将它们用作特征提取器就像将标志include_top
设置为False
一样简单。 在下面的代码片段中,我们通过将两组层堆叠在一起,然后冻结 InceptionV3 中的层来准备最终模型:
model = Model(inputs=base_inception.input,
outputs=predictions)
# only if we want to freeze layers
for layer in base_inception.layers:
layer.trainable = False
现在,我们有了模型,所有模型都将在“狗品种识别”数据集中进行训练。 我们使用fit_generator()
方法训练模型,以利用上一步中准备的数据增强。 我们将批次大小设置为 32,并训练模型 13 个周期。 以下代码片段设定了滚动的方向:
batch_size = BATCH_SIZE
train_steps_per_epoch = x_train.shape[0] // batch_size
val_steps_per_epoch = x_val.shape[0] // batch_size
history = model.fit_generator(train_generator,
steps_per_epoch=train_steps_per_epoch,
validation_data=val_generator,
validation_steps=val_steps_per_epoch,
epochs=15,
verbose=1)
由于我们在每个周期(history
对象)之后都保存了模型参数和表现的输出,因此我们现在将利用它来了解模型表现。 下图绘制了模型的训练和测试精度以及其损失表现:
狗品种分类器表现
该模型在训练集和验证集上仅在 15 个周期内就获得了 80% 精度以上的可嘉表现。 右侧的图显示了损耗下降并收敛到 0.5 的速度。 这是一个很好的例子,说明了迁移学习的强大而简单。
训练和验证的表现相当不错,但是对看不见的数据的表现如何? 由于我们已经将原始数据集分为三个单独的部分。 这里要记住的重要一点是,测试数据集必须经过与训练数据集相似的预处理。 为了解决这个问题,在将测试数据集输入到函数之前,我们还对它进行了缩放。
该模型在测试数据集上实现了惊人的 85% 准确率以及 0.85 F1 得分。 鉴于我们仅用最少的投入就训练了 15 个周期,迁移学习帮助我们实现了一个不错的分类器:
狗的品种分类器预测
上图显示了模型表现的视觉证明。 正如我们所看到的,在大多数情况下,该模型不仅可以预测正确的犬种,而且还可以非常有把握地进行预测。
总结
在本书的前两个部分中,我们讨论了很多理论。 建立了强大的概念和技术基础后,我们在本章中开始了用例驱动的旅程。 本章是一系列后续章节中的第一章,这些章节将展示不同场景和领域中迁移学习的实际用例。 在本章中,我们将迁移学习应用于视觉对象识别或俗称图像分类的领域。
我们从围绕 CNN 的快速更新开始,并随着 2012 年深度学习模型的出现,计算机辅助对象识别的整个过程将一劳永逸。我们简要介绍了各种最新的图像分类模型 ,它已经超越了人类的表现。 我们还快速浏览了学术界和行业专家用于训练和调整模型的不同基准数据集。 设置上下文后,我们从 CIFAR-10 数据集开始。 我们使用 Keras 和 TensorFlow 作为后端从头开始构建了一个分类器。 我们利用 VGG-16 作为特征提取的预训练模型,利用迁移学习来改善此表现。
在本章的最后部分,我们利用迁移学习来解决稍微复杂的问题。 我们准备了一个犬种分类器,而不是基于类别不重叠的数据集(CIFAR-10),以基于 Stanford Dogs 数据集识别 120 种不同的犬种。 只需几行代码,我们就可以实现近乎最新的表现。 第二种用例也称为细粒度图像分类任务,并且比通常的图像分类任务复杂。 在本章中,我们展示了通过迁移学习获得惊人结果的强大功能和简便性。 在接下来的章节中,请继续关注来自计算机视觉,音频分析等领域的惊人用例。
七、文本文件分类
在本章中,我们讨论了迁移学习在文本文档分类中的应用。 文本分类是一种非常流行的自然语言处理任务。 关键目标是根据文档的文本内容将文档分配到一个或多个类别或类别。 这在行业中得到了广泛的应用,包括将电子邮件分类为垃圾邮件/非垃圾邮件,审阅和评级分类,情感分析,电子邮件或事件路由,在此我们将电子邮件\事件分类,以便可以将其自动分配给相应的人员。 以下是本章将涉及的主要主题:
- 文本分类概述,行业应用和挑战
- 基准文本分类数据集和传统模型的表现
- 密集向量的单词表示 — 深度学习模型
- CNN 文档模型-单词到句子的嵌入,然后进行文档嵌入
- 源和目标域分布不同的迁移学习的应用; 也就是说,源域由重叠较少的类组成,目标域具有许多混合类
- 源和目标域本身不同的迁移学习的应用(例如,源是新闻,目标是电影评论,依此类推)
- 训练有素的模型在完成其他文本分析任务(例如文档摘要)中的应用-解释为什么将评论归类为负面/正面
我们将通过动手示例来关注概念和实际实现。 您可以在 GitHub 存储库中的Chapter 7
文件夹中快速阅读本章的代码。 可以根据需要参考本章。
文本分类
给定一组文本文档和一组预定义类别,文本分类的目的是将每个文档分配给一个类别。 根据问题,输出可以是软分配或硬分配。 软分配意味着将类别分配定义为所有类别上的概率分布。
文本分类在工业中有广泛的应用。 以下是一些示例:
- 垃圾邮件过滤:给定电子邮件,将其分类为垃圾邮件或合法电子邮件。
- 情感分类:给定评论文本(电影评论,产品评论),请确定用户的极性-无论是正面评论,负面评论还是神经评论。
- 问题单分配:通常,在任何行业中,每当用户遇到有关任何 IT 应用或软件/硬件产品的问题时,第一步就是创建问题单。 这些票证是描述用户所面临问题的文本文档。 下一个合乎逻辑的步骤是,某人必须阅读说明并将其分配给具有适当专业知识的团队才能解决问题。 现在,给定一些历史故障单和解决方案团队类别,可以构建文本分类器以自动对问题故障单进行分类。
- 问题单的自动解决方案:在某些情况下,问题的解决方案也是预先定义的; 也就是说,专家团队知道解决该问题应遵循的步骤。 因此,在这种情况下,如果可以高精度地构建文本分类器来对票证进行分类,则一旦预测了票证类别,便可以运行自动脚本来直接解决问题。 这是未来 IT 运营人工智能(AIOps)的目标之一。
- 有针对性的营销:营销人员可以监视社交媒体中的用户,并将其分类为促进者或破坏者,并基于此,对在线产品发表评论。
- 体裁分类:自动文本体裁分类对于分类和检索非常重要。 即使一组文档属于同一类别,因为它们共享一个共同的主题,但它们通常具有不同的用途,属于不同的流派类别。 如果可以检测到搜索数据库中每个文档的类型,则可以根据用户的喜好更好地向用户呈现信息检索结果。
- 索赔中的欺诈检测:分析保险索赔文本文档并检测索赔是否为欺诈。
传统文本分类
构建文本分类算法/模型涉及一组预处理步骤以及将文本数据正确表示为数值向量。 以下是一般的预处理步骤:
-
句子拆分:将文档拆分为一组句子。
-
分词:将句子拆分为组成词。
-
词干或词根去除:单词标记被简化为它们的基本形式。 例如,诸如演奏,演奏和演奏之类的单词具有一个基数:演奏。 词干的基本单词输出不必是词典中的单词。 而来自残词化的根词,也称为引理,将始终存在于字典中。
-
文本清除:大小写转换,更正拼写并删除停用词和其他不必要的单词。
给定文本文档的语料库,我们可以应用前面的步骤,然后获得构成语料库的单词的纯净词汇。 下一步是文本表示。 词袋(BoW)模型是从文本文档中提取特征并创建文本向量表示的最简单但功能最强大的技术之一。 如果我们在提取的词汇表中有N
个单词,则任何文档都可以表示为D = {w[1], w[2], ...
,其中w[i]
代表文档中单词出现的频率。 这种文本作为稀疏向量的表示称为 BoW 模型。 在这里,我们不考虑文本数据的顺序性质。 一种部分捕获顺序信息的方法是在构建词汇表时考虑单词短语或 n-gram 和单个单词特征。 但是,挑战之一是我们的代表人数。 也就是说,我们的词汇量爆炸了。
文档向量也可以表示为二元向量,其中每个w[i] ∈ {0, 1}
表示文档中单词的存在或不存在。 最受欢迎的表示形式是单词频率的归一化表示形式,称为词频-逆文档频率(TF-IDF)表示形式。 通过将我们语料库中的文档总数除以每个单词的文档频率,然后对结果应用对数缩放,可以计算出 IDF 表示的文档逆频率。 TF-IDF 值是词频与逆文档频率的乘积。 它与单词在文档中出现的次数成正比地增加,并根据语料库中单词的频率按比例缩小,这有助于调整某些单词通常更频繁出现的事实。
现在,我们都准备建立一个分类模型。 我们需要一套带有标签的文件或训练数据。 以下是一些流行的文本分类算法:
- 多项式朴素贝叶斯
- 支持向量机
- K 最近邻
具有线性核的支持向量机(SVM)与用于文本分类的基准数据集相比,通常显示出更高的准确率。
BoW 模型的缺点
使用基于单词计数的 BoW 模型,我们将丢失其他信息,例如每个文本文档中附近单词周围的语义,结构,序列和上下文。 在 BoW 中,具有相似含义的单词将得到不同的对待。 其他文本模型是潜在语义索引(LSI),其中文档以低维度(k 远小于词汇量)-隐藏的主题空间表示。 在 LSI 中,文档中的组成词也可以表示为k
维密集向量。 据观察,在 LSI 模型中,具有相似语义的单词具有紧密的表示形式。 而且,单词的这种密集表示是将深度学习模型应用于文本的第一步,被称为单词嵌入。 基于神经网络的语言模型试图通过查看语料库中的单词序列来预测其相邻单词的单词,并在此过程中学习分布式表示,从而使我们能够密集地嵌入单词。
基准数据集
以下是大多数文本分类研究中使用的基准数据集的列表:
- IMDB 电影评论数据集:这是用于二元情感分类的数据集。 它包含一组用于训练的 25,000 条电影评论和用于测试的 25,000 条电影。 也有其他未标记的数据可供使用。 该数据集可从这个链接下载。
- 路透数据集:此数据集包含 90 个类别,9,584 个训练文档和 3,744 个测试文档。 它是包
nltk.corpus
的一部分。 该数据集中文档的类分布非常不正确,其中两个最常见的类包含大约所有文档的 70%。 即使仅考虑 10 个最频繁的类,该数据集中的两个最频繁的类也拥有大约 80% 的文档。 因此,大多数分类结果都是在这些最常见的类别的子集上进行评估的,它们在训练集中的最常见的 8、10 和 52 类别分别命名为 R8,R10 和 R52。 - 20 个新闻组数据集:此数据被组织成 20 个不同的新闻组,每个新闻组对应一个不同的主题。 一些新闻组彼此之间有着非常密切的关联(例如:
comp.sys.ibm.pc.hardware
/comp.sys.mac.hardware
),而其他新闻组则是高度不相关的(例如:misc.forsale
/soc.religion.christian
)。 这是 20 个新闻组的列表,根据主题分为六个主要类别。 该数据集在sklearn.datasets
中可用:
comp.graphics ,comp.os.ms-windows.misc ,comp.sys.ibm.pc.hardware ,comp.sys.mac.hardware ,comp.windows.x |
rec.autos ,rec.motorcycles ,rec.sport.baseball ,rec.sport.hockey |
sci.crypt ,sci.electronics ,sci.med ,sci.space |
misc.forsale |
talk.politics.misc ,talk.politics.guns ,talk.politics.mideast |
talk.religion.misc ,alt.atheism ,soc.religion.christian |
稍后我们将讨论如何加载此数据集以进行进一步分析。
单词表示
让我们看一下这些用于处理文本数据并从中提取有意义的特征或单词嵌入的高级策略,这些策略可用于其他机器学习(ML)系统中,以执行更高级的任务,例如分类,摘要和翻译。 我们可以将学习到的单词表示形式转移到另一个模型中。 如果我们拥有大量的训练数据,则可以与最终任务一起共同学习单词嵌入。
Word2vec 模型
该模型由 Google 于 2013 年创建,是一种基于深度学习的预测模型,该模型可计算并生成高质量,分布式和连续密集的单词向量表示,从而捕获上下文和语义相似性。 从本质上讲,这些是无监督模型,可以吸收大量文本语料库,创建可能单词的词汇表,并为代表该词汇表的向量空间中的每个单词生成密集的单词嵌入。 通常,您可以指定单词嵌入向量的大小,向量的总数本质上是词汇表的大小。 这使得该密集向量空间的维数大大低于使用传统 BoW 模型构建的高维稀疏向量空间。
Word2vec 可以利用两种不同的模型架构来创建这些词嵌入表示。 这些是:
- 连续词袋(CBOW)模型
- SkipGram模型
CBOW 模型架构尝试根据源上下文单词(环绕单词)来预测当前的目标单词(中心单词)。 考虑一个简单的句子the quick brown fox jumps over the lazy dog
,这可以是一对context_window
和target_word
,如果我们考虑一个大小为 2 的上下文窗口,我们有一些例子,像([quick, fox], brown)
,([the, brown], quick)
,([the, dog], lazy)
,依此类推。 因此,该模型尝试根据上下文窗口词来预测目标词。 Word2vec 系列模型是不受监管的; 这意味着您可以给它一个语料库,而无需附加标签或信息,并且它可以从语料库构建密集的单词嵌入。 但是,一旦有了这个语料库,您仍然需要利用监督分类方法。 但是我们将在没有任何辅助信息的情况下从语料库内部进行操作。 我们可以将此 CBOW 架构建模为深度学习分类模型,以便将上下文词作为输入X
,并尝试预测目标词Y
。 实际上,构建这种架构比跳过语法模型更简单,在该模型中,我们尝试从源目标词预测一大堆上下文词。
跳过语法模型架构通常尝试实现与 CBOW 模型相反的功能。 它尝试在给定目标词(中心词)的情况下预测源上下文词(环绕词)。 考虑一下前面的简单句子the quick brown fox jumps over the lazy dog
。 如果我们使用 CBOW 模型,则会得到(context_window
和target_word
)对,其中,如果考虑大小为 2 的上下文窗口,则有示例,类似([quick, fox], brown)
,([the, brown], quick)
,([the, dog], lazy)
等。 现在,考虑到跳过语法模型的目的是根据目标单词预测上下文,该模型通常会反转上下文和目标,并尝试根据其目标单词预测每个上下文单词。
因此,任务变为给定目标单词brown
来预测上下文[quick, fox]
,或给定目标单词quick
来预测上下文[the, brown]
,依此类推。 因此,模型试图基于target_word
来预测context_window
单词。
以下是前两个模型的架构图:
我们可以在以下博客文章中找到这些模型在 Keras 中的实现。
使用 Gensim 的 Word2vec
Radim Rehurek 创建的 gensim 框架由 Word2vec 模型的可靠,高效且可扩展的实现组成。 它使我们可以选择跳跃语法模型或 CBOW 模型之一。 让我们尝试学习和可视化 IMDB 语料库的词嵌入。 如前所述,它有 50,000 个带标签的文档和 50,000 个无标签的文档。 对于学习单词表示,我们不需要任何标签,因此可以使用所有可用的 100,000 个文档。
首先加载完整的语料库。 下载的文档分为train
,test
和unsup
文件夹:
def load_imdb_data(directory = 'train', datafile = None):
'''
Parse IMDB review data sets from Dataset from
http://ai.stanford.edu/~amaas/data/sentiment/
and save to csv.
'''
labels = {'pos': 1, 'neg': 0}
df = pd.DataFrame()
for sentiment in ('pos', 'neg'):
path =r'{}/{}/{}'.format(config.IMDB_DATA, directory,
sentiment)
for review_file in os.listdir(path):
with open(os.path.join(path, review_file), 'r',
encoding= 'utf-8') as input_file:
review = input_file.read()
df = df.append([[utils.strip_html_tags(review),
labels[sentiment]]],
ignore_index=True)
df.columns = ['review', 'sentiment']
indices = df.index.tolist()
np.random.shuffle(indices)
indices = np.array(indices)
df = df.reindex(index=indices)
if datafile is not None:
df.to_csv(os.path.join(config.IMDB_DATA_CSV, datafile),
index=False)
return df
我们可以将所有三个数据源结合起来,得到 100,000 个文档的列表,如下所示:
corpus = unsupervised['review'].tolist() + train_df['review'].tolist()
+ test_df['review'].tolist()
我们可以对该语料进行预处理,并将每个文档转换为单词标记序列。 为此,我们使用nltk
。 然后,我们可以开始进行如下训练。 我们使用了大量的迭代,因此需要 6-8 个小时的时间来训练 CPU:
# tokenize sentences in corpus
wpt = nltk.WordPunctTokenizer()
tokenized_corpus = [wpt.tokenize(document.lower()) for document in corpus]
w2v_model = word2vec.Word2Vec(tokenized_corpus, size=50,
window=10, min_count=5,
sample=1e-3, iter=1000)
现在让我们看看该模型学到了什么。 让我们从这个语料库中选择一些见解的词。 可以在以下位置找到电影评论中通常使用的大量意见词。 我们首先将找到与这些给定单词具有相似嵌入的前五个单词。 以下是此代码:
similar_words = {search_term: [item[0] for item in w2v_model.wv.most_similar([search_term], topn=5)]
for search_term in ['good','superior','violent',
'romantic','nasty','unfortunate',
'predictable', 'hilarious',
'fascinating', 'boring','confused',
'sensitive',
'imaginative','senseless',
'bland','disappointing']}
pd.DataFrame(similar_words).transpose()
前面的代码的输出如下:
我们可以看到,学习到的嵌入表示具有相似嵌入向量的相似上下文中使用的单词。 这些词不必一直都是同义词,它们也可以相反。 但是,它们在类似的上下文中使用。
GloVe 模型
GloVe 模型代表全局向量,它是一种无监督的学习模型,可用于获取类似于 Word2Vec 的密集词向量。 但是,该技术不同,并且对聚合的全局单词-单词共现矩阵执行训练,从而为我们提供了具有有意义子结构的向量空间。 该方法发表在 Pennington 及其合作者的论文《GloVe:用于词表示的全局向量》。 我们已经讨论了基于计数的矩阵分解方法,例如潜在语义分析(LSA)和预测方法,例如 Word2vec。 本文声称,目前这两个家庭都遭受重大弊端。 像 LSA 之类的方法可以有效地利用统计信息,但是它们在词类比任务上的表现相对较差-我们是如何找到语义相似的词的。 像 skip-gram 这样的方法在类比任务上可能会做得更好,但它们在全局级别上却很少利用语料库的统计信息。
GloVe 模型的基本方法是首先创建一个庞大的单词-上下文共现矩阵,该矩阵由(单词,上下文)对组成,这样该矩阵中的每个元素都代表一个单词在上下文中出现的频率(可以是一个单词序列)。 这个词-语境矩阵WC
与在各种任务的文本分析中普遍使用的单词-文档矩阵非常相似。 矩阵分解用于将矩阵WC
表示为两个矩阵的乘积。 字特征(WF)矩阵和特征上下文(FC)矩阵。 WC = WF x FC
。 用一些随机权重初始化WF
和FC
,然后将它们相乘得到WC'
(近似于WC
)并测量与WC
有多近。 我们使用随机梯度下降(SGD)进行多次操作,以最大程度地减少误差。 最后,WF
矩阵为我们提供了每个单词的单词嵌入,其中F
可以预设为特定数量的维。 要记住的非常重要的一点是,Word2vec 和 GloVe 模型在工作方式上非常相似。 他们两个的目的都是建立一个向量空间,每个词的位置根据其上下文和语义而受到其相邻词的影响。 Word2vec 从单词共现对的本地单个示例开始,而 GloVe 从整个语料库中所有单词的全局汇总共现统计开始。
在以下各节中,我们将同时使用 Word2vec 和 GloVe 来解决各种分类问题。 我们已经开发了一些工具代码,可从文件读取和加载 GloVe 和 Word2vec 向量,并返回嵌入矩阵。 预期的文件格式是标准 GloVe 文件格式。 以下是几个单词的五维嵌入格式示例:单词后跟向量,所有空格分开:
- 甩动
7.068106 -5.410074 1.430083 -4.482612 -1.079401
- 心
-1.584336 4.421625 -12.552878 4.940779 -5.281123
- 侧面
0.461367 4.773087 -0.176744 8.251079 -11.168787
- 恐怖
7.324110 -9.026680 -0.616853 -4.993752 -4.057131
以下是读取 GloVe 向量的主要函数,给定一个词汇表作为 Python 字典,字典键作为词汇表中的单词。 仅需要为我们的训练词汇中出现的单词加载所需的嵌入。 同样,用所有嵌入的均值向量和一些白噪声来初始化 GloVe 嵌入中不存在的词汇。 0
和1
行专用于空格和语音外(OOV)单词。 这些单词不在词汇表中,而是在语料库中,例如非常少见的单词或一些过滤掉的杂音。 空间的嵌入是零向量。 OOV 的嵌入是所有其余嵌入的均值向量:
def _init_embedding_matrix(self, word_index_dict,
oov_words_file='OOV-Words.txt'):
# reserve 0, 1 index for empty and OOV
self.embedding_matrix = np.zeros((len(word_index_dict)+2 ,
self.EMBEDDING_DIM))
not_found_words=0
missing_word_index = []
with open(oov_words_file, 'w') as f:
for word, i in word_index_dict.items():
embedding_vector = self.embeddings_index.get(word)
if embedding_vector is not None:
# words not found in embedding index will be all-
zeros.
self.embedding_matrix[i] = embedding_vector
else:
not_found_words+=1
f.write(word + ','+str(i)+'\n')
missing_word_index.append(i)
#oov by average vector:
self.embedding_matrix[1] = np.mean(self.embedding_matrix,
axis=0)
for indx in missing_word_index:
self.embedding_matrix[indx] =
np.random.rand(self.EMBEDDING_DIM)+
self.embedding_matrix[1]
print("words not found in embeddings:
{}".format(not_found_words))
另一个工具函数是update_embeddings
。 这是转学的必要条件。 我们可能希望将一个模型学习的嵌入更新为另一模型学习的嵌入:
def update_embeddings(self, word_index_dict, other_embedding, other_word_index):
num_updated = 0
for word, i in other_word_index.items():
if word_index_dict.get(word) is not None:
embedding_vector = other_embedding[i]
this_vocab_word_indx = word_index_dict.get(word)
self.embedding_matrix[this_vocab_word_indx] =
embedding_vector
num_updated+=1
print('{} words are updated out of {}'.format(num_updated,
len(word_index_dict)))
CNN 文件模型
先前我们看到了词嵌入如何能够捕获它们表示的概念之间的许多语义关系。 现在,我们将介绍一个 ConvNet 文档模型,该模型可构建文档的分层分布式表示形式。 这发表在 Misha Denil 等人的论文中。 该模型分为两个级别,一个句子级别和一个文档级别,这两个级别都使用 ConvNets 实现。 在句子级别,使用 ConvNet 将每个句子中单词的嵌入转换为整个句子的嵌入。 在文档级别,另一个 ConvNet 用于将句子嵌入转换为文档嵌入。
在任何 ConvNet 架构中,卷积层之后都是子采样/池化层。 在这里,我们使用 k-max 池。 k-max 合并操作与正常 max 合并略有不同,后者从神经元的滑动窗口获取最大值。 在 k-max 合并操作中,最大的k
神经元取自下一层中的所有神经元。 例如,对[3, 1, 5, 2]
应用 2-max 合并将产生[3, 5]
。 在这里,内核大小为 3 且步幅为 1 的常规最大池将得到相同的结果。 让我们来考虑另一种情况。 如果我们对[1, 2, 3, 4, 5]
应用最大池,则将得到[3, 5]
,但是 2-max 池将给出[4, 5]
。 K-max 池可以应用于可变大小的输入,并且我们仍然可以获得相同数量的输出单元。
下图描述了卷积神经网络(CNN)架构。 我们已针对各种用例对该结构进行了一些微调,将在此处进行讨论:
此网络的输入层未在此处显示。 输入层按顺序是文档中的句子序列,其中每个句子由单词索引序列表示。 以下代码段描述了在给定训练语料库的情况下如何定义单词索引。 索引 0 和 1 保留用于空字和 OOV 字。 首先,将语料库中的文档标记为单词。 非英语单词被过滤掉。 同样,计算整个语料库中每个单词的频率。 对于大型语料库,我们可以从词汇表中过滤掉不常用的词。 然后,为词汇表中的每个单词分配一个整数索引:
from nltk.tokenize import sent_tokenize, wordpunct_tokenize
import re
corpus = ['The cat sat on the mat . It was a nice mat !',
'The rat sat on the mat . The mat was damaged found at 2 places.']
vocab ={}
word_index = {}
for doc in corpus:
for sentence in sent_tokenize(doc):
tokens = wordpunct_tokenize(sentence)
tokens = [token.lower().strip() for token in tokens]
tokens = [token for token in tokens
if re.match('^[a-z,.;!?]+$',token) is not None ]
for token in tokens:
vocab[token] = vocab.get(token, 0)+1
# i= 0 for empty, 1 for OOV
i = 2
for word, count in vocab.items():
word_index[word] = i
i +=1
print(word_index.items())
#Here is the output:
dict_items([('the', 2), ('cat', 3), ('sat', 4), ('on', 5), ('mat', 6), ('.', 7), ('it', 8), ('was', #9), ('a', 10), ('nice', 11), ('!', 12), ('rat', 13), ('damaged', 14), ('found', 15), ('at', 16), ('places', 17)])
现在,可以将语料库转换为单词索引数组。 在语料库中,不同的句子和文档的长度不同。 尽管卷积可以处理任意宽度的输入,但是为了简化实现,我们可以为网络定义一个固定大小的输入。 我们可以将短句子置零,并截断较长的句子以适应固定的句子长度,并在文档级别执行相同的操作。 在下面的代码片段中,我们显示了如何使用keras.preprocessing
模块对句子和文档进行零填充并准备数据:
from keras.preprocessing.sequence import pad_sequences
SENTENCE_LEN = 10; NUM_SENTENCES=3;
for doc in corpus:
doc2wordseq = []
sent_num =0
for sentence in sent_tokenize(doc):
words = wordpunct_tokenize(sentence)
words = [token.lower().strip() for token in words]
word_id_seq = [word_index[word] if word_index.get(word) is not
None \
else 1 for word in words]
padded_word_id_seq = pad_sequences([word_id_seq],
maxlen=SENTENCE_LEN,
padding='post',
truncating='post')
if sent_num < NUM_SENTENCES:
doc2wordseq = doc2wordseq + list(padded_word_id_seq[0])
doc2wordseq = pad_sequences([doc2wordseq],
maxlen=SENTENCE_LEN*NUM_SENTENCES,
padding='post',
truncating='post')
print(doc2wordseq)
# sample output
[ 2 3 4 5 2 6 7 0 0 0 8 9 10 11 6 12 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 2 13 4 5 2 6 7 0 0 0 2 6 9 14 15 16 1 17 7 0 0 0 0 0 0 0 0 0 0 0]
因此,您可以看到每个文档输入都是一个尺寸为doc_length = SENTENCE_LEN * NUM_SENTENCES
的一维张量。 这些张量通过网络的第一层,即嵌入层,以将单词索引转换为密集的单词表示形式,然后得到形状为doc_length×embedding_dimension
的二维张量。 所有先前的预处理代码都捆绑在Preprocess
类中,并具有fit
和transform
方法,例如scikit
模块。 fit
方法将训练语料库作为输入,构建词汇表,并为词汇表中的每个单词分配单词索引。 然后,可以使用transform
方法将测试或保留集转换为填充的单词索引序列,如先前所示。 transform
方法将使用由fit
计算的单词索引。
可以使用 GloVe 或 Word2vec 初始化嵌入矩阵。 在这里,我们使用了 50 维 GloVe 嵌入来初始化嵌入矩阵。 在 GloVe 和 OOV 单词中找不到的单词初始化如下:
- OOV 单词-训练数据词汇(索引 1)中排除的单词由所有 GloVe 向量的均值初始化
- 在 GloVe 中找不到的单词由所有 Glove 向量和相同维数的随机向量的均值初始化
以下代码段在之前讨论的 GloVe class
的_init_embedding_matrix
方法中具有相同的功能:
#oov by average vector:
self.embedding_matrix[1] = np.mean(self.embedding_matrix, axis=0)
for indx in missing_word_index:
self.embedding_matrix[indx] = np.random.rand(self.EMBEDDING_DIM)+
self.embedding_matrix[1]
初始化嵌入矩阵之后,我们现在可以构建第一层,即嵌入层,如下所示:
from keras.layers import Embedding
embedding_layer = Embedding(vocab_size,
embedding_dim,
weights=[embedding_weights],
input_length=max_seq_length,
trainable=True,
name='embedding')
接下来,我们必须构建单词卷积层。 我们希望在所有句子上应用相同的一维卷积滤波器,也就是说,在所有句子之间共享相同的卷积滤波器权重。 首先,我们使用Lambda
层将输入分成句子。 然后,如果我们使用 C 卷积滤波器,则每个句子的二维张量形状(SENTENCE_LEN×EMBEDDING _DIM
)将转换为(SENTENCE_LEN-filter + 1) × C
张量。 以下代码执行相同的操作:
#Let's take sentence_len=30, embedding_dim=50, num_sentences = 10
#following convolution filters to be used for all sentences.
word_conv_model = Conv1D(filters= 6,
kernel_size= 5,
padding="valid",
activation="relu",
trainable = True,
name = "word_conv",
strides=1)
for sent in range(num_sentences):
##get one sentence from the input document
sentence = Lambda(lambda x : x[:, sent*sentence_len:
(sent+1)*sentence_len, :])(z)
##sentence shape : (None, 30, 50)
conv = word_conv_model(sentence)
## convolution shape : (None, 26, 6)
k-max 合并层在keras
中不可用。 我们可以将 k-max 池实现为自定义层。 要实现自定义层,我们需要实现三种方法:
call(x)
:这是实现层的逻辑的地方compute_output_shape(input_shape)
:如果自定义层修改了其输入的形状build(input_shape)
:定义层权重(我们不需要此,因为我们的层没有权重)
这是 k-max 合并层的完整代码:
import tensorflow as tf
from keras.layers import Layer, InputSpec
class KMaxPooling(Layer):
def __init__(self, k=1, **kwargs):
super().__init__(**kwargs)
self.input_spec = InputSpec(ndim=3)
self.k = k
def compute_output_shape(self, input_shape):
return (input_shape[0], (input_shape[2] * self.k))
def call(self, inputs):
# swap last two dimensions since top_k will be
# applied along the last dimension
shifted_input = tf.transpose(inputs, [0, 2, 1])
# extract top_k, returns two tensors [values, indices]
top_k = tf.nn.top_k(shifted_input, k=self.k, sorted=True,
name=None)[0]
# return flattened output
return top_k
将前面的 k-max 池化层应用于单词卷积,我们得到句子嵌入层:
for sent in range(num_sentences):
##get one sentence from the input document
sentence = Lambda(lambda x : x[:,sent*sentence_len:
(sent+1)*sentence_len, :])(z)
##sentence shape : (None, 30, 50)
conv = word_conv_model(sentence)
## convolution shape : (None, 26, 6)
conv = KMaxPooling(k=3)(conv)
#transpose pooled values per sentence
conv = Reshape([word_filters*sent_k_maxpool,1])(conv)
## shape post k-max pooling and reshape (None, 18=6*3, 1)
因此,我们将形状为30×50
的每个句子转换为18×1
,然后将这些张量连接起来以获得句子嵌入。 我们使用 Keras 中的Concatenate
层来实现相同的功能:
z = Concatenate()(conv_blocks) if len(conv_blocks) > 1 else conv_blocks[0]
z = Permute([2,1], name='sentence_embeddings')(z)
## output shape of sentence embedding is : (None, 10, 18)
如前所述,对前一句子嵌入应用一维卷积,然后进行 k-max 合并,以获得文档嵌入。 这样就完成了文本的文档模型。 根据手头的学习任务,可以定义下一层。 对于分类任务,可以将文档嵌入连接到密集层,然后连接到具有K
单元的最终 softmax 层,以解决 k 类分类问题。 在最后一层之前,我们可以有多个致密层。 以下代码段实现了相同的功能:
sent_conv = Conv1D(filters=16,
kernel_size=3,
padding="valid",
activation="relu",
trainable = True,
name = 'sentence_conv',
strides=1)(z)
z = KMaxPooling(k=5)(sent_conv)
z = Flatten(name='document_embedding')(z)
for i in range(num_hidden_layers):
layer_name = 'hidden_{}'.format(i)
z = Dense(hidden_dims, activation=hidden_activation,
name=layer_name)(z)
model_output = Dense(K, activation='sigmoid',name='final')(z)
整个代码包含在cnn_document_model
模块中。
建立评论情感分类器
现在,通过训练前面的 CNN 文档模型来构建情感分类器。 我们将使用“亚马逊情感分析评论”数据集来训练该模型。 该数据集由数百万个 Amazon 客户评论(输入文本)和星级(输出标签)组成。 数据格式如下:标签,后跟空格,审阅标题,后跟:
和空格,位于审阅文本之前。 该数据集比流行的 IMDB 电影评论数据集大得多。 此外,此数据集包含各种产品和电影的相当多的评论集:
__label__<X> <summary/title>: <Review Text>
Example:
__label__2 Good Movie: Awesome.... simply awesome. I couldn't put this down and laughed, smiled, and even got tears! A brand new favorite author.
在此,__label__1
对应于 1 星和 2 星评论,__label__2
对应于 4 星和 5 星评论。 但是,此数据集中未包含三星级评论,即具有中性情感的评论。 在此数据集中,我们总共有 360 万个训练示例和 40 万个测试示例。 我们将从训练示例中随机抽取一个大小为 200,000 的样本,以便我们可以猜测一个很好的超参数来进行训练:
train_df = Loader.load_amazon_reviews('train')
print(train_df.shape)
test_df = Loader.load_amazon_reviews('test')
print(test_df.shape)
dataset = train_df.sample(n=200000, random_state=42)
dataset.sentiment.value_counts()
接下来,我们使用Preprocess
类将语料库转换为填充的单词索引序列,如下所示:
preprocessor = Preprocess()
corpus_to_seq = preprocessor.fit(corpus=corpus)
holdout_corpus = test_df['review'].values
holdout_target = test_df['sentiment'].values
holdout_corpus_to_seq = preprocessor.transform(holdout_corpus)
让我们使用GloVe
类用 GloVe 初始化嵌入,并构建文档模型。 我们还需要定义文档模型参数,例如卷积过滤器的数量,激活函数,隐藏单元等。 为了避免网络的过拟合,我们可以在输入层,卷积层甚至最终层或密集层之间插入丢弃层。 同样,正如我们在密集层所观察到的,放置高斯噪声层可作为更好的正则化器。 可以使用以下定义的所有这些参数初始化DocumentModel
类。 为了对模型参数进行良好的初始化,我们从少量的周期和少量的采样训练示例开始。 最初,我们开始使用六个词卷积过滤器(如针对 IMDB 数据的论文所述),然后发现该模型不适合-训练的精度未超过 80%,然后我们继续缓慢地增加词过滤器的数量 。 同样,我们发现了大量的句子卷积过滤器。 我们尝试了卷积层的 ReLU 和 tanh 激活。 如论文所述,他们将 tanh 激活用于其模型:
glove=GloVe(50)
initial_embeddings = glove.get_embedding(preprocessor.word_index)
amazon_review_model =
DocumentModel(vocab_size=preprocessor.get_vocab_size(),
word_index = preprocessor.word_index,
num_sentences = Preprocess.NUM_SENTENCES,
embedding_weights = initial_embeddings,
conv_activation = 'tanh',
hidden_dims=64,
input_dropout=0.40,
hidden_gaussian_noise_sd=0.5)
以下是此模型的参数的完整列表,我们已将其用于训练了 360 万个完整的训练示例:
{
"embedding_dim":50,
"train_embedding":true,
"sentence_len":30,
"num_sentences":10,
"word_kernel_size":5,
"word_filters":30,
"sent_kernel_size":5,
"sent_filters":16,
"sent_k_maxpool":3,
"input_dropout":0.4,
"doc_k_maxpool":4,
"sent_dropout":0,
"hidden_dims":64,
"conv_activation":"relu",
"hidden_activation":"relu",
"hidden_dropout":0,
"num_hidden_layers":1,
"hidden_gaussian_noise_sd":0.5,
"final_layer_kernel_regularizer":0.0,
"learn_word_conv":true,
"learn_sent_conv":true
}
最后,在开始全面训练之前,我们需要确定一个好的批次大小。 对于大批量(如 256),训练非常慢,因此我们使用了64
的批量。 我们使用rmsprop
优化器来训练我们的模型,并从keras
使用的默认学习率开始。 以下是训练参数的完整列表,它们存储在TrainingParameters
类中:
{"seed":55,
"batch_size":64,
"num_epochs":35,
"validation_split":0.05,
"optimizer":"rmsprop",
"learning_rate":0.001}
以下是开始训练的代码:
train_params = TrainingParameters('model_with_tanh_activation')
amazon_review_model.get_classification_model().compile(
loss="binary_crossentropy",
optimizer=
train_params.optimizer,
metrics=["accuracy"])
checkpointer = ModelCheckpoint(filepath=train_params.model_file_path,
verbose=1,
save_best_only=True,
save_weights_only=True)
x_train = np.array(corpus_to_seq)
y_train = np.array(target)
x_test = np.array(holdout_corpus_to_seq)
y_test = np.array(holdout_target)
amazon_review_model.get_classification_model().fit(x_train, y_train,
batch_size=train_params.batch_size,
epochs=train_params.num_epochs,
verbose=2,
validation_split=train_params.validation_split,
callbacks=[checkpointer])
我们已经在 CPU 上训练了该模型,下面是五个周期后的结果。 对于 190k 样本,只有一个周期非常慢,大约需要 10 分钟才能运行。 但是,您可以在下面看到,在五个周期之后的训练和验证准确率达到 92%,这是相当不错的:
Train on 190000 samples, validate on 10000 samples
Epoch 1/35
- 577s - loss: 0.3891 - acc: 0.8171 - val_loss: 0.2533 - val_acc: 0.8369
Epoch 2/35
- 614s - loss: 0.2618 - acc: 0.8928 - val_loss: 0.2198 - val_acc: 0.9137
Epoch 3/35
- 581s - loss: 0.2332 - acc: 0.9067 - val_loss: 0.2105 - val_acc: 0.9191
Epoch 4/35
- 640s - loss: 0.2197 - acc: 0.9128 - val_loss: 0.1998 - val_acc: 0.9206
Epoch 5/35
...
...
我们对 40 万条评论进行了评估,对模型进行了评估,结果的准确率也达到 92%。 这清楚地表明该模型非常适合此审阅数据,并且随着数据的增加,还有更多的改进空间。 到目前为止,在整个训练过程中,迁移学习的主要用途是用于初始化单词嵌入的 GloVe 嵌入向量。 在这里,由于我们拥有大量数据,因此我们可以从头开始学习权重。 但是,让我们看看在整个训练过程中,哪些词嵌入更新最多。
变化最大的嵌入是什么?
我们可以采用初始 GloVe 嵌入和最终学习的嵌入,并通过对每个单词的差异进行归一化来比较它们。 然后,我们可以对标准值进行排序,以查看哪些词变化最大。 这是执行此操作的代码:
learned_embeddings = amazon_review_model.get_classification_model()
.get_layer('embedding').get_weights()[0]
embd_change = {}
for word, i in preprocessor.word_index.items():
embd_change[word] = np.linalg.norm(initial_embeddings[i]-
learned_embeddings[i])
embd_change = sorted(embd_change.items(), key=lambda x: x[1],
reverse=True)
embd_change[0:20]
您可以检查是否最新的嵌入是针对意见词的。
迁移学习 – 应用到 IMDB 数据集
我们应该使用迁移学习的一种情况是,手头任务的标签数据少得多,而相似但不同的领域的训练数据很多。 IMDB 数据集是二元情感分类数据集。 它拥有 25,000 条用于训练的电影评论和 25,000 条用于测试的电影评论。 关于此数据集,有很多已发表的论文,并且可能通过来自 Google 的 Mikolov 的段落向量在此数据集上获得最佳结果。 他们在此数据集上实现了 92.58% 的准确率。 SVM 达到了 89%。 这个数据集的大小不错,我们可以从头开始训练 CNN 模型。 这为我们提供了与 SVM 相当的结果。 下一节将对此进行讨论。
现在,让我们尝试使用少量的 IMDB 数据样本(例如 5%)构建模型。 在许多实际情况下,我们面临训练数据不足的问题。 我们无法使用此小型数据集训练 CNN。 因此,我们将使用迁移学习为该数据集构建模型。
我们首先按照与其他数据集相同的步骤预处理和准备数据:
train_df = Loader.load_imdb_data(directory = 'train')
train_df = train_df.sample(frac=0.05, random_state = train_params.seed)
#take only 5%
print(train_df.shape)
test_df = Loader.load_imdb_data(directory = 'test')
print(test_df.shape)
corpus = train_df['review'].tolist()
target = train_df['sentiment'].tolist()
corpus, target = remove_empty_docs(corpus, target)
print(len(corpus))
preprocessor = Preprocess(corpus=corpus)
corpus_to_seq = preprocessor.fit()
test_corpus = test_df['review'].tolist()
test_target = test_df['sentiment'].tolist()
test_corpus, test_target = remove_empty_docs(test_corpus, test_target)
print(len(test_corpus))
test_corpus_to_seq = preprocessor.transform(test_corpus)
x_train = np.array(corpus_to_seq)
x_test = np.array(test_corpus_to_seq)
y_train = np.array(target)
y_test = np.array(test_target)
print(x_train.shape, y_train.shape)
glove=GloVe(50)
initial_embeddings = glove.get_embedding(preprocessor.word_index)
#IMDB MODEL
现在,让我们先加载训练后的模型。 我们有两种加载方法:模型的超参数和DocumentModel
类中学习的模型权重:
def load_model(file_name):
with open(file_name, "r", encoding= "utf-8") as hp_file:
model_params = json.load(hp_file)
doc_model = DocumentModel( **model_params)
print(model_params)
return doc_model
def load_model_weights(self, model_weights_filename):
self._model.load_weights(model_weights_filename, by_name=True)
然后,我们使用前述方法加载预训练的模型,然后按如下方法将学习到的权重转移到新模型中。 预训练模型的嵌入矩阵比语料库更大,单词更多。 因此,我们不能直接使用预训练模型中的嵌入矩阵。 我们将使用GloVe
类中的update_embedding
方法,使用经过训练的模型中的嵌入来更新 IMDB 模型的 GloVe 初始化的嵌入:
amazon_review_model = DocumentModel.load_model("model_file.json")
amazon_review_model.load_model_weights("model_weights.hdf5")
learned_embeddings = amazon_review_model.get_classification_model()\
.get_layer('embedding').get_weights()[0]
#update the GloVe embeddings.
glove.update_embeddings(preprocessor.word_index,
np.array(learned_embeddings),
amazon_review_model.word_index)
现在,我们都准备建立迁移学习模型。 让我们首先构建 IMDB 模型,然后从其他预训练模型初始化权重。 我们不会使用少量数据来训练该网络的较低层。 因此,我们将为其设置trainable=False
。 我们将仅训练具有较大丢弃法率的最后一层:
initial_embeddings = glove.get_embedding(preprocessor.word_index)#get
updated embeddings
imdb_model = DocumentModel(vocab_size=preprocessor.get_vocab_size(),
word_index = preprocessor.word_index,
num_sentences=Preprocess.NUM_SENTENCES,
embedding_weights=initial_embeddings,
conv_activation = 'tanh',
train_embedding = False,
learn_word_conv = False,
learn_sent_conv = False,
hidden_dims=64,
input_dropout=0.0,
hidden_layer_kernel_regularizer=0.001,
final_layer_kernel_regularizer=0.01)
#transfer word & sentence conv filters
for l_name in ['word_conv','sentence_conv','hidden_0', 'final']:
imdb_model.get_classification_model()\
.get_layer(l_name).set_weights(weights=amazon_review_model
.get_classification_model()
.get_layer(l_name).get_weights())
在经过几个周期的训练之后,仅对隐藏层和最终的 S 型层进行了微调,我们在 25k 测试集上获得了 86% 测试精度。 如果我们尝试在这个小的数据集上训练 SVM 模型并预测整个 25k 测试集,则只能获得 82% 的准确率。 因此,即使我们的数据较少,迁移学习显然也有助于建立更好的模型。
使用 Word2vec 嵌入来训练完整 IMDB 数据集
现在,让我们尝试通过迁移学习到的 Word2vec 嵌入,在完整的 IMDB 数据集上训练文档 CNN 模型。
请注意,我们没有使用从 Amazon Review 模型中学到的权重。 我们将从头开始训练模型。 实际上,这就是本文所做的。
此代码与前面的 IMDB 训练代码非常相似。 您只需要从 Amazon 模型中排除权重加载部分。 该代码位于存储库中名为imdb_model.py
的模块中。 另外,这是模型参数:
{
"embedding_dim":50,
"train_embedding":true,
"embedding_regularizer_l2":0.0,
"sentence_len":30,
"num_sentences":20,
"word_kernel_size":5,
"word_filters":30,
"sent_kernel_size":5,
"sent_filters":16,
"sent_k_maxpool":3,
"input_dropout":0.4,
"doc_k_maxpool":5,
"sent_dropout":0.2,
"hidden_dims":64,
"conv_activation":"relu",
"hidden_activation":"relu",
"hidden_dropout":0,
"num_hidden_layers":1,
"hidden_gaussian_noise_sd":0.3,
"final_layer_kernel_regularizer":0.04,
"hidden_layer_kernel_regularizer":0.0,
"learn_word_conv":true,
"learn_sent_conv":true,
"num_units_final_layer":1
}
训练时,我们使用了另一种技巧来避免过拟合。 我们在前 10 个时间段后冻结嵌入层(即train_embedding=False
),仅训练其余层。 经过 50 个周期后,我们在 IMDB 数据集上实现了 89% 的准确率,这是本文提出的结果。 我们观察到,如果我们在训练之前不初始化嵌入权重,则模型将开始过拟合,并且无法实现 80% 以上的准确率验证。
使用 CNN 模型创建文档摘要
评论有很多句子。 这些句子中的一些是中性的,而某些则是多余的,无法确定整个文档的极性。 总结评论或在评论中突出显示用户实际表达意见的句子非常有用。 实际上,它也为我们所做的预测提供了解释,从而使模型可以解释。
如本文所述,文本摘要的第一步是通过为每个句子分配重要性分数来为文档创建显着性图。 为了生成给定文档的显着性图,我们可以应用以下技术:
- 我们首先通过网络执行前向传递,以生成文档的类别预测。
- 然后,我们通过反转网络预测来构造伪标签。
- 将伪标签作为真实标签输入到训练损失函数中。 伪标签的这种选择使我们能够造成最大的损失。 反过来,这将使反向传播修改对决定类标签贡献最大的句子嵌入的权重。 因此,在实际上是肯定标签的情况下,如果我们将 0 作为伪标签传递,则强阳性语句嵌入应该会看到最大的变化,即高梯度范数。
- 计算损失函数相对于句子嵌入层的导数。
- 按梯度范数按降序对句子进行排序,以使最重要的句子排在顶部。
让我们在 Keras 中实现它。 我们必须像以前一样进行预处理,并获得x_train
和y_train
NumPy 数组。 我们将首先加载训练有素的 IMDB 模型和学习的权重。 然后,我们还需要使用用于从该模型中获取导数和损失函数的优化器来编译模型:
imdb_model = DocumentModel.load_model(config.MODEL_DIR+
'/imdb/model_02.json')
imdb_model.load_model_weights(config.MODEL_DIR+ '/imdb/model_02.hdf5')
model = imdb_model.get_classification_model()
model.compile(loss="binary_crossentropy", optimizer='rmsprop',
metrics=["accuracy"])
现在,让我们进行前面提到的步骤 1,即前向传递,然后生成伪标签:
preds = model.predict(x_train)
#invert predicted label
pseudo_label = np.subtract(1,preds)
为了计算梯度,我们将使用 Keras 函数model.optimizer.get_gradients()
:
#Get the learned sentence embeddings
sentence_ebd = imdb_model.get_sentence_model().predict(x_train)
input_tensors = [model.inputs[0], # input data
# how much to weight each sample by
model.sample_weights[0],
model.targets[0], # labels
]
#variable tensor at the sentence embedding layer
weights = imdb_model.get_sentence_model().outputs
#calculate gradient of the total model loss w.r.t
#the variables at sentence embd layer
gradients = model.optimizer.get_gradients(model.total_loss, weights)
get_gradients = K.function(inputs=input_tensors, outputs=gradients)
现在,我们可以计算出一个文档(例如,文档编号10
)的梯度,如下所示:
document_number = 10
K.set_learning_phase(0)
inputs = [[x_train[document_number]], # X
[1], # sample weights
[[pseudo_label[document_number][0]]], # y
]
grad = get_gradients(inputs)
现在,我们可以按梯度范数对句子进行排序。 我们将使用与预处理中使用的相同的nltk sent_tokenize
函数来获取文本句子:
sent_score = []
for i in range(Preprocess.NUM_SENTENCES):
sent_score.append((i, -np.linalg.norm(grad[0][0][i])))
sent_score.sort(key=lambda tup: tup[1])
summary_sentences = [ i for i, s in sent_score[:4]]
doc = corpus[document_number]
label = y_train[document_number]
prediction = preds[document_number]
print(doc, label , prediction)
sentences = sent_tokenize(doc)
for i in summary_sentences:
print(i, sentences[i])
以下是否定的评论:
Wow, what a great cast! Julia Roberts, John Cusack, Christopher Walken, Catherine Zeta-Jones, Hank Azaria...what's that? A script, you say? Now you're just being greedy! Surely such a charismatic bunch of thespians will weave such fetching tapestries of cinematic wonder that a script will be unnecessary? You'd think so, but no. America's Sweethearts is one missed opportunity after another. It's like everyone involved woke up before each day's writing/shooting/editing and though "You know what? I've been working pretty hard lately, and this is guaranteed to be a hit with all these big names, right? I'm just gonna cruise along and let somebody else carry the can." So much potential, yet so painful to sit through. There isn't a single aspect of this thing that doesn't suck. Even Julia's fat suit is lame.
从前两个句子看来,这是非常积极的。 我们对该文件的预测得分是 0.15,这是正确的。 让我们看看我们得到了什么总结:
4 Surely such a charismatic bunch of thespians will weave such
fetching tapestries of cinematic wonder that a script will be
unnecessary?
2 A script, you say?
6 America's Sweethearts is one missed opportunity after another.
让我们再举一个积极的例子,这里我们的模型预测为 0.98:
This is what I was expecting when star trek DS9 premiered. Not to slight DS9\. That was a wonderful show in it's own right, however it never really gave the fans more of what they wanted. Enterprise is that show. While having a similarity to the original trek it differs enough to be original in it's own ways. It makes the ideas of exploration exciting to us again. And that was one of the primary ingredients that made the original so loved. Another ingredient to success was the relationships that evolved between the crew members. Viewers really cared deeply for the crew. Enterprise has much promise in this area as well. The chemistry between Bakula and Blalock seems very promising. While sexual tension in a show can often become a crutch, I feel the tensions on enterprise can lead to much more and say alot more than is typical. I think when we deal with such grand scale characters of different races or species even, we get some very interesting ideas and television. Also, we should note the performances, Blalock is very convincing as Vulcan T'pol and Bacula really has a whimsy and strength of character that delivers a great performance. The rest of the cast delivered good performances also. My only gripes are as follows. The theme. It's good it's different, but a little to light hearted for my liking. We need something a little more grand. Doesn't have to be orchestral. Maybe something with a little more electronic sound would suffice. And my one other complaint. They sell too many adds. They could fix this by selling less ads, or making all shows two parters. Otherwise we'll end up seeing the shows final act getting wrapped up way too quickly as was one of my complaints of Voyager.
这是摘要:
2 That was a wonderful show in it's own right, however it never really
gave the fans more of what they wanted.
5 It makes the ideas of exploration exciting to us again.
6 And that was one of the primary ingredients that made the original
so loved.
8 Viewers really cared deeply for the crew.
您会看到它很好地掌握了摘要句子。 您真的不需要通过整个审查来理解它。 因此,此文本 CNN 模型可与 IMDB 数据集的最新模型相媲美,而且一旦学习,它就可以执行其他高级文本分析任务,例如文本摘要。
使用 CNN 模型进行多类分类
现在,我们将相同的模型应用于多类分类。 我们将为此使用 20 个新闻组数据集。 为了训练 CNN 模型,此数据集很小。 我们仍然会尝试解决一个更简单的问题。 如前所述,该数据集中的 20 个类有很多混合,使用 SVM,我们可以获得最高 70% 的准确率。 在这里,我们将采用该数据集的六大类,并尝试构建 CNN 分类器。 因此,首先我们将 20 个类别映射到 6 个大类别。 以下是首先从 scikit Learn 加载数据集的代码:
def load_20newsgroup_data(categories = None, subset='all'):
data = fetch_20newsgroups(subset=subset,
shuffle=True,
remove=('headers', 'footers', 'quotes'),
categories = categories)
return data
dataset = Loader.load_20newsgroup_data(subset='train')
corpus, labels = dataset.data, dataset.target
test_dataset = Loader.load_20newsgroup_data(subset='test')
test_corpus, test_labels = test_dataset.data, test_dataset.target
接下来,我们将 20 个类映射到六个类别,如下所示:
six_groups = {
'comp.graphics':0,'comp.os.ms-
windows.misc':0,'comp.sys.ibm.pc.hardware':0,
'comp.sys.mac.hardware':0, 'comp.windows.x':0,
'rec.autos':1, 'rec.motorcycles':1, 'rec.sport.baseball':1,
'rec.sport.hockey':1,
'sci.crypt':2, 'sci.electronics':2,'sci.med':2, 'sci.space':2,
'misc.forsale':3,
'talk.politics.misc':4, 'talk.politics.guns':4,
'talk.politics.mideast':4,
'talk.religion.misc':5, 'alt.atheism':5, 'soc.religion.christian':5
}
map_20_2_6 = [six_groups[dataset.target_names[i]] for i in range(20)]
labels = [six_groups[dataset.target_names[i]] for i in labels]
test_labels = [six_groups[dataset.target_names[i]] for i in
test_labels]
我们将执行相同的预处理步骤,然后进行模型初始化。 同样,在这里,我们使用了 GloVe 嵌入来初始化单词嵌入向量。 详细代码在20newsgrp_model
模块的存储库中。 这是模型的超参数:
{
"embedding_dim":50,
"train_embedding":false,
"embedding_regularizer_l2":0.0,
"sentence_len":30,
"num_sentences":10,
"word_kernel_size":5,
"word_filters":30,
"sent_kernel_size":5,
"sent_filters":20,
"sent_k_maxpool":3,
"input_dropout":0.2,
"doc_k_maxpool":4,
"sent_dropout":0.3,
"hidden_dims":64,
"conv_activation":"relu",
"hidden_activation":"relu",
"hidden_dropout":0,
"num_hidden_layers":2,
"hidden_gaussian_noise_sd":0.3,
"final_layer_kernel_regularizer":0.01,
"hidden_layer_kernel_regularizer":0.0,
"learn_word_conv":true,
"learn_sent_conv":true,
"num_units_final_layer":6
}
这是测试集上模型的详细结果:
precision recall f1-score support
0 0.80 0.91 0.85 1912
1 0.86 0.85 0.86 1534
2 0.75 0.79 0.77 1523
3 0.88 0.34 0.49 382
4 0.78 0.76 0.77 1027
5 0.84 0.79 0.82 940
avg / total 0.81 0.80 0.80 7318
[[1733 41 114 1 14 9]
[ 49 1302 110 11 47 15]
[ 159 63 1196 5 75 25]
[ 198 21 23 130 9 1]
[ 10 53 94 0 782 88]
[ 22 30 61 0 81 746]]
0.8047280677780815
让我们在此数据集上尝试 SVM,看看我们获得的最佳精度是多少:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import SVC
tv = TfidfVectorizer(use_idf=True, min_df=0.00005, max_df=1.0,
ngram_range=(1, 1), stop_words = 'english',
sublinear_tf=True)
tv_train_features = tv.fit_transform(corpus)
tv_test_features = tv.transform(test_corpus)
clf = SVC(C=1,kernel='linear', random_state=1, gamma=0.01)
svm=clf.fit(tv_train_features, labels)
preds_test = svm.predict(tv_test_features)
from sklearn.metrics import
classification_report,accuracy_score,confusion_matrix
print(classification_report(test_labels, preds_test))
print(confusion_matrix(test_labels, preds_test))
print(accuracy_score(test_labels, preds_test))
以下是 SVM 模型的结果。 我们已经对参数C
进行了调整,以便获得最佳的交叉验证精度:
precision recall f1-score support
0 0.86 0.89 0.87 1912
1 0.83 0.89 0.86 1534
2 0.75 0.78 0.76 1523
3 0.87 0.73 0.80 382
4 0.82 0.75 0.79 1027
5 0.85 0.76 0.80 940
avg / total 0.82 0.82 0.82 7318
0.82344902978956
因此,我们看到,在多类分类结果的情况下,本文的 CNN 模型也可以给出可比较的结果。 再次,和以前一样,现在也可以使用经过训练的模型来执行文本摘要。
可视化文档嵌入
在我们的文档 CNN 模型中,我们具有文档嵌入层。 让我们尝试可视化模型在这一层中学到的特征。 我们将首先获取测试集,并按如下方式计算文档嵌入:
doc_embeddings = newsgrp_model.get_document_model().predict(x_test)
print(doc_embeddings.shape)
(7318, 80)
我们为所有测试文档获得了 80 维嵌入向量。 为了可视化这些向量,我们将使用流行的 t-SNE 二维还原技术将向量投影到二维空间中,并绘制散点图,如下所示:
from utils import scatter_plot
doc_proj = TSNE(n_components=2, random_state=42,
).fit_transform(doc_embeddings)
f, ax, sc, txts = scatter_plot(doc_proj, np.array(test_labels))
前面代码的输出如下:
散点图上的标签(0-5)代表六个类别。 如您所见,该模型学习了不错的嵌入,并且能够在 80 维空间中很好地分离出六个类。 我们可以将这些嵌入用于其他文本分析任务,例如信息检索或文本搜索。 给定一个查询文档,我们可以计算其密集嵌入,然后将其与整个语料库中的相似嵌入进行比较。 这可以帮助我们提高基于关键字的查询结果并提高检索表现。
总结
我们已经从自然语言处理,文本分类,文本摘要以及深度学习 CNN 模型在文本域中的应用中学习了一些概念。 我们已经看到,在大多数用例中,尤其是如果我们的训练数据较少时,默认的第一步就是以词嵌入为基础的迁移学习。 我们已经看到了如何将迁移学习应用于在巨大的 Amazon 产品评论数据集上学习的文本 CNN 模型,以对小型电影评论数据集(相关但不相同的领域)进行预测。
此外,我们在这里还学习了如何将学习到的 CNN 模型用于其他文本处理任务,例如将文档汇总和表示为密集向量,这些信息可以在信息检索系统中使用,以提高检索表现。
八、音频事件识别与分类
在前面的章节中,我们已经研究了一些非常有趣的案例研究,这些案例将迁移学习应用于实际问题。 图像和文本数据是我们先前已解决的两种非结构化数据形式。 我们已经展示了各种方法来应用迁移学习来获得更强大和更出色的模型,以及解决诸如缺少训练数据之类的约束。 在本章中,我们将解决识别和分类音频事件的新现实问题。
为音频数据创建预训练的深度学习模型是一个巨大的挑战,因为我们没有高效的预训练的视觉模型(例如 VGG 或 Inception(适用于图像数据)或基于词嵌入的模型(如 Word2vec 或 GloVe)的优势) 文本数据)。 然后可能会出现一个问题,那就是我们对音频数据的策略是什么。 我们将在本章中探索一些创新方法,敬请期待! 本章将涵盖以下主要方面:
- 了解音频事件分类
- 制定我们的现实问题
- 探索性音频事件分析
- 特征工程和音频事件的表示
- 使用迁移学习的音频事件分类
- 构建深度学习音频事件识别器
在本章中,我们将研究识别和分类音频事件的实际案例研究。 诸如音频特征工程,转换学习,深度学习和面向对象编程等概念将用于构建健壮的,自动化的音频事件标识符以进行分类。 您可以在 GitHub 存储库中的Chapter 8
文件夹中快速阅读本章的代码。 可以根据需要参考本章。
了解音频事件分类
到现在为止,您应该了解分类或分类的基本任务,在这里我们已经有了结构化或非结构化的数据,这些数据通常用特定的组或类别进行标记或标注。 自动分类的主要任务是建立一个模型,以便使用未来的数据点,我们可以根据各种数据属性或特征将每个数据点分类或记录为一种特定的类别。
在前面的章节中,我们已经研究了文本和图像的分类。 在本章中,我们将研究对音频事件进行分类。 音频事件基本上是通常由音频信号捕获的事件或活动的发生。 通常,短的音频片段用于表示音频事件,因为即使它们反复出现,声音通常也很相似。 但是,有时,可能会使用更长的音频剪辑来表示更复杂的音频事件。 音频事件的示例可能是儿童在操场上玩耍,警笛警报,狗吠等。 实际上,谷歌已经建立了一个名为 AudioSet 的海量数据集,它是带标注的音频事件的一个手动的大规模数据集,他们还发表了几篇有关音频事件识别和分类的论文。 我们将使用较小的数据集来解决问题,但有兴趣的读者一定应该查看这个庞大的数据集,其中包含 632 个音频事件类,其中包括从 YouTube 视频中提取的 208420 个人工标记的 10 秒声音剪辑的集合。
制定我们的现实问题
我们这里的实际案例研究的主要目标是音频事件的识别和分类。 这是一个监督学习问题,我们将在音频事件数据集上使用属于特定类别(它们是声音的来源)的音频数据样本进行处理。
我们将利用迁移学习和深度学习中的概念来构建可靠的分类器,从而在任何给定音频样本属于我们预定类别之一的情况下,我们都应该能够正确预测该声音的来源。 我们将使用的数据集通常被称为 UrbanSound8K 数据集,并且具有 8,732 个带标签的音频声音文件(其持续时间通常等于或大于 4 秒),其中包含城市常见声音的摘录。 该数据集中的声音的十个类别如下:
-
air_conditioner
-
car_horn
-
children_playing
-
dog_bark
-
drilling
-
engine_idling
-
gun_shot
-
jackhammer
-
siren
-
streen_music
有关此数据集以及其他可能的数据集和计划的详细说明,我们建议读者访问 UrbanSound 网站,并查看创建者 J. Salamon,C。Jacoby 和 JP Bello 的这篇令人惊异的论文,《数据集和城市声音研究分类法》(22 届 ACM 国际多媒体国际会议,2014 年 11 月,美国奥兰多)。我们感谢他们,以及纽约大学城市科学与进步中心(CUSP), 现实。
要获取数据,您需要在其网站上填写表格,然后您将通过电子邮件获得下载链接。 解压缩文件后,您应该能够看到十个文件夹(十折)中的所有音频文件,以及一个包含有关数据集更多详细信息的readme
文件。
探索性音频事件分析
我们将遵循标准的工作流程,对音频数据进行模型的分析,可视化,建模和评估。 下载完所有数据后,您会注意到总共有十个文件夹包含WAV
格式的音频数据样本。 我们还有一个元数据文件夹,其中包含UrbanSound8K.csv
文件中每个音频文件的元数据信息。 您可以使用此文件为每个文件分配类标签,也可以了解文件命名术语以进行相同的操作。
每个音频文件都以特定格式命名。 该名称采用[fsID]-[classID]-[occurrenceID]-[sliceID].wav
格式,其格式如下:
[fsID]
:从中摘录该片段(片段)的录音的自由声音 ID[classID]
:声音类别的数字标识符[occurrenceID]
:数字标识符,用于区分原始录音中声音的不同出现[sliceID]
:数字标识符,用于区分同一事件中获取的不同片段
每个类标识符都是一个数字,可以映射到特定的类标签。 我们将在不久的将来对此进行更多的扩展。 让我们从对音频数据的一些基本探索性分析开始。 如果您想自己运行示例,可以从我们的 GitHub 存储库中引用Exploratory Analysis Sound Data.ipynb
Jupyter 笔记本。
首先,我们加载以下依赖项,包括librosa
模块,如果没有该模块,则可能需要安装:
import glob
import os
import librosa
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import specgram
import pandas as pd
import librosa.display
import IPython.display
import soundfile as sf
%matplotlib inline
librosa
模块是用于音频和音乐分析的出色的开源 Python 框架。 我们建议读者更详细地检查该框架。 在接下来的部分中,我们将使用它来分析音频数据并从中提取特征。 现在让我们加载一个数据文件夹进行分析:
files = glob.glob('UrbanSound8K/audio/fold1/*')
len(files)
873
我们可以看到每个文件夹大致包含 870 多个音频样本。 现在,基于metadata
和readme
文件的信息,我们可以创建一个类 ID,以名称映射音频样本类别:
class_map = {'0' : 'air_conditioner', '1' : 'car_horn',
'2' : 'children_playing', '3' : 'dog_bark',
'4' : 'drilling', '5' : 'engine_idling',
'6' : 'gun_shot', '7' : 'jackhammer',
'8' : 'siren', '9' : 'street_music'}
pd.DataFrame(sorted(list(class_map.items())))
现在让我们从属于这些类别的每个类别中抽取十个不同的音频样本,以进行进一步分析:
samples = [(class_map[label],
[f for f in files if f.split('-')[1] == label][0])
for label in class_map.keys()]
samples
[('street_music', 'UrbanSound8K/audio/fold1\108041-9-0-11.wav'),
('engine_idling', 'UrbanSound8K/audio/fold1\103258-5-0-0.wav'),
('jackhammer', 'UrbanSound8K/audio/fold1\103074-7-0-0.wav'),
('air_conditioner', 'UrbanSound8K/audio/fold1\127873-0-0-0.wav'),
('drilling', 'UrbanSound8K/audio/fold1\14113-4-0-0.wav'),
('children_playing', 'UrbanSound8K/audio/fold1\105415-2-0-1.wav'),
('gun_shot', 'UrbanSound8K/audio/fold1\102305-6-0-0.wav'),
('siren', 'UrbanSound8K/audio/fold1\106905-8-0-0.wav'),
('car_horn', 'UrbanSound8K/audio/fold1\156194-1-0-0.wav'),
('dog_bark', 'UrbanSound8K/audio/fold1\101415-3-0-2.wav')]
现在我们有了示例数据文件,在执行任何分析之前,我们仍然需要将音频数据读入内存。 我们注意到librosa
对某些音频文件抛出了错误(因为它们的长度或采样率很短)。 因此,我们利用soundfile
Python 框架读取音频文件,以获取其原始数据和原始采样率。 您可以在此处获取有关soundfile
框架的更多信息。
音频采样率定义为每秒传输的音频采样数,通常以 Hz 或 kHz(1 kHz 为 1,000 Hz)为单位。 librosa
的默认采样率为 22,050 Hz,这是我们将重新采样所有音频数据以保持一致性的方式。 以下代码可帮助我们读取数据,并显示原始音频数据的总长度:
def get_sound_data(path, sr=22050):
data, fsr = sf.read(path)
data_22k = librosa.resample(data.T, fsr, sr)
if len(data_22k.shape) > 1:
data_22k = np.average(data_22k, axis=0)
return data_22k, sr
sample_data = [(sample[0], get_sound_data(sample[1])) for sample in
samples]
[(sample[0], sample[1][0].shape) for sample in sample_data]
[('street_music', (88200,)), ('engine_idling', (88200,)),
('jackhammer', (88200,)), ('air_conditioner', (44982,)),
('drilling', (88200,)), ('children_playing', (88200,)),
('gun_shot', (57551,)), ('siren', (88200,)),
('car_horn', (5513,)), ('dog_bark', (88200,))]
很明显,大多数音频采样的持续时间约为四秒钟,但有些采样的持续时间却很短。 Jupyter 笔记本的魅力在于,您甚至可以将音频嵌入笔记本本身,并使用以下片段播放它。
对于sample_data
中的数据:
print(data[0], ':')
IPython.display.display(IPython.display.Audio(data=data[1[0],rate=data[ 1][1]))
这将创建以下内容:
现在让我们通过绘制它们的波形来形象化这些不同的音频源的外观。 通常,这将是每个音频样本的波形幅度图:
i = 1
fig = plt.figure(figsize=(15, 6))
for item in sample_data:
plt.subplot(2, 5, i)
librosa.display.waveplot(item[1][0], sr=item[1][1], color='r',
alpha=0.7)
plt.title(item[0])
i += 1
plt.tight_layout()
创建的图将如下所示:
您可以在上图中清楚地看到不同的音频数据样本及其源标签和相应的音频波形图。 这描绘了一些有趣的见解。 engine_idling
,jackhammer
和air_conditioner
等音源通常具有恒定的声音,不会随时间变化。 因此,您可以注意到波形中的振幅恒定。 siren
和car_horn
通常也具有恒定的音频波形,并具有间歇性的幅度增加。gun_shot
通常在开始时会发出很大的声音,然后保持沉默。 dog_bark
间歇地进入。 因此,除了静音以外,声音还具有短的高振幅间隔。 您还能找到更多有趣的模式吗?
音频数据的另一种有趣的可视化技术是声谱图。 通常,声谱图是一种视觉表示技术,用于表示音频数据中的频谱。 它们也被普遍称为超声检查仪和语音图。 让我们将音频样本可视化为频谱图:
i = 1
fig = plt.figure(figsize=(15, 6))
for item in sample_data:
plt.subplot(2, 5, i)
specgram(item[1][0], Fs=item[1][1])
plt.title(item[0])
i += 1
plt.tight_layout()
频谱图显示如下:
我们可以看到如何用频谱图将音频数据表示为很好的图像表示形式,这对于像卷积神经网络(CNN)这样的模型很有用,因为可以肯定地看到不同音频源在声谱图中存在明显差异。 但是,我们将使用梅尔谱图,它通常比基本谱图更好,因为它代表了梅尔刻度的谱图。 名称 mel 来自单词 melody。 这表明比例尺基于音高比较。 因此,梅尔音阶是对音高的感知尺度,听众已将其判断为彼此之间的距离相等。 如果我们使用 CNN 从这些频谱图中提取特征,这将非常有用。 以下代码段描绘了梅尔频谱图:
i = 1
fig = plt.figure(figsize=(15, 6))
for item in sample_data:
plt.subplot(2, 5, i)
S = librosa.feature.melspectrogram(item[1][0], sr=item[1]
[1],n_mels=128)
log_S = librosa.logamplitude(S)
librosa.display.specshow(log_S, sr=item[1][1],
x_axis='time',y_axis='mel')
plt.title(item[0])
plt.colorbar(format='%+02.0f dB')
i += 1
plt.tight_layout()
梅尔频谱图显示如下:
我们可以看到,借助梅尔音阶,可以更容易地根据音频源来区分频谱图。 现在,让我们集中讨论下一节中将用作特征工程基础资源的一些特定视觉技术。 首先,让我们看一下gun_shot
音频样本作为梅尔频谱图的样子:
y = sample_data[6][1][0]
S = librosa.feature.melspectrogram(y, sr=22050, n_mels=128)
log_S = librosa.logamplitude(S)
plt.figure(figsize=(12,4))
librosa.display.specshow(log_S, sr=22050, x_axis='time', y_axis='mel')
plt.colorbar(format='%+02.0f dB')
频谱图显示如下:
频谱图与该音频源的音频波形图一致。 音频的另一个有趣方面是,通常任何音频时间序列数据都可以分解为谐波和打击乐成分。 这些可以呈现任何音频样本的全新有趣的表示形式。 让我们获取这些组件并将它们绘制成频谱图:
y_harmonic, y_percussive = librosa.effects.hpss(y)
S_harmonic = librosa.feature.melspectrogram(y_harmonic,sr=22050,
n_mels=128)
S_percussive = librosa.feature.melspectrogram(y_percussive,sr=22050)
log_Sh = librosa.power_to_db(S_harmonic)
log_Sp = librosa.power_to_db(S_percussive)
# Make a new figure
plt.figure(figsize=(12,6))
plt.subplot(2,1,1)
librosa.display.specshow(log_Sh, sr=sr, y_axis='mel')
plt.title('mel power spectrogram (Harmonic)')
plt.colorbar(format='%+02.0f dB')
plt.subplot(2,1,2)
librosa.display.specshow(log_Sp, sr=sr, x_axis='time', y_axis='mel')
plt.title('mel power spectrogram (Percussive)')
plt.colorbar(format='%+02.0f dB')
plt.tight_layout()
频谱图将显示如下:
您可以看到音频样本的两个不同成分显示为两个独特的声谱图,分别描述了谐波成分和打击乐成分。
音频数据的另一个非常有趣的描述是使用一个色谱图,该图显示了基于十二种不同音高类别(即{C, C#, D, D#, E, F, F#, G, G#, A, A#, B}
。 这是用于描述音频信号随时间变化的各种音调强度的出色视觉工具。 通常,在构建色谱图之前,会对原始音频信号执行傅立叶变换或 Q 变换:
C = librosa.feature.chroma_cqt(y=y_harmonic, sr=sr)
# Make a new figure
plt.figure(figsize=(12, 4))
# Display the chromagram: the energy in each chromatic pitch class
# as a function of time
librosa.display.specshow(C, sr=sr, x_axis='time', y_axis='chroma',
vmin=0, vmax=1)
plt.title('Chromagram')
plt.colorbar()
plt.tight_layout()
色谱图将显示如下:
随着时间的推移,我们可以清楚地看到gun_shot
音频样本的各种音调强度,这对于作为特征提取的基础图像肯定是有效的。 在下一节中,我们将使用其中一些技术进行特征提取。
特征工程和音频事件的表示
要构建可靠的分类模型,我们需要从原始音频数据中获得可靠且良好的特征表示。 我们将利用上一节中学到的一些技术进行特征工程。 如果您想自己运行示例,可以在Feature Engineering.ipynb
Jupyter 笔记本中使用本节中使用的代码段。 我们将重用先前导入的所有库,并在此处利用joblib
将特征保存到磁盘:
from sklearn.externals import joblib
接下来,我们将加载所有文件名,并定义一些工具函数以读取音频数据,还使我们能够获取音频子样本的窗口索引,我们将在短期内利用它们:
# get all file names
ROOT_DIR = 'UrbanSound8K/audio/'
files = glob.glob(ROOT_DIR+'/**/*')
# load raw audio data
def get_sound_data(path, sr=22050):
data, fsr = sf.read(path)
data_resample = librosa.resample(data.T, fsr, sr)
if len(data_resample.shape) > 1:
data_resample = np.average(data_resample, axis=0)
return data_resample, sr
# function to get start and end indices for audio sub-sample
def windows(data, window_size):
start = 0
while start < len(data):
yield int(start), int(start + window_size)
start += (window_size / 2)
我们将遵循的特征工程策略有些复杂,但是我们将在此处尝试以简洁的方式对其进行说明。 我们已经看到我们的音频数据样本的长度不同。 但是,如果我们要构建一个强大的分类器,则每个样本的特征必须保持一致。 因此,我们将从每个音频文件中提取(固定长度的)音频子样本,并从每个这些子样本中提取特征。
我们将总共使用三种特征工程技术来构建三个特征表示图,这最终将为我们的每个音频子样本提供一个三维图像特征图。 下图描述了我们将采用的工作流程:
这个想法来自 Karol J. Piczak 的出色论文,《具有卷积神经网络的环境声音分类》(IEEE2015)。他将梅尔频谱图用于一般必要的特征,CNN 可以使用这些特征来进行特征提取。 但是,我们已经考虑了对最终特征图的一些其他转换。
第一步是将帧(列)的总数定义为 64 ,将波段(行)的总数定义为 64,这形成了每个特征图的尺寸(64 x 64
)。 然后,基于此,我们提取音频数据的窗口,从每个音频数据样本中形成子样本。
考虑每个音频子样本,我们首先创建一个梅尔声谱图。 由此,我们创建了一个对数缩放的梅尔频谱图,作为特征图之一,音频子样本的谐波分量和敲击分量的平均特征图(再次对数缩放),以及对数缩放的 mel 频谱图的增量或导数作为第三特征图。 这些特征图的每一个都可以表示为64 x 64
图像,并且通过组合它们,我们可以为每个音频子样本获得尺寸为(64, 64, 3)
的 3-D 特征图。 现在,为该工作流程定义函数:
def extract_features(file_names, bands=64, frames=64):
window_size = 512 * (frames - 1)
log_specgrams_full = []
log_specgrams_hp = []
class_labels = []
# for each audio sample
for fn in file_names:
file_name = fn.split('\')[-1]
class_label = file_name.split('-')[1]
sound_data, sr = get_sound_data(fn, sr=22050)
# for each audio signal sub-sample window of data
for (start,end) in windows(sound_data, window_size):
if(len(sound_data[start:end]) == window_size):
signal = sound_data[start:end]
# get the log-scaled mel-spectrogram
melspec_full = librosa.feature.melspectrogram(signal,
n_mels =
bands)
logspec_full = librosa.logamplitude(melspec_full)
logspec_full = logspec_full.T.flatten()[:,np.newaxis].T
# get the log-scaled, averaged values for the
# harmonic and percussive components
y_harmonic, y_percussive =librosa.effects.hpss(signal)
melspec_harmonic =
librosa.feature.melspectrogram(y_harmonic,
n_mels=bands)
melspec_percussive =
librosa.feature.melspectrogram(y_percussive,
n_mels=bands)
logspec_harmonic =
librosa.logamplitude(melspec_harmonic)
logspec_percussive =
librosa.logamplitude(melspec_percussive)
logspec_harmonic = logspec_harmonic.T.flatten()[:,
np.newaxis].T
logspec_percussive = logspec_percussive.T.flatten()[:,
np.newaxis].T
logspec_hp = np.average([logspec_harmonic,
logspec_percussive],
axis=0)
log_specgrams_full.append(logspec_full)
log_specgrams_hp.append(logspec_hp)
class_labels.append(class_label)
# create the first two feature maps
log_specgrams_full = np.asarray(log_specgrams_full).reshape(
len(log_specgrams_full), bands,
frames, 1)
log_specgrams_hp = np.asarray(log_specgrams_hp).reshape(
len(log_specgrams_hp), bands,
frames, 1)
features = np.concatenate((log_specgrams_full,
log_specgrams_hp,
np.zeros(np.shape(
log_specgrams_full))),
axis=3)
# create the third feature map which is the delta (derivative)
# of the log-scaled mel-spectrogram
for i in range(len(features)):
features[i, :, :, 2] = librosa.feature.delta(features[i,
:, :, 0])
return np.array(features), np.array(class_labels, dtype = np.int)
现在我们准备使用此函数。 我们将在前面的工作流程中讨论的策略基础上,将其用于所有 8,732 音频样本,以从该数据的许多子样本中创建特征图。
features, labels = extract_features(files)
features.shape, labels.shape
((30500, 64, 64, 3), (30500,))
我们从 8,732 个音频数据文件中总共获得了 30,500 个特征图。 这非常好,并且正如我们前面所讨论的,每个特征图都是尺寸(64, 64, 3)
。 现在,基于以下 30,500 个数据点,查看音频源的整体类表示形式:
from collections import Counter
Counter(labels)
Counter({0: 3993, 1: 913, 2: 3947, 3: 2912, 4: 3405,
5: 3910, 6: 336, 7: 3473, 8: 3611, 9: 4000})
我们可以看到,不同类别中数据点的总体分布是相当均匀和适当的。 对于诸如 1(car_horn
)和 6(gun_shot
)的某些类别,表示与其他类别相比非常低; 这是可以预期的,因为这些类别的音频数据持续时间通常比其他类别要短得多。 现在让我们继续可视化这些特征图:
class_map = {'0' : 'air_conditioner', '1' : 'car_horn', '2' :
'children_playing','3' : 'dog_bark', '4' : 'drilling','5' :
'engine_idling','6' : 'gun_shot', '7' : 'jackhammer', '8' :
'siren', '9' : 'street_music'}
categories = list(set(labels))
sample_idxs = [np.where(labels == label_id)[0][0] for label_id in
categories]
feature_samples = features[sample_idxs]
plt.figure(figsize=(16, 4))
for index, (feature_map, category) in enumerate(zip(feature_samples,
categories)):
plt.subplot(2, 5, index+1)
plt.imshow(np.concatenate((feature_map[:,:,0],
feature_map[:,:,1],
feature_map[:,:,2]),
axis=1),
cmap='viridis')
plt.title(class_map[str(category)])
plt.tight_layout()
t = plt.suptitle('Visualizing Feature Maps for Audio Clips')
特征图将显示如下:
上图向我们展示了每个音频类别的一些示例特征图看起来是什么样的,并且显而易见的是,每个特征图都是三维图像。 现在,我们将这些基本特征保存到磁盘:
joblib.dump(features, 'base_features.pkl')
joblib.dump(labels, 'dataset_labels.pkl')
这些基本特征将作为下一部分进一步特征设计的起点,在此我们将释放迁移学习的真正力量。
使用迁移学习的音频事件分类
现在,我们准备开始构建音频事件分类器。 我们有基本的特征图,但仍然需要做更多的特征工程。 您始终可以从头开始构建 CNN 以摄取这些图像,然后将其连接到完全连接的深多层感知器(MLP)来构建分类器。 但是,在这里,我们将通过使用一种预训练的模型进行特征提取来利用迁移学习的力量。 更具体地说,我们将使用 VGG-16 模型作为特征提取器,然后在这些特征上训练完全连接的深度网络。
从基本特征构建数据集
第一步是加载基本特征,并创建训练,验证和测试数据集。 为此,我们需要从磁盘加载基本特征和标签:
features = joblib.load('base_features.pkl')
labels = joblib.load('dataset_labels.pkl')
data = np.array(list(zip(features, labels)))
features.shape, labels.shape
((30500, 64, 64, 3), (30500,))
现在,我们将随机整理数据并创建训练,验证和测试数据集:
np.random.shuffle(data)
train, validate, test = np.split(data, [int(.6*len(data)),int(.8*len(data))])
train.shape, validate.shape, test.shape
((18300, 2), (6100, 2), (6100, 2))
最后,我们还可以使用以下代码段检查每个数据集中的每类分布:
print('Train:', Counter(item[1] for item in train),'nValidate:', Counter(item[1] for item in validate),'nTest:',Counter(item[1] for item
in test))
Train: Counter({9: 2448, 2: 2423, 0: 2378, 5: 2366, 8: 2140,
7: 2033, 4: 2020, 3: 1753, 1: 542, 6: 197})
Validate: Counter({0: 802, 5: 799, 2: 774, 9: 744, 8: 721,
7: 705, 4: 688, 3: 616, 1: 183, 6: 68})
Test: Counter({0: 813, 9: 808, 2: 750, 8: 750, 5: 745, 7: 735,
4: 697, 3: 543, 1: 188, 6: 71})
因此,我们可以看到整个数据集中每个类的数据点一致且均匀地分布。
迁移学习以进行特征提取
现在来了有趣的部分。 我们准备利用迁移学习从基本特征映射图中为每个数据点提取有用的特征。 为此,我们将使用出色的预训练深度学习模型,该模型已被证明是图像上非常有效的特征提取器。 我们将在这里使用 VGG-16 模型。 但是,我们将在这里使用它作为简单的特征提取器,而无需进行任何微调(这是我们在前几章中探讨的内容)。
随意利用微调,这甚至可以带来更好的分类器。 我们首先定义一些基本的工具和函数来处理基本图像:
from keras.preprocessing import image
from keras.applications.imagenet_utils import preprocess_input
from PIL import Image
def process_sound_data(data):
data = np.expand_dims(data, axis=0)
data = preprocess_input(data)
return data
现在,我们将加载 VGG-16 模型,但仅作为特征提取器。 因此,我们最终将不会使用其密集层:
from keras.applications import vgg16
from keras.models import Model
import keras
vgg = vgg16.VGG16(include_top=False, weights='imagenet',input_shape=
(64, 64, 3))
output = vgg.layers[-1].output
output = keras.layers.Flatten()(output)
model = Model(vgg.input, output)
model.trainable = False
model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) (None, 64, 64, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 64, 64, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 64, 64, 64) 36928
_________________________________________________________________
...
...
_________________________________________________________________
block5_conv3 (Conv2D) (None, 4, 4, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 2, 2, 512) 0
_________________________________________________________________
flatten_2 (Flatten) (None, 2048) 0
=================================================================
Total params: 14,714,688
Trainable params: 0
Non-trainable params: 14,714,688
_________________________________________________________________
从前面的模型摘要中可以明显看出,我们输入的基本特征图图像的尺寸为(64, 64, 3)
,从中我们最终将得到大小为 2,048 的一维特征向量。 让我们构建一个通用函数,以帮助我们利用迁移学习并获得这些特征,这些特征通常被称为瓶颈特征:
def extract_tl_features(model, base_feature_data):
dataset_tl_features = []
for index, feature_data in enumerate(base_feature_data):
if (index+1) % 1000 == 0:
print('Finished processing', index+1, 'sound feature maps')
pr_data = process_sound_data(feature_data)
tl_features = model.predict(pr_data)
tl_features = np.reshape(tl_features,
tl_features.shape[1])
dataset_tl_features.append(tl_features)
return np.array(dataset_tl_features)
现在可以将此函数与我们的 VGG-16 模型一起使用,以从我们的每个音频子样本基本特征图图像中提取有用的特征。 我们将对所有数据集执行此操作:
# extract train dataset features
train_base_features = [item[0] for item in train]
train_labels = np.array([item[1] for item in train])
train_tl_features = extract_tl_features(model=model,
base_feature_data=train_base_features)
# extract validation dataset features
validate_base_features = [item[0] for item in validate]
validate_labels = np.array([item[1] for item in validate])
validate_tl_features = extract_tl_features(model=model,
base_feature_data=validate_base_features)
# extract test dataset features
test_base_features = [item[0] for item in test]
test_labels = np.array([item[1] for item in test])
test_tl_features = extract_tl_features(model=model,
base_feature_data=test_base_features)
train_tl_features.shape, validate_tl_features.shape, test_tl_features.shape
((18300, 2048), (6100, 2048), (6100, 2048))
现在,我们可以将这些特征和标签保存到磁盘上,以便以后可以随时用于构建分类器,而不必依赖于始终保持笔记本计算机处于打开状态:
joblib.dump(train_tl_features, 'train_tl_features.pkl')
joblib.dump(train_labels, 'train_labels.pkl')
joblib.dump(validate_tl_features, 'validate_tl_features.pkl')
joblib.dump(validate_labels, 'validate_labels.pkl')
joblib.dump(test_tl_features, 'test_tl_features.pkl')
joblib.dump(test_labels, 'test_labels.pkl')
建立分类模型
现在,我们准备在上一节中提取的特征上构建分类模型。 如果您想自己运行示例,可以在Modeling.ipynb
Jupyter 笔记本中使用此部分的代码。 首先,让我们加载一些基本的依赖项:
from sklearn.externals import joblib
import keras
from keras import models
from keras import layers
import model_evaluation_utils as meu
import matplotlib.pyplot as plt
%matplotlib inline
我们将使用名为model_evaluation_utils
的漂亮模型评估工具模块来评估我们的分类器并稍后测试其表现。 现在让我们加载特征集和数据点类标签:
train_features = joblib.load('train_tl_features.pkl')
train_labels = joblib.load('train_labels.pkl')
validation_features = joblib.load('validate_tl_features.pkl')
validation_labels = joblib.load('validate_labels.pkl')
test_features = joblib.load('test_tl_features.pkl')
test_labels = joblib.load('test_labels.pkl')
train_features.shape, validation_features.shape, test_features.shape
((18300, 2048), (6100, 2048), (6100, 2048))
train_labels.shape, validation_labels.shape, test_labels.shape
((18300,), (6100,), (6100,))
因此,我们可以看到我们所有的特征集和相应的标签均已加载。 输入特征集是从上一节中使用的 VGG-16 模型获得的大小为 2,048 的一维向量。 现在,我们需要对分类类标签进行一次热编码,然后才能将其输入到深度学习模型中。 以下代码段可帮助我们实现这一目标:
from keras.utils import to_categorical
train_labels_ohe = to_categorical(train_labels)
validation_labels_ohe = to_categorical(validation_labels)
test_labels_ohe = to_categorical(test_labels)
train_labels_ohe.shape, validation_labels_ohe.shape, test_labels_ohe.shape
((18300, 10), (6100, 10), (6100, 10))
现在,我们将使用具有四个隐藏层的完全连接的网络来构建深度学习分类器。 我们将使用常见的组件(如丢弃法)来防止过拟合,并使用模型的 Adam 优化器。 以下代码描述了模型架构的详细信息:
model = models.Sequential()
model.add(layers.Dense(1024, activation='relu',
input_shape=(train_features.shape[1],)))
model.add(layers.Dropout(0.4))
model.add(layers.Dense(1024, activation='relu'))
model.add(layers.Dropout(0.4))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(train_labels_ohe.shape[1],activation='softmax'))
model.compile(loss='categorical_crossentropy',
optimizer='adam',metrics=['accuracy'])
model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1024) 2098176
_________________________________________________________________
dropout_1 (Dropout) (None, 1024) 0
_________________________________________________________________
dense_2 (Dense) (None, 1024) 1049600
_________________________________________________________________
dropout_2 (Dropout) (None, 1024) 0
_________________________________________________________________
dense_3 (Dense) (None, 512) 524800
_________________________________________________________________
dropout_3 (Dropout) (None, 512) 0
_________________________________________________________________
dense_4 (Dense) (None, 512) 262656
_________________________________________________________________
dropout_4 (Dropout) (None, 512) 0
_________________________________________________________________
dense_5 (Dense) (None, 10) 5130
=================================================================
Total params: 3,940,362
Trainable params: 3,940,362
Non-trainable params: 0
然后,在 AWS p2.x 实例上对该模型进行了约 50 个周期的训练,批量大小为 128。 您可以尝试使用时间和批量大小来获得可靠的模型,如下所示:
history = model.fit(train_features, train_labels_ohe,epochs=50,
batch_size=128,
validation_data=(validation_features,
validation_labels_ohe),shuffle=True, verbose=1)
Train on 18300 samples, validate on 6100 samples
Epoch 1/50
18300/18300 - 2s - loss: 2.7953 - acc: 0.3959 - val_loss: 1.0665 - val_acc: 0.6675
Epoch 2/50
18300/18300 - 1s - loss: 1.1606 - acc: 0.6211 - val_loss: 0.8179 - val_acc: 0.7444
...
...
Epoch 48/50
18300/18300 - 1s - loss: 0.2753 - acc: 0.9157 - val_loss: 0.4218 - val_acc: 0.8797
Epoch 49/50
18300/18300 - 1s - loss: 0.2813 - acc: 0.9142 - val_loss: 0.4220 - val_acc: 0.8810
Epoch 50/50
18300/18300 - 1s - loss: 0.2631 - acc: 0.9197 - val_loss: 0.3887 - val_acc: 0.8890
我们获得的验证准确率接近 89%,这非常好,看起来很有希望。 我们还可以绘制模型的整体精度图和损耗图,以更好地了解事物的外观,如下所示:
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
t = f.suptitle('Deep Neural Net Performance', fontsize=12)
f.subplots_adjust(top=0.85, wspace=0.2)
epochs = list(range(1,51))
ax1.plot(epochs, history.history['acc'], label='Train Accuracy')
ax1.plot(epochs, history.history['val_acc'], label='Validation Accuracy')
ax1.set_ylabel('Accuracy Value')
ax1.set_xlabel('Epoch')
ax1.set_title('Accuracy')
l1 = ax1.legend(loc="best")
ax2.plot(epochs, history.history['loss'], label='Train Loss')
ax2.plot(epochs, history.history['val_loss'], label='Validation Loss')
ax2.set_ylabel('Loss Value')
ax2.set_xlabel('Epoch')
ax2.set_title('Loss')
l2 = ax2.legend(loc="best")
这将创建以下图:
我们可以看到模型在训练和验证之间的损失和准确率是相当一致的。 也许略有过拟合,但考虑到它们之间的差异很小,可以忽略不计。
评估分类器表现
从字面上看,现在该对我们的模型进行测试了。 我们将使用测试数据集对模型进行预测,然后根据基本事实标签对它们进行评估。 为此,我们首先需要使用以下代码片段获取测试数据对模型的预测,并从数字标签到实际文本标签进行反向映射:
predictions = model.predict_classes(test_features)
class_map = {'0' : 'air_conditioner', '1' : 'car_horn',
'2' : 'children_playing', '3' : 'dog_bark',
'4' : 'drilling', '5' : 'engine_idling',
'6' : 'gun_shot', '7' : 'jackhammer',
'8' : 'siren', '9' : 'street_music'}
test_labels_categories = [class_map[str(label)]for label in
test_labels]
prediction_labels_categories = [class_map[str(label)]for label in
predictions] category_names = list(class_map.values())
现在让我们使用model_evaluation_utils
模块来根据测试数据评估模型的表现。 我们首先获得总体表现指标:
meu.get_metrics(true_labels=test_labels_categories,
predicted_labels=prediction_labels_categories)
Accuracy: 0.8869
Precision: 0.8864
Recall: 0.8869
F1 Score: 0.8861
我们获得了总体模型准确率,并且f1-score
接近 89%,这非常好,并且与我们从验证数据集中获得的一致。 接下来让我们看一下每类模型的表现:
meu.display_classification_report(true_labels=test_labels_categories,
predicted_labels=prediction_labels_categories,
classes=category_names)
precision recall f1-score support
car_horn 0.87 0.73 0.79 188
siren 0.95 0.94 0.94 750
drilling 0.88 0.93 0.90 697
gun_shot 0.94 0.94 0.94 71
children_playing 0.83 0.79 0.81 750
air_conditioner 0.89 0.94 0.92 813
jackhammer 0.92 0.93 0.92 735
engine_idling 0.94 0.95 0.95 745
dog_bark 0.87 0.83 0.85 543
street_music 0.81 0.81 0.81 808
avg / total 0.89 0.89 0.89 6100
这使我们可以更清楚地了解模型确实运行良好以及可能遇到问题的确切类。 大多数类似乎运行良好,尤其是设备声音,例如gun_shot
,jackhammer
和engine_idling
等。 似乎street_music
和children_playing
最麻烦。
混淆矩阵可以帮助我们了解最可能发生错误分类的地方,并帮助我们更好地理解这一点:
meu.display_confusion_matrix_pretty(true_labels=test_labels_categories,
predicted_labels=prediction_labels_categories,
classes=category_names)
矩阵将显示如下:
从矩阵的对角线看,我们可以看到大多数模型预测都是正确的,这非常好。 关于错误分类,我们可以看到,属于street_music
,dog_bark
和children_playing
的许多样本彼此之间都被错误分类了,考虑到所有这些事件都是在公开场合和外部发生的,一种预期的他们有可能一起发生。 对于drilling
和jackhammer
也是一样。 幸运的是,gun_shot
和children_playing
之间的错误分类几乎没有重叠。
因此,在这个复杂的案例研究中,我们可以看到有效的迁移学习是如何工作的,在该案例中,我们利用图像分类器帮助我们构建了强大而有效的音频事件分类器。 现在,我们可以使用以下代码保存此模型以供将来使用:
model.save('sound_classification_model.h5')
您现在可能会认为这很好。 但是,我们在静态数据集上进行了所有操作。 我们将如何在现实世界中使用此模型进行音频事件识别和分类? 我们将在下一节中讨论策略。
构建深度学习音频事件识别器
现在,我们将研究一种策略,利用该策略,我们可以利用上一节中构建的分类模型来构建实际的音频事件标识符。 这将使我们能够利用本章中定义的整个工作流程来获取任何新的音频文件,并预测该文件可能属于的类别,从构建基本特征图开始,使用 VGG-16 模型提取特征,然后利用我们的分类模型做出预测。 如果您想自己运行示例,可以在Prediction Pipeline.ipynb
Jupyter 笔记本中使用本节中使用的代码段。 笔记本包含AudioIdentifier
类,该类是通过重用本章前面各节中构建的所有组件而创建的。 请参阅笔记本以访问该类的完整代码,因为我们将更加关注实际的预测流水线,以使内容更加简洁。 我们将通过为类的实例提供分类模型的路径来初始化它:
ai =
AudioIdentifier(prediction_model_path='sound_classification_model.h5')
现在,我们已经下载了十个音频类别中的三个的三个全新的音频数据文件。 让我们加载它们,以便我们可以在它们上测试模型的表现:
siren_path = 'UrbanSound8K/test/sirenpolice.wav'
gunshot_path = 'UrbanSound8K/test/gunfight.wav'
dogbark_path = 'UrbanSound8K/test/dog_bark.wav'
siren_audio, siren_sr = ai.get_sound_data(siren_path)
gunshot_audio, gunshot_sr = ai.get_sound_data(gunshot_path)
dogbark_audio, dogbark_sr = ai.get_sound_data(dogbark_path)
actual_sounds = ['siren', 'gun_shot', 'dog_bark']
sound_data = [siren_audio, gunshot_audio, dogbark_audio]
sound_rate = [siren_sr, gunshot_sr, dogbark_sr]
sound_paths = [siren_path, gunshot_path, dogbark_path]
让我们可视化这三个音频文件的波形,并了解它们的结构:
i = 1
fig = plt.figure(figsize=(12, 3.5))
t = plt.suptitle('Visualizing Amplitude Waveforms for Audio Clips',
fontsize=14)
fig.subplots_adjust(top=0.8, wspace=0.2)
for sound_class, data, sr in zip(actual_sounds, sound_data,sound_rate):
plt.subplot(1, 3, i)
librosa.display.waveplot(data, sr=sr, color='r', alpha=0.7)
plt.title(sound_class)
i += 1
plt.tight_layout(pad=2.5)
可视化效果如下所示:
基于可视化,基于音频源,它们似乎是一致的,到目前为止,我们的流水线运行良好。 现在,我们为这些音频文件提取基本特征图:
siren_feature_map = ai.extract_base_features(siren_audio)[0]
gunshot_feature_map = ai.extract_base_features(gunshot_audio)[0]
dogbark_feature_map = ai.extract_base_features(dogbark_audio)[0]
feature_maps = [siren_feature_map, gunshot_feature_map,dogbark_feature_map]
plt.figure(figsize=(14, 3))
t = plt.suptitle('Visualizing Feature Maps for Audio
Clips',fontsize=14)
fig.subplots_adjust(top=0.8, wspace=0.1)
for index, (feature_map, category) in
enumerate(zip(feature_maps,actual_sounds)):
plt.subplot(1, 3, index+1)
plt.imshow(np.concatenate((feature_map[:,:,0],
feature_map[:,:,1],
feature_map[:,:,2]), axis=1),
cmap='viridis')
plt.title(category)
plt.tight_layout(pad=1.5)
特征图将显示如下:
根据我们在训练阶段观察到的图像,图像特征图看起来非常一致。 现在,我们可以利用我们的预测流水线来预测每种声音的音频源类别:
predictions =
[ai.prediction_pipeline(audiofile_path,return_class_label=True)
for audiofile_path in sound_paths]
result_df = pd.DataFrame({'Actual Sound': actual_sounds,
'Predicted Sound': predictions,
'Location': sound_paths})
result_df
我们得出以下预测:
看起来我们的模型能够正确识别所有这些音频样本。 我们鼓励您检查笔记本中的AudioIdentifier
类,以了解我们如何在后台实现预测流水线。 我们利用了在本章中学到的所有概念来构建此流水线。
总结
在本章中,我们研究了一个全新的问题和案例研究,涉及音频识别和分类。 涵盖了围绕音频数据和信号的概念,包括可视化和理解此数据类型的有效技术。
我们还研究了有效的特征工程技术,以及如何使用迁移学习从音频数据的图像表示中提取有效特征。 这向我们展示了迁移学习的希望,以及如何利用知识从一个领域(图像)迁移到另一个领域(音频),并建立一个非常强大且有效的分类器。 最后,我们建立了一个完整的端到端流水线,用于识别和分类音频数据的新样本。 请在网络上进一步检查带标注的音频的数据集,看看是否可以利用从此处学习的迁移学习中获得的概念来构建更大,更好的音频标识符和分类器。 请继续关注有关迁移学习的更多有趣示例和案例研究。
九、DeepDream
本章重点介绍了生成型深度学习的领域,这已成为真正的人工智能(AI)最前沿的核心思想之一。 我们将关注卷积神经网络(CNN)如何利用迁移学习来思考或可视化图像中的图案。 它们可以生成描述这些卷积网络思维甚至梦境方式之前从未见过的图像模式! DeepDream 于 2015 年由 Google 首次发布,由于深层网络开始从图像生成有趣的图案,因此引起了轰动。 本章将涵盖以下主要主题:
- 动机 — 心理幻觉
- 计算机视觉中的算法异同
- 通过可视化 CNN 的内部层来了解 CNN 所学的知识
- DeepDream 算法以及如何创建自己的梦境
就像前面的章节一样,我们将结合使用概念知识和直观的实际操作示例。 您可以在 GitHub 存储库中的Chapter 9
文件夹中快速阅读本章的代码。 可以根据需要参考本章。
介绍
在详细介绍神经 DeepDream 之前,让我们看一下人类所经历的类似行为。 您是否曾经尝试过寻找云中的形状,电视机中的抖动和嘈杂信号,甚至看过一张被烤面包烤成的面孔?
Pareidolia 是一种心理现象,使我们看到随机刺激中的模式。 人类倾向于感知实际上不存在的面孔或风格的趋势。 这通常导致将人的特征分配给对象。 请注意,看到不存在的模式(假阳性)相对于看不到存在的模式(假阴性)对进化结果的重要性。 例如,看到没有狮子的狮子很少会致命。 但是,没有看到有一只的掠食性狮子,那当然是致命的。
pareidolia 的神经学基础主要位于大脑深处的大脑颞叶区域,称为梭状回,在此区域,人类和其他动物的神经元专用于识别面部和其他物体。
计算机视觉中的算法异同
计算机视觉的主要任务之一是特别是对象检测和面部检测。 有许多具有面部检测功能的电子设备在后台运行此类算法并检测面部。 那么,当我们在这些软件的前面放置诱发 Pareidolia 的物体时会发生什么呢? 有时,这些软件解释面孔的方式与我们完全相同。 有时它可能与我们一致,有时它会引起我们全新的面貌。
在使用人工神经网络构建的对象识别系统的情况下,更高级别的特征/层对应于更易识别的特征,例如面部或物体。 增强这些特征可以带出计算机的视觉效果。 这些反映了网络以前看到的训练图像集。 让我们以 Inception 网络为例,让它预测一些诱发 Pareidolia 的图像中看到的物体。 让我们在下面的照片中拍摄这些三色堇花。 对我而言,这些花有时看起来像蝴蝶,有时又像愤怒的人,留着浓密的胡须的脸:
让我们看看 Inception 模型在其中的表现。 我们将使用在 ImageNet 数据上训练的预训练的 Inception 网络模型。 要加载模型,请使用以下代码:
from keras.applications import inception_v3
from keras import backend as K
from keras.applications.imagenet_utils import decode_predictions
from keras.preprocessing import image
K.set_learning_phase(0)
model = inception_v3.InceptionV3(weights='imagenet',include_top=True)
要读取图像文件并将其转换为一个图像的数据批,这是 Inception 网络模型的predict
函数的预期输入,我们使用以下函数:
def preprocess_image(image_path):
img = image.load_img(image_path)
img = image.img_to_array(img)
#convert single image to a batch with 1 image
img = np.expand_dims(img, axis=0)
img = inception_v3.preprocess_input(img)
return img
现在,让我们使用前面的方法预处理输入图像并预测模型看到的对象。 我们将使用modeld.predict
方法来获取 ImageNet 中所有 1,000 个类的预测类概率。 要将此概率数组转换为按概率得分的降序排列的实类标签,我们使用keras
中的decode_predictions
方法。 可在此处找到所有 1,000 个 ImageNet 类或同义词集的列表。 请注意,三色堇花不在训练模型的已知类集中:
img = preprocess_image(base_image_path)
preds = model.predict(img)
for n, label, prob in decode_predictions(preds)[0]:
print (label, prob)
的预测。 最高预测的类别都不具有很大的概率,这是可以预期的,因为模型之前没有看到过这种特殊的花朵:
bee 0.022255851
earthstar 0.018780833
sulphur_butterfly 0.015787734
daisy 0.013633176
cabbage_butterfly 0.012270376
在上一张照片中,模型找到蜜蜂。 好吧,这不是一个不好的猜测。 如您所见,在黄色的花朵中,中间的黑色/棕色阴影的下半部分确实像蜜蜂。 此外,它还会看到一些黄色和白色的蝴蝶,如硫和卷心菜蝴蝶,就像我们人类一眼就能看到的。 下图显示了这些已识别对象/类的实际图像。 显然,此输入激活了该网络中的某些特征检测器隐藏层。 也许检测昆虫/鸟类翅膀的过滤器与一些与颜色相关的过滤器一起被激活,以得出上述结论:
ImageNet 架构及其中的特征图数量很多。 让我们假设一下,我们知道可以检测这些机翼的特征映射层。 现在,给定输入图像,我们可以从这一层提取特征。 我们可以更改输入图像,以使来自该层的激活增加吗? 这意味着我们必须修改输入图像,以便在输入图像中看到更多类似机翼的物体,即使它们不在那里。 最终的图像将像梦一样,到处都是蝴蝶。 这正是 DeepDream 中完成的工作。
现在,让我们看一下 Inception 网络中的一些特征图。 要了解卷积模型学到的知识,我们可以尝试可视化卷积过滤器。
可视化特征图
可视化 CNN 模型涉及在给定一定输入的情况下,查看网络中各种卷积和池化层输出的中间层特征图。 这样就可以了解网络如何处理输入以及如何分层提取各种图像特征。 所有特征图都具有三个维度:宽度,高度和深度(通道)。 我们将尝试将它们可视化为 InceptionV3 模型。
让我们为拉布拉多犬拍摄以下照片,并尝试形象化各种特征图。 由于 InceptionV3 模型具有很深的深度,因此我们将仅可视化一些层:
首先,让我们创建一个模型以获取输入图像并输出所有内部激活层。 InceptionV3 中的激活层称为activation_i
。 因此,我们可以从加载的 Inception 模型中过滤掉激活层,如以下代码所示:
activation_layers = [ layer.output for layer in model.layers if
layer.name.startswith("activation_")]
layer_names = [ layer.name for layer in model.layers if
layer.name.startswith("activation_")]
现在,让我们创建一个模型,该模型获取输入图像并将所有上述激活层特征作为列表输出,如以下代码所示:
from keras.models import Model
activation_model = Model(inputs=model.input, outputs=activation_layers)
现在,要获得输出激活,我们可以使用predict
函数。 我们必须使用与先前定义的相同的预处理函数对图像进行预处理,然后再将其提供给 Inception 网络:
img = preprocess_image(base_image_path)
activations = activation_model.predict(img)
我们可以绘制这些先前的激活。 一个激活层中的所有过滤器/特征图都可以绘制在网格中。 因此,根据层中滤镜的数量,我们将图像网格定义为 NumPy 数组,如以下代码所示(以下代码的某些部分来自这里):
import matplotlib.pyplot as plt
images_per_row = 8
idx = 1 #activation layer index
layer_activation=activations[idx]
# This is the number of features in the feature map
n_features = layer_activation.shape[-1]
# The feature map has shape (1, size1, size2, n_features)
r = layer_activation.shape[1]
c = layer_activation.shape[2]
# We will tile the activation channels in this matrix
n_cols = n_features // images_per_row
display_grid = np.zeros((r * n_cols, images_per_row * c))
print(display_grid.shape)
现在,我们将遍历激活层中的所有特征映射,并将缩放后的输出放到网格中,如以下代码所示:
# We'll tile each filter into this big horizontal grid
for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0,:, :, col *
images_per_row + row]
# Post-process the feature to make it visually palatable
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0,
255).astype('uint8')
display_grid[col * r : (col + 1) * r,
row * c : (row + 1) * c] = channel_image
# Display the grid
scale = 1\. / r
plt.figure(figsize=(scale * display_grid.shape[1],
scale * display_grid.shape[0]))
plt.title(layer_names[idx]+" #filters="+str(n_features))
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')
以下是各层的输出:
前面的前两个激活层充当各种边缘检测器的集合。 这些激活保留了初始图片中几乎所有的信息。
让我们看下面的屏幕快照,它显示了网络中间的一层。 在这里,它开始识别更高级别的特征,例如鼻子,眼睛,舌头,嘴巴等:
随着我们的上移,地物图在视觉上的解释也越来越少。 较高层的激活会携带有关所看到的特定输入的最少信息,以及有关图像目标类别(在此情况下为狗)的更多信息。
可视化 InceptionV3 学习的过滤器的另一种方法是显示每个过滤器输出最大激活值的可视模式。 这可以通过输入空间中的梯度上升来完成。 基本上,通过使用图像空间中的梯度上升进行优化,找到使感兴趣的活动(层中神经元的激活)最大化的输入图像。 最终的输入图像将是所选过滤器最大程度地响应的输入图像。
每个激活层都有许多特征图。 以下代码演示了如何从最后一个激活层提取单个特征图。 这个激活值实际上是我们要最大化的损失:
layer_name = 'activation_94'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
要相对于此loss
函数计算输入图像的梯度,我们可以如下使用keras
后端梯度函数:
grads = K.gradients(loss, model.input)[0]
# We add 1e-5 before dividing so as to avoid accidentally dividing by
# 0.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
因此,给定一个激活层和一个可能是随机噪声的起始输入图像,我们可以使用上面的梯度计算应用梯度上升来获得特征图所表示的图案。 跟随generate_pattern
函数执行相同的操作。 归一化输出模式,以便我们在图像矩阵中具有可行的 RGB 值,这是通过使用deprocess_image
方法完成的。 以下代码是不言自明的,并具有内联注释来解释每一行:
def generate_pattern(layer_name, filter_index, size=150):
# Build a loss function that maximizes the activation
# of the nth filter of the layer considered.
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
# Compute the gradient of the input picture wrt this loss
grads = K.gradients(loss, model.input)[0]
# Normalization trick: we normalize the gradient
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
# This function returns the loss and grads given the input picture
iterate = K.function([model.input], [loss, grads])
# We start from a gray image with some noise
input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
# Run gradient ascent for 40 steps
step = 1.
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
img = input_img_data[0]
return deprocess_image(img)
def deprocess_image(x):
# normalize tensor: center on 0., ensure std is 0.1
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1
# clip to [0, 1]
x += 0.5
x = np.clip(x, 0, 1)
# convert to RGB array
x *= 255
x = np.clip(x, 0, 255).astype('uint8')
return x
以下屏幕截图是某些过滤器层的可视化。 第一层具有各种类型的点图案:
DeepDream
DeepDream 是一种艺术性的图像修改技术,它利用了以同名电影命名的深层 CNN 代码 Inception 所学习的表示形式。 我们可以拍摄任何输入图像并对其进行处理,以生成令人毛骨悚然的图片,其中充满了算法上的拟南芥伪像,鸟羽毛,狗似的面孔,狗眼-这是 DeepDream 修道院在 ImageNet 上接受过训练的事实,狗在这里繁殖,鸟类种类过多。
DeepDream 算法与使用梯度上升的 ConvNet 过滤器可视化技术几乎相同,不同之处在于:
- 在 DeepDream 中,最大程度地激活了整个层,而在可视化中,只最大化了一个特定的过滤器,因此将大量特征图的可视化混合在一起
- 我们不是从随机噪声输入开始,而是从现有图像开始; 因此,最终的可视化效果将修改先前存在的视觉模式,从而以某种艺术性的方式扭曲图像的元素
- 输入图像以不同的比例(称为八度)进行处理,从而提高了可视化效果的质量
现在,让我们修改上一部分中的可视化代码。 首先,我们必须更改loss
函数和梯度计算。 以下是执行相同操作的代码:
layer_name = 'activation_41'
activation = model.get_layer(layer_name).output
# We avoid border artifacts by only involving non-border pixels in the #loss.
scaling = K.prod(K.cast(K.shape(activation), 'float32'))
loss = K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling
# This tensor holds our generated image
dream = model.input
# Compute the gradients of the dream with regard to the loss.
grads = K.gradients(loss, dream)[0]
# Normalize gradients.
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
iterate_grad_ac_step = K.function([dream], [loss, grads])
第二个变化是输入图像,因此我们必须提供要在其上运行 DeepDream 算法的输入图像。 第三个变化是,我们没有在单个图像上应用梯度强调,而是创建了各种比例的输入图像并应用了梯度强调,如以下代码所示:
num_octave = 4 # Number of scales at which to run gradient ascent
octave_scale = 1.4 # Size ratio between scales
iterations = 20 # Number of ascent steps per scale
# If our loss gets larger than 10,
# we will interrupt the gradient ascent process, to avoid ugly
# artifacts
max_loss = 20.
base_image_path = 'Path to Image You Want to Use'
# Load the image into a Numpy array
img = preprocess_image(base_image_path)
print(img.shape)
# We prepare a list of shape tuples
# defining the different scales at which we will run gradient ascent
original_shape = img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
shape = tuple([int(dim / (octave_scale ** i)) for dim in
original_shape])
successive_shapes.append(shape)
# Reverse list of shapes, so that they are in increasing order
successive_shapes = successive_shapes[::-1]
# Resize the Numpy array of the image to our smallest scale
original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])
print(successive_shapes)
#Example Octaves for image of shape (1318, 1977)
[(480, 720), (672, 1008), (941, 1412), (1318, 1977)]
以下代码显示了 DeepDream 算法的一些工具函数。 函数deprocess_image
基本上是 InceptionV3 模型的预处理输入的逆运算符:
import scipy
def deprocess_image(x):
# Util function to convert a tensor into a valid image.
if K.image_data_format() == 'channels_first':
x = x.reshape((3, x.shape[2], x.shape[3]))
x = x.transpose((1, 2, 0))
else:
x = x.reshape((x.shape[1], x.shape[2], 3))
x /= 2.
x += 0.5
x *= 255.
x = np.clip(x, 0, 255).astype('uint8')
return x
def resize_img(img, size):
img = np.copy(img)
factors = (1,
float(size[0]) / img.shape[1],
float(size[1]) / img.shape[2],
1)
return scipy.ndimage.zoom(img, factors, order=1)
def save_img(img, fname):
pil_img = deprocess_image(np.copy(img))
scipy.misc. (fname, pil_img)
在每个连续的音阶上,从最小到最大的八度音程,我们都执行梯度上升以使该音阶上的先前定义的损耗最大化。 每次梯度爬升后,生成的图像将放大 40%。 在每个升级步骤中,一些图像细节都会丢失; 但是我们可以通过添加丢失的信息来恢复它,因为我们知道该比例的原始图像:
MAX_ITRN = 20
MAX_LOSS = 20
learning_rate = 0.01
for shape in successive_shapes:
print('Processing image shape', shape)
img = resize_img(img, shape)
img = gradient_ascent(img,
iterations=MAX_ITRN,
step=learning_rate,
max_loss=MAX_LOSS)
upscaled_shrunk_original_img = resize_img(shrunk_original_img,
shape)
same_size_original = resize_img(original_img, shape)
lost_detail = same_size_original - upscaled_shrunk_original_img
print('adding lost details', lost_detail.shape)
img += lost_detail
shrunk_original_img = resize_img(original_img, shape)
save_img(img, fname='dream_at_scale_' + str(shape) + '.png')
save_img(img, fname='final_dream.png')
示例
以下是 DeepDream 输出的一些示例:
- 在激活层 41 上运行梯度重音。这是我们之前看到的同一层,带有狗图像输入。 在下面的照片中,您可以看到一些动物从云层和蓝天中冒出来:
- 在激活层 45 上运行梯度重音。在下图中,您可以看到山上出现了一些类似狗的动物面孔:
- 在激活层 50 上运行梯度。在下面的照片中,您可以看到在蓝天白云下某些特殊的类似叶的图案梦:
生成这些梦境的原始图像在代码存储库中共享。
总结
在本章中,我们学习了计算机视觉中的算法稀疏。 我们已经解释了如何通过各种可视化技术来解释 CNN 模型,例如基于前向通过的激活可视化,基于梯度上升的过滤器可视化。 最后,我们介绍了 DeepDream 算法,该算法再次是对基于梯度上升的可视化技术的略微修改。 DeepDream 算法是将迁移学习应用于计算机视觉或图像处理任务的示例。
在下一章中,我们将看到更多类似的应用,它们将重点放在风格转换上。
风格迁移
绘画需要特殊技能,只有少数人已经掌握。 绘画呈现出内容和风格的复杂相互作用。 另一方面,照片是视角和光线的结合。 当两者结合时,结果是惊人的和令人惊讶的。 该过程称为艺术风格迁移。 以下是一个示例,其中输入图像是德国图宾根的 Neckarfront,风格图像是梵高着名的画作《星空》。 有趣,不是吗? 看一下以下图像:
左图:描绘德国蒂宾根 Neckarfront 的原始照片。 梵高的《星空》)。 来源:《一种艺术风格的神经算法》(Gatys 等人,arXiv:1508.06576v2)
如果您仔细查看前面的图像,则右侧的绘画风格图像似乎已经从左侧的照片中拾取了内容。 绘画的风格,颜色和笔触风格产生了最终结果。 令人着迷的结果是 Gatys 等人在论文《一种用于艺术风格的神经算法》中提出的一种迁移学习算法的结果。 我们将从实现的角度讨论本文的复杂性,并了解如何自己执行此技术。
在本章中,我们将专注于利用深度学习和传递学习来构建神经风格传递系统。 本章重点关注的领域包括:
- 了解神经风格转换
- 图像预处理方法
- 架构损失函数
- 构造自定义优化器
- 风格迁移实战
我们将涵盖有关神经风格迁移,损失函数和优化的理论概念。 除此之外,我们将使用动手方法来实现我们自己的神经风格转换模型。 本章的代码可在 GitHub 存储库的第 10 章文件夹中快速参考。 请根据需要参考本章。
了解神经风格转换
神经风格迁移是将参考图像的风格应用于特定目标图像的过程,以使目标图像的原始内容保持不变。 在这里,风格定义为参考图像中存在的颜色,图案和纹理,而内容定义为图像的整体结构和更高层次的组件。
在此,主要目的是保留原始目标图像的内容,同时在目标图像上叠加或采用参考图像的风格。 为了从数学上定义这个概念,请考虑三个图像:原始内容(表示为c
),参考风格(表示为s
)和生成的图像(表示为g
)。 我们需要一种方法来衡量在内容方面, c
和g
不同的图像的程度。 同样,就输出的风格特征而言,与风格图像相比,输出图像应具有较小的差异。 形式上,神经风格转换的目标函数可以表述为:
此处,α
和β
是用于控制内容和风格成分对整体损失的影响的权重。 此描述可以进一步简化,并表示如下:
在这里,我们可以根据前面的公式定义以下组件:
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
加载并了解我们系统执行的风格转换魔术!
我们将非常受欢迎的《指环王》电影中的以下图像用作我们的内容图像,并将基于花卉图案的精美艺术品用作我们的风格图像:
在以下代码段中,我们将在各种迭代之后加载生成的风格化图像:
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')
以下是显示原始图像和每五次迭代后生成的风格图像的输出:
以下是高分辨率的最终风格图像。 您可以清楚地看到花卉图案的纹理和风格是如何在原始《指环王》电影图像中慢慢传播的,并赋予了其良好的复古外观:
让我们再举一个风格迁移示例。 下图包含我们的内容图像,即来自黑豹的著名虚构的城市瓦卡达。 风格图片是梵高非常受欢迎的画作《星空》! 我们将在风格传递系统中将它们用作输入图像:
以下是高分辨率的最终风格图像,显示在下面的图像中。 您可以清楚地看到风格绘画中的纹理,边缘,颜色和图案如何传播到城市内容图像中:
天空和架构物采用了与您在绘画中可以观察到的非常相似的形式,但是内容图像的整体结构得以保留。 令人着迷,不是吗? 现在用您自己感兴趣的图像尝试一下!
总结
本章介绍了深度学习领域中一种非常新颖的技术,它利用了深度学习的力量来创造艺术! 确实,数据科学既是一门艺术,也是正确使用数据的科学,而创新则是推动这一发展的事物。 我们介绍了神经风格迁移的核心概念,如何使用有效的损失函数来表示和表达问题,以及如何利用迁移学习的力量和像 VGG-16 这样的预训练模型来提取正确的特征表示。
计算机视觉领域不断发展,深度学习与迁移学习相结合为创新和构建新颖的应用打开了大门。 本章中的示例应帮助您了解该领域的广泛新颖性,并使您能够走出去并尝试新技术,模型和方法来构建诸如神经风格转换的系统! 随之而来的是有关图像标题和着色的更有趣,更复杂的案例研究。 敬请关注!
十、自动图像字幕生成器
在前面的章节中,我们研究了一些案例研究,这些案例研究将迁移学习应用于计算机视觉以及自然语言处理(NLP)中的问题。 但是,这些都是它们各自特定领域中的问题。 在本章中,我们将专注于构建将这两个流行领域(计算机视觉和 NLP)结合在一起的智能系统。 更具体地说,我们将专注于构建与机器翻译相结合的对象识别系统,以构建自动图像字幕生成器。
图像字幕的想法并不是什么新鲜事物。 通常,存在于各种媒体资源(例如书籍,论文或社交媒体)中的任何图像通常都需要加上适当的文本说明,以获取更好的含义和上下文。 使这项任务变得艰巨的是,图像标题通常是由一个或多个句子组成的自由流动的自然语言。 因此,由于用于图像标题的文本数据的非结构化性质,这不是传统的图像分类问题。
可以通过结合使用计算机视觉领域专家的预训练模型(例如视觉几何组(VGG)和 Inception )以及序列模型(例如循环神经网络(RNN)或长短期记忆(LSTM)),以生成单词序列以形成图片说明的适当单词。 在本章中,我们将探索一种有趣的方法来构建自动图像字幕或场景识别系统。
我们将涵盖构建此系统的以下主要方面,该系统由深度学习和迁移学习提供支持:
- 了解图像字幕
- 制定目标
- 了解数据
- 自动图像字幕的方法
- 使用迁移学习的图像特征提取
- 为我们的字幕建立词汇表
- 构建图像标题数据集生成器
- 建立我们的图像字幕编解码器深度学习模型
- 训练我们的图像字幕深度学习模型
- 自动图像字幕实战
我们将涵盖计算机视觉和 NLP 的基本概念,以构建我们的自动图像标题生成器。 我们将深入研究适合的深度学习架构,并结合迁移学习,以在流行且易于使用的图像数据集之上实现该系统。 我们还将展示如何在新的照片和场景上构建和测试我们的自动图像标题生成器。 您可以在 GitHub 存储库中的Chapter 11
文件夹中快速阅读本章的代码。 可以根据需要参考本章。 我们还将在那里发布一些奖金示例。
了解图像字幕
到目前为止,您应该了解图像字幕的意义和含义。 该任务可以简单地定义为为任何图像编写和记录自由流动的自然文本描述。 通常用于描述图像中的各种场景或事件。 这也通常称为场景识别。 让我们看下面的例子:
看着这个场景,合适的标题或描述是什么? 以下是对场景的所有有效描述:
- 越野摩托车手在山上
- 一个家伙在山上的空中的自行车上
- 一辆越野车车手正在一条肮脏的道路上快速移动
- 骑自行车的人在空中骑黑摩托车
您会看到所有这些标题都是有效的并且相似,但是使用不同的词来传达相同的含义。 这就是为什么自动生成图像标题并非易事的原因。
实际上,流行论文《展示和演讲:神经图像字幕生成器》(Vinyals 及其合作者,2015 年)描述了图像字幕,从中我们汲取了构建此系统的灵感:
自动描述图像的内容是连接计算机视觉和自然语言处理的人工智能的基本问题。
对于一个人来说,只需瞥一眼照片或图像几秒钟就足以生成基于自然语言的字幕。 但是,由于大多数计算机视觉问题都集中在识别和分类问题上,因此使人工智能(AI)执行此任务极具挑战性。 就复杂性而言,这是有关核心计算机视觉问题的一些主要任务:
- 图像分类和识别:这涉及经典的有监督学习问题,其中主要目标是基于几个预定义的类类别(通常称为类标签)将图像分配给特定类别 。 流行的 ImageNet 竞赛就是这样一项任务。
- 图像标注:稍微复杂一点的任务,我们尝试使用图像中各个实体的描述来标注图像。 通常,这涉及图像中特定部分或区域的类别,甚至是基于自然语言的文本描述。
- 图像标题或场景识别:我们尝试使用准确的基于自然语言的文本描述来描述图像的另一项复杂任务。 这是本章重点关注的领域。
图像字幕的任务不是什么新鲜事。 已有多种利用技术的现有方法,例如将图像中各个实体的文本描述缝合在一起以形成描述,甚至使用基于模板的文本生成技术。 但是,对于该任务,使用深度学习是一种更强大,更有效的方法。
制定目标
我们实际案例研究的主要目标是图像字幕或场景识别。 在一定程度上,这是一个监督学习问题,而不是传统的分类问题。 在这里,我们将处理一个称为Flickr8K
的图像数据集,其中包含图像或场景的样本以及描述它们的相应自然语言标题。 这个想法是建立一个可以从这些图像中学习并自动开始为图像添加字幕的系统。
如前所述,传统的图像分类系统通常将图像分类或分类为预定义的类。 在前面的章节中,我们已经构建了这样的系统。 但是,图像字幕系统的输出通常是形成自然语言文本描述的单词序列; 这比传统的监督分类系统更加困难。
我们仍将监督模型训练的性质,因为我们将必须基于训练图像数据及其相应的字幕说明来构建模型。 但是,建立模型的方法会略有不同。 我们将利用迁移学习和深度学习中的概念照常构建此系统。 更具体地说,我们将结合使用深层卷积神经网络(DCNNs)和顺序模型。
了解数据
让我们看一下将用于构建模型的数据。 为简单起见,我们将使用Flickr8K
数据集。 该数据集包括从流行的图像共享网站 Flickr 获得的图像。 要下载数据集,可以通过填写以下伊利诺伊大学计算机科学系的表格来请求它,您应该在电子邮件中获取下载链接。
要查看与每个图像有关的详细信息,可以访问其网站,其中讨论了每个图像及其图像。 源,以及每个图像的五个基于文本的标题。 通常,任何样本图像都将具有类似于以下内容的标题:
您可以清楚地看到图像及其相应的标题。 很明显,所有标题都试图描述相同的图像或场景,但是它可能专注于图像的特定和不同方面,这使自动化成为一项艰巨的任务。 我们还建议读者查看《将图像描述作为排名任务:数据,模型和评估指标》(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 模型的输入,该模型将生成字幕说明(如单词序列)。 从原始论文中汲取灵感,可以用数学方式表示要最大化的目标,如下所示:
在此,Θ
表示模型参数,I
表示输入图像,S
是其相应的由单词序列组成的标题描述。 考虑到长度为N
的字幕说明,表示总共N
个字,我们可以对{S[0], S[1], ...,S[N]}
使用链式规则,如下所示:
因此,在模型训练期间,我们有一对(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》研究论文中的下图:
存储块包含 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]
获得的。 下图中描述了其中的一些定义和操作以及必要的方程式:
在这里,⊙
是乘积运算符,尤其用于当前的门状态和值。 W
矩阵是网络中的可训练参数。 这些门有助于解决诸如爆炸和消失梯度的问题。 网络中的非线性是由我们的常规 S 型σ
和双曲正切h
函数引入的。 如前所述,内存输出m[t]
被馈送到 softmax 以预测下一个单词,其中输出是所有单词上的概率分布。
因此,基于此知识,您可以考虑基于 LSTM 的序列模型需要与必要的词嵌入层和基于 CNN 的模型结合,以从源图像生成密集特征。 因此,LSTM 模型的目标是根据预测的所有先前单词以及输入图像(由我们先前的p(S[t] | I, S[0], S[1], ..., S[t-1])
。 为了简化 LSTM 中的循环连接,我们可以以展开形式来表示它,其中我们代表一系列 LSTM,它们共享下图所示的相同参数:
从上图可以明显看出,基于展开的 LSTM 架构,循环连接由蓝色水平箭头表示,并已转换为前馈连接。 同样,很明显,在时间t-1
处 LSTM 的输出m[t-1]
在时间t
被馈送到下一个 LSTM。 将源输入图像视为I
,将字幕视为S = {S[0], S[1], ..., S[N]}
,下图描述了先前描述的展开架构中涉及的主要操作:
在这里,标题中的每个文本单词都由单热门向量S[t]
表示,因此其尺寸等于我们词汇量(唯一的单词)。 另外要注意的一点是,我们为S[0]
设置了特殊的标记或分隔符,分别由<START>
和S[N]
表示,我们用<END>
来表示字幕的开头和结尾。 这有助于 LSTM 理解何时完全生成了字幕。
输入图像I
输入到我们的 DCNN 模型中,该模型生成密集特征向量,并将基于嵌入层的单词转换为密集单词嵌入W[eps]
。 因此,要最小化的整体损失函数是每个步骤右词的对数似然比,如以下等式所示:
因此,在模型训练期间,考虑模型中的所有参数(包括 DCNN,LSTM 和嵌入),可以将这种损失最小化。 现在让我们看一下如何将其付诸实践。
实用的实践方法
现在我们知道了可用于构建成功的图像字幕生成器的基本概念和理论,下面让我们看一下需要动手实践来解决此问题的主要构建块。 基于图像字幕的主要操作,要构建模型,我们将需要以下主要组件:
- 图像特征提取器 — 带迁移学习的 DCNN 模型
- 文本字幕生成器 - 使用 LSTM 的基于序列的语言模型
- 编解码器模型
在为字幕生成系统实现它们之前,让我们简要介绍一下这三个组件。
图像特征提取器 – 使用迁移学习的 DCNN 模型
我们系统的主要输入之一是源图像或照片。 我们都知道,机器学习(ML)或深度学习模型不能仅使用原始图像。 我们需要进行一些处理,还需要从图像中提取相关特征,然后将这些特征用于识别和分类等任务。
图像特征提取器本质上应该接收输入图像,从中提取丰富的层次特征表示,并以固定长度的密集向量的形式表示输出。 我们已经看到了 DCNN 在处理计算机视觉任务方面的强大功能。 在这里,我们将通过使用预训练的 VGG-16 模型作为特征提取器来从所有图像中提取瓶颈特征,从而充分利用迁移学习的力量。 就像快速刷新一样,下图显示了 VGG-16 模型:
为了进行特征提取,我们将删除模型的顶部,即 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)
前面代码的输出如下:
显然,每个输入图像都有五个标题,并且将其保留在数据集中。 现在,我们将这些数据集的记录和从迁移学习中学到的图像特征保存到磁盘上,以便我们可以在模型训练期间轻松地将其加载到内存中,而不必每次运行模型时都提取这些特征:
# 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 的密集特征向量表示。 现在的想法是构建一个模型数据集生成器,该生成器将生成(输入,输出)对。 对于我们的输入,我们将使用转换为密集特征向量的源图像以及相应的图像标题,在每个时间步添加一个单词。 对应的输出将是对应输入图像和标题的相同标题的下一个单词(必须预测)。 下图使此方法更加清晰:
基于此架构,很明显,对于同一图像,在每个时间步上,我们都传递相同的特征向量,并保持每次添加一个单词的标题,同时传递下一个要预测的单词作为相应的输出来训练我们的模型。 以下函数将帮助我们实现这一点,我们利用 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>']]
我们可以清楚地看到在<START>
符号后的每个步骤中如何将一个单词添加到输入标题,这表示文本标题的开始。 现在让我们看一下对应的下一单词生成输出(通常是根据两个输入预测的下一单词):
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']
很清楚,下一个单词通常基于输入字幕中每个时间步的单词顺序指向字幕中的下一个正确单词。 这些数据将在训练期间的每个周期馈入我们的模型。
建立我们的图像字幕编解码器深度学习模型
现在,我们拥有构建模型所需的所有基本组件和工具。 如前所述,我们将使用编码器-解码器深度学习模型架构来构建图像捕获系统。
以下代码帮助我们构建此模型的架构,在该模型中,我们将成对的图像特征和字幕序列作为输入,以预测每个时间步长的字幕中的下一个可能单词:
from keras.models import Sequential, Model
from keras.layers import LSTM, Embedding, TimeDistributed, Dense, RepeatVector, Activation, Flatten, concatenate
DENSE_DIM = 256
EMBEDDING_DIM = 256
MAX_CAPTION_SIZE = max_caption_size
VOCABULARY_SIZE = vocab_size
image_model = Sequential()
image_model.add(Dense(DENSE_DIM, input_dim=4096, activation='relu'))
image_model.add(RepeatVector(MAX_CAPTION_SIZE))
language_model = Sequential()
language_model.add(Embedding(VOCABULARY_SIZE, EMBEDDING_DIM, input_length=MAX_CAPTION_SIZE))
language_model.add(LSTM(256, return_sequences=True))
language_model.add(TimeDistributed(Dense(DENSE_DIM)))
merged_output = concatenate([image_model.output, language_model.output])
merged_output = LSTM(1024, return_sequences=False)(merged_output)
merged_output = (Dense(VOCABULARY_SIZE))(merged_output)
merged_output = Activation('softmax')(merged_output)
model = Model([image_model.input, language_model.input], merged_output)
model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
model.summary()
前面的代码的输出如下:
从前面的架构中我们可以看到,我们有一个图像模型,该模型更着重于处理基于图像的特征作为其输入,而语言模型则利用 LSTM 来处理每个图像标题中流入的单词序列。 最后一层是 softmax 层,具有 7,927 个单元,因为我们的词汇表中总共有 7,927 个唯一词,并且字幕中的下一个预测词将是其中一个作为输出生成的词。 我们还可以使用以下代码片段来可视化我们的模型架构:
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
SVG(model_to_dot(model, show_shapes=True, show_layer_names=False,
rankdir='TB').create(prog='dot', format='svg'))
前面的代码的输出如下:
训练我们的图像字幕深度学习模型
在开始训练模型之前,由于我们正在处理模型中的一些复杂组件,因此在模型的准确率在整个连续周期中都达到稳定状态的情况下,我们会在模型中使用回调来降低学习率。 这对于在不停止训练的情况下即时更改模型的学习率非常有帮助:
from keras.callbacks import ReduceLROnPlateau
reduce_lr = ReduceLROnPlateau(monitor='loss', factor=0.15,
patience=2, min_lr=0.000005)
让我们现在训练我们的模型! 我们已经将模型训练到大约 30 到 50 个周期,并在大约 30 个周期和 50 个周期保存了模型:
BATCH_SIZE = 256
EPOCHS = 30
cap_lens = [(cl-1) for cl in tc_tokens_length]
total_size = sum(cap_lens)
history = model.fit_generator(
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),
steps_per_epoch=int(total_size/BATCH_SIZE),
callbacks=[reduce_lr],
epochs=EPOCHS, verbose=1)
Epoch 1/30
1617/1617 - 724s 448ms/step - loss: 4.1236 - acc: 0.2823
Epoch 2/30
1617/1617 - 725s 448ms/step - loss: 3.9182 - acc: 0.3150
Epoch 3/30
1617/1617 - 724s 448ms/step - loss: 3.8286 - acc: 0.3281
...
...
Epoch 29/30
1617/1617 - 724s 447ms/step - loss: 3.6443 - acc: 0.3885
Epoch 30/30
1617/1617 - 724s 448ms/step - loss: 3.4656 - acc: 0.4078
model.save('ic_model_rmsprop_b256ep30.h5')
保存该模型后,我们将继续训练并对其进行另外 20 个周期的训练,并在50
处停止。 当然,您也可以随意在 Keras 中使用模型检查点定期自动保存它:
EPOCHS = 50
history_rest = model.fit_generator(
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),
steps_per_epoch=int(total_size/BATCH_SIZE),
callbacks=[reduce_lr],
epochs=EPOCHS, verbose=1, initial_epoch=30)
Epoch 31/50
1617/1617 - 724s 447ms/step - loss: 3.3988 - acc: 0.4144
Epoch 32/50
1617/1617 - 724s 448ms/step - loss: 3.3633 - acc: 0.4184
...
...
Epoch 49/50
1617/1617 - 724s 448ms/step - loss: 3.1330 - acc: 0.4509
Epoch 50/50
1617/1617 - 724s 448ms/step - loss: 3.1260 - acc: 0.4523
model.save('ic_model_rmsprop_b256ep50.h5')
这样就结束了我们的模型训练过程; 我们已经成功地训练了图像字幕模型,并可以开始使用它来为新图像生成图像字幕。
模型训练技巧:图像字幕模型通常使用大量数据,并且在训练过程中涉及许多参数。 建议使用生成器来构建和生成数据以训练深度学习模型。 否则,您可能会遇到内存问题。 另外,在带有 Tesla K80 GPU 的 Amazon AWS p2.x 实例上,该模型在每个周期运行将近 12 分钟,因此请考虑在 GPU 上构建该模型,因为在传统系统上进行训练可能会花费很长时间。
我们还可以根据训练过程中的不同周期,查看有关模型准确率,损失和学习率的趋势:
epochs = list(range(1,51))
losses = history.history['loss'] + history_rest.history['loss']
accs = history.history['acc'] + history_rest.history['acc']
lrs = history.history['lr'] + history_rest.history['lr']
f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14, 4))
title = f.suptitle("Model Training History", fontsize=14)
f.subplots_adjust(top=0.85, wspace=0.35)
ax1.plot(epochs, losses, label='Loss')
ax2.plot(epochs, accs, label='Accuracy')
ax3.plot(epochs, lrs, label='Learning Rate')
ax1.set_xlabel('Epochs')
ax2.set_xlabel('Epochs')
ax3.set_xlabel('Epochs')
ax1.set_ylabel('Loss')
ax2.set_ylabel('Accuracy')
ax3.set_ylabel('Learning Rate')
前面的代码的输出如下:
我们可以看到,在第 28 和 29 阶段,准确率略有下降,损失增加了,这导致我们的回调成功降低了学习率,从第 30 阶段开始提高了准确率。 这无疑为我们提供了有关模型行为的有用见解!
评估我们的图像字幕深度学习模型
训练模型而不评估其表现根本没有任何意义。 因此,我们现在将在测试数据集上评估深度学习模型的表现,该数据集与Flickr8K
数据集共有 1000 幅不同的图像。 我们从加载通常的依赖关系开始(如果您还没有的话):
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
pd.options.display.max_colwidth = 500
%matplotlib inline
加载数据和模型
下一步包括将必要的数据,模型和其他资产从磁盘加载到内存中。 我们首先加载测试数据集和训练有素的深度学习模型:
# load test dataset
test_df = pd.read_csv('image_test_dataset.tsv', delimiter='\t')
# load the models
from keras.models import load_model
model1 = load_model('ic_model_rmsprop_b256ep30.h5')
model2 = load_model('ic_model_rmsprop_b256ep50.h5')
现在,我们需要加载必要的元数据资产,例如之前为测试数据提取的图像特征以及词汇元数据:
from sklearn.externals import joblib
tl_img_feature_map = joblib.load('transfer_learn_img_features.pkl')
vocab_metadata = joblib.load('vocabulary_metadata.pkl')
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']
了解贪婪和集束搜索
为了从基于深度学习的神经图像字幕模型生成预测,请记住,它不像基本分类或分类模型那样简单。 我们将需要根据输入图像特征在每个时间步从模型中生成一系列单词。 有多种方式为字幕生成这些单词序列。
一种方法称为采样或贪婪搜索,我们从<START>
令牌开始,输入图像特征,然后基于 LSTM 输出中的p1
来生成第一个单词。 然后,我们将相应的预测词嵌入作为输入,并根据来自下一个 LSTM 的p2
生成下一个词(以我们之前讨论的展开形式)。 继续此步骤,直到到达<END>
令牌(表示字幕结束)为止,或者达到基于预定义阈值的令牌的最大可能长度。
第二种方法称为集束搜索,它比基于贪婪的搜索更有效,在基于贪婪的搜索中,我们在考虑到每个单词之前生成的单词的基础上,根据最高概率在每个步骤中选择最可能的单词顺序,这正是采样的作用。 集束搜索扩展了贪婪搜索技术,并始终返回最可能输出的项的序列的列表。 因此,在构建每个序列时,为了在时间步t+1
生成下一项,而不是进行贪婪搜索并生成最可能的下一项,迭代地考虑了一组k
最佳句子基于下一步扩展到所有可能的下一词。 k
的值通常是用户指定的参数,用于控制进行平行搜索或集束搜索以生成字幕序列的总数。 因此,在集束搜索中,我们以k
最可能的单词作为字幕序列中的第一时间步输出开始,并继续生成下一个序列项,直到其中一个达到结束状态为止。 涵盖围绕集束搜索的详细概念的全部范围将不在当前范围之内,因此,如果您有兴趣,我们建议您查看有关 AI 上下文中集束搜索的任何标准文献。
实现基于集束搜索的字幕生成器
现在,我们将实现一种基于集束搜索的基本算法来生成字幕序列:
from keras.preprocessing import image, sequence
def get_raw_caption_sequences(model, word_to_index, image_features,
max_caption_size, beam_size=1):
start = [word_to_index['<START>']]
caption_seqs = [[start, 0.0]]
while len(caption_seqs[0][0]) < max_caption_size:
temp_caption_seqs = []
for caption_seq in caption_seqs:
partial_caption_seq = sequence.pad_sequences(
[caption_seq[0]],
maxlen=max_caption_size,
padding='post')
next_words_pred = model.predict(
[np.asarray([image_features]),
np.asarray(partial_caption_seq)])[0]
next_words = np.argsort(next_words_pred)[-beam_size:]
for word in next_words:
new_partial_caption, new_partial_caption_prob =
caption_seq[0][:], caption_seq[1]
new_partial_caption.append(word)
new_partial_caption_prob += next_words_pred[word]
temp_caption_seqs.append([new_partial_caption,
new_partial_caption_prob])
caption_seqs = temp_caption_seqs
caption_seqs.sort(key = lambda item: item[1])
caption_seqs = caption_seqs[-beam_size:]
return caption_seqs
这有助于我们使用集束搜索基于输入图像特征生成字幕。 但是,它是在每个步骤中基于先前标记的原始标记序列。 因此,我们将在此基础上构建一个包装器函数,该函数将利用先前的函数生成一个纯文本句子作为输入图像的标题:
def generate_image_caption(model, word_to_index_map, index_to_word_map,
image_features, max_caption_size,
beam_size=1):
raw_caption_seqs = get_raw_caption_sequences(model=model,
word_to_index=word_to_index_map,
image_features=image_features,
max_caption_size=max_caption_size,
beam_size=beam_size)
raw_caption_seqs.sort(key = lambda l: -l[1])
caption_list = [item[0] for item in raw_caption_seqs]
captions = [[index_to_word_map[idx] for idx in caption]
for caption in caption_list]
final_captions = []
for caption in captions:
start_index = caption.index('<START>')+1
max_len = len(caption)
if len(caption) < max_caption_size
else max_caption_size
end_index = caption.index('<END>')
if '<END>' in caption
else max_len-1
proc_caption = ' '.join(caption[start_index:end_index])
final_captions.append(proc_caption)
return final_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())
pc.append(caption)
return pc
了解和实现 BLEU 评分
现在,我们需要选择适当的模型表现评估指标,以评估模型的表现。 这里的一个相关指标是双语评估学习(BLEU)得分。 这是一种评估模型在翻译语言时的表现的出色算法。 BLEU 背后的动机是,所生成的输出越接近于人工翻译,则得分越高。 时至今日,它仍然是将模型输出与人为输出进行比较的最受欢迎的指标之一。
BLEU 算法的简单原理是针对一组参考字幕评估生成的文本字幕(通常针对一个或多个字幕评估一个字幕,在这种情况下,每个图像五个字幕)。 计算每个字幕的分数,然后在整个语料库中平均以得到质量的总体估计。 BLEU 分数始终介于 0 到 1 之间,分数接近 1 表示高质量的翻译。 甚至参考文本数据也不是完美的,因为人类在字幕图像期间也会出错,因此,其想法不是获得完美的 1,而是获得良好的整体 BLEU 分数。
我们将使用 NLTK 中的翻译模块中的corpus_bleu(...)
函数来计算 BLEU 分数。 我们将计算 1、2、3 和 4 元组的总累积 BLEU 分数。 如我们已实现的评估函数所示,为bleu2
,bleu3
和bleu4
分数的每个 n-gram 分数分配了相等的权重:
from nltk.translate.bleu_score import corpus_bleu
def compute_bleu_evaluation(reference_captions,
predicted_captions):
actual_caps = [[caption.split() for caption in sublist]
for sublist in reference_captions]
predicted_caps = [caption.split()
for caption in predicted_captions]
bleu1 = corpus_bleu(actual_caps,
predicted_caps, weights=(1.0, 0, 0, 0))
bleu2 = corpus_bleu(actual_caps,
predicted_caps, weights=(0.5, 0.5, 0, 0))
bleu3 = corpus_bleu(actual_caps,
predicted_caps,
weights=(0.3, 0.3, 0.3, 0))
bleu4 = corpus_bleu(actual_caps, predicted_caps,
weights=(0.25, 0.25, 0.25, 0.25))
print('BLEU-1: {}'.format(bleu1))
print('BLEU-2: {}'.format(bleu2))
print('BLEU-3: {}'.format(bleu3))
print('BLEU-4: {}'.format(bleu4))
return [bleu1, bleu2, bleu3, bleu4]
评估测试数据的模型表现
现在已经准备好用于模型表现评估的所有组件。 为了评估模型在测试数据集上的表现,我们现在将使用传递学习来加载之前提取的图像特征,这些特征将作为模型的输入。 我们还将加载字幕,对其进行预处理,并将其作为每个图像的参考字幕列表进行分离,如下所示:
test_images = list(test_df['image'].unique())
test_img_features = [tl_img_feature_map[img_name]
for img_name in test_images]
actual_captions = list(test_df['caption'])
actual_captions = preprocess_captions(actual_captions)
actual_captions = [actual_captions[x:x+5]
for x in range(0, len(actual_captions),5)]
actual_captions[:2]
[['the dogs are in the snow in front of a fence',
'the dogs play on the snow',
'two brown dogs playfully fight in the snow',
'two brown dogs wrestle in the snow',
'two dogs playing in the snow'],
['a brown and white dog swimming towards some in the pool',
'a dog in a swimming pool swims toward sombody we cannot see',
'a dog swims in a pool near a person',
'small dog is paddling through the water in a pool',
'the small brown and white dog is in the pool']]
您可以清楚地看到每个图像标题现在如何位于整齐的单独列表中,这些列表将在计算 BLEU 分数时形成我们的标题参考集。 现在,我们可以生成 BLEU 分数,并使用不同的光束大小值测试模型的表现。 这里描述了一些示例:
# Beam Size 1 - Model 1 with 30 epochs
predicted_captions_ep30bs1 = [generate_image_caption(model=model1,
word_to_index_map=word_to_index,
index_to_word_map=index_to_word,
image_features=img_feat,
max_caption_size=max_caption_size,
beam_size=1)[0]
for img_feat
in test_img_features]
ep30bs1_bleu = compute_bleu_evaluation(
reference_captions=actual_captions,
predicted_captions=predicted_captions_ep30bs1)
BLEU-1: 0.5049574449416513
BLEU-2: 0.3224643449851107
BLEU-3: 0.22962263359362023
BLEU-4: 0.1201459697546317
# Beam Size 1 - Model 2 with 50 epochs
predicted_captions_ep50bs1 = [generate_image_caption(model=model2,
word_to_index_map=word_to_index,
index_to_word_map=index_to_word,
image_features=img_feat,
max_caption_size=max_caption_size,
beam_size=1)[0]
for img_feat
in test_img_features]
ep50bs1_bleu = compute_bleu_evaluation(
reference_captions=actual_captions,
predicted_captions=predicted_captions_ep50bs1)
您可以清楚地看到,随着我们开始考虑更高水平的 n-gram,分数开始下降。 总体而言,运行此过程非常耗时,要在集束搜索中获得更高的阶数会花费大量时间。 我们尝试了光束大小分别为 1、3、5 和 10 的实验。下表描述了每个实验的模型表现:
我们还可以通过图表的形式轻松地将其可视化,以查看哪种模型参数组合为我们提供了具有最高 BLEU 得分的最佳模型:
从上一张图可以很明显地看出,基于 BLEU 指标,我们在集束搜索期间具有 50 个周期且集束大小为 10 的模型为我们提供了最佳表现。
自动图片字幕实战!
对我们的测试数据集进行评估是测试模型表现的好方法,但是我们如何开始在现实世界中使用模型并为全新照片加上标题呢? 在这里,我们需要一些知识来构建端到端系统,该系统以任何图像作为输入,并为我们提供自由文本的自然语言标题作为输出。
以下是我们的自动字幕生成器的主要组件和函数:
- 字幕模型和元数据初始化器
- 图像特征提取模型初始化器
- 基于迁移学习的特征提取器
- 字幕生成器
为了使它通用,我们构建了一个类,该类利用了前面几节中提到的几个工具函数:
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input as preprocess_vgg16_input
from keras.applications import vgg16
from keras.models import Model
class CaptionGenerator:
def __init__(self, image_locations=[],
word_to_index_map=None, index_to_word_map=None,
max_caption_size=None, caption_model=None,
beam_size=1):
self.image_locs = image_locations
self.captions = []
self.image_feats = []
self.word2index = word_to_index_map
self.index2word = index_to_word_map
self.max_caption_size = max_caption_size
self.vision_model = None
self.caption_model = caption_model
self.beam_size = beam_size
def process_image2arr(self, 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
def initialize_model(self):
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
self.vision_model = vgg_model
def process_images(self):
if self.image_locs:
image_feats = [self.vision_model.predict
(self.process_image2arr
(path=img_path)) for img_path
in self.image_locs]
image_feats = [np.reshape(img_feat, img_feat.shape[1]) for
img_feat in image_feats]
self.image_feats = image_feats
else:
print('No images specified')
def generate_captions(self):
captions = [generate_image_caption(model=self.caption_model,
word_to_index_map=self.word2index,
index_to_word_map=self.index2word,
image_features=img_feat,
max_caption_size=self.max_caption_size, beam_size=self.beam_size)[0]
for img_feat in self.image_feats]
self.captions = captions
现在我们的字幕生成器已经实现,现在该将其付诸实践了! 为了测试字幕生成器,我们下载了几张全新的图像,这些图像在Flickr8K
数据集中不存在。 我们从 Flickr 下载了特定的图像,这些图像遵循必要的基于商业使用的许可证,因此我们可以在本书中进行描述。 我们将在下一部分中展示一些演示。
为室外场景中的样本图像加字幕
我们从 Flickr 拍摄了几张针对各种户外场景的图像,并使用了我们的两个图像字幕模型为每个图像生成字幕,如下所示:
# load files
import glob
outdoor1_files = glob.glob('real_test/outdoor1/*')
# initialize caption generators and generate captions
cg1 = CaptionGenerator(image_locations=outdoor1_files, word_to_index_map=word_to_index, index_to_word_map=index_to_word,
max_caption_size=max_caption_size, caption_model=model1, beam_size=3)
cg2 = CaptionGenerator(image_locations=outdoor1_files, word_to_index_map=word_to_index, index_to_word_map=index_to_word,
max_caption_size=max_caption_size, caption_model=model2, beam_size=3)
cg1.initialize_model()
cg1.process_images()
cg1.generate_captions()
cg2.initialize_model()
cg2.process_images()
cg2.generate_captions()
model30ep_captions_outdoor1 = cg1.captions
model50ep_captions_outdoor1 = cg2.captions
# plot images and their captions
fig=plt.figure(figsize=(13, 11))
plt.suptitle('Automated Image Captioning: Outdoor Scenes 1', verticalalignment='top', size=15)
columns = 2
rows = 3
for i in range(1, columns*rows +1):
fig.add_subplot(rows, columns, i)
image_name = outdoor1_files[i-1]
img = image.load_img(image_name)
plt.imshow(img, aspect='auto')
modelep30_caption_text = 'Caption(ep30): '+ model30ep_captions_outdoor1[i-1]
modelep50_caption_text = 'Caption(ep50): '+ model50ep_captions_outdoor1[i-1]
plt.xlabel(modelep30_caption_text+'\n'+modelep50_caption_text,size=11, wrap=True)
fig.tight_layout()
plt.subplots_adjust(top=0.955)
前面代码的输出如下:
根据前面的图像,您可以清楚地看到它已正确识别每个场景。 这不是一个完美的模型,因为我们可以清楚地看到它并没有在第二行的第二张图像中识别出狗,而是清楚地识别了一群人。 此外,我们的模型确实犯了一些颜色识别错误,例如将绿色球识别为红色球。 总体而言,生成的字幕绝对适用于源图像!
以下图像是从更多样化的户外场景中摘录的,并基于流行的户外活动。 我们将专注于不同的活动,以查看我们的模型在不同类型的场景上的表现如何,而不是仅关注一个特定的场景:
在前面的图像中,我们专注于各种各样的户外活动,包括越野自行车,滑雪,冲浪,皮划艇和攀岩。 如果您查看生成的字幕,它们与每个场景都相关,并可以很好地描述它们。 在某些情况下,我们的模型会变得非常具体,甚至描述每个人的穿着。 但是,正如我们前面提到的,它会在几种情况下错误地识别颜色,可能可以通过添加更多数据以及对高分辨率图像进行训练来改善颜色。
为流行运动的样本图像加字幕
在模型测试的最后一部分,我们从 Flickr 拍摄了几张图像,这些图像专注于世界各地通常进行的各种体育运动。 我们肯定获得了一些有趣的结果,因为我们不仅仅关注一两个基于运动的场景。 生成此代码的代码与我们在上一节中使用的代码完全相同,只是源图像发生了变化。 与往常一样,笔记本中提供了详细的代码以供参考。 以下是我们的字幕生成器在第一批运动场景上的结果:
在前面的图像中,我们可以清楚地看到训练有 50 个周期的模型在视觉上更详细地描述图像方面优于具有 30 个周期的模型。 这包括特定的球衣和服装颜色,例如白色,蓝色和红色。 我们还看到字幕中提到了一些具体的活动,例如:踢足球,看曲棍球的进球或在泥泞的赛道上驾驶。 这无疑为生成的字幕提供了更多的深度和含义。 我们的模型具有 30 个周期,因此在某些图像中所进行的确切运动方面也会犯一些错误。
现在,让我们看一下体育场景的最后一组,以了解我们的字幕生成器在与前一组场景完全不同的体育活动中的表现:
我们可以从前面的输出中观察到,我们的两个模型都运行良好,在 30 个周期上训练的模型在几种情况下都表现出色,例如识别出踢足球的孩子或男孩,甚至是比赛中 BMX 骑手的颜色和配饰。 总体而言,这两种模型都表现良好,并且在某种程度上解释了风景,类似于人类对这些场面的描述。
成功的主要方面是我们的模型不仅可以正确识别每个活动,而且还能够生成有意义且适用的标题。 我们鼓励您尝试在不同的场景上构建和测试自己的字幕生成器!
未来的改进空间
根据我们在本章中采用的方法,有多种方法可以改进此模型。 以下是一些可以改进的特定方面:
- 使用更好的图像特征提取模型,例如 Google 的 Inception 模型
- 分辨率更高,质量更好的训练图像(需要 GPU 功能!)
- 基于 Flickr30K 等数据集甚至图像增强的更多训练数据
- 在模型中引入注意力
如果您拥有必要的数据和基础架构,那么这些点子值得探讨!
总结
这绝对是我们整本书中解决的最棘手的现实问题之一。 它是迁移学习和生成型深度学习的完美结合,可应用于来自图像和文本的数据组合,这些组合结合了围绕计算机视觉和 NLP 的不同领域。 我们介绍了有关理解图像字幕的基本概念,构建字幕生成器所需的主要组件,并从头开始构建了我们自己的模型。 我们通过利用预先训练的计算机视觉模型从要字幕的图像中提取正确的特征,然后将它们与一些顺序模型(例如 LSTM)结合使用,以有效地利用迁移学习原理。 顺序模型的有效评估非常困难,我们利用行业标准的 BLEU 评分标准来达到目的。 我们从头开始实现评分函数,并在测试数据集上评估了我们的模型。
最后,我们使用以前构建的所有资产和组件从头构建了一个通用的自动图像字幕系统,并在来自不同领域的多种图像上对其进行了测试。 我们希望这能给您一个很好的入门介绍,这是计算机视觉和 NLP 的完美结合,并且我们绝对鼓励您构建自己的图像捕获系统!
十一、图像着色
颜色是大自然的笑容。
——雷·亨特
直到 1840 年代,世界都是以黑白捕获。 加布里埃尔·利普曼(Gabriel Lippmann)于 1908 年获得诺贝尔物理学奖,从而开始了色彩捕捉的时代。 1935 年,伊士曼·柯达(Eastman Kodak)推出了一体式三重彩色胶卷,称为 Kodachrome,用于拍摄彩色照片。
彩色图像不仅与美学和美感有关,而且比黑白图像捕获的信息要多得多。 颜色是现实世界对象的重要属性,它为我们对周围世界的感知增加了另一个维度。 色彩的重要性是如此之大,以至于有许多项目为整个历史上的艺术作品和摄影作品着色。 随着 Adobe Photoshop 和 GIMP 等工具的出现,人们一直在努力地将旧照片转换为彩色照片。 reddit r / Colorization 子组是一个在线社区,人们在这里分享经验并致力于将黑白图像转换为彩色图像。
到目前为止,在本书中,我们涵盖了不同的领域和场景,以展示迁移学习的惊人好处。 在本章中,我们将介绍使用深度学习进行图像着色的概念,并利用迁移学习来改善结果。 本章将涵盖以下主题:
- 问题陈述
- 了解图像着色
- 彩色图像
- 建立基于深度神经网络的着色网络
- 改进之处
- 挑战
在接下来的部分中,我们将使用术语黑白,单色和灰度来表示没有任何颜色信息的图像。 我们将这些术语互换使用。
问题陈述
照片可以帮助我们及时保存事件。 它们不仅帮助我们重温记忆,还提供对过去重要事件的见解。 在彩色摄影成为主流之前,我们的摄影历史是用黑白拍摄的。 图像着色的任务是将给定的灰度图像转换为合理的颜色版本。
图像着色的任务可以从不同的角度进行。 手动过程非常耗时,并且需要出色的技能。 计算机视觉和深度学习领域的研究人员一直在研究使过程自动化的不同方法。 通过本章,我们将努力理解如何将深层神经网络用于此类任务。 我们还将尝试利用迁移学习的力量来改善结果。
我们鼓励读者在继续进行之前,先对问题陈述进行思考。 考虑一下您将如何处理这样的任务。 在深入探讨该解决方案之前,让我们获取有关彩色图像和相关概念的一些信息。 下一节涵盖了处理当前任务所需的基本概念。
彩色图像
不到 100 年前,单色捕获是一个限制,而不是一种选择。 数码和移动摄影的出现使黑白图像或灰度图像成为一种艺术选择。 当然,这样的图像具有戏剧性的效果,但是黑白图像不仅仅是改变捕获设备(无论是数码相机还是电话)上的选项。
我们对颜色和正式颜色模型的了解早于彩色图像。 托马斯·杨(Thomas Young)在 1802 年提出了三种类型的感光器或视锥细胞的存在(如下图所示)。 他的理论详述了这三个视锥细胞中的每一个仅对特定范围的可见光敏感。 进一步发展了该理论,将这些视锥细胞分为短,中和长三种,分别优选蓝色,绿色和红色:
托马斯·杨(Thomas Young)和赫尔曼·赫尔姆霍尔茨(Hermann Helmholtz):三锥细胞理论
我们对颜色的理解以及对颜色的理解方式的进步导致了颜色理论的形式化。 由于色彩理论本身是一个完整的领域,因此在本章中,我们将对其进行简要介绍。 关于这些主题的详细讨论超出了本书的范围。
色彩理论
简单来说,色彩理论是用于指导色彩感知,混合,匹配和复制方法的正式框架。 多年来,已经进行了各种尝试来基于色轮,原色,第二色等正式定义颜色。 因此,颜色理论是一个广阔的领域,在此之下,我们可以正式定义与色度,色相,配方等颜色相关的属性。
色彩模型和色彩空间
颜色模型是颜色理论到颜色表示的表述。 颜色模型是一种抽象的数学概念,当与它的组成的精确理解相关联时,被称为颜色空间。 大多数颜色模型都用表示特定颜色成分的三到四个数字的元组表示。
RGB
托马斯·杨(Thomas Young)三锥理论红色绿色蓝色(RGB)的延续,是最古老,使用最广泛的颜色模型和颜色空间之一。 RGB 是加色模型。 在此模型中,以不同的浓度添加了光的三个分量(红色,绿色和蓝色),以实现可见光的完整光谱。 附加色空间如下图所示:
RGB 颜色空间(来源:英语 Wikipedia 的 SharkD。更高版本由 Jacobolus 上传,已从 wikipedia en 转移到 Public Commons 域)
每种成分的零强度会导致黑色,而全强度会导致对白色的感觉。 尽管简单,但此颜色模型和颜色空间构成了大多数电子显示器(包括 CRT,LCD 和 LED)的基础。
YUV
Y
代表亮度,而U
和V
通道代表色度。 该编码方案在视频系统中被广泛使用以映射人类的颜色感知。 紫外线通道主要帮助确定红色和蓝色的相对含量。 由于该方案使用较低的带宽并且不易出现传输错误的能力,因此被广泛使用,如下所示:
YUV 色彩空间(来源:Tonyle,本人著作,CC BY-SA 3.0)
此图像是 UV 颜色通道在 0.5 Y 处的样本表示。
LAB
这种与设备无关的色彩空间参考是由国际照明委员会开发的。 L
通道表示颜色的亮度(0 为黑色,而 100 为漫射白色)。
A
表示绿色和品红色之间的位置,而B
表示蓝色和黄色之间的位置,如下所示:
LAB 色彩空间(来源:Holger kkk Everding,自己的作品,CC BY-SA 4.0)
除了这三种以外,还存在其他各种颜色模型。 出于当前有关图像着色的用例的目的,我们将采用一种非常有趣的方法。
重新陈述问题
如果我们遵循使用最广泛的颜色模型 RGB,那么事实证明,训练模型以将输入的单色图像映射到颜色将是一项艰巨的任务。
深度学习领域的研究人员在解决和提出问题方面颇具创造力。 在图像着色的情况下,研究人员巧妙地研究了利用不同输入来实现灰度图像逼真的幻觉的方法。
在最初的尝试中,参考图像和颜色涂鸦形式的颜色引导输入的不同变体被用来产生巨大的效果。 请参阅威尔士和合著者以及莱文和合著者。
最近的工作集中在利用深层 CNN 中的迁移学习使整个过程自动化。 结果令人鼓舞,有时甚至足以愚弄人类。
最近的工作,以及迁移学习的力量,已经巧妙地尝试利用包含灰度通道作为其组成部分之一的颜色模型。 那会响吗? 现在让我们从另一个角度看问题陈述。
除了无所不在的 RGB 颜色空间外,我们还讨论了 LAB。 LAB 色彩空间包含灰度值,因为L
通道(用于亮度),而其余两个通道(a
和b
)赋予颜色属性。 因此,着色问题可以用以下数学方式建模:
在上述方程式中,我们表示从给定数据将L
通道映射到同一图像的a
和b
通道的函数。 下图说明了这一点:
colornet 转换
简而言之,我们已经将图像着色的任务转换为将一个通道(灰度L
通道)转换为两个颜色通道(A
和B
)的任务 ,说明如下:
彩色图像及其组件 -- RGB,YUV 和 LAB
前面的图像显示了彩色图像的L
,A
和B
通道,基于 Zhang 及其合著者(2016)和 Federico 及其合著者(2017)的作品。 我们将在接下来的部分中详细研究它们。
我们鼓励读者阅读标题为《Deep Koalarization:使用 CNN 和 Inception-ResNet-v2 进行图像着色》的论文。 我们要感谢 Federico Baldassarre,Diego Gonzalez-Morin 和 Lucas Rodes-Guirao 为他们的工作及其实现提供了详细的信息和见解。 我们还要感谢 Emil Wallner 使用 Keras 出色地实现了本文。
读者应注意,类似的过程也可以应用于 YUV 色彩空间。 Jeff Hwang 和他的合著者在题为《利用深度卷积神经网络进行图像着色》的论文中讨论了利用这种色彩空间的尝试,效果也很好。
建立着色深层神经网络
现在是时候构建着色深层神经网络或色网。 如前一节所述,如果我们使用替代颜色空间,例如 LAB(或 YUV),则可以将着色任务转换为数学转换。 转换如下:
数学公式和创造力很好,但是学习这些转换的图像在哪里呢? 深度学习网络需要大量数据,但幸运的是,我们有来自各种开源数据集的大量不同图像的集合。 在本章中,我们将依赖于 ImageNet 本身的一些示例图像。 由于 ImageNet 是一个庞大的数据集,因此我们为问题陈述随机选择了一些彩色图像。 在后面的部分中,我们将讨论为什么选择此子集及其一些细微差别。
我们依靠 Baldassarre 及其合作者开发的图像提取工具,用于《Deep Koalarization:使用 CNN 和 Inception-ResNet-v2 进行图像着色》的论文,来整理本章中使用的 ImageNet 样本的子集。 可以在这个页面上获取数据提取的代码。
本书的 GitHub 存储库中提供了本章使用的代码和示例图像以及colornet_vgg16.ipynb
笔记本。
预处理
获取/整理所需数据集后的第一步是预处理。 对于当前的图像着色任务,我们需要执行以下预处理步骤:
- 重新缩放:ImageNet 是一个具有各种图像的多样化数据集,包括类和大小(尺寸)。 为了实现此目的,我们将所有图像重新缩放为固定大小。
- 利用 24 位 RGB:由于人眼只能区分 2 和 1000 万种颜色,因此我们可以利用 24 位 RGB 来近似 1600 万种颜色。 减少每个通道的位数将有助于我们以更少的资源更快地训练模型。 这可以通过简单地将像素值除以 255 来实现。
- RGB 到 LAB:由于在 LAB 色彩空间中更容易解决图像着色问题,因此我们将利用 skimage 来转换和提取 RGB 图像中的 LAB 通道。
标准化
LAB 颜色空间的值介于 -128 至 +128 之间。 由于神经网络对输入值的大小敏感,因此我们将从-128 到+128 的变换后的像素值归一化,并将它们置于 -1 到 +1 范围内。 以下代码片段中展示了相同的内容:
def prep_data(file_list=[],
dir_path=None,
dim_x=256,
dim_y=256):
#Get images
X = []
for filename in file_list:
X.append(img_to_array(
sp.misc.imresize(
load_img(
dir_path+filename),
(dim_x, dim_y))
)
)
X = np.array(X, dtype=np.float64)
X = 1.0/255*X
return X
转换后,我们将数据分为训练集和测试集。 对于拆分,我们使用了 sklearn 的train_test_split utility
。
损失函数
模型是通过改善损失函数或目标函数来学习的。 任务是使用反向传播学习最佳参数,以最小化原始彩色图像和模型输出之间的差异。 来自模型的输出彩色图像也称为灰度图像的幻觉着色。 在此实现中,我们将均方误差(MSE)用作损失函数。 以下等式对此进行了总结:
原始颜色和色网输出之间的损失函数(来源:Baldassarre 和合著者)
对于 Keras,使用此损失函数就像在编译 Keras 模型时设置参数一样容易。 我们利用 RMSprop 优化器来训练我们的模型(本文使用 Adam 代替)。
编码器
卷积神经网络(CNN)是令人惊叹的图像分类器。 他们通过提取位置不变特征来实现。 在此过程中,它们倾向于使输入图像失真。
在图像着色的情况下,这种失真将是灾难性的。 为此,我们使用编码器将H x W
尺寸的输入灰度图像转换为H / 8 x W / 8
。 编码器通过使用零填充来保持图像通过不同层的纵横比。 以下代码片段显示了使用 Keras 的编码器:
#Encoder
enc_input = Input(shape=(DIM, DIM, 1,))
enc_output = Conv2D(64, (3,3),
activation='relu',
padding='same', strides=2)(enc_input)
enc_output = Conv2D(128, (3,3),
activation='relu',
padding='same')(enc_output)
enc_output = Conv2D(128, (3,3),
activation='relu',
padding='same', strides=2)(enc_output)
enc_output = Conv2D(256, (3,3),
activation='relu',
padding='same')(enc_output)
enc_output = Conv2D(256, (3,3),
activation='relu',
padding='same', strides=2)(enc_output)
enc_output = Conv2D(512, (3,3),
activation='relu',
padding='same')(enc_output)
enc_output = Conv2D(512, (3,3),
activation='relu',
padding='same')(enc_output)
enc_output = Conv2D(256, (3,3),
activation='relu',
padding='same')(enc_output)
在前面的代码片段中,有趣的方面是对第 1 层,第 3 层和第 5 层使用了 2 的步幅大小。2 的步幅长度将图像尺寸减半,但仍设法保持了纵横比。 这有助于增加信息密度而不会扭曲原始图像。
迁移学习 – 特征提取
本章讨论的图像着色网络是一个非常独特的网络。 它的独特性来自我们使用迁移学习来增强模型的方式。 我们知道可以将预训练的网络用作特征提取器,以帮助迁移学习的模式并提高模型的表现。
在这种当前设置下,我们利用预训练的 VGG16(本文指的是利用预训练的 Inception 模型)进行迁移学习。 由于 VGG16 需要以特定格式输入,因此我们通过调整输入图像的大小并将其连接 3 次以补偿丢失的通道信息,来转换输入的灰度图像(输入到网络编码器部分的相同灰度图像)。
以下代码段获取输入的灰度图像并生成所需的嵌入:
#Create embedding
def create_vgg_embedding(grayscaled_rgb):
gs_rgb_resized = []
for i in grayscaled_rgb:
i = resize(i, (224, 224, 3),
mode='constant')
gs_rgb_resized.append(i)
gs_rgb_resized = np.array(gs_rgb_resized)
gs_rgb_resized = preprocess_input(gs_rgb_resized)
with vgg16.graph.as_default():
embedding = vgg16.predict(gs_rgb_resized)
return embedding
前面的代码段生成大小为1,000 x 1 x 1
的输出特征向量。
融合层
我们在前几章中构建的大多数网络都使用了 Keras 的顺序 API。 融合层是在这种情况下利用迁移学习的创新方式。 请记住,我们已将输入灰度图像用作两个不同网络(一个编码器和一个预训练的 VGG16)的输入。 由于两个网络的输出具有不同的形状,因此我们将 VGG16 的输出重复 1,000 次,然后将其与编码器输出连接或合并。 以下代码段准备了融合层:
#Fusion
fusion_layer_output = RepeatVector(32*32)(emd_input)
fusion_layer_output = Reshape(([32,32,
1000]))(fusion_layer_output)
fusion_layer_output = concatenate([enc_output,
fusion_layer_output], axis=3)
fusion_layer_output = Conv2D(DIM, (1, 1),
activation='relu',
padding='same')(fusion_layer_output)
VGG16 的输出重复沿编码器输出的深度轴连接。 这样可以确保从 VGG16 中提取的图像特征嵌入均匀地分布在整个图像中:
级联灰度输入预训练网络(左),融合层(右)
来源:Baldassarre 等
上图显示了特征提取器或预训练的 VGG16 的输入以及融合层的结构。
解码器
网络的最后阶段是解码器。 在网络的前两个部分中,我们利用编码器和预训练模型来学习不同的特征并生成嵌入。 融合层的输出为张量,大小为H / 8 x W / 8 x 256
,其中H
和W
是灰度图像的原始高度和宽度(在我们的情况是256 x 256
)。 该输入经过一个八层解码器,该解码器使用五个卷积层和三个上采样层构建。 上采样层可帮助我们使用基本的最近邻方法将图像大小增加一倍。 以下代码片段展示了网络的解码器部分:
#Decoder
dec_output = Conv2D(128, (3,3),
activation='relu',
padding='same')(fusion_layer_output)
dec_output = UpSampling2D((2, 2))(dec_output)
dec_output = Conv2D(64, (3,3),
activation='relu',
padding='same')(dec_output)
dec_output = UpSampling2D((2, 2))(dec_output)
dec_output = Conv2D(32, (3,3),
activation='relu',
padding='same')(dec_output)
dec_output = Conv2D(16, (3,3),
activation='relu',
padding='same')(dec_output)
dec_output = Conv2D(2, (3, 3),
activation='tanh',
padding='same')(dec_output)
dec_output = UpSampling2D((2, 2))(dec_output)
解码器网络的输出是具有两个通道的原始大小的图像,即,输出是形状为H x W x 2
的张量。 最终的卷积层使用 tanh 激活函数将预测像素值保持在 -1 到 +1 范围内。
下图显示了具有三个组成部分的网络:
Colornet 由编码器,作为特征提取器的预训练模型,融合层和解码器组成
使用 Keras 构建的深度学习模型通常是使用顺序 API 构建的。 在这种情况下,我们的着色网络(即 colornet)利用函数式 API 来实现融合层。
后处理
解决问题的技巧还没有结束。 如“预处理”小节中所述,我们将 -1 到 +1 之间的像素值标准化,以确保我们的网络正确训练。 同样,两个颜色通道的 LAB 颜色空间的值在 -128 到 +128 之间。 因此,执行以下两个后处理步骤:
- 我们将每个像素值乘以 128,以将值带入所需的颜色通道范围
- 我们将灰度输入图像与输出两通道图像连接起来,以获得幻觉的彩色图像
以下代码段执行后处理步骤,以产生幻觉的彩色图像:
sample_img = []
for filename in test_files:
sample_img.append(sp.misc.imresize(load_img(IMG_DIR+filename),
(DIM, DIM)))
sample_img = np.array(sample_img,
dtype=float)
sample_img = 1.0/255*sample_img
sample_img = gray2rgb(rgb2gray(sample_img))
sample_img = rgb2lab(sample_img)[:,:,:,0]
sample_img = sample_img.reshape(sample_img.shape+(1,))
#embedding input
sample_img_embed = create_vgg_embedding(sample_img)
如前面的代码片段所示,我们使用 skimage 中的lab2rgb
工具将生成的输出转换为 RGB 颜色空间。 这样做是为了便于可视化输出图像。
训练与结果
训练如此复杂的网络可能很棘手。 在本章中,我们从 ImageNet 中选择了一小部分图像。 为了帮助我们的网络学习和推广,我们使用 Keras 的ImageDataGenerator
类来扩充数据集并在输入数据集中产生变化。 以下代码片段展示了图像增强和模型训练:
# Image transformer
datagen = ImageDataGenerator(
shear_range=0.2,
zoom_range=0.2,
rotation_range=20,
horizontal_flip=True)
def colornet_img_generator(X,
batch_size=BATCH_SIZE):
for batch in datagen.flow(X, batch_size=batch_size):
gs_rgb = gray2rgb(rgb2gray(batch))
batch_lab = rgb2lab(batch)
batch_l = batch_lab[:,:,:,0]
batch_l = batch_l.reshape(batch_l.shape+(1,))
batch_ab = batch_lab[:,:,:,1:] / 128
yield ([batch_l,
create_vgg_embedding(gs_rgb)], batch_ab)
history = model.fit_generator(colornet_img_generator(X_train,
BATCH_SIZE),
epochs=EPOCH,
steps_per_epoch=STEPS_PER_EPOCH)
在着色网络的情况下,这种损失可能会产生误导。 它似乎已稳定在 100 个周期以下,但所产生的结果更多是乌贼色而不是颜色。 因此,我们做了更多的实验以达到以下结果:
Colornet 输出:第一列代表灰度输入,第二列代表模型输出,第三列代表原始图像
前面的结果虽然不令人吃惊,但令人鼓舞。 通过对模型进行 600 个周期的训练,批次大小为 64,可以实现上述结果。
挑战
深度神经网络是功能强大的模型,具有成千上万个可学习的参数。 当前训练着色网络的方案提出了一系列新的挑战,其中一些挑战如下:
- 当前的网络似乎已经学习了高级特征,例如草地和运动球衣(在一定程度上),而它发现学习较小物体的颜色模式有些困难。
- 训练集仅限于非常具体的图像子集,因此反映在测试数据集中。 该模型对训练集中不存在的对象或包含这些对象的样本不多的表现不佳。
- 即使训练损失似乎已稳定在 50 个周期以下,但我们看到,除非进行数百个周期训练,否则该模型的着色表现相当差。
- 该模型很容易将大多数对象着色为灰色或棕褐色。 在训练了较少周期的模型中观察到了这一点。
除了这些挑战之外,对于如此复杂的架构,计算和内存要求也很高。
进一步改进
当前的实现尽管显示出令人鼓舞的结果,但是可以进一步调整。 通过利用更大,更多样化的数据集可以实现进一步的改进。
也可以通过使用功能更强大的最新预训练图像分类模型(例如 InceptionV3 或 InceptionResNetV2)来进行改进。
我们还可以通过准备由更复杂的架构组成的集成网络来利用 Keras 的函数式 API。 接下来的步骤之一可能是向网络提供时间信息,并查看是否还可以学习为视频着色。
总结
图像着色是深度学习领域的前沿主题之一。 随着我们对迁移学习和深度学习的理解日趋成熟,应用范围变得越来越令人兴奋且更具创造力。 图像着色是研究的活跃领域,最近,深度学习专家分享了一些激动人心的工作。
在本章中,我们学习了颜色理论,不同的颜色模型和颜色空间。 这种理解帮助我们将问题陈述重新表述为从单通道灰度图像到两通道输出的映射。 然后,我们根据 Baldassarre 和他的合著者的作品,着手建立一个色网。 该实现涉及一个独特的三层网络,该网络由编码器,解码器和融合层组成。 融合层使我们能够通过将 VGG16 嵌入与编码器输出连接来利用迁移学习。 网络需要一些特定的预处理和后处理步骤来训练给定的图像集。 我们的训练和测试数据集由 ImageNet 样本的子集组成。 我们对色网进行了数百次训练。 最后,我们提供了一些幻影图像,以了解该模型对着色任务的学习程度。 训练有素的色网学习了某些高级对象,例如草,但在较小或较不频繁的对象上表现不佳。 我们还讨论了这种类型的网络带来的一些挑战。
这结束了本书中由用例驱动的系列文章中的最后一章。 我们介绍了跨不同领域的不同用例。 每个用例都帮助我们利用了迁移学习的概念,本书的前两部分对此进行了详细讨论。 机器学习和深度学习领域的领先人物之一 Andrew Ng 在他的 NIPS 2016 教程中表示:
迁移学习将成为机器学习商业成功的下一个推动力。
在本书中讨论和展示了各种应用及其优势之后,您现在应该了解迁移学习的巨大潜力。
标签:11,指南,img,Python,模型,train,图像,model,我们 From: https://www.cnblogs.com/apachecn/p/17322031.html