Show and Tell
引言
查看Kaggle笔记本
几年前,如果有人说:“我们将会有这样一种虚拟助理,它们能正确无误地描述展示给它们的情景”,应该会一笑而过。但着机器学习逐渐进入深度学习,我们以前做梦都想不到的无尽可能性和想法开始走进现实。奥里奥尔·温亚尔斯(Oriol Vinyals)等人所著的《Show and Tell:神经网络图像描述生成器》Show and Tell: A Neural Image Caption Generator就描述了这样一个想法。在这篇论文中,作者们针对图像描述生成器提出了一个端到端的解决方案。在此论文之前,针对这种任务提出的方案包括独立任务优化(视觉和自然语言)并且随后要手动缝合那些独立任务。
该论文的灵感源于神经网络机器翻译,在神经网络机器翻译中,编码器在给定语言序列上训练,然后生成一个固定长度的表征并用于解码器,解码器就给出另一种语言的序列。该论文的作者们在这一理念的基础上,使用视觉特征提取器作为编码器, 同时使用序列模型作为解码器。
📜阅读那篇论文
好的学术论文,让人读过之后也能想到文中所提出的想法,这着实令人着迷。就在那一刻,你会觉得,这个想法是多么简单,但又多么强大啊。《Show and Tell:神经网络图像描述生成器》就是这样一篇令人惊叹的论文,它为图像描述生成器开启了深度学习研究的大门。
作者们称其自身深受机器翻译的启发。因此我们把这篇文章编排成路线图的形式。跟着路线图走,读者在读这篇论文时应该会和我们一样激动。
路线图:
- 任务:我们研究手上任务的核心概念。
- 模块:我们探究所用的不同架构并进一步探讨我们使用它们的原因。
- 代码:不展示代码的话我们怎么出一篇教程?
- 损失与结果:用了什么目标函数?表现如何?
👨🏫任务
给定一个图像,我们就要给出这个图像的描述。这不是一个人工代理决定图像属于哪一类的简单分类问题。也不是一个人工代理在其所分类的对象上绘制边界框的检测任务。在这里,我们要理解图像的内容,然后形成一个词语序列,这个词语序列要能描述图像内容之间的关系。
说到这里,我们想到的最简单方法就是把那个任务分为两个不同的任务。
- 👁️ ️计算机视觉:这部分负责处理提供的图像。它尝试提取图像的特征、从分层特征构建概念并建立数据分布模型。一个简单的卷积神经网络就可以搞定这个任务了。输入一个图像后,卷积神经网络内核以分层形式提取图像特征。这些特征就是给定图像的内容的精简表征。
Goodfellow等人提供的深度学习资料
- 🗣️ ️ 生成词语:在该任务中,提供给我们一个图像以及图像的描述并用于训练。需要对描述建模。描述是一条描述图像的词语序列。需要用自然语言处理器对描述数据分布建模。模型需要理解词语分布以及词语上下文。这里我们可以用一个简单的循环(recurrent)架构,这个循环架构能对描述建模并生成词语,所生成的词语是从提供的描述数据空间严密采样而来的。
我发现非常有趣的一点就是数字的运用。我们人类创造了一门美妙的沟通语言,叫做数学。我们可以用数字和符号描述概念、创意等很多东西。现在我们暂且把目光转向数字。计算机视觉模型从图像中提取有价值的特征,这些特征实质上就是数字(模型的权值和阈值)。同理,语言和词语也可以用数字来描述(词嵌入)。就是这个想法,利用它就能解决缝合任务中多个领域的难题。我们要把视觉模型数字输入到语言模型,并且输入方式要能够使图像描述生成成功。
Show and Tell:一个神经网络图像描述生成器
我发现非常有趣的一点领悟就是数字的使用。我们人类创造了一门美妙的沟通语言,叫做数学。我们可以用数字和符号描述概念、创意等很多东西。我们暂且把目光转向数字。计算机视觉模型从图像中提取的有价值的特征实质上就是数字(模型的权值和阈值)。同理,语言和词语也可以用数字来描述(词嵌入)。就是这个想法,利用它就能解决缝合任务中多个领域的难题。我们要把视觉模型数字输入到语言模型,并且输入方式要能够使图像描述生成成功。
数据
对于我们的实验,我们选择使用Flickr30k数据集,该数据集包含30000张图像以及对应每个图像的多个不同描述。该数据已被预处理且托管在Kaggle,方便我们使用。我们将继续进入到Flickr30k的Kaggle数据集。
该数据集包含一个CSV
文件,这一文件所包含的图像记录将图像与其对应的描述关联到一起。
读取数据集如下:
]在进入下一步之前,我们要向读者说明<start>
和<end>
的用法。这对于让模型知道描述的起始和结束尤为重要。它虽然在训练数据期间并不那么必要,但在测试阶段我们需要把标记<start>
输入到模型,从而生成描述的第一个词语,模型产生标记<end>
后就要停止生成词语。
🏋️️模型
我们已经对任务以及要处理的模型有了相当的了解。在这部分,运用数字将很有帮助。我们首先从作者们提出的模型架构开始,然后再深入探究其工作原理。
所提出的架构
在这里我们手边有两个不同的模型,分别用于不同的任务。左侧是一个视觉模型,右侧是一个用于生成词语的循环模型。这里的思路是将图像特征当作另一个词,输入到循环模型中。从图像提取的特征是一个数字集合(向量),如果我们在描述的嵌入空间中绘制出该向量,我们肯定会得到词语的表征。图像特征转化成词语是整个过程的美妙之处。在嵌入空间绘制的图像特征可能不代表词典中的一个实际的字,但是让循环模型去学习已经足够了。这个所谓的image-word是循环模型的初始输入。读者在深刻思考之后,必定会发现这个思路是多么简单而有效。具有相同特征的两张图片在嵌入空间中就离得近,把这些特征提供给循环模型后,这一模型就会为两个图像生成相似的描述。image-word这个思路的重要之处还在于,由于现在可以在嵌入空间上计算向量代数,因此仅仅通过增加两个概念,就可以学习新的概念了。IMG_370
图像-词(image-word)示例
按照该架构,我们用一个卷积神经网络(CNN)作为编码器,用一个门控循环单元(GRU)的堆栈层作为解码器。我们用的是门控循环单元(Gated Recurrent Units),而不用长短期记忆模型(LSTM)
,因为门控循环单元比长短期记忆模型计算效率高,比简单的循环神经网络(RNN)更有效。要记住,我们处理的是大量数据集,我们选用门控循环单元可提升管道的效率,但有一定的代价,那就是会损失一些计算效率(梯度耗散问题)。
👀 Show(显示)
模型的这一部分充当信息编码器。我们使用一个restnet50
模型,该模型已经用Imagenet
的数据进行了预训练。这里我们忽略模型的最后一层,从倒数第二层开始提取输出。在resnet
输出的顶端,我们堆叠一个全局平均池化(GAP)和一个全连接层。我们利用全局平均池化(GAP)取倒数第二个内核输出的平均值,利用全连接(Dense)层尝试把图像特征塑造成与词嵌入相同的形状。
🗣️️ Tell(说出)
模型的这一部分充当信息解码器。现在,为了解释这些,我们必须探究一下循环神经网络模型(RNN)的根源。如果有人需要温习循环架构的有关知识, Under the Hood of RNN(循环神经网络探秘)将是个不错的参考资料。单个单元格yt的输出是由其当前输入xt以及来自上一个单元格ht−1h_{t-1}ht−1的隐藏状态激活共同决定的。现在,如果第一个循环神经网络单元格把编码的图像作为自己的输入,它生成的隐藏状态将被转入下一个单元格。这一隐藏状态也将会充当当前单元格输入和输出的链接,并且对序列中的所有单元格都重复此操作。总而言之,编码图像的效用实质上会传递到整个单元格序列,以便于被预测的每个词语都是在记住给定图像的情况下完成的。
循环公式的描述
🖥️️代码
查看Kaggle笔记本
现在我们进入最重要的部分——代码。
文本处理
在下列代码块中,我们做基本的文本处理并从可用的描述集创建词汇表。我们还要手动添加一个‘pad’(填补)标记,以便于之后让全部句子长度相同,这对我们有用。
train_df = df.iloc[:train_size,:]
val_df = df.iloc[train_size+1:train_size+val_size,:]
test_df = df.iloc[train_size+val_size+1:,:]
# Choose the top 5000 words from the vocabulary
top_k = 10000
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
oov_token="<unk>",
filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~')
# build the vocabulary
tokenizer.fit_on_texts(train_df['comment'])
tokenizer.word_index['<pad>'] = 0
tokenizer.index_word[0] = '<pad>'
# This is a sanity check function
def check_vocab(word):
i = tokenizer.word_index[word]
print(f"The index of the word: {i}")
print(f"Index {i} is word {tokenizer.index_word[i]}")
check_vocab("pajama")
我们将得到这样一个输出:
接下来,我们必须要根据分词器(tokenizer)生成的词汇表创建训练数据。我们手动补齐所有句子,使其长度相同。接着我们把数据集成到管道 tf.data 。
# Create the tokenized vectors
train_seqs = tokenizer.texts_to_sequences(train_df['comment'])
val_seqs = tokenizer.texts_to_sequences(val_df['comment'])
test_seqs = tokenizer.texts_to_sequences(test_df['comment'])
# If you do not provide a max_length value, pad_sequences calculates it automatically
train_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
val_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(val_seqs, padding='post')
test_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(test_seqs, padding='post')
train_cap_ds = tf.data.Dataset.from_tensor_slices(train_cap_vector)
val_cap_ds = tf.data.Dataset.from_tensor_slices(val_cap_vector)
test_cap_ds = tf.data.Dataset.from_tensor_slices(test_cap_vector)
图像处理
现在我们要为数据集flicker30K 中的图像创建一个管道tf.data。我们完成基本的操作,比如载入图像、解码图像、数据类型转换以及改变大小。
@tf.function
def load_img(image_path):
img = tf.io.read_file(image_path)
img = tf.image.decode_jpeg(img)
img = tf.image.convert_image_dtype(img, tf.float32)
img = tf.image.resize(img, (224, 224))
return img
train_img_name = train_df['image_name'].values
val_img_name = val_df['image_name'].values
test_img_name = test_df['image_name'].values
train_img_ds = tf.data.Dataset.from_tensor_slices(train_img_name).map(load_img)
val_img_ds = tf.data.Dataset.from_tensor_slices(val_img_name).map(load_img)
test_img_ds = tf.data.Dataset.from_tensor_slices(test_img_name).map(load_img)
合并数据
我们的意图是把创建的两个数据管道合并在一起,这样我们就能将其直接输入至我们的网络中。我们分批取数据,因为整个数据集太大了。
# prefecth and batch the dataset
AUTOTUNE = tf.data.experimental.AUTOTUNE
BATCH_SIZE = 512
train_ds = tf.data.Dataset.zip((train_img_ds, train_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
val_ds = tf.data.Dataset.zip((val_img_ds, val_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
test_ds = tf.data.Dataset.zip((test_img_ds, test_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
模型
Show(显示)
如前所述,Show指的是架构的编码部分,用来压缩图像。在ImageNet 上训练的一个ResNet50模型作为特征提取器,随后就是全局平均池化(GAP)。最后,我们用一个完全连接的层把架构的这一部分收集起来。
class CNN_Encoder(tf.keras.Model):
def __init__(self, embedding_dim):
super(CNN_Encoder, self).__init__()
self.embedding_dim = embedding_dim
def build(self, input_shape):
self.resnet = tf.keras.applications.ResNet50(include_top=False,
weights='imagenet')
self.resnet.trainable=False
self.gap = GlobalAveragePooling2D()
self.fc = Dense(units=self.embedding_dim,
activation='sigmoid')
def call(self, x):
x = self.resnet(x)
x = self.gap(x)
x = self.fc(x)
return x
Tell(说出)
Tell是包含门控循环单元(GRU)的解码器,它利用从编码器得来的信息,在学到的描述和原始输入之间建立关联。
class RNN_Decoder(tf.keras.Model):
def __init__(self, embedding_dim, units, vocab_size):
super(RNN_Decoder, self).__init__()
self.units = units
self.embedding_dim = embedding_dim
self.vocab_size = vocab_size
self.embedding = Embedding(input_dim=self.vocab_size,
output_dim=self.embedding_dim)
def build(self, input_shape):
self.gru1 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru2 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru3 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru4 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.fc1 = Dense(self.units)
self.fc2 = Dense(self.vocab_size)
def call(self, x, initial_zero=False):
# x, (batch, 512)
# hidden, (batch, 256)
if initial_zero:
initial_state = decoder.reset_state(batch_size=x.shape[0])
output, state = self.gru1(inputs=x,
initial_state=initial_state)
output, state = self.gru2(inputs=output,
initial_state=initial_state)
output, state = self.gru3(inputs=output,
initial_state=initial_state)
output, state = self.gru4(inputs=output,
initial_state=initial_state)
else:
output, state = self.gru1(inputs=x)
output, state = self.gru2(inputs=output)
output, state = self.gru3(inputs=output)
output, state = self.gru4(inputs=output)
# output, (batch, 256)
x = self.fc1(output)
x = self.fc2(x)
return x, state
def embed(self, x):
return self.embedding(x)
def reset_state(self, batch_size):
return tf.zeros((batch_size, self.units))
训练
我们创建类对象并把我们的优化器(optimizer)赋值为Adam(自适应矩估计)。损失就是稀疏分类交叉熵(Sparse Categorical Cross entropy),因为这里使用独热编码(one-hot-encoders)的话效率不高,实际情况就是这样。我们还将会使用掩码以协助掩蔽,这样就不会让序列模型在相同情况下学习过度拟合。
encoder = CNN_Encoder(EMBEDDIN_DIM)
decoder = RNN_Decoder(embedding_dim=EMBEDDIN_DIM,
units=UNITS_RNN,
vocab_size=VOCAB_SIZE)
optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
def loss_function(real, pred):
mask = tf.math.logical_not(tf.math.equal(real, 0))
loss_ = loss_object(real, pred)
mask = tf.cast(mask, dtype=loss_.dtype)
loss_ *= mask
return tf.reduce_mean(loss_)
接下来,我们编写训练步长函数,该函数将通过反向传播算法计算梯度。
@tf.function
def train_step(img_tensor, target):
# img_tensor (batch, 224,224,3)
# target (batch, 80)
loss = 0
with tf.GradientTape() as tape:
features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
em_words = decoder.embed(target)
x = tf.concat([features,em_words],axis=1)
predictions, _ = decoder(x, True)
loss = loss_function(target[:,1:], predictions[:,1:-1,:])
trainable_variables = encoder.trainable_variables + decoder.trainable_variables
gradients = tape.gradient(loss, trainable_variables)
optimizer.apply_gradients(zip(gradients, trainable_variables))
return loss
@tf.function
def val_step(img_tensor, target):
# img_tensor (batch, 224,224,3)
# target (batch, 80)
loss = 0
features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
em_words = decoder.embed(target)
x = tf.concat([features,em_words],axis=1)
predictions, _ = decoder(x, True)
loss = loss_function(target[:,1:], predictions[:,1:-1,:])
return loss
📉损失与结果
查看Kaggle笔记本
目标函数是生成的词语的负对数似然函数。为了让这更直观一点,我们在提供的架构中做一次前馈运行。当把一个图像输入到卷积神经网络,同时会提供对应的图像特征。然后图像特征被编码为一个与提供的词嵌入形状相同的张量。图像特征在第一个时间步被输入到门控循环单元(GRU)。然后,该单元格就生成整个词汇表的柔性最大值传输函数(softmax)。我们的任务目标是提高词语描述图像的贴切程度。我们用负对数似然函数,就是为了使该指标最小化并训练模型。在下一个时间步,我们把描述中的词语作为输入,并尝试最大程度地增加最接近的下一个词语的概率。
结论
通过利用从机器翻译中学到的简单概念,作者们真的想出了一个绝妙的方法来自动生成给定图像的描述。自动生成描述领域后续几篇论文的灵感就源自此篇论文,其中最著名的就是Show, Attend and Tell,顾名思义,该论文涉及了注意力机制以及本报告所探讨的这些概念。
与作者交流:
姓名 | GitHub | |
---|---|---|
Devjyoti Chakrobarty | @Cr0wley_zz | @cr0wley-zz |
Aritra Roy Gosthipaty | @ariG23498 | @ariG23498 |