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



    《从锅炉工到AI专家(6)》一文中,我们把神经网络模型降维,简单的在二维空间中介绍了过拟合和欠拟合的现象和解决方法。但是因为条件所限,在该文中我们只介绍了理论,并没有实际观察现象和应对。
    现在有了TensorFLow 2.0 / Keras的支持,可以非常容易的构建模型。我们可以方便的人工模拟过拟合的情形,实际来操作监控、调整模型,从而显著改善模型指标。

    从图中识别过拟合和欠拟合

    先借用上一篇的两组图:


    先看上边的一组图,随着训练迭代次数的增加,预测的错误率迅速下降。
    我们上一篇中讲,达到一定迭代次数之后,验证的错误率就稳定不变了。实际上你仔细观察,训练集的错误率在稳定下降,但验证集的错误率还会略有上升。两者之间的差异越来越大,图中的两条曲线,显著分离了,并且分离的趋势还在增加。这就是过拟合的典型特征。
    这表示,模型过分适应了当前的训练集数据,对于训练集数据有了较好表现。对于之外的数据,反而不适应,从而效果很差。
    这通常都是由于较小的数据样本造成的。如果数据集足够大,较多的训练通常都能让模型表现的更好。过拟合对于生产环境伤害是比较大的,因为生产中大多接收到的都是新数据,而过拟合无法对这些新数据达成较好表现。
    所以如果数据集不够的情况下,采用适当的迭代次数可能是更好的选择。这也是上一节我们采用EarlyStopping机制的原因之一。最终的表现是上边下面一组图的样子。
    欠拟合与此相反,表示模型还有较大改善空间。上面两组图中,左侧下降沿的曲线都可以认为是欠拟合。表现特征是无论测试集还是验证集,都没有足够的正确率。当然也因此,测试集和验证集表现类似,拟合非常紧密。
    欠拟合的情况,除了训练不足之外,模型不够强大或者或者模型不适合业务情况都是可能的原因。

    实验模拟过拟合

    我们使用IMDB影评样本库来做这个实验。实验程序主要部分来自于本系列第五篇中第二个例子,当然有较大的修改。
    程序主要分为几个部分:

    • 下载IMDB影评库(仅第一次),载入内存,并做单词向量化。
    • 单词向量化编码使用了multi-hot-sequences,这种编码跟one-hot类似,但一句话中有多个单词,因此会有多个'1'。一个影评就是一个0、1序列。这种编码模型非常有用,但在本例中,数据歧义会更多,更容易出现过拟合。
    • 定义baseline/small/big三个不同规模的神经网络模型,并分别编译训练,训练时保存过程数据。
    • 使用三组过程数据绘制曲线图,指标是binary_crossentropy,这是我们经常当做损失函数使用的指征,这个值在正常训练的时候收敛到越小越好。

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

    #!/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)])
    
    

    程序在命令行的输出就不贴出来了,除了输出的训练迭代过程,在之前还输出了每个模型的summary()。这里主要看最后的binary_crossentropy曲线图。

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

    优化过拟合

    优化过拟合首先要知道过拟合产生的原因,我们借用一张前一系列讲解过拟合时候用过的图,是吴恩达老师课程的笔记:

    如果一个模型产生过拟合,那这个模型的总体效果就可能是一个非常复杂的非线性方程。方程非常努力的学习所有“可见”数据,导致了复杂的权重值,使得曲线弯来弯去,变得极为复杂。多层网络更加剧了这种复杂度,最终的复杂曲线绕开了可行的区域,只对局部的可见数据有效,对于实际数据命中率低。所以从我们程序跑的结果图来看,也是越复杂的网络模型,过拟合现象反而越严重。
    这么说简单的模型就好喽?并非如此,太简单的模型往往无法表达复杂的逻辑,从而产生欠拟合。其实看看成熟的那些模型比如ResNet50,都是非常复杂的结构。
    过拟合既然产生的主要原因是在权重值上,我们在这方面做工作即可。

    增加权重的规范化

    通常有两种方法,称为L1规范化和L2规范化。前者为代价值增加一定比例的权重值的绝对值。后者增加一定比例权重值的平方值。具体的实现来源于公式,有兴趣的可以参考一下这篇文章《L1 and L2 Regularization》
    我们删除掉上面源码中的bigger模型和small模型的部分,包括模型的构建、编译和训练,添加下面的代码:

    # 构建一个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)
    

    这个模型的逻辑结构同baseline的模型完全一致,只是在前两层中增加了L2规范化的设置参数。
    先不着急运行,我们继续另外一种方法。

    添加DropOut

    DropOut是我们在上个系列中已经讲过的方法,应用的很广泛也非常有效。
    其机理非常简单,就是在一层网络中,“丢弃”一定比例的输出(设置为数值0)给下一层。丢弃的比例通常设置为0.2至0.5。这个过程只在训练过程中有效,一般会在预测过程中关闭这个机制。
    我们继续在上面代码中,添加一组采用DropOut机制的模型,模型的基本结构依然同baseline相同:

    
    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规范化(黄色曲线)和DropOut(绿色曲线)都有效的改善了模型的过拟合问题。

    (待续...)

  • 相关阅读:
    swift 中 Self 与self
    Swift 中的泛型
    mac 报文件已损坏 怎么办
    winxp秘钥
    字符串拷贝函数strcpy, strcat, sprintf, strncpy, strncat和snprintf的区别
    【原创】Linux应用程序完整调用自己写的字符设备驱动过程
    idea中新建git分支,并提交到远程github
    (JS-PHP)使用RSA算法进行加密通讯
    Linux日志相关的命令
    hibernate中关于is null的查询
  • 原文地址:https://www.cnblogs.com/andrewwang/p/10772798.html
Copyright © 2020-2023  润新知