在Keras中如何对超参数进行调优?

由于没有一个成熟的理论来解释神经网络,所以配置神经网络通常是困难的,经常被同学们调侃为“炼丹”。

对于一个给定的预测建模问题,你必须系统地尝试不同的配置然后从客观和变化的视角来审视不同配置的结果,然后尝试理解在不同的配置下分别发生了什么,从而对模型进行合理的调优。

本教程将专注于时间预测问题并讨论如何对LSTM(long-short term memory,长短期记忆,最流行的RNN网络之一)网络进行配置。

认真完成本教程后,您将掌握以下技能:

  • 如何调整训练的epoch数量并解释调整后的结果。
  • 如何调整单次训练中的batch size并解释调整后的结果。
  • 如何调整神经元的数量并解释调整后的结果。

事不宜迟,我们现在就开始吧。

摄影:[David Saddler](https://www.flickr.com/photos/80502454@N00/6585205675/),保留部分权利

教程概述

本教程可以分为以下6个部分;:

  1. 数据集准备
  2. 设计实验测试套件
  3. 调整epoch的大小
  4. 调整Batch Size的大小
  5. 调整神经元的数量
  6. 结果汇总

环境要求

本教程假设您已经安装了Python SciPy环境。Python 2或3皆可。

本教程假定您已经安装了Keras v2.0或更高版本(TensorFlow或Theano作为后端皆可)。

本教程还假设您已经安装了scikit-learn,pandas,NumPy和Matplotlib。

如果你不知道如何配置这些环境,可以参考下面的文章:

数据集准备

该数据集描述了3年期间每月洗发剂的销量。

这个数据集的基本单元是与时间对应的销量,一共有36个观测值。这个数据集是由Makridakis, Wheelwright, and Hyndman (1998)贡献。

下载并了解更多关于数据集的信息

下面的代码演示了如何导入数据集并据此绘制销量随时间的变化情况

# load and plot dataset
from pandas import read_csv
from pandas import datetime
from matplotlib import pyplot
# load dataset
def parser(x):
    return datetime.strptime('190'+x, '%Y-%m')
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# summarize first few rows
print(series.head())
# line plot
series.plot()
pyplot.show()

运行代码导入数据集,数据会被加载为Pandas Series类型并且控制台会输出前五行的数据。

Month
1901-01-01    266.0
1901-02-01    145.9
1901-03-01    183.1
1901-04-01    119.3
1901-05-01    180.3
Name: Sales, dtype: float64

绘制销量随时间变化的折线图,可以很清楚地看到销量的增长趋势。

接下来,我们来看一下实验中LSTM的配置和实验所需的测试套件。

测试套件

本节将介绍本教程中使用的测试套件。

数据分割

我们将销量数据集分为两部分:训练集和测试集。

前两年的数据作为训练集,剩余一年的数据作为测试集。

训练集将用于模型的训练,训练得到的模型将用于预测测试集中的销量。

如果以最后一个月的销量作为恒定的预测值,对测试集中的销量值预测误差为平均每月136.761的。这也是我们对模型在测试集上性能要求的下限。

模型评估

我们将使用滚动预测方案,也称为前向模型验证。

测试数据集上的时间步长每次挪动一个单位.每次挪动后模型对下一个单位时长中的销量进行预测,然后取出真实的销量同时对下一个单位时长中的销量进行预测。

这种方案模拟了真实世界当中的场景,每个月都会有新的销量数据,我们会利用过去月份的销量数据对下个月的销量进行预测。

这个过程可以通过借助训练集和测试集中的时间标记来完成,在后面我们会一次性预测出测试集中所有的销量数据。

我们将会利用测试集中所有的数据对模型的预测性能进行训练并通过误差值来评判模型的性能。在这个例子里我们使用均方根误差(RMSE),因为相比于其他损失函数它可以提供较大的罚值,从而使模型的预测值更趋近于真实的销量值。

数据准备

在我们在数据集上拟合LSTM模型之前,我们必须先对数据集格式进行转换。

下面就是我们在拟合模型进行预测前要先做的三个数据转换:

  1. 固定时间序列数据。具体到这个问题就是让销量数据严格按照时间顺序排列,下一条的数据就是下一个月的销量数据。
  2. 将时间序列信息隐含与监督学习当中,可以通过组织数据的输入输出方式来实现,在这个问题中只需将前一段时间的销量作为模型输入来预测当前月份的销量数据即可。
  3. 对销量数据进行合适的缩放。具体来说,为了让数据的输入范围与LSTM模型的激励函数输出范围相匹配,需要将销量值缩放至-1~1的范围当中。

为了让模型仍旧可以输出我们需要的销量预测值,需要对变换后的输出进行逆变换从而输出模型的原始预测值。

实验运行

每个实验场景需要重现10次以上。

这是因为即使给定了模型训练所需的超参数,LSTM模型的随机初始化也可能会引起模型训练结果间的巨大差异。

通过审视不同模型超参数下模型性能随迭代次数(epochs)的变化曲线,我们可以得到一些可能提升模型性能的超参数调整区间或方向。

在每个批次的数据集训练结束后,测试集和训练集上的得分(即均方根误差)会打印输出出来。

每个批次结束后打印输出性能评估指标可以帮助我们更好地了解到模型的现状,比如说是否发生了过拟合。

训练集和测试集上的RMSE损失值曲线在运行结束之后通过折线图展现,我们设定测试集曲线为蓝色,测试集为橙色。

下面让我们编写代码,然后对模型的结果进行分析。

调整epochs的大小

我们调整的第一个模型超参数是epochs。

为了保持其他超参数的一致,我们固定神经元数量为1,Batch Size为4。下面我们通过调整epochs来观察模型性能参数的变化。

epochs = 500

下面给出了我们实验使用的代码清单。

代码是最好的注释,同时代码也易于复用和变更,这份代码是我们下面所有的实验的基础代码,我们只需要在上面稍作变更就可以完成后续的所有实验。

from pandas import DataFrame
from pandas import Series
from pandas import concat
from pandas import read_csv
from pandas import datetime
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from math import sqrt
import matplotlib
# be able to save images on serve
matplotlib.use('Agg')
from matplotlib import pyplot
import numpy
 
# date-time parsing function for loading the dataset
def parser(x):
    return datetime.strptime('190'+x, '%Y-%m')
 
# frame a sequence as a supervised learning problem
def timeseries_to_supervised(data, lag=1):
    df = DataFrame(data)
    columns = [df.shift(i) for i in range(1, lag+1)]
    columns.append(df)
    df = concat(columns, axis=1)
    df = df.drop(0)
    return df
 
# create a differenced series
def difference(dataset, interval=1):
    diff = list()
    for i in range(interval, len(dataset)):
        value = dataset[i] - dataset[i - interval]
        diff.append(value)
    return Series(diff)
 
# scale train and test data to [-1, 1]
def scale(train, test):
    # fit scale
    scaler = MinMaxScaler(feature_range=(-1, 1))
    scaler = scaler.fit(train)
    # transform train
    train = train.reshape(train.shape[0], train.shape[1])
    train_scaled = scaler.transform(train)
    # transform test
    test = test.reshape(test.shape[0], test.shape[1])
    test_scaled = scaler.transform(test)
    return scaler, train_scaled, test_scaled
 
# inverse scaling for a forecasted value
def invert_scale(scaler, X, yhat):
    new_row = [x for x in X] + [yhat]
    array = numpy.array(new_row)
    array = array.reshape(1, len(array))
    inverted = scaler.inverse_transform(array)
    return inverted[0, -1]
 
# evaluate the model on a dataset, returns RMSE in transformed units
def evaluate(model, raw_data, scaled_dataset, scaler, offset, batch_size):
    # separate
    X, y = scaled_dataset[:,0:-1], scaled_dataset[:,-1]
    # reshape
    reshaped = X.reshape(len(X), 1, 1)
    # forecast dataset
    output = model.predict(reshaped, batch_size=batch_size)
    # invert data transforms on forecast
    predictions = list()
    for i in range(len(output)):
        yhat = output[i,0]
        # invert scaling
        yhat = invert_scale(scaler, X[i], yhat)
        # invert differencing
        yhat = yhat + raw_data[i]
        # store forecast
        predictions.append(yhat)
    # report performance
    rmse = sqrt(mean_squared_error(raw_data[1:], predictions))
    return rmse
 
# fit an LSTM network to training data
def fit_lstm(train, test, raw, scaler, batch_size, nb_epoch, neurons):
    X, y = train[:, 0:-1], train[:, -1]
    X = X.reshape(X.shape[0], 1, X.shape[1])
    # prepare model
    model = Sequential()
    model.add(LSTM(neurons, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
    model.add(Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
    # fit model
    train_rmse, test_rmse = list(), list()
    for i in range(nb_epoch):
        model.fit(X, y, epochs=1, batch_size=batch_size, verbose=0, shuffle=False)
        model.reset_states()
        # evaluate model on train data
        raw_train = raw[-(len(train)+len(test)+1):-len(test)]
        train_rmse.append(evaluate(model, raw_train, train, scaler, 0, batch_size))
        model.reset_states()
        # evaluate model on test data
        raw_test = raw[-(len(test)+1):]
        test_rmse.append(evaluate(model, raw_test, test, scaler, 0, batch_size))
        model.reset_states()
    history = DataFrame()
    history['train'], history['test'] = train_rmse, test_rmse
    return history
 
# run diagnostic experiments
def run():
    # load dataset
    series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
    # transform data to be stationary
    raw_values = series.values
    diff_values = difference(raw_values, 1)
    # transform data to be supervised learning
    supervised = timeseries_to_supervised(diff_values, 1)
    supervised_values = supervised.values
    # split data into train and test-sets
    train, test = supervised_values[0:-12], supervised_values[-12:]
    # transform the scale of the data
    scaler, train_scaled, test_scaled = scale(train, test)
    # fit and evaluate model
    train_trimmed = train_scaled[2:, :]
    # config
    repeats = 10
    n_batch = 4
    n_epochs = 500
    n_neurons = 1
    # run diagnostic tests
    for i in range(repeats):
        history = fit_lstm(train_trimmed, test_scaled, raw_values, scaler, n_batch, n_epochs, n_neurons)
        pyplot.plot(history['train'], color='blue')
        pyplot.plot(history['test'], color='orange')
        print('%d) TrainRMSE=%f, TestRMSE=%f' % (i, history['train'].iloc[-1], history['test'].iloc[-1]))
    pyplot.savefig('epochs_diagnostic.png')
 
# entry point
run()

运行实验代码。稍等片刻即可看到十次实验结束时训练集和测试集上的均方根误差。

0) TrainRMSE=63.495594, TestRMSE=113.472643
1) TrainRMSE=60.446307, TestRMSE=100.147470
2) TrainRMSE=59.879681, TestRMSE=95.112331
3) TrainRMSE=66.115269, TestRMSE=106.444401
4) TrainRMSE=61.878702, TestRMSE=86.572920
5) TrainRMSE=73.519382, TestRMSE=103.551694
6) TrainRMSE=64.407033, TestRMSE=98.849227
7) TrainRMSE=72.684834, TestRMSE=98.499976
8) TrainRMSE=77.593773, TestRMSE=124.404747
9) TrainRMSE=71.749335, TestRMSE=126.396615

