前言
Ranjay Krishna,Ines Chami,Michael Bernstein,Li Fei-Fei,CVPR,2018,代码:
Referring Relationship 是 Li Fei-Fei 团队 2018 年 CVPR 的一篇论文,定义了这样的一个任务:给定一个 relationship <subject-predicate-object>,检测出图像中 relationship 涉及到的目标,如论文中下图所示:
作者指出在此类任务中,已知 relationship 中的某个目标位置会对另一个目标的检测带来帮助,同时通过将连接 relationship 中两个目标的 predicate 形式化为 attention 的转移,而非之前相关工作中将其形式化为某种特征(作者指出因为 predicate 的视觉表现差异很大),本文可以更好的利用目标间的关系来定位目标。论文给出算法的整个模型框架,如下图所示:
主要流程:
1. 给定图像和 relationship,图像经一个pre-trained 的网络提取图像视觉特征,image_feature(Resnet 或 VGG,可在输出后添加若干层卷积以便更有针对性地提取特征),特征 feature map 的尺寸是(L, L, C),C 通道数;
2. 将 subject 和 object(作者在代码中将 subject 和 object 表示为其类别 id,为一个整数)映射为一个稠密 C 维向量,embedded_subject,embedded_object;
3. image_feature 分别与 embedded_subject、embedded_object,逐位置进行内积,计算初始的 subject attention map 和 object attention map,尺寸均为 (L, L, 1);
4. 以 subject attention map 为输入,经若干层卷积处理(卷积核尺寸为 kxk,中间层 feature map 通道数为 c,最后一层通道数为 1),计算 subject->object 的 predicate shift (L, L, 1);同时以 object attention map 为输入,经若干层卷积处理(配置与 subject->object 相同),计算 object->subject 的 predicate shift (L, L, 1);
5. 将 subject->object predicate shift 与 image_feature 相乘(相当于对 image_feature 的每个位置进行加权)后,逐位置 embedded_object 进行内积,计算得到新的 object attention map;同时, 将 object->subject predicate shift 与 image_feature 相乘,并逐位置与 embedded_subject 进行内积,计算得到新的 subject attention map;
6. 更新 subject/object attention map,进行第 4 步,循环迭代多次;
7. 迭代完成之后,基于 subject attention map 计算 subject 区域,基于 object attention map 计算 object 区域(所以说论文里提供的上面流程图中最后的示例图像是反了吗?);
注意:第 5 步利用了作者所指出的:利用一个已知目标的位置,帮助另一个目标的检测。
代码阅读
阅读论文对整个算法的思想和流程有一个概念性的理解之后,通过阅读作者提供的,进一步理解算法。作者代码是基于 编写,不得不说,高手写的代码就是简洁美观,值得学习!
数据准备
数据准备的代码在 中,主要是对训练和测试数据进行组织,便于网络训练时的数据载入。作者将图像和 relationship 组织为 hdf5 文件,其中 relationship 的 hdf5 文件包含 3 个 databasde,图像的 hdf5 文件包含一个 database,如下代码展示:
1 dataset = h5py.File(os.path.join(save_dir, 'dataset.hdf5'), 'w')2 categories_db = dataset.create_dataset('categories', (total_relationships, 4), dtype='f')3 subject_db = dataset.create_dataset('subject_locations', (total_relationships, self.output_dim, self.output_dim), dtype='f')4 object_db = dataset.create_dataset('object_locations', (total_relationships, self.output_dim, self.output_dim), dtype='f')5 6 dataset = h5py.File(os.path.join(save_dir, 'images.hdf5'), 'w')7 images_db = dataset.create_dataset('images',(num_images, self.im_dim, self.im_dim, 3), dtype='f')
total_relationships 是图像集合所有图像包含的 relationship 总数,output_dim 是网络输出的 subject/object attention map 尺寸,即前面介绍的 L。
categories_db 存储了所有的 relationship,可以将其理解为 relationship x 4 的矩阵,矩阵每一行记录一个 relationship,形如:subject-predicate-object-image_id。subject、predicate、object分别为各自的类别标签,为整数,对于 VRD 数据集,subject、object 类别数为 100,predicate 类别数为 70,根据 image_id 可以在存储图像的 hdf5 文件中获取对应图像。
subject_db 存储了所有 relationship 中 subject 的 groundtruth mask,每个 mask 的尺寸 output_dim x output_dim;
object_db 存储了所有 relationship 中 object 的 groundtruth mask,每个 mask 尺寸为 output_dim x output_dim;
images_db 存储了所有的图像,图像已经过缩放和预处理,满足网络的输入要求,num_images 是图像数目,im_dim 为缩放后的图像尺寸;
获取数据的代码如下所示:
1 images = h5py.File(os.path.join(self.data_dir, 'images.hdf5'), 'r')2 dataset = h5py.File(os.path.join(self.data_dir, 'dataset.hdf5'), 'r')3 self.images = images['images']4 self.categories = dataset['categories']5 self.subjects = dataset['subject_locations']6 self.objects = dataset['object_locations']
训练时数据获取
作者使用 Keras 的数据自动生成器,继承自 keras.utils.Sequence,集合 fit_generator 实现在训练是并行读取数据,节约内存。对应的代码位于 ,重载了 __getitem__ 方法实现每个训练迭代时的批数据获取,核心代码如下:
inputs = [batch_image]if self.use_subject: subject_masks = np.random.choice( 2, end_idx-start_idx, p=[self.subject_droprate, 1.0 - self.subject_droprate,]) # 以 subject_droprate 的概率丢弃训练样本的 subject subject_cats = batch_rel[:, 0] subject_cats[subject_masks == 0] = self.num_objects inputs.append(subject_cats)if self.use_predicate: if self.categorical_predicate: inputs.append(to_categorical(batch_rel[:, 1], num_classes=self.num_predicates)) else: inputs.append(batch_rel[:, 1]) if self.use_object: object_masks = np.random.choice( 2, end_idx-start_idx, p=[self.object_droprate, 1.0 - self.object_droprate]) # 以 object_droprate 概率丢弃训练样本中的 object object_cats = batch_rel[:, 2] object_cats[object_masks == 0] = self.num_objects inputs.append(object_cats)outputs = [batch_s_regions, batch_o_regions]return inputs, outputs
batch_rel 是从 hdrf5 文件读取当前批次 relationship 信息(Bx4),batch_image 是尺寸为 BxHxWx3 的 tensor,B 为 batch size;
指明训练时使用 subject 信息时,以 subject_droprate 的概率丢弃当前 batch 中某些训练样本的 subject 信息,得到 subject_cats (B,),append 至 inputs
指明训练是使用 predicat 信息时,将 predicate 信息进行处理,变换为 one-hot 的向量(训练 SSAS模型时),尺寸为 B x num_predicates,append 至 inputs
指明训练时使用 object 信息时,以 subject_droprate 的概率丢弃当前 batch 中某些训练样本的 object 信息,得到 object_cats (B,),append 至 inputs
inputs = [batch_image (BxHxWx3), subject_cats (B,),predicat (B x num_predicates), object_cats (B, )]
对从 hdf5 文件中读取的 mask 进行了 reshape (L, L -> LxL) 构成当前批训练样本的目标输出,batch_s_regions, batch_o_regions 尺寸为 (B, LxL)
outputs = [batch_s_regions (B, LxL), batch_o_regions (B, LxL)],
网络模型搭建
以 SSAS 的模型构建为例,代码位于 以及 文件
relationships_model = ReferringRelationshipsModel(args)model = relationships_model.build_model()
第一行代码为网络的参数进行赋值,如下:
self.input_dim = args.input_dim # 网络输入图像尺寸self.feat_map_dim = args.feat_map_dim # 网络输出的 feature map 尺寸,即前面介绍过的 Lself.hidden_dim = args.hidden_dim # 在 pre-trained 网络输出 feature map 后添加卷积层学习 feature map 的 *卷积核数目*,即前面介绍的 Cself.num_objects = args.num_objects # 训练集中 subject/object 类别数self.num_predicates = args.num_predicates # 训练集中 predicate 类别数self.dropout = args.dropout # 网络中某些层学习时的 dropput 参数self.use_subject = args.use_subject # 训练时是否使用 subject 信息self.use_predicate = args.use_predicate # 训练时是否使用 predicate 信息self.use_object = args.use_object # 训练时是否使用 object 信息self.nb_conv_att_map = args.nb_conv_att_map # 学习 predicate shift 时,使用的 *卷积层数目*self.nb_conv_im_map = args.nb_conv_im_map # 在 pre-trained 网络基础添加卷积层学习 feature map 的 *卷积层数目*self.cnn = args.cnn # pre-trained 网络种类, resnet or vggself.feat_map_layer = args.feat_map_layer # 使用 pre-trained 网络的 *哪一层* 作为输出,进一步学习 feature mapself.conv_im_kernel = args.conv_im_kernel # pre-trained 网络输出基础上添加卷积层的 *卷积核尺寸*self.conv_predicate_kernel = args.conv_predicate_kernel # 学习 predicate shift 时,*卷积核尺寸*,即前面介绍的 kself.conv_predicate_channels = args.conv_predicate_channels # 学习 predicate shift 时,*卷积核数目*,即前面介绍的 cself.model = args.model # 建立何种类型的网络,本文以 SSAS 为例介绍self.use_internal_loss = args.use_internal_loss # 训练目标 是否加入 predicate shift 迭代中间过程输出的 predict shift map 指导学习self.internal_loss_weight = args.internal_loss_weight # 每个中间迭代结果的权重self.iterations = args.iterations # 计算 predicate shift 的迭代次数self.attention_conv_kernel = args.attention_conv_kernel # 未用到self.refinement_conv_kernel = args.refinement_conv_kernel # 未用到self.output_dim = args.output_dim # 网络目标检测输出 heatmap 尺寸,即 Lself.embedding_dim = args.embedding_dim # subject/object 映射为稠密向量时,第一层映射的全连接数目self.finetune_cnn = args.finetune_cnn # 训练时是否对 pre-trained 网络进行 finetune
第二行代码完成网络模型创建,创建网络的代码位于 。以 SSAS 模型为例:
1. 定义网络输入
input_im = Input(shape=(self.input_dim, self.input_dim, 3))input_subj = Input(shape=(1,))input_obj = Input(shape=(1,))if self.use_predicate: input_pred = Input(shape=(self.num_predicates,)) inputs=[input_im, input_subj, input_pred, input_obj]else: inputs=[input_im, input_subj, input_obj]
网络输入的类型需要跟前面定义的训练数据获取 generator 中生成的训练样本类型相同(忽略 batch_size 维度);
2. 基于 pre-trained 网络创建提取图像特征的网络计算图 im_feature,对应于开始流程图的第一步,主要代码如下:
im_features = self.build_image_model(input_im)def build_image_model(self, input_im): if self.cnn == "resnet": base_model = ResNet50( weights='imagenet', include_top=False, input_shape=(self.input_dim, self.input_dim, 3)) elif self.cnn == "vgg": base_model = VGG19( weights='imagenet', include_top=False, input_shape=(self.input_dim, self.input_dim, 3)) else: raise ValueError('--cnn must be [resnet, vgg] but got {}'.format(self.cnn)) if self.finetune_cnn: for layer in base_model.layers: layer.trainable = True layer.training = True else: for layer in base_model.layers: layer.trainable = False layer.training = False output = base_model.get_layer(self.feat_map_layer).output # 获取 basemodel 指定层为基础特征 image_branch = Model(inputs=base_model.input, outputs=output) # 以函数式模型构建方式,声明提取图像基础特征的网络 im_features = image_branch(input_im) # 提取图像特征的计算图 im_features = Dropout(self.dropout)(im_features) for i in range(self.nb_conv_im_map): # 在基础特征后接 nb_conv_im_map 个卷积层,每个卷积层卷积核个数为 hidden_dim,尺寸为 conv_im_kernel x conv_im_kernel im_features = Conv2D(self.hidden_dim, self.conv_im_kernel, strides=(1, 1), padding='same', activation='relu')(im_features) im_features = Dropout(self.dropout)(im_features) return im_features # 返回图像特征,尺寸为 feat_map_dim x feat_map_dim x hidden_dim
3. 定义 subject/object 由类别 id (整数)映射为稠密高维向量的网络计算图,计算 embedded_subject/object 对应于开始流程图的第二步
subj_obj_embedding = self.build_embedding_layer(self.num_objects, self.embedding_dim) # 定义全连接,将数字映射维度为 embedding_dim 的向量 def build_embedding_layer(self, num_categories, emb_dim): return Embedding(num_categories, emb_dim, input_length=1)
embedded_subject = subj_obj_embedding(input_subj) # 将 subject 应为为向量,R^(embedding_dim) embedded_subject = Dense(self.hidden_dim, activation="relu")(embedded_subject)# 增加一个全连接层,将 R^(embedding_dim) 向量应为为 R^(hidden_dim)向量,与上一步定义的图像特征通道数相同 embedded_subject = Dropout(self.dropout)(embedded_subject)
# object 的操作与 subject 相同embedded_object = subj_obj_embedding(input_obj)embedded_object = Dense(self.hidden_dim, activation="relu")(embedded_object)embedded_object = Dropout(self.dropout)(embedded_object)
4. 基于图像特征 im_feature 和 subject/object embedding,计算 subject/ojbect attention map 的网络计算图,对应开始流程图的第三步
subject_att = self.attend(im_features, embedded_subject, name='subject-att-0')# 基于 im_features (LxLxC) 和 embedded_subject (R^C),计算 subject attention mapobject_att = self.attend(im_features, embedded_object, name='object-att-0')def attend(self, feature_map, query, name=None): query = Reshape((1, 1, self.hidden_dim,))(query) # 输入向量由 R^(hidden_dim) reshape 为 (1,1, hidden_dim) attention_weights = Multiply()([feature_map, query]) # 在 feature_map 每个位置的 hidden_dim 个通道上与 query 进行 element-wise 求乘积 attention_weights = Lambda(lambda x: K.sum(x, axis=3, keepdims=True))(attention_weights) # 每个位置上的对所有通道的乘积结果求和 attention_weights = Activation("relu", name=name)(attention_weights) # 使用 relu 激活函数计算响应,得到 attention map return attention_weights
5. 基于 subject/ojbect attention map 和 predicate,计算 predicate-shift-map 的网络计算图,对应开始流程的第四步
首先定义所有 predicate 对应的 predicate-shift-map 网络计算图,由多个卷积层连接而成:
# 定义基于 subject attention map 和 predicate,计算 subject->object 的 predicate shift 网络流程图 # 注意,这里对 *所有的 predicate *都定义了一个专属计算 predicate-shift/inv-predicate-shift 网络流程图 predicate_modules = self.build_conv_modules(basename='conv{}-predicate{}') inverse_predicate_modules = self.build_conv_modules(basename='conv{}-inv-predicate{}')def build_conv_modules(self, basename): predicate_modules = [] for k in range(self.num_predicates): # 训练集有 num_predicates 个 predicate,每个 predicate 定义一个网络流程图 predicate_module_group = [] for i in range(self.nb_conv_att_map-1): # 前 nb_conv_att_map-1 个卷积层,卷积核数目为 conv_predicate_channels,尺寸为 conv_predicate_kernel predicate_conv = Conv2D(self.conv_predicate_channels, self.conv_predicate_kernel, strides=(1, 1), padding='same', use_bias=False, activation='relu', name=basename.format(i, k)) predicate_module_group.append(predicate_conv) # last conv with only one channel,最后一个卷积层,卷积核数目为 1,保证 predicate shift 通道数为 1 predicate_conv = Conv2D(1, self.conv_predicate_kernel, strides=(1, 1), padding='same', use_bias=False, activation='relu', name=basename.format(self.nb_conv_att_map-1, k)) predicate_module_group.append(predicate_conv) # 同一个 predicate 的所有卷积构成一组 predicate_modules.append(predicate_module_group) # 保存所有 predicate 的卷积层组合 return predicate_modules
以初始 subject/object attention,input_pred 为基础,迭代计算更新 predicate-shift-map 和 subject/object attention map,对应开始流程图的第五步:
循环流程如下:
subject-attetion-map + predicate -> subject-to-object-shift-map -> subject-to-object-shift-map + im_feature + object-embedding -> new-object-attention-map
object-attention-map + predicate -> object-to-subject-shift-map -> object-to-subject-shift-map + im_feature + subject-embedding -> new-subject-attention-map
predicate_masks = Reshape((1, 1, self.num_predicates))(input_pred) # 将 predicate 的 one-hot 向量 reshape 为 (1,1,num_predicates) for iteration in range(self.itreations): # 以 subject attention map 为输出,使用 predicate 对应的卷积层配置,计算 subject->object 的 shift map predicate_att = self.shift_conv_attention(subject_att, predicate_modules, predicate_masks) # 计算 *subject->object* 的 predicate shift map predicate_att = Lambda(lambda x: x, name='shift-{}'.format(iteration+1))(predicate_att) new_image_features = Multiply()([im_features, predicate_att]) # 对 im_feature 不同区域进行加权,选择 object 显著区域 new_object_att = self.attend(new_image_features, embedded_object, name='object-att-{}'.format(iteration+1)) # 更新 object attention map # 以 object attention map 为输入,使用 predicate 对应的卷积层配置,计算 object->subject 的 shift-map inv_predicate_att = self.shift_conv_attention(object_att, inverse_predicate_modules, predicate_masks) # 计算 *object->subject* 的 predicate shift map inv_predicate_att = Lambda(lambda x: x, name='inv-shift-{}'.format(iteration+1))(inv_predicate_att) new_image_features = Multiply()([im_features, inv_predicate_att]) # 对 im_feature 不同区域加权,选择 subject 显著区域 new_subject_att = self.attend(new_image_features, embedded_subject, name='subject-att-{}'.format(iteration+1)) # 更新 subject attention map if self.use_internal_loss: # 如果指定了目标函数考虑中间结果,则将中间结果保存 object_outputs.append(new_object_att) subject_outputs.append(new_subject_att) object_att = new_object_att # 更新 subject/object attention map,准备下次迭代 subject_att = new_subject_attdef shift_conv_attention(self, att, merged_modules, predicate_masks): conv_outputs = [] for group in merged_modules: att_map = att for conv_module in group: att_map = conv_module(att_map) conv_outputs.append(att_map) merged_output = Concatenate(axis=3)(conv_outputs) # 使用所有 predicate 的 shift 计算网络,计算 predicate-shfit map,所有的计算结果 concat predicate_att = Multiply()([predicate_masks, merged_output]) # 与 predicate_mask 相乘,输入 predicate 对应的 shift map 保留,其余置 0 predicate_att = Lambda(lambda x: K.sum(x, axis=3, keepdims=True))(predicate_att) # 乘积结果相加,得到正确的 shift-map predicate_att = Activation("tanh")(predicate_att) # 使用 tanh 为响应函数,输出最终的 shift-map (LxLx1) return predicate_att
最后,以迭代计算最终的 subject/object attention map 为基础,预测 subject/object 目标位置,定义最终的网络模型计算图
subject_att = Activation("tanh")(subject_att)object_att = Activation("tanh")(object_att)subject_regions = Reshape((self.output_dim * self.output_dim,), name="subject")(subject_att)object_regions = Reshape((self.output_dim * self.output_dim,), name="object")(object_att) model = Model(inputs=inputs, outputs=[subject_regions, object_regions]) return model
至此,SSAS 网络模型定义完成,注意,网络的 inputs 和 outputs 要和训练数据获取的类型尺寸相同(去掉 batch-size 维度)
网络训练
所有工作完成之后,使用 model.compile 配置网络训练,如目标函数,优化器,测试函数等;使用 model.fit_generator 进行训练,通过看工程代码可以很清楚的了解,此处不再赘述了