掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务

从头开始在Python中开发深度学习字幕生成模型(上)

本文经机器之心(微信公众号:almosthuman2014)授权转载,禁止二次转载。

从头开始在Python中开发深度学习字幕生成模型(中)

从头开始在Python中开发深度学习字幕生成模型(下)

图像描述是一个有挑战性的人工智能问题,涉及为给定图像生成文本描述。

字幕生成是一个有挑战性的人工智能问题,涉及为给定图像生成文本描述。

一般图像描述或字幕生成需要使用计算机视觉方法来了解图像内容,也需要自然语言处理模型将对图像的理解转换成正确顺序的文字。近期,深度学习方法在该问题的多个示例上获得了顶尖结果。

深度学习方法在字幕生成问题上展现了顶尖的结果。这些方法最令人印象深刻的地方:给定一个图像,我们无需复杂的数据准备和特殊设计的流程,就可以使用端到端的方式预测字幕。

本教程将介绍如何从头开发能生成图像字幕的深度学习模型。

完成本教程,你将学会:

  • 如何为训练深度学习模型准备图像和文本数据。
  • 如何设计和训练深度学习字幕生成模型。
  • 如何评估一个训练后的字幕生成模型,并使用它为全新的图像生成字幕。


教程概览

该教程共分为 6 部分:

  • 1. 图像和字幕数据集
  • 2. 准备图像数据
  • 3. 准备文本数据
  • 4. 开发深度学习模型
  • 5. 评估模型
  • 6. 生成新的字幕

Python 环境

本教程假设你已经安装了 Python SciPy 环境,该环境完美适合 Python 3。你必须安装 Keras(2.0 版本或更高),TensorFlow 或 Theano 后端。本教程还假设你已经安装了 scikit-learn、Pandas、NumPy 和 Matplotlib 等科学计算与绘图软件库。

我推荐在 GPU 系统上运行代码。你可以在 Amazon Web Services 上用廉价的方式获取 GPU:如何在 AWS GPU 上运行 Jupyter noterbook

图像和字幕数据集

图像字幕生成可使用的优秀数据集有 Flickr8K 数据集。原因在于它逼真且相对较小,即使你的工作站使用的是 CPU 也可以下载它,并用于构建模型。

对该数据集的明确描述见 2013 年的论文《Framing Image Description as a Ranking Task: Data, Models and Evaluation Metrics》。

作者对该数据集的描述如下:

我们介绍了一种用于基于句子的图像描述和搜索的新型基准集合,包括 8000 张图像,每个图像有五个不同的字幕描述对突出实体和事件提供清晰描述。

图像选自六个不同的 Flickr 组,往往不包含名人或有名的地点,而是手动选择多种场景和情形。

该数据集可免费获取。你必须填写一份申请表,然后就可以通过电子邮箱收到数据集。申请表链接:https://illinois.edu/fb/sec/1713398

很快,你会收到电子邮件,包含以下两个文件的链接:

  • Flickr8k_Dataset.zip(1 Gigabyte)包含所有图像。
  • Flickr8k_text.zip(2.2 Megabytes)包含所有图像文本描述。

下载数据集,并在当前工作文件夹里进行解压缩。你将得到两个目录:

  • Flicker8k_Dataset:包含 8092 张 JPEG 格式图像。
  • Flickr8k_text:包含大量不同来源的图像描述文件。

该数据集包含一个预制训练数据集(6000 张图像)、开发数据集(1000 张图像)和测试数据集(1000 张图像)。

用于评估模型技能的一个指标是 BLEU 值。对于推断,下面是一些精巧的模型在测试数据集上进行评估时获得的大概 BLEU 值(来源:2017 年论文《Where to put the Image in an Image Caption Generator》):

  • BLEU-1: 0.401 to 0.578.
  • BLEU-2: 0.176 to 0.390.
  • BLEU-3: 0.099 to 0.260.
  • BLEU-4: 0.059 to 0.170.

稍后在评估模型部分将详细介绍 BLEU 值。下面,我们来看一下如何加载图像。

准备图像数据

我们将使用预训练模型解析图像内容,且目前有很多可选模型。在这种情况下,我们将使用 Oxford Visual Geometry Group 或 VGG(该模型赢得了 2014 年 ImageNet 竞赛冠军)。

Keras 可直接提供该预训练模型。注意,第一次使用该模型时,Keras 将从互联网上下载模型权重,大概 500Megabytes。这可能需要一段时间(时间长度取决于你的网络连接)。

