前言
最近在学习吴恩达佬的机器学习网课。不得不说这个网课的资源还是相当完善的,lab、课件质量都很高,吴恩达佬本人的讲课水平更不用说了(还可以顺便练习听力
这个简单的翻译的主要目的是帮助我巩固学习,如果有英语不好的同学,也可以参考。翻译有错误或者疏漏,欢迎指出。
lab简介
这个lab是第二课(神经网络)的第二个lab,所以只涉及比较初级的神经网络知识,比如sigmoid激活函数、神经网络的基本结构等。手把手地教你搭建一个简单的神经网络来处理咖啡豆烘培好坏的分类问题。
结构很完整,数据集定义、数据预处理、模型搭建、模型预测都有,而最有趣的是最后的直观化分析部分,这部分介绍了神经网络每一层的工作成果,真的很有很有意思!!!
可选Lab - 简单神经网络
在这次实验中,我们将使用Tensorflow框架搭建一个小型的神经网络模型。
导入要使用的库。
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle')
import tensorflow as tf
from tensorflow.keras.models 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)
数据集(DataSet)
导入数据集。
X,Y = load_coffee_data();
print(X.shape, Y.shape)
运行结果:
(200, 2) (200, 1)
来把上面的咖啡烘培数据画成图。数据的两个特征分贝是温度(摄氏度)和烘培时间(分钟)。网站Coffee Roasting at HOme建议烘焙时间最好控制在12到15分钟,同时最好将烘焙温度保持在175到260摄氏度。当然,如果温度上升了,那么最佳烘焙时间理应缩短。
plt_roast(X,Y)
运行结果:
数据正则化
如果对数据进行归一化处理,那么权重对数据的拟合(反向传播,下周的课程将介绍)将会运行得更快。这与Course 1中使用的程序相同,其中数据中的特征都被规范化以具有类似的范围。
下面的过程使用到了Keras框架中的正则化层(normalization layer)。正则化包括如下步骤:
- 创建一个“正则化层”。注意,由于这层用于数据预处理,所以这层不属于你建立的模型。
- ‘适应’(adapt)数据。这将学习数据集的平均值和方差,并将这些值保存在内部。
- 正则化数据
对未来要使用训练过的模型的数据应用正则化是非常重要的。
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}")
运行结果:
Temperature Max, Min pre normalization: 284.99, 151.32
Duration Max, Min pre normalization: 15.45, 11.51
Temperature Max, Min post normalization: 1.66, -1.69
Duration Max, Min post normalization: 1.79, -1.70
复制数据来增加训练集的大小,以减少训练周期数(number of training epochs)
Xt = np.tile(Xn,(1000,1))
Yt= np.tile(Y,(1000,1))
print(Xt.shape, Yt.shape)
运行结果:
(200000, 2) (200000, 1)
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')
]
)
model.summary()
可以展示网络的基本描述。
运行结果:
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
layer1 (Dense) (None, 3) 9
layer2 (Dense) (None, 1) 4
=================================================================
Total params: 13
Trainable params: 13
Non-trainable params: 0
_________________________________________________________________
总结中显示的参数计数对应于如下所示的权重和偏置矩阵中的元素数量。
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 )
运行结果:
L1 params = 9 , L2 params = 4
来看看Tensorflow实例化的权重和偏置。权重矩阵W的形状(shape)应该是(输入特征数, 层神经元数)。而偏置b矩阵的元素个数应该和层中的单元数匹配。
- 在具有三个神经元的第一层中,我们期望W的形状是(2, 3)而b应该有3个元素。
- 在只有一个神经元的第二层中,我们期望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)
运行结果:
W1(2, 3):
[[-0.15 0.38 -0.17]
[-1.07 0.23 -0.82]]
b1(3,): [0. 0. 0.]
W2(3, 1):
[[-1.02]
[ 1.16]
[-0.88]]
b2(1,): [0.]
接下里的语句会在第二周的课程中详细说明。现在只需要简单的了解:
model.complie
语句定义损失函数,并且声明一个编译优化。model.fit
语句运行梯度下降,并将权重拟合到数据上。
运行结果:
Epoch 1/10
6250/6250 [==============================] - 5s 712us/step - loss: 0.2316
Epoch 2/10
6250/6250 [==============================] - 5s 732us/step - loss: 0.1187
Epoch 3/10
6250/6250 [==============================] - 4s 718us/step - loss: 0.0890
Epoch 4/10
6250/6250 [==============================] - 5s 726us/step - loss: 0.0216
Epoch 5/10
6250/6250 [==============================] - 5s 734us/step - loss: 0.0122
Epoch 6/10
6250/6250 [==============================] - 5s 724us/step - loss: 0.0082
Epoch 7/10
6250/6250 [==============================] - 5s 721us/step - loss: 0.0057
Epoch 8/10
6250/6250 [==============================] - 5s 722us/step - loss: 0.0039
Epoch 9/10
6250/6250 [==============================] - 4s 715us/step - loss: 0.0028
Epoch 10/10
6250/6250 [==============================] - 5s 728us/step - loss: 0.0020
<keras.callbacks.History at 0x2883f38f508>
更新权重
拟合结束后,权重已经被更新了。
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)
运行结果:
W1:
[[ 0.15 14.76 -10.85]
[ 10.33 12.24 -0.3 ]]
b1: [ 12.48 2. -11.66]
W2:
[[ 42.37]
[-44.23]
[-51.88]]
b2: [-13.21]
接下来,我们加载从此前一次训练中保存的权重。每次训练会产生略微不同的结果。下面的讨论适用于特定的解决方案。
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)
运行结果:
1/1 [==============================] - 0s 57ms/step
predictions =
[[9.63e-01]
[3.03e-08]]
Epochs and batches
在上述的compile
语句,epochs的大小被设置为10。这表明整个数据集将被在10次训练中使用。在训练过程中,你可以看到描述训练过程的输出,像下面这样:
Epoch 1/10
6250/6250 [==============================] - 6s 910us/step - loss: 0.1782
第一行,Epoch 1/10
描述模型正在运行哪一个epoch。考虑到效率问题,训练集被分为"batches"。在Tensorflow中,batch的默认大小是32。因此,我们扩展过的数据集中有200000个样本,或者说有6250个batch。第二行6250\6250[====
描述哪一个batch正在被用于训练。
为了将模型输出的概率转换为一个决策,我们对概率设置一个阈值:
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}")
运行结果
decisions =
[[1.]
[0.]]
神经网络中每一层的功能
现在来看看每个神经元在咖啡烘培好坏决策中是如何发挥作用的。我们将输入特征和每个神经元的输出绘制成图像。每个神经元都是一个逻辑回归模型,它的输出取值区间为[0, 1]。绘制出图像的阴影深度(或者说,蓝度。。)代表输出值的大小。
plt_layer(X,Y.reshape(-1,),W1,b1,norm_l)
输出结果:
这个图表明每个神经元都负责不同的“坏烘焙”区域。当烘焙温度比较低时,神经元0输出比较大的值(或者说比较高的概率),说明它负责温度较低的区域。而神经元1则负责烘焙时间短的区域。神经元2负责的区域比较复杂,首先我们知道当烘焙时间比较长时,烘焙温度理应比较短,而神经元2就负责这样的情况:烘焙温度超过了烘焙时间对应的最高烘焙温度。
值得注意的是,神经网络是经过梯度下降,自己学会这些功能的。没有人告诉它应该让三个神经元负责不同的区域。
第二层,也就是最后一层的功能,比较难可视化。我们创建一个3-d图,其中x、y、z轴分别代表前一层三个神经元的输出,可以看到它们也是分布在0到1内的。3-d图内的点则代表这一层的输出,点的颜色越蓝,说明这是一次好的烘焙的概率越接近1。
恭喜!
你已经在Tensorflow中构建了一个小型的神经网络。这个网络表明了,神经网络处理通过把决策分散到多个神经元中,来处理复杂的决策问题。