代码环境:
- python-3.7.6
- tensorflow-2.1.0
深度学习神经网络的一个优势是可以在相关问题上重用。迁移学习(Transfer learning)指的是对某种程度上相似的问题进行预测建模的技术,然后可以部分或全部重用该技术,以加快训练速度并改善所关注问题的模型性能。
在深度学习中,这意味着在新模型中重用预训练网络模型中的一层或多层权重,或者保持权重固定,对其进行微调,或者在训练模型时完全调整权重。
1. 迁移学习简介
迁移学习通常是指对第二个相关问题使用某种方式训练过的模型的过程。转移学习(transfer learning)和领域适应(domain adaptation)指的是在一种情况下(即分布P1)所学到的东西被用来改善另一种情况下的泛化能力(即分布P2)。
这通常是在有监督的学习环境中理解的,其中输入是相同的,但是目标可能具有不同的性质。例如,在第一个设置中了解一组视觉类别,例如猫和狗;然后在第二个设置中了解一组不同的视觉类别,例如蚂蚁和黄蜂。
迁移学习的好处是减少了神经网络模型的训练时间,并降低了泛化误差。迁移学习有两种主要方法:
- 权重初始化。
- 特征提取。
重用权重可以用作模型训练的起点,并可以根据新问题进行调整。这种用法将转移学习视为一种权重初始化方法。当第一个相关问题比感兴趣的问题具有更多的标记数据时,这可能会很有用,并且问题的结构相似性在两种情况下都可能有用。权重重用的目的是利用第一个设置中的数据来提取信息,这些信息在学习甚至直接在第二个设置中进行预测时都可能有用。
可替代地,可以不响应于新问题而调整网络的权重,仅训练重用层之后的新层来解释其输出。这种用法将转移学习视为一种特征提取方法。这种方法的一个例子是图片文本描述,重用经过深层卷积神经网络模型训练用于照片分类的模型作为特征提取器。
这些用法的变化可能不涉及最初不针对新问题训练模型的权重,而是稍后以较小的学习率对学习到的模型的所有权重进行微调。
2. 多分类问题实例
使用一个简单的多类分类问题来探讨迁移学习对模型性能的影响。在本部分创建具有两个特征(变量)和三个输出(三类)的简单实例。
2.1 构建数据
首先,使用scikit-learn库中的 make_blobs()
函数创建两个标准差为2的随机样本集,对于不同的样本数据集中,分别使用相同的随机状态(用于伪随机数生成器的种子)来确保始终获得相同的数据点。
X, y = make_blobs(n_samples=1000, centers=3, n_features=2, cluster_std=2, random_state=seed)
其中X是二维数组,分别表示样本的横纵轴座标,y表示样本标签。
from sklearn.datasets import make_blobs
from numpy import where, random
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 150
plt.style.use('ggplot')
def samples_for_seed(seed):
X, y = make_blobs(n_samples=1000, centers=3, n_features=2, cluster_std=2, random_state=seed)
return X, y
def plot_samples(X, y, classes=3):
for i in range(classes):
# select indices of points with each class label
samples_ix = where(y == i)
# plot points for this class with a given color
plt.scatter(X[samples_ix, 0], X[samples_ix, 1])
n_problems = 2
for i in range(1, n_problems+1):
# specify subplot
plt.subplot(2,1,i)
# generate samples
X, y = samples_for_seed(i)
# scatter plot of samples
plot_samples(X, y)
plt.show()
这为转移学习提供了一个良好的基础,因为问题的每个版本都具有相似的输入数据且规模相似,尽管目标信息不同(例如,聚类中心)。
2.2 问题1的MLP模型
为问题1开发一个多层感知器模型(MLP),并将模型保存到文件中,以便以后可以重用权重。
from sklearn.datasets import make_blobs
from keras.layers import Dense
from keras.models import Sequential
from keras.optimizers import SGD
from keras.utils import to_categorical
import matplotlib.pyplot as plt
# 准备数据
def samples_for_seed(seed):
# 生成分类样本
X, y = make_blobs(n_samples=1000, centers=3, n_features=2, cluster_std=2, random_state=seed)
# one-hot编码
y = to_categorical(y)
# 划分训练集和验证集
n_train = 500
trainX, testX = X[:n_train, :], X[n_train:, :]
trainy, testy = y[:n_train], y[n_train:]
return trainX, trainy, testX, testy
def fit_model(trainX, trainy, testX, testy):
# 定义模型
model = Sequential()
model.add(Dense(5, input_dim=2, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(5, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(3, activation='softmax'))
# 编译模型
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
# 训练模型
history = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=100, verbose=0)
return model, history
def summarize_model(model, history, trainX, trainy, testX, testy):
# 评估模型
_, train_acc = model.evaluate(trainX, trainy, verbose=0)
_, test_acc = model.evaluate(testX, testy, verbose=0)
print('Train: %.3f, Test: %.3f' % (train_acc, test_acc))
# 绘制损失曲线
plt.subplot(211) # plt.subplot(2,1,1)
plt.title('Loss')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
# 绘制准确率曲线
plt.subplot(212) # plt.subplot(2,1,2)
plt.title('Accuracy')
plt.plot(history.history['accuracy'], label='train')
plt.plot(history.history['val_accuracy'], label='test')
plt.legend()
plt.tight_layout()
plt.show()
# 准备数据
trainX, trainy, testX, testy = samples_for_seed(1)
# 训练模型
model, history = fit_model(trainX, trainy, testX, testy)
# 评估模型
summarize_model(model, history, trainX, trainy, testX, testy)
# 保存模型
model.save('model.h5')
输出:
Train: 0.922, Test: 0.932
在这种情况下,可以看出该模型可以快速准确地学习到该问题,在大约40个epoch收敛,并且训练集和验证集上都很稳定。
2.3 问题2的MLP模型
首先,要了解模型在问题2上的性能基线,该基线可用于与使用转移学习的模型相比较。
首先设置新的伪随机数生成器的种子:
trainX, trainy, testX, testy = samples_for_seed(2)
其它保持不变,并且不保存模型,得到输出:
# 准备数据
trainX, trainy, testX, testy = samples_for_seed(2)
# 训练模型
model, history = fit_model(trainX, trainy, testX, testy)
# 评估模型
summarize_model(model, history, trainX, trainy, testX, testy)
输出:
Train: 0.814, Test: 0.828
2.4 问题2使用迁移学习的MLP模型
from keras.models import load_model
def fit_model_2(trainX, trainy, testX, testy):
# 加载问题1的模型
model = load_model('model.h5')
# 编译模型
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
# 重新训练模型
history = model.fit(trainX, trainy, validation_data=(testX, testy), epochs=100, verbose=0)
return model, history
trainX, trainy, testX, testy = samples_for_seed(2)
model, history = fit_model_2(trainX, trainy, testX, testy)
summarize_model(model, history, trainX, trainy, testX, testy)
输出:
Train: 0.822, Test: 0.828
在这种情况下,可以看出模型确实具有相似的学习曲线,测试集(橙色线)的学习曲线有了明显的改善,无论是在更早的性能(从第20个epoch开始)还是从更好的性能方面而言高于模型在训练集上的表现。
2.5 特征提取与权重初始化性能对比
在每个循环中,加载在问题1上训练的模型,适合问题2的训练数据集,然后在问题2的测试集上进行评估。
另外,在加载的模型中配置0、1或2个隐藏层的权重保持固定。保持0个隐藏层固定不变,意味着在学习问题2时,将迁移学习作为权重初始化方案,可以适应模型中的所有权重。而将2个隐藏层都保持固定意味着使用迁移学习作为特征提取方法在训练期间仅适应模型的输出层。
from numpy import mean, std
def fit_model_3(trainX, trainy):
model = Sequential()
model.add(Dense(5, input_dim=2, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(5, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(3, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
model.fit(trainX, trainy, epochs=100, verbose=0)
return model
# 在基本模型上重复评估
def eval_standalone_model(trainX, trainy, testX, testy, n_repeats):
scores = []
for _ in range(n_repeats):
model = fit_model_3(trainX, trainy)
_, test_acc = model.evaluate(testX, testy, verbose=0)
scores.append(test_acc)
return scores
# 加载迁移学习模型并评估
def eval_transfer_model(trainX, trainy, testX, testy, n_fixed, n_repeats):
'''
n_fixed:表示加载的模型有多少层不可训练,即权重保持不变。
'''
scores = []
for _ in range(n_repeats):
model = load_model('model.h5')
# 标记迁移学习模型的隐藏层是否可训练
for i in range(n_fixed):
model.layers[i].trainable = False
model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
model.fit(trainX, trainy, epochs=100, verbose=0)
_, test_acc = model.evaluate(testX, testy, verbose=0)
scores.append(test_acc)
return scores
trainX, trainy, testX, testy = samples_for_seed(2)
n_repeats = 10
dists, dist_labels = [], []
standalone_scores = eval_standalone_model(trainX, trainy, testX, testy, n_repeats)
print('Standalone %.3f (%.3f)' % (mean(standalone_scores), std(standalone_scores)))
dists.append(standalone_scores)
dist_labels.append('standalone')
# 设置不同数量的可训练的隐藏层,重复评估迁移学习模型的性能
n_fixed = 3
for i in range(n_fixed):
scores = eval_transfer_model(trainX, trainy, testX, testy, i, n_repeats)
print('Transfer (fixed=%d) %.3f (%.3f)' % (i, mean(scores), std(scores)))
dists.append(scores)
dist_labels.append('transfer f='+str(i))
plt.boxplot(dists, labels=dist_labels)
plt.show()
输出:
Standalone 0.769 (0.108)
Transfer (fixed=0) 0.828 (0.005)
Transfer (fixed=1) 0.819 (0.006)
Transfer (fixed=2) 0.792 (0.007)
在这种情况下,我们可以看到独立模型在问题2上达到了约77%的准确率,标准差为10%,所有迁移学习模型的标准差都较小。测试准确性分数的标准偏差之间的这种差异显示了转移学习可以为模型带来的稳定性,从而减少了通过随机学习算法引入的最终模型的性能差异。
比较模型的平均测试准确性,可以看出,将模型用作权重初始化方法(fixed = 0)的迁移学习比独立模型的性能更好,准确度率为80%;将所有隐藏层固定(fixed = 2),即用作特征提取方法会导致平均性能比独立模型差;这表明这种方法在这种情况下过于严格。当第一隐藏层保持固定(fixed = 1),得到了最佳性能,约82%的测试分类精度。这表明在这种情况下,该问题将从转移学习的特征提取和权重初始化属性中受益。在该模型中,第二个隐藏层(甚至是输出层)的权重已用随机数重新初始化。该比较证明迁移学习的特征提取特性和权重初始化特性两者都是有益的。
独立模型的箱线图显示了许多异常值,这表明该模型在平均情况下表现良好,但有可能表现非常差。相反,看到带有迁移学习的模型的表现更稳定,表现出更紧密的性能分布。
参考:
https://machinelearningmastery.com/how-to-improve-performance-with-transfer-learning-for-deep-learning-neural-networks/