我们可以将该模型作为更大的图像字幕生成模型的一部分。问题在于模型太大,每次我们想测试新语言模型配置(下行)时在该网络中运行每张图像非常冗余。

我们可以使用预训练模型对「图像特征」进行预计算,并保存至文件中。然后加载这些特征,将其馈送至模型中作为数据集中给定图像的描述。在完整的 VGG 模型中运行图像也是这样,我们需要提前运行该步骤。

优化可以加快模型训练过程,消耗更少内存。我们可以使用 VGG class 在 Keras 中运行 VGG 模型。我们将移除加载模型的最后一层,因为该层用于预测图像的分类。我们对图像分类不感兴趣,我们感兴趣的是分类之前图像的内部表征。这些就是模型从图像中提取出的「特征」。

Keras 还提供工具将加载图像改造成模型的偏好大小(如 3 通道 224 x 224 像素图像)。

下面是 extract_features() 函数,即给出一个目录名,该函数将加载每个图像、为 VGG 准备图像数据,并从 VGG 模型中收集预测到的特征。图像特征是包含 4096 个元素的向量,该函数向图像特征返回一个图像标识符(identifier)词典。

# extract features from each photo in the directory
def extract_features(directory):
	# load the model
	model = VGG16()
	# re-structure the model
	model.layers.pop()
	model = Model(inputs=model.inputs, outputs=model.layers[-1].output)
	# summarize
	print(model.summary())
	# extract features from each photo
	features = dict()
	for name in listdir(directory):
		# load an image from file
		filename = directory + '/' + name
		image = load_img(filename, target_size=(224, 224))
		# convert the image pixels to a numpy array
		image = img_to_array(image)
		# reshape data for the model
		image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
		# prepare the image for the VGG model
		image = preprocess_input(image)
		# get features
		feature = model.predict(image, verbose=0)
		# get image id
		image_id = name.split('.')[0]
		# store feature
		features[image_id] = feature
		print('>%s' % name)
	return features

我们调用该函数为模型测试准备图像数据,然后将词典保存至 features.pkl 文件。

完整示例如下:

from os import listdir
from pickle import dump
from keras.applications.vgg16 import VGG16
from keras.preprocessing.image import load_img
from keras.preprocessing.image import img_to_array
from keras.applications.vgg16 import preprocess_input
from keras.models import Model

# extract features from each photo in the directory
def extract_features(directory):
	# load the model
	model = VGG16()
	# re-structure the model
	model.layers.pop()
	model = Model(inputs=model.inputs, outputs=model.layers[-1].output)
	# summarize
	print(model.summary())
	# extract features from each photo
	features = dict()
	for name in listdir(directory):
		# load an image from file
		filename = directory + '/' + name
		image = load_img(filename, target_size=(224, 224))
		# convert the image pixels to a numpy array
		image = img_to_array(image)
		# reshape data for the model
		image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
		# prepare the image for the VGG model
		image = preprocess_input(image)
		# get features
		feature = model.predict(image, verbose=0)
		# get image id
		image_id = name.split('.')[0]
		# store feature
		features[image_id] = feature
		print('>%s' % name)
	return features

# extract features from all images
directory = 'Flicker8k_Dataset'
features = extract_features(directory)
print('Extracted Features: %d' % len(features))
# save to file
dump(features, open('features.pkl', 'wb'))

运行该数据准备步骤可能需要一点时间,时间长度取决于你的硬件,带有 CPU 的现代工作站可能需要一个小时。

运行结束时,提取出的特征将存储在 features.pkl 文件中以备后用。该文件大概 127 Megabytes 大小。

准备文本数据

该数据集中每个图像有多个描述,文本描述需要进行最低限度的清洗。首先,加载包含所有文本描述的文件。

# load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

filename = 'Flickr8k_text/Flickr8k.token.txt'
# load descriptions
doc = load_doc(filename)

每个图像有一个独有的标识符,该标识符出现在文件名和文本描述文件中。

接下来,我们将逐步对图像描述进行操作。下面定义一个 load_descriptions() 函数:给出一个需要加载的文本文档,该函数将返回图像标识符词典。每个图像标识符映射到一或多个文本描述。

# extract descriptions for images
def load_descriptions(doc):
	mapping = dict()
	# process lines
	for line in doc.split('\n'):
		# split line by white space
		tokens = line.split()
		if len(line) < 2:
			continue
		# take the first token as the image id, the rest as the description
		image_id, image_desc = tokens[0], tokens[1:]
		# remove filename from image id
		image_id = image_id.split('.')[0]
		# convert description tokens back to string
		image_desc = ' '.join(image_desc)
		# create the list if needed
		if image_id not in mapping:
			mapping[image_id] = list()
		# store description
		mapping[image_id].append(image_desc)
	return mapping