运行结束后,在代码文件所在目录下会保存十次实验中测试集和训练集上的均方根误差随训练批次变化的曲线图。

epochs=500时的结果

从结果可以清楚地看到,几乎所有的实验中均方根误差都是呈下降趋势的。

这是一个令我们满意的标志,这个趋势意味着模型确实从训练集中学习得到了预测能力。事实上从曲线上我们也看到了在epochs=500时最终的预测误差RSME均低于136.761。

这个趋势也告诉了我们,继续增大epochs的值还可以使模型的性能进一步提升。

下面我们就将模型的epochs提升至1000。

1000个时代的诊断

要实现将epochs提升至1000只需要改变代码中的epochs设定值即可。

具体而言,就是将n_epochs参数设置为1000 再执行run()函数。

n_epochs = 1000

运行代码后可以看到与下面相似的输出结果:

0) TrainRMSE=69.242394, TestRMSE=90.832025
1) TrainRMSE=65.445810, TestRMSE=113.013681
2) TrainRMSE=57.949335, TestRMSE=103.727228
3) TrainRMSE=61.808586, TestRMSE=89.071392
4) TrainRMSE=68.127167, TestRMSE=88.122807
5) TrainRMSE=61.030678, TestRMSE=93.526607
6) TrainRMSE=61.144466, TestRMSE=97.963895
7) TrainRMSE=59.922150, TestRMSE=94.291120
8) TrainRMSE=60.170052, TestRMSE=90.076229
9) TrainRMSE=62.232470, TestRMSE=98.174839

