Technology

Deep Learning with Python, Third Edition

AI 读书笔记 01

Chpater 2: The mathematical building blocks of neural networks

MNIST 手写数字识别

MNIST 手写数字识别堪称深度学习领域的 Hello World,通常用于 Python 深度学习的入门教学,用它构建的最简单的神经网络模型训练与推理仅需不到 15 行关键 Python 代码(不包括 print 输出代码)即可完成。MNIST 数据集位于 keras 库中,代码如下:

from keras.datasets import mnist # 导入 mnist 数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data() # 将 mnist 的数据拆分为训练集和测试集,两者都拥有各自的图像和对应的标签。运行 train_images.shape 查看形状会得到 (60000, 28, 28),运行 test_images.shape 得到 (10000, 28, 28),两者的类型都是 uint8

import keras
from keras import layers # 用于构建训练使用的神经网络

# 构建具有两层的神经网络
model = keras.Sequential(
    [
        layers.Dense(512, activation="relu"), # 包含 512 个神经元,激活函数是 ReLU,作用是将负数变为 0,正数保持不变,同 max(0, x)。本层负责提取图像特征
        layers.Dense(10, activation="softmax"), # 包含 10 个神经元,对应 0~9 十个数字类别,激活函数是 Softmax,将上一层输出的 10 个原始数值(logits)转为概率分布,保证每个值都在 [0, 1] 之间,所有值的总和为 1
    ]
)

# 编译模型,规定优化器、损失函数和测试期间需要监控的指标
model.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"] # 此处只关注准确率,即被正确分类的图像比例
)

# 对数据进行预处理
train_images = train_images.reshape((60000, 28 * 28)) # 将数据重塑为模型期望的形状
train_images = train_images.astype("float32") / 255 # 转换数据类型为 float32,同时缩放模型,使所有数值都落在 [0, 1] 区间内
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

# 使用 fit() 在训练集上训练模型,训练过程中会输出在训练数据上的损失和准确率
model.fit(train_images, train_labels, epochs=5, batch_size=128)

# 使用训练后的模型对测试集数字的类别概率进行预测
test_digits = test_images[0:10]
predictions = model.predict(test_digits)
print(predictions[0]) # 最终输出 array([1.4771027e-07, 2.0540814e-07, 1.7365463e-05, 1.7965662e-04, 5.6883709e-10, 2.0564130e-08, 1.7420004e-10, 9.9979675e-01, 9.1168306e-07, 4.9426658e-06], dtype=float32),不同计算机可能存在差异
print(predictions[0].argmax()) # 输出 7,也就是预测的数字类别
print(predictions[0][7]) # 输出 0.99979675,也就是在索引为 7 的元素上确信度达到了约 99.98%
print(test_labels[0]) # 输出 7,也就是该图像原本的标签

# 计算模型在新数据上的性能来评估其性能
test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"test_acc: {test_acc}") # 输出 test_acc: 0.9800000190734863,不同计算机可能存在差异

特别要注意的是最后的评估代码。如果模型在训练集数据上的表现超过了在测试集数据上的表现,也就是说,在测试集数据上的预测错误率大于训练集数据,则需要警惕 过拟合(overfitting)。这个差距越大,过拟合出现的风险越高。

张量

张量(tensor)本质上就是由数字构成的多维列表,是 NumPy 中的核心内容。它的核心属性包括:

  • 秩(rank),也称为轴数(axis),可以直接理解为 Numpy 的张量有多少层大括号。例如,np.array([12, 13, 14]) 的秩为 1,np.array(12) 的秩为 0(称为标量 scalar),np.array([[12, 13], [14, 15]]) 的秩为 2。使用 ndim 查看张量的秩。
  • 形状,用 shape 查看,以元组形式表示,常见的格式如 (60000, 28, 28),这个元组的长度就是秩。例如,通常来说,单张彩色图像的秩为 3,以 (height, width, color_depth) 的秩 3 张量表示。但灰度图像没有颜色通道,因此秩为 2。当把多个彩色图像样本归于同一个数据集中,此时最左侧会加上一个样本量表示数据集中的样本总数,因此秩变为 4,也就是 (samples, height, width, color_depth)。同理,单个视频的秩为 4,包含帧数、高度、宽度和通道数,而视频数据集的秩为 5。
  • 数据类型,用 dtype 查看,常见的如 uint8、float32、float64 等。