# parse descriptions
descriptions = load_descriptions(doc)
print('Loaded: %d ' % len(descriptions))

下面,我们需要清洗描述文本。因为描述已经经过符号化,所以它十分易于处理。

我们将用以下方式清洗文本,以减少需要处理的词汇量:

  • 所有单词全部转换成小写。
  • 移除所有标点符号。
  • 移除所有少于或等于一个字符的单词(如 a)。
  • 移除所有带数字的单词。

下面定义了 clean_descriptions() 函数:给出描述的图像标识符词典,遍历每个描述,清洗文本。

import string

def clean_descriptions(descriptions):
	# prepare translation table for removing punctuation
	table = str.maketrans('', '', string.punctuation)
	for key, desc_list in descriptions.items():
		for i in range(len(desc_list)):
			desc = desc_list[i]
			# tokenize
			desc = desc.split()
			# convert to lower case
			desc = [word.lower() for word in desc]
			# remove punctuation from each token
			desc = [w.translate(table) for w in desc]
			# remove hanging 's' and 'a'
			desc = [word for word in desc if len(word)>1]
			# remove tokens with numbers in them
			desc = [word for word in desc if word.isalpha()]
			# store as string
			desc_list[i] =  ' '.join(desc)

# clean descriptions
clean_descriptions(descriptions)

清洗后,我们可以总结词汇量。

理想情况下,我们希望使用尽可能少的词汇而得到强大的表达性。词汇越少则模型越小、训练速度越快。

对于推断,我们可以将干净的描述转换成一个集,将它的规模打印出来,这样就可以了解我们的数据集词汇量的大小了。

# convert the loaded descriptions into a vocabulary of words
def to_vocabulary(descriptions):
	# build a list of all description strings
	all_desc = set()
	for key in descriptions.keys():
		[all_desc.update(d.split()) for d in descriptions[key]]
	return all_desc

# summarize vocabulary
vocabulary = to_vocabulary(descriptions)
print('Vocabulary Size: %d' % len(vocabulary))

最后,我们保存图像标识符词典和描述至一个新文本 descriptions.txt,该文件中每行只有一个图像和一个描述。

下面我们定义了 save_doc() 函数,即给出一个包含标识符和描述之间映射的词典和文件名,将该映射保存至文件中。

# save descriptions to file, one per line
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
save_doc(descriptions, 'descriptions.txt')

汇总起来,完整的函数定义如下所示:

import string

# load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

# extract descriptions for images
def load_descriptions(doc):
	mapping = dict()
	# process lines
	for line in doc.split('\n'):
		# split line by white space
		tokens = line.split()
		if len(line) < 2:
			continue
		# take the first token as the image id, the rest as the description
		image_id, image_desc = tokens[0], tokens[1:]
		# remove filename from image id
		image_id = image_id.split('.')[0]
		# convert description tokens back to string
		image_desc = ' '.join(image_desc)
		# create the list if needed
		if image_id not in mapping:
			mapping[image_id] = list()
		# store description
		mapping[image_id].append(image_desc)
	return mapping

def clean_descriptions(descriptions):
	# prepare translation table for removing punctuation
	table = str.maketrans('', '', string.punctuation)
	for key, desc_list in descriptions.items():
		for i in range(len(desc_list)):
			desc = desc_list[i]
			# tokenize
			desc = desc.split()
			# convert to lower case
			desc = [word.lower() for word in desc]
			# remove punctuation from each token
			desc = [w.translate(table) for w in desc]
			# remove hanging 's' and 'a'
			desc = [word for word in desc if len(word)>1]
			# remove tokens with numbers in them
			desc = [word for word in desc if word.isalpha()]
			# store as string
			desc_list[i] =  ' '.join(desc)

# convert the loaded descriptions into a vocabulary of words
def to_vocabulary(descriptions):
	# build a list of all description strings
	all_desc = set()
	for key in descriptions.keys():
		[all_desc.update(d.split()) for d in descriptions[key]]
	return all_desc

# save descriptions to file, one per line
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 = 'Flickr8k_text/Flickr8k.token.txt'
# load descriptions
doc = load_doc(filename)
# parse descriptions
descriptions = load_descriptions(doc)
print('Loaded: %d ' % len(descriptions))
# clean descriptions
clean_descriptions(descriptions)
# summarize vocabulary
vocabulary = to_vocabulary(descriptions)
print('Vocabulary Size: %d' % len(vocabulary))
# save to file
save_descriptions(descriptions, 'descriptions.txt')