同样的,训练结束后可以得到与下相似的曲线图。

epochs=1000的结果

从曲线中可以看出虽然误差在进一步减小,但是减小的趋势已经越来越不明显了。

虽然训练集和测试集的误差曲线变得越来越平坦,但总体上还是下降趋势,不过在最糟糕的一条测试曲线中我们观察到了测试误差在随训练批次的增大而增大。

不过这种曲线只是个例,在大多数的实验中继续增加epochs还是值得的。

这次让我们把epochs从1000提升至2000。

epochs=2000

和之前一样,将 run() 函数中的 n_epochs 参数设置为2000再次运行即可 。

n_epochs = 2000

运行示例可以看到每次实验中测试集和训练集最终的RMSE误差。

0) TrainRMSE=67.292970, TestRMSE=83.096856
1) TrainRMSE=55.098951, TestRMSE=104.211509
2) TrainRMSE=69.237206, TestRMSE=117.392007
3) TrainRMSE=61.319941, TestRMSE=115.868142
4) TrainRMSE=60.147575, TestRMSE=87.793270
5) TrainRMSE=59.424241, TestRMSE=99.000790
6) TrainRMSE=66.990082, TestRMSE=80.490660
7) TrainRMSE=56.467012, TestRMSE=97.799062
8) TrainRMSE=60.386380, TestRMSE=103.810569
9) TrainRMSE=58.250862, TestRMSE=86.212094

下面是最终得到的曲线图。

epochs=2000

正如上面所说,还有很多的模型在epochs=1000之后仍然保持下降趋势。

