Keras:人工神经网络

319 阅读9分钟

人工神经网络是深度学习的核心。它们用途广泛、功能强大且可扩展,使其非常适合处理大型和高度复杂的机器学习任务,例如对数十亿张图像进行分类,为语音识别服务提供支持,每天向成千上万的用户推荐观看的最佳视频,或学习在围棋游戏中击败世界冠军。

本章第一部分介绍人工神经网络,首先对第一个人工神经网络(ANN)架构的快速浏览,然后介绍多层感知机(MLP)。第二部分,我们将研究如何使用流行的Keras API实现神经网络。

1.1 感知器

感知器是最简单的ANN架构之一,称为阈值逻辑单元(TLU),也称为线性阈值单元(LTU)。输入和输出是数字,并且每个输入连接都与权重关联。TLU计算其输入加权和(z=w1x1+w1x1+...+w1x1=xTwz=w_{1}x_{1}+w_{1}x_{1}+...+w_{1}x_{1}=x^{T}w),然后将阶跃函数应用于该和,并输出结果:hw(x)=step(z),其中z=xTwh_{w}(x)=step(z), 其中z=x^{T}w

image.png 感知器中最常用的阶跃函数是Heaviside阶跃函数,有时使用符号函数代替。

heaviside(z)={0,如果z<01,如果z0sgn(z)={1,如果z<00,如果z=0+1,如果z>0heaviside(z)=\left\{\begin{matrix}0,如果z<0\\1,如果z\geq0 \end{matrix}\right. \quad\quad sgn(z)=\left\{\begin{matrix}-1,如果z<0\\0,如果z=0\\+1,如果z>0 \end{matrix}\right.

单个TLU可用于简单的线性二进制分类。它计算输入的线性组合,如果结果超过阈值,则输出正类,否则,输出负类。

感知器仅由单层TLU组成,每个TLU连接到所有的输入。当一层中所有的神经元都连接到上一层中的每个神经元时,该层称为全连接层或密基层全连接层或密基层。 感知器的输入称为输入神经元,所有的输入神经元形成输入层,此外,通常会添加一个额外的偏置特征x0=1x_{0}=1,该神经元始终输出为1。

具有输入和三个输出的感知器,该感知器将实例分为三个不同的二进制类,这使其成为多输出分类器。

image.png

Scikit-Learn提供了一个Perceptron类,该类实现了单个TLU网络。它可以像你期望的那样使用。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import Perceptron
iris = load_iris()
X = iris.data[:, (2, 3)]
print('X shape:', X.shape)
y = (iris.target == 0).astype(np.int32)
per_clf = Perceptron()
per_clf.fit(X, y)
y_pred = per_clf.predict([[2, 0.5]])
print('pred y:', y_pred)
X shape: (150, 2)
pred y: [0]

注意:与逻辑回归分类器相反,感知器不输出分类概率;相反,它们基于硬阈值进行预测。这是逻辑回归胜过感知器的原因。另外,感知器的一些严重缺陷,即它们无法解决一些琐碎的问题,例如异或(XOR)分类问题。但事实证明,可以通过堆叠多个感知器来消除感知器的某些局限性,所得的ANN称为多层感知器(MLP),MLP可以解决XOR问题!

image.png

当一个ANN包含一个深层的隐藏层时,它称为深度神经网络(DNN)。另外,作者对MLP的架构进行了重要更改,将阶跃函数替换为逻辑函数。

1.2 使用Keras实现MLP

1.2 安装TensorFlow 2

使用pip 安装TensorFlow

$cd $ML_PATH    # your ML working directory
$source my_env/bin/activate # 进入虚拟环境
$python3 -m pip install -U tensorflow
import tensorflow as tf
from tensorflow import keras
print(tf.__version__)
print(keras.__version__)
2.7.0
2.7.0

1.3 使用顺序API构建图像分类器

加载数据集,本章我们使用Fashion MNIST,它是MINIST的直接替代品,它包含70000张灰度图像,28*28像素,有10类。

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

当使用keras而不是scikit-learn来加载MNIST或Fashion MNIST时,一个重要区别是每个图像都表示28*28阵列,而不是尺寸为784的一维阵列。

X_train_full.shape, X_train_full.dtype
((60000, 28, 28), dtype('uint8'))

请注意,数据集已经分为训练集和测试集,但是没有验证集,因此我们需要创建一个。另外,由于要使用梯度下降训练神经网络,因此必须比例缩放输入特征。

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:]
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"))
2022-02-25 20:35:39.514794: I tensorflow/core/platform/cpu_feature_guard.cc:151] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.

逐行浏览下我们的代码:

  • 第一行创建了一个Sequential模型,它有顺序连接的单层堆栈组成,这称为顺序API。
  • 构建第一层并将其添加到模型中,它是Flatten层,其作用是将每个输入图像转换为一维度组:如果接收到输入数据X,则计算X.reshape(-1, 1)。该层没有任何参数,它只是在那里做一些简单的预处理。由于它是模型的第一层,因此应指定input_shape,其中不包括批处理大小,而仅包括实例的形状。或者,我们可以添加keras.layers.InputLayer作为第一层,设置input_shape=[28, 28]
  • 继续添加具有300个神经元的Dense隐藏层。它使用ReLU激活函数
  • 最后一层,添加一个包含10个神经元的Dense输出层(每个类一个),使用softmax激活函数。
# 当然,可以在创建顺序模型时传递一个层列表
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")
                                ])

