标签:kaggle 人脸关键点检测 卷积神经网络 数据扩充 机器学习模型分析
上图演示了卷积操作
LeNet-5式的卷积神经网络,是计算机视觉领域近期取得的巨大突破的核心。卷积层和之前的全连接层不同,采用了一些技巧来避免过多的参数个数,但保持了模型的描述能力。这些技巧是:
1, 局部联结:神经元仅仅联结前一层神经元的一小部分。
2, 权重共享:在卷积层,神经元子集之间的权重是共享的。(这些神经元的形式被称为特征图[feature map])
3, 池化:对输入进行静态的子采样。
局部性和权重共享的图示
卷积层的单元实际上连接了前一层神经元中的一个小的2维patch,先验知识让网络利用了输入中的2维结构。
当使用Lasagne中的卷积层时,我们必须进行一些输入准备。输入不再像刚刚一样是一个9216维的扁平向量,而是一个有着(c,0,1)形式的三维矩阵,其中C代表通道(颜色),0和1对应着图像的x和y维度。在我们的问题中,具体的三维矩阵为(1,96,96),因为我们仅仅使用了灰度一个颜色通道。
一个函数load2d对前述的load函数进行了包装,完成这个2维到三维的转变:
def load2d(test=False, cols=None):
X, y = load(test=test)
X = X.reshape(-1, 1, 96, 96)
return X, y
我们将要创建一个具有三个卷积层和两个全连接层的卷积神经网络。每个卷积层都跟着一个2*2的最大化池化层。初始卷积层有32个filter,之后每个卷积层我们把filter的数量翻番。形式为全连接的隐层将包含500个神经元。
这里还是一样没有任何形式(惩罚权重或者dropout)的正则化。事实证明当我们使用尺寸非常小的filter,如3*3或2*2,这些filter事实上已经起到了非常不错的正则化效果。
代码如下:
net2 = NeuralNet(
layers=[
(‘input‘, layers.InputLayer),
(‘conv1‘, layers.Conv2DLayer),
(‘pool1‘, layers.MaxPool2DLayer),
(‘conv2‘, layers.Conv2DLayer),
(‘pool2‘, layers.MaxPool2DLayer),
(‘conv3‘, layers.Conv2DLayer),
(‘pool3‘, layers.MaxPool2DLayer),
(‘hidden4‘, layers.DenseLayer),
(‘hidden5‘, layers.DenseLayer),
(‘output‘, layers.DenseLayer),
],
input_shape=(None, 1, 96, 96),
conv1_num_filters=32, conv1_filter_size=(3, 3), pool1_pool_size=(2, 2),
conv2_num_filters=64, conv2_filter_size=(2, 2), pool2_pool_size=(2, 2),
conv3_num_filters=128, conv3_filter_size=(2, 2), pool3_pool_size=(2, 2),
hidden4_num_units=500, hidden5_num_units=500,
output_num_units=30, output_nonlinearity=None,
update_learning_rate=0.01,
update_momentum=0.9,
regression=True,
max_epochs=1000,
verbose=1,
)
X, y = load2d() # load 2-d data
net2.fit(X, y)
# Training for 1000 epochs will take a while. We‘ll pickle the
# trained model so that we can load it back later:
import cPickle as pickle
with open(‘net2.pickle‘, ‘wb‘) as f:
pickle.dump(net2, f, -1)
训练这个网络和第一个网络相比,将要耗费巨大的时空资源。每次迭代要慢15倍,整个1000次迭代下来要耗费20多分钟的时间,这还是在你有一个相当不错的GPU的基础上。
然而耐心总是得到回馈,我们的结果自然比刚刚好得多。让我们来看一看运行脚本时的输出,显示一系列网络层的形状,注意因为我们选择的窗口尺寸,第一个卷积层的32个filter输出了32张94*94 的特征图。
InputLayer (None, 1, 96, 96) produces 9216 outputs
Conv2DCCLayer (None, 32, 94, 94) produces 282752 outputs
MaxPool2DCCLayer (None, 32, 47, 47) produces 70688 outputs
Conv2DCCLayer (None, 64, 46, 46) produces 135424 outputs
MaxPool2DCCLayer (None, 64, 23, 23) produces 33856 outputs
Conv2DCCLayer (None, 128, 22, 22) produces 61952 outputs
MaxPool2DCCLayer (None, 128, 11, 11) produces 15488 outputs
DenseLayer (None, 500) produces 500 outputs
DenseLayer (None, 500) produces 500 outputs
DenseLayer (None, 30) produces 30 outputs
接下来我们看到,和第一个网络输出相同,是每一次迭代训练损失和验证损失以及他们之间的比率。
1000次迭代后的结果相对第一个网络,有了非常不错的提升
>>> np.sqrt(0.001566) * 48
1.8994904579913006
我们从测试集合里面取同一个样例画出两个网络的预测结果来进行对比:
sample1 = load(test=True)[0][6:7]
sample2 = load2d(test=True)[0][6:7]
y_pred1 = net1.predict(sample1)[0]
y_pred2 = net2.predict(sample2)[0]
fig = pyplot.figure(figsize=(6, 3))
ax = fig.add_subplot(1, 2, 1, xticks=[], yticks=[])
plot_sample(sample1[0], y_pred1, ax)
ax = fig.add_subplot(1, 2, 2, xticks=[], yticks=[])
plot_sample(sample1[0], y_pred2, ax)
pyplot.show()
net1(左边)和net2预测结果的对比
然后我们可以画出两个网络的学习曲线:
看起来非常不错,新的曲线非常平滑,但是应该注意的是,net2的验证错误曲线相对net1来说,比训练错误曲线更快的趋于水平。我相信如果使用更大的训练集合,结果还会进一步提升。如果我们把训练用例进行水平对称变换,会不会因为训练集的翻倍使得网络进一步改进呢?
通常情况下,增加训练集的数量会让一个过拟合的网络取得更好的训练结果。(如果你的网络没有过拟合,那么你最好把它变大。(否则不足以描述数据))
数据扩成使我们人为通过一些手段(变形、添加噪声等等)增加训练用例的个数。显然,这要比手工收集更多的样本经济许多。所以,数据扩充是深度学习工具箱里必不可少的工具。
我们曾经在前面简短的提到batch learning。采集一个训练集合的矩阵,分成不同的批次(在我们的任务128个用例一批),是批处理迭代器的任务。 当把训练样本分成批次的时候,批处理迭代器可以顺便把输入变形这件事做的又快又好。所以当我们想要进行水平翻转的时候,我们不需要去翻倍数量巨大的训练集合。更好的方法是,我们在进行批处理迭代的时候,选取50%的几率进行水平翻转就可以了。这非常的方便,对某些问题来说这种手段可以让我们生产近乎无限的训练集,而不需要增加内存的使用。同时,输入图片的变形操作可以在GPU进行上一个批次运算的时候进行,所以可以说,这个操作几乎不增加任何额外的资源消耗。
水平翻转图片事实上仅仅是矩阵的切片操作:
X, y = load2d()
X_flipped = X[:, :, :, ::-1] # simple slice to flip all images
# plot two images:
fig = pyplot.figure(figsize=(6, 3))
ax = fig.add_subplot(1, 2, 1, xticks=[], yticks=[])
plot_sample(X[1], y[1], ax)
ax = fig.add_subplot(1, 2, 2, xticks=[], yticks=[])
plot_sample(X_flipped[1], y[1], ax)
pyplot.show()
原始图片(左)和翻转图片
在右边的图片中,值得注意的是关键点的位置并不匹配。因为我们翻转了图片,所以我们也必须翻转目标位置的横坐标,同时,还要交换目标值的位置,因为left_eye_center_x 变换过之后的值,指示的实际上是right_eye_center_x。我们建立一个元组flip_indices来保存哪一列的目标向量需要交换位置。如果你还记得,最开始的时候我们读取的数据条数是这样的:
left_eye_center_x 7034
left_eye_center_y 7034
right_eye_center_x 7032
right_eye_center_y 7032
left_eye_inner_corner_x 2266
left_eye_inner_corner_y 2266
...
因为left_eye_center_x要和right_eye_center_x换位置,我们记录(0,2),同样left_eye_center_y要和right_eye_center_y换位置,我们记录元组(1,3),以此类推。最后,我们获得元组集合如下:
flip_indices = [
(0, 2), (1, 3),
(4, 8), (5, 9), (6, 10), (7, 11),
(12, 16), (13, 17), (14, 18), (15, 19),
(22, 24), (23, 25),
]
# Let‘s see if we got it right:
df = read_csv(os.path.expanduser(FTRAIN))
for i, j in flip_indices:
print("# {} -> {}".format(df.columns[i], df.columns[j]))
# this prints out:
# left_eye_center_x -> right_eye_center_x
# left_eye_center_y -> right_eye_center_y
# left_eye_inner_corner_x -> right_eye_inner_corner_x
# left_eye_inner_corner_y -> right_eye_inner_corner_y
# left_eye_outer_corner_x -> right_eye_outer_corner_x
# left_eye_outer_corner_y -> right_eye_outer_corner_y
# left_eyebrow_inner_end_x -> right_eyebrow_inner_end_x
# left_eyebrow_inner_end_y -> right_eyebrow_inner_end_y
# left_eyebrow_outer_end_x -> right_eyebrow_outer_end_x
# left_eyebrow_outer_end_y -> right_eyebrow_outer_end_y
# mouth_left_corner_x -> mouth_right_corner_x
# mouth_left_corner_y -> mouth_right_corner_y
我们的批处理迭代器的实现将会从BachIterator类派生,重载transform()方法。把这些东西组合到一起,看看完整的代码:
class FlipBatchIterator(BatchIterator):
flip_indices = [
(0, 2), (1, 3),
(4, 8), (5, 9), (6, 10), (7, 11),
(12, 16), (13, 17), (14, 18), (15, 19),
(22, 24), (23, 25),
]
def transform(self, Xb, yb):
Xb, yb = super(FlipBatchIterator, self).transform(Xb, yb)
# Flip half of the images in this batch at random:
bs = Xb.shape[0]
indices = np.random.choice(bs, bs / 2, replace=False)
Xb[indices] = Xb[indices, :, :, ::-1]
if yb is not None:
# Horizontal flip of all x coordinates:
yb[indices, ::2] = yb[indices, ::2] * -1
# Swap places, e.g. left_eye_center_x -> right_eye_center_x
for a, b in self.flip_indices:
yb[indices, a], yb[indices, b] = (
yb[indices, b], yb[indices, a])
return Xb, yb
使用上述批处理迭代器进行训练,需要把它作为batch_iterator_train参数传递给NeuralNet。让我们来定义net3,一个和net2非常相似的网络。仅仅是在网络的最后添加了这些行:
net3 = NeuralNet(
# ...
regression=True,
batch_iterator_train=FlipBatchIterator(batch_size=128),
max_epochs=3000,
verbose=1,
)
现在我们已经采用了最新的翻转技巧,但同时我们将迭代次数增长了三倍。因为我们并没有真正改变训练集合的总数,所以每个epoch仍然使用和刚刚一样多的样本个数。事实证明,采用了新技巧后,每个训练epoch还是比刚刚多用了一些时间。这次我们的网络学到的东西更具一般性,理论上来讲学习更一般性的规律比学出过拟合总是要更难一些。
这次网络将花费一小时的训练时间,我们要确保在训练之后,把得到的模型保存起来。然后就可以去喝杯茶或者做做家务活,洗衣服也是不错的选择。
net3.fit(X, y)
import cPickle as pickle
with open(‘net3.pickle‘, ‘wb‘) as f:
pickle.dump(net3, f, -1)
$ python kfkd.py
...
Epoch | Train loss | Valid loss | Train / Val
--------|--------------|--------------|----------------
...
500 | 0.002238 | 0.002303 | 0.971519
...
1000 | 0.001365 | 0.001623 | 0.841110
1500 | 0.001067 | 0.001457 | 0.732018
2000 | 0.000895 | 0.001369 | 0.653721
2500 | 0.000761 | 0.001320 | 0.576831
3000 | 0.000678 | 0.001288 | 0.526410
让我们画出学习曲线和net2对比。应该看到3000次迭代后的效果,net3比net2的验证损失要小了5%。我们看到在2000次迭代之后,net2已经停止学习了,并且曲线变得不光滑;但是net3的效果却在一直改进,尽管进展缓慢。
看起来费了很大的劲却只取得了一点点加成,是这样吗?我们将在下一部分找到答案。
To be continue
使用CNN(convolutional neural nets)检测脸部关键点教程(三):卷积神经网络训练和数据扩充
标签:kaggle 人脸关键点检测 卷积神经网络 数据扩充 机器学习模型分析
原文地址:http://blog.csdn.net/tanhongguang1/article/details/46279991