不过epochs=2000时,大约只有一半的测试样例还能保持下降趋势了,其他的误差都开始上升。

增长的趋势是过拟合的迹象。过拟合即对训练集的过度拟合致使在训练集上产生了反作用,表现为模型性能的下降。从测试集上误差曲线的继续下降和测试集上误差曲线从下降到上升的现象也可以印证这一点。一少半的测试样例已经满足了这一要求。

尽管如此,在测试集上的最终性能仍然很好,也许进一步加大epochs还可以获得更大的提升,我们不妨再调大epochs一次。

这次仍然增大一倍,令epochs=4000。

epochs=4000

run() 函数中的 n_epochs 参数设置为4000 。

n_epochs = 4000

再次运行代码观察到输出如下。

0) TrainRMSE=58.889277, TestRMSE=99.121765
1) TrainRMSE=56.839065, TestRMSE=95.144846
2) TrainRMSE=58.522271, TestRMSE=87.671309
3) TrainRMSE=53.873962, TestRMSE=113.920076
4) TrainRMSE=66.386299, TestRMSE=77.523432
5) TrainRMSE=58.996230, TestRMSE=136.367014
6) TrainRMSE=55.725800, TestRMSE=113.206607
7) TrainRMSE=57.334604, TestRMSE=90.814642
8) TrainRMSE=54.593069, TestRMSE=105.724825
9) TrainRMSE=56.678498, TestRMSE=83.082262

运行结束后看到如下的损失曲线图。

epochs=4000

变化趋势和epochs=2000时相似。

看到即使epochs提到4000,性能也仍然有着提升的趋势,不过这次出现了一个很严重的过拟合情况。

大多数的样例再次以良好(能够满足我们设置的下限)的测试结果结束。

结果汇总

从上面参数的动态调整中我们更好地理解了模型随参数的动态变化,但是我们还没有将结果做客观和严谨的比较。

我们可以通过重复实验和列表总结来完成这一步,这里我完成了epochs=500,1000,2000,4000,6000时各30次的运行。

这个方法是通过大量运行相应的配置并进行统计分析,从而更准确地判断哪些配置更优的。

完整的代码示例如下所示。

from pandas import DataFrame
from pandas import Series
from pandas import concat
from pandas import read_csv
from pandas import datetime
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from math import sqrt
import matplotlib
# be able to save images on serve
matplotlib.use('Agg')
from matplotlib import pyplot
import numpy
 
# date-time parsing function for loading the dataset
def parser(x):
    return datetime.strptime('190'+x, '%Y-%m')
 
# frame a sequence as a supervised learning problem
def timeseries_to_supervised(data, lag=1):
    df = DataFrame(data)
    columns = [df.shift(i) for i in range(1, lag+1)]
    columns.append(df)
    df = concat(columns, axis=1)
    df = df.drop(0)
    return df
 
# create a differenced series
def difference(dataset, interval=1):
    diff = list()
    for i in range(interval, len(dataset)):
        value = dataset[i] - dataset[i - interval]
        diff.append(value)
    return Series(diff)
 
# invert differenced value
def inverse_difference(history, yhat, interval=1):
    return yhat + history[-interval]
 
# scale train and test data to [-1, 1]
def scale(train, test):
    # fit scale
    scaler = MinMaxScaler(feature_range=(-1, 1))
    scaler = scaler.fit(train)
    # transform train
    train = train.reshape(train.shape[0], train.shape[1])
    train_scaled = scaler.transform(train)
    # transform test
    test = test.reshape(test.shape[0], test.shape[1])
    test_scaled = scaler.transform(test)
    return scaler, train_scaled, test_scaled
 
# inverse scaling for a forecasted value
def invert_scale(scaler, X, yhat):
    new_row = [x for x in X] + [yhat]
    array = numpy.array(new_row)
    array = array.reshape(1, len(array))
    inverted = scaler.inverse_transform(array)
    return inverted[0, -1]
 