模型的summary()方法显示模型的所有层,包括每个层的名称(除非在创建层时进行设置,否则会自动生成),其输出形状(None表示批处理大小任意),以及它的参数数量。

model.summary()
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten_1 (Flatten)         (None, 784)               0         
                                                                 
 dense_3 (Dense)             (None, 300)               235500    
                                                                 
 dense_4 (Dense)             (None, 100)               30100     
                                                                 
 dense_5 (Dense)             (None, 10)                1010      
                                                                 
=================================================================
Total params: 266,610
Trainable params: 266,610
Non-trainable params: 0
_________________________________________________________________

我们还可以轻松获取模型的层列表,按其索引获取层,也可以按名称获取:

model.layers
[<keras.layers.core.flatten.Flatten at 0x7f905b912d30>,
 <keras.layers.core.dense.Dense at 0x7f90856569a0>,
 <keras.layers.core.dense.Dense at 0x7f9085656940>,
 <keras.layers.core.dense.Dense at 0x7f9085656be0>]
# 按其索引获取层
hidden1 = model.layers[1]
hidden1.name
'dense_3'
# 按其名称获取层
model.get_layer('dense_3') is hidden1
True

用get_weights()和set_weights()方法访问层的所有参数。

weights, biases = hidden1.get_weights()
weights
array([[-0.03004253,  0.01330522, -0.07171833, ...,  0.0123945 ,
        -0.04613641, -0.00885969],
       [-0.05429018, -0.03919855,  0.05382234, ..., -0.05402016,
        -0.06703871, -0.03962466],
       [-0.02403101, -0.06023717,  0.03482912, ..., -0.06273153,
         0.02592501, -0.04797506],
       ...,
       [ 0.00548309, -0.06492002,  0.02081493, ...,  0.02499246,
         0.06820726, -0.02501992],
       [-0.01385025,  0.01451471,  0.02132039, ..., -0.01430049,
         0.03074094,  0.01711423],
       [ 0.07433248,  0.04770296, -0.01485591, ..., -0.036945  ,
        -0.0489348 , -0.01231431]], dtype=float32)
weights.shape, biases.shape
((784, 300), (300,))

注意:密基层随机初始化了连接权重,并且偏置被初始化为0,这是可以的,如果要使用其他初始化方法,可以在创建层时设置kernel_initializer或bias_initializer。

编译模型

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

使用loss = "sparse_categorical_crossentropy",等同于使用loss = keras.losses.sparse_categorical_crossentropy.同样,指定optimizer="sgd"等同于指定optimizer=keras.optimizers.SGD(),是随机梯度下降法。而metrics=["accuracy"],等同于metrics=[keras.metrics.sparse_categorical_accuracy]。

