在本次实验中,我们将构建一个简单的神经网络
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle')
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
from lab_utils_common import dlc
from lab_coffee_utils import load_coffee_data, plt_roast, plt_prob, plt_layer, plt_network, plt_output_unit
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
tf.autograph.set_verbosity(0)
数据集
X,Y = load_coffee_data();
print(X.shape, Y.shape)
让我们在下面绘制咖啡烘焙数据。这两个功能是以摄氏度为单位的温度和以分钟为单位的持续时间。在家烘焙咖啡建议持续时间最好保持在 12 到 15 分钟之间,而温度应在 175 到 260 摄氏度之间。当然,随着温度的升高,持续时间应该会缩短。
plt_roast(X,Y)
规范化数据
如果数据被归一化,那么将权重拟合到数据(反向传播,在下周的讲座中将介绍)将更快地进行。这与您在课程 1 中使用的过程相同,其中数据中的每个要素都被归一化为具有相似的范围。以下过程使用 Keras 归一化层。它包含以下步骤:
1.创建一个“归一化层”。请注意,如此处应用的,这不是模型中的层。
2.“适应”数据。这将学习数据集的均值和方差,并在内部保存值。
3.对数据进行规范化。将归一化应用于利用学习模型的任何未来数据非常重要。
print(f"Temperature Max, Min pre normalization: {np.max(X[:,0]):0.2f}, {np.min(X[:,0]):0.2f}")
print(f"Duration Max, Min pre normalization: {np.max(X[:,1]):0.2f}, {np.min(X[:,1]):0.2f}")
norm_l = tf.keras.layers.Normalization(axis=-1)
norm_l.adapt(X) # learns mean, variance
Xn = norm_l(X)
print(f"Temperature Max, Min post normalization: {np.max(Xn[:,0]):0.2f}, {np.min(Xn[:,0]):0.2f}")
print(f"Duration Max, Min post normalization: {np.max(Xn[:,1]):0.2f}, {np.min(Xn[:,1]):0.2f}")
平铺/复制我们的数据以增加训练集大小并减少训练周期的数量。
Xt = np.tile(Xn,(1000,1))
Yt= np.tile(Y,(1000,1))
print(Xt.shape, Yt.shape)
Tensorflow模型
让我们建立讲座中描述的“咖啡烘焙网络”。有两层具有 sigmoid 激活,如下所示
tf.random.set_seed(1234) # applied to achieve consistent results
model = Sequential(
[
tf.keras.Input(shape=(2,)),
Dense(3, activation='sigmoid', name = 'layer1'),
Dense(1, activation='sigmoid', name = 'layer2')
]
)
注 1:'tf.keras.Input(shape=(2,)),' 指定了输入的预期形状。这使得 Tensorflow 可以在此时调整权重和偏置参数的大小。 这在探索 Tensorflow 模型时非常有用。在实践中可以省略此语句,当在“model.fit”语句中指定输入数据时,Tensorflow 将调整网络参数的大小。
注 2:在最后一层包含 sigmoid 激活不被视为最佳实践。相反,它将被计入损失中,从而提高了数值稳定性。这将在后面的实验中更详细地描述。
model.summary()
摘要中显示的参数计数对应于权重和偏置数组中的元素数,如下所示。
L1_num_params = 2 * 3 + 3 # W1 parameters + b1 parameters
L2_num_params = 3 * 1 + 1 # W2 parameters + b2 parameters
print("L1 params = ", L1_num_params, ", L2 params = ", L2_num_params )
让我们来看看 Tensorflow 实例化的权重和偏差。权重 W应具有大小(输入中的特征数量,层中的单元数量)而偏置大小应与图层中的单元数匹配:
1.在具有 3 个单元的第一层中,我们期望 W 的大小为(2,3)和b应该有 3 个元素。
2.在具有 1 个单位的第二层中,我们期望 W 的大小为(3,1)和b应该有 1 个元素。
W1, b1 = model.get_layer("layer1").get_weights()
W2, b2 = model.get_layer("layer2").get_weights()
print(f"W1{W1.shape}:\n", W1, f"\nb1{b1.shape}:", b1)
print(f"W2{W2.shape}:\n", W2, f"\nb2{b2.shape}:", b2)
以下陈述将在第 2 周中详细描述。目前:
model.compile 语句定义了一个损失函数,并指定了一个编译优化。
model.fit 语句运行梯度下降并将权重拟合到数据。
model.compile(
loss = tf.keras.losses.BinaryCrossentropy(),
optimizer = tf.keras.optimizers.Adam(learning_rate=0.01),
)
model.fit(
Xt,Yt,
epochs=10,
)
更新的权重
拟合后,权重已更新:
W1, b1 = model.get_layer("layer1").get_weights()
W2, b2 = model.get_layer("layer2").get_weights()
print("W1:\n", W1, "\nb1:", b1)
print("W2:\n", W2, "\nb2:", b2)
接下来,我们将加载一些从上一次训练运行中保存的权重。这样一来,这个笔记本就对 Tensorflow 随时间的变化保持鲁棒性。不同的训练运行可能会产生一些不同的结果,下面的讨论适用于特定的解决方案。随意重新运行此单元格注释掉的笔记本,以查看差异。
W1 = np.array([
[-8.94, 0.29, 12.89],
[-0.17, -7.34, 10.79]] )
b1 = np.array([-9.87, -9.28, 1.01])
W2 = np.array([
[-31.38],
[-27.86],
[-32.79]])
b2 = np.array([15.54])
model.get_layer("layer1").set_weights([W1,b1])
model.get_layer("layer2").set_weights([W2,b2])
预测
一旦你有一个训练好的模型,你就可以用它来进行预测。回想一下,我们模型的输出是一个概率。在这种情况下,良好烤肉的概率。要做出决定,必须将概率应用于阈值。在本例中,我们将使用 0.5
让我们从创建输入数据开始。该模型需要一个或多个示例,其中示例位于矩阵行中。在本例中,我们有两个特征,因此矩阵将为 (m,2),其中 m 是示例的数量。回想一下,我们已经对输入特征进行了规范化,因此我们也必须对测试数据进行规范化。
若要进行预测,请应用 predict 方法。
X_test = np.array([
[200,13.9], # postive example
[200,17]]) # negative example
X_testn = norm_l(X_test)
predictions = model.predict(X_testn)
print("predictions = \n", predictions)
epoch和batch
在上面的编译语句中,epoch 的数量设置为 10。这指定了整个数据集应在训练期间应用 10 次。在训练期间,您会看到描述训练进度的输出,如下所示:
epoch1/10
6250/6250 [==============================] - 6s 910us/步 - 损失:0.1782
第一行 Epoch 1/10 描述模型当前正在运行的纪元。为了提高效率,训练数据集被分解为“批次”。Tensorflow 中批处理的默认大小为 32。我们扩展的数据集中有 200000 个示例或 6250 个批次。第 2 行 6250/6250 [==== 上的符号描述了已执行的批次。
为了将概率转换为决策,我们应用了一个阈值:
yhat = (predictions >= 0.5).astype(int)
print(f"decisions = \n{yhat}")
这可以更简洁地完成:
yhat = np.zeros_like(predictions)
for i in range(len(predictions)):
if predictions[i] >= 0.5:
yhat[i] = 1
else:
yhat[i] = 0
print(f"decisions = \n{yhat}")
Layer Functions
让我们检查一下单元的功能,以确定它们在咖啡烘焙决策中的作用。我们将绘制每个节点的所有输入值(持续时间、温度)的输出。每个单元都是一个逻辑函数,其输出范围可以从 0 到 1。图中的阴影表示输出值 。
注意:在实验室中,我们通常从零开始对事物进行编号,而讲座可能从 1 开始。
plt_layer(X,Y.reshape(-1,),W1,b1,norm_l)
阴影显示每个单元负责不同的“坏烤”区域。当温度过低时,单元 0 的值较大。当持续时间太短时,单元 1 的值较大,而单元 2 的值较大,用于时间/温度的不良组合。值得注意的是,网络通过梯度下降过程自行学习了这些功能。它们与一个人可能选择做出相同决定的功能非常相似。
最后一层的功能图有点难以可视化。它的输入是第一层的输出。我们知道第一层使用 sigmoid,因此它们的输出范围在 0 到 1 之间。我们可以创建一个 3D 图,用于计算三个输入的所有可能组合的输出。如下所示。在上面,高输出值对应于“不良烘焙”区域。下面,最大输出在区域中,其中三个输入是对应于“良好烘焙”区域的小值。
plt_output_unit(W2,b2)
最后一张图显示了整个网络的运行情况。
左图是蓝色阴影表示的最后一层的原始输出。这覆盖在由 X 和 O 表示的训练数据上。
右图是决策阈值后网络的输出。此处的 X 和 O 对应于网络做出的决策。以下内容需要一些时间才能运行
netf= lambda x : model.predict(norm_l(x))
plt_network(X,Y,netf)