第22章 如何准备用于建模的图片字幕数据集
自动照片字幕是模型在给定照片的情况下自动生成人类可读的文本描述。这在人工智能领域中极具挑战性,需要结合使用计算机视觉领域的图像理解和来自自然语言处理领域的语言生成才能妥善处理。现在可以使用深度学习和免费提供的照片数据集及其描述来开发自己的图像字幕模型。在本教程中,您将了解如何准备照片和文本描述,以便开发深度学习的自动图片字幕生成模型。完成本教程后,您将了解:
- 关于Flickr8K数据集,包含8,000多张照片和每张照片最多5个字幕。
- 如何使用深度学习框架为模型准备和加载照片和文本数据。
- 如何在Keras中为两种不同类型的深度学习模型专门编码数据
22.1 教程概述
本教程分为以下几部分:
- 下载Flickr8K数据集
- 如何加载照片
- 预先计算照片特征
- 如何加载描述
- 准备说明文字
- 整个描述序列模型
- 逐字模型
- 渐进式加载
22.2 下载Flickr8K数据集
Flickr8K数据集是初学者入门图像字幕的一个很好的数据集,原因是它是来自真实生活并且相对较小,可以在使用CPU工作站上下载它并构建练习模型。数据集的权威性描述在论文Framing
Image Description as a Ranking Task: Data, Models and Evaluation Metrics from 2013。作者描述数据集如下:
我们为基于句子的图像描述和搜索引入了一个新的基准集合:8,000个图像每个图像与五个不同的字幕配对,并提供了对显着实体和事件的清晰描述。
...
这些图片是从六个不同的Flickr群组中挑选出来的,通常不包含任何知名的人物或地点,而是手工从描绘各种场景和情况的图片中选择出来的。
Framing Image Description as a Ranking Task: Data, Models and Evaluation Metrics, 2013.
数据集可免费获得,您必须填写一份申请表,并将通过电子邮件发送给您的数据集链接。我很乐意为您链接,但电子邮件地址明确要求:请不要重新分发数据集。您可以使用以下链接来请求数据集:
- 数据集申请表。
https://illinois.edu/fb/sec/1713398
在短时间内,您将收到一封电子邮件,其中包含指向两个文件的链接:
- Flickr8k Dataset.zip(1 Gigabyte)所有照片的存档。
- Flickr8k text.zip(2.2兆字节)照片的所有文字说明的存档。下载数据集并将其解压缩到当前工作目录中。您将有两个目录:
-
-
-
-
-
-
-
- 数据集:包含8000多张JPEG格式的照片(是的目录名称拼写为'Flicker'而不是'Flickr')。
- 文本:包含 许多包含照片不同描述来源的文件。接下来,我们来看看如何加载图像。
-
-
-
-
-
-
-
22.3 如何加载照片
在本节中,我们将开发一些代码来加载照片,以便与Python中的Keras深度学习库一起使用。图像文件名是唯一的图像标识符。例如,以下是图像文件名的示例:
990890291_afc72be141.jpg
99171998_7cc800ceef.jpg
99679241_adc853a5c0.jpg
997338199_7343367d7f.jpg
997722733_0cb5439472.jpg
代码清单22.1:照片文件名示例
Keras提供了load_img()函数,可用于将图像文件直接作为像素数组加载。
from keras.preprocessing.image import load_img
image = load_img( 'Flicker8k_Dataset/990890291_afc72be141.jpg' )
代码清单22.2:加载单张照片的示例
像素数据需要转换为NumPy阵列以便在Keras中使用。我们可以使用Keras的img_to_array()函数转换加载的数据。
from keras.preprocessing.image import img_to_array
image = img_to_array(image)
代码清单22.3:将照片转换为NumPy数组的示例
我们可能想要使用预定义的特征提取模型,例如在Image网络上训练的最先进的深度图像分类网络。牛津视觉几何组(VGG)模型很受欢迎,可用于Keras。如果我们决定在模型中使用这个预先训练的模型作为特征提取器,我们可以使用Keras中的preprocess_input()函数预处理模型的像素数据,例如:
from keras.applications.vgg16 import preprocess_input
# reshape data into a single sample of an image
image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
# prepare the image for the VGG model
image = preprocess_input(image)
代码清单22.4:为VGG16模型准备图像数据
我们可能还想强制加载照片以使其具有与VGG模型相同的像素尺寸,即224 x 224像素。我们可以在调用load_img()时这样做,例如:
image = load_img( 'Flicker8k_Dataset/990890291_afc72be141.jpg' , target_size=(224, 224))
代码清单22.5:将Keras中的图像加载到特定大小
我们可能想从图像文件名中提取唯一的图像标识符,可以通过将文件名字符串以“.”来拆分实现。结果数组的第一个元素就是图像的唯一标识符:
image_id = filename.split( '.' )[0]
代码清单22.6:从filename中检索图像标识符
我们可以将上面所有这些过程组织在一起,定义成一个函数,该函数从包含照片的目录加载所有图片,并按照VGG模型输入格式预处理所有照片,最后返回一个图片唯一标识的字典。
from os import listdir
from os import path
from keras.preprocessing.image import load_img
from keras.preprocessing.image import img_to_array
from keras.applications.vgg16 import preprocess_input
def load_photos(directory):
images = dict()
for name in listdir(directory):
# load an image from file
filename = path.join(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 image id
image_id = name.split( ' . ' )[0]
images[image_id] = image
return images
# load images
directory = 'Flicker8k_Dataset'
images = load_photos(directory)
print( 'Loaded Images: %d ' % len(images))
代码清单22.7:从文件加载照片的完整示例
运行此示例将打印已加载图像的数量。运行需要几分钟。
Loaded Images: 8091
代码清单22.8:从文件加载照片的示例输出
22.4 预先计算照片特征
可以使用预先训练的模型从数据集中的照片中提其取特征并将特征存储到文件中,这是一种效率,意味着,模型中从提取的照片特征转换为文本描述的语言部分的模型,可以独立于特征提取模型进行训练。好处是,在训练语言模型时,不需要同时加载预训练模型到内存中处理每张照片(预训练模型通常非常巨大)。
之后,特征提取模型和语言模型可以重新组合在一起,以便对新照片进行预测。在本节中,我们将扩展上一节中定义的照片加载函数,加载所有照片,使用预先训练的VGG模型提取其特征,并将提取的特征存储到可以用于训练语言模型的新文件中。第一步是加载VGG模型。这个模型Keras已封装,可以按如下方式加载。请注意,这将把50MB模型权重下载到您的计算机中(最新的notop的模型大小,完全模型是500MB+),这可能需要几分钟。
from keras.applications.vgg16 import VGG16
# load the model
in_layer = Input(shape=(224, 224, 3))
model = VGG16(include_top=False, input_tensor=in_layer, pooling= 'avg' )
model.summary()
代码清单22.9:加载VGG模式
这将加载VGG 16层模型。通过设置include_top=False从模型中删除两个Dense输出图层和分类输出图层,最终池化层的输出被视为从图像中提取的特征,接下来,我们可以像上一节一样遍历图像目录中的所有图像,并在模型上为每个准备好的图像调用predict()函数以获取提取的特征。然后可以将这些特征存储在以图像id为关键字的字典中。下面列出了完整的示例。
from os import listdir
from os import path
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.layers import Input
# extract features from each photo in the directory
def extract_features(directory):
# load the model
in_layer = Input(shape=(224, 224, 3))
model = VGG16(include_top=False, input_tensor=in_layer)
model.summary()
# extract features from each photo
features = dict()
for name in listdir(directory):
# load an image from file
filename = path.join(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' ))
代码清单22.10:预先计算VGG16照片函数的完整示例
这个例子可能需要一些时间才能完成,也许一个小时(建议下载放在你用户目录的.keras/models下,国内可能亚马逊云有时候被屏蔽)。提取所有特征后,特征字典存储在当前工作目录中的features.pkl文件中,稍后可以加载这些特征并将其用作训练语言模型的输入。您可以在Keras中尝试其他类型的预训练模型。
22.5 如何加载描述
花点时间谈谈描述是很重要的,文件Flickr8k.token.txt包含图像标识符列表(用于图像文件名)和标记化描述。每个图像都有多个描述。以下是文件中的描述示例,显示了单个图像的5种不同描述。
1305564994_00513f9a5b.jpg#0 A man in street racer armor is examining the tire of another racer 's motorbike .
1305564994_00513f9a5b.jpg#1 The two racers drove the white bike down the road .
1305564994_00513f9a5b.jpg#2 Two motorists are riding along on their vehicle that is oddly designed and colored .
1305564994_00513f9a5b.jpg#3 Two people are in a small race car driving by a green hill .
1305564994_00513f9a5b.jpg#4 Two people in racing uniforms in a street car .
代码清单22.11:原始照片描述的示例
ExpertAnnotations.txt文件指出每个图像的描述哪些是由专家编写的,哪些描述是由众包工作者根据要求编写的,最后,文件CrowdFlowerAnnotations.txt提供了群众工作者的频率,表明字幕是否适合每个图像。可以概率地解释这些频率。该论文的作者描述了注释如下:
......要求注释者写出描述描绘的场景、情境、事件和实体(人,动物,其他物
体)的句子。我们为每个图像收集了多个字幕,因为可以描述许多图像的方式存在相当大的差异。
Framing Image Description as a Ranking Task: Data, Models and Evaluation Metrics, 2013.
还有,还列出了在训练/测试分割中使用的照片标识符,以便您可以比较论文中报告的结果。第一步是决定使用哪些字幕,最简单的方法是对每张照片使用第一个描述,首先,我们需要一个函数将整个注释文件(Flickr8k.token.txt)加载到内存中。下面定义load_doc()函数完成加载文件的功能,参数是文件名,将文档作为字符串返回。
# 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
代码清单22.12:将文件加载到内存的函数
从上面的文件示例中可以看到,我们只需要用空格分割每一行,并将第一个元素作为图像标识符,其余元素作为图像描述。例如:
# split line by white space
tokens = line.split()
# take the first token as the image id, the rest as the description
image_id, image_desc = tokens[0], tokens[1:]
代码清单22.13:将一行拆分为标识符和描述的示例
然后我们可以通过删除文件扩展名和描述号来清理图像标识符。
# remove filename from image id
image_id = image_id.split( '.' )[0]
代码清单22.14:清理照片标识符的示例
我们还可以将描述标记重新组合成一个字符串,以便以后处理。
# convert description tokens back to string
image_desc = ' ' .join(image_desc)
代码清单22.15:将描述标记转换为字符串的示例
我们可以将所有这些组合成一个函数load_description():获取加载的文件,逐行处理,返回一个图像标识符和他的第一个描述的字典。
# 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)
# store the first description for each image
if image_id not in mapping:
mapping[image_id] = image_desc
return mapping
filename = 'Flickr8k_text/Flickr8k.token.txt'
doc = load_doc(filename)
descriptions = load_descriptions(doc)
print( 'Loaded: %d' % len(descriptions))
代码清单22.16:加载照片描述的完整示例
运行该示例将打印已加载的图像描述的数量。
Loaded: 8092
代码清单22.17:加载照片描述的示例输出
还有其他方法可以加载可能对数据更准确的描述。以上面的例子为起点,你可以自由发挥。
22.6 准备说明文字
描述是标记化的;这意味着每个标记由用空格分隔的单词组成,它还意味着标点符号被分隔为它们自己的标记,例如句点('.')和单词复数('s)的撇号。在模型中使用之前清理描述文本是个好主意。我们可以形成一些数据清理的想法:
- 将所有标记的大小写归一化为小写。
- 从标记中删除所有标点符号。
- 删除包含一个或多个字符的所有标记(删除标点符号后),例如'a'和悬挂的‘s’字符。
我们可以在一个函数中实现这些简单的清理操作,该函数清除上一节中加载的字典中的每个描述。下面定义了clean_description()函数,它将清理每个加载的描述。
# clean description text
def clean_descriptions(descriptions):
# prepare regex for char filtering
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
for key, desc in descriptions.items():
# tokenize
desc = desc.split()
# convert to lower case
desc = [word.lower() for word in desc]
# remove punctuation from each word
desc = [re_punc.sub('', w) for w in desc]
# remove hanging ' s ' and ' a '
desc = [word for word in desc if len(word) > 1]
# store as string
descriptions[key] = ' '.join(desc)
代码清单22.18:清理照片描述的函数
然后,我们可以将干净的文本保存到文件中,供我们的模型稍后使用:每行将包含图像标识符,后跟干净的描述。下面定义了save_doc()函数,用于将已清理的描述保存到文件中。
# save descriptions to file, one per line
def save_doc(descriptions, filename):
lines = list()
for key, desc in mapping.items():
lines.append(key + ' ' + desc)
data = '\n'.join(lines)
file = open(filename, 'w')
file.write(data)
file.close()
代码清单22.19:保存简洁描述的函数
将这一切与上一节中的描述加载一起,下面列出了完整的示例。
import string
import re
# 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)
# store the first description for each image
if image_id not in mapping:
mapping[image_id] = image_desc
return mapping
# clean description text
def clean_descriptions(descriptions):
# prepare regex for char filtering
re_punc = re.compile( '[%s]' % re.escape(string.punctuation))
for key, desc in descriptions.items():
# tokenize
desc = desc.split()
# convert to lower case
desc = [word.lower() for word in desc]
# remove punctuation from each word
desc = [re_punc.sub( '' , w) for w in desc]
# remove hanging ' s ' and ' a '
desc = [word for word in desc if len(word)>1]
# store as string
descriptions[key] = ' ' .join(desc)
# save descriptions to file, one per line
def save_doc(descriptions, filename):
lines = list()
for key, desc in descriptions.items():
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
all_tokens = ' ' .join(descriptions.values()).split()
vocabulary = set(all_tokens)
print( 'Vocabulary Size: %d' % len(vocabulary))
# save descriptions
save_doc(descriptions, 'descriptions.txt' )
代码清单22.20:清理照片描述的完整示例
运行该示例首先加载8,092个描述,清洗它们,汇总4,484个唯一单词的词汇表,然后将它们保存到名为descript.txt的新文件中
Loaded: 8092
Vocabulary Size: 4484
代码清单22.21:清理照片描述的示例输出
在文本编辑器中打开新文件description.txt并查看内容。您应该看到准备好进行建模的照片的可读描述。
1000268201_693b08cb0e child in pink dress is climbing up set of stairs in an entry way
1001773457_577c3a7d70 black dog and spotted dog are fighting
1002674143_1b742ab4b8 little girl covered in paint sits in front of painted rainbow with her hands in bowl
1003163366_44323f5815 man lays on bench while his dog sits by him
1007129816_e794419615 man in an orange hat starring at something
1007320043_627395c3d8 child playing on rope net
代码清单22.22:干净的照片描述示例
词汇量仍然相对较大,为了使建模更容易,特别是第一次,我建议删除仅在所有描述中出现一次或两次的单词,来进一步减少词汇量。
22.7 整个描述序列模型
有很多方法可以给字幕生成问题建立模型,一种简单的方法是创建一个一次性输出整个文本描述的模型。这是一个简单的模型,但它给模型带来了沉重的负担,他既要解释照片的意义,又要生成描述单词,还要将这些单词排列成正确的顺序。
图片字幕生成问题与编码器-解码器递归神经网络中使用的语言翻译问题没什么不同,都是在给定输入序列的编码的情况下,整个翻译的句子一次输出一个字,在这里,我们使用用于图像分类的预训练模型对图像进行编码,使用图像的编码在生成输出句子,可以例如在上述ImageNet模型上训练的VGG。
模型的输出将是词汇表中每个单词的概率分布,输入序列的长度就是最长的照片描述的长度,因此,图片描述需要首先转换成整数编码,词汇表中的每个单词被赋予一个唯一的整数,并且单词序列将被整数序列替换,然后,整数序列在转换成one-hot编码,以表示序列中每个单词的词汇表的理想化概率分布。我们可以使用Keras中的工具来准备此类模型的描述。第一步是将图像标识符和其干净描述的映射保存到description.txt中。
# 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 clean descriptions into memory
def load_clean_descriptions(filename):
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:]
# store
descriptions[image_id] = ' '.join(image_desc)
return descriptions
descriptions = load_clean_descriptions('descriptions.txt')
print(' Loaded %d ' % (len(descriptions)))
代码清单22.23:加载已清理的描述
运行此片段代码,将8,092张照片描述加载到一个图像标识符和它对应描述的字典,然后,使用这些标识符将每个照片文件加载作为模型的相应输入。
Loaded 8092
代码清单22.24:加载干净描述的示例输出
接下来,我们需要提取所有描述文本,以便我们对其进行编码。
# extract all text
desc_text = list(descriptions.values())
代码清单22.25:将加载的描述文本转换为列表。
我们可以使用Keras Tokenizer类将词汇表中的每个单词一致地映射到整数,首先,创建Tokenizer对象,然后拟合描述文本,接着将拟合标记模型保存到文件中,以便将预测的编码解码回词汇单词。
from keras.preprocessing.text import Tokenizer
# prepare tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(desc_text)
vocab_size = len(tokenizer.word_index) + 1
print( ' Vocabulary Size: %d ' % vocab_size)
代码22.26:拟合照片描述文本的Tokenizer。
接下来,我们可以使用拟合好的tokenizer的对象将照片描述编码为整数序列。
# integer encode descriptions
sequences = tokenizer.texts_to_sequences(desc_text)
代码清单22.27:编码描述文本到整数示例
该模型将要求作为训练数据的所有前一步的输出序列都具有相同长度,我们可以通过填充所有编码序列以使其与最长编码序列具有相同长度来实现此目的。我们可以在单词列表之后用0值填充序列。Keras提供pad_sequence()函数来填充序列。
from keras.preprocessing.sequence import pad_sequences
# pad all sequences to a fixed length
max_length = max(len(s) for s in sequences)
print( ' Description Length: %d ' % max_length)
padded = pad_sequences(sequences, maxlen=max_length, padding= 'post' )
代码清单22.28:填充描述到最大长度
最后,我们可以对填充序列进行one-hot编码,使序列中的每个字具有一个稀疏矢量。Keras提供了to_categorical()函数来执行此操作。
from keras.utils import to_categorical
# one hot encode
y = to_categorical(padded, num_classes=vocab_size)
代码清单22.29:一个one-hot编码输出文本的示例
编码后,我们可以确保序列输出数据具有正确的模型形状。
y = y.reshape((len(descriptions), max_length, vocab_size))
print(y.shape)
代码清单22.30:重塑编码文本的示例
- 将所有这些放在一起,下面列出了完整的示例。
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
# 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 clean descriptions into memory
def load_clean_descriptions(filename):
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:]
# store
descriptions[image_id] = ' ' .join(image_desc)
return descriptions
descriptions = load_clean_descriptions( 'descriptions.txt' )
print( 'Loaded %d' % (len(descriptions)))
# extract all text
desc_text = list(descriptions.values())
# prepare tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(desc_text)
vocab_size = len(tokenizer.word_index) + 1
print( 'Vocabulary Size: %d' % vocab_size)
# integer encode descriptions
sequences = tokenizer.texts_to_sequences(desc_text)
# pad all sequences to a fixed length
max_length = max(len(s) for s in sequences)
print( 'Description Length: %d' % max_length)
padded = pad_sequences(sequences, maxlen=max_length, padding= 'post' )
# one hot encode
y = to_categorical(padded, num_classes=vocab_size)
y = y.reshape((len(descriptions), max_length, vocab_size))
print(y.shape)
代码清单22.31:完整序列模型数据准备的完整示例
运行该示例,首先打印加载的图像描述的数量(8,092张照片),数据集词汇量大小(4,485个单词),最长描述的长度(28个单词),然后最终打印用于拟合预测模型的数据的形状。shape[samples, sequence length,features]。
Loaded 8092
Vocabulary Size: 4485
Description Length: 28
(8092, 28, 4485)
代码清单22.32:为整个序列预测模型准备数据的示例输出
如上所述,输出整个序列对于模型可能是具有挑战性的。我们将在下一节中介绍一个更简单的模型。
22.8 逐字模型
用于生成图像字幕的更简单的模型是这样的,其在被给定图像和生成的最后一个单词作为输入的情况下生成一个单词,然后递归地调用该模型生成描述中的每个单词,在这个模型中先前的预测会作为输入传给模型。使用单词作为输入,为模型提供强制上下文,以预测序列中的下一个单词。
这类模型在前几年的研究中经常使用,例如:Show and Tell: A Neural Image CaptionGenerator, 2015。单词嵌入层可用于表示输入单词,就像照片的特征提取模型一样,这也可以在大型语料库中或者在所有描述的数据集上进行预训练。
该模型用完整的单词序列作为输入;序列的长度将是数据集中描述的最大长度。该模型必须以某种方式开始:一种方法是每个照片描述以特定的标签来表示描述的开始和结束,例如STARTDESC和ENDDESC。例如,描述:
boy rides horse
代码清单22.33:照片描述的示例
会成为:
STARTDESC boy rides horse ENDDESC
代码清单22.34:包装照片描述的示例
然后用相同的图像输入到模型中,结果是如下的输入-输出词汇序列对:
Input (X), Output (y)
STARTDESC, boy
STARTDESC, boy, rides
STARTDESC, boy, rides, horse
STARTDESC, boy, rides, horse ENDDESC
代码清单22.35:包装描述的示例输入-输出对
数据准备工作将与上一节中描述的内容大致相同,每个描述必须是整数编码,在编码之后,序列被分成多个输入和输出对,并且只有输出字(y)是一个one-hot编码,这是因为该模型仅需要一次预测一个单词的概率分布,直到我们计算序列的最大长度代码都是相同的,。
# ...
descriptions = load_clean_descriptions( 'descriptions.txt' )
print( 'Loaded %d' % (len(descriptions)))
# extract all text
desc_text = list(descriptions.values())
# prepare tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(desc_text)
vocab_size = len(tokenizer.word_index) + 1
print( 'Vocabulary Size: %d' % vocab_size)
# integer encode descriptions
sequences = tokenizer.texts_to_sequences(desc_text)
# determine the maximum sequence length
max_length = max(len(s) for s in sequences)
print( 'Description Length: %d' % max_length)
代码清单22.36:加载和编码照片描述的示例
接下来,我们将每个整数编码序列分成输入和输出对,让我们在序列中的第i个单词中逐步执行一个名为seq的序列,其中i大于或等于1.首先,我们将第一个i-1个单词作为输入序列,将第i个单词作为输出字。
# split into input and output pair
in_seq, out_seq = seq[:i], seq[i]
代码清单22.37:拆分描述序列的示例
接下来,输入序列被填充到输入序列的最大长度。使用预填充(默认值),以便在序列的末尾显示新单词,而不是输入开头。
# pad input sequence
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
代码清单22.38:填充拆分描述序列的示例
输出字是一个one-hot编码,与上一节非常相似。
# encode output sequence
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
代码清单22.39:输出字的一个热编码示例
我们可以将所有这些放在一个完整的例子中,为逐字模型准备描述数据。
from numpy import array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
# 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 clean descriptions into memory
def load_clean_descriptions(filename):
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:]
# store
descriptions[image_id] = ' '.join(image_desc)
return descriptions
descriptions = load_clean_descriptions('descriptions.txt')
print('Loaded %d' % (len(descriptions)))
# extract all text
desc_text = list(descriptions.values())
# prepare tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(desc_text)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size: %d' % vocab_size)
# integer encode descriptions
sequences = tokenizer.texts_to_sequences(desc_text)
# determine the maximum sequence length
max_length = max(len(s) for s in sequences)
print('Description Length: %d' % max_length)
X, y = list(), list()
for img_no, seq in enumerate(sequences):
# split one sequence into multiple X,y pairs
for i in range(1, len(seq)):
# split into input and output pair
in_seq, out_seq = seq[:i], seq[i]
# pad input sequence
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
# encode output sequence
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
# store
X.append(in_seq)
y.append(out_seq)
# convert to numpy arrays
X, y = array(X), array(y)
print(X.shape)
print(y.shape)
代码清单22.40:逐字模型的数据准备的完整示例
运行该示例将打印相同的统计信息,但会打印生成的编码输入和输出序列的大小。请注意,图像的输入必须遵循完全相同的顺序,其中针对从单个描述中构造的实例都要显示相同的照片。一种方法是加载照片并将其与为从单个描述准备的每个示例建立对应关系。
Loaded 8092
Vocabulary Size: 4485
Description Length: 28
(66456, 28)
(66456, 4485)
代码清单22.41:逐字预测模型的数据准备输出示例21
22.9 渐进式加载
如果你有大量的RAM(例如8GB或更多),大多数现代系统都有,那么照片和描述的Flicr8K数据集可以放入RAM中。如果您想使用CPU训练深度学习模型,这很好。或者,您想使用GPU训练模型,那么您将无法一次性将数据放入普通GPU视频卡的内存中,一种解决方案是逐步加载照片和描述。
Keras通过在模型上使用fit_generator()函数来支持数据集地逐步加载,生成器是一个术语,用于描述一个函数,该函数按批次返回用于训练模型的样本,这类函数可以像独立函数一样简单,其名称在拟合模型时传递给fit_generator()函数。值得注意的是,模型需要训练多个迭代,其中一个迭代是单次遍历整个训练数据集,例如所有照片。一个迭代由多批次样本组成,其中模型权重在每批结束时都会更新。
生成器必须创建并生成一批样本,例如,数据集中的平均句子长度为11个字;这意味着每张照片将产生11个用于训练模型的样本,而两张照片将产生平均约22个样本。现代硬件的默认批量大小可能是32个样本,因此这是大约2-3张照片的样本数据。
我们可以编写一个自定义生成器来加载一些照片并将样本作为一个批次返回。让我们假设我们正在使用上一节中描述的逐字模型,该模型使用一系列单词和准备好的图像作为输入并预测单个单词。我们设计一个数据生成器,参数为加载的图像标识符和其干净描述的映射字典,一个训练好标记器,每个批次加载一个图像样本数据的最大序列长度。
生成器必须永远循环并产生每批样本数据,我们可以使用while循环永远循环,循环遍历图像目录中的每个图像,对于每个图像文件,加载图像并从图像的描述中创建所有输入-输出序列对。下面是数据生成器函数。
def data_generator(mapping, tokenizer, max_length):
# loop for ever over images
directory = 'Flicker8k_Dataset'
while 1:
for name in listdir(directory):
# load an image from file
filename = directory + '/' + name
image, image_id = load_image(filename)
# create word sequences
desc = mapping[image_id]
in_img, in_seq, out_word = create_sequences(tokenizer, max_length, desc, image)
yield [[in_img, in_seq], out_word]
代码清单22.42:渐进式加载的生成器示例
您还可以把图像目录名称作为参数来扩展这个函数,生成器返回一个数组,包含模型的输入(X)和输出(y),输入也是一个包含两个元素的数组:输入图像、图像描述单词编码序列。输出是一个one-hot编码的单词,你可以看到它调用一个名为load_image()的函数来加载一张照片并返回像素和图像标识符。这是本教程开头开发的照片加载函数的简化版本。
# load a single photo intended as input for the VGG feature extractor model
def load_image(filename):
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)[0]
# get image id
image_id = filename.split('/')[-1].split('.')[0]
return image, image_id
代码清单22.43:加载和准备照片的函数示例
调用另一个名为create_sequences()的函数,该函数创建图像序列、输入单词序列和输出单词,然后我们将其输出给调用者,其过程在上一节中已经详细说明了,这个函数还创建了图像像素的副本、根据每个照片的描述创建的输入 - 输出对。
# create sequences of images, input sequences and output words for an image
def create_sequences(tokenizer, max_length, descriptions, images):
Ximages, XSeq, y = list(), list(),list()
vocab_size = len(tokenizer.word_index) + 1
for j in range(len(descriptions)):
seq = descriptions[j]
image = images[j]
# integer encode
seq = tokenizer.texts_to_sequences([seq])[0]
# split one sequence into multiple X,y pairs
for i in range(1, len(seq)):
# select
in_seq, out_seq = seq[:i], seq[i]
# pad input sequence
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
# encode output sequence
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
# store
Ximages.append(image)
XSeq.append(in_seq)
y.append(out_seq)
Ximages, XSeq, y = array(Ximages), array(XSeq), array(y)
return Ximages, XSeq, y
代码清单22.44:准备描述文本的函数示例
在准备使用数据生成器的模型之前,我们必须加载干净的描述,准备标记生成器,并计算最大序列长度,这三个必须作为参数传递给data_generator()函数。我们使用先前开发的load_clean_description()函数和定义一个新的create_tokenizer()函数,它简化了tokenizer的创建。将所有过程结合在一起,下面列出了完整的数据生成器,随时可用于训练模型。
from os import listdir
from os import path
from numpy import array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from keras.preprocessing.image import load_img
from keras.preprocessing.image import img_to_array
from keras.applications.vgg16 import preprocess_input
# 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 clean descriptions into memory
def load_clean_descriptions(filename):
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:]
# store
descriptions[image_id] = ' '.join(image_desc)
return descriptions
# fit a tokenizer given caption descriptions
def create_tokenizer(descriptions):
lines = list(descriptions.values())
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# load a single photo intended as input for the VGG feature extractor model
def load_photo(filename):
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)[0]
# get image id
image_id = path.basename(filename).split('.')[0]
return image, image_id
# create sequences of images, input sequences and output words for an image
def create_sequences(tokenizer, max_length, desc, image):
Ximages, XSeq, y = list(), list(), list()
vocab_size = len(tokenizer.word_index) + 1
# integer encode the description
seq = tokenizer.texts_to_sequences([desc])[0]
# split one sequence into multiple X,y pairs
for i in range(1, len(seq)):
# select
in_seq, out_seq = seq[:i], seq[i]
# pad input sequence
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
# encode output sequence
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
# store
Ximages.append(image)
XSeq.append(in_seq)
y.append(out_seq)
Ximages, XSeq, y = array(Ximages), array(XSeq), array(y)
return [Ximages, XSeq, y]
# data generator, intended to be used in a call to model.fit_generator()
def data_generator(descriptions, tokenizer, max_length):
# loop for ever over images
directory = 'Flicker8k_Dataset'
while 1:
for name in listdir(directory):
# load an image from file
filename = path.join(directory, name)
image, image_id = load_photo(filename)
# create word sequences
desc = descriptions[image_id]
in_img, in_seq, out_word = create_sequences(tokenizer, max_length, desc, image)
yield [[in_img, in_seq], out_word]
# load mapping of ids to descriptions
descriptions = load_clean_descriptions('descriptions.txt')
# integer encode sequences of words
tokenizer = create_tokenizer(descriptions)
# pad to fixed length
max_length = max(len(s.split()) for s in list(descriptions.values()))
print('Description Length: %d' % max_length)
# test the data generator
generator = data_generator(descriptions, tokenizer, max_length)
inputs, outputs = next(generator)
print(inputs[0].shape)
print(inputs[1].shape)
print(outputs.shape)
代码清单22.45:渐进式加载的完整示例
可以通过调用next()函数来测试数据生成器。我们可以按如下方式测试生成器。
# test the data generator
generator = data_generator(descriptions, tokenizer, max_length)
inputs, outputs = next(generator)
print(inputs[0].shape)
print(inputs[1].shape)
print(outputs.shape)
代码清单22.46:测试自定义生成器函数的示例
(11, 224, 224, 3)
(11, 28)
(11, 4485)
代码清单22.47:测试生成器函数的示例输出
通过调用模型上的fit_generator()函数(而不是fit())并传入生成器,可以使用生成器来拟合模型。我们还必须指定每个迭代的step或批次数。我们可以将此估计为(训练数据集大小的10倍),如果使用7,000个图像进行训练,则可能估计为70,000。
# define model
# ...
# fit model
model.fit_generator(data_generator(descriptions, tokenizer, max_length),steps_per_epoch=70000,epochs=10)
代码清单22.48:在安装Keras模型时使用渐进式加载数据生成器的示例
0 条 查看最新 评论
没有评论