我们使用sparse_categorical_crossentropy,因为我们具有稀疏标签(对于每个实例,只有一个目标类索引,该图像分类从0-9)。相反,如果每个实例的每个类都有一个目标概率,如:[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优化器时,调整学习率很重要,因此通常使用optimizer=keras.optimimzers.SGD(lr=??)来设置学习率,默认lr=0.01.

训练和评估模型

history = model.fit(X_train, y_train, epochs=30, validation_data=(X_valid, y_valid))
Epoch 1/30
1719/1719 [==============================] - 4s 2ms/step - loss: 0.7196 - accuracy: 0.7589 - val_loss: 0.5087 - val_accuracy: 0.8304
Epoch 2/30
1719/1719 [==============================] - 4s 2ms/step - loss: 0.4907 - accuracy: 0.8297 - val_loss: 0.4485 - val_accuracy: 0.8516


Epoch 30/30
1719/1719 [==============================] - 3s 2ms/step - loss: 0.2252 - accuracy: 0.9189 - val_loss: 0.3223 - val_accuracy: 0.8856

我们将输入特征(X_train)和目标类(y_train)以及要训练的轮次数传递给它,否则它将默认是1.我们还传递了一个验证集(这是可选的)。Keras将在每个轮次结束时,测量验证集的损失和其他指标。

当然,我们可以将validation_split设置为希望Keras用于验证的训练集的比率,而不是使用validation_data参数传递验证集。如validation_split=0.1,那么使用训练集的最后10%进行验证。

如果训练集非常不平衡,其中某些类的代表过多,而其他类的代表不足,那么在调用fit()方法时设置class_weight参数会很有用。这给代表性不足的类更大的权重,给代表过多的类更小的权重。这样Keras在计算损失时,将使用这些权重。如果需要每个实例的权重,设置sample_weight参数。如果class_weight和sample_weight都提供了,Keras会把它们相乘。

fit()方法返回一个history对象,其中包含训练参数(history.params)、经历的轮次列表(history.epoch)。最重要是包含在训练集和验证集上每个轮次结束时测得的损失和额外指标的字典(history.history)。

# 每个轮次测得的平均训练损失和准确率,以及每个轮次结束时测得的平均验证损失和准确率

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) # 设置纵轴范围在【0-1】
plt.show()


image.png

如果对模型的性能不满意,则应回头调整超参数。首先要检查学习率,如果没有帮助,那请尝试另一个优化器。如果性能仍然不佳,则尝试调整模型超参数(例如层数、每层神经元数以及每个隐藏层的激活函数的类型、批处理大小batch_size等)。然后我们对测试集进行评估,使用evaluate()方法轻松完成。

model.evaluate(X_test, y_test)
313/313 [==============================] - 1s 2ms/step - loss: 82.5435 - accuracy: 0.8250


[82.54351806640625, 0.824999988079071]

使用模型进行预测

使用predict()方法对新实例进行预测。

X_new = X_test[40:45]
y_proba = model.predict(X_new)
y_proba.round(2)
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]], dtype=float32)
y_pred = model.predict(X_new)
y_pred = np.argmax(y_pred, axis=1).astype('int32')
np.array(class_names)[y_pred]
array(['T-shirt/top', 'Trouser', 'Shirt', 'Ankle boot', 'Coat'],
      dtype='<U11')

1.4 使用顺序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()
# 注意:训练集用fit_transform归一化后,测试集和验证集用transform
X_train = scaler.fit_transform(X_train)
X_valid = scaler.transform(X_valid)
X_test = scaler.transform(X_test)
print('train shape:', X_train.shape, 'test shape:', X_test.shape)
train shape: (11610, 8) test shape: (5160, 8)
model = keras.models.Sequential([
    keras.layers.Dense(30, activation="relu", input_shape=X_train.shape[1:]),
    keras.layers.Dense(1)
])
model.compile(loss='mean_squared_error', optimizer='sgd')
history = model.fit(X_train, y_train, epochs=20, validation_data=(X_valid, y_valid))
mse_test = model.evaluate(X_test, y_test)
print("mse: ", mse_test)
X_new = X_test[:3]
y_pred = model.predict(X_new)
print("pred value:", y_pred)
Epoch 1/20
363/363 [==============================] - 1s 1ms/step - loss: 0.7940 - val_loss: 0.5367
Epoch 2/20
363/363 [==============================] - 0s 1ms/step - loss: 0.5242 - val_loss: 5.8111

