考虑到标注数据贫乏的数量,作者选择了预训练模型的方式。作者在大型辅助数据集(ILSVRC2012分类)上使用Caffe CNN开源库预先训练CNN,该过程只是训练了CNN的图像分类能力,并不具备目标定位能力(训练标签中并不含有图像中物体的位置信息)。预训练CNN基于ImageNet训练的网络,具有1000路输出。
通过selective search在每一张图片上进行候选区提取,得到候选区后,将候选区变换到匹配CNN输入的维度。重新定义CNN,输出层维度为N+1(N是目标的类别数,1代表背景,作者使用的VOC 2010数据集,N=20,背景为1)。然后加载预训练分类模型,通过设置参数舍弃输出层,替代为重定义的输出层。使用随机梯度下降法(SGD),学习率使用0.001(预训练模型初始学习率的1/10),它允许在不破坏初始化的情况下进行微调。在每一次的迭代训练中采样32个正样本(目标物体类别)及96个负样本(背景)包装成一个128的batch进行训练。这里作者采用了一个trick,将抽样倾向于正样本,因为与背景相比,正样本是非常罕见的。训练完成后,撤去最后的softmax层,最后一层全连接层输出的就是在候选区提取的固定长度的特征向量(论文中使用的是AlexNet作为主干网络,提取固定的4096维特征向量)。
注意此时的CNN功能,因为输入数据格式仍是[images,labels],因此,fine-tuning后的CNN仍然只具有分类功能,不具备目标定位功能。
作者使用了IOU为候选区决定标签,并采用了网格搜索的方法测试了不同IOU选择下的模型的效果,论文中确定为IOU=0.3。详细内容在后面解释。利用微调后的AlexNet作为主干网络提取到候选区的4096维特征向量,搭配候选区的标签,就可以对每一个类训练一个SVM分类器。在论文中,使用了负例采样(hard negative mining method)进行训练模型,发现模型收敛很快。实际上为每一个类别训练线性 SVM的过程就是学习一个M*N维权重矩阵的过程,M为输入的特征向量的维度,N+1为类别的分类总数(包含背景)即线性SVM的个数。因此,将每一个特定类别的线性SVM分类器组合起来我们可以得到将不同目标物体划分开来的超平面。
为什么需要专门训练目标检测回归器呢?实际上,在之前的步骤我们已经可以实现简单的目标定位了,因为通过selective search我们已经可以得到图像上很多的目标候选区,然后借助于SVM分类器我们可以得到每一个候选区有没有目标物体,如果有是什么。因此知道了候选区的位置与可能包含的目标类别,再通过非极大值抑制方法滤除多余的候选框就可以将目标物体检测出来了。然而,这张方法中得到的定位信息基于selective search算法,而selective search算法是基于图片的颜色和纹理,最简单的理解就是容易受到光照的干扰,鲁棒性差,检测效果不好。
因此,论文中作者单独训练了目标回归器,在selective search算法得到的候选区的基础上修正定位信息,从而提高了目标定位的准确率。详细做法见后文。
通过selective search得到候选区后,需要为候选区挂上标签。
可以简单解释为:
如上图需要定位一个车辆,使用算法找出了一堆的候选区,我们需要判别哪些矩形框是没用的。
非极大值抑制的方法是:先假设有6个候选框,根据分类器的类别分类概率做排序,假设从小到大属于车辆的概率 分别为A、B、C、D、E、F。
使用线性分类器SVM为每一个region proposal打分之后,训练新的检测回归器修正使用selective search算法得到的定位信息。这里所谓的打分,就是分类正确的概率,即使用SVM正确预测候选区类别的概率。
训练目标回归器时,模型的输入数据是一系列的训练数据对,格式为:
{ ( P i , G i ) } \left\{\begin{matrix}(P^i, \end{matrix}G^i)\right\} {(Pi,Gi)}
P i = ( P x i , P y i , P w i , P h i ) P^i = (P^i_x, P^i_y, P^i_w, P^i_h) Pi=(Pxi,Pyi,Pwi,Phi)
G = ( G x , G y , G w , G h ) G^ = (G_x, G_y, G_w, G_h) G=(Gx,Gy,Gw,Gh)
P i P^i Pi是region proposal的位置信息, G G G为ground truth的定位信息,回归器的目标是去学习到将region proposal映射到ground truth的转换。作者构造了四个函数: d x ( P ) , d y ( P ) , d w ( P ) , d h ( P ) d_x(P), d_y(P), d_w(P), d_h(P) dx(P),dy(P),dw(P),dh(P)
其中, d x ( P ) , d y ( P ) d_x(P), d_y(P) dx(P),dy(P)指定了region proposal中心坐标尺度不变的转换方式, d w ( P ) , d h ( P ) d_w(P), d_h(P) dw(P),dh(P)指定了基于对数空间的region proposal高和宽的转换方式
学习到这些函数之后,就可以将region proposal映射到最终预测的定位框 G i G^i Gi:
G ^ x = P w d x ( P ) + P x \hat G_x = P_wd_x(P) + P_x G^x=Pwdx(P)+Px
G ^ y = P h d y ( P ) + P y \hat G_y = P_hd_y(P) + P_y G^y=Phdy(P)+Py
G ^ w = P w e x p ( d w ( P ) ) \hat G_w = P_wexp(d_w(P)) G^w=Pwexp(dw(P))
G ^ h = P h e x p ( d h ( P ) ) \hat G_h = P_hexp(d_h(P)) G^h=Phexp(dh(P))
作者将这四个函数建模为region proposals 的pool5特征( φ 5 ( P ) φ_5(P) φ5(P))的线性函数(作者将pool5输出的特征假设是独立的)
进一步表示为:
d ∗ = w ∗ T φ 5 ( P ) d_* = w^T_*φ_5(P) d∗=w∗Tφ5(P)
其中, w ∗ 是 需 要 学 习 的 参 数 向 量 w_*是需要学习的参数向量 w∗是需要学习的参数向量
损失函数使用了均方差损失函数,搭配L2正则化即岭回归(enginelong的博客):
w ∗ = a r g m i n ∑ i N ( t ∗ i − w ∗ T φ 5 ( P i ) ) 2 + λ ∣ ∣ w ∗ ∣ ∣ 2 w_* = argmin\sum_i^{N}(t^i_* - w^T_*φ_5(P^i))^2 + \lambda||w_*||^2 w∗=argmini∑N(t∗i−w∗Tφ5(Pi))2+λ∣∣w∗∣∣2
其中 t ∗ t_* t∗为:
t x = ( G x − P x ) / P w t_x = (G_x - P_x) / P_w tx=(Gx−Px)/Pw
t y = ( G y − P y ) / P h t_y = (G_y - P_y) / P_h ty=(Gy−Py)/Ph
t w = l o g ( G w / P w ) t_w = log(G_w/P_w) tw=log(Gw/Pw)
t h = l o g ( G h / P h ) t_h = log(G_h/P_h) th=log(Gh/Ph)
这里有两个细节:
当然,R-CNN原文中还有大量的细节,比如
详见代码
喜欢使用matlab的小伙伴请见论文源码·
# Building 'AlexNet'
def create_alexnet(num_classes):
network = input_data(shape=[None, config.IMAGE_SIZE, config.IMAGE_SIZE, 3])
# 4维输入张量,卷积核个数,卷积核尺寸,步长
network = conv_2d(network, 96, 11, strides=4, activation='relu')
network = max_pool_2d(network, 3, strides=2)
# 数据归一化
network = local_response_normalization(network)
network = conv_2d(network, 256, 5, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 256, 3, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, num_classes, activation='softmax')
momentum = tflearn.Momentum(learning_rate=0.001, lr_decay=0.95, decay_step=200)
network = regression(network, optimizer=momentum,
loss='categorical_crossentropy')
return network
# Use a already trained alexnet with the last layer redesigned
def create_alexnet(num_classes, restore=False):
# Building 'AlexNet'
network = input_data(shape=[None, config.IMAGE_SIZE, config.IMAGE_SIZE, 3])
network = conv_2d(network, 96, 11, strides=4, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 256, 5, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 256, 3, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
# do not restore this layer
network = fully_connected(network, num_classes, activation='softmax', restore=restore)
network = regression(network, optimizer='momentum',
loss='categorical_crossentropy',
learning_rate=0.001)
return network
# Use a already trained alexnet with the last layer redesigned
# 减去softmax输出层,获得图片的特征
def create_alexnet():
# Building 'AlexNet'
network = input_data(shape=[None, config.IMAGE_SIZE, config.IMAGE_SIZE, 3])
network = conv_2d(network, 96, 11, strides=4, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 256, 5, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 256, 3, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, 4096, activation='tanh')
network = regression(network, optimizer='momentum',
loss='categorical_crossentropy',
learning_rate=0.001)
return network
# Construct cascade svms
def train_svms(train_file_folder, model):
files = os.listdir(train_file_folder)
svms = []
train_features = []
bbox_train_features = []
rects = []
for train_file in files:
if train_file.split('.')[-1] == 'txt':
pred_last = -1
pred_now = 0
X, Y, R = generate_single_svm_train(os.path.join(train_file_folder, train_file))
Y1 = []
features1 = []
Y_hard = []
features_hard = []
for ind, i in enumerate(X):
# extract features 提取特征
feats = model.predict([i])
train_features.append(feats[0])
# 所有正负样本加入feature1,Y1
if Y[ind]>=0:
Y1.append(Y[ind])
features1.append(feats[0])
# 对与groundtruth的iou>0.6的加入boundingbox训练集
if Y[ind]>0:
bbox_train_features.append(feats[0])
rects.append(R[ind])
# 剩下作为测试集
else:
Y_hard.append(Y[ind])
features_hard.append(feats[0])
tools.view_bar("extract features of %s" % train_file, ind + 1, len(X))
# 难负例挖掘
clf = SVC(probability=True)
# 训练直到准确率不再提高
while pred_now > pred_last:
clf.fit(features1, Y1)
features_new_hard = []
Y_new_hard = []
index_new_hard = []
# 统计测试正确数量
count = 0
for ind, i in enumerate(features_hard):
# print(clf.predict([i.tolist()])[0])
if clf.predict([i.tolist()])[0] == 0:
count += 1
# 如果被误判为正样本,加入难负例集合
elif clf.predict([i.tolist()])[0] > 0:
# 找到被误判的难负例
features_new_hard.append(i)
Y_new_hard.append(clf.predict_proba([i.tolist()])[0][1])
index_new_hard.append(ind)
# 如果难负例样本过少,停止迭代
if len(features_new_hard)/10<1:
break
pred_last = pred_now
# 计算新的测试正确率
pred_now = count / len(features_hard)
# print(pred_now)
# 难负例样本根据分类概率排序,取前10%作为负样本加入训练集
sorted_index = np.argsort(-np.array(Y_new_hard)).tolist()[0:int(len(features_new_hard)/10)]
for idx in sorted_index:
index = index_new_hard[idx]
features1.append(features_new_hard[idx])
Y1.append(0)
# 测试集中删除这些作为负样本加入训练集的样本。
features_hard.pop(index)
Y_hard.pop(index)
print(' ')
print("feature dimension")
print(np.shape(features1))
svms.append(clf)
# 将clf序列化,保存svm分类器
joblib.dump(clf, os.path.join(train_file_folder, str(train_file.split('.')[0]) + '_svm.pkl'))
# 保存boundingbox回归训练集
np.save((os.path.join(train_file_folder, 'bbox_train.npy')),
[bbox_train_features, rects])
# print(rects[0])
return svms
# 训练boundingbox回归
def train_bbox(npy_path):
features, rects = np.load((os.path.join(npy_path, 'bbox_train.npy')))
# 不能直接np.array(),应该把元素全部取出放入空列表中。因为features和rects建立时用的append,导致其中元素结构不能直接转换成矩阵
X = []
Y = []
for ind, i in enumerate(features):
X.append(i)
X_train = np.array(X)
for ind, i in enumerate(rects):
Y.append(i)
Y_train = np.array(Y)
# 线性回归模型训练
clf = Ridge(alpha=1.0)
clf.fit(X_train, Y_train)
# 序列化,保存bbox回归
joblib.dump(clf, os.path.join(npy_path,'bbox_train.pkl'))
return clf
学习论文初级阶段,由于考研时间紧张,时间不充分肯定会有理解错误的地方,希望各位小伙伴批评指正!!!