运行示例首先打印出加载图像描述的数量(8092)和干净词汇量的规模(8763 个单词)。

Loaded: 8,092
Vocabulary Size: 8,763

最后,把干净的描述写入 descriptions.txt。

查看文件,我们能够看到该描述可用于建模。文件中描述的顺序可能会发生改变。

2252123185_487f21e336 bunch on people are seated in stadium
2252123185_487f21e336 crowded stadium is full of people watching an event
2252123185_487f21e336 crowd of people fill up packed stadium
2252123185_487f21e336 crowd sitting in an indoor stadium
2252123185_487f21e336 stadium full of people watch game
...

开发深度学习模型

本节我们将定义深度学习模型,在训练数据集上进行拟合。本节分为以下几部分:

  • 1. 加载数据。
  • 2. 定义模型。
  • 3. 拟合模型。
  • 4. 完成示例。

加载数据

首先,我们必须加载准备好的图像和文本数据来拟合模型。

我们将在训练数据集中的所有图像和描述上训练数据。训练过程中,我们计划在开发数据集上监控模型性能,使用该性能确定什么时候保存模型至文件。

训练和开发数据集已经预制好,并分别保存在 Flickr_8k.trainImages.txt 和 Flickr_8k.devImages.txt 文件中,二者均包含图像文件名列表。从这些文件名中,我们可以提取图像标识符,并使用它们为每个集过滤图像和描述。

如下所示,load_set() 函数将根据训练或开发集文件名加载一个预定义标识符集。

# load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

# load a pre-defined list of photo identifiers
def load_set(filename):
	doc = load_doc(filename)
	dataset = list()
	# process line by line
	for line in doc.split('\n'):
		# skip empty lines
		if len(line) < 1:
			continue
		# get the image identifier
		identifier = line.split('.')[0]
		dataset.append(identifier)
	return set(dataset)

现在,我们可以使用预定义训练或开发标识符集加载图像和描述了。

下面是 load_clean_descriptions() 函数,该函数从给定标识符集的 descriptions.txt 中加载干净的文本描述,并向文本描述列表返回标识符词典。

我们将要开发的模型能够生成给定图像的字幕,一次生成一个单词。先前生成的单词序列作为输入。因此,我们需要一个 first word 来开启生成步骤和一个 last word 来表示字幕生成结束。

我们将使用字符串 startseq 和 endseq 完成该目的。这些标记被添加至加载描述,像它们本身就是加载出的那样。在对文本进行编码之前进行该操作非常重要,这样这些标记才能得到正确编码。

# load clean descriptions into memory
def load_clean_descriptions(filename, dataset):
	# load document
	doc = load_doc(filename)
	descriptions = dict()
	for line in doc.split('\n'):
		# split line by white space
		tokens = line.split()
		# split id from description
		image_id, image_desc = tokens[0], tokens[1:]
		# skip images not in the set
		if image_id in dataset:
			# create list
			if image_id not in descriptions:
				descriptions[image_id] = list()
			# wrap description in tokens
			desc = 'startseq ' + ' '.join(image_desc) + ' endseq'
			# store
			descriptions[image_id].append(desc)
	return descriptions

接下来,我们可以为给定数据集加载图像特征。

下面定义了 load_photo_features() 函数,该函数加载了整个图像描述集,然后返回给定图像标识符集你感兴趣的子集。

这不是很高效,但是,这可以帮助我们启动,快速运行。

# load photo features
def load_photo_features(filename, dataset):
	# load all features
	all_features = load(open(filename, 'rb'))
	# filter features
	features = {k: all_features[k] for k in dataset}
	return features

我们可以在这里暂停一下,测试目前开发的所有内容。

完整的代码示例如下:

# load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

# load a pre-defined list of photo identifiers
def load_set(filename):
	doc = load_doc(filename)
	dataset = list()
	# process line by line
	for line in doc.split('\n'):
		# skip empty lines
		if len(line) < 1:
			continue
		# get the image identifier
		identifier = line.split('.')[0]
		dataset.append(identifier)
	return set(dataset)

# load clean descriptions into memory
def load_clean_descriptions(filename, dataset):
	# load document
	doc = load_doc(filename)
	descriptions = dict()
	for line in doc.split('\n'):
		# split line by white space
		tokens = line.split()
		# split id from description
		image_id, image_desc = tokens[0], tokens[1:]
		# skip images not in the set
		if image_id in dataset:
			# create list
			if image_id not in descriptions:
				descriptions[image_id] = list()
			# wrap description in tokens
			desc = 'startseq ' + ' '.join(image_desc) + ' endseq'
			# store
			descriptions[image_id].append(desc)
	return descriptions