Epoch 19/20
363/363 [==============================] - 0s 980us/step - loss: 0.3542 - val_loss: 0.3633
Epoch 20/20
363/363 [==============================] - 0s 991us/step - loss: 0.3529 - val_loss: 0.3593
162/162 [==============================] - 0s 682us/step - loss: 0.3728
mse:  0.3727506995201111
pred value: [[2.4597797]
 [4.722451 ]
 [2.3456736]]

1.5 使用函数式API构建复杂模型

非顺序神经网络的一个示例是宽深神经网络。它将所有或部分输入直接连接到输出层,这种架构使神经网络能够学习深度模式。

# 建立一个神经网络解决加州住房问题
input_ = keras.layers.Input(shape=X_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])

对以上每行代码解读:

  • 创建一个Input对象,这是模型需要的输入类型的规范,包括其shape和dtype。实际上,一个模型可以有多个输入。
  • 创建一个包含30个神经元的Dense层,使用Relu激活函数。创建后,我们像调用函数一样给其输入参数。这就是将其称为函数式API的原因。
  • 创建一个Concatenate层,再次像函数一样立即使用它来合并输入和第二个隐藏层的输出。可能你更喜欢keras.layers.concatenate()函数,该函数创建一个Concatenate层并立即使用给定的输入对其进行调用。
  • 最后创建keras model,指定要使用的输入和输出
# 通过宽路径输入5个特征,通过深路径输入6个特征,  处理多输入
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])

编译可以像往常一样编译时候,但调用fit()方法,我们必须传递一对矩阵(X_train_A, X_train_B),同样,evaluate()或者predict()时也同样。

model.compile(loss='mse', optimizer=keras.optimizers.SGD(learning_rate=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))
Epoch 1/20
363/363 [==============================] - 1s 1ms/step - loss: 2.6928 - val_loss: 1.4076
Epoch 2/20
363/363 [==============================] - 0s 1ms/step - loss: 0.8533 - val_loss: 0.8083


Epoch 20/20
363/363 [==============================] - 0s 1ms/step - loss: 0.4402 - val_loss: 0.4530
162/162 [==============================] - 0s 767us/step - loss: 0.4621

在许多用例中,可能需要多个输出:

  • 如:想在图片中定位和分类主要物体,这既是回归任务(查找物体中心的坐标以及宽度和高度),又是分类任务。
  • 可能有基于同一数据的多个独立任务。当然我们可以为每个任务训练一个神经网络,但是许多情况下,通过训练每个任务一个输出的的单个神经网络会在所有任务上获得结果。这是因为神经网络可以学习数据中对任务有用的特征。例如:你可以对面部图片执行多任务分类,使用一个输出对人的面部表情进行分类,使用另一个输出识别他们是否戴眼镜。
  • 另一个示例是作为正则化技术,即训练约束,其目的是减少过拟合,从而提高模型的泛化能力。如:在神经网络中添加一些辅助输出,以确保网络的主要部分自己能学习有用的东西,而不依赖网络的其余部分。
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")

当训练模型时,需要为每个输出提供标签。本示例中,主要输出和辅助输出应预测相同的结果,因此它们应该使用相同的标签。

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])
                   )
Epoch 1/20
363/363 [==============================] - 1s 2ms/step - loss: 0.7694 - main_output_loss: 0.6891 - aux_output_loss: 1.4924 - val_loss: 0.5181 - val_main_output_loss: 0.4666 - val_aux_output_loss: 0.9817
Epoch 2/20
363/363 [==============================] - 0s 1ms/step - loss: 0.5139 - main_output_loss: 0.4765 - aux_output_loss: 0.8501 - val_loss: 1.0316 - val_main_output_loss: 1.0580 - val_aux_output_loss: 0.7943


