看图说话实战教程 | 第二节 | 图像及文本数据预处理
欢迎来到《看图说话实战教程》系列第二节。在这一节中,我们要为看图说话模型准备所需的图片及文本数据集。
准备图片数据
图片是看图说话模型训练的两个输入之一。任何神经网络模型的输入必须以向量的形式才能喂给模型。因此,需要采用某种方法把每张图片转换成一个固定大小向量,然后输入到神经网络中。
在这篇教程中,我们使用一个预训练好的模型来抽取图像中的信息,而不是让模型自己从头开始训练。这是迁移学习 (Transfer Learning) 的一种实现方式,将已有的知识迁移到另一个模型当中。这样做的一个好处是我们无需耗费大量资源重新学习图片知识,就能直接利用已有的知识来进行图像信息的抽取与理解。而且,这些模型一般是在大规模的图像数据集(如ImageNet)上进行训练的,本身已经包含了大量的图像知识。这里,我们使用赢得2014年ImageNet竞赛冠军的VGG16模型,详细的模型信息可以阅读原始论文《Very Deep Convolutional Networks for Large-Scale Visual Recognition》。也有其他的一些流行的模型可供选择,如InceptionV3、ResNet等,关于如何使用这些模型可以参见Keras的官方文档。
VGG16的模型结构如下图所示:
在Keras中使用VGG16模型代码如下:
# 加载VGG16模型
model = VGG16()
# 移除最后一层
model.layers.pop()
model = Model(inputs=model.inputs, outputs=model.layers[-1].output)
VGG16模型是训练在ImageNet数据集上的图像分类模型,在这篇教程中,我们的目的并不是进行图像分类,而是通过VGG16模型获取固定长度的包含图像特征信息的向量。所以,我们需要将其最后的softmax层移除,抽取的图片特征向量长度为4096维。
Keras框架已经提供了训练好的VGG16模型权重文件,大家可以直接拿来使用。如果是第一次使用这个模型,Keras框架会自动下载对应的模型文件,大小约为500MB。下载快慢取决于电脑网速。如果还是无法下载成功,大家可以直接打开此链接下载。下载好的文件保存在对应的目录下。
- Mac或Linux系统: ~/.keras/models
- Windows系统:C:\Users\你的用户名.keras\models
现在,我们把每张图片喂给这个模型,得到相应的4096维的特征向量,代码如下:
# 加载图片
image = load_img(filename, target_size=(224, 224))
# 转换图像像素值为NumPy数组类型
image = img_to_array(image)
# 扩展数据维度 [height, width, channels]->[1, height, width, channels]
image = np.expend_dims(image, axis=0)
# 将图片数处理成可被VGG16模型接受的格式
image = preprocess_input(image)
# 执行预测获取图像特征
feature = model.predict(image, verbose=0)
获得每张图片的特征向量后,我们需要将所有图片及对应的特征向量以字典的形式保存到文件中。当需要图像特征时,再加载这些特征并喂给我们的模型。其实这跟直接将VGG16模型集成到我们的模型中没有任何区别。这样做的好处在于可以减少模型的内存占用及加快模型的训练速度。
用VGG16模型提取所有图片的特征,完整的代码如下:
import os
import pickle
from keras.applications.vgg16 import VGG16, preprocess_input
from keras.preprocessing.image import load_img, img_to_array
# 定义一个指定目录下所有图片的特征抽取函数
def extract_features(directory):
# 加载VGG16模型
model = VGG16(weights='imagenet', include_top=False)
# 打印模型的结构概要信息
print(model.summary())
# 定义一个字典类型的变量存储每张图片对应的特征表示
features = dict()
# 列出指定目录下所有的图片文件
files = listdir(directory)
# 遍历所有的图片文件,通过进度条来显示跟踪处理进度
for i in tqdm(range(len(files)), total=len(files)):
# 加载图片
filename = os.path.join(directory, files[i])
image = load_img(filename, target_size=(224, 224))
# 转换图像像素值为NumPy数组类型
image = img_to_array(image)
# 扩展数据维度 [height, width, channels]->[1, height, width, channels]
image = np.expend_dims(image, axis=0)
# 将图片数处理成可被VGG16模型接受的格式
image = preprocess_input(image)
# 执行预测获取图像特征
feature = model.predict(image, verbose=0)
# 获取图片唯一标识
image_id = files[i].split('.')[0]
# 存储抽取的特征到字典中
features[image_id] = feature
return features
# call the extract_features function
directory = 'Flickr8k_Dataset'
features = extract_features(directory)
# 保存到文件中
pickle.dump(features, open("features.pkl", "wb"))
这个过程可能会花费大约一到两个小时,时间长短主要取决于你的工作机的性能。
准备文本数据
Flickr8K数据集中包含了对应图像的描述性文本,为了能够让我们的模型能够学习生成这样的一句话,我们需要对文本做一些处理。
1. 加载文件
Flickr8k.token.txt文件中包含了每张图片的唯一标识符及其对应的5条文字描述,如下:
1000268201_693b08cb0e.jpg#0 A child in a pink dress is climbing up a set of stairs in an entry way .
1000268201_693b08cb0e.jpg#1 A girl going into a wooden building .
1000268201_693b08cb0e.jpg#2 A little girl climbing into a wooden playhouse .
1000268201_693b08cb0e.jpg#3 A little girl climbing the stairs to her playhouse .
1000268201_693b08cb0e.jpg#4 A little girl in a pink dress going into a wooden cabin .
1001773457_577c3a7d70.jpg#0 A black dog and a spotted dog are fighting
1001773457_577c3a7d70.jpg#1 A black dog and a tri-colored dog playing with each other on the road .
1001773457_577c3a7d70.jpg#2 A black dog and a white dog with brown spots are staring at each other in the street .
1001773457_577c3a7d70.jpg#3 Two dogs of different breeds looking at each other on the road .
1001773457_577c3a7d70.jpg#4 Two dogs on pavement moving toward each other .
文件中每一行包含了图片名称、描述序号及描述文本,格式为 <image name>#i <caption>,。
首先,我们定义一个加载所有文字描述的函数,代码如下:
# 加载所有的文字描述到内存
def load_doc(filename):
# 以只读方式打开文件
file = open(filename, 'r')
# 读取所有文本
text = file.read()
# 关闭该文件
file.close()
return text
filename = 'Flick8k_text/Flickr8k.token.txt'
# 加载所有的文字描述
doc = load_doc(filename)
接下来,我们一步步遍历文件的每一行,并创建一个字典保存每张图片ID及其对应的文字描述列表,把文字描述与图片关联起来:
# 抽取每个图像对应的描述文件,并保存在字典中
def load_descriptions(doc):
mapping = dict()
# 遍历处理文件的每一行
for line in doc.split('\n'):
# 以空格拆分行,过滤掉字符数量小于2的行
tokens = line.split()
if len(line) < 2:
continue
# 取第一个token作为图片id,其余的作为该图片的文字描述
image_id, image_desc = tokens[0], tokens[1:]
image_id = image_id.split('.')[0]
# 将拆分后的文字描述的字符列表拼接成正常的字符串
image_desc = ' '.join(image_desc)
# 如果图像有多个文字描述,创建列表类型
if image_id not in mapping:
mapping[image_id] = list()
# 存储文字描述
mapping[image_id].append(image_desc)
return mapping
# 解析文字描述
descriptions = load_descriptions(doc)
print('Loaded: %d ' % len(descriptions))
字典变量descriptions的结果如下(部分):
descriptions['101654506_8eb26cfb60'] = ['A brown and white dog is running through the snow .', 'A dog is running in the snow', 'A dog running through snow .', 'a white and brown dog is running through a snow covered field .', 'The white and brown dog is running over the surface of the snow .']
执行结果为:
Loaded: 8,092
2. 数据清洗
接下来,我们需要对文字描述做一些清理工作,以减少字典的大小。处理方式包括:
- 将字母全部变成小写形式,如 (将’Hello’变成’hello’)
- 移除所有的标点符号、特殊字符,如 (’%’、’$’、’#'等)
- 移除只有一个字母组成的单词或字符
- 移除所有的数字及包含数字的单词,如 ('hey199’等)
数据清洗代码如下:
import string
# 定义一个函数来对文字描述做处理
def clean_descriptions(descriptions):
# 标点符号转换表
table = str.maketrans('', '', string.punctuation)
for key, desc_list in descriptions.items():
for i in range(len(desc_list)):
desc = desc_list[i]
desc = desc.split()
# 转换成小写字母
desc = [word.lower() for word in desc]
# 移除标点字符
desc = [w.translate(table) for w in desc]
# 移除单个字母的单词
desc = [word for word in desc if len(word) > 1]
# 移除数字
desc = [word for word in desc if word.isalpha()]
# 存储为字符串
desc_list[i] = ' '.join(desc)
# 调用函数
clean_descriptions(descriptions)
3. 生成词汇表
一旦清理完毕,我们就可以计算语料库中词汇表的大小。词汇表是训练深度学习模型必须的重要文件,其重要性等价于小时候学习汉字必备的新华字典。词汇表大小描绘了模型已经掌握的词汇量。它包含了在经过处理后语料库中的所有唯一的单词。理想状态下,词汇表既要有意义,又要大小适中。一般情况下,词汇表由常用的单词组成。如果词汇表文件包含了大量的不常用的单词,一方面会导致模型训练速度下降,另一方面增大模型需要学习的参数量。
# 将文字描述转换成单词组成的词汇表集合
def to_vocabulary(descriptions):
all_desc = set()
for key in descriptions.keys():
[all_desc.update(d.split()) for d in descriptions[key]]
return all_desc
# 调用函数
vocabulary = to_vocabulary(descriptions)
print('Vocabulary Size: %d ' % len(vocabulary))
输出结果如下:
Vocabulary Size: 8,763
4. 保存文字描述
最后,我们需要把字典descriptions保存为文件。
def save_descriptions(descriptions, filename):
lines = list()
for key, desc_list in descriptions.items():
for desc in desc_list:
lines.append(key + ' ' + desc)
data = '\n'.join(lines)
file = open(filename, 'w')
file.write(data)
file.close()
# 调用函数
save_descriptions(descriptions, 'descriptions.txt')
完整代码
这篇教程第一讲完整的代码如下:
# 加载所有的文字描述到内存
def load_doc(filename):
# 以只读方式打开文件
file = open(filename, 'r')
# 读取所有文本
text = file.read()
# 关闭该文件
file.close()
return text
# 抽取每个图像对应的描述文件,并保存在字典中
def load_descriptions(doc):
mapping = dict()
# 遍历处理文件的每一行
for line in doc.split('\n'):
# 以空格拆分行,过滤掉字符数量小于2的行
tokens = line.split()
if len(line) < 2:
continue
# 取第一个token作为图片id,其余的作为该图片的文字描述
image_id, image_desc = tokens[0], tokens[1:]
image_id = image_id.split('.')[0]
# 将拆分后的文字描述的字符列表拼接成正常的字符串
image_desc = ' '.join(image_desc)
# 如果图像有多个文字描述,创建列表类型
if image_id not in mapping:
mapping[image_id] = list()
# 存储文字描述
mapping[image_id].append(image_desc)
return mapping
# 将文字描述转换成单词组成的词汇表集合
def to_vocabulary(descriptions):
all_desc = set()
for key in descriptions.keys():
[all_desc.update(d.split()) for d in descriptions[key]]
return all_desc
def save_descriptions(descriptions, filename):
lines = list()
for key, desc_list in descriptions.items():
for desc in desc_list:
lines.append(key + ' ' + desc)
data = '\n'.join(lines)
file = open(filename, 'w')
file.write(data)
file.close()
filename = 'Flick8k_text/Flickr8k.token.txt'
# 加载所有的文字描述
doc = load_doc(filename)
# 解析文字描述
descriptions = load_descriptions(doc)
print('Loaded: %d ' % len(descriptions))
# 调用函数
vocabulary = to_vocabulary(descriptions)
print('Vocabulary Size: %d ' % len(vocabulary))
# 调用函数
save_descriptions(descriptions, 'descriptions.txt')
感谢您的阅读,敬请期待下一讲!希望您能这篇教程中受益匪浅!
想要了解更多的自然语言处理最新进展、技术干货及学习教程,欢迎关注微信公众号“语言智能技术笔记簿”或扫描二维码添加关注。