# load photo features
def load_photo_features(filename, dataset):
	# load all features
	all_features = load(open(filename, 'rb'))
	# filter features
	features = {k: all_features[k] for k in dataset}
	return features

# load training dataset (6K)
filename = 'Flickr8k_text/Flickr_8k.trainImages.txt'
train = load_set(filename)
print('Dataset: %d' % len(train))
# descriptions
train_descriptions = load_clean_descriptions('descriptions.txt', train)
print('Descriptions: train=%d' % len(train_descriptions))
# photo features
train_features = load_photo_features('features.pkl', train)
print('Photos: train=%d' % len(train_features))

运行该示例首先在测试数据集中加载 6000 张图像标识符。这些特征之后将用于加载干净描述文本和预计算的图像特征。

Dataset: 6,000
Descriptions: train=6,000
Photos: train=6,000

描述文本在作为输入馈送至模型或与模型预测进行对比之前需要先编码成数值。

编码数据的第一步是创建单词到唯一整数值之间的持续映射。Keras 提供 Tokenizer class,可根据加载的描述数据学习该映射。

下面定义了用于将描述词典转换成字符串列表的 to_lines() 函数,和对加载图像描述文本拟合 Tokenizer 的 create_tokenizer() 函数。

# convert a dictionary of clean descriptions to a list of descriptions
def to_lines(descriptions):
	all_desc = list()
	for key in descriptions.keys():
		[all_desc.append(d) for d in descriptions[key]]
	return all_desc

# fit a tokenizer given caption descriptions
def create_tokenizer(descriptions):
	lines = to_lines(descriptions)
	tokenizer = Tokenizer()
	tokenizer.fit_on_texts(lines)
	return tokenizer

# prepare tokenizer
tokenizer = create_tokenizer(train_descriptions)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size: %d' % vocab_size)

我们现在对文本进行编码。

每个描述将被分割成单词。我们向该模型提供一个单词和图像,然后模型生成下一个单词。描述的前两个单词和图像将作为模型输入以生成下一个单词,这就是该模型的训练方式。

例如,输入序列「a little girl running in field」将被分割成 6 个输入-输出对来训练该模型:

X1,		X2 (text sequence), 						y (word)
photo	startseq, 									little
photo	startseq, little,							girl
photo	startseq, little, girl, 					running
photo	startseq, little, girl, running, 			in
photo	startseq, little, girl, running, in, 		field
photo	startseq, little, girl, running, in, field, endseq

稍后,当模型用于生成描述时,生成的单词将被连结起来,递归地作为输入以生成图像字幕。

下面是 create_sequences() 函数,给出 tokenizer、最大序列长度和所有描述和图像的词典,该函数将这些数据转换成输入-输出对来训练模型。该模型有两个输入数组:一个用于图像特征,一个用于编码文<

声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com

  • 个人/企业涉诉查询

    通过企业关键词查询企业涉松详情,如裁判文书、开庭公告、执行公告、失信公告、案件流程等等。

    通过企业关键词查询企业涉松详情,如裁判文书、开庭公告、执行公告、失信公告、案件流程等等。

  • 账号黑产风险识别

    根据手机号来查询是否命中黑产风险

    根据手机号来查询是否命中黑产风险

  • IP反查域名

    IP反查域名是通过IP查询相关联的域名信息的功能,它提供IP地址历史上绑定过的域名信息。

    IP反查域名是通过IP查询相关联的域名信息的功能,它提供IP地址历史上绑定过的域名信息。

  • 人脸卫士

    结合权威身份认证的精准人脸风险查询服务,提升人脸应用及身份认证生态的安全性。人脸风险情报库,覆盖范围广、准确性高,数据权威可靠。

    结合权威身份认证的精准人脸风险查询服务,提升人脸应用及身份认证生态的安全性。人脸风险情报库,覆盖范围广、准确性高,数据权威可靠。

  • 全国城市空气质量

    全国城市和站点空气质量查询,污染物浓度及空气质量分指数、空气质量指数、首要污染物及空气质量级别、健康指引及建议采取的措施等。

    全国城市和站点空气质量查询,污染物浓度及空气质量分指数、空气质量指数、首要污染物及空气质量级别、健康指引及建议采取的措施等。

0512-88869195
数 据 驱 动 未 来
Data Drives The Future