Epoch 19/20
363/363 [==============================] - 0s 1ms/step - loss: 0.3686 - main_output_loss: 0.3533 - aux_output_loss: 0.5071 - val_loss: 0.3863 - val_main_output_loss: 0.3714 - val_aux_output_loss: 0.5201
Epoch 20/20
363/363 [==============================] - 0s 1ms/step - loss: 0.3631 - main_output_loss: 0.3477 - aux_output_loss: 0.5014 - val_loss: 0.3852 - val_main_output_loss: 0.3709 - val_aux_output_loss: 0.5141

评估模型时,Keras将返回总损失以及所有单个损失

total_loss, main_loss, aux_loss = model.evaluate([X_test_A, X_test_B], [y_test, y_test])
162/162 [==============================] - 0s 1ms/step - loss: 0.3865 - main_output_loss: 0.3723 - aux_output_loss: 0.5140

同样,predict()方法将为每个输出返回预测值

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

1.6 使用子类API构建动态模型

顺序API和函数式API都是声明性的:首先声明要使用层以及应该如何连接它们,然后才能开始向模型提供一些数据进行训练或推断。这具有许多优点:可以轻松保存、克隆和共享模型;可以显示和分析它的结构;框架可以推断形状和检查类型,可以及早发现错误。由于整个模型是一个静态图,因此调试起来也相当容易。但另一方面它是静态的,一些模型涉及循环、变化的形状、条件分支和其他动态行为。对于这种情况,或许你喜欢命令式的编程风格,则子类API非常适合你。

class WideAndDeepModel(keras.Model):
    def __init__(self, units=30, activation="relu", **kwargs):
        super().__init__(**kwargs)
        self.hidden1 = keras.layers.Dense(units, activation=activation)
        self.hidden12 = keras.layers.Dense(units, activation=activation)
        self.main_output = keras.layers.Dense(1)
        self.aux_output = keras.layers.Dense(1)
    
    def call(self, inputs):
        input_A, input_B = inputs
        hidden1 = self.hidden1(input_B)
        hidden2 = self.hidden2(hidden1)
        concat = keras.layers.concatenate([input_A, hidden2])
        main_output = self.main_output(concat)
        aux_output = self.aux_output(hidden2)
        return main_output, aux_output
model = WideAndDeepModel()

上面这个示例看起来类似于函数式API,只是我们不需要创建输入。我们只使用call()方法输入参数,就可以将构造函数中层的创建与其在call()方法中的用法分开。最大的区别是你可以在call()方法中执行几乎所有你想做的操作:for循环、if语句、底层TensorFlow操作等。

但这种模型架构隐藏在call()方法中,因此keras无法对其进行检查。它无法保存或克隆。当你调用summary()方法时,我们只会得到一个图层列表,而没有有关它们如何相互连接的信息。而且无法提前检查类型和形状。

1.7 保存和还原模型

使用顺序API或函数式API时,保存训练好的模型非常简单。

keras使用hdf5格式保存模型的结构,包括每一层超参数和每一层的所有模型参数值(连接权重和偏置),它还可以保存优化器。 model.save("my_kears_model.h5")

加载模型同样简单: model=keras.models.load_model("my_kears_model.h5")

当使用顺序API和函数式API时,这是适用的,但不幸,在子类API时,它不起作用。我们可以使用save_weights()和load_weights()来保存和还原模型参数,但是我们需要自己保存和还原其他所有内容。

1.8使用回调函数

fit()方法接受一个callbacks参数,该参数使你可以指定Keras在训练开始和结束时,每个轮次的开始和结束,甚至每个批量之前和之后都将调用的对象列表。

例如:在训练期间ModelCheckpoint回调会定期保存模型的检查点,默认情况下,在每个轮次结束时:


\[...\] # build and compile the model

checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5")

