使用Sequential API搭建回归MLP
前面的文章讲述了如何用Sequential API搭建分类MLP,接下来我们要用它来搭建回归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)
使用Sequential 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="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)
X_new = X_test[:3]
y_pred = model.predict(X_new)
我们可以看到,Sequential API使用方法非常简单。然而,尽管Sequential模型很常用,但是搭建一些更加复杂拓扑结构的模型也是十分有用的,有时还会增加一些输入或输出神经元。出于这个目的,Keras提供了Functional API。
使用Functional API搭建复杂模型
非Sequential神经网络的一个例子是Wide & Deep神经网络。这种神经网络结构是由Heng-Tze Cheng等在2016年的一篇论文中提出的。如图1.14所示,它将所有或部分输入神经元直接连接到输出层,这种体系结构使神经网络能够同时学习深层规则(使用深路径)和简单规则(通过短路径)。
让我们建立这样一个神经网络来解决加州的房价问题:
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)。这是模型将得到的输入数据的说明,包括数据的形状和类型。一个模型实际上可能有多个输入对象,稍后我们将看到这一点。
- 接下来,我们创建了一个30个神经元的Dense层,使用ReLU激活函数。在创建完成之后的第二个括号里,我们加了一个类似函数的结构,设置这个Dense层的输入。这个类似函数的结构就是Functional API这个名字的由来。但是这里要注意,我们只是告诉Keras如何将这些层连接起来,目前并没有将实际的数据进行传输。
- 然后我们创建第二个隐藏层,并再次使用函数的结构。然后再将第一个隐藏层的输出传递给它。
- 接下来,我们创建一个Concatenate()层,我们再次像使用函数一样用它来连接第二个隐藏层的输入和输出。Concatenate层的功能非常简单,就是将一个列表的输入张量连接起来。
- 然后我们创建只有一个神经元输出层,没有激活函数,并且把Concatenate层连接的结果传递给它。
- 最后创建Keras模型,指定要使用哪些输入和输出。
在构建了Keras模型之后,所有的工作就都跟前文一样了,编译,训练,评估,预测,这里就不再次赘述了。
但是,如果想要用宽路径发送特征的子集,而通过深路径发送不同的子集(也有可能重叠),那该怎么办呢(见图10-15)?在这种情况下,一种解决办法是使用多个输入。例如,假设我们想通过深路径发送5个特征(特征0到4),通过宽路径发送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_A, X_train_B),这样每个矩阵与每个输入对应。在评估和预测的时候,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))
在很多情况下,还可能会用到多个输出:
- 任务要求使用多个输出。例如,图片中的主要对象分类和定位的任务,既是一个回归任务(查找对象中心的坐标,以及它的宽度和高度),也是一个分类任务。
- 类似地,还可能会有多个基于相同数据集的独立任务。当然,我们可以为每个任务训练一个神经网络,但是通常通过训练一个神经网络并为每个任务设置一个输出会获得更好的结果。这是因为神经网络可以学习数据中对不同任务有用的特征。例如,对人脸图片执行多任务分类,使用一个输出来识别人的面部表情(微笑、惊讶等),另一个输出来识别他们是否戴眼镜等。
- 另一个用例是作为正则化技术(即训练约束,其目标是减少过拟合,从而提高模型的泛化能力)。例如,您可能希望在神经网络体系结构中添加一些辅助输出(参见图1.16),以确保网络的底层部分能独立地学习一些有用的东西,而不依赖于网络的其他部分。
添加额外的输出非常简单:只需将输出层连接到适当的层,并将它们添加到模型的输出列表中。例如,下面的代码构建图1.16中所示的网络:
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会默认将计算所有损失,并将它们简单相加,得到用于训练的最终损失。 我们更关心主要输出的表现,而不是辅助输出(因为它只是用于正则化),所以我们希望赋予主输出更大的权重。所以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])
正如上文所述,我们可以很简单地用Functional API构建任何类型的模型架构。下一篇文章我们将介绍如何搭建动态模型,以及模型的一些常用操作。
敬请期待吧!