Keras实现MLP

320 阅读20分钟

Keras实现MLP

Keras 是一个深度学习高级 API,可以用它轻松地搭建、训练、评估和运行各种神经网络。为了进行神经网络计算,必须要有计算后端的支持。目前可选三个流行库:TensorFlow、CNTK 和 Theano。TensorFlow 也捆绑了自身的 Keras 实现 —— tf.keras,它只支持 TensorFlow 作为后端,但提供了更多使用的功能。

img

使用GPU加速

限制使用的gpu,不限制消耗内存的大小

通过tf.config.experimental.set_visible_devices可以设置当前程序可见的设备范围(当前程序只会使用自己可见的设备,不可见的设备不会被当前程序使用。使用部分gpu加速。如下面使用gpu设备0

import tensorflow as tf
gpus = tf.config.experimental.list_physical_devices(device_type='GPU')
tf.config.experimental.set_visible_devices(devices=gpus[0], device_type='GPU')
print(gpus)
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]

使用顺序 API 创建图片分类器

首先加载数据集。使用Keras自带的Fashion MNIST,它是 MNIST 一个替代品,格式与 MNIST 完全相同(70000 张灰度图,每张的像素是28 × 28,共有 10 类),图的内容是流行物品,而不是数字,每类中的图片更丰富,识图的挑战性比 MNIST 高得多。例如,线性模型可以在 MNIST 上达到 92% 的准确率,但在 Fashion MNIST 上只有 83% 的准确率。

使用 Keras 加载数据集

Keras 提供一些实用的函数用来获取和加载常见的数据集,包括 MNIST、Fashion MNIST

加载 Fashion MNIST:

import tensorflow.keras as keras
fashion_mnist = keras.datasets.fashion_mnist
(X_train_full, y_train_full), (X_test, y_test) = fashion_mnist.load_data() 

当使用 Keras 加载 MNIST 或 Fashion MNIST 时,和 Scikit-Learn 加载数据的一个重要区别是,每张图片是28 × 28的数组,而不是大小是 784 的一维数组。另外像素是用整数(0 到 255)表示的,而不是浮点数(0.0 到 255.0)

X_train_full.shape
(60000, 28, 28)

首次执行会下载数据集,该数据集已经分成了训练集和测试集,但没有验证集。所以要建一个验证集,另外,因为要用梯度下降训练神经网络,必须要对输入特征进行缩放。简单起见,通过除以 255.0 将强度范围变为 0-1:

X_valid, X_train = X_train_full[:5000] / 255.0, X_train_full[5000:] / 255.0
y_valid, y_train = y_train_full[:5000], y_train_full[5000:] 

对于 MNIST,当标签等于 5 时,表明图片是手写的数字 5。但对于 Fashion MNIST,需要分类名的列表,例如,训练集的第一张图片表示外套

class_names = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat",
               "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"] 
class_names[y_train[0]]
'Coat'

用顺序 API 创建模型

搭建一个拥有两个隐含层的分类 MLP:

model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation="relu"))
model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))

逐行看下代码:

  • 第一行代码创建了一个顺序 模型,这是 Keras 最简单的模型,是由单层神经元顺序连起来的,被称为顺序 API;
  • 接下来创建了第一层,输入层,这是一个Flatten层,它的作用是将每个输入图片转变为 1D 数组:如果输入数据是X,该层则计算X.reshape(-1, 1)。该层没有任何参数,只是做一些简单预处理。因为是模型的第一层,必须要指明input_shapeinput_shape不包括批次大小,只是实例的形状。另外,第一层也可以是keras.layers.InputLayer,设置input_shape=[28,28];
  • 然后,添加了一个有 300 个神经元的紧密层,激活函数是 ReLU。每个紧密层只负责自身的权重矩阵,权重矩阵是神经元与输入的所有连接权重。紧密层还要负责偏置项(每个神经元都有一个偏置项)向量。当紧密层收到输入数据时,就利用公式进行计算;
  • 接着再添加第二个紧密层,激活函数仍然是 ReLU;
  • 最后,加上一个拥有 10 个神经元的输出层(每有一个类就要有一个神经元),激活函数是 softmax(保证输出的概率和等于 1,因为就只有这是个类,具有排他性)。

设置activation="relu",等同于activation=keras.activations.relukeras.activations包中还有其它激活函数

除了一层一层加层,也可以传递一个层组成的列表:

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="relu"),
    keras.layers.Dense(100, activation="relu"),
    keras.layers.Dense(10, activation="softmax")
])

keras.io 上的代码也可以用于tf.keras,但是需要修改引入。例如,对于下面的代码:

from keras.layers import Dense
output_layer = Dense(10)

