TensorFlow从1到2(八)过拟合和欠拟合的优化

先看上面这组图,随着训练迭代次数的增加,预测错误率迅速下降。

我们在上一篇文章中说过,达到一定的迭代次数后,验证的错误率是稳定的。实际上,如果仔细观察,训练集上的错误率在稳步下降,但验证集上的错误率会略有增加。两者的差距越来越大,图中两条曲线明显分离,而且分离的趋势还在增加。这是典型的过拟合。

这意味着模型对当前训练集数据过度适应,对训练集数据有更好的表现。对于其他数据,不适合,所以效果很差。

这通常是由于数据样本较小。如果数据集足够大,更多的训练通常会带来更好的模型性能。过拟合对生产环境是比较有害的,因为生产中接收到的数据大部分是新数据,过拟合无法在这些新数据上取得更好的性能。

所以如果数据集不够,使用合适的迭代次数可能是更好的选择。这也是我们采用上一节机制的原因之一。最终的表现就是上图和下图的样子。

欠拟合则相反,说明模型还有更大的改进空间。在上面两组图中,左边下降沿的曲线可以认为是欠拟合。性能特征是测试集和验证集都没有足够的准确性。当然,也正因为如此,测试集和验证集的表现差不多,拟合非常紧密。

欠拟合,除了训练不足外,模型不够强或者模型不适合业务情况都是可能的原因。

实验模拟过拟合

我们在本实验中使用 IMDB 电影评论示例库。实验程序的主要部分来自本系列第五部分的第二个示例,当然有较大的修改。

程序主要分为几个部分:

在程序中,文本的编码方式和模型不是很合理,因为我们不想得到一个最优的模型,而是想演示过拟合的场景。

#!/usr/bin/env python3
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
NUM_WORDS = 10000
# 载入IMDB样本数据
(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)
# 将单词数字化,转化为multi-hot序列编码方式
def multi_hot_sequences(sequences, dimension):
    # 建立一个空矩阵保存结果
    results = np.zeros((len(sequences), dimension))

    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # 出现过的词设置为1.0
    return results
train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)
# 建立baseline模型,并编译训练
baseline_model = keras.Sequential([
    # 指定`input_shape`以保证下面的.summary()可以执行,
    # 否则在模型结构无法确定
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])
baseline_model.summary()
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)
# 小模型定义、编译、训练
smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

smaller_model.compile(optimizer='adam',
                      loss='binary_crossentropy',
                      metrics=['accuracy', 'binary_crossentropy'])
smaller_model.summary()
smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)
# 大模型定义、编译、训练
bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])
bigger_model.summary()
bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)
# 绘图函数
def plot_history(histories, key='binary_crossentropy'):
    plt.figure(figsize=(16,10))

    for name, history in histories:
        val = plt.plot(
            history.epoch, history.history['val_'+key],
            '--', label=name.title()+' Val')
        plt.plot(
            history.epoch, history.history[key], color=val[0].get_color(),
            label=name.title()+' Train')
    plt.xlabel('Epochs')
    plt.ylabel(key.replace('_',' ').title())
    plt.legend()
    plt.xlim([0,max(history.epoch)])
    plt.show()
# 绘制三个模型的三组曲线
plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

程序在命令行的输出不会被发布。除了输出训练迭代过程之外,每个模型的()也是之前输出的。这里主要看最终的图。

图中虚线是验证集数据的表现,实线是训练集数据的表现。三个模型的训练数据和测试数据的交叉熵曲线都表现出很大的分离,这代表了过拟合。尤其是模型的两条绿线几乎从一开始就有很大的分歧。

优化过拟合

要优化过拟合,首先要知道过拟合的原因。我们借用上一个系列中使用的图表来解释拟合。吴恩达老师课程笔记:

如果模型过拟合,模型的整体效果可能是一个非常复杂的非线性方程。方程非常努力地学习所有“可见”数据,导致复杂的权重值,使曲线曲折并变得极其复杂。多层网络加剧了这种复杂性,最终的复杂曲线绕过了可行区域,仅对局部可见数据有效,对实际数据的命中率较低。因此,从我们程序运行的结果图来看,网络模型越复杂,过拟合现象就越严重。

所以一个简单的模型就足够了吗?事实并非如此,过于简单的模型往往无法表达复杂的逻辑,从而导致欠拟合。事实上,如果你看看成熟的模型,例如,它们都是非常复杂的结构。

由于过拟合的主要原因是权重值,我们可以在这方面工作。

权重增加的归一化

通常有两种方法,称为 L1 归一化和 L2 归一化。前者在成本值上加上一定百分比的权重值的绝对值。后者将权重值的平方值增加一定百分比。具体实现来自公式。有兴趣的可以参考这篇文章《L1和L2》。

我们删除上述源码中的模型和小模型,包括模型的构建、编译和训练,添加如下代码:

# 构建一个L2规范化的模型
l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])
l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)

这个模型的逻辑结构和其他模型完全一样,只是前两层增加了L2归一化设置参数。

别急着跑,我们换个方法吧。

添加

这是我们在上一个系列中已经介绍过的一种方法,它使用非常广泛,非常有效。

机制很简单,就是在网络的某一层,一定比例的输出(设置为值0)给下一层。丢弃的比例通常设置为2 to 0.5.这个过程只在训练过程中有效,在预测过程中机制一般是关闭的。

我们继续使用上面代码中的机制添加一组模型,模型的基本结构还是一样的:


dpt_model = keras.models.Sequential([

    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])
dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])
dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)
		....
# 最后的绘图函数不变,绘图语句修改如下:
plot_history([
            ('baseline', baseline_history),
            ('l2', l2_model_history),
            ('dropout', dpt_model_history)])

现在可以执行程序了。

程序得到的曲线如下,如图 可以看出L2归一化(黄色曲线)和(绿色曲线)都可以在不降低模型复杂度的情况下有效改善模型的过拟合问题型号。

(待续……)

分类:

技术要点:

相关文章:

© 版权声明
THE END
喜欢就支持一下吧
点赞13赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容