history = model.fit(X_train, y_train, epochs=10, callbacks=\[checkpoint_cb\])

此外,如果在训练期间使用验证集,则可以在创建ModelCheckpoint时设置save_best_only=True。在这种情况情况下,只有验证集上模型性能达到目前最好时,它才会保存模型。这样,你就不必担心训练时间太长而过拟合训练集。

input_ = keras.layers.Input(shape=X_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])

model.compile(loss='mse', optimizer=keras.optimizers.SGD(learning_rate=1e-3))

checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5", save_best_only=True)
history = model.fit(X_train, y_train, epochs=10, validation_data=(X_valid, y_valid), callbacks=[checkpoint_cb])
model = keras.models.load_model("my_keras_model.h5")
Epoch 1/10
363/363 [==============================] - 1s 1ms/step - loss: 2.4140 - val_loss: 0.9221
Epoch 2/10
363/363 [==============================] - 0s 1ms/step - loss: 0.6950 - val_loss: 0.6770
Epoch 3/10
363/363 [==============================] - 0s 1ms/step - loss: 0.6374 - val_loss: 0.6460
Epoch 4/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5949 - val_loss: 0.6327
Epoch 5/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5654 - val_loss: 0.5852
Epoch 6/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5526 - val_loss: 0.5525
Epoch 7/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5276 - val_loss: 0.5292
Epoch 8/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5150 - val_loss: 0.5144
Epoch 9/10
363/363 [==============================] - 0s 1ms/step - loss: 0.5029 - val_loss: 0.5450
Epoch 10/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4932 - val_loss: 0.5020

实现提前停止的另一种方法是使用EarlyStopping回调。如果在多个轮次(由patience参数定义)的验证集上没有任何进展,将中断训练,并且可以选择回滚到最佳模型。我们可以将两个回调结合起来,以保存模型的检查点,同时尽早中断训练。

early_stopping_cb = keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=10, 
                    validation_data=(X_valid, y_valid), 
                    callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 1/10
363/363 [==============================] - 1s 2ms/step - loss: 0.4850 - val_loss: 0.5024
Epoch 2/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4770 - val_loss: 0.5001
Epoch 3/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4781 - val_loss: 0.5288
Epoch 4/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4675 - val_loss: 0.4843
Epoch 5/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4652 - val_loss: 0.4918
Epoch 6/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4577 - val_loss: 0.4645
Epoch 7/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4540 - val_loss: 0.4709
Epoch 8/10
363/363 [==============================] - 1s 2ms/step - loss: 0.4502 - val_loss: 0.4564
Epoch 9/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4461 - val_loss: 0.4513
Epoch 10/10
363/363 [==============================] - 0s 1ms/step - loss: 0.4431 - val_loss: 0.4672

如果需要额外控制,可以轻松编写自己的自定义回调。示例:自定义回调将显示训练过程中验证损失与训练损失之间的比率。

class PrintValTrainRatioCallback(keras.callbacks.Callback):
    def on_opoch_end(self, epoch, logs):
        print("\nval/train:{:.2f}".format(logs["val_loss"]/logs["loss"]))

正如我们看到的,我们可以实现on_train_begin()、on_train_end()、on_epoch_begin()、on_epoch_end()、on_batch_begin()、on_batch_end()。

以及on_test_begin()、on_test_end()、on_test_batch_begin()、on_test_batch_end(),这些有evaluate()调用。

为了进行预测,我们还应该实现on_predict_begin()、on_predict_end()、on_predict_batch_begin()、on_predict_batch_(),这些有predict()调用。

1.9 使用TensorBoard进行可视化

TensorBoard是一款交互式的可视化工具,用于在训练期间查看学习曲线;比较多次运行的学习曲线;可视化计算图;分析训练统计数据;查看由模型生成的图像;把复杂的多维数据投影到3D、自动聚类并进行可视化。

要使用它,必须修改程序以便将要可视化的数据输出到名为事件文件的特殊二进制日志文件中,每个二进制数据记录称为摘要。TensorBoard服务器将监视日志目录,并将自动获取更改并更新可视化效果。