# fit an LSTM network to training data
def fit_lstm(train, batch_size, nb_epoch, neurons):
    X, y = train[:, 0:-1], train[:, -1]
    X = X.reshape(X.shape[0], 1, X.shape[1])
    model = Sequential()
    model.add(LSTM(neurons, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
    model.add(Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
    for i in range(nb_epoch):
        model.fit(X, y, epochs=1, batch_size=batch_size, verbose=0, shuffle=False)
        model.reset_states()
    return model
 
# run a repeated experiment
def experiment(repeats, series, epochs):
    # transform data to be stationary
    raw_values = series.values
    diff_values = difference(raw_values, 1)
    # transform data to be supervised learning
    supervised = timeseries_to_supervised(diff_values, 1)
    supervised_values = supervised.values
    # split data into train and test-sets
    train, test = supervised_values[0:-12], supervised_values[-12:]
    # transform the scale of the data
    scaler, train_scaled, test_scaled = scale(train, test)
    # run experiment
    error_scores = list()
    for r in range(repeats):
        # fit the model
        batch_size = 4
        train_trimmed = train_scaled[2:, :]
        lstm_model = fit_lstm(train_trimmed, batch_size, epochs, 1)
        # forecast the entire training dataset to build up state for forecasting
        train_reshaped = train_trimmed[:, 0].reshape(len(train_trimmed), 1, 1)
        lstm_model.predict(train_reshaped, batch_size=batch_size)
        # forecast test dataset
        test_reshaped = test_scaled[:,0:-1]
        test_reshaped = test_reshaped.reshape(len(test_reshaped), 1, 1)
        output = lstm_model.predict(test_reshaped, batch_size=batch_size)
        predictions = list()
        for i in range(len(output)):
            yhat = output[i,0]
            X = test_scaled[i, 0:-1]
            # invert scaling
            yhat = invert_scale(scaler, X, yhat)
            # invert differencing
            yhat = inverse_difference(raw_values, yhat, len(test_scaled)+1-i)
            # store forecast
            predictions.append(yhat)
        # report performance
        rmse = sqrt(mean_squared_error(raw_values[-12:], predictions))
        print('%d) Test RMSE: %.3f' % (r+1, rmse))
        error_scores.append(rmse)
    return error_scores
 
 
# load dataset
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# experiment
repeats = 30
results = DataFrame()
# vary training epochs
epochs = [500, 1000, 2000, 4000, 6000]
for e in epochs:
    results[str(e)] = experiment(repeats, series, e)
# summarize results
print(results.describe())
# save boxplot
results.boxplot()
pyplot.savefig('boxplot_epochs.png')

运行代码,可以看到这五个配置下的结果概要信息。需要注意的是,这里不仅有平均的RMSE值,还有标准偏差值。

平均值代表了当前配置下的平均预期性能,而标准差代表着当前配置下测试模型间的性能偏差大小。最大值和最小值界定了模型性能预期出现在的范围。

如果只看平均的RMSE值,epochs=1000时是最好的,如果要得到更具体的值则需要在1000-2000的范围内进一步细分。

              500        1000        2000        4000        6000
count   30.000000   30.000000   30.000000   30.000000   30.000000
mean   109.439203  104.566259  107.882390  116.339792  127.618305
std     14.874031   19.097098   22.083335   21.590424   24.866763
min     87.747708   81.621783   75.327883   77.399968   90.512409
25%     96.484568   87.686776   86.753694  102.127451  105.861881
50%    110.891939   98.942264  116.264027  121.898248  125.273050
75%    121.067498  119.248849  125.518589  130.107772  150.832313
max    138.879278  139.928055  146.840997  157.026562  166.111151

从箱形图也可以看出这种分布而且比较起来更为直观。

在我们的箱形图中,绿线代表中位数,上下边代表的是性能中前25%和75%的分界线,黑线代表最优值和最差值。从箱形图我们可以看出,如果从测试角度来看,epochs设置为1000更合理,但是如果想获得最佳的性能,那么可能需要牺牲平均性能选取2000附近的epochs值进行重复。

用于总结和比较的箱形图

接下来,我们看看Batch Size对模型的影响。

调整Batch Size的大小

Batch Size的大小决定着网络权重的更新频率。

注意:在Keras中,Batch Size也是会影响训练集和测试集大小的。

在上一节中我们探究epochs对模型训练的影响时将Batch Size固定为4,此时测试数据集数量不受影响,仍然为12,但是训练数据集的前四个数据的利用率是低于后面的,因此只能划分出20个训练样本。

根据上一节的结论,我们在这一节中将epochs固定为1000(忽略参数之间的相互影响)。

epochs=1000,Batch Size=4

这个配置在上一节的部分已经使用过了,此处不再重复赘述,只做简单总结。

结果显示直到训练的最后,大多数模型的错误率都还是呈下降趋势的。

Batch Size=4

Batch Size=2

现在我们将Batch Size做减半处理。

修改 run() 函数中的 n_batch 参数即可:

n_batch = 2

运行代码,从输出结果和曲线图对比看出模型训练过程中的趋势大致相同,但是从趋势上看可能Batch Size=2时可以达到更低的RMSE值。

在Batch Size=2时,与Batch Size=4相比模型的下降趋势并没有那么明显,更加趋于稳定。

下面列出了运行过程中输出的每次重复得到的训练集和测试集上的RMSE值。

0) TrainRMSE=63.510219, TestRMSE=115.855819
1) TrainRMSE=58.336003, TestRMSE=97.954374
2) TrainRMSE=69.163685, TestRMSE=96.721446
3) TrainRMSE=65.201764, TestRMSE=110.104828
4) TrainRMSE=62.146057, TestRMSE=112.153553
5) TrainRMSE=58.253952, TestRMSE=98.442715
6) TrainRMSE=67.306530, TestRMSE=108.132021
7) TrainRMSE=63.545292, TestRMSE=102.821356
8) TrainRMSE=61.693847, TestRMSE=99.859398
9) TrainRMSE=58.348250, TestRMSE=99.682159

