一、背景
本教程用于记录自己学习视觉问答代码编写的学习过程。
二、VQA关键部分代码
标准VQA模型包括3个模块,分别是图像特征提取模块,文本特征提取模块,以及特征融合后的分类模块。标准VQA模型如下图所示:
1. 图像特征提取
一般我们用预训练好的CNN模型,这里常用的包括vgg16/19,resnet-152/101,faster rcnn。主要的是这三类,当然你也可以自己写cnn或者用其他模型。
(1)使用预训练的vgg16/19来提取图像特征
这里以vgg16为例。我们需要下载预训练好的vgg16和相应的python文件。
常用的预训练好的vgg16有两种格式:
①预训练的vgg16.tfmodel文件和vgg16.py文件:这里的tfmodel可以从这里下载:https://github.com/ry/tensorflow-vgg16,如果你没有安装caffe,那么就用种子下载训练好的文件吧:
下载上面的几个文件,然后种子文件用迅雷就可以下载。
②vgg16.npy文件和vgg16.py文件:另外一种就是npy文件格式,我个人也是用这种格式比较多。文件我传到了自己的网盘上(链接:https://pan.baidu.com/s/1o2-h5Vq6Ff4mCAHWMV7e8w 提取码:3pcx)。
③vgg19.npy文件和vgg19.py文件:和vgg16类似,文件的下载地址可以参考我这一篇博客:对抗生成网络学习(八)——DeblurGAN实现运动图像的去模糊化(tensorflow实现)。
由于自己不太用tfmodel文件,所以这里先主要介绍npy文件如何提取图像特征。
这里先声明一下,这个vgg16.py文件我简单修改过,初始化的时候需要传入一个config类,这个类里面定义了很多参数,需要用到config的地方包括:
class vgg_16():
def __init__(self, config):
self.model_path = config.vgg_path
self.image_height = config.image_height
self.image_width = config.image_width
self.data_dict = np.load(self.model_path, encoding='latin1').item()
print("vgg16.npy file loaded")
接下来加载vgg16,首先是vgg16初始化,然后用到一个占位符,传入vgg中建立模型。
vgg = vgg_16(config)
images = tf.placeholder("float", [None, image_height, image_width, dim_image])
with tf.name_scope("content_vgg"):
vgg.build(images)
这时候如果我需要提取一张图中的特征,就可以用下面的方法,这里我取出了‘fc7’层的特征,最终的特征保存在img_vgg_faeture变量中:
img = resize(img, (image_height, image_width))
img = img.reshape((1, image_height, image_width, dim_image))
sess = tf.Session()
sess.run(tf.global_variables_initializer())
img_vgg_feature = self.sess.run(vgg.fc7, feed_dict={images: img})
(2)使用预训练的resnet-152/101来提取图像特征
非常幸运的是,tensorflow里面集成好了resnet-50/101/152,我们可以直接调用。调用需要用到的库包括:
from tensorflow.contrib.slim.nets import resnet_v2
from tensorflow.contrib.slim.python.slim.nets.resnet_utils import resnet_arg_scope
调用方法:
with slim.arg_scope(resnet_arg_scope(is_training=False)):
net, end_points = resnet_v2.resnet_v2_152(data_set)
resnet-50和101都是一样的调用思路,net是网络最终的返回,大小是(1,1,1,2048),endpoint里面保存了resnet各个网络层节点信息。加入我们要用resnet提取图片特征,抽出中间的一个层,那么可以用如下的方法:
# 随机读取一张图片,将其变成448*448的
image = io.imread('0.png')
image = transform.resize(image, (448, 448))
# 由于一张图像是一个三维立方体,我们要将其变为四维张量
data_set = np.empty((1, 448, 448, 3), dtype="float32")
data_set[0, :, :, :] = image
# 构建resnet网络
with slim.arg_scope(resnet_arg_scope):
net, end_points = resnet_v2.resnet_v2_152(data_set)
# 运行网络
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
sess.run(net)
# 随便记录其中中间的某一层
show_img = end_points['resnet_v2_152/block1/unit_1/bottleneck_v2/conv1'][0].eval()
# 输出一下这一层的大小,是(112,112,64)
print(show_img.shape)
# 随便拿一个feature map输出一下
io.imshow(show_img[:, :, 0])
io.show()
这个feature map输出的结果和原图如下所示:
最后还有resnet_arg_scope是可以修改的,比如可以修改成下面的这种方式:
def resnet_arg_scope(is_training=True, # 训练标记
weight_decay=0.0001, # 权重衰减速率
batch_norm_decay=0.997, # BN的衰减速率
batch_norm_epsilon=1e-5, # BN的epsilon默认1e-5
batch_norm_scale=True): # BN的scale默认值
batch_norm_params = { # 定义batch normalization(标准化)的参数字典
'is_training': is_training,
'decay': batch_norm_decay,
'epsilon': batch_norm_epsilon,
'scale': batch_norm_scale,
'updates_collections': tf.GraphKeys.UPDATE_OPS,
}
with slim.arg_scope( # 通过slim.arg_scope将[slim.conv2d]的几个默认参数设置好
[slim.conv2d],
weights_regularizer=slim.l2_regularizer(weight_decay), # 权重正则器设置为L2正则
weights_initializer=slim.variance_scaling_initializer(), # 权重初始化器
activation_fn=tf.nn.relu, # 激活函数
normalizer_fn=slim.batch_norm, # 标准化器设置为BN
normalizer_params=batch_norm_params):
with slim.arg_scope([slim.batch_norm], **batch_norm_params):
with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc: # ResNet原论文是VALID模式,SAME模式可让特征对齐更简单
return arg_sc # 最后将基层嵌套的arg_scope作为结果返回
(3)使用预训练的faster rcnn来提取图像特征
2. 文本特征提取
文本特征常用的提取方法一般有两种,LSTM或者GRU(其实GRU是LSTM的一个变种),但是在做文本特征提取之前,我们需要先将文本转成向量。
(1)embedding嵌入
这一步的目的是将文本转化为向量,当然文本转向量的方法有很多种,一种最简单的API如下:
embeddings = tf.Variable(
tf.random_uniform([self.vocabulary_size, self.word_embedding_dim], -1.0, 1.0))
也就是我从[-1 1]之间随机初始化,初始化的参数服从均匀分布,生成一个[vocabulary_size, word_embedding_size]的向量。这里的vocabulary_size是所有unique word的数量,word_embedding_size就是每个单词要嵌入为多少长度的向量,一般设置300的比较多,这样做的意思就是说,我现在将所有单词建议一个映射表,每一个单词对应的300维的向量,当然这个映射表是随机建立的。
下面要考虑的就是对unique word建立id表了,也就是对每个单词建立一个编号,根据这个编号,我们才能找到他在映射表中对应的向量:
ques_word_list = []
ques_word_list.append(ques_list[q].split(' '))
上面这个问题就是找到ques_list中第q个问题,然后根据" "将问题中的所有单词分开,添加到ques_word_list中。
添加的ques_word_list一般情况是个二维的,就是[ques_num, word_in_each_question],需要将其变为一维的:
unique_word_in_q = [i for item in ques_word_list for i in item] # 二维变一维
接下来就是统计这个数组中的唯一单词数量了:
all_word = list(set(list(set(unique_word_in_q)))
最后就可以根据这个all_word构建一个word和id的编号表了:
word2id = {word: index+1 for index, word in enumerate(all_word)}
(2)word2vec可以看:
(3)word2glove可以看:https://nlp.stanford.edu/projects/glove/
三、其他
1. 注意力机制
使用注意力的方式有很多种,下面介绍一些常用的
(1)SAN中的注意力
SAN中的注意力比较简单,就是将问题向量作为一个query,来找图像向量中的关键部分。算法也很简单,两个向量结合在做一个tanh,最后再加上权重和bias做一次softmax就行了,代码如下:
def compute_attention(self, image_tensor, question_tensor, out_dim, dropout=True):
# 先将嵌入拉伸为1维向量
img = tf.nn.tanh(tf.layers.dense(image_tensor, out_dim))
ques = tf.nn.tanh(tf.layers.dense(question_tensor, out_dim))
# 连接问题和图像
ques = tf.expand_dims(ques, axis=-2)
IQ = tf.nn.tanh(img + ques)
if dropout:
IQ = tf.nn.dropout(IQ, self.drop_out_rate)
# 再将连接好的拉伸为1维向量,再reshape
temp = tf.layers.dense(IQ, 1)
temp = tf.reshape(temp, [-1, temp.shape[1]])
# softmax获得注意力
p = tf.nn.softmax(temp)
p_exp = tf.expand_dims(p, axis=-1)
att_layer = tf.reduce_sum(p_exp * image_tensor, axis=1)
# 最终的注意力结果
final_out = att_layer + question_tensor
return p, final_out