3. 微调神经网络超参数

神经网络需要调整的超参数,如层数,每层神经元数、每层要使用的激活函数的类型、权重初始化逻辑等。

一种选择是简单地尝试超参数的许多组合,然后查看哪种验证集最有效。我们可以像scikit-learn中使用GridSearchCV或RandomizedSearchCV来探索超参数空间。

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=keras.optimizers.SGD(learning_rate=1e-3))
    return model
# 基于build_model()函数创建一个 KerasRegressor
keras_reg = keras.wrappers.scikit_learn.KerasRegressor(build_model)
/var/folders/bn/0lpcbtqn6w7159rr0gp940z80000gn/T/ipykernel_1031/3314319895.py:2: DeprecationWarning: KerasRegressor is deprecated, use Sci-Keras (https://github.com/adriangb/scikeras) instead.
  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_new)
Epoch 1/100
363/363 [==============================] - 1s 1ms/step - loss: 2.2772 - val_loss: 4.5923
Epoch 2/100
363/363 [==============================] - 0s 1ms/step - loss: 0.9441 - val_loss: 1.4326

Epoch 99/100
363/363 [==============================] - 0s 1ms/step - loss: 0.3619 - val_loss: 0.3775
Epoch 100/100
363/363 [==============================] - 0s 1ms/step - loss: 0.3617 - val_loss: 0.3764
162/162 [==============================] - 0s 905us/step - loss: 0.3802
WARNING:tensorflow:6 out of the last 7 calls to <function Model.make_predict_function.<locals>.predict_function at 0x7f904b0b1d30> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.

训练数百个变体,查看哪种变体在验证集上表现最佳。由于存在许多超参数,最好使用随机搜索而不是网格搜索

from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV

param_distribs = {
    "n_hidden": [1, 2],
    "n_neurons": np.arange(1, 3),
    "learning_rate": reciprocal(3e-4, 3e-2)
}

rnd_search_cv = RandomizedSearchCV(keras_reg, param_distribs, n_iter=2, cv=3)
rnd_search_cv.fit(X_train, y_train, epochs=100, 
                  validation_data=(X_valid, y_valid),
                  callbacks=[keras.callbacks.EarlyStopping(patience=3)])
Epoch 1/100
242/242 [==============================] - 1s 1ms/step - loss: 4.0019 - val_loss: 2.9071
Epoch 2/100
242/242 [==============================] - 0s 1ms/step - loss: 2.4416 - val_loss: 2.0311

Epoch 100/100
242/242 [==============================] - 0s 2ms/step - loss: 0.5250 - val_loss: 0.5378
121/121 [==============================] - 0s 909us/step - loss: 0.4944
Epoch 1/100
242/242 [==============================] - 1s 2ms/step - loss: 3.4371 - val_loss: 2.2954
Epoch 2/100
242/242 [==============================] - 0s 1ms/step - loss: 1.8399 - val_loss: 1.4620

Epoch 72/100
363/363 [==============================] - 0s 1ms/step - loss: 0.5108 - val_loss: 0.5433





RandomizedSearchCV(cv=3,
                   estimator=<keras.wrappers.scikit_learn.KerasRegressor object at 0x7f90477dad00>,
                   n_iter=2,
                   param_distributions={'learning_rate': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f904e39d400>,
                                        'n_hidden': [1, 2],
                                        'n_neurons': array([1, 2])})

请注意,RandomizedSearchCV使用K折交叉验证,因此它不使用X_valid和y_valid,它们仅用于提前停止。

rnd_search_cv.best_params_
{'learning_rate': 0.01125341957768439, 'n_hidden': 1, 'n_neurons': 1}
rnd_search_cv.best_score_
-0.4558099905649821
model = rnd_search_cv.best_estimator_.model

现在,我们可以保存模型,在测试集上对其进行评估。还有很多技术可以比随机方法更有效的探索空间。以下是一些用于优化超参数的Python库:

  • Hyperopt
  • Keras Tuner
  • Hyperas