需要改成:

from tensorflow.keras.layers import Dense
output_layer = Dense(10)

或使用完整路径:

from tensorflow import keras
output_layer = keras.layers.Dense(10) 

模型的summary()方法可以展示所有层,包括每个层的名字(名字是自动生成的,除非建层时指定名字),输出的形状(None代表批次大小可以是任意值),和参数的数量。最后会输出所有参数的数量,包括可训练和不可训练参数。

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
flatten (Flatten)            (None, 784)               0         
_________________________________________________________________
dense (Dense)                (None, 300)               235500    
_________________________________________________________________
dense_1 (Dense)              (None, 100)               30100     
_________________________________________________________________
dense_2 (Dense)              (None, 10)                1010      
=================================================================
Total params: 266,610
Trainable params: 266,610
Non-trainable params: 0
_________________________________________________________________

紧密层通常有许多参数。比如,第一个隐含层有784 × 300个连接权重,再加上 300 个偏置项,总共有 235500 个参数。这么多参数可以让模型具有足够的灵活度以拟合训练数据,但也意味着可能有过拟合的风险,特别是当训练数据不足时。

使用属性,获取神经层很容易,可以通过索引或名称获取对应的层:

>>> model.layers
[<tensorflow.python.keras.layers.core.Flatten at 0x132414e48>,
 <tensorflow.python.keras.layers.core.Dense at 0x1324149b0>,
 <tensorflow.python.keras.layers.core.Dense at 0x1356ba8d0>,
 <tensorflow.python.keras.layers.core.Dense at 0x13240d240>]
>>> hidden1 = model.layers[1]
>>> hidden1.name
'dense'
>>> model.get_layer('dense') is hidden1
True

可以用get_weights()set_weights()方法,获取神经层的所有参数。对于紧密层,参数包括连接权重和偏置项:

>>> weights, biases = hidden1.get_weights()
>>> weights
array([[ 0.02448617, -0.00877795, -0.02189048, ..., -0.02766046,
         0.03859074, -0.06889391],
       ...,
       [-0.06022581,  0.01577859, -0.02585464, ..., -0.00527829,
         0.00272203, -0.06793761]], dtype=float32)
>>> weights.shape
(784, 300)
>>> biases
array([0., 0., 0., 0., 0., 0., 0., 0., 0., ...,  0., 0., 0.], dtype=float32)
>>> biases.shape
(300,)

紧密层是随机初始化连接权重的(为了避免对称性),偏置项则是 0。如果想使用不同的初始化方法,可以在创建层时设置kernel_initializer(核是连接矩阵的另一个名字)或bias_initializer。初始化器的完整列表见这里

笔记:权重矩阵的形状取决于输入的数量。这就是为什么要在创建Sequential模型的第一层时指定input_shape。但是,如果不指定形状也没关系:Keras 会在真正搭建模型前一直等待,直到弄清输入的形状(输入真实数据时,或调用build()方法时)。在搭建模型之前,神经层是没有权重的,也干不了什么事(比如打印模型概要或保存模型)。所以如果在创建模型时知道输入的形状,最好就设置好。

编译模型

创建好模型之后,必须调用compile()方法,设置损失函数和优化器。另外,还可以指定训练和评估过程中要计算的额外指标的列表:

model.compile(loss="sparse_categorical_crossentropy",
              optimizer="sgd",
              metrics=["accuracy"])

使用loss="sparse_categorical_crossentropy"等同于loss=keras.losses.sparse_categorical_crossentropyoptimizer="sgd"等同于optimizer=keras.optimizers.SGD()metrics=["accuracy"]等同于metrics=[keras.metrics.sparse_categorical_accuracy]。后面还会使用其他的损失函数、优化器和指标,它们的完整列表见这里这里、和这里

首先,因为使用的是稀疏标签(每个实例只有一个目标类的索引,在这个例子中,目标类索引是 0 到 9),且就是这十个类,没有其它的,所以使用的是"sparse_categorical_crossentropy"损失函数。如果每个实例的每个类都有一个目标概率(比如独热向量,[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],来表示类 3),则就要使用"categorical_crossentropy"损失函数。如果是做二元分类(有一个或多个二元标签),输出层就得使用"sigmoid"激活函数,损失函数则变为"binary_crossentropy"

如果要将稀疏标签转变为独热向量标签,可以使用函数keras.utils.to_categorical()。还以使用函数np.argmax()axis=1

对于优化器,"sgd"表示使用随机梯度下降训练模型。换句话说,Keras 会进行反向传播算法

使用SGD时,调整学习率很重要,必须要手动设置好,optimizer=keras.optimizers.SGD(lr=???)optimizer="sgd"不同,它的学习率默认为lr=0.01

最后,因为是个分类器,最好在训练和评估时测量"accuracy"

训练和评估模型

可以训练模型了。只需调用fit()方法:

history = model.fit(X_train, y_train, epochs=30,
                     validation_data=(X_valid, y_valid))
Epoch 1/30
1719/1719 [==============================] - 13s 3ms/step - loss: 0.7144 - accuracy: 0.7665

这里,向fit()方法传递了输入特征(X_train)和目标类(y_train),还要要训练的周期数(不设置的话,默认的周期数是 1,肯定是不能收敛到一个好的解的)。另外还传递了验证集(它是可选的)。Keras 会在每个周期结束后,测量损失和指标,这样就可以监测模型的表现。如果模型在训练集上的表现优于在验证集上的表现,可能模型在训练集上就过拟合了(或者就是存在 bug,比如训练集和验证集的数据不匹配)。

仅需如此,神经网络就训练好了。训练中的每个周期,Keras 会展示到目前为止一共处理了多少个实例(还带有进度条),每个样本的平均训练时间,以及在训练集和验证集上的损失和准确率(和其它指标)。可以看到,损失是一直下降的,这是一个好现象。经过 30 个周期,验证集的准确率达到了 89.26%,与在训练集上的准确率差不多,所以没有过拟合。

除了通过参数validation_data传递验证集,也可以通过参数validation_split从训练集分割出一部分作为验证集。比如,validation_split=0.1可以让 Keras 使用训练数据(打散前)的末尾 10% 作为验证集。

如果训练集非常倾斜,一些类过渡表达,一些欠表达,在调用fit()时最好设置class_weight参数,可以加大欠表达类的权重,减小过渡表达类的权重。Keras 在计算损失时,会使用这些权重。如果每个实例都要加权重,可以设置sample_weight(这个参数优先于class_weight)。如果一些实例的标签是通过专家添加的,其它实例是通过众包平台添加的,最好加大前者的权重,此时给每个实例都加权重就很有必要。通过在validation_data元组中,给验证集加上样本权重作为第三项,还可以给验证集添加样本权重。

fit()方法会返回History对象,包含:训练参数(history.params)、周期列表(history.epoch)、以及最重要的包含训练集和验证集的每个周期后的损失和指标的字典(history.history)。如果用这个字典创建一个 pandas 的DataFrame,然后使用方法plot(),就可以画出学习曲线:

import pandas as pd
import matplotlib.pyplot as plt
​
pd.DataFrame(history.history).plot(figsize=(8, 5))
plt.grid(True)
plt.gca().set_ylim(0, 1) # set the vertical range to [0-1]
plt.show()

output_19_0.png

可以看到,训练准确率和验证准确率稳步提高,训练损失和验证损失持续下降。另外,验证曲线和训练曲线靠的很近,意味着没有什么过拟合。在这个例子中,在训练一开始时,模型在验证集上的表现由于训练集。但实际情况是,验证误差是在每个周期结束后算出来的,而训练误差在每个周期期间,用流动平均误差算出来的。所以训练曲线(图中橙色的那条)实际应该向左移动半个周期。移动之后,就可以发现在训练开始时,训练和验证曲线几乎是完美重合起来的。

提示:在绘制训练曲线时,应该向左移动半个周期。

通常只要训练时间足够长,训练集的表现就能超越验证集。从图中可以看到,验证损失仍然在下降,模型收敛的还不好,所以训练应该持续下去。只需要再次调用方法fit()即可,因为 Keras 可以从断点处继续(验证准确率可以达到 89%。)

如果仍然对模型的表现不满意,就需要调节超参数了。首先是学习率。如果调节学习率没有帮助,就尝试换一个优化器(记得再调节任何超参数之后都重新调节学习率)。如果效果仍然不好,就调节模型自身的超参数,比如层数、每层的神经元数,每个隐藏层的激活函数。还可以调节其它超参数,比如批次大小(通过fit()的参数batch_size,默认是 32)。当对验证准确率达到满意之后,就可以用测试集评估泛化误差。只需使用evaluate()方法(evaluate()方法包含参数batch_size和sample_weight):

model.evaluate(X_test,y_test)
313/313 [==============================] - 1s 3ms/step - loss: 58.9726 - accuracy: 0.8529
[58.97262954711914, 0.8529000282287598]

使用模型进行预测

接下来,就可以用模型的predict()方法对新实例做预测了。因为并没有新实例,所以就用测试集的前 3 个实例来演示:

X_new = X_test[:3]
y_probably = model.predict(X_new)
y_probably.round(2)
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)

可以看到,模型会对每个实例的每个类(从 0 到 9)都给出一个概率。如果只关心概率最高的类(即使概率不高),可以使用方法predict_classes()

import numpy as np
y_predict = model.predict_classes(X_new)
np.array(class_names)[y_predict]
F:\conda\condakb\envs\DLTensorflow26\lib\site-packages\tensorflow\python\keras\engine\sequential.py:455: UserWarning: `model.predict_classes()` is deprecated and will be removed after 2021-01-01. Please use instead:* `np.argmax(model.predict(x), axis=-1)`,   if your model does multi-class classification   (e.g. if it uses a `softmax` last-layer activation).* `(model.predict(x) > 0.5).astype("int32")`,   if your model does binary classification   (e.g. if it uses a `sigmoid` last-layer activation).
  warnings.warn('`model.predict_classes()` is deprecated and '
array(['Ankle boot', 'Pullover', 'Trouser'], dtype='<U11')

使用顺序 API 搭建回归 MLP

创建-编译-训练-评估-预测

使用回归神经网络来处理加州房价问题。简便起见,使用 Scikit-Learn 的fetch_california_housing()函数来加载数据,它只包括数值特征,也不包括缺失值。加载好数据之后,将数据集分割成训练集、验证集和测试集,并做特征缩放

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
​
#加载数据集
housing = fetch_california_housing()
​
#划分训练集、验证集、测试集并特征缩放
X_train_full, X_test, y_train_full, y_test = train_test_split(housing.data,housing.target)
X_train, X_valid, y_train, y_valid = train_test_split(X_train_full,y_train_full)
​
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_valid = scaler.transform(X_valid)
X_test = scaler.transform(X_test)
​
X_test
array([[-0.75633766, -0.29593675, -0.46912854, ..., -0.01887961,        -0.79953901,  0.68242146],
       [ 1.1546365 , -0.93106871,  0.40681833, ...,  0.01123479,        -0.54672281, -0.18847915],
       [ 0.11981755,  0.49797819, -0.23501919, ...,  0.05533783,         0.97485621, -1.45979383],
       ...,
       [ 0.0608231 , -0.93106871,  5.83310675, ..., -0.03962976,         1.55539713, -0.22852055],
       [-0.92795789,  0.4185867 ,  0.07287988, ..., -0.02728906,         2.42152672, -2.29565821],
       [-0.0723525 ,  1.05371865, -0.14742675, ...,  0.00947871,         0.8250392 , -1.13946257]])

使用顺序 API 搭建、训练、评估和使用回归 MLP 做预测,和前面的分类 MLP 很像。区别在于输出层只有一个神经元(因为只想预测一个值而已),也没有使用激活函数,损失函数是均方误差。因为数据集有噪音,我们就是用一个隐藏层,并且神经元也比之前少,以避免过拟合

# 设置模型、超参数
model = keras.models.Sequential([
    keras.layers.Dense(30,activation="relu",input_shape=X_train.shape[1:]),#一个隐藏层,输入神经元数量、激活函数、特征
    keras.layers.Dense(1) # 回归、一个输出的神经元
])
​
# 编译模型
model.compile(loss = keras.losses.mean_squared_error,optimizer = "sgd")#损失函数、优化器(随机梯度下降)#训练模型
history = model.fit(X_train,y_train,epochs=20,validation_data = (X_valid,y_valid))
363/363 [==============================] - 1s 2ms/step - loss: 0.3518 - val_loss: 0.3384
# 评估误差,损失函数
model.evaluate(X_test,y_test)
162/162 [==============================] - 0s 1ms/step - loss: 0.3576
0.3575988709926605
# 回归预测 预测结果与真实结果对比
housing_predict = model.predict(X_test)
plt.figure(figsize=(24, 6))
plt.plot(housing_predict.flatten()[:300])
plt.plot(y_test[:300])
plt.show()

output_31_0.png

可以看到,使用顺序 API 是很方便的。但是,尽管Sequential十分常见,但用它搭建复杂拓扑形态或多输入多输出的神经网络还是不多。所以,Keras 还提供了函数式 API。

使用函数式 API 搭建复杂模型

Wide & Deep 是一个非序列化的神经网络模型。这个架构是 Heng-Tze Cheng 在 2016 年在论文中提出来的。这个模型可以将全部或部分输入与输出层连起来。这样,就可以既学到深层模式(使用深度路径)和简单规则(使用短路径)。作为对比,常规 MLP 会强制所有数据流经所有层,因此数据中的简单模式在多次变换后会被扭曲。

使用函数式API来搭建一个这样的神经网络,来解决加州房价问题

# 指定输入层、隐藏层、连接层、输出层、并组合
input_layer = keras.layers.Input(shape = X_train.shape[1:])
hidden1 = keras.layers.Dense(30,activation="relu")(input_layer) # 指定前置层为输入层
hidden2 = keras.layers.Dense(30,activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_layer,hidden2]) # 指定前置层为输入层、隐藏层2
output_layer = keras.layers.Dense(1)(concat)
​
#创建模型,指定输入层、输出层
model = keras.Model(inputs = [input_layer],outputs = [output_layer])

每行代码的作用:

  • 首先创建一个Input对象。包括模型输入的形状shape和数据类型dtype。模型可能会有多种输入。
  • 然后,创建一个有 30 个神经元的紧密层,激活函数是 ReLU。创建好之后,将其作为函数,直接将输入传给它。这就是函数式 API 的得名原因。这里只是告诉 Keras 如何将层连起来,并没有导入实际数据。
  • 然后创建第二个隐藏层,还是将其作为函数使用,输入时第一个隐藏层的输出;
  • 接着,创建一个连接Concatenate层,也是作为函数使用,将输入和第二个隐藏层的输出连起来。可以使用keras.layers.concatenate()。
  • 然后创建输出层,只有一个神经元,没有激活函数,将连接层的输出作为输入。
  • 最后,创建一个 Keras 的Model,指明输入和输出。

搭建好模型之后,重复之前的步骤:编译模型、训练、评估、做预测。

但是如果你想将部分特征发送给 wide 路径,将部分特征(可以有重叠)发送给 deep 路径,该怎么做呢?答案是可以使用多输入。例如,假设向 wide 路径发送 5 个特征(特征 0 到 4),向 deep 路径发送 6 个特征(特征 2 到 7):

input_A = keras.layers.Input(shape=[5], name="wide_input")
input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name="output")(concat)
model = keras.Model(inputs=[input_A, input_B], outputs=[output]) 

代码非常浅显易懂。值得注意的是,在创建模型时,我们指明了inputs=[input_A, input_B]。然后就可以像通常那样编译模型了,但当调用fit()时,不是传入矩阵X_train,而是传入一对矩阵(X_train_A, X_train_B):每个输入一个矩阵。同理调用evaluate()或predict()时,X_valid、X_test、X_new也要变化:

​
model.compile(loss="mse", optimizer=keras.optimizers.SGD(lr=1e-3))
​
X_train_A, X_train_B = X_train[:, :5], X_train[:, 2:]
X_valid_A, X_valid_B = X_valid[:, :5], X_valid[:, 2:]
X_test_A, X_test_B = X_test[:, :5], X_test[:, 2:]
X_new_A, X_new_B = X_test_A[:3], X_test_B[:3]
​
history = model.fit((X_train_A, X_train_B), y_train, epochs=20,
                    validation_data=((X_valid_A, X_valid_B), y_valid))
mse_test = model.evaluate((X_test_A, X_test_B), y_test)
y_pred = model.predict((X_new_A, X_new_B)) 

有以下要使用多输入的场景:

  • 任务要求。例如,你想定位和分类图片中的主要物体。这既是一个回归任务(找到目标中心的坐标、宽度和高度)和分类任务。
  • 相似的,对于相同的数据,你可能有多个独立的任务。当然可以每个任务训练一个神经网络,但在多数情况下,同时对所有任务训练一个神经网络,每个任务一个输出,后者的效果更好。这是因为神经网络可以在不同任务间学习有用的数据特征。例如,在人脸的多任务分类时,你可以用一个输出做人物表情的分类(微笑惊讶等等),用另一个输出判断是否戴着眼镜。
  • 另一种情况是作为一种正则的方法(即,一种降低过拟合和提高泛化能力的训练约束)。例如,你想在神经网络中加入一些辅助输出,好让神经网络的一部分依靠自身就能学到一些东西。

添加额外的输出很容易:只需要将输出和相关的层连起来、将输出写入输出列表就行。

[...] # output 层前面都一样
output = keras.layers.Dense(1, name="main_output")(concat)
aux_output = keras.layers.Dense(1, name="aux_output")(hidden2)
model = keras.Model(inputs=[input_A, input_B], outputs=[output, aux_output]) 

每个输出都要有自己的损失函数。因此在编译模型时,需要传入损失列表(如果只传入一个损失,Keras 会认为所有输出是同一个损失函数)。Keras 默认计算所有损失,将其求和得到最终损失用于训练。主输出比辅助输出更值得关心,所以要提高它的权重,如下所示:

model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd") 

此时若要训练模型,必须给每个输出贴上标签。在这个例子中,主输出和辅输出预测的是同一件事,因此标签相同。传入数据必须是(y_train, y_train)(y_valid和y_test也是如此):

