总结迁移学习的各种情况,并在TensorFlow中对AlexNet进行迁移学习以对德国交通标志进行分类。
1. 迁移学习的四种情况
在迁移学习中,有四种情况决定着该如何训练pre-trained neural network以便将其应用于新的问题。而这取决于两个因素:新数据集的大小以及新数据集与原始数据集(指训练pre-trained neural network使用的数据集)的相似程度。
下面介绍一个三卷积三全连接层的网络,将其作为pre-trained neural network,来看四种情况下如何对其进行修改:
假设该训练好的网络第一层卷积用于检测边沿信息,第二层用于检测形状信息,第三层用于检测更为高级的特征。三个全连接层对高级特征组合后进行softmax分类。
1.1 新数据集小并且与原始数据集相似
在这种情况下(Feature extraction):
- 先去掉pre-trained neural network的最后一个全连接层。
- 新增加符合新数据集类别个数的全连接层。
- 其他预训练层的权值不动,只随机初始化新增加层的权值。
- 使用新数据集来训练新的全连接层。
为什么这样修改呢?因为新数据集小,首要考虑的一点就是防止过拟合,所有就保持原网络层的权值不动。另外因为数据集之间是相似的,那么也就可以认为网络之前在旧数据集上学得的高级特征(权值)也可用于新数据集。
1.2 新数据集小且数据集间不相似
在这种情况下:
- 只保留前几层卷积层,后面的其它层全部去掉。
- 然后添加一个新的全连接层,其神经元个数符合新数据集的类别数。
- 其他预训练层的权值不动,只随机初始化新增加层的权值。
- 使用新数据集来训练新的全连接层。
因为数据集小,所以首要考虑的还是防止发生过拟合,所以原网络层的权值不动。但是由于数据集已经不相似了,原网络中深层卷积所检测的高级特征就不能应用于新数据集了,所以就将其去掉。但由于原网络一般都在大量的训练集上训练,所以低级特征的检测,也就是前几层卷积还是可以用于新数据集的。
1.3 新数据集大且数据集间相似
在这种情况下(Fine tune):
- 先去掉pre-trained neural network的最后一个全连接层,然后添加一个新的全连接层,其神经元个数符合新数据集的类别数。
- 随机初始化新添加的全连接层的权值。
- 将预训练网络其它层的权值作为初始值。
- 在新数据集上训练网络。
此时,因为数据量大,并不容易发生过拟合,所以可以重训练整个网络。此外由于数据集相似,原网络层检测到的特征可用于新数据集,所以网络层上的原权值可以用来作为初始值,这样可以加快训练的速度。
1.4 新数据集大且数据集间不相似
这时可以:
- 先去掉pre-trained neural network的最后一个全连接层,然后添加一个新的全连接层,其神经元个数符合新数据集的类别数。
- 随机初始化全部层的权值,然后再新数据集上训练。
- 或者,像1.3中一样,将原网络权值作为初始值进行训练。虽然此时数据集并不相同,但是使用原网络权值也可能会加快训练速度。如果这样不行,那就重新随机初始化权值。
Feature extraction就是只训练新增加层的权值,其他层的权值保留不动。
Finetune就是使用原网络层的权值作为初始值,训练整个网络。
2. 使用TensorFlow对AlexNet进行特征提取
这一节介绍如何对AlexNet进行特征提取(Feature extraction),将其应用于德国交通标志数据集的识别。
2.1 AlexNet简介
AlexNet的论文在这里。
AlexNet为八层网络,包括五个卷积层和三个全连接层,最后接一个分1000类的softmax。其第一次使用ReLu作为激励函数,为了防止过拟合,在全连接层中还加入了dropout。论文中还介绍了一个regularize技术,即Local Response Normalization,是对卷积层的输出特征图按channel做regularize(并没有改变特征图的维度),使用后可提升网络的泛化能力,吴恩达老师说现在这个很少用了。使用了两块当时(2012)最好的GTX 580 3GB GPU以加速训练,两块GPU只在特定层进行内存通信。将特征图按channel分为两部分,分别存到两个GPU中。同样的将一层中的kernel数目也分为两部分,分别存到两个GPU中。当GPU通信时,相当于正常的卷积运算,即kernel的channel与输入特征图的channel相同。而当GPU不通信时,相当于单个GPU内的kernel只对该GPU内的部分特征图做卷积运算,此时kernel的channel只等于这部分特征图的channel,即总体特征图channel的一半。整体网络结构如下图:
第一层、第二层和第五层卷积后接了max-pooling,只有在第三层卷积和所有全连接层中,两GPU才相互通信。普遍认为,论文中对输入图片的大小描述错误,如果想要得到输出为48x48的特征图,对于kernel size=11,stride=4,而且没有padding的情况下,只有输入为227x227才对。此外,除第一层卷积之外,其他层卷积在运算前应该是都做了same-padding操作,但论文中并没有说。
作者分别使用了ILSVRC-2010和ILSVRC-2012训练集训练了两个版本的网络,为什么用了两个训练集呢?因为当时只有ILSVRC-2010的测试集数据有label,而ILSVRC-2012的测试集数据没有label,训练的效果可看论文。ILSVRC使用了ImageNet数据集的子集,有1000个类别,每个类别大概有1000张图片。由于每张图片的分辨率都不相同,所以论文中对各图片下采样到256x256像素,训练时会对其随机采227x227的patches,以进行data augment。只对数据进行了一种预处理,即使训练集上各像素点的均值为零。
还有一些训练上的细节,如如何增强的数据集、训练的过程可看论文。模型具体的效果可看论文,反正是ILSVRC-2012的冠军,错误率为15%。
2.2 AlexNet的TensorFlow实现
AlexNet的TensorFlow实现代码及权值来自这里。
下面的代码略有不同,只是将AlexNet的前向传播封装到一个函数中,并且提供了一个参数用于特征提取,注意这里在卷积运算前没有使用tf.pad来做padding,而是使用卷积运算API和max-pool API中的padding参数来设置,而TensorFlow中的padding操作略有区别,可看这里。这就使第一层和第二层卷积后的特征图尺寸略有不同:
"""
File - alexnet.py
改代码完全对应了AlexNet的结构,具体结构要看论文。
"""
import numpy as np
import tensorflow as tf
# npy的数组中只有一个字典,使用item()方法将其提取出来
# 字典的key为各层名,对应的value为一两元素列表
# 第一个为权值,第二个为偏置
net_data = np.load("bvlc-alexnet.npy", encoding="latin1").item()
# 这个group是什么意思呢?这就要看AlexNet的结构了。
# AlexNet使用了两块GPU来训练。
# 但两块GPU不是在所有卷积层上都传递信息
# 而只在特定层上传递信息。
# 没传递信息时,就相当于两个并行的卷积层,此时group=2,某一kernel只在一个GPU中做卷积运算
# 传递信息时,就是一个卷积层,此时group=1,某一kernel在两块GPU中都做卷积运算。
def conv(input, kernel, biases, k_h, k_w, c_o, s_h, s_w, padding="VALID", group=1):
'''
From https://github.com/ethereon/caffe-tensorflow
'''
c_i = input.get_shape()[-1]
assert c_i % group == 0
assert c_o % group == 0
convolve = lambda i, k: tf.nn.conv2d(i, k, [1, s_h, s_w, 1], padding=padding)
if tf.__version__ < "1.0.0":
if group == 1:
conv = convolve(input, kernel)
else:
input_groups = tf.split(3, group, input)
kernel_groups = tf.split(3, group, kernel)
output_groups = [convolve(i, k) for i, k in zip(input_groups, kernel_groups)]
conv = tf.concat(3, output_groups)
else:
if group == 1:
conv = convolve(input, kernel)
else:
input_groups = tf.split(input, group, 3)
kernel_groups = tf.split(kernel, group, 3)
output_groups = [convolve(i, k) for i, k in zip(input_groups, kernel_groups)]
conv = tf.concat(output_groups, 3)
return tf.reshape(tf.nn.bias_add(conv, biases), [-1] + conv.get_shape().as_list()[1:])
# 为AlexNet的前向传播过程
# 如果feature_extract=True,则返回倒数第二个全连接层的输出
# 可用于后面的特征提取。
def AlexNet(features, feature_extract=False):
"""
Builds an AlexNet model, loads pretrained weights
"""
# conv1
# conv(11, 11, 96, 4, 4, padding='VALID', name='conv1')
k_h = 11
k_w = 11
c_o = 96
s_h = 4
s_w = 4
# 使用训练好的权值初始化。
conv1W = tf.Variable(net_data["conv1"][0])
conv1b = tf.Variable(net_data["conv1"][1])
# input:(227,227,3)
conv1_in = conv(features, conv1W, conv1b, k_h, k_w, c_o, s_h, s_w, padding="SAME", group=1)
# output:(57,57,96)
conv1 = tf.nn.relu(conv1_in)
# lrn1
# lrn(2, 2e-05, 0.75, name='norm1')
radius = 2
alpha = 2e-05
beta = 0.75
bias = 1.0
lrn1 = tf.nn.local_response_normalization(conv1, depth_radius=radius, alpha=alpha, beta=beta, bias=bias)
# maxpool1
# max_pool(3, 3, 2, 2, padding='VALID', name='pool1')
k_h = 3
k_w = 3
s_h = 2
s_w = 2
padding = 'VALID'
# input: (57,57,96)
maxpool1 = tf.nn.max_pool(lrn1, ksize=[1, k_h, k_w, 1], strides=[1, s_h, s_w, 1], padding=padding)
# output: (28,28,96)
# conv2
# conv(5, 5, 256, 1, 1, group=2, name='conv2')
k_h = 5
k_w = 5
c_o = 256
s_h = 1
s_w = 1
group = 2
conv2W = tf.Variable(net_data["conv2"][0])
conv2b = tf.Variable(net_data["conv2"][1])
# input: (28,28,96)
conv2_in = conv(maxpool1, conv2W, conv2b, k_h, k_w, c_o, s_h, s_w, padding="SAME", group=group)
# output: (28,28,256)
conv2 = tf.nn.relu(conv2_in)
# lrn2
# lrn(2, 2e-05, 0.75, name='norm2')
radius = 2
alpha = 2e-05
beta = 0.75
bias = 1.0
lrn2 = tf.nn.local_response_normalization(conv2, depth_radius=radius, alpha=alpha, beta=beta, bias=bias)
# maxpool2
# max_pool(3, 3, 2, 2, padding='VALID', name='pool2')
k_h = 3
k_w = 3
s_h = 2
s_w = 2
padding = 'VALID'
# input: (28,28,256)
maxpool2 = tf.nn.max_pool(lrn2, ksize=[1, k_h, k_w, 1], strides=[1, s_h, s_w, 1], padding=padding)
# output: (13,13,256)
# conv3
# conv(3, 3, 384, 1, 1, name='conv3')
k_h = 3
k_w = 3
c_o = 384
s_h = 1
s_w = 1
group = 1
conv3W = tf.Variable(net_data["conv3"][0])
conv3b = tf.Variable(net_data["conv3"][1])
# input: (13,13,256)
conv3_in = conv(maxpool2, conv3W, conv3b, k_h, k_w, c_o, s_h, s_w, padding="SAME", group=group)
# output: (13,13,384)
conv3 = tf.nn.relu(conv3_in)
# conv4
# conv(3, 3, 384, 1, 1, group=2, name='conv4')
k_h = 3
k_w = 3
c_o = 384
s_h = 1
s_w = 1
group = 2
conv4W = tf.Variable(net_data["conv4"][0])
conv4b = tf.Variable(net_data["conv4"][1])
# input: (13,13,384)
conv4_in = conv(conv3, conv4W, conv4b, k_h, k_w, c_o, s_h, s_w, padding="SAME", group=group)
# output: (13,13,384)
conv4 = tf.nn.relu(conv4_in)
# conv5
# conv(3, 3, 256, 1, 1, group=2, name='conv5')
k_h = 3
k_w = 3
c_o = 256
s_h = 1
s_w = 1
group = 2
conv5W = tf.Variable(net_data["conv5"][0])
conv5b = tf.Variable(net_data["conv5"][1])
# input: (13,13,384)
conv5_in = conv(conv4, conv5W, conv5b, k_h, k_w, c_o, s_h, s_w, padding="SAME", group=group)
# output: (13,13,256)
conv5 = tf.nn.relu(conv5_in)
# maxpool5
# max_pool(3, 3, 2, 2, padding='VALID', name='pool5')
k_h = 3
k_w = 3
s_h = 2
s_w = 2
padding = 'VALID'
# input: (13,13,256)
maxpool5 = tf.nn.max_pool(conv5, ksize=[1, k_h, k_w, 1], strides=[1, s_h, s_w, 1], padding=padding)
# output: (6,6,256)
# fc6, 4096
fc6W = tf.Variable(net_data["fc6"][0])
fc6b = tf.Variable(net_data["fc6"][1])
flat5 = tf.reshape(maxpool5, [-1, int(np.prod(maxpool5.get_shape()[1:]))])
fc6 = tf.nn.relu(tf.matmul(flat5, fc6W) + fc6b)
# fc7, 4096
fc7W = tf.Variable(net_data["fc7"][0])
fc7b = tf.Variable(net_data["fc7"][1])
fc7 = tf.nn.relu(tf.matmul(fc6, fc7W) + fc7b)
if feature_extract:
return fc7
# fc8, 1000
fc8W = tf.Variable(net_data["fc8"][0])
fc8b = tf.Variable(net_data["fc8"][1])
logits = tf.matmul(fc7, fc8W) + fc8b
probabilities = tf.nn.softmax(logits)
return probabilities
如果要使用AlexNet对图片进行分类,只要在代码中导入该函数(AlexNet),在Session中run就可以了。使用如下:
"""
File - image_inference.py
"""
import time
import tensorflow as tf
import numpy as np
from scipy.misc import imread
# 这里的class_name是一个1000元素列表
# 对应着AlexNet分的1000个类别名
from caffe_classes import class_names
from alexnet import AlexNet
# AlexNet的输入图片为(227,227,3)
x = tf.placeholder(tf.float32, (None, 227, 227, 3))
# 这里禁止feature extraction
# 调用AlexNet,构建图上的Ops
probs = AlexNet(x, feature_extract=False)
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
# 这里读入两张图片,并使均值为0
im1 = (imread("poodle.png")[:, :, :3]).astype(np.float32)
im1 = im1 - np.mean(im1)
im2 = (imread("weasel.png")[:, :, :3]).astype(np.float32)
im2 = im2 - np.mean(im2)
# 对图片进行分类
t = time.time()
output = sess.run(probs, feed_dict={x: [im1, im2]})
# 输出分类的结果
for input_im_ind in range(output.shape[0]):
inds = np.argsort(output)[input_im_ind, :]
print("Image", input_im_ind)
for i in range(5):
# np.argsort()是升序排列,所以从-1开始索引
# 输出最高的5个概率
print("%s: %.3f" % (class_names[inds[-1 - i]], output[input_im_ind, inds[-1 - i]]))
print()
print("Time: %.3f seconds" % (time.time() - t))
2.3 将AlexNet应用于交通标志分类
AlexNet的输入图片为(227,227,3),而交通标志为(32,32,3)。这里要去掉AlexNet的最后一层的1000分类,新添加输出43类的全连接层。其它层的权值固定,只训练最后一层的权值。文件alexnet.py中的内容无需改变,训练代码如下:
"""
File - train_feature_extraction.py
"""
import pickle
import time
import numpy as np
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from alexnet import AlexNet
# 交通标志共43类
nb_classes = 43
epochs = 15
batch_size = 128
with open('./train.p', 'rb') as f:
data = pickle.load(f)
X_train, X_val, y_train, y_val = train_test_split(data['features'], data['labels'], test_size=0.33, random_state=0)
features = tf.placeholder(tf.float32, (None, 32, 32, 3))
labels = tf.placeholder(tf.int64, None)
# 因为只训练最后一个全连接层,所以只在其上加dropout
# 这个placeholder用于控制keep_prob
keep_prob = tf.placeholder(tf.float32, (None))
resized = tf.image.resize_images(features, (227, 227))
# Returns the second final layer of the AlexNet model,
# this allows us to redo the last layer for the traffic signs
# model.
fc7 = AlexNet(resized, feature_extract=True)
fc7 = tf.stop_gradient(fc7)
fc7 = tf.nn.dropout(fc7, keep_prob)
shape = (fc7.get_shape().as_list()[-1], nb_classes)
fc8W = tf.Variable(tf.truncated_normal(shape, stddev=1e-2))
fc8b = tf.Variable(tf.zeros(nb_classes))
logits = tf.nn.xw_plus_b(fc7, fc8W, fc8b)
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels)
loss_op = tf.reduce_mean(cross_entropy)
opt = tf.train.AdamOptimizer()
# 指定优化变量列表
train_op = opt.minimize(loss_op, var_list=[fc8W, fc8b])
init_op = tf.global_variables_initializer()
preds = tf.argmax(logits, 1)
accuracy_op = tf.reduce_mean(tf.cast(tf.equal(preds, labels), tf.float32))
def eval_on_data(X, y, sess):
total_acc = 0
total_loss = 0
for offset in range(0, X.shape[0], batch_size):
end = offset + batch_size
X_batch = X[offset:end]
y_batch = y[offset:end]
loss, acc = sess.run([loss_op, accuracy_op], feed_dict={features: X_batch, labels: y_batch, keep_prob:1.0})
total_loss += (loss * X_batch.shape[0])
total_acc += (acc * X_batch.shape[0])
return total_loss/X.shape[0], total_acc/X.shape[0]
with tf.Session() as sess:
sess.run(init_op)
print("Start training! ")
for i in range(epochs):
# training
X_train, y_train = shuffle(X_train, y_train)
t0 = time.time()
for offset in range(0, X_train.shape[0], batch_size):
end = offset + batch_size
# 本来想设置dropout防止过拟合的,结果发现不设置的效果更好。
# dropout虽然可以防止过拟合,但也会降低训练速度。
# 所以不应该先设置,等发生了过拟合后再解决过拟合问题
sess.run(train_op, feed_dict={features: X_train[offset:end], labels: y_train[offset:end], keep_prob:1.0})
val_loss, val_acc = eval_on_data(X_val, y_val, sess)
val_losst, val_acct = eval_on_data(X_train, y_train, sess)
print("Epoch", i+1)
print("Time: %.3f seconds" % (time.time() - t0))
print("Train Loss =", val_losst)
print("Train Accuracy =", val_acct)
print("Validation Loss =", val_loss)
print("Validation Accuracy =", val_acc)
print("")
w, b= sess.run([fc8W, fc8b])
para = dict(fc8=[w,b])
np.save('fc8.npy', np.array(para))
print("Done!!!!!!")
训练网络,15个epoch后,在验证集上可达到97%的正确率。