目录
- ==参考课程==
- ==参考项目==
- ==参考文章==
- ==基本理论==
- 1. one-hot编码:
- 2. keras的flow方法:
- 3. 端到端学习
- 4. keras.callbacks 回调函数
- 5. fit_generator()
- 6. 自然语言评价标准
- 7. 使用Keras画流程图
- 8. LSTM原理
- 9. yield
- 10. 张量升维方法
- 11. 图形转换成数组(image to numpy)
- 12. VGG16模型的input、output
- 13. '\n'.join(descriptions)
- 14. Tokenizer()
- 15. Attention
- ==程序环境==
- ==程序实现==
- 程序流程
- 程序:
- *数据预处理(Flickr8k)*
- *图像预处理:*
- *caption预处理:*
- *train*
- *评价模型*
- 模型选择:
参考课程
刚开始接触搜索image caption怎么都找不到资料,后来发现可以搜索image captioning,图像描述,看图说话,图像字幕,图像自动标注,图像理解,照片字幕这一类的词汇也可以找到想要的资料。
斯坦福cs231n计算机视觉——RNN , LSTM 资料CS231n Winter 2016: Lecture 10: Recurrent Neural Networks, Image Captioning参考项目完成 coco 数据集上的 image caption 小项目youtube pytorch image caption
参考项目
Develop an image captioning deep learning model using Flickr 8K data如何从头开始开发深度学习照片字幕生成器keras项目教你用PyTorch实现“看图说话”(附代码,学习资源)Show and Tell: A Neural Image Caption Generator||谷歌图像描述实现
图像理解(Image Captioning)(2)文本处理和模型
参考文章
Framing Image Description as a Ranking Task: Data, Models and Evaluation Met
基本理论
1. one-hot编码:
将离散型特征使用one-hot编码,确实会让特征之间的距离计算更加合理。比如,有一个离散型特征,代表工作类型,该离散型特征,共有三个取值,不使用one-hot编码,其表示分别是x_1 = (1), x_2 = (2), x_3 = (3)。两个工作之间的距离是,(x_1, x_2) = 1, d(x_2, x_3) = 1, d(x_1, x_3) = 2。那么x_1和x_3工作之间就越不相似吗?显然这样的表示,计算出来的特征的距离是不合理。那如果使用one-hot编码,则得到x_1 = (1, 0, 0), x_2 = (0, 1, 0), x_3 = (0, 0, 1),那么两个工作之间的距离就都是sqrt(2).即每两个工作之间的距离是一样的,显得更合理。
转换成二进制
2. keras的flow方法:
ImageDataGenerator.flow
def flow(self, x,y=None, batch_size=32, shuffle=True,sample_weight=None, seed=None,save_to_dir=None, save_prefix='', save_format='png', subset=None)
x: 输入数据。秩为 4 的 Numpy 矩阵或元组。
如果是元组,第一个元素应该包含图像,第二个元素是另一个 Numpy 数组或一列 Numpy 数组,它们不经过任何修改就传递给输出。可用于将模型杂项数据与图像一起输入。对于灰度数据,图像数组的通道轴的值应该为 1,而对于 RGB 数据,其值应该为 3。
y: 标签。
batch_size: 整数 (默认为 32)。
shuffle: 布尔值 (默认为 True)。
sample_weight: 样本权重。
seed: 整数(默认为 None)。
save_to_dir: None 或 字符串(默认为 None)。这使您可以选择指定要保存的正在生成的增强图片的目录(用于可视化您正在执行的操作)。
save_prefix: 字符串(默认 '')。保存图片的文件名前缀(仅当 save_to_dir 设置时可用)。
save_format: "png", "jpeg" 之一(仅当 save_to_dir 设置时可用)。默认:"png"。
subset: 数据子集 ("training" 或 "validation"),如果 在 ImageDataGenerator 中设置了 validation_split。
返回:
一个生成元组 (x, y) 的 Iterator,其中
x 是图像数据的 Numpy 数组(在单张图像输入时),或 Numpy 数组列表(在额外多个输入时),
y 是对应的标签的 Numpy 数组。
如果 'sample_weight' 不是 None,生成的元组形式为 (x, y, sample_weight)。如果 y 是 None, 只有 Numpy 数组 x 被返回
3. 端到端学习
端到端是由输入端的数据直接得到输出端的结果
4. keras.callbacks 回调函数
CSVLogger (filename, separator=’,’, append=False)
- filename:csv 文件的文件名,例如 ‘run/log.csv’。
- separator:用来隔离 csv 文件中元素的字符串。
- append:True:如果文件存在则增加(可以被用于继续训练)。False:覆盖存在的文件。
- 输出:把训练轮结果数据流到 csv 文件的回调函数
ModelCheckpoint (filepath, monitor=‘val_loss’, verbose=0, save_best_only=False, save_weights_only=False, mode=‘auto’, period=1)
- filepath: 字符串,保存模型的路径。
- monitor: 被监测的数据。
- verbose: 详细信息模式,0 或者 1 。
- save_best_only: 如果 save_best_only=True, 被监测数据的最佳模型就不会被覆盖。
mode: {auto, min, max} 的其中之一。 如果save_best_only=True,那么是否覆盖保存文件的决定就取决于被监测数据的最大或者最小值。 对于 val_acc,模式就会是 max,而对于 val_loss,模式就需要是 min,等等。 在 auto 模式中,方向会自动从被监测的数据的名字中判断出来。- save_weights_only: 如果 True,那么只有模型的权重会被保存 (model.save_weights(filepath)), 否则的话,整个模型会被保存 (model.save(filepath))。
- period: 每个检查点之间的间隔(训练轮数)。
- 输出:在每个训练期之后保存模型。
keras ModelCheckpoint 实现断点续训功能
ReduceLROnPlateau (monitor=‘val_loss’, factor=0.1, patience=10, verbose=0, mode=‘auto’, min_delta=0.0001, cooldown=0, min_lr=0)
- monitor: 被监测的数据。
- factor: 学习速率被降低的因数。新的学习速率 = 学习速率 * 因数
- patience: 没有进步的训练轮数,在这之后训练速率会被降低。
- verbose: 整数。0:安静,1:更新信息。
- mode: {auto, min, max} 其中之一。如果是 min 模式,学习速率会被降低如果被监测的数据已经停止下降; 在 max 模式,学习塑料会被降低如果被监测的数据已经停止上升; 在 auto 模式,方向会被从被监测的数据中自动推断出来。
- min_delta: 对于测量新的最优化的阀值,只关注巨大的改变。
- cooldown: 在学习速率被降低之后,重新恢复正常操作之前等待的训练轮数量。
- min_lr: 学习速率的下边界。
- 输出:当标准评估停止提升时,降低学习速率。
EarlyStopping等都是keras的回调函数,用来观察训练结果。
5. fit_generator()
fit_generator ( generator, steps_per_epoch=None, epochs=1, verbose=1, callbacks=None, validation_data=None, validation_steps=None, class_weight=None, max_queue_size=10, workers=1, use_multiprocessing=False, shuffle=True, initial_epoch=0)
- generator:一个生成器,或者一个 Sequence (keras.utils.Sequence) 对象的实例。这是我们实现的重点,后面会着介绍生成器和sequence的两种实现方式。
- steps_per_epoch:这个是我们在每个epoch中需要执行多少次生成器来生产数据,fit_generator函数没有batch_size这个参数,是通过steps_per_epoch来实现的,每次生产的数据就是一个batch,因此steps_per_epoch的值我们通过会设为(样本数/batch_size)。如果我们的generator是sequence类型,那么这个参数是可选的,默认使用len(generator) 。
- epochs:即我们训练的迭代次数。
- verbose:0, 1 或 2。日志显示模式。 0 = 安静模式, 1 = 进度条, 2 = 每轮一行
- callbacks:在训练时调用的一系列回调函数。
- validation_data:和我们的generator类似,只是这个使用于验证的,不参与训练。
-validation_steps:和前面的steps_per_epoch类似。
-class_weight:可选的将类索引(整数)映射到权重(浮点)值的字典,用于加权损失函数(仅在训练期间)。 这可以用来告诉模型「更多地关注」来自代表性不足的类的样本。(感觉这个参数用的比较少)
-max_queue_size:整数。生成器队列的最大尺寸。默认为10.
-workers:整数。使用的最大进程数量,如果使用基于进程的多线程。 如未指定,workers 将默认为 1。如果为 0,将在主线程上执行生成器。
-use_multiprocessing:布尔值。如果 True,则使用基于进程的多线程。默认为False。
-shuffle:是否在每轮迭代之前打乱 batch 的顺序。 只能与Sequence(keras.utils.Sequence) 实例同用。
-initial_epoch: 开始训练的轮次(有助于恢复之前的训练)
model.fit_generator()函数参数
6. 自然语言评价标准
论文以人工审查为主,BLEU标准为辅实际上在做的事:判断两个句子的相似程度。我想知道一个句子翻译前后的表示是否意思一致,显然没法直接比较,那我就拿这个句子的标准人工翻译与我的机器翻译的结果作比较,如果它们是很相似的,说明我的翻译很成功。因此,BLUE去做判断:一句机器翻译的话与其相对应的几个参考翻译作比较,算出一个综合分数。这个分数越高说明机器翻译得越好。(注:BLEU算法是句子之间的比较,不是词组,也不是段落)
自然语言中的评价指标
NIC《Show and Tell: A Neural Image Caption Generator》
7. 使用Keras画流程图
from keras.utils import plot_model
plot_model(NIC_model, to_file='./model.png',show_shapes=True)
8. LSTM原理
【自然语言处理基础知识】LSTM
9. yield
python中yield的用法详解——最简单,最清晰的解释
10. 张量升维方法
img = np.expand_dims(img, axis=0) # 矩阵增维
11. 图形转换成数组(image to numpy)
方法一(keras框架中):
x = img_to_array(img)
方法二(numpy中):
x = np.array(img)
或
x = np.asarray(img)
方法三(pytorch中):
pytorch tensor张量维度转换(tensor维度转换)
12. VGG16模型的input、output
keras—VGG16
input
类型 : numpy数组
shap : (1,224,224,3)output
类型 : 张量
shap : (4096,1)
13. ‘\n’.join(descriptions)
每条描述间用换行符分隔
14. Tokenizer()
Tokenizer()
15. Attention
NLP中遇到的各类Attention结构汇总以及代码复现
程序环境
Keras 2.0.8(更高版本由于API变化可能会报错)
tensorflow
numpy
pickle
nltk
程序实现
程序流程
- 预处理flickr8k数据
- caption预处理
- 提取描述
- 构建词汇表 vocab
- 构建词汇表字典 每个单词对应一个索引 idx_to_word、word_to_idx
- 描述正确性指标
- 预处理flickr8k图片
- 构建CNN网络预训练图片
- 获得和保存图片对应的特征(vgg-16为例是4096维的特征,注意和LSTM中输入的维数匹配)
- 构建ImageCaptioning模型
- NIC: CNN编码+LSTM解码网络结构
- 正向传播
- 反向传播
- 计算loss,计算正确率
- 采用SGD, ADAM等更新权重参数
- 测试模型
- 对测试集运用训练好的模型
- 评价模型准确度
- 比较几种不同的网络和参数对于模型准确度的影响,并分析原因,反过来验证猜想,如此往复
程序:
数据预处理(Flickr8k)
图像预处理:
# -*- coding: utf-8 -*-
# 准备图像数据(图片特征)
# 加载模型vgg16,去除最后一层
# 用该模型提取图片特征(1×4096的矩阵)
# 将提取的特征保存到名为 “ features.pkl ” 的文件中
import numpy as np
from keras.models import Model
from keras.applications.vgg16 import VGG16, preprocess_input
from keras.preprocessing.image import load_img, img_to_array
from os import listdir
from pickle import dump
# 用vgg16提取图片特征 ,从特定目录
def extract_features(directory):
model = VGG16() # 加载模型VGG16
model.layers.pop() # 去掉最后一层
model = Model(inputs=model.inputs, outputs=model.layers[-1].output)
model.summary() # 输出summarize
features = {} # 从图片中提取特征,先构建一个空字典
img_namelist = listdir(directory) # 创建图像名列表
for img_name in img_namelist:
name = directory + '/' + img_name # 创建文件名
# 加载图片
img = load_img(name, target_size=(224, 224))
img = img_to_array(img) # 转换为矩阵
img = np.expand_dims(img, axis=0) # 矩阵增维
img = preprocess_input(img) # 预处理:均值化
feature = model.predict(img, verbose=0) # 提取特征并保存
features[img_name[:-4]] = feature # 将向量写入字典
print('>正在处理:', img_name)
return features
directory = './Datasets/Flickr8k/Flickr8k_Dataset/'
features = extract_features(directory)
dump(features, open('features.pkl', 'wb')) # 保存文件
print('图片特征提取完成,文件已保存!')
caption预处理:
定义一个预处理的类Preprocess():
- w2v_path:word2vec的存储路径
- sentences:句子
- sen_len:句子的固定长度
- idx2word 是一个列表,比如:self.idx2word[1] = ‘he’
- word2idx 是一个字典,记录单词在 idx2word 中的下标,比如:self.word2idx[‘he’] = 1
- embedding_matrix 是一个列表,记录词嵌入的向量,比如:self.embedding_matrix[1] = ‘he’ vector
对于句子,我们就可以通过 embedding_matrix[word2idx[‘he’] ] 找到 ‘he’ 的词嵌入向量。
Preprocess()的调用如下:- 训练模型:preprocess = Preprocess(train_x, sen_len, w2v_path=w2v_path)
- 测试模型:preprocess = Preprocess(test_x, sen_len, w2v_path=w2v_path)
另外,这里除了出现在 train_x 和 test_x 中的单词外,还需要两个单词(或者叫特殊符号):- “ PAD ”:Padding的缩写,把所有句子都变成一样长度时,需要用"PAD"补上空白符
- “UNK”:Unknown的缩写,凡是在 train_x 和 test_x 中没有出现过的单词,都用"UNK"来表示
token中
1000268201_693b08cb0e.jpg#0 A child in a pink dress is climbing up a set of stairs in an entry way .line中
['1000268201_693b08cb0e.jpg#0', 'A', 'child', 'in', 'a', 'pink', 'dress', 'is', 'climbing', 'up', 'a', 'set', 'of', 'stairs', 'in', 'an', 'entry', 'way', '.']img_id中(line[0][:-6])
1000268201_693b08cb0eimg_dsc中(line[1:])
['A', 'child', 'in', 'a', 'pink', 'dress', 'is', 'climbing', 'up', 'a', 'set', 'of', 'stairs', 'in', 'an', 'entry', 'way', '.']descriptions
['101669240_b2d3e7f17b man in hat is displaying pictures next to skier in blue hat', '101669240_b2d3e7f17b man skis past another man displaying paintings in the snow'......]
# -*- coding: utf-8 -*-
# 准备文本数据(图片描述)
# 加载原始txt数据,清洗后保存
import string
import help_func as func
# 清洗文本
def clean_dsc(dsc):
dsc = [word.lower() for word in dsc] # 转小写
dsc = [word for word in dsc if len(word) > 1] # 去除单字符
dsc = ' '.join(dsc) # list2str
for i in (string.punctuation + string.digits):
dsc = dsc.replace(i,'') # 去除字符、数字 (单词内部的)
return dsc
# 保存txt文件
def save_descriptions(txt, filename):
txt = txt.split('\n') # 按换行符分割
descriptions = []
for line in txt:
line = line.split() # 再次分割
if len(line)<2:
continue
img_id, img_dsc = line[0][:-6], line[1:] # 提取图片名和图片描述
img_dsc = clean_dsc(img_dsc) # 清洗描述文本
descriptions.append(img_id + ' ' + img_dsc)# 拼接图片名和图片描述
data = '\n'.join(descriptions) # 每条描述间用换行符分隔
file = open(filename, 'w')
file.write(data)
file.close()
return print('Descriptions saved!')
filename = './Datasets/Flickr8k/Flickr8k_text/Flickr8k.token.txt'
txt = func.load_doc(filename)
save_descriptions(txt, 'descriptions.txt')
train
# -*- coding: utf-8 -*-
# 逐步加载训练,避免 memory error
# 加载训练数据:图片描述,图片特征
# 创建tokenizer
# 训练模型
from numpy import array
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from keras.models import Model
from keras.layers import Input,Dense,LSTM,Embedding,Dropout
from keras.layers.merge import add
from keras.callbacks import ModelCheckpoint
import help_func as func
# 为神经网络创建输入和标签序列
# X1--图片特征
# X2--图片描述(上文)
# y---标签(图片描述/下文)
# create sequences of images, input sequences and output words for an image
def create_sequences(tokenizer, max_length, desc_list, photo, vocab_size):
X1, X2, y = list(), list(), list()
# walk through each description for the image
for desc in desc_list:
# encode the sequence
seq = tokenizer.texts_to_sequences([desc])[0]
# split one sequence into multiple X,y pairs
for i in range(1, len(seq)):
# split into input and output pair
in_seq, out_seq = seq[:i], seq[i]
# pad input sequence
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
# encode output sequence
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
# store
X1.append(photo)
X2.append(in_seq)
y.append(out_seq)
return array(X1), array(X2), array(y)
# 构建模型,模型是词汇量和句子长度的函数
def define_model(vocab_size, max_length):
# 图像特征
inputs1 = Input(shape=(4096, ))
fe1 = Dropout(0.5)(inputs1)
fe2 = Dense(256, activation='relu')(fe1)
# 图像描述
inputs2 = Input(shape=(max_length,))
se1 = Embedding(vocab_size, 256, mask_zero=True)(inputs2)
se2 = Dropout(0.5)(se1)
se3 = LSTM(256)(se2)
# 融合层
decoder1 = add([fe2, se3])
decoder2 = Dense(256, activation='relu')(decoder1)
outputs = Dense(vocab_size, activation='softmax')(decoder2)
# 输入输出
model = Model(inputs=[inputs1, inputs2], outputs=outputs)
# 编译模型
model.compile(loss='categorical_crossentropy', optimizer='adam')
# 输出模型结构
model.summary()
return model
# 数据生成器
# data generator, intended to be used in a call to model.fit_generator()
def data_generator(descriptions, photos, tokenizer, max_length, vocab_size):
# loop for ever over images
while 1:
for key, desc_list in descriptions.items():
# retrieve the photo feature
photo = photos[key][0]
in_img, in_seq, out_word = create_sequences(tokenizer, max_length, desc_list, photo, vocab_size)
yield [in_img, in_seq], out_word
# 加载训练数据
filename = './Datasets/Flickr8k/Flickr8k_text/Flickr_8k.trainImages.txt'
train = func.load_set(filename)
print('Namelist of train data:%d' % len(train))
# 6000
# 生成训练图片的描述文件
train_descriptions = func.load_descriptions('descriptions.txt', train)
print('Descriptions of train data:%d' % len(train_descriptions))
# 加载训练图片的特征文件
train_features = func.load_photo_features('features.pkl', train)
print('Photo features of train data:%d' % len(train_features))
# 在训练数据上创建tokenizer,计算词汇表长度
tokenizer = func.create_tokenizer(train_descriptions)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size:%d' % vocab_size)
# 计算描述语段最大词长度
max_length = func.max_length(train_descriptions)
print('Description Length:%d' % max_length)
# define the model
model = define_model(vocab_size, max_length)
# train the model, run epochs manually and save after each epoch
epochs = 20
steps = len(train_descriptions)
for i in range(epochs):
# create the data generator
generator = data_generator(train_descriptions, train_features, tokenizer, max_length, vocab_size)
# fit for one epoch
model.fit_generator(generator, epochs=1, steps_per_epoch=steps, verbose=1)
# save model
model.save('model_' + str(i) + '.h5')
评价模型
# -*- coding: utf-8 -*-
# 使用bleu指标评估模型
import numpy as np
from numpy import argmax
from pickle import load,dump
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import load_model,Model
from nltk.translate.bleu_score import corpus_bleu
import help_func as func
from keras.applications.vgg16 import VGG16,preprocess_input
from keras.preprocessing.image import load_img,img_to_array
# 根据tokenizer将一个整数转为单词
def word_for_id(integer, tokenizer):
for word,word_id in tokenizer.word_index.items(): # 遍历整个tokenizer
if word_id == integer: # 寻找匹配项并输出word
return word
return None
# 根据图像特征为单张图片生成一段描述
def generate_dsc(model, tokenizer, photo, max_length):
in_text = 'startseq' # 文件头
for i in range(max_length):
input_seq = tokenizer.texts_to_sequences([in_text])[0] # 使用tokenizer处理(生成数字)
input_seq = pad_sequences([input_seq],maxlen=max_length)# 按照最大长度充0补齐
output_seq = model.predict([photo,input_seq], verbose=0)# 使用模型预测输出(生成数字)
output_int = argmax(output_seq) # 将预测输出转为整数数字
output_word = word_for_id(output_int,tokenizer) # 将预测值(数字)转为单词
if output_word == None: # 排除特殊情况None
break
in_text = in_text + ' ' + output_word # 逐个将单词拼接为句子
if output_word == 'endseq': # 遇到结束词就退出循环
break
return in_text
# 用bleu评估模型
def evaluate_model(model, descriptions, photos, tokenizer, max_length):
''' 模型↑ 图片描述↑ 特征↑ '''
y_tag, y_pdc = [],[] # 定义标签值和预测值列表
for img_id, dsc_list in descriptions.items(): # 遍历整个descriptions
yhat = generate_dsc(model, tokenizer, photos[img_id], max_length)
references = [d.split() for d in dsc_list]
y_tag.append(references)
y_pdc.append(yhat.split())
print('BLEU-1: %f' % corpus_bleu(y_tag, y_pdc, weights=(1.0, 0, 0, 0)))
print('BLEU-2: %f' % corpus_bleu(y_tag, y_pdc, weights=(0.5, 0.5, 0, 0)))
print('BLEU-3: %f' % corpus_bleu(y_tag, y_pdc, weights=(0.34, 0.33, 0.33, 0)))
print('BLEU-4: %f' % corpus_bleu(y_tag, y_pdc, weights=(0.25, 0.25, 0.25, 0.25)))
return None
# 用vgg16提取单张图片特征
def extract_features(filename):
model = VGG16()
model.layers.pop()
model = Model(inputs=model.inputs, outputs=model.layers[-1].output)
img = load_img(filename, target_size=(224, 224))
img = img_to_array(img)
img = np.expand_dims(img,axis=0)
img = preprocess_input(img)
feature = model.predict(img, verbose=0)
return feature
# 加载训练数据(6K)
filename = './Datasets/Flickr8k/Flickr8k_text/Flickr_8k.trainImages.txt'
train = func.load_set(filename)
print('Namelist of train data:%d' % len(train))
# 生成训练图片的描述文件
train_descriptions = func.load_descriptions('descriptions.txt', train)
print('Descriptions of train data:%d' % len(train_descriptions))
# 在训练数据上创建tokenizer,计算词汇表长度
tokenizer = func.create_tokenizer(train_descriptions)
vocab_size = len(tokenizer.word_index) + 1
print('Vocabulary Size:%d' % vocab_size)
# 计算描述语段最大词长度
max_length = func.max_length(train_descriptions)
print('Description Length:%d' % max_length)
# 保存tokenizer到文件
dump(tokenizer, open('tokenizer.pkl', 'wb'))
# 加载测试数据(1K)
filename = './Datasets/Flickr8k/Flickr8k_text/Flickr_8k.testImages.txt'
test = func.load_set(filename)
print('Namelist of test data:%d' % len(test))
# 生成测试图片的描述文件
test_descriptions = func.load_descriptions('descriptions.txt', test)
print('Descriptions of test data:%d' % len(test_descriptions))
# 加载测试图片的特征文件
test_features = func.load_photo_features('features.pkl', test)
print('Photo features of test data:%d' % len(test_features))
# 加载训练好的模型并评估
filename = 'model_0.h5'
model = load_model(filename)
evaluate_model(model, test_descriptions, test_features, tokenizer, max_length)
# 处理单张图片,先加载tokenizer,再为其添加描述
#tokenizer = load(open('tokenizer.pkl', 'rb'))
photo = extract_features('example.jpg')
description = generate_dsc(model, tokenizer, photo, max_length)
print(description[9:-7]) # 输出描述时去除首尾标识符
模型选择: