在本文中,我们将为各种图像生成文字描述
图像描述是为图像提供适当文字描述的过程。作为人类,这似乎是一件容易的任务,即使是五岁的孩子也可以轻松完成,但是我们如何编写一个将输入作为图像并生成标题作为输出的计算机程序呢?
在深度神经网络的最新发展之前,业内最聪明的人都无法解决这个问题,但是在深度神经网络问世之后,考虑到我们拥有所需的数据集,这样做是完全有可能的。
例如,网络模型可以生成与下图相关的以下任何标题,即“A white dog in a grassy area”,“white dog with brown spots”甚至“A dog on grass and some pink flowers ”。
数据集
我们选择的数据集为“ Flickr 8k”。我们之所以选择此数据,是因为它易于访问且具有可以在普通PC上进行训练的完美大小,也足够训练网络生成适当的标题。数据分为三组,主要是包含6k图像的训练集,包含1k图像的开发集和包含1k图像的测试集。每个图像包含5个标题。示例之一如下:
Achildinapinkdressisclimbingupasetofstairsinanentryway. Agirlgoingintoawoodenbuilding. Alittlegirlclimbingintoawoodenplayhouse. Alittlegirlclimbingthestairstoherplayhouse. Alittlegirlinapinkdressgoingintoawoodencabin.
数据清理
任何机器学习程序的第一步也是最重要的一步是清理数据并清除所有不需要的数据。在处理标题中的文本数据时,我们将执行基本的清理步骤,例如将计算机中的所有字母都转换为小写字母“ Hey”和“ hey”是两个完全不同的单词,删除特殊标记和标点符号,例如*, (,£,$,%等),并消除所有包含数字的单词。
我们首先为数据集中的所有唯一内容创建词汇表,即8000(图片数量)* 5(每个图像的标题)= 40000标题。我们发现它等于8763。但是这些词中的大多数只出现了1到2次,我们不希望它们出现在我们的模型中,因为这不会使我们的模型对异常值具有鲁棒性。因此,我们将词汇中包含的单词的最少出现次数设置为10个阈值,该阈值等于1652个唯一单词。
我们要做的另一件事是在每个描述中添加两个标记,以指示字幕的开始和结束。这两个标记分别是“ startseq”和“ endseq”,分别表示字幕的开始和结尾。
首先,导入所有必需的库:
importnumpyasnpfromnumpyimportarrayimportpandasaspdimportmatplotlib.pyplotaspltimportstringimportosfromPILimportImageimportglobimportpicklefromtimeimporttimefromkeras.preprocessingimportsequencefromkeras.modelsimportSequentialfromkeras.layersimportLSTM, Embedding, Dense, Flatten, Reshape, concatenate, Dropoutfromkeras.optimizersimportAdamfromkeras.layers.mergeimportaddfromkeras.applications.inception_v3importInceptionV3fromkeras.preprocessingimportimagefromkeras.modelsimportModelfromkerasimportInput, layersfromkeras.applications.inception_v3importpreprocess_inputfromkeras.preprocessing.sequenceimportpad_sequencesfromkeras.utilsimportto_categorical
让我们定义一些辅助函数:
#loaddescriptionsdefload_doc(filename): file=open(filename, 'r') text=file.read() file.close() returntextdefload_descriptions(doc): mapping=dict() forlineindoc.split('\n'): tokens=line.split() iflen(line) <2: continueimage_id, image_desc=tokens[0], tokens[1:] image_id=image_id.split('.')[0] image_desc=' '.join(image_desc) ifimage_idnotinmapping: mapping[image_id] =list() mapping[image_id].append(image_desc) returnmappingdefclean_descriptions(descriptions): table=str.maketrans('', '', string.punctuation) forkey, desc_listindescriptions.items(): foriinrange(len(desc_list)): desc=desc_list[i] desc=desc.split() desc= [word.lower() forwordindesc] desc= [w.translate(table) forwindesc] desc= [wordforwordindesciflen(word)>1] desc= [wordforwordindescifword.isalpha()] desc_list[i] =' '.join(desc) returndescriptions#savedescriptionstofile, oneperlinedefsave_descriptions(descriptions, filename): lines=list() forkey, desc_listindescriptions.items(): fordescindesc_list: lines.append(key+' '+desc) data='\n'.join(lines) file=open(filename, 'w') file.write(data) file.close() #loadcleandescriptionsintomemorydefload_clean_descriptions(filename, dataset): doc=load_doc(filename) descriptions=dict() forlineindoc.split('\n'): tokens=line.split() image_id, image_desc=tokens[0], tokens[1:] ifimage_idindataset: ifimage_idnotindescriptions: descriptions[image_id] =list() desc='startseq '+' '.join(image_desc) +' endseq'descriptions[image_id].append(desc) returndescriptionsdefload_set(filename): doc=load_doc(filename) dataset=list() forlineindoc.split('\n'): iflen(line) <1: continueidentifier=line.split('.')[0] dataset.append(identifier) returnset(dataset) #loadtrainingdatasetfilename="dataset/Flickr8k_text/Flickr8k.token.txt"doc=load_doc(filename) descriptions=load_descriptions(doc) descriptions=clean_descriptions(descriptions) save_descriptions(descriptions, 'descriptions.txt') filename='dataset/Flickr8k_text/Flickr_8k.trainImages.txt'train=load_set(filename) train_descriptions=load_clean_descriptions('descriptions.txt', train)
让我们一一解释:
load_doc:获取文件的路径并返回该文件内的内容
load_descriptions:获取包含描述的文件的内容,并生成一个字典,其中以图像id为键,以描述为值列表
clean_descriptions:通过将所有字母都转换为小写字母,忽略数字和标点符号以及仅包含一个字符的单词来清理描述
save_descriptions:将描述字典作为文本文件保存到内存中
load_set:从文本文件加载图像的所有唯一标识符
load_clean_descriptions:使用上面提取的唯一标识符加载所有已清理的描述
数据预处理
接下来,我们对图像和字幕进行一些数据预处理。图像基本上是我们的特征向量,即我们对网络的输入。因此,我们需要先将它们转换为固定大小的向量,然后再将其传递到神经网络中。为此,我们使用了由Google Research [3]创建的Inception V3模型(卷积神经网络)进行迁移学习。该模型在'ImageNet'数据集[4]上进行了训练,可以对1000张图像进行图像分类,但是我们的目标不是进行分类,因此我们删除了最后一个softmax层,并为每张图像提取了2048个固定矢量,如图所示 以下:
标题文字是我们模型的输出,即我们必须预测的内容。但是预测并不会一次全部发生,而是会逐字预测字幕。为此,我们需要将每个单词编码为固定大小的向量(将在下一部分中完成)。为此,我们首先需要创建两个字典,即“单词到索引”将每个单词映射到一个索引(在我们的情况下为1到1652),以及“索引到单词”将字典将每个索引 映射到其对应的单词字典。我们要做的最后一件事是计算在数据集中具有最大长度的描述的长度,以便我们可以填充所有其他内容以保持固定长度。在我们的情况下,该长度等于34。
字词嵌入
如前所述,我们将每个单词映射到固定大小的向量(即200)中,我们将使用预训练的GLOVE模型。最后,我们为词汇表中的所有1652个单词创建一个嵌入矩阵,其中为词汇表中的每个单词包含一个固定大小的向量。
#Createalistofallthetrainingcaptionsall_train_captions= [] forkey, valintrain_descriptions.items(): forcapinval: all_train_captions.append(cap) #Consideronlywordswhichoccuratleast10timesinthecorpusword_count_threshold=10word_counts= {} nsents=0forsentinall_train_captions: nsents+=1forwinsent.split(' '): word_counts[w] =word_counts.get(w, 0) +1vocab= [wforwinword_countsifword_counts[w] >=word_count_threshold] print('Preprocessed words {} -> {}'.format(len(word_counts), len(vocab))) ixtoword= {} wordtoix= {} ix=1forwinvocab: wordtoix[w] =ixixtoword[ix] =wix+=1vocab_size=len(ixtoword) +1#oneforappended0's# Load Glove vectorsglove_dir = 'glove.6B'embeddings_index = {}f = open(os.path.join(glove_dir, 'glove.6B.200d.txt'), encoding="utf-8")for line in f:values = line.split()word = values[0]coefs = np.asarray(values[1:], dtype='float32')embeddings_index[word] = coefsf.close()embedding_dim = 200# Get 200-dim dense vector for each of the words in out vocabularyembedding_matrix = np.zeros((vocab_size, embedding_dim))for word, i in wordtoix.items():embedding_vector = embeddings_index.get(word)if embedding_vector is not None:embedding_matrix[i] = embedding_vector
让我们接收下这段代码:
第1至5行:将所有训练图像的所有描述提取到一个列表中
第9-18行:仅选择词汇中出现次数超过10次的单词
第21–30行:创建一个要索引的单词和一个对单词词典的索引。
第33–42行:将Glove Embeddings加载到字典中,以单词作为键,将vector嵌入为值
第44–52行:使用上面加载的嵌入为词汇表中的单词创建嵌入矩阵