history = model.fit(
    [X_train_A, X_train_B], [y_train, y_train], epochs=20,
    validation_data=([X_valid_A, X_valid_B], [y_valid, y_valid])) 

当评估模型时,Keras 会返回总损失和各个损失值:

total_loss, main_loss, aux_loss = model.evaluate(
    [X_test_A, X_test_B], [y_test, y_test]) 

相似的,方法predict()会返回每个输出的预测值:

y_pred_main, y_pred_aux = model.predict([X_new_A, X_new_B]) 

可以看到,用函数式 API 可以轻易搭建任意架构。接下来再看最后一种搭建 Keras 模型的方法。

#编译模型
model.compile(loss = keras.losses.mean_squared_error,optimizer = "sgd")#损失函数、优化器(随机梯度下降)#训练模型
history = model.fit(X_train,y_train,epochs=10,validation_data = (X_valid,y_valid))
​
# 评估误差,损失函数
model.evaluate(X_test,y_test)
​
# 回归预测 预测结果与真实结果对比
housing_predict = model.predict(X_test)
plt.figure(figsize=(24, 6))
plt.plot(housing_predict.flatten()[:300])
plt.plot(y_test[:300])
plt.show()
Epoch 1/10
162/162 [==============================] - 0s 1ms/step - loss: 0.3259

output_35_1.png

微调神经网络的超参数

神经网络的灵活性同时也是它的缺点:要微调的超参数太多了。不仅架构可能不同,就算对于一个简单的 MLP,就可以调节层数、每层的神经元数、每层使用什么激活函数、初始化的权重,等等。如何找出最佳的超参数组合

一种方法是直接试验超参数的组合,看哪一个在验证集(或使用 K 折交叉验证)的表现最好。例如,可以使用GridSearchCV或RandomizedSearchCV探索超参数空间。要这么做的话,必须将 Keras 模型包装进模仿 Scikit-Learn 回归器的对象中。第一步是给定一组超参数,创建一个搭建和编译 Keras 模型的函数

#构造模型,指定超参数:隐藏层、神经元、学习率、特征等
def build_model(n_hidden=1, n_neurons=30, learning_rate=3e-3, input_shape=[8]):
    model = keras.models.Sequential() #顺序模型
    model.add(keras.layers.InputLayer(input_shape=input_shape)) #输入层
    for layer in range(n_hidden): #增加隐藏层 
        model.add(keras.layers.Dense(n_neurons, activation="relu"))
    model.add(keras.layers.Dense(1)) #输出层
    optimizer = keras.optimizers.SGD(learning_rate=learning_rate) #优化器
    model.compile(loss="mse", optimizer=optimizer) # 编译模型,指定损失函数
    return model #返回模型

这个函数创建了一个单回归(只有一个输出神经元)顺序模型,数据形状、隐藏层的层数和神经元数是给定的,使用指定学习率的SGD优化器编译。最好尽量给大多数超参数都设置合理的默认值,就像 Scikit-Learn 那样。

然后使用函数build_model()创建一个KerasRegressor:

# 创建Keras的sklearn包装器、探索超参数空间
keras_reg = keras.wrappers.scikit_learn.KerasRegressor(build_model) 

KerasRegressor是通过build_model()将 Keras 模型包装起来的。因为在创建时没有指定任何超参数,使用的是build_model()的默认参数。现在就可以像常规的 Scikit-Learn 回归器一样来使用它了:使用fit()方法训练,使用score()方法评估,使用predict()方法预测

#训练模型
keras_reg.fit(X_train, y_train, epochs=100,
              validation_data=(X_valid, y_valid),
              callbacks=[keras.callbacks.EarlyStopping(patience=10)])