训练和测试集上的RMSE值变化曲线

Batch Size=2

让我们进一步缩小Batch Size。

Batch Size=1

Batch Size=1的情况在术语上又称作在线学习(Online Learning)

Batch Size=1时,每个样本训练完后就会更新权值,而批量学习(Batch Learning)不同,只有在每个批次的数据训练结束后才会更新网络权值。

run() 函数中更改 n_batch 参数:

n_batch = 1

再次运行函数,观察控制台的输出:

0) TrainRMSE=60.349798, TestRMSE=100.182293
1) TrainRMSE=62.624106, TestRMSE=95.716070
2) TrainRMSE=64.091859, TestRMSE=98.598958
3) TrainRMSE=59.929993, TestRMSE=96.139427
4) TrainRMSE=59.890593, TestRMSE=94.173619
5) TrainRMSE=55.944968, TestRMSE=106.644275
6) TrainRMSE=60.570245, TestRMSE=99.981562
7) TrainRMSE=56.704995, TestRMSE=111.404182
8) TrainRMSE=59.909065, TestRMSE=90.238473
9) TrainRMSE=60.863807, TestRMSE=105.331214

最后得到RSME的损失值变化曲线图。

从曲线变化中可以看出与之前相比RMSE的下降速度更快,在epochs=1000时显得更加稳定。考虑到训练集中很小的变化都可能导致网络权值的较大变化,测试集中RMSE的变化范围可能会更大。

从这个图像中还可以看出如果增大epoches,模型很可能可以达到更优的性能。

Batch Size=1

结果汇总

和探究epoches时一样,我们需要通过统计的方法客观严谨地分析Batch Size的不同大小对网络性能的影响。

同样,将每个超参数配置重复实验30次,将结果以表格和箱形图的方式展示。下面给出需要改动的代码。

...
 
# run a repeated experiment
def experiment(repeats, series, batch_size):
    # transform data to be stationary
    raw_values = series.values
    diff_values = difference(raw_values, 1)
    # transform data to be supervised learning
    supervised = timeseries_to_supervised(diff_values, 1)
    supervised_values = supervised.values
    # split data into train and test-sets
    train, test = supervised_values[0:-12], supervised_values[-12:]
    # transform the scale of the data
    scaler, train_scaled, test_scaled = scale(train, test)
    # run experiment
    error_scores = list()
    for r in range(repeats):
        # fit the model
        train_trimmed = train_scaled[2:, :]
        lstm_model = fit_lstm(train_trimmed, batch_size, 1000, 1)
        # forecast the entire training dataset to build up state for forecasting
        train_reshaped = train_trimmed[:, 0].reshape(len(train_trimmed), 1, 1)
        lstm_model.predict(train_reshaped, batch_size=batch_size)
        # forecast test dataset
        test_reshaped = test_scaled[:,0:-1]
        test_reshaped = test_reshaped.reshape(len(test_reshaped), 1, 1)
        output = lstm_model.predict(test_reshaped, batch_size=batch_size)
        predictions = list()
        for i in range(len(output)):
            yhat = output[i,0]
            X = test_scaled[i, 0:-1]
            # invert scaling
            yhat = invert_scale(scaler, X, yhat)
            # invert differencing
            yhat = inverse_difference(raw_values, yhat, len(test_scaled)+1-i)
            # store forecast
            predictions.append(yhat)
        # report performance
        rmse = sqrt(mean_squared_error(raw_values[-12:], predictions))
        print('%d) Test RMSE: %.3f' % (r+1, rmse))
        error_scores.append(rmse)
    return error_scores
 
 
# load dataset
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# experiment
repeats = 30
results = DataFrame()
# vary training batches
batches = [1, 2, 4]
for b in batches:
    results[str(b)] = experiment(repeats, series, b)
# summarize results
print(results.describe())
# save boxplot
results.boxplot()
pyplot.savefig('boxplot_batches.png')

如果单独从平均的RSME损失来看,Batch Size=1时的RSME值最小,如果再考虑到上一节的结论,通过调整epochs可以进一步提高模型的性能。

                1           2           4
count   30.000000   30.000000   30.000000
mean    98.697017  102.642594  100.320203
std     12.227885    9.144163   15.957767
min     85.172215   85.072441   83.636365
25%     92.023175   96.834628   87.671461
50%     95.981688  101.139527   91.628144
75%    102.009268  110.171802  114.660192
max    147.688818  120.038036  135.290829

箱形图中各个边界与标注线与之前一致。可以看出Batch Size时模型的差异性最大,同时RSME的中间水平(不是平均值)也更优。

