迁移学习总结

总结迁移学习的各种情况,并在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%的正确率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容