​
#根据损失函数计算得分
mse_test = keras_reg.score(X_test, y_test)
​
#预测
y_pred = keras_reg.predict(X_test) 
F:\conda\condakb\envs\DLTensorflow26\lib\site-packages\tensorflow\python\keras\optimizer_v2\optimizer_v2.py:374: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
  warnings.warn(
Epoch 1/100
162/162 [==============================] - 0s 2ms/step - loss: 0.3574

任何传给fit()的参数都会传给底层的 Keras 模型。另外,分数的意义和 MSE 是相反的(即,分数越高越好)。因为超参数太多,最好使用随机搜索而不是网格搜索。下面来探索下隐藏层的层数、神经元数和学习率:

from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
​
# 超参数探索空间
param_distribs = {
    "n_hidden": [0, 1, 2, 3], #隐藏层
    "n_neurons": np.arange(1, 100), #神经元范围
    "learning_rate": reciprocal(3e-4, 3e-2), #学习率范围
}
# 随机探索
rnd_search_cv = RandomizedSearchCV(keras_reg, param_distribs, n_iter=10, cv=3)
#开始训练
rnd_search_cv.fit(X_train, y_train, epochs=100,
                  validation_data=(X_valid, y_valid),
                  callbacks=[keras.callbacks.EarlyStopping(patience=10)]) 
​
158/242 [==================>...........] - ETA: 0s - loss: 0.3625

这里参数传给fit(),fit()再传给底层的 Keras。注意,RandomizedSearchCV使用的是 K 折交叉验证,没有用X_valid和y_valid(只有早停时才使用)。

取决于硬件、数据集大小、模型复杂度、n_iter和cv,求解过程可能会持续几个小时。计算完毕后,就能得到最佳参数、最佳得分和训练好的 Keras 模型,如下所示:

>>> rnd_search_cv.best_params_
{'learning_rate': 0.0033625641252688094, 'n_hidden': 2, 'n_neurons': 42}
>>> rnd_search_cv.best_score_
-0.3189529188278931
>>> model = rnd_search_cv.best_estimator_.model 

现在就可以保存模型、在测试集上评估,如果对效果满意,就可以部署了。使用随机搜索并不难,适用于许多相对简单的问题。但是当训练较慢时(大数据集的复杂问题),这个方法就只能探索超参数空间的一小部分而已。通过手动调节可以缓解一下:首先使用大范围的超参数值先做一次随机搜索,然后根据第一次的结果再做一次小范围的计算,以此类推。这样就能缩放到最优超参数的范围了。但是,这么做很耗时。

幸好,有比随机搜索更好的探索超参数空间的方法。核心思想很简单:当某块空间的区域表现好时,就多探索这块区域。这些方法可以代替用户做“放大”工作,可以在更短的时间得到更好的结果。下面是一些可以用来优化超参数的 Python 库:

  • Hyperopt 一个可以优化各种复杂搜索空间(包括真实值,比如学习率和离散值,比如层数)的库。
  • Hyperas,kopt 或 Talos 用来优化 Keras 模型超参数的库(前两个是基于 Hyperopt 的)。
  • Keras Tuner Google 开发的简单易用的 Keras 超参数优化库,还有可视化和分析功能。
  • Scikit-Optimize (skopt) 一个通用的优化库。类BayesSearchCV使用类似于GridSearchCV的接口做贝叶斯优化。
  • Spearmint 一个贝叶斯优化库。
  • Hyperband 一个快速超参数调节库,基于 Lisha Li 的论文《Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization》。
  • Sklearn-Deap 一个基于进化算法的超参数优化库,接口类似GridSearchCV。

隐藏层数

对于许多问题,开始时只用一个隐藏层就能得到不错的结果。只要有足够多的神经元,只有一个隐藏层的 MLP 就可以对复杂函数建模。但是对于复杂问题,深层网络比浅层网络有更高的参数效率:深层网络可以用指数级别更少的神经元对复杂函数建模,因此对于同样的训练数据量性能更好。

要明白为什么,假设别人让你用绘图软件画一片森林,但你不能复制和粘贴。这样的话,就得花很长时间,你需要手动来画每一棵树,一个树枝然后一个树枝,一片叶子然后一片叶子。如果可以先画一片叶子,然后将叶子复制粘贴到整个树枝上,再将树枝复制粘贴到整棵树上,然后再复制树,就可以画出一片森林了,所用的时间可以大大缩短。真实世界的数据通常都是有层次化结构的,深层神经网络正式利用了这一点:浅隐藏层对低级结构(比如各种形状的线段和方向),中隐藏层结合这些低级结构对中级结构(方,圆)建模,深隐藏层和输出层结合中级结构对高级结构(比如,脸)建模。

层级化的结构不仅帮助深度神经网络收敛更快,,也提高了对新数据集的泛化能力。例如,如果已经训练好了一个图片人脸识别的模型,现在想训练一个识别发型的神经网络,你就可以复用第一个网络的浅层。不用随机初始化前几层的权重和偏置项,而是初始化为第一个网络浅层的权重和偏置项。这样,网络就不用从多数图片的低级结构开始学起;只要学高级结构(发型)就行了。这就称为迁移学习。

概括来讲,对于许多问题,神经网络只有一或两层就够了。例如,只用一个隐藏层和几百个神经元,就能在 MNIST 上轻松达到 97% 的准确率;同样的神经元数,两个隐藏层,训练时间几乎相同,就能达到 98% 的准确率。对于更复杂的问题,可以增加隐藏层的数量,直到在训练集上过拟合为止。非常复杂的任务,比如大图片分类或语音识别,神经网络通常需要几十层(甚至上百,但不是全连接的),需要的训练数据量很大。对于这样的网络,很少是从零训练的:常见的是使用预训练好的、表现出众的任务相近的网络,训练可以快得多,需要的数据也可以不那么多

每个隐藏层的神经元数

输入层和输出层的神经元数是由任务确定的输入和输出类型决定的。例如,MNIST 任务需要28 × 28 = 784个输入神经元和 10 个输出神经元。

对于隐藏层,惯用的方法是模拟金字塔的形状,神经元数逐层递减 —— 底层思想是,许多低级特征可以聚合成少得多的高级特征。MNIST 的典型神经网络可能需要 3 个隐藏层,第一层有 300 个神经元,第二层有 200 个神经元,第三层有 100 个神经元。然而,这种方法已经被抛弃了,因为所有隐藏层使用同样多的神经元不仅表现更好,要调节的超参数也只变成了一个,而不是每层都有一个。或者,取决于数据集的情况,有时可以让第一个隐藏层比其它层更大。

和层数相同,可以逐步提高神经元的数量,直到发生过拟合为止。但在实际中,通常的简便而高效的方法是使用层数和神经元数都超量的模型,然后使用早停和其它正则技术防止过拟合。一位 Google 的科学家 Vincent Vanhoucke,称这种方法为“弹力裤”:不浪费时间选择尺寸完美匹配的裤子,而是选择一条大的弹力裤,它能自动收缩到合适的尺寸。通过这种方法,可以避免影响模型的瓶颈层。另一方面,如果某层的神经元太少,就没有足够强的表征能力,保存所有的输入信息(比如,只有两个神经元的的层只能输出 2D 数据,如果用它处理 3D 数据,就会丢失信息)。无论模型网络的其它部分如何强大,丢失的信息也找不回来了。

提示:通常,增加层数比增加每层的神经元的收益更高。

学习率,批次大小和其它超参数

隐藏层的层数和神经元数不是 MLP 唯二要调节的参数。下面是一些其它的超参数和调节策略:

学习率: 学习率可能是最重要的超参数。通常,最佳学习率是最大学习率(最大学习率是超过一定值,训练算法发生分叉的学习率)的大概一半。找到最佳学习率的方式之一是从一个极小值开始(比如10^(-5))训练模型几百次,直到学习率达到一个比较大的值(比如 10)。这是通过在每次迭代,将学习率乘以一个常数实现的(例如exp(log(10^6)/500,通过 500 次迭代,从10^(-5)到 10 )。如果将损失作为学习率的函数画出来(学习率使用 log),能看到损失一开始是下降的。过了一段时间,学习率会变得非常高,损失就会升高:最佳学习率要比损失开始升高的点低一点(通常比拐点低 10 倍)。然后就可以重新初始化模型,用这个学习率开始训练了。

优化器: 选择一个更好的优化器(并调节超参数)而不是传统的小批量梯度下降优化器同样重要。

批次大小: 批次大小对模型的表现和训练时间非常重要。使用大批次的好处是硬件(比如 GPU)可以快速处理,每秒可以处理更多实例。因此,许多人建议批次大小开到 GPU 内存的最大值。但也有缺点:在实际中,大批次,会导致训练不稳定,特别是在训练开始时,并且不如小批次模型的泛化能力好。2018 年四月,Yann LeCun 甚至发了一条推特:“朋友之间不会让对方的批次大小超过 32”,引用的是 Dominic Masters 和 Carlo Luschi 的论文《Revisiting Small Batch Training for Deep Neural Networks》,在这篇论文中,作者的结论是小批次(2 到 32)更可取,因为小批次可以在更短的训练时间得到更好的模型。但是,有的论文的结论截然相反:2017 年,两篇论文《Train longer, generalize better: closing the generalization gap in large batch training of neural networks》和《Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour》建议,通过多种方法,比如给学习率热身(即学习率一开始很小,然后逐渐提高,就能使用大批次(最大 8192)。这样,训练时间就能非常短,也没有泛化鸿沟。因此,一种策略是通过学习率热身使用大批次,如果训练不稳定或效果不好,就换成小批次。

激活函数: 如何选择激活函数:通常来讲,ReLU 适用于所有隐藏层。对于输出层,就要取决于任务。

迭代次数: 对于大多数情况,用不着调节训练的迭代次数:使用早停就成了。

提示:最佳学习率还取决于其它超参数,特别是批次大小,所以如果调节了任意超参数,最好也更新学习率。

想看更多关于调节超参数的实践,可以参考 Leslie Smith 的论文《A disciplined approach to neural network hyper-parameters: Part 1 -- learning rate, batch size, momentum, and weight decay》。

内容来源