自然语言处理的数据集
原文:
machinelearningmastery.com/datasets-natural-language-processing/
在开始深入学习自然语言处理任务时,您需要数据集来练习。
最好使用可以快速下载的小型数据集,并且不需要太长时间来适应模型。此外,使用易于理解和广泛使用的标准数据集也很有帮助,这样您就可以比较结果,看看您是否在取得进展。
在这篇文章中,您将发现一套用于自然语言处理任务的标准数据集,您可以在深入学习入门时使用这些数据集。
概观
这篇文章分为 7 个部分;他们是:
- 文本分类
- 语言建模
- 图像标题
- 机器翻译
- 问题回答
- 语音识别
- 文件摘要
我试图提供一种混合的数据集,这些数据集很受欢迎,适用于规模适中的学术论文。
几乎所有数据集都可以免费下载。
如果您没有列出您最喜欢的数据集,或者您认为您知道应该列出的更好的数据集,请在下面的评论中告诉我。
让我们开始吧。
自然语言处理数据集
照格兰特,保留一些权利。
1.文本分类
文本分类是指标记句子或文档,例如电子邮件垃圾邮件分类和情感分析。
下面是一些很好的初学者文本分类数据集。
- 路透社 Newswire 主题分类(路透社-21578)。 1987 年路透社出现的一系列新闻文件,按类别编制索引。 另见 RCV1,RCV2 和 TRC2 。
- IMDB 电影评论情感分类(斯坦福)。来自网站 imdb.com 的一系列电影评论及其积极或消极的情感。
- 新闻集团电影评论情感分类(康奈尔)。来自网站 imdb.com 的一系列电影评论及其积极或消极的情感。
有关更多信息,请参阅帖子:
2.语言建模
语言建模涉及开发一种统计模型,用于预测句子中的下一个单词或单词中的下一个单词。它是语音识别和机器翻译等任务中的前置任务。
它是语音识别和机器翻译等任务中的前置任务。
下面是一些很好的初学者语言建模数据集。
- Project Gutenberg ,这是一系列免费书籍,可以用纯文本检索各种语言。
还有更多正式的语料库得到了很好的研究;例如:
- 布朗大学现代美国英语标准语料库。大量英语单词样本。
- 谷歌 10 亿字语料库。
3.图像标题
图像字幕是为给定图像生成文本描述的任务。
下面是一些很好的初学者图像字幕数据集。
- 上下文中的通用对象(COCO)。包含超过 12 万张描述图像的集合
- Flickr 8K 。从 flickr.com 获取的 8 千个描述图像的集合。
- Flickr 30K 。从 flickr.com 获取的 3 万个描述图像的集合。
欲了解更多,请看帖子:
- 探索图像字幕数据集,2016 年
4.机器翻译
机器翻译是将文本从一种语言翻译成另一种语言的任务。
下面是一些很好的初学者机器翻译数据集。
- 加拿大第 36 届议会的协调议长。成对的英语和法语句子。
- 欧洲议会诉讼平行语料库 1996-2011 。句子对一套欧洲语言。
有大量标准数据集用于年度机器翻译挑战;看到:
5.问题回答
问答是一项任务,其中提供了一个句子或文本样本,从中提出问题并且必须回答问题。
下面是一些很好的初学者问题回答数据集。
- 斯坦福问题答疑数据集(SQuAD)。回答有关维基百科文章的问题。
- Deepmind Question Answering Corpus 。从每日邮报回答有关新闻文章的问题。
- 亚马逊问答数据。回答关于亚马逊产品的问题。
有关更多信息,请参阅帖子:
6.语音识别
语音识别是将口语的音频转换为人类可读文本的任务。
下面是一些很好的初学者语音识别数据集。
- TIMIT 声 - 语音连续语音语料库。不是免费的,但因其广泛使用而上市。口语美国英语和相关的转录。
- VoxForge 。用于构建用于语音识别的开源数据库的项目。
- LibriSpeech ASR 语料库。从 LibriVox 中收集的大量英语有声读物。
你知道一些更好的自动语音识别数据集吗?
请在评论中告诉我。
7.文件摘要
文档摘要是创建较大文档的简短有意义描述的任务。
下面是一些很好的初学者文档摘要数据集。
- 法律案例报告数据集。收集了 4000 份法律案件及其摘要。
- TIPSTER 文本摘要评估会议语料库。收集了近 200 份文件及其摘要。
- 英语新闻文本的 AQUAINT 语料库。不是免费的,而是广泛使用的。新闻文章的语料库。
欲了解更多信息:
进一步阅读
如果您希望更深入,本节提供了其他数据集列表。
- 维基百科研究中使用的文本数据集
- 数据集:计算语言学家和自然语言处理研究人员使用的主要文本语料库是什么?
- 斯坦福统计自然语言处理语料库
- 按字母顺序排列的 NLP 数据集
- NLTK Corpora
- DL4J 深度学习开放数据
你知道其他任何自然语言处理数据集的好名单吗?
请在下面的评论中告诉我。
摘要
在这篇文章中,您发现了一套标准数据集,您可以在深入学习入门时用于自然语言处理任务。
你选择了一个数据集吗?您使用上述数据集之一吗?
请在下面的评论中告诉我。
如何开发一种深度学习的词袋模型来预测电影评论情感
原文:
machinelearningmastery.com/deep-learning-bag-of-words-model-sentiment-analysis/
电影评论可以被分类为有利或无。
电影评论文本的评估是一种通常称为情感分析的分类问题。用于开发情感分析模型的流行技术是使用词袋模型,其将文档转换为向量,其中文档中的每个单词被分配分数。
在本教程中,您将了解如何使用词袋表示形成电影评论情感分类来开发深度学习预测模型。
完成本教程后,您将了解:
- 如何准备评论文本数据以便使用受限词汇表进行建模。
- 如何使用词袋模型来准备训练和测试数据。
- 如何开发多层 Perceptron 词袋模型并使用它来预测新的评论文本数据。
让我们开始吧。
- 2017 年 10 月更新:修正了加载和命名正面和负面评论时的小错字(感谢 Arthur)。
如何开发一种用于预测电影评论情感的深度学习词袋模型
jai Mansson 的照片,保留一些权利。
教程概述
本教程分为 4 个部分;他们是:
- 电影评论数据集
- 数据准备
- 词袋表示
- 情感分析模型
电影评论数据集
电影评论数据是 Bo Pang 和 Lillian Lee 在 21 世纪初从 imdb.com 网站上检索到的电影评论的集合。收集的评论作为他们自然语言处理研究的一部分。
评论最初于 2002 年发布,但更新和清理版本于 2004 年发布,称为“v2.0”。
该数据集包含 1,000 个正面和 1,000 个负面电影评论,这些评论来自 imdb.com 上托管的 rec.arts.movies.reviews 新闻组的存档。作者将此数据集称为“极性数据集”。
我们的数据包含 2000 年之前写的 1000 份正面和 1000 份负面评论,每位作者的评论上限为 20(每位作者共 312 位)。我们将此语料库称为极性数据集。
- 感伤教育:基于最小削减的主观性总结的情感分析,2004。
数据已经有所清理,例如:
- 数据集仅包含英语评论。
- 所有文本都已转换为小写。
- 标点符号周围有空格,如句号,逗号和括号。
- 文本每行被分成一个句子。
该数据已用于一些相关的自然语言处理任务。对于分类,经典模型(例如支持向量机)对数据的表现在高 70%至低 80%(例如 78%-82%)的范围内。
更复杂的数据准备可以看到高达 86%的结果,交叉验证 10 倍。如果我们想在现代方法的实验中使用这个数据集,这给了我们 80 年代中期的球场。
…根据下游极性分类器的选择,我们可以实现高度统计上的显着改善(从 82.8%到 86.4%)
- 感伤教育:基于最小削减的主观性总结的情感分析,2004。
您可以从此处下载数据集:
- 电影评论 Polarity Dataset (review_polarity.tar.gz,3MB)
解压缩文件后,您将有一个名为“txt_sent
oken”的目录,其中包含两个子目录,其中包含文本“neg
”和“pos
”消极和积极的评论。对于每个 neg 和 pos,每个文件存储一个评论约定cv000
到cv999
。
接下来,我们来看看加载和准备文本数据。
数据准备
在本节中,我们将看看 3 件事:
- 将数据分成训练和测试集。
- 加载和清理数据以删除标点符号和数字。
- 定义首选词汇的词汇。
分为训练和测试装置
我们假装我们正在开发一种系统,可以预测文本电影评论的情感是积极的还是消极的。
这意味着在开发模型之后,我们需要对新的文本评论做出预测。这将要求对这些新评论执行所有相同的数据准备,就像对模型的训练数据执行一样。
我们将通过在任何数据准备之前拆分训练和测试数据集来确保将此约束纳入我们模型的评估中。这意味着在数据准备和模型训练期间,测试集中可以帮助我们更好地准备数据(例如使用的单词)的任何知识都是不可用的。
话虽如此,我们将使用最近 100 次正面评论和最后 100 次负面评论作为测试集(100 条评论),其余 1,800 条评论作为训练数据集。
这是 90%的训练,10%的数据分割。
通过使用评论的文件名可以轻松实现拆分,其中评论为 000 至 899 的评论用于训练数据,而评论为 900 以上的评论用于测试模型。
装载和清洁评论
文本数据已经相当干净,因此不需要太多准备工作。
在不了解细节的情况下,我们将使用以下方法准备数据:
- 在白色空间的分裂标记。
- 从单词中删除所有标点符号。
- 删除所有不完全由字母字符组成的单词。
- 删除所有已知停用词的单词。
- 删除长度为< = 1 个字符的所有单词。
我们可以将所有这些步骤放入一个名为 clean_doc()的函数中,该函数将从文件加载的原始文本作为参数,并返回已清理的标记列表。我们还可以定义一个函数 load_doc(),它从文件中加载文档,以便与 clean_doc()函数一起使用。
下面列出了清理第一次正面评价的示例。
from nltk.corpus import stopwords
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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', string.punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load the document
filename = 'txt_sentoken/pos/cv000_29590.txt'
text = load_doc(filename)
tokens = clean_doc(text)
print(tokens)
运行该示例会打印一长串清洁令牌。
我们可能想要探索更多的清洁步骤,并将其作为进一步的练习。我很想知道你能想出什么。
...
'creepy', 'place', 'even', 'acting', 'hell', 'solid', 'dreamy', 'depp', 'turning', 'typically', 'strong', 'performance', 'deftly', 'handling', 'british', 'accent', 'ians', 'holm', 'joe', 'goulds', 'secret', 'richardson', 'dalmatians', 'log', 'great', 'supporting', 'roles', 'big', 'surprise', 'graham', 'cringed', 'first', 'time', 'opened', 'mouth', 'imagining', 'attempt', 'irish', 'accent', 'actually', 'wasnt', 'half', 'bad', 'film', 'however', 'good', 'strong', 'violencegore', 'sexuality', 'language', 'drug', 'content']
定义词汇表
在使用词袋模型时,定义已知单词的词汇表很重要。
单词越多,文档的表示越大,因此将单词限制为仅被认为具有预测性的单词是很重要的。这很难事先知道,并且通常重要的是测试关于如何构建有用词汇的不同假设。
我们已经看到了如何从上一节中的词汇表中删除标点符号和数字。我们可以对所有文档重复此操作,并构建一组所有已知单词。
我们可以开发一个词汇作为 _ 计数器 _,这是一个词典及其计数的字典映射,可以让我们轻松更新和查询。
每个文档都可以添加到计数器(一个名为add_doc_to_vocab()
的新函数),我们可以跳过负目录中的所有评论,然后是肯定目录(一个名为 process_docs 的新函数) ())。
下面列出了完整的示例。
from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
# 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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load doc and add to vocab
def add_doc_to_vocab(filename, vocab):
# load doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# update counts
vocab.update(tokens)
# load all docs in a directory
def process_docs(directory, vocab):
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# add doc to vocab
add_doc_to_vocab(path, vocab)
# define vocab
vocab = Counter()
# add all docs to vocab
process_docs('txt_sentoken/pos', vocab)
process_docs('txt_sentoken/neg', vocab)
# print the size of the vocab
print(len(vocab))
# print the top words in the vocab
print(vocab.most_common(50))
运行该示例表明我们的词汇量为 43,476 个单词。
我们还可以看到电影评论中前 50 个最常用单词的样本。
请注意,此词汇表仅基于训练数据集中的那些评论构建。
44276
[('film', 7983), ('one', 4946), ('movie', 4826), ('like', 3201), ('even', 2262), ('good', 2080), ('time', 2041), ('story', 1907), ('films', 1873), ('would', 1844), ('much', 1824), ('also', 1757), ('characters', 1735), ('get', 1724), ('character', 1703), ('two', 1643), ('first', 1588), ('see', 1557), ('way', 1515), ('well', 1511), ('make', 1418), ('really', 1407), ('little', 1351), ('life', 1334), ('plot', 1288), ('people', 1269), ('could', 1248), ('bad', 1248), ('scene', 1241), ('movies', 1238), ('never', 1201), ('best', 1179), ('new', 1140), ('scenes', 1135), ('man', 1131), ('many', 1130), ('doesnt', 1118), ('know', 1092), ('dont', 1086), ('hes', 1024), ('great', 1014), ('another', 992), ('action', 985), ('love', 977), ('us', 967), ('go', 952), ('director', 948), ('end', 946), ('something', 945), ('still', 936)]
我们可以逐步浏览词汇表并删除所有发生率较低的单词,例如仅在所有评论中使用一次或两次。
例如,以下代码段将仅检索在所有评论中出现 2 次或更多次的代币。
# keep tokens with a min occurrence
min_occurane = 2
tokens = [k for k,c in vocab.items() if c >= min_occurane]
print(len(tokens))
使用此添加运行上面的示例表明,词汇量大小略大于其大小的一半,从 43,476 到 25,767 个单词。
25767
最后,可以将词汇表保存到名为 vocab.txt 的新文件中,以后我们可以加载并使用它来过滤电影评论,然后再对其进行编码以进行建模。我们定义了一个名为 save_list()的新函数,它将词汇表保存到文件中,每个文件只有一个单词。
例如:
# save list to file
def save_list(lines, filename):
# convert lines to a single blob of text
data = '\n'.join(lines)
# open file
file = open(filename, 'w')
# write text
file.write(data)
# close file
file.close()
# save tokens to a vocabulary file
save_list(tokens, 'vocab.txt')
在词汇表上运行最小出现过滤器并将其保存到文件,您现在应该有一个名为vocab.txt
的新文件,其中只包含我们感兴趣的词。
文件中的单词顺序会有所不同,但应如下所示:
aberdeen
dupe
burt
libido
hamlet
arlene
available
corners
web
columbia
...
我们现在已准备好从准备建模的评论中提取特征。
词袋表示
在本节中,我们将了解如何将每个评论转换为我们可以为多层感知机模型提供的表示。
词袋模型是一种从文本中提取特征的方法,因此文本输入可以与神经网络等机器学习算法一起使用。
每个文档(在这种情况下是评论)被转换为向量表示。表示文档的向量中的项目数对应于词汇表中的单词数。词汇量越大,向量表示越长,因此在前一部分中对较小词汇表的偏好。
对文档中的单词进行评分,并将分数放在表示中的相应位置。我们将在下一节中介绍不同的单词评分方法。
在本节中,我们关注的是将评论转换为准备用于训练第一神经网络模型的向量。
本节分为两个步骤:
- 将评论转换为代币行。
- 使用词袋模型表示编码评论。
对令牌行的评论
在我们将评论转换为向量进行建模之前,我们必须首先清理它们。
这涉及加载它们,执行上面开发的清洁操作,过滤掉不在所选词汇表中的单词,并将剩余的标记转换成准备编码的单个字符串或行。
首先,我们需要一个函数来准备一个文档。下面列出了函数 doc_to_line(),它将加载文档,清理它,过滤掉不在词汇表中的标记,然后将文档作为一串空白分隔的标记返回。
# load doc, clean and return line of tokens
def doc_to_line(filename, vocab):
# load the doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
接下来,我们需要一个函数来处理目录中的所有文档(例如’pos
’和’neg
’)将文档转换为行。
下面列出了process_docs()
函数,该函数执行此操作,期望将目录名称和词汇表设置为输入参数并返回已处理文档的列表。
# load all docs in a directory
def process_docs(directory, vocab):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
最后,我们需要加载词汇表并将其转换为用于清理评论的集合。
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
我们可以将所有这些放在一起,重复使用前面部分中开发的加载和清理功能。
下面列出了完整的示例,演示了如何从训练数据集准备正面和负面评论。
from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
# 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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load doc, clean and return line of tokens
def doc_to_line(filename, vocab):
# load the doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# load all docs in a directory
def process_docs(directory, vocab):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
# load all training reviews
positive_lines = process_docs('txt_sentoken/pos', vocab)
negative_lines = process_docs('txt_sentoken/neg', vocab)
# summarize what we have
print(len(positive_lines), len(negative_lines))
电影评论到词袋向量
我们将使用 Keras API 将评论转换为编码的文档向量。
Keras 提供 Tokenize 类,它可以执行我们在上一节中处理的一些清理和词汇定义任务。
最好自己做这件事,以确切知道做了什么以及为什么做。然而,Tokenizer 类很方便,很容易将文档转换为编码向量。
首先,必须创建 Tokenizer,然后适合训练数据集中的文本文档。
在这种情况下,这些是前一节中开发的positive_lines
和negative_lines
数组的聚合。
# create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
docs = positive_lines + negative_lines
tokenizer.fit_on_texts(docs)
此过程确定将词汇表转换为具有 25,768 个元素的固定长度向量的一致方式,这是词汇表文件vocab.txt
中的单词总数。
接下来,可以使用 Tokenizer 通过调用texts_to_matrix()
对文档进行编码。该函数接受要编码的文档列表和编码模式,这是用于对文档中的单词进行评分的方法。在这里,我们指定’freq
’根据文档中的频率对单词进行评分。
这可用于编码训练数据,例如:
# encode training data set
Xtrain = tokenizer.texts_to_matrix(docs, mode='freq')
print(Xtrain.shape)
这将对训练数据集中的所有正面和负面评论进行编码,并将所得矩阵的形状打印为 1,800 个文档,每个文档的长度为 25,768 个元素。它可以用作模型的训练数据。
(1800, 25768)
我们可以用类似的方式对测试数据进行编码。
首先,需要修改上一节中的process_docs()
函数,以仅处理测试数据集中的评论,而不是训练数据集。
我们通过添加is_trian
参数并使用它来决定要跳过哪些评论文件名来支持加载训练和测试数据集。
# load all docs in a directory
def process_docs(directory, vocab, is_trian):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_trian and filename.startswith('cv9'):
continue
if not is_trian and not filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
接下来,我们可以像在训练集中一样,在测试集中加载和编码正面和负面评论。
...
# load all test reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, False)
negative_lines = process_docs('txt_sentoken/neg', vocab, False)
docs = negative_lines + positive_lines
# encode training data set
Xtest = tokenizer.texts_to_matrix(docs, mode='freq')
print(Xtest.shape)
我们可以将所有这些放在一个例子中。
from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
# 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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load doc, clean and return line of tokens
def doc_to_line(filename, vocab):
# load the doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# load all docs in a directory
def process_docs(directory, vocab, is_trian):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_trian and filename.startswith('cv9'):
continue
if not is_trian and not filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
# load all training reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, True)
negative_lines = process_docs('txt_sentoken/neg', vocab, True)
# create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
docs = negative_lines + positive_lines
tokenizer.fit_on_texts(docs)
# encode training data set
Xtrain = tokenizer.texts_to_matrix(docs, mode='freq')
print(Xtrain.shape)
# load all test reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, False)
negative_lines = process_docs('txt_sentoken/neg', vocab, False)
docs = negative_lines + positive_lines
# encode training data set
Xtest = tokenizer.texts_to_matrix(docs, mode='freq')
print(Xtest.shape)
运行该示例分别打印编码的训练数据集和测试数据集的形状,分别具有 1,800 和 200 个文档,每个文档具有相同大小的编码词汇表(向量长度)。
(1800, 25768)
(200, 25768)
情感分析模型
在本节中,我们将开发多层感知机(MLP)模型,将编码文档分类为正面或负面。
模型将是简单的前馈网络模型,在 Keras 深度学习库中具有称为Dense
的完全连接层。
本节分为 3 个部分:
- 第一个情感分析模型
- 比较单词评分模式
- 预测新的评论
第一情感分析模型
我们可以开发一个简单的 MLP 模型来预测编码评论的情感。
模型将具有一个输入层,该输入层等于词汇表中的单词数,进而是输入文档的长度。
我们可以将它存储在一个名为n_words
的新变量中,如下所示:
n_words = Xtest.shape[1]
我们还需要所有训练和测试审核数据的类标签。我们确定性地加载并编码了这些评论(否定,然后是正面),因此我们可以直接指定标签,如下所示:
ytrain = array([0 for _ in range(900)] + [1 for _ in range(900)])
ytest = array([0 for _ in range(100)] + [1 for _ in range(100)])
我们现在可以定义网络。
发现所有模型配置的试验和错误非常少,不应该考虑针对此问题进行调整。
我们将使用具有 50 个神经元和整流线性激活函数的单个隐藏层。输出层是具有 S 形激活函数的单个神经元,用于预测 0 为阴性,1 为阳性评价。
将使用梯度下降的有效 Adam 实现和二元交叉熵损失函数来训练网络,适合于二分类问题。在训练和评估模型时,我们将跟踪准确率。
# define network
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compile network
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
接下来,我们可以将模型拟合到训练数据上;在这种情况下,该模型很小,很容易适应 50 个时代。
# fit network
model.fit(Xtrain, ytrain, epochs=50, verbose=2)
最后,一旦训练了模型,我们就可以通过在测试数据集中做出预测并打印精度来评估其表现。
# evaluate
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
print('Test Accuracy: %f' % (acc*100))
下面列出了完整的示例。
from numpy import array
from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
# 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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load doc, clean and return line of tokens
def doc_to_line(filename, vocab):
# load the doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# load all docs in a directory
def process_docs(directory, vocab, is_trian):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_trian and filename.startswith('cv9'):
continue
if not is_trian and not filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
# load all training reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, True)
negative_lines = process_docs('txt_sentoken/neg', vocab, True)
# create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
docs = negative_lines + positive_lines
tokenizer.fit_on_texts(docs)
# encode training data set
Xtrain = tokenizer.texts_to_matrix(docs, mode='freq')
ytrain = array([0 for _ in range(900)] + [1 for _ in range(900)])
# load all test reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, False)
negative_lines = process_docs('txt_sentoken/neg', vocab, False)
docs = negative_lines + positive_lines
# encode training data set
Xtest = tokenizer.texts_to_matrix(docs, mode='freq')
ytest = array([0 for _ in range(100)] + [1 for _ in range(100)])
n_words = Xtest.shape[1]
# define network
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compile network
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit network
model.fit(Xtrain, ytrain, epochs=50, verbose=2)
# evaluate
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
print('Test Accuracy: %f' % (acc*100))
运行该示例,我们可以看到该模型很容易适应 50 个时期内的训练数据,实现 100%的准确率。
在测试数据集上评估模型,我们可以看到模型表现良好,达到 90%以上的准确率,完全在原始论文中看到的 80-80 年代中期。
虽然,重要的是要注意这不是一个苹果对苹果的比较,因为原始论文使用 10 倍交叉验证来估计模型技能而不是单个训练/测试分裂。
...
Epoch 46/50
0s - loss: 0.0167 - acc: 1.0000
Epoch 47/50
0s - loss: 0.0157 - acc: 1.0000
Epoch 48/50
0s - loss: 0.0148 - acc: 1.0000
Epoch 49/50
0s - loss: 0.0140 - acc: 1.0000
Epoch 50/50
0s - loss: 0.0132 - acc: 1.0000
Test Accuracy: 91.000000
接下来,让我们看看为词袋模型测试不同的单词评分方法。
比较单词评分方法
Keras API 中 Tokenizer 的texts_to_matrix()
函数提供了 4 种不同的评分方法;他们是:
- “_ 二元 _”其中单词被标记为存在(1)或不存在(0)。
- “
count
”将每个单词的出现次数标记为整数。 - “
tfidf
”每个单词根据其频率进行评分,其中所有文档中共同的单词都会受到惩罚。 - “
freq
”根据文档中出现的频率对单词进行评分。
我们可以使用 4 种支持的单词评分模式中的每一种来评估上一节中开发的模型的技能。
这首先涉及开发一种函数,以基于所选择的评分模型来创建所加载文档的编码。该函数创建标记器,将其拟合到训练文档上,然后使用所选模型创建训练和测试编码。函数prepare_data()
在给定训练和测试文档列表的情况下实现此行为。
# prepare bag of words encoding of docs
def prepare_data(train_docs, test_docs, mode):
# create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
tokenizer.fit_on_texts(train_docs)
# encode training data set
Xtrain = tokenizer.texts_to_matrix(train_docs, mode=mode)
# encode training data set
Xtest = tokenizer.texts_to_matrix(test_docs, mode=mode)
return Xtrain, Xtest
我们还需要一个函数来评估给定特定数据编码的 MLP。
因为神经网络是随机的,当相同的模型适合相同的数据时,它们可以产生不同的结果。这主要是因为随机初始权重和小批量梯度下降期间的模式混洗。这意味着任何一个模型评分都是不可靠的,我们应该根据多次运行的平均值来估计模型技能。
下面的函数名为 evaluate_mode(),它通过在训练上训练它并在测试集上估计技能 30 次来获取编码文档并评估 MLP,并返回所有这些精度得分的列表。运行。
# evaluate a neural network model
def evaluate_mode(Xtrain, ytrain, Xtest, ytest):
scores = list()
n_repeats = 30
n_words = Xtest.shape[1]
for i in range(n_repeats):
# define network
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compile network
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit network
model.fit(Xtrain, ytrain, epochs=50, verbose=2)
# evaluate
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
scores.append(acc)
print('%d accuracy: %s' % ((i+1), acc))
return scores
我们现在准备评估 4 种不同单词评分方法的表现。
将所有这些结合在一起,下面列出了完整的示例。
from numpy import array
from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from pandas import DataFrame
from matplotlib import pyplot
# 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
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# remove punctuation from each token
table = str.maketrans('', '', punctuation)
tokens = [w.translate(table) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load doc, clean and return line of tokens
def doc_to_line(filename, vocab):
# load the doc
doc = load_doc(filename)
# clean doc
tokens = clean_doc(doc)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# load all docs in a directory
def process_docs(directory, vocab, is_trian):
lines = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_trian and filename.startswith('cv9'):
continue
if not is_trian and not filename.startswith('cv9'):
continue
# create the full path of the file to open
path = directory + '/' + filename
# load and clean the doc
line = doc_to_line(path, vocab)
# add to list
lines.append(line)
return lines
# evaluate a neural network model
def evaluate_mode(Xtrain, ytrain, Xtest, ytest):
scores = list()
n_repeats = 30
n_words = Xtest.shape[1]
for i in range(n_repeats):
# define network
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compile network
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit network
model.fit(Xtrain, ytrain, epochs=50, verbose=2)
# evaluate
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
scores.append(acc)
print('%d accuracy: %s' % ((i+1), acc))
return scores
# prepare bag of words encoding of docs
def prepare_data(train_docs, test_docs, mode):
# create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
tokenizer.fit_on_texts(train_docs)
# encode training data set
Xtrain = tokenizer.texts_to_matrix(train_docs, mode=mode)
# encode training data set
Xtest = tokenizer.texts_to_matrix(test_docs, mode=mode)
return Xtrain, Xtest
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
# load all training reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, True)
negative_lines = process_docs('txt_sentoken/neg', vocab, True)
train_docs = negative_lines + positive_lines
# load all test reviews
positive_lines = process_docs('txt_sentoken/pos', vocab, False)
negative_lines = process_docs('txt_sentoken/neg', vocab, False)
test_docs = negative_lines + positive_lines
# prepare labels
ytrain = array([0 for _ in range(900)] + [1 for _ in range(900)])
ytest = array([0 for _ in range(100)] + [1 for _ in range(100)])
modes = ['binary', 'count', 'tfidf', 'freq']
results = DataFrame()
for mode in modes:
# prepare data for mode
Xtrain, Xtest = prepare_data(train_docs, test_docs, mode)
# evaluate model on data for mode
results[mode] = evaluate_mode(Xtrain, ytrain, Xtest, ytest)
# summarize results
print(results.describe())
# plot results
results.boxplot()
pyplot.show()
运行该示例可能需要一段时间(在具有 CPU 的现代硬件上大约一个小时,而不是 GPU)。
在运行结束时,提供了每个单词评分方法的摘要统计,总结了每个模式 30 次运行中每个模型技能得分的分布。
我们可以看到’freq
’和’_ 二元 ‘方法的平均得分似乎优于’ 计数 _‘和’tfidf
’。
binary count tfidf freq
count 30.000000 30.00000 30.000000 30.000000
mean 0.915833 0.88900 0.856333 0.908167
std 0.009010 0.01012 0.013126 0.002451
min 0.900000 0.86500 0.830000 0.905000
25% 0.906250 0.88500 0.850000 0.905000
50% 0.915000 0.89000 0.857500 0.910000
75% 0.920000 0.89500 0.865000 0.910000
max 0.935000 0.90500 0.885000 0.910000
还给出了结果的盒子和须状图,总结了每种配置的准确度分布。
我们可以看到’freq’配置的分布是紧张的,这是令人鼓舞的,因为它也表现良好。此外,我们可以看到“二元”通过适度的传播实现了最佳结果,可能是此数据集的首选方法。
不同单词评分方法的模型准确率框和晶须图
预测新评论
最后,我们可以使用最终模型对新的文本评论做出预测。
这就是我们首先想要模型的原因。
预测新评论的情感涉及遵循用于准备测试数据的相同步骤。具体来说,加载文本,清理文档,通过所选词汇过滤标记,将剩余标记转换为线,使用 Tokenizer 对其进行编码,以及做出预测。
我们可以通过调用predict()
直接使用拟合模型预测类值,该值将返回一个值,该值可以舍入为 0 的整数表示负面评论,1 表示正面评论。
所有这些步骤都可以放入一个名为predict_sentiment()
的新函数中,该函数需要复习文本,词汇表,标记符和拟合模型,如下所示:
# classify a review as negative (0) or positive (1)
def predict_sentiment(review, vocab, tokenizer, model):
# clean
tokens = clean_doc(review)
# filter by vocab
tokens = [w for w in tokens if w in vocab]
# convert to line
line = ' '.join(tokens)
# encode
encoded = tokenizer.texts_to_matrix([line], mode='freq')
# prediction
yhat = model.predict(encoded, verbose=0)
return round(yhat[0,0])
我们现在可以对新的评论文本做出预测。
以下是使用频率词评分模式使用上面开发的简单 MLP 进行明确肯定和明显否定评论的示例。
# test positive text
text = 'Best movie ever!'
print(predict_sentiment(text, vocab, tokenizer, model))
# test negative text
text = 'This is a bad movie.'
print(predict_sentiment(text, vocab, tokenizer, model))
正确运行示例会对这些评论进行分类。
1
0
理想情况下,我们将模型放在所有可用数据(训练和测试)上以创建最终模型并将模型和标记器保存到文件中,以便可以在新软件中加载和使用它们。
扩展
如果您希望从本教程中获得更多信息,本节列出了一些扩展。
- 管理词汇。使用更大或更小的词汇进行探索。也许你可以通过一组较小的单词获得更好的表现。
- 调整网络拓扑。探索其他网络拓扑,例如更深或更广的网络。也许您可以通过更适合的网络获得更好的表现。
- 使用正则化。探索正规化技术的使用,例如丢失。也许您可以延迟模型的收敛并实现更好的测试集表现。
进一步阅读
如果您要深入了解,本节将提供有关该主题的更多资源。
数据集
- 电影评论数据
- 一种感伤教育:基于最小削减的主观性总结的情感分析,2004。
- 电影评论 Polarity Dataset (。tgz)
- 数据集自述文件 v2.0 和 v1.1 。
蜜蜂
摘要
在本教程中,您了解了如何开发一个词袋模型来预测电影评论的情感。
具体来说,你学到了:
- 如何准备评论文本数据以便使用受限词汇表进行建模。
- 如何使用词袋模型来准备训练和测试数据。
- 如何开发多层 Perceptron 词袋模型并使用它来预测新的评论文本数据。
你有任何问题吗?
在下面的评论中提出您的问题,我会尽力回答。
深度学习字幕生成模型的温和介绍
原文:
machinelearningmastery.com/deep-learning-caption-generation-models/
字幕生成是在给定照片的情况下生成人类可读的文本描述的具有挑战性的人工智能问题。
它需要来自计算机视觉领域的图像理解和来自自然语言处理领域的语言模型。
重要的是要考虑和测试多种方法来构建给定的预测性建模问题,并且确实有很多方法来构建为照片生成字幕的问题。
在本教程中,您将发现 3 种可以构建字幕生成方式以及如何为每种方法开发模型的方法。
我们将看到的三个字幕生成模型是:
- 模型 1:生成整个序列
- 模型 2:从 Word 生成 Word
- 模型 3:从序列生成单词
我们还将回顾一些在准备数据和开发字幕生成模型时要考虑的最佳实践。
让我们开始吧。
模型 1:生成整个序列
第一种方法涉及为给定照片的照片生成整个文本描述。
- 输入:照片
- 输出:完整的文字说明。
这是一对多序列预测模型,以一次性方式生成整个输出。
模型 1 - 生成整个序列
该模型给语言模型带来了沉重的负担,以正确的顺序生成正确的单词。
照片通过特征提取模型,例如在 ImageNet 数据集上预先训练的模型。
单热编码用于输出序列,允许模型预测整个词汇表中序列中每个单词的概率分布。
所有序列都填充到相同的长度。这意味着模型被迫在输出序列中生成多个“_ 无字 _”时间步长。
测试这种方法,我发现需要一个非常大的语言模型,即使这样,也很难超越生成 NLP 等效持久性的模型,例如:生成与整个序列长度重复的相同字作为输出。
模型 2:从 Word 生成 Word
这是一种不同的方法,其中 LSTM 生成给定照片和一个单词作为输入的一个单词的预测。
- 输入 1 :照片。
- 输入 2 :先前生成的单词或序列标记的开始。
- 输出:顺序的下一个字。
这是一对一序列预测模型,通过对模型的递归调用生成文本描述。
模型 2 - 从 Word 生成单词
单词输入是在第一次调用模型时指示序列开始的标记,或者是从上次调用模型时生成的单词。
照片通过特征提取模型,例如在 ImageNet 数据集上预先训练的模型。输入字是整数编码的,并通过字嵌入。
输出字是单热编码,以允许模型预测整个词汇表中单词的概率。
重复递归字生成过程,直到生成序列标记的结束。
测试这个方法,我发现模型确实产生了一些好的 n-gram 序列,但是在一个循环中被捕获,重复相同的单词序列以进行长描述。模型中没有足够的内存来记住之前生成的内容。
模型 3:从序列生成单词
给定照片和已经为照片生成的一系列单词作为输入,预测描述中的下一个单词。
- 输入 1 :照片。
- 输入 2 :先前生成的单词序列,或序列标记的开始。
- 输出:顺序的下一个字。
这是一个多对一序列预测模型,通过对模型的递归调用生成文本描述。
模型 3 - 从序列生成单词
它是上述模型 2 的概括,其中单词的输入序列为模型提供了用于生成序列中的下一个单词的上下文。
照片通过特征提取模型,例如在 ImageNet 数据集上预先训练的模型。可以在每个时间步骤中提供照片,或者在开始时提供一次,这可能是优选的方法。
输入序列被填充为固定长度和整数编码以通过字嵌入。
输出字是单热编码,以允许模型预测整个词汇表中单词的概率。
重复递归字生成过程,直到生成序列标记的结束。
这似乎是关于该主题的论文中描述的首选模型,并且可能是我们目前针对此类问题的最佳结构。
测试这种方法,我发现该模型很容易生成可读的描述,其质量通常由经过更长时间训练的大型模型改进。该模型技能的关键是屏蔽填充的输入序列。没有掩蔽,所产生的单词序列是可怕的,例如,序列标记的结束反复重复。
建模最佳实践
本节列出了开发字幕生成模型时的一些常规技巧。
- 预训练的照片特征提取模型。使用在 ImageNet 等大型数据集上预训练的照片特征提取模型。这称为转学习。 2014 年赢得 ImageNet 竞赛的牛津视觉几何组(VGG)模型是一个良好的开端。
- 预训练的单词嵌入模型。使用经过预先训练的单词嵌入模型,该模型使用平均大型语料库训练的向量或训练您的特定文本数据。
- Fine Tune 预训练模型。探索在您的模型中训练预训练模型,看看它们是否可以针对您的特定问题进行拨入,并导致技能略有提升。
- 预处理文本。预处理文本描述以减少要生成的单词的词汇量,进而减少模型的大小。
- 预处理照片。预处理照片特征提取模型的照片,甚至预提取特征,以便在训练模型时不需要完整的特征提取模型。
- 填充文字。将输入序列填充到固定长度;这实际上是为深度学习库向量化输入的要求。
- 掩蔽填充。在嵌入层上使用掩码忽略“_ 无字 _”时间步长,当字是整数编码时通常为零值。
- 注意。在生成输出字时要注意输入序列,以便在生成每个字时获得更好的表现并理解模型“_ 看 _”的位置。
- 评估。使用标准文本翻译指标(如 BLEU)评估模型,并将生成的描述与多个参考图像标题进行比较。
您是否有自己的最佳实践来开发强大的字幕模型?
请在下面的评论中告诉我。
进一步阅读
如果您要深入了解,本节将提供有关该主题的更多资源。
- Show and Tell:神经图像标题生成器,2015。
- 显示,参与和讲述:视觉注意的神经图像标题生成,2015。
- 将图像放在图像标题生成器中的位置,2017。
- 图像自动生成描述:模型,数据集和评估措施的调查,2016。
摘要
在本教程中,您发现了 3 个序列预测模型,可用于解决为照片生成人类可读文本描述的问题。
你有没有尝试过这些模型?
在下面的评论中分享您的经历。
你有任何问题吗?
在下面的评论中提出您的问题,我会尽力回答。
如何在 Keras 中定义神经机器翻译的编解码器序列到序列模型
编解码器模型提供了使用循环神经网络来解决具有挑战性的序列到序列预测问题(例如机器翻译)的模式。
可以在 Keras Python 深度学习库中开发编解码器模型,并且在 Keras 博客上描述了使用该模型开发的神经机器翻译系统的示例,其中示例代码与 Keras 项目一起分发。
在本文中,您将了解如何定义用于机器翻译的编解码器序列到序列预测模型,如 Keras 深度学习库的作者所述。
阅读这篇文章后,你会知道:
- 神奇机器翻译示例与 Keras 一起提供并在 Keras 博客上进行了描述。
- 如何正确定义编解码器 LSTM 以训练神经机器翻译模型。
- 如何正确定义推理模型以使用经过训练的编解码器模型来转换新序列。
让我们开始吧。
- 更新 Apr / 2018 :有关应用此复杂模型的示例,请参阅帖子:如何开发 Keras 中序列到序列预测的编解码器模型
如何在 Keras
中定义用于神经机器翻译的编解码器序列到序列模型 Tom Lee ,保留一些权利。
Keras 中的序列到序列预测
Keras 深度学习库的作者 Francois Chollet 最近发布了一篇博文,其中介绍了一个代码示例,用于开发一个序列到序列预测的编解码器 LSTM,标题为“ A ten - 对 Keras 中序列到序列学习的细致介绍。
博客文章中开发的代码也已添加到 Keras 中,作为文件 lstm_seq2seq.py 中的示例。
该帖子开发了编解码器 LSTM 的复杂实现,如关于该主题的规范论文中所述:
- 用神经网络进行序列学习的序列,2014。
- 使用 RNN 编解码器进行统计机器翻译的学习短语表示,2014。
该模型适用于机器翻译问题,与首次描述该方法的源文件相同。从技术上讲,该模型是神经机器翻译模型。
Francois 的实现提供了一个模板,用于在编写本文时在 Keras 深度学习库中如何(正确地)实现序列到序列预测。
在这篇文章中,我们将详细了解训练和推理模型的设计方式以及它们的工作原理。
您将能够利用这种理解为您自己的序列到序列预测问题开发类似的模型。
机器翻译数据
该示例中使用的数据集涉及闪存卡软件 Anki 中使用的简短的法语和英语句子对。
该数据集被称为“制表符分隔的双语句子对”,并且是 Tatoeba 项目的一部分,并列在 ManyThings.org 网站上,用于帮助英语作为第二语言学生。
可以从此处下载本教程中使用的数据集:
下面是解压缩下载的存档后您将看到的fra.txt
数据文件的前 10 行示例。
Go. Va !
Run! Cours !
Run! Courez !
Wow! Ça alors !
Fire! Au feu !
Help! À l'aide !
Jump. Saute.
Stop! Ça suffit !
Stop! Stop !
Stop! Arrête-toi !
该问题被定义为序列预测问题,其中字符的输入序列是英语并且输出的字符序列是法语。
数据集中使用了数据文件中近 150,000 个示例中的 10,000 个。准备数据的一些技术细节如下:
- 输入序列:填充最大长度为 16 个字符,词汇量为 71 个不同的字符(10000,16,71)。
- 输出序列:填充最大长度为 59 个字符,词汇量为 93 个不同的字符(10000,59,93)。
对训练数据进行框架化,使得模型的输入包括一个完整的英文字符输入序列和整个法语字符输出序列。模型的输出是整个法语字符序列,但向前偏移一个步骤。
例如(使用最小填充并且没有单热编码):
- 输入 1:[‘G’,‘o’,‘。’,“]
- 输入 2:[“,‘V’,‘a’,‘’]
- 输出:[‘V’,‘a’,‘’,‘!’]
机器翻译模型
神经翻译模型是编解码器循环神经网络。
它由读取可变长度输入序列的编码器和预测可变长度输出序列的解码器组成。
在本节中,我们将逐步介绍模型定义的每个元素,代码直接来自 Keras 项目中的帖子和代码示例(在撰写本文时)。
该模型分为两个子模型:负责输出输入英语序列的固定长度编码的编码器,以及负责预测输出序列的解码器,每个输出时间步长一个字符。
第一步是定义编码器。
编码器的输入是一系列字符,每个字符编码为长度为num_encoder_tokens
的单热向量。
编码器中的 LSTM 层定义为return_state
参数设置为True
。这将返回 LSTM 层返回的隐藏状态输出,以及层中所有单元格的隐藏状态和单元格状态。这些在定义解码器时使用。
# Define an input sequence and process it.
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
接下来,我们定义解码器。
解码器输入被定义为法语字符一热编码到二元向量的序列,其长度为num_decoder_tokens
。
LSTM 层定义为返回序列和状态。忽略最终的隐藏和单元状态,仅引用隐藏状态的输出序列。
重要的是,编码器的最终隐藏和单元状态用于初始化解码器的状态。这意味着每次编码器模型对输入序列进行编码时,编码器模型的最终内部状态将用作输出输出序列中第一个字符的起始点。这也意味着编码器和解码器 LSTM 层必须具有相同数量的单元,在这种情况下为 256。
Dense
输出层用于预测每个字符。该Dense
用于以一次性方式产生输出序列中的每个字符,而不是递归地,至少在训练期间。这是因为在训练期间已知输入模型所需的整个目标序列。
Dense 不需要包含在TimeDistributed
层中。
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the
# return states in the training model, but we will use them in inference.
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
最后,使用编码器和解码器的输入以及输出目标序列来定义模型。
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
我们可以在一个独立的示例中将所有这些组合在一起并修复配置并打印模型图。下面列出了定义模型的完整代码示例。
from keras.models import Model
from keras.layers import Input
from keras.layers import LSTM
from keras.layers import Dense
from keras.utils.vis_utils import plot_model
# configure
num_encoder_tokens = 71
num_decoder_tokens = 93
latent_dim = 256
# Define an input sequence and process it.
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the
# return states in the training model, but we will use them in inference.
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
# plot the model
plot_model(model, to_file='model.png', show_shapes=True)
运行该示例会创建已定义模型的图,可帮助您更好地了解所有内容是如何挂起的。
注意,编码器 LSTM 不直接将其输出作为输入传递给解码器 LSTM;如上所述,解码器使用最终隐藏和单元状态作为解码器的初始状态。
还要注意,解码器 LSTM 仅将隐藏状态序列传递给密集输出,而不是输出形状信息所建议的最终隐藏状态和单元状态。
用于训练的编解码器模型图
神经机器翻译推理
一旦定义的模型适合,它就可以用于做出预测。具体而言,输出英文源文本的法语翻译。
为训练定义的模型已经学习了此操作的权重,但模型的结构并非设计为递归调用以一次生成一个字符。
相反,预测步骤需要新模型,特别是用于编码英文输入字符序列的模型和模型,该模型采用到目前为止生成的法语字符序列和编码作为输入并预测序列中的下一个字符。
定义推理模型需要参考示例中用于训练的模型的元素。或者,可以定义具有相同形状的新模型并从文件加载权重。
编码器模型被定义为从训练模型中的编码器获取输入层(encoder_inputs
)并输出隐藏和单元状态张量(encoder_states
)。
# define encoder inference model
encoder_model = Model(encoder_inputs, encoder_states)
解码器更精细。
解码器需要来自编码器的隐藏和单元状态作为新定义的编码器模型的初始状态。由于解码器是一个单独的独立模型,因此这些状态将作为模型的输入提供,因此必须首先定义为输入。
decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
然后可以指定它们用作解码器 LSTM 层的初始状态。
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)
对于要在翻译序列中生成的每个字符,将递归地调用编码器和解码器。
在第一次调用时,来自编码器的隐藏和单元状态将用于初始化解码器 LSTM 层,作为模型的输入直接提供。
在随后对解码器的递归调用中,必须向模型提供最后隐藏和单元状态。这些状态值已经在解码器内;尽管如此,我们必须在每次调用时重新初始化状态,给定模型的定义方式,以便在第一次调用时从编码器中获取最终状态。
因此,解码器必须在每次调用时输出隐藏和单元状态以及预测字符,以便可以将这些状态分配给变量并在每个后续递归调用上用于要翻译的给定输入英语文本序列。
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model([decoder_inputs] + decoder_states_inputs, [decoder_outputs] + decoder_states)
考虑到一些元素的重用,我们可以将所有这些结合在一起,形成一个独立的代码示例,并结合上一节训练模型的定义。完整的代码清单如下。
from keras.models import Model
from keras.layers import Input
from keras.layers import LSTM
from keras.layers import Dense
from keras.utils.vis_utils import plot_model
# configure
num_encoder_tokens = 71
num_decoder_tokens = 93
latent_dim = 256
# Define an input sequence and process it.
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the
# return states in the training model, but we will use them in inference.
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
# plot the model
plot_model(model, to_file='model.png', show_shapes=True)
# define encoder inference model
encoder_model = Model(encoder_inputs, encoder_states)
# define decoder inference model
decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model([decoder_inputs] + decoder_states_inputs, [decoder_outputs] + decoder_states)
# summarize model
plot_model(encoder_model, to_file='encoder_model.png', show_shapes=True)
plot_model(decoder_model, to_file='decoder_model.png', show_shapes=True)
运行该示例定义了训练模型,推理编码器和推理解码器。
然后创建所有三个模型的图。
用于推理的编码器模型图
编码器的图是直截了当的。
解码器显示解码翻译序列中单个字符所需的三个输入,到目前为止的编码转换输出,以及首先从编码器然后从解码器的输出提供的隐藏和单元状态,因为模型被递归调用给定的翻译。
用于推理的解码器模型图
进一步阅读
如果您希望深入了解,本节将提供有关该主题的更多资源。
- Francois Chollet 在 Twitter
- Keras 中序列到序列学习的十分钟介绍
- Keras seq2seq 代码示例(lstm_seq2seq)
- Keras 功能 API
- Keras 的 LSTM API
- 长期短期记忆,1997 年。
- 了解 LSTM 网络,2015 年。
- 用神经网络进行序列学习的序列,2014。
- 使用 RNN 编解码器进行统计机器翻译的学习短语表示,2014。
更新
有关如何在独立问题上使用此模型的示例,请参阅此帖子:
摘要
在这篇文章中,您发现了如何定义用于机器翻译的编解码器序列到序列预测模型,如 Keras 深度学习库的作者所描述的。
具体来说,你学到了:
- 神奇机器翻译示例与 Keras 一起提供并在 Keras 博客上进行了描述。
- 如何正确定义编解码器 LSTM 以训练神经机器翻译模型。
- 如何正确定义推理模型以使用经过训练的编解码器模型来转换新序列。
你有任何问题吗?
在下面的评论中提出您的问题,我会尽力回答。
如何利用小实验在 Keras 中开发标题生成模型
原文:
machinelearningmastery.com/develop-a-caption-generation-model-in-keras/
标题生成是一个具有挑战性的人工智能问题,其中必须为照片生成文本描述。
它既需要计算机视觉的方法来理解图像的内容,也需要来自自然语言处理领域的语言模型,以便将图像的理解转化为正确的单词。最近,深度学习方法已经在该问题的示例上获得了现有技术的结果。
在您自己的数据上开发标题生成模型可能很困难,主要是因为数据集和模型太大而需要数天才能进行训练。另一种方法是使用较小数据集的小样本来探索模型配置。
在本教程中,您将了解如何使用标准照片字幕数据集的小样本来探索不同的深度模型设计。
完成本教程后,您将了解:
- 如何为照片字幕建模准备数据。
- 如何设计基线和测试工具来评估模型的技能和控制其随机性。
- 如何评估模型技能,特征提取模型和单词嵌入等属性,以提升模型技能。
让我们开始吧。
- 2019 年 2 月 2 日:提供了 Flickr8k_Dataset 数据集的直接链接,因为官方网站被删除了。
如何使用小实验开发 Keras 中的标题生成模型
照片由 Per ,保留一些权利。
教程概述
本教程分为 6 个部分;他们是:
- 数据准备
- 基线标题生成模型
- 网络大小参数
- 配置特征提取模型
- 词嵌入模型
- 结果分析
Python 环境
本教程假设您安装了 Python SciPy 环境,理想情况下使用 Python 3。
您必须安装带有 TensorFlow 或 Theano 后端的 Keras(2.0 或更高版本)。
本教程还假设您安装了 scikit-learn,Pandas,NumPy 和 Matplotlib。
如果您需要有关环境的帮助,请参阅本教程:
我建议在带 GPU 的系统上运行代码。
您可以在 Amazon Web Services 上以低成本方式访问 GPU。在本教程中学习如何:
让我们潜入。
数据准备
首先,我们需要准备数据集来训练模型。
我们将使用 Flickr8K 数据集,该数据集包含超过 8,000 张照片及其描述。
您可以从此处下载数据集:
UPDATE(April / 2019):官方网站似乎已被删除(虽然表格仍然有效)。以下是我的数据集 GitHub 存储库的一些直接下载链接:
将照片和说明分别解压缩到Flicker8k_Dataset
和Flickr8k_text
目录中的当前工作目录中。
数据准备分为两部分,它们是:
- 准备文本
- 准备照片
准备文本
数据集包含每张照片的多个描述,描述文本需要一些最小的清洁。
首先,我们将加载包含所有描述的文件。
# 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)
# store the first description for each image
if image_id not in mapping:
mapping[image_id] = 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 in descriptions.items():
# 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]
# store as string
descriptions[key] = ' '.join(desc)
# clean descriptions
clean_descriptions(descriptions)
# summarize vocabulary
all_tokens = ' '.join(descriptions.values()).split()
vocabulary = set(all_tokens)
print('Vocabulary Size: %d' % len(vocabulary))
最后,我们将图像标识符和描述字典保存到名为descriptionss.txt
的新文件中,每行有一个图像标识符和描述。
下面定义了save_doc()
函数,该函数给出了包含标识符到描述和文件名的映射的字典,将映射保存到文件。
# 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()
# 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)
# store the first description for each image
if image_id not in mapping:
mapping[image_id] = image_desc
return mapping
def clean_descriptions(descriptions):
# prepare translation table for removing punctuation
table = str.maketrans('', '', 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 token
desc = [w.translate(table) 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')
首先运行示例打印已加载的照片描述数(8,092)和干净词汇表的大小(4,484 个单词)。
Loaded: 8092
Vocabulary Size: 4484
然后将干净的描述写入’descriptionss.txt
’。看一下文件,我们可以看到描述已准备好进行建模。
看一下文件,我们可以看到描述已准备好进行建模。
3621647714_fc67ab2617 man is standing on snow with trees and mountains all around him
365128300_6966058139 group of people are rafting on river rapids
2751694538_fffa3d307d man and boy sit in the driver seat
537628742_146f2c24f8 little girl running in field
2320125735_27fe729948 black and brown dog with blue collar goes on alert by soccer ball in the grass
...
准备照片
我们将使用预先训练的模型来解释照片的内容。
有很多型号可供选择。在这种情况下,我们将使用 2014 年赢得 ImageNet 竞赛的牛津视觉几何组或 VGG 模型。在此处了解有关模型的更多信息:
Keras 直接提供这种预先训练的模型。请注意,第一次使用此模型时,Keras 将从 Internet 下载模型权重,大约为 500 兆字节。这可能需要几分钟,具体取决于您的互联网连接。
我们可以将此模型用作更广泛的图像标题模型的一部分。问题是,它是一个大型模型,每次我们想要测试一个新的语言模型配置(下游)是多余的时,通过网络运行每张照片。
相反,我们可以使用预先训练的模型预先计算“照片功能”并将其保存到文件中。然后,我们可以稍后加载这些功能,并将它们作为数据集中给定照片的解释提供给我们的模型。通过完整的 VGG 模型运行照片也没有什么不同,只是我们提前完成了一次。
这是一种优化,可以更快地训练我们的模型并消耗更少的内存。
我们可以使用 VGG 类在 Keras 中加载 VGG 模型。我们将加载没有顶部的模型;这意味着没有网络末端的层用于解释从输入中提取的特征并将它们转换为类预测。我们对照片的图像网络分类不感兴趣,我们将训练自己对图像特征的解释。
Keras 还提供了用于将加载的照片整形为模型的优选尺寸的工具(例如,3 通道 224×224 像素图像)。
下面是一个名为extract_features()
的函数,给定目录名称将加载每张照片,为 VGG 准备并从 VGG 模型中收集预测的特征。图像特征是具有形状(7,7,512)的三维数组。
该函数返回图像标识符的字典到图像特征。
# 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)
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.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)
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
’中供以后使用。
基线标题生成模型
在本节中,我们将定义一个基线模型,用于生成照片的标题以及如何对其进行评估,以便将其与此基线的变体进行比较。
本节分为 5 部分:
- 加载数据。
- 适合模型。
- 评估模型。
- 完整的例子
- “A”与“A”测试
- 生成照片标题
1.加载数据
我们不会在所有字幕数据上,甚至在大量数据样本上使用该模型。
在本教程中,我们感兴趣的是快速测试一组标题模型的不同配置,以查看对此数据有何用处。这意味着我们需要快速评估一个模型配置。为此,我们将在 100 张照片和标题上训练模型,然后在训练数据集和 100 张照片和标题的新测试集上进行评估。
首先,我们需要加载预定义的照片子集。提供的数据集具有用于训练,测试和开发的单独集合,这些集合实际上只是不同的照片标识符组。我们将加载开发集并使用前 100 个列表标识符和第二个 100 标识符(例如从 100 到 200)作为测试集。
下面的函数load_set()
将加载一组预定义的标识符,我们将使用’Flickr_8k.devImages.txt
’文件名作为参数调用它。
# 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)
接下来,我们需要将集合拆分为训练集和测试集。
我们将首先通过对标识符进行排序来对它们进行排序,以确保我们始终在机器和运行中对它们进行一致的分割,然后将前 100 个用于训练,接下来的 100 个用于测试。
下面的train_test_split()
函数将在加载的标识符集作为输入的情况下创建此拆分。
# split a dataset into train/test elements
def train_test_split(dataset):
# order keys so the split is consistent
ordered = sorted(dataset)
# return split dataset as two new sets
return set(ordered[:100]), set(ordered[100:200])
现在,我们可以使用预定义的一组训练或测试标识符加载照片描述。
下面是函数 load_clean_descriptions(),它为来自’descriptionss.txt
’的已清除文本描述加载给定的一组标识符,并将标识符字典返回给文本。
我们将开发的模型将生成给定照片的标题,并且标题将一次生成一个单词。将提供先前生成的单词的序列作为输入。因此,我们需要一个“_ 第一个字 ”来启动生成过程和’ 最后一个字 _‘来表示标题的结束。为此,我们将使用字符串’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:
# store
descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq'
return descriptions
接下来,我们可以加载给定数据集的照片功能。
下面定义了一个名为load_photo_features()
的函数,它加载了整套照片描述,然后返回给定照片标识符集的感兴趣子集。这不是非常有效,因为所有照片功能的加载字典大约是 700 兆字节。然而,这将使我们快速起步。
请注意,如果您有更好的方法,请在下面的评论中分享。
# 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
我们可以暂停一下,测试迄今为止开发的所有内容
完整的代码示例如下所示。
from pickle import load
# 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)
# split a dataset into train/test elements
def train_test_split(dataset):
# order keys so the split is consistent
ordered = sorted(dataset)
# return split dataset as two new sets
return set(ordered[:100]), set(ordered[100:200])
# 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:
# store
descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq'
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 dev set
filename = 'Flickr8k_text/Flickr_8k.devImages.txt'
dataset = load_set(filename)
print('Dataset: %d' % len(dataset))
# train-test split
train, test = train_test_split(dataset)
print('Train=%d, Test=%d' % (len(train), len(test)))
# descriptions
train_descriptions = load_clean_descriptions('descriptions.txt', train)
test_descriptions = load_clean_descriptions('descriptions.txt', test)
print('Descriptions: train=%d, test=%d' % (len(train_descriptions), len(test_descriptions)))
# photo features
train_features = load_photo_features('features.pkl', train)
test_features = load_photo_features('features.pkl', test)
print('Photos: train=%d, test=%d' % (len(train_features), len(test_features)))
运行此示例首先在开发数据集中加载 1,000 个照片标识符。选择训练和测试集并用于过滤一组干净的照片描述和准备好的图像特征。
我们快到了。
Dataset: 1,000
Train=100, Test=100
Descriptions: train=100, test=100
Photos: train=100, test=100
描述文本需要先编码为数字,然后才能像输入中那样呈现给模型,或者与模型的预测进行比较。
编码数据的第一步是创建从单词到唯一整数值的一致映射。 Keras 提供了 Tokenizer 类,可以从加载的描述数据中学习这种映射。
下面定义 create_tokenizer(),它将在给定加载的照片描述文本的情况下适合 Tokenizer。
# fit a tokenizer given caption descriptions
def create_tokenizer(descriptions):
lines = list(descriptions.values())
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# prepare tokenizer
tokenizer = create_tokenizer(descriptions)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size: %d' % vocab_size)
我们现在可以对文本进行编码。
每个描述将分为单词。该模型将提供一个单词和照片,并生成下一个单词。然后,将描述的前两个单词作为输入提供给模型,以生成下一个单词。这就是模型的训练方式。
例如,输入序列“_ 在场 _ 中运行的小女孩”将被分成 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()
的函数,单个干净的描述,照片的特征和最大描述长度将为训练模型准备一组输入 - 输出对。调用此函数将返回X1
和X2
,用于图像数据和输入序列数据的数组以及输出字的y
值。
输入序列是整数编码的,输出字是单热编码的,以表示在整个可能单词的词汇表中预期单词的概率分布。
# create sequences of images, input sequences and output words for an image
def create_sequences(tokenizer, desc, image, max_length):
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]
2.适合模型
我们几乎准备好适应这个模型。
已经讨论了模型的部分内容,但让我们重新进行迭代。
该模型基于文章“ Show and Tell:A Neural Image Caption Generator ”,2015 年。
该模型包括三个部分:
- 照片功能提取器。这是在 ImageNet 数据集上预训练的 16 层 VGG 模型。我们使用 VGG 模型(没有顶部)预处理照片,并将使用此模型预测的提取特征作为输入。
- 序列处理器。这是用于处理文本输入的单词嵌入层,后跟 LSTM 层。 LSTM 输出由 Dense 层一次解释一个输出。
- 口译员(缺少更好的名字)。特征提取器和序列处理器都输出固定长度的向量,该向量是最大序列的长度。它们连接在一起并由 LSTM 和 Dense 层处理,然后进行最终预测。
在基础模型中使用保守数量的神经元。具体来说,在特征提取器之后的 128 Dense 层,在序列处理器之后是 50 维单词嵌入,接着是 256 单元 LSTM 和 128 神经元密集,最后是 500 单元 LSTM,接着是网络末端的 500 神经元密集。
该模型预测了词汇表中的概率分布,因此使用 softmax 激活函数,并且在拟合网络时最小化分类交叉熵损失函数。
函数define_model()
定义基线模型,给定词汇量的大小和照片描述的最大长度。 Keras 功能 API 用于定义模型,因为它提供了定义采用两个输入流并组合它们的模型所需的灵活性。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
plot_model(model, show_shapes=True, to_file='plot.png')
return model
要了解模型的结构,特别是层的形状,请参阅下面列出的摘要。
____________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
====================================================================================================
input_1 (InputLayer) (None, 7, 7, 512) 0
____________________________________________________________________________________________________
input_2 (InputLayer) (None, 25) 0
____________________________________________________________________________________________________
global_max_pooling2d_1 (GlobalMa (None, 512) 0 input_1[0][0]
____________________________________________________________________________________________________
embedding_1 (Embedding) (None, 25, 50) 18300 input_2[0][0]
____________________________________________________________________________________________________
dense_1 (Dense) (None, 128) 65664 global_max_pooling2d_1[0][0]
____________________________________________________________________________________________________
lstm_1 (LSTM) (None, 25, 256) 314368 embedding_1[0][0]
____________________________________________________________________________________________________
repeat_vector_1 (RepeatVector) (None, 25, 128) 0 dense_1[0][0]
____________________________________________________________________________________________________
time_distributed_1 (TimeDistribu (None, 25, 128) 32896 lstm_1[0][0]
____________________________________________________________________________________________________
concatenate_1 (Concatenate) (None, 25, 256) 0 repeat_vector_1[0][0]
time_distributed_1[0][0]
____________________________________________________________________________________________________
lstm_2 (LSTM) (None, 500) 1514000 concatenate_1[0][0]
____________________________________________________________________________________________________
dense_3 (Dense) (None, 500) 250500 lstm_2[0][0]
____________________________________________________________________________________________________
dense_4 (Dense) (None, 366) 183366 dense_3[0][0]
====================================================================================================
Total params: 2,379,094
Trainable params: 2,379,094
Non-trainable params: 0
____________________________________________________________________________________________________
我们还创建了一个图表来可视化网络结构,更好地帮助理解两个输入流。
基线标题深度学习模型的情节
我们将使用数据生成器训练模型。鉴于字幕和提取的照片特征可能作为单个数据集适合存储器,因此严格来说不需要这样做。然而,当您在整个数据集上训练最终模型时,这是一种很好的做法。
调用时,生成器将产生结果。在 Keras 中,它将产生一批输入 - 输出样本,用于估计误差梯度并更新模型权重。
函数data_generator()
定义数据生成器,给定加载的照片描述字典,照片特征,整数编码序列的分词器以及数据集中的最大序列长度。
生成器永远循环,并在被问及时保持产生批量的输入 - 输出对。我们还有一个n_step
参数,它允许我们调整每批次要生成的输入输出对的图像数量。平均序列有 10 个字,即 10 个输入 - 输出对,良好的批量大小可能是 30 个样本,大约 2 到 3 个图像值。
# data generator, intended to be used in a call to model.fit_generator()
def data_generator(descriptions, features, tokenizer, max_length, n_step):
# loop until we finish training
while 1:
# loop over photo identifiers in the dataset
keys = list(descriptions.keys())
for i in range(0, len(keys), n_step):
Ximages, XSeq, y = list(), list(),list()
for j in range(i, min(len(keys), i+n_step)):
image_id = keys[j]
# retrieve photo feature input
image = features[image_id][0]
# retrieve text input
desc = descriptions[image_id]
# generate input-output pairs
in_img, in_seq, out_word = create_sequences(tokenizer, desc, image, max_length)
for k in range(len(in_img)):
Ximages.append(in_img[k])
XSeq.append(in_seq[k])
y.append(out_word[k])
# yield this batch of samples to the model
yield [[array(Ximages), array(XSeq)], array(y)]
通过调用fit_generator()
并将其传递给数据生成器以及所需的所有参数,可以拟合模型。在拟合模型时,我们还可以指定每个时期运行的批次数和时期数。
model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose)
对于这些实验,我们将每批使用 2 个图像,每个时期使用 50 个批次(或 100 个图像),以及 50 个训练时期。您可以在自己的实验中尝试不同的配置。
3.评估模型
现在我们知道如何准备数据并定义模型,我们必须定义一个测试工具来评估给定的模型。
我们将通过在数据集上训练模型来评估模型,生成训练数据集中所有照片的描述,使用成本函数评估这些预测,然后多次重复此评估过程。
结果将是模型的技能分数分布,我们可以通过计算平均值和标准差来总结。这是评估深度学习模型的首选方式。看这篇文章:
首先,我们需要能够使用训练有素的模型生成照片的描述。
这包括传入开始描述标记’startseq
’,生成一个单词,然后以生成的单词作为输入递归调用模型,直到到达序列标记结尾’endseq
’或达到最大描述长度。
以下名为generate_desc()
的函数实现此行为,并在给定训练模型和给定准备照片作为输入的情况下生成文本描述。它调用函数word_for_id()
以将整数预测映射回一个字。
# map an integer to a word
def word_for_id(integer, tokenizer):
for word, index in tokenizer.word_index.items():
if index == integer:
return word
return None
# generate a description for an image
def generate_desc(model, tokenizer, photo, max_length):
# seed the generation process
in_text = 'startseq'
# iterate over the whole length of the sequence
for i in range(max_length):
# integer encode input sequence
sequence = tokenizer.texts_to_sequences([in_text])[0]
# pad input
sequence = pad_sequences([sequence], maxlen=max_length)
# predict next word
yhat = model.predict([photo,sequence], verbose=0)
# convert probability to integer
yhat = argmax(yhat)
# map integer to word
word = word_for_id(yhat, tokenizer)
# stop if we cannot map the word
if word is None:
break
# append as input for generating the next word
in_text += ' ' + word
# stop if we predict the end of the sequence
if word == 'endseq':
break
return in_text
我们将为训练数据集和测试数据集中的所有照片生成预测。
以下名为evaluate_model()
的函数将针对给定的照片描述和照片特征数据集评估训练模型。使用语料库 BLEU 分数收集和评估实际和预测的描述,该分数总结了生成的文本与预期文本的接近程度。
# evaluate the skill of the model
def evaluate_model(model, descriptions, photos, tokenizer, max_length):
actual, predicted = list(), list()
# step over the whole set
for key, desc in descriptions.items():
# generate description
yhat = generate_desc(model, tokenizer, photos[key], max_length)
# store actual and predicted
actual.append([desc.split()])
predicted.append(yhat.split())
# calculate BLEU score
bleu = corpus_bleu(actual, predicted)
return bleu
BLEU 分数用于文本翻译,用于针对一个或多个参考翻译评估翻译文本。事实上,我们可以访问我们可以比较的每个图像的多个参考描述,但为了简单起见,我们将使用数据集中每张照片的第一个描述(例如清理版本)。
您可以在此处了解有关 BLEU 分数的更多信息:
- 维基百科 BLEU(双语评估替补)
NLTK Python 库在 corpus_bleu() 函数中实现 BLEU 分数计算。接近 1.0 的较高分数更好,接近零的分数更差。
最后,我们需要做的就是在循环中多次定义,拟合和评估模型,然后报告最终的平均分数。
理想情况下,我们会重复实验 30 次或更多次,但这对我们的小型测试工具来说需要很长时间。相反,将评估模型 3 次。它会更快,但平均分数会有更高的差异。
下面定义了模型评估循环。在运行结束时,训练和测试集的 BLEU 分数的分布被保存到文件中。
# run experiment
train_results, test_results = list(), list()
for i in range(n_repeats):
# define the model
model = define_model(vocab_size, max_length)
# fit model
model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose)
# evaluate model on training data
train_score = evaluate_model(model, train_descriptions, train_features, tokenizer, max_length)
test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length)
# store
train_results.append(train_score)
test_results.append(test_score)
print('>%d: train=%f test=%f' % ((i+1), train_score, test_score))
# save results to file
df = DataFrame()
df['train'] = train_results
df['test'] = test_results
print(df.describe())
df.to_csv(model_name+'.csv', index=False)
我们按如下方式对运行进行参数化,允许我们命名每次运行并将结果保存到单独的文件中。
# define experiment
model_name = 'baseline1'
verbose = 2
n_epochs = 50
n_photos_per_update = 2
n_batches_per_epoch = int(len(train) / n_photos_per_update)
n_repeats = 3
4.完成示例
下面列出了完整的示例。
from os import listdir
from numpy import array
from numpy import argmax
from pandas import DataFrame
from nltk.translate.bleu_score import corpus_bleu
from pickle import load
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
from keras.applications.vgg16 import VGG16
from keras.utils import plot_model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import LSTM
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
from keras.layers import Embedding
from keras.layers.merge import concatenate
from keras.layers.pooling import GlobalMaxPooling2D
# 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)
# split a dataset into train/test elements
def train_test_split(dataset):
# order keys so the split is consistent
ordered = sorted(dataset)
# return split dataset as two new sets
return set(ordered[:100]), set(ordered[100:200])
# 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:
# store
descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq'
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
# fit a tokenizer given caption descriptions
def create_tokenizer(descriptions):
lines = list(descriptions.values())
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# create sequences of images, input sequences and output words for an image
def create_sequences(tokenizer, desc, image, max_length):
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]
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
plot_model(model, show_shapes=True, to_file='plot.png')
return model
# data generator, intended to be used in a call to model.fit_generator()
def data_generator(descriptions, features, tokenizer, max_length, n_step):
# loop until we finish training
while 1:
# loop over photo identifiers in the dataset
keys = list(descriptions.keys())
for i in range(0, len(keys), n_step):
Ximages, XSeq, y = list(), list(),list()
for j in range(i, min(len(keys), i+n_step)):
image_id = keys[j]
# retrieve photo feature input
image = features[image_id][0]
# retrieve text input
desc = descriptions[image_id]
# generate input-output pairs
in_img, in_seq, out_word = create_sequences(tokenizer, desc, image, max_length)
for k in range(len(in_img)):
Ximages.append(in_img[k])
XSeq.append(in_seq[k])
y.append(out_word[k])
# yield this batch of samples to the model
yield [[array(Ximages), array(XSeq)], array(y)]
# map an integer to a word
def word_for_id(integer, tokenizer):
for word, index in tokenizer.word_index.items():
if index == integer:
return word
return None
# generate a description for an image
def generate_desc(model, tokenizer, photo, max_length):
# seed the generation process
in_text = 'startseq'
# iterate over the whole length of the sequence
for i in range(max_length):
# integer encode input sequence
sequence = tokenizer.texts_to_sequences([in_text])[0]
# pad input
sequence = pad_sequences([sequence], maxlen=max_length)
# predict next word
yhat = model.predict([photo,sequence], verbose=0)
# convert probability to integer
yhat = argmax(yhat)
# map integer to word
word = word_for_id(yhat, tokenizer)
# stop if we cannot map the word
if word is None:
break
# append as input for generating the next word
in_text += ' ' + word
# stop if we predict the end of the sequence
if word == 'endseq':
break
return in_text
# evaluate the skill of the model
def evaluate_model(model, descriptions, photos, tokenizer, max_length):
actual, predicted = list(), list()
# step over the whole set
for key, desc in descriptions.items():
# generate description
yhat = generate_desc(model, tokenizer, photos[key], max_length)
# store actual and predicted
actual.append([desc.split()])
predicted.append(yhat.split())
# calculate BLEU score
bleu = corpus_bleu(actual, predicted)
return bleu
# load dev set
filename = 'Flickr8k_text/Flickr_8k.devImages.txt'
dataset = load_set(filename)
print('Dataset: %d' % len(dataset))
# train-test split
train, test = train_test_split(dataset)
# descriptions
train_descriptions = load_clean_descriptions('descriptions.txt', train)
test_descriptions = load_clean_descriptions('descriptions.txt', test)
print('Descriptions: train=%d, test=%d' % (len(train_descriptions), len(test_descriptions)))
# photo features
train_features = load_photo_features('features.pkl', train)
test_features = load_photo_features('features.pkl', test)
print('Photos: train=%d, test=%d' % (len(train_features), len(test_features)))
# prepare tokenizer
tokenizer = create_tokenizer(train_descriptions)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size: %d' % vocab_size)
# determine the maximum sequence length
max_length = max(len(s.split()) for s in list(train_descriptions.values()))
print('Description Length: %d' % max_length)
# define experiment
model_name = 'baseline1'
verbose = 2
n_epochs = 50
n_photos_per_update = 2
n_batches_per_epoch = int(len(train) / n_photos_per_update)
n_repeats = 3
# run experiment
train_results, test_results = list(), list()
for i in range(n_repeats):
# define the model
model = define_model(vocab_size, max_length)
# fit model
model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose)
# evaluate model on training data
train_score = evaluate_model(model, train_descriptions, train_features, tokenizer, max_length)
test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length)
# store
train_results.append(train_score)
test_results.append(test_score)
print('>%d: train=%f test=%f' % ((i+1), train_score, test_score))
# save results to file
df = DataFrame()
df['train'] = train_results
df['test'] = test_results
print(df.describe())
df.to_csv(model_name+'.csv', index=False)
首先运行该示例打印已加载的训练数据的摘要统计量。
Dataset: 1,000
Descriptions: train=100, test=100
Photos: train=100, test=100
Vocabulary Size: 366
Description Length: 25
该示例在 GPU 硬件上需要大约 20 分钟,在 CPU 硬件上需要更长时间。
在运行结束时,训练集上报告的平均 BLEU 为 0.06,测试集上报告为 0.04。结果存储在baseline1.csv
中。
train test
count 3.000000 3.000000
mean 0.060617 0.040978
std 0.023498 0.025105
min 0.042882 0.012101
25% 0.047291 0.032658
50% 0.051701 0.053215
75% 0.069484 0.055416
max 0.087268 0.057617
这提供了用于与备用配置进行比较的基线模型。
“A”与“A”测试
在我们开始测试模型的变化之前,了解测试装置是否稳定非常重要。
也就是说,5 次运行的模型的总结技巧是否足以控制模型的随机性。
我们可以通过在 A / B 测试区域中所谓的 A 对 A 测试再次运行实验来了解这一点。如果我们再次进行相同的实验,我们期望获得相同的结果;如果我们不这样做,可能需要额外的重复来控制方法的随机性和数据集。
以下是该算法的第二次运行的结果。
train test
count 3.000000 3.000000
mean 0.036902 0.043003
std 0.020281 0.017295
min 0.018522 0.026055
25% 0.026023 0.034192
50% 0.033525 0.042329
75% 0.046093 0.051477
max 0.058660 0.060624
我们可以看到该运行获得了非常相似的均值和标准差 BLEU 分数。具体而言,在训练上的平均 BLEU 为 0.03 对 0.06,对于测试为 0.04 至 0.04。
线束有点吵,但足够稳定,可以进行比较。
模特有什么好处吗?
生成照片标题
我们希望该模型训练不足,甚至可能在配置下,但是它可以生成任何类型的可读文本吗?
重要的是,基线模型具有一定的能力,以便我们可以将基线的 BLEU 分数与产生什么样的描述质量的想法联系起来。
让我们训练一个模型并从训练和测试集生成一些描述作为健全性检查。
将重复次数更改为 1,将运行名称更改为“baseline_generate
”。
model_name = 'baseline_generate'
n_repeats = 1
然后更新evaluate_model()
函数以仅评估数据集中的前 5 张照片并打印描述,如下所示。
# evaluate the skill of the model
def evaluate_model(model, descriptions, photos, tokenizer, max_length):
actual, predicted = list(), list()
# step over the whole set
for key, desc in descriptions.items():
# generate description
yhat = generate_desc(model, tokenizer, photos[key], max_length)
# store actual and predicted
actual.append([desc.split()])
predicted.append(yhat.split())
print('Actual: %s' % desc)
print('Predicted: %s' % yhat)
if len(actual) >= 5:
break
# calculate BLEU score
bleu = corpus_bleu(actual, predicted)
return bleu
重新运行示例。
您应该看到训练的结果如下所示(具体结果将根据算法的随机性质而变化):
Actual: startseq boy bites hard into treat while he sits outside endseq
Predicted: startseq boy boy while while he while outside endseq
Actual: startseq man in field backed by american flags endseq
Predicted: startseq man in in standing city endseq
Actual: startseq two girls are walking down dirt road in park endseq
Predicted: startseq man walking down down road in endseq
Actual: startseq girl laying on the tree with boy kneeling before her endseq
Predicted: startseq boy while in up up up water endseq
Actual: startseq boy in striped shirt is jumping in front of water fountain endseq
Predicted: startseq man is is shirt is on on on on bike endseq
您应该在测试数据集上看到如下结果:
Actual: startseq three people are looking into photographic equipment endseq
Predicted: startseq boy racer on on on on bike endseq
Actual: startseq boy is leaning on chair whilst another boy pulls him around with rope endseq
Predicted: startseq girl in playing on on on sword endseq
Actual: startseq black and brown dog jumping in midair near field endseq
Predicted: startseq dog dog running running running and dog in grass endseq
Actual: startseq dog places his head on man face endseq
Predicted: startseq brown dog dog to to to to to to to ball endseq
Actual: startseq man in green hat is someplace up high endseq
Predicted: startseq man in up up his waves endseq
我们可以看到描述并不完美,有些是粗略的,但通常模型会生成一些可读的文本。一个很好的改善起点。
接下来,让我们看一些实验来改变不同子模型的大小或容量。
网络大小参数
在本节中,我们将了解网络结构的总体变化如何影响模型技能。
我们将看看模型大小的以下几个方面:
- '编码器’的固定向量输出的大小。
- 序列编码器模型的大小。
- 语言模型的大小。
让我们潜入。
固定长度向量的大小
在基线模型中,照片特征提取器和文本序列编码器都输出 128 个元素向量。然后将这些向量连接起来以由语言模型处理。
来自每个子模型的 128 个元素向量包含有关输入序列和照片的所有已知信息。我们可以改变这个向量的大小,看它是否会影响模型技能
首先,我们可以将大小从 128 个元素减少到 64 个元素。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(64, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(64, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
我们将此模型命名为“size_sm_fixed_vec
”。
model_name = 'size_sm_fixed_vec'
运行此实验会产生以下 BLEU 分数,可能是测试集上基线的小增益。
train test
count 3.000000 3.000000
mean 0.204421 0.063148
std 0.026992 0.003264
min 0.174769 0.059391
25% 0.192849 0.062074
50% 0.210929 0.064757
75% 0.219246 0.065026
max 0.227564 0.065295
我们还可以将固定长度向量的大小从 128 增加到 256 个单位。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(256, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(256, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
我们将此配置命名为“size_lg_fixed_vec
”。
model_name = 'size_lg_fixed_vec'
运行此实验显示 BLEU 分数表明该模型并没有更好。
有可能通过更多数据和/或更长时间的训练,我们可能会看到不同的故事。
train test
count 3.000000 3.000000
mean 0.023517 0.027813
std 0.009951 0.010525
min 0.012037 0.021737
25% 0.020435 0.021737
50% 0.028833 0.021737
75% 0.029257 0.030852
max 0.029682 0.039966
序列编码器大小
我们可以调用子模型来解释生成到目前为止的序列编码器的单词的输入序列。
首先,我们可以尝试降低序列编码器的代表表现力是否会影响模型技能。我们可以将 LSTM 层中的内存单元数从 256 减少到 128。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(128, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'size_sm_seq_model'
运行这个例子,我们可以看到两列训练上的小凹凸和基线测试。这可能是小训练集大小的神器。
train test
count 3.000000 3.000000
mean 0.074944 0.053917
std 0.014263 0.013264
min 0.066292 0.039142
25% 0.066713 0.048476
50% 0.067134 0.057810
75% 0.079270 0.061304
max 0.091406 0.064799
换句话说,我们可以将 LSTM 层的数量从一个增加到两个,看看是否会产生显着的差异。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = LSTM(256, return_sequences=True)(emb3)
emb5 = TimeDistributed(Dense(128, activation='relu'))(emb4)
# merge inputs
merged = concatenate([fe3, emb5])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'size_lg_seq_model'
运行此实验表明 BLEU 在训练和测试装置上都有不错的碰撞。
train test
count 3.000000 3.000000
mean 0.094937 0.096970
std 0.022394 0.079270
min 0.069151 0.046722
25% 0.087656 0.051279
50% 0.106161 0.055836
75% 0.107830 0.122094
max 0.109499 0.188351
我们还可以尝试通过将其从 50 维加倍到 100 维来增加单词嵌入的表示能力。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 100, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'size_em_seq_model'
我们在训练数据集上看到一个大的运动,但测试数据集上的运动可能很少。
count 3.000000 3.000000
mean 0.112743 0.050935
std 0.017136 0.006860
min 0.096121 0.043741
25% 0.103940 0.047701
50% 0.111759 0.051661
75% 0.121055 0.054533
max 0.130350 0.057404
语言模型的大小
我们可以参考从连接序列和照片特征输入中学习的模型作为语言模型。它负责生成单词。
首先,我们可以通过将 LSTM 和密集层切割为 500 到 256 个神经元来研究对模型技能的影响。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(256)(merged)
lm3 = Dense(256, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'size_sm_lang_model'
我们可以看到,这对 BLEU 对训练和测试数据集的影响都很小,同样可能与数据集的小尺寸有关。
train test
count 3.000000 3.000000
mean 0.063632 0.056059
std 0.018521 0.009064
min 0.045127 0.048916
25% 0.054363 0.050961
50% 0.063599 0.053005
75% 0.072884 0.059630
max 0.082169 0.066256
我们还可以通过添加相同大小的第二个 LSTM 层来查看加倍语言模型容量的影响。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500, return_sequences=True)(merged)
lm3 = LSTM(500)(lm2)
lm4 = Dense(500, activation='relu')(lm3)
outputs = Dense(vocab_size, activation='softmax')(lm4)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'size_lg_lang_model'
同样,我们看到 BLEU 中的微小运动,可能是噪声和数据集大小的人为因素。测试的改进
测试数据集的改进可能是个好兆头。这可能是一个值得探索的变化。
train test
count 3.000000 3.000000
mean 0.043838 0.067658
std 0.037580 0.045813
min 0.017990 0.015757
25% 0.022284 0.050252
50% 0.026578 0.084748
75% 0.056763 0.093608
max 0.086948 0.102469
在更小的数据集上调整模型大小具有挑战性。
配置特征提取模型
使用预先训练的 VGG16 模型提供了一些额外的配置点。
基线模型从 VGG 模型中移除了顶部,包括全局最大池化层,然后将特征的编码提供给 128 元素向量。
在本节中,我们将查看对基线模型的以下修改:
- 在 VGG 模型之后使用全局平均池层。
- 不使用任何全局池。
全球平均汇集
我们可以用 GlobalAveragePooling2D 替换 GlobalMaxPooling2D 层以实现平均池化。
开发全局平均合并以减少图像分类问题的过拟合,但可以在解释从图像中提取的特征方面提供一些益处。
有关全球平均合并的更多信息,请参阅论文:
- 网络网络,2013 年。
下面列出了更新的define_model()
函数和实验名称。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalAveragePooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'fe_avg_pool'
结果表明训练数据集得到了显着改善,这可能是过拟合的标志。我们也看到了测试技巧的小幅提升。这可能是一个值得探索的变化。
我们也看到了测试技巧的小幅提升。这可能是一个值得探索的变化。
train test
count 3.000000 3.000000
mean 0.834627 0.060847
std 0.083259 0.040463
min 0.745074 0.017705
25% 0.797096 0.042294
50% 0.849118 0.066884
75% 0.879404 0.082418
max 0.909690 0.097952
没有合并
我们可以删除 GlobalMaxPooling2D 并展平 3D 照片功能并将其直接送入 Dense 层。
我不认为这是一个很好的模型设计,但值得测试这个假设。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = Flatten()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'fe_flat'
令人惊讶的是,我们看到训练数据的小幅提升和测试数据的大幅提升。这对我来说是令人惊讶的,可能值得进一步调查。
train test
count 3.000000 3.000000
mean 0.055988 0.135231
std 0.017566 0.079714
min 0.038605 0.044177
25% 0.047116 0.106633
50% 0.055627 0.169089
75% 0.064679 0.180758
max 0.073731 0.192428
我们可以尝试重复此实验,并提供更多容量来解释提取的照片功能。在 Flatten 层之后添加具有 500 个神经元的新 Dense 层。
# define the captioning model
def define_model(vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = Flatten()(inputs1)
fe2 = Dense(500, activation='relu')(fe1)
fe3 = Dense(128, activation='relu')(fe2)
fe4 = RepeatVector(max_length)(fe3)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe4, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'fe_flat2'
这导致更改不太令人印象深刻,并且测试数据集上的 BLEU 结果可能更差。
train test
count 3.000000 3.000000
mean 0.060126 0.029487
std 0.030300 0.013205
min 0.031235 0.020850
25% 0.044359 0.021887
50% 0.057483 0.022923
75% 0.074572 0.033805
max 0.091661 0.044688
词嵌入模型
模型的关键部分是序列学习模型,它必须解释到目前为止为照片生成的单词序列。
在该子模型的输入处是单词嵌入和改进单词嵌入而不是从零开始学习它作为模型的一部分(如在基线模型中)的好方法是使用预训练的单词嵌入。
在本节中,我们将探讨在模型上使用预先训练的单词嵌入的影响。特别:
- 训练 Word2Vec 模型
- 训练 Word2Vec 模型+微调
训练有素的 word2vec 嵌入
用于从文本语料库预训练单词嵌入的有效学习算法是 word2vec 算法。
您可以在此处了解有关 word2vec 算法的更多信息:
我们可以使用此算法使用数据集中的已清理照片描述来训练新的独立单词向量集。
Gensim 库提供对算法实现的访问,我们可以使用它来预先训练嵌入。
首先,我们必须像以前一样加载训练数据集的干净照片描述。
接下来,我们可以在所有干净的描述中使用 word2vec 模型。我们应该注意,这包括比训练数据集中使用的 50 更多的描述。这些实验的更公平的模型应该只训练训练数据集中的那些描述。
一旦适合,我们可以将单词和单词向量保存为 ASCII 文件,可能用于以后的检查或可视化。
# train word2vec model
lines = [s.split() for s in train_descriptions.values()]
model = Word2Vec(lines, size=100, window=5, workers=8, min_count=1)
# summarize vocabulary size in model
words = list(model.wv.vocab)
print('Vocabulary size: %d' % len(words))
# save model in ASCII (word2vec) format
filename = 'custom_embedding.txt'
model.wv.save_word2vec_format(filename, binary=False)
单词嵌入保存到文件’custom_embedding.txt
’。
现在,我们可以将嵌入加载到内存中,只检索词汇表中单词的单词向量,然后将它们保存到新文件中。
# load the whole embedding into memory
embedding = dict()
file = open('custom_embedding.txt')
for line in file:
values = line.split()
word = values[0]
coefs = asarray(values[1:], dtype='float32')
embedding[word] = coefs
file.close()
print('Embedding Size: %d' % len(embedding))
# summarize vocabulary
all_tokens = ' '.join(train_descriptions.values()).split()
vocabulary = set(all_tokens)
print('Vocabulary Size: %d' % len(vocabulary))
# get the vectors for words in our vocab
cust_embedding = dict()
for word in vocabulary:
# check if word in embedding
if word not in embedding:
continue
cust_embedding[word] = embedding[word]
print('Custom Embedding %d' % len(cust_embedding))
# save
dump(cust_embedding, open('word2vec_embedding.pkl', 'wb'))
print('Saved Embedding')
下面列出了完整的示例。
# prepare word vectors for captioning model
from numpy import asarray
from pickle import dump
from gensim.models import Word2Vec
# 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)
# split a dataset into train/test elements
def train_test_split(dataset):
# order keys so the split is consistent
ordered = sorted(dataset)
# return split dataset as two new sets
return set(ordered[:100]), set(ordered[100:200])
# 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:
# store
descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq'
return descriptions
# load dev set
filename = 'Flickr8k_text/Flickr_8k.devImages.txt'
dataset = load_set(filename)
print('Dataset: %d' % len(dataset))
# train-test split
train, test = train_test_split(dataset)
print('Train=%d, Test=%d' % (len(train), len(test)))
# descriptions
train_descriptions = load_clean_descriptions('descriptions.txt', train)
print('Descriptions: train=%d' % len(train_descriptions))
# train word2vec model
lines = [s.split() for s in train_descriptions.values()]
model = Word2Vec(lines, size=100, window=5, workers=8, min_count=1)
# summarize vocabulary size in model
words = list(model.wv.vocab)
print('Vocabulary size: %d' % len(words))
# save model in ASCII (word2vec) format
filename = 'custom_embedding.txt'
model.wv.save_word2vec_format(filename, binary=False)
# load the whole embedding into memory
embedding = dict()
file = open('custom_embedding.txt')
for line in file:
values = line.split()
word = values[0]
coefs = asarray(values[1:], dtype='float32')
embedding[word] = coefs
file.close()
print('Embedding Size: %d' % len(embedding))
# summarize vocabulary
all_tokens = ' '.join(train_descriptions.values()).split()
vocabulary = set(all_tokens)
print('Vocabulary Size: %d' % len(vocabulary))
# get the vectors for words in our vocab
cust_embedding = dict()
for word in vocabulary:
# check if word in embedding
if word not in embedding:
continue
cust_embedding[word] = embedding[word]
print('Custom Embedding %d' % len(cust_embedding))
# save
dump(cust_embedding, open('word2vec_embedding.pkl', 'wb'))
print('Saved Embedding')
运行此示例将创建存储在文件’word2vec_embedding.pkl
’中的单词到单词向量的新字典映射。
Dataset: 1000
Train=100, Test=100
Descriptions: train=100
Vocabulary size: 365
Embedding Size: 366
Vocabulary Size: 365
Custom Embedding 365
Saved Embedding
接下来,我们可以加载此嵌入并使用单词向量作为嵌入层中的固定权重。
下面提供load_embedding()
函数,它加载自定义 word2vec 嵌入并返回新的嵌入层以供在模型中使用。
# load a word embedding
def load_embedding(tokenizer, vocab_size, max_length):
# load the tokenizer
embedding = load(open('word2vec_embedding.pkl', 'rb'))
dimensions = 100
trainable = False
# create a weight matrix for words in training docs
weights = zeros((vocab_size, dimensions))
# walk words in order of tokenizer vocab to ensure vectors are in the right index
for word, i in tokenizer.word_index.items():
if word not in embedding:
continue
weights[i] = embedding[word]
layer = Embedding(vocab_size, dimensions, weights=[weights], input_length=max_length, trainable=trainable, mask_zero=True)
return layer
我们可以通过直接从define_model()
函数调用函数在我们的模型中使用它。
# define the captioning model
def define_model(tokenizer, vocab_size, max_length):
# feature extractor (encoder)
inputs1 = Input(shape=(7, 7, 512))
fe1 = GlobalMaxPooling2D()(inputs1)
fe2 = Dense(128, activation='relu')(fe1)
fe3 = RepeatVector(max_length)(fe2)
# embedding
inputs2 = Input(shape=(max_length,))
emb2 = load_embedding(tokenizer, vocab_size, max_length)(inputs2)
emb3 = LSTM(256, return_sequences=True)(emb2)
emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3)
# merge inputs
merged = concatenate([fe3, emb4])
# language model (decoder)
lm2 = LSTM(500)(merged)
lm3 = Dense(500, activation='relu')(lm2)
outputs = Dense(vocab_size, activation='softmax')(lm3)
# tie it together [image, seq] [word]
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
model_name = 'seq_w2v_fixed'
我们可以在训练数据集上看到一些提升,也许在测试数据集上没有真正显着的变化。
train test
count 3.000000 3.000000
mean 0.096780 0.047540
std 0.055073 0.008445
min 0.033511 0.038340
25% 0.078186 0.043840
50% 0.122861 0.049341
75% 0.128414 0.052140
max 0.133967 0.054939
训练有素的 word2vec 嵌入微调
我们可以重复之前的实验,并允许模型在拟合模型时调整单词向量。
下面列出了允许微调嵌入层的更新的load_embedding()
功能。
# load a word embedding
def load_embedding(tokenizer, vocab_size, max_length):
# load the tokenizer
embedding = load(open('word2vec_embedding.pkl', 'rb'))
dimensions = 100
trainable = True
# create a weight matrix for words in training docs
weights = zeros((vocab_size, dimensions))
# walk words in order of tokenizer vocab to ensure vectors are in the right index
for word, i in tokenizer.word_index.items():
if word not in embedding:
continue
weights[i] = embedding[word]
layer = Embedding(vocab_size, dimensions, weights=[weights], input_length=max_length, trainable=trainable, mask_zero=True)
return layer
model_name = 'seq_w2v_tuned'
同样,我们认为在基线模型中使用这些预先训练的字嵌入向量并没有太大差异。
train test
count 3.000000 3.000000
mean 0.065297 0.042712
std 0.080194 0.007697
min 0.017675 0.034593
25% 0.019003 0.039117
50% 0.020332 0.043641
75% 0.089108 0.046772
max 0.157885 0.049904
结果分析
我们对来自 8,000 张照片的 Flickr8k 训练数据集的非常小的样本(1.6%)进行了一些实验。
样本可能太小,模型没有经过足够长时间的训练,并且每个模型的 3 次重复会导致过多的变化。这些方面也可以通过设计实验来评估,例如:
- 模型技能是否随着数据集的大小而缩放?
- 更多的时代会带来更好的技能吗?
- 更多重复会产生一个方差较小的技能吗?
尽管如此,我们对如何为更全面的数据集配置模型有一些想法。
以下是本教程中进行的实验的平均结果摘要。
查看结果图表很有帮助。如果我们有更多的重复,每个分数分布的盒子和胡须图可能是一个很好的可视化。这里我们使用一个简单的条形图。请记住,较大的 BLEU 分数更好。
训练数据集的结果:
实验条形图与训练数据集的模型技巧
测试数据集上的结果:
实验条形图与测试数据集的模型技巧
从仅查看测试数据集的平均结果,我们可以建议:
- 在照片特征提取器(fe_flat 在 0.135231)之后可能不需要合并。
- 在照片特征提取器(fe_avg_pool 为 0.060847)之后,平均合并可能比最大合并更有优势。
- 也许在子模型之后的较小尺寸的固定长度向量是一个好主意(size_sm_fixed_vec 在 0.063148)。
- 也许在语言模型中添加更多层可以带来一些好处(size_lg_lang_model 为 0.067658)。
- 也许在序列模型中添加更多层可以带来一些好处(size_lg_seq_model 为 0.09697)。
我还建议探索这些建议的组合。
我们还可以查看结果的分布。
下面是一些代码,用于加载每个实验的保存结果,并在训练和测试集上创建结果的盒子和须状图以供审查。
from os import listdir
from pandas import read_csv
from pandas import DataFrame
from matplotlib import pyplot
# load all .csv results into a dataframe
train, test = DataFrame(), DataFrame()
directory = 'results'
for name in listdir(directory):
if not name.endswith('csv'):
continue
filename = directory + '/' + name
data = read_csv(filename, header=0)
experiment = name.split('.')[0]
train[experiment] = data['train']
test[experiment] = data['test']
# plot results on train
train.boxplot(vert=False)
pyplot.show()
# plot results on test
test.boxplot(vert=False)
pyplot.show()
在训练数据集上分配结果。
训练数据集中实验与模型技巧的盒子和晶须图
在测试数据集上分配结果。
测试数据集的实验与模型技巧的盒子和晶须图
对这些分布的审查表明:
- 平面上的利差很大;也许平均合并可能更安全。
- 较大的语言模型的传播很大,并且在错误/危险的方向上倾斜。
- 较大序列模型上的扩散很大,并且向右倾斜。
- 较小的固定长度向量大小可能有一些好处。
我预计增加重复到 5,10 或 30 会稍微收紧这些分布。
进一步阅读
如果您要深入了解,本节将提供有关该主题的更多资源。
文件
- Show and Tell:神经图像标题生成器,2015。
- 显示,参与和讲述:视觉注意的神经图像标题生成,2016。
- 网络网络,2013 年。
相关字幕项目
其他
API
摘要
在本教程中,您了解了如何使用照片字幕数据集的一小部分样本来探索不同的模型设计。
具体来说,你学到了:
- 如何为照片字幕建模准备数据。
- 如何设计基线和测试工具来评估模型的技能和控制其随机性。
- 如何评估模型技能,特征提取模型和单词嵌入等属性,以提升模型技能。
你能想到什么实验?
你还尝试了什么?
您可以在训练和测试数据集上获得哪些最佳结果?
请在下面的评论中告诉我。