赞
踩
最近完成了image captioning的小项目,想要将这个项目的思路和收获总结成文。下面文章从raw数据集开始来记录项目的思路和细节。本文旨在记录思路以及调试中遇到的错误。
首先从数据集开始,数据集首先是有四个部分组成。
一个包含了8090张的图片的文件夹。
一个包含了图片名以及对应描述的txt文件。
一个包含了训练集图片的名字的txt文件。
一个包含了测试集图片的名字的txt文件。
image captioning 项目效果是给定一张图片,模型会自动的对这张照片进行描述。
这个项目和其他的深度学习项目还是有很多的相似之处。首先肯定要对数据集进相应的处理。将图片读入,并样它们用ndarray的形式进行表示。我考虑到了两种方式来将图片变成ndarray。第一种就是在基本的PIL库,用其将照片一张一张的读入,然后再用Numpy将其转变成合适的形状的ndarray。第二种我想到的是,pytorch中的Imagefolder方式,因为使用这个方式可以直接将图片以规定的形状,读入成为一个生成器。但是在这次小项目中,我使用的是第一种方式。
读入图片的目的是为了提取每一张照片的特征。提起图像特征则是使用VGG16网络,将VGG16网络的最后一层删除之后,倒数第二层的输出就是每一张图片的特征。我们需要将每一张图片的特征和其名字对应起来进行保存。所以字典就很符合要求。但是在进行特征提取和保存的时候,我们需要注意到的是在图片描述中,图片的名字是加入了图片后缀.jpg的,但是在训练和测试集的文件中,图像名字没有添加后缀。所以添加到这个字典之前,我们需要将名字的.jpg去掉。在保存完成之后,使用pickle文件进行保存,因为数据量相对较大,所有保存下来之后,会方便调试。
提取图片特征的目的是为了让其与被序列化之后的文本进行学习。因为之后给到了相应图片,网络才会根据不同的图像特征产生不同的描述。在处理文字这一个部分中,LSTM的性能是很出众的。现在要做的是对训练文本数据进行处理。LSTM在处理序列化输入的时候是如何操作的?首先RNN的第一个hidden state是随机初始化的。所以需要告诉LSTM,哪里是句子的开始,哪里是句子的结尾。达成这一目的的方法就是在每一句描述最开始和最后面加上一个标致,这个标致可以是任何字母的组合,只要这个标志不和词库中的单词重复就好。
整个image captioning的网络结构不算太复杂。该网络总共需要两个输入,正如前文所提到的,图像特征以及序列化的文本。和其他的网络训练一样,网络需要data X 还有 Label y, 如何将上述整理好的数据,合理地组合成一个网络输入呢?
假如有一句话 = 【startseq dog is eating endseq】
序列化之后 = 【1,2,3,4,5】(为了方便起见,写成1234)
和这句话对应的图片特征 = 【0.12 ,0.45,…,0.87,0.65】
如何组成网络的输入。这里是网络设计中的一个小难点。每一次的输入都需要有对应的图像特征。为了统一序列长度,使用描述中最长的那一句的长度作为统一的序列长度,若有些序列的长度没有那么长,就用0填充即可。第N时刻的网络输出,需要前N-1个时刻的序列。所以网络的输入可以设置成:
x = [input_1 = [img_features] * max_len(最长序列长度),
input_2 = [[0,0,0,0,1],
[0,0,0,1,2],
[0,0,1,2,3],
[0,1,2,3,4],
[1,2,3,4,5]]
y = [n+1时刻对应文字的独热编码]
网络输入 = [x,y]
最后,因为数据量相对来讲还是比较大的,所以最好还是使用生成器的方式来为网络提供训练数据。在tensorflow 2.4.0中的fit函数已经兼容了generator,所以不需要使用fit_generator的方法了。
该模型的实现一共用到了12个函数,每一个函数的功能如下所示:
def img_name_desc_dict(description): ''' 将图片名和对应的图片描述,从文本中提取出来,并以字典的形式进行保存。 ''' def get_train_file_name(train_file_name): ''' 读取训练图片的名字,并将其后缀去掉。 ''' def extract_img_features(img): ''' 主要是为了提取图片特征,使用VGG16的修改版。 ''' def generate_training_corpus(img_name_list,img_info): ''' 将所有的训练图片所对应的描述,整理到一个列表中,作为词库。 ''' def max_len(train_desc_corpus): ''' 获取最长描述的长度。 ''' def create_tokenizer(train_desc_corpus): ''' 创建tokenizer,将文字序列化。 ''' def vocabulary_size(tokenizer): ''' 获取整个词库的大小。 ''' def model(maxlen,voca_size): ''' 构建网络。 ''' def generate_input_LSTM(img_desc_list,img_feature,max_len_,tokenizer,voca_size): ''' 为DNN创建输入。 ''' def data_generator(img_name_list img_info,feature_dict,maxlen,tokenizer,voca_size) ''' 为将训练数据以生成器的形式提供给网络。 ''' def train(): ''' 训练网络 ''' def seq2word(next_word_seq,tokenizer) ''' 将网络预测出来的序列,转化成单词 ''' def predict(img_feature,tokenizer,maxlen,my_model): ''' 预测序列。 '''
模型示意图以及每一个部分所对应的实现函数由下图所示:
使用网络提取图片特征的时候,因为使用的是GPU。出现了以下报错。
tensorflow.python.framework.errors_impl.InternalError: Blas GEMM launch failed : a.shape=(1, 25088), b.shape=(25088, 4096), m=1, n=4096, k=25088 [Op:MatMul]
解决方式是在代码在使用网络之前加入以下两句代码,即可解决问题:
physical_devices = tf.config.list_physical_devices(‘GPU’)
tf.config.experimental.set_memory_growth(physical_devices[0], True)
因为用于训练的训练数据是6000张图片,所以训练集的还是比较大的。想使用生成器为网络提供每一次需要的训练数据的时候,一直出现以下报错。
ValueError: Layer model_merge expects 2 input(s), but it received 3 input tensors. Inputs received: [<tf.Tensor ‘IteratorGetNext:0’ shape=(None, None) dtype=float32>, <tf.Tensor ‘IteratorGetNext:1’ shape=(None, None) dtype=int32>, <tf.Tensor ‘IteratorGetNext:2’ shape=(None, None) dtype=float32>]
报错显示的是在合并层应该只有两个输入,但是显示输入了三个Tensor。
生成器定义如下:
def dataloader(desc_dict,feature_dict,maxlen,voca_size,tokenizer): ''' what is the training set? 1.output of each hidden state 2.img feature for each hidden state 3.one hot code of text of current state :return: ''' while 1: for img_id,img_value in desc_dict.items(): seq = tokenizer.texts_to_sequences([img_value])[0] #查错重点 print('seq',seq) feature = feature_dict[img_id] # get corresponding feature print('feature:',feature) input_1,input_2,input_3 = input_for_LSTM(seq,feature,maxlen,voca_size) # print('input_1:',input_1) # print('input_2:',input_2) # print('input_3:',input_3) #training_data = [[input_1,input_2] #label = input_3 yield [[input_1,input_2],input_3]
每一次yield包括由input_1 与 input_2组成的输入,和input_3(可以理解成标签)
按理讲这就是一个长度为2的输入。经过查阅,将列表改成元组可以解决问题,但是现在仍不清楚,为什么这么做可以。
在确定好网络结构之后,可以从网络的输入函数还是写起,然后一点一点的将辅助函数补齐,这样写的好处在于,可以让我一直有一个很连贯的逻辑和思路。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。