NumPy 支持直接使用算术符号进行张量加减乘运算,相比于通过 for 循环进行逐元素加减,效率更高。

张量广播

NumPy 和 PyTorch 都支持 广播 broadcasting 技术,它允许不同形状的张量进行逐元素运算。广播的方法是从最后一个维度开始,逐个往前比较。如果相等,则继续向前比较,直到第一个维度;如果其中一个维度为 1,则该维度会被拉伸以匹配另一个张量;如果其中一个维度不存在,则先补 1 再扩展。其余情况无法广播。例如:

  1. 张量 x 维度为 (2, 3, 4),张量 y 维度为 (3, 4),则广播后为 (2, 3, 4)
  2. 张量 x 维度为 (2, 3, 4),张量 y 维度为 (1, 3, 4),则广播后为 (2, 3, 4),但数据只是在逻辑上复制,没有实际复制,因此不额外占据内存。
  3. 张量 x 维度为 (2, 3, 4, 5),张量 y 维度为 (4, 5),则广播后为 (2, 3, 4, 5)
  4. 张量 x 维度为 (2, 3, 4),张量 y 维度为 (2, 4),则由于第二位的 3 不等于 2,无法广播。

张量积

Numpy 使用 np.matmul(x, y),Python 也可直接使用 @ 运算符。只有两个向量满足条件:x.shape[1] == y.shape[0](即 x 每一行的元素数量与 y 每一列的元素数量相等)时,两者才能相乘,乘积结果是 (x.shape[0], y.shape[1])。如果其中一个张量的 ndim 大于 1,则 matmul(x, y)matmul(y, x) 的结果将会不同。对于高维张量,则首先比较第一个张量的最后一维和第二个张量的第一维,如果二者相同,则乘积就是第一个张量除最后一维外的所有维度与第二个张量最后一维的拼接。

张量重塑与转置

张量重塑,也就是通过 reshape 方法变化张量的形状,变化前和变化后的张量含有的总元素数量不变,如从 (3, 2) 变为 (2, 3)(6, 1)

转置是特殊的重塑方式,通过 transpose 方法对换行和列,例如从 (60000, 28) 转置为 (28, 60000)

梯度

梯度的英文是 gradient,作用是在每次训练后告诉模型每个参数应当如何调整,是增大值还是减小值,以让模型预测值与真实值逐渐缩小。梯度给出的调整基本上都在细微层面,在多轮训练过后,梯度趋于 0,参数逐渐停止更新。

训练五步骤

  1. 获取一批训练样本 x 及其对应的真实标签 y_true
  2. x 上运行模型以获得预测值 y_pred,称为前向传播 forward pass;
  3. 计算该数据集上的模型损失,即 y_predy_true 之间的差异;
  4. 计算损失函数关于模型参数的梯度,称为反向传播 backward pass;
  5. 将参数沿与梯度相反的方向稍作移动以降低损失。这里通常使用称为 学习率(learning rate) 的标量乘以梯度,也就是公式 W -= learning_rate * gradient,学习率用于调节梯度下降的速度,其值不能太小,否则迭代次数过多,还可能陷入局部最小值;也不能太大,否则可能会反复跳到曲线上的随机点。对于每一批数据都是随机抽取的情形,这种技术称为 随机梯度下降(stochastic gradient descent, SGD)。SGD 又分为单样本、小批量和批量梯度下降,其中的批量梯度下降是在所有可用数据上执行操作,准确率最高,但成本也最高。

动量

动量的英文是 momentum,作用是尽可能让参数不卡在局部最小值。在损失函数的值随模型参数变化而变化的函数曲线上,往往存在一些局部最小值,称为 local minimum。全局最小值则是 global minimum。在没有动量的情况下,参数更新到局部最小值时,可能会由于左右两侧的函数值都是上升导致参数更新缓慢甚至停止,无法到达全局最小值,类似于球滚动凹陷处,并且没有足够的力推动球滚出凹陷。动量的作用就是帮助球(参数)跳出这个局部最小值。因此,参数矩阵 W 的更新不仅需要考虑当前的梯度值,还要考虑之前的参数更新情况。