对神经网络的调优实质上就是模型平均性能和性能稳定性(指重复训练得到的模型间的偏差)的折衷,最理想的结果是得到一个平均误差小同时稳定性又强的模型,这意味着模型是良好且易于重复的。

探究Batch Size得到的箱形图

调整神经元的数量

在本节,我们将探究网络中神经元数量对网络的影响。

神经元的数量与网络的学习能力直接相关。通常来说更多的神经元可以从问题中习得更多的结构,不过这伴随着更高的时间成本和过拟合的潜在风险。

我们固定Batch Size为4,epochs为1000。

神经元数量为1

我们从1个神经元开始。

这个配置在前两个实验都已经重复过了,不再赘述,直接展示结果。

神经元数量为1

神经元数量为2

我们将神经元的数量从1调整至2,一般来说这会提高网络的学习能力。

我们需要改变 run() 函数中的 n_neurons 变量来完成新的实验。

n_neurons = 2

运行代码,打印得到每次重复实验得到的RMSE损失值。

从结果来看,网络的学习结果满足了我们的最低要求,但是并没有我们想象中那么大的提升,表现一般。

0) TrainRMSE=59.466223, TestRMSE=95.554547
1) TrainRMSE=58.752515, TestRMSE=101.908449
2) TrainRMSE=58.061139, TestRMSE=86.589039
3) TrainRMSE=55.883708, TestRMSE=94.747927
4) TrainRMSE=58.700290, TestRMSE=86.393213
5) TrainRMSE=60.564511, TestRMSE=101.956549
6) TrainRMSE=63.160916, TestRMSE=98.925108
7) TrainRMSE=60.148595, TestRMSE=95.082825
8) TrainRMSE=63.029242, TestRMSE=89.285092
9) TrainRMSE=57.794717, TestRMSE=91.425071

再看一下我们得到的RMSE损失折线图。

这张图相比打印数据更具有说服力,从图中可以看到在epochs为500-750时,除了一条异常曲线以外,其他模型在测试集上的RMSE曲线也都出现了上升的拐点。与此同时,所有模型在训练集上的损失值都有值持续的下降趋势。

这很明显就是模型在训练集上过拟合的体现。

神经元数量为2

让我们继续增加神经元的数量,看模型会不会更快地达到过拟合。

神经元数量为3

将神经元的数量增加至3

run() 函数中设置 n_neurons 变量为3。

n_neurons = 3

运行代码,控制台会输出每次运行最后得到的RMSE损失值。

结果与将神经元数量调整至2时大致相似,我们没有看到神经元数量为2或3有着什么大的差异。而且以最后的性能来说,3个神经元的网络性能似乎因为更快达到过拟合而显得更差一点。

从损失值的变化曲线可以看出模型在训练数据集上更快地达到了上面所说的拐点,大概在epochs位于300-400时。

如果此时减小学习率,增加神经元数量可以减小过拟合的速度,从而提供更丰富的模型。除此以外也可以使用一些其他的正则化方法比如说增加Dropout层,减小Batch Size,减小epochs等。

0) TrainRMSE=55.686242, TestRMSE=90.955555
1) TrainRMSE=55.198617, TestRMSE=124.989622
2) TrainRMSE=55.767668, TestRMSE=104.751183
3) TrainRMSE=60.716046, TestRMSE=93.566307
4) TrainRMSE=57.703663, TestRMSE=110.813226
5) TrainRMSE=56.874231, TestRMSE=98.588524
6) TrainRMSE=57.206756, TestRMSE=94.386134
7) TrainRMSE=55.770377, TestRMSE=124.949862
8) TrainRMSE=56.876467, TestRMSE=95.059656
9) TrainRMSE=57.067810, TestRMSE=94.123620

测试集与训练集上的RMSE曲线图

神经元数量为3

结果汇总

现在,让我们来通过统计的方法对比观察网络其他超参数固定的情况下,神经元数量的增加对网络的影响。

还是和上面一样每个配置重复30次,神经元数量为1-5,下面给出重复实验需要更改的函数和代码。

...
 
# run a repeated experiment
def experiment(repeats, series, neurons):
    # transform data to be stationary
    raw_values = series.values
    diff_values = difference(raw_values, 1)
    # transform data to be supervised learning
    supervised = timeseries_to_supervised(diff_values, 1)
    supervised_values = supervised.values
    # split data into train and test-sets
    train, test = supervised_values[0:-12], supervised_values[-12:]
    # transform the scale of the data
    scaler, train_scaled, test_scaled = scale(train, test)
    # run experiment
    error_scores = list()
    for r in range(repeats):
        # fit the model
        batch_size = 4
        train_trimmed = train_scaled[2:, :]
        lstm_model = fit_lstm(train_trimmed, batch_size, 1000, neurons)
        # forecast the entire training dataset to build up state for forecasting
        train_reshaped = train_trimmed[:, 0].reshape(len(train_trimmed), 1, 1)
        lstm_model.predict(train_reshaped, batch_size=batch_size)
        # forecast test dataset
        test_reshaped = test_scaled[:,0:-1]
        test_reshaped = test_reshaped.reshape(len(test_reshaped), 1, 1)
        output = lstm_model.predict(test_reshaped, batch_size=batch_size)
        predictions = list()
        for i in range(len(output)):
            yhat = output[i,0]
            X = test_scaled[i, 0:-1]
            # invert scaling
            yhat = invert_scale(scaler, X, yhat)
            # invert differencing
            yhat = inverse_difference(raw_values, yhat, len(test_scaled)+1-i)
            # store forecast
            predictions.append(yhat)
        # report performance
        rmse = sqrt(mean_squared_error(raw_values[-12:], predictions))
        print('%d) Test RMSE: %.3f' % (r+1, rmse))
        error_scores.append(rmse)
    return error_scores
 
 
# load dataset
series = read_csv('shampoo-sales.csv', header=0, parse_dates=[0], index_col=0, squeeze=True, date_parser=parser)
# experiment
repeats = 30
results = DataFrame()
# vary neurons
neurons = [1, 2, 3, 4, 5]
for n in neurons:
    results[str(n)] = experiment(repeats, series, n)
# summarize results
print(results.describe())
# save boxplot
results.boxplot()
pyplot.savefig('boxplot_neurons.png') 

运行代码过程中会打印每个配置中测试集上的损失值。

单从性能上看,具有一个神经元的网络在epochs=1000,Batch Size=4时具有最佳的性能,同时模型训练的稳定性也最好。

                1           2           3           4           5
count   30.000000   30.000000   30.000000   30.000000   30.000000
mean    98.344696  103.268147  102.726894  112.453766  122.843032
std     13.538599   14.720989   12.905631   16.296657   25.586013
min     81.764721   87.731385   77.545899   85.632492   85.955093
25%     88.524334   94.040807   95.152752  102.477366  104.192588
50%     93.543948  100.330678  103.622600  110.906970  117.022724
75%    102.944050  105.087384  110.235754  118.653850  133.343669
max    132.934054  152.588092  130.551521  162.889845  184.678185

从箱形图可以看到神经元数量的增加导致了损失值的增加,模型性能的下降。

探究神经元数量影响的汇总箱形图

所有实验的汇总分析

在本教程中,我们在Shampoo Sales数据集上完成了一系列LSTM实验。

对于本文中的数据集来说,一个神经元,Batch Size为4,epoches为1000对于LSTM来说是一个很好的超参数配置。

除此之外,如果希望获得更好的模型,Batch Size设为1,使用更大的epoches也值得一试。

从本文的描述也可以看出神经网络超参数的调整是一项困难的经验性工作,LSTM网络自然也不例外。

本教程也印证了开头所说的以动态和客观的角度来审视模型的工作情况对于我们的调参是大有裨益的。

当然,除了本文的话题之外,还有许多有趣又有意义的工作和研究,下一节我也列出了一些来供读者参考。

扩展内容

本节列出了一些可以基于本教程继续探究的内容和想法。

如果你尝试了其中的任一个,欢迎在评论中分享你的结果~

  • 添加Dropout。这是减慢机器学习速度更是避免过拟合的一大利器。
  • 使用层级的神经网络。通过多层的神经网络可以为模型带来分级分层学习的能力。
  • 正则化。可以通过权重正则化(如L1和L2)来减缓模型的学习同时降低模型的复杂度,防止过拟合。
  • 优化算法。探索使用Keras提供的其他优化器,如经典的梯度下降,看看在其他算法下模型参数对模型训练和过拟合的速度有怎样的影响。
  • 损失函数。尝试使用Keras其他可用的损失函数,探究选用其他的损失函数是否可以提升模型的性能。
  • 特征与时间步长。你可以尝试其他的组合方式或者时间步长,比如说你可以跳过上个月的数据等的。
  • 更大的Batch Size。使用更大的Batch Size意味着模型在训练集和测试集上的数据操作规模更大了,看看这会带来什么影响。

总结

通过本教程,你应当可以了解到在时间序列预测问题中,如何系统地对LSTM网络的参数进行探究并调优。

具体来说,通过本文我希望你可以掌握以下技能:

如何设计评估模型配置的系统测试套件。

如何利用模型的性能评估指标以及指标随epochs的变化曲线对模型的行为进行分析。

如何探究和解释epoches,Batch Size和神经元数量对模型的影响。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
在Keras中如何对超参数进行调优?
由于没有一个成熟的理论来解释神经网络,所以配置神经网络通常是困难的,经常被同学们调侃为“炼丹”。
<<上一篇
下一篇>>