目录
第一章:人工神经网络基础
比较人工智能和传统机器学习
人工神经网络(Artificial Neural Network,ANN) 是一种受人类大脑运作方式启发而构建的监督学习算法。神经网络与人类大脑中神经元连接和激活的方式比较类似,神经网络接收输入并通过一个函数传递,导致随后的某些神经元被激活,从而产生输出。
有几种标准的 ANN 架构。通用近似定理认为,总是可以找到一个足够大的神经网络结构,它具有正确的权重集,可以准确地预测任何给定输入下的任何输出。这就意味着,对于给定的数据集 / 任务,我们可以创建一个架构,并可以不断调整其权重,直到 ANN 预测出我们希望它预测的内容为止。调整权重直到这种情况发生的过程称为神经网络训练过程。ANN 在大型数据集和自定义架构上能够获得成功的训练,这正是 ANN 能够在解决各种相关任务中获得突出地位的主要原因。
在本章中,我们将在一个简单的数据集上创建一个非常简单的架构,主要关注 ANN 的各种构建模块(前向、反向传播、学习率)如何帮助调整神经网络权重,以便该神经网络学习从给定的输入预测出预期的输出。我们将首先从数学上学习什么是神经网络,然后从零
开始建立一个坚实的基础,接着将学习负责训练神经网络的每个组件,并对它们进行编码。
神经网络提供了结合特征提取(手工调整)和使用这些特征进行分类 / 回归的独特优势,几乎不需要手工特征工程。只需要标记数据(例如,哪些图片是狗,哪些图片不是狗)和神经网络架构这两个子任务。它不需要人类想出分类图像的规则,这样就消除了传统技术强加给程序员的大部分负担。
人工神经网络的构建模块
人工神经网络是一系列张量(权重)和数学运算的组合,它们以一种松散的排列方式复制人脑的功能。可以将人工神经网络看作一个数学函数,输入一个或多个张量,输出一个或多个张量。连接这些输入和输出的运算排列称为神经网络的架构—我们可以根据手头
的任务对它们进行定制,也就是说,基于这个问题是否包含结构化(表格)数据或非结构化(图像、文本、音频)数据(即输入张量和输出张量的列表)。
人工神经网络由下列模块构成:
输入层:该层将自变量作为输入。
隐藏(中间)层:该层连接输入层和输出层,并对输入数据进行转换。此外,隐藏层包含节点(图 1-6 中的单元 / 圆圈),用于将其输入值修改为更高 / 更低维度的值。可以通过修改中间层节点的各种激活函数来实现更加复杂的表示功能。
输出层:该层包含了输入变量期望产生的值。
根据上述内容,神经网络的典型结构如图 1-6 所示。
输出层中的节点数量(图 1-6 中的圆圈)取决于手头的任务以及试图预测的是连续变量还是分类变量。如果输出是一个连续变量,则输出层只有一个节点。如果输出具有 m 个可能的类别,那么输出层中将有 m 个节点。
术语深度学习指的是具有更多的隐藏层。在神经网络必须理解一些诸如图像识别等复杂事情的时候,通常需要更多的隐藏层。
实现前向传播
为了对前向传播的工作原理有一个深入的了解,我们训练一个简单的神经网络,其中神经网络的输入是(1,1),对应的(期望)输出是0。这里将基于这个单一的输入 – 输出对找到神经网络的最优权重。然而,你应该注意到,事实上将有成千上万的数据点用于训练 ANN。
注意,如果不在隐藏层中应用非线性激活函数,那么无论存在多少隐藏层,神经网络从输入到输出都将成为一个巨大的线性连接。
计算隐藏层的值
总结:输入数据(通常是张量,浮点类型)乘以权重参数加上偏置项的结果通过非线性激活函数即可完成隐藏层中一个隐藏单元的值的计算。
激活函数
激活函数有助于建模输入和输出之间的复杂关系。
一些常用的激活函数计算公式如下(其中 x 为输入):
计算输出层的值
总结:使用隐藏层值(被激活函数激活之后的值)和权重值的乘积加上偏置项后的和(因为隐藏层中的隐藏单元有多个)来计算输出的值。
计算损失值
损失值(或者称为损失函数)是需要在神经网络中进行优化的值。
为了正确理解损失值是如何计算的,我们看看如下两种情况:
分类变量预测;
连续变量预测。
计算连续变量预测的损失:
当变量连续时,损失值通常是实际值和预测值之差平方的平均值,也就是说,我们通过改变与神经网络相关的权重值来尽量减小均方误差。均方误差值的计算公式如下:
在上式中, yi 为实际输出; yˆi 是神经网络计算出来的预测值(其权重以 θ 的形式存储),其中输入为 xi , m 为数据集的行数。
关键的结论应该是,对于每个唯一的权重集,神经网络将会预测出相应的损失值,我们需要找到损失值为零(或者,在现实场景中,尽可能接近零)的黄金权重集。
说白了,我感觉意思就是如果最后我们的ANN输出的只有一个值,那么损失函数就应该选择均方误差(因为它就只会算出一个值嘛),而分类变量预测则相反,在下面会说到。
计算分类变量预测的损失:
对于预测变量是离散值(即变量中只有几个类别)的情形,通常使用分类交叉熵损失函数。当预测变量只有两个不同取值的时候,损失函数是二元交叉熵。
二元交叉熵的计算公式如下:
yi 为实际的输出值,pi 为预测的输出值,m 为数据点总数。
yi 为输出的实际值,pi 为输出的预测值, m 为数据点总数, C 为类别总数。
前向传播的代码
import numpy as np
# 这个函数 feed_forward 实现了一个简单的前向传播过程,用于一个单隐藏层的神经网络
def feed_forward(inputs, outputs, weights):
"""
:param inputs: 输入数据
:param outputs: 实际输出数据
:param weights: 权重列表,包含了神经网络当中的所有权重和偏置项
weights[0]:连接输入层和隐藏层的权重矩阵
weights[1]:隐藏层的偏置向量
weights[2]:连接隐藏层和输出层的权重矩阵
weights[3]:输出层的偏置向量
:return: 函数返回 mean_square_error,即整个批次数据的均方误差,用于评估神经网络的性能和用于优化训练过程中的权重和偏置。
"""
# 首先,计算隐藏层的加权输入 pre_hidden,通过矩阵乘法 np.dot(inputs, weights[0]) 加上偏置 weights[1] 得到:
pre_hidden = np.dot(inputs, weights[0]) + weights[1]
# 将加权输入 pre_hidden 经过激活函数处理,这里使用了 sigmoid 函数 1/(1+np.exp(-pre_hidden)),
# 得到隐藏层的输出 hidden,形状为 (batch_size, hidden_dim):
hidden = 1/(1+np.exp(-pre_hidden))
# 接着,计算输出层的加权输入 pred_out,
# 同样通过矩阵乘法 np.dot(hidden, weights[2]) 加上偏置 weights[3] 得到,这里需要注意修正过的索引:
pred_out = np.dot(hidden, weights[2]) + weights[3]
# 最后,计算预测输出与实际输出之间的均方误差 mean_square_error,
# 使用 np.mean(np.square(pred_out - outputs)) 计算整个批次的误差,并返回:
mean_square_error = np.mean(np.square(pred_out - outputs))
return mean_square_error
此时当数据向前通过网络时,就可以得到均方误差值。
softmax
与其他激活函数不同,softmax 在一组值上执行。这样做通常是为了确定某个输入属于给定场景中m个可能输出类别中某个类别的概率。假设需要分类的图像具有 10 个可能的类别(对应数字 0 到 9)。此时就有 10 个输出值,每个输出值代表输入图像属于这 10 个类别中某一个类别的概率。
softmax 激活用于为输出中的每个类提供一个概率值,计算代码如下:
def softmax(x):
return np.exp(x)/np.sum(np.exp(x))
注意输入 x 上面的两个操作 —np.exp 将使所有值为正,除以所有这些指数的 np.sum(np.exp(x)) 可以将所有值限制在 0 和 1 之间。这个范围与事件发生的概率一致。这就是我们所说的返回一个概率向量。
损失函数的代码
损失值(在神经网络训练过程中被最小化)通过更新权重值被最小化。确定合理的损失函数是建立可靠神经网络模型的关键。构建神经网络时,通常使用的损失函数如下:
目前已经学习了前向传播,以及构成前向传播的各种组件,如权重初始化、与节点相关的偏置项、激活和损失函数。在下一节中,我们将学习反向传播(backpropagation),这是一种调整权重的技术,使损失值尽可能小。
实现反向传播
在前向传播中,将输入层连接到隐藏层,隐藏层再连接到输出层。在第一次迭代中,随机初始化权重,然后计算这些权重造成的损失。在反向传播中,我们采用相反的方法。从前向传播中得到的损失值开始,更新网络的权重,使损失值尽可能小。
我们执行以下步骤来减小损失值:
如果在整个数据集上执行n次上述步骤(完成前向传播和反向传播),就会实现对模型n轮(epoch)的训练。
由于一个典型的神经网络包含数千或数百万(如果不是数十亿)个权重,改变每个权重的值,并检查损失是增加还是减少并不是最优的做法。上述列表中的核心步骤是权重变化时对“损失变化”的度量。正如你可能在微积分中学习过的那样,这个度量和计算权重相关的损失梯度是一样的。在下一节讨论反向传播链式法则时,将有更多关于利用微积分中的偏导数来计算与权重相关的损失梯度的内容。
在本节中,我们将通过每次对一个权重进行少量更新的方式来实现梯度下降,这在本节开始部分已经进行了详细介绍。不过,在实现反向传播之前,需要先了解神经网络的另一个细节:学习率。
直观地说,学习率有助于在算法中建立信任。例如,在决定权重更新大小的时候,可能不会一次性改变权重值,而是进行较慢的更新。
模型通过学习率获得了稳定性,将在 1.6 节中具体讨论学习率如何有助于提高稳定性。
通过更新权重来减少误差的整个过程称为梯度下降。
随机梯度下降是最小化前述误差的一种具体实现方法。如前所述,梯度表示差异(即权重值被少量更新时损失值的差异),下降表示减少。随机表示对随机样本的选择,并在此基础上做出决定。
除了随机梯度下降之外,还有许多其他类似的优化器可以帮助最小化损失值。下一章将讨论这些不同的优化器。
在接下来的两节中,我们将学习如何使用 Python 从头开始编写反向传播算法的代码,并简要讨论如何使用链式法则进行反向传播。
梯度下降的代码
在上一节的前馈网络代码的基础上,将每个权重和偏置项增加一个非常小的量(0.0001),并对每个权重和偏置项更新一次,计算总体误差损失的平方值。
# 这段代码用于实现简单的梯度下降算法来更新神经网络的权重
# update_weights 函数的目的是通过计算每个权重的梯度来更新神经网络的权重,以减少前向传播过程中的损失函数值。
def update_weights(inputs, outputs, weights, lr):
"""
:param inputs: 输入数据
:param outputs: 实际输出数据
:param weights: 当前的权重和偏置项
:param lr: 学习率,用于控制每次更新的步长大小
:return: updated_weights:更新后的权重矩阵和偏置向量。
original_loss:使用原始权重计算的损失值,用于评估权重更新的效果。
"""
original_weights = deepcopy(weights)
temp_weights = deepcopy(weights)
updated_weights = deepcopy(weights)
original_loss = feed_forward(inputs, outputs, original_weights)
# 遍历 original_weights 中的每一层和每一个权重元素,
# 使用 np.ndenumerate 获取权重矩阵中每个元素的索引。
for i, layer in enumerate(original_weights):
for index, weight in np.ndenumerate(layer):
temp_weights = deepcopy(weights)
# 对每个权重元素,将 temp_weights 中对应位置的权重增加一个小的增量 0.0001,
# 这里使用一个小的数值增量来近似计算梯度。
temp_weights[i][index] += 0.0001
# 使用修改后的 temp_weights 计算新的损失 _loss_plus。
_loss_plus = feed_forward(inputs, outputs, temp_weights)
# 计算权重元素的梯度 grad,通过 (新损失 - 原始损失) / 增量 计算得到。
grad = (_loss_plus - original_loss) / (0.0001)
# 根据梯度和学习率 lr 更新 updated_weights 中对应权重的值。
updated_weights[i][index] -= grad*lr
return updated_weights, original_loss
神经网络的另一个参数是在计算损失值时需要考虑的批大小(batch size)。
前面使用所有数据点来计算损失(均方误差)值。然而在实践中,当有成千上万(或者在某些情况下数百万)的数据点时,使用较多数据点计算损失值,其增量贡献将遵循收益递减规律,因此我们将使用比数据点总数要小得多的批大小进行模型训练。在一轮的训练中,每次使用一个批次数据点进行梯度下降(在前向传播之后),直到用尽所有的数据点。
训练模型时典型的批大小是 32 和 1024 之间的任意数。
在本节中,我们了解了当权重值发生少量变化时,如何基于损失值的变化更新权重值。在下一节中,将学习如何在不计算梯度的情况下更新权重。
使用链式法则实现反向传播
很简单:上面我们说过在前向网络计算过程中,会通过一系列的计算公式拿到最后的损失值:
上图已经写出了所有的计算公式,可以计算权重变化对损失值(C)变化的影响,具体计算公式如下:
这称为链式法则。它本质上是通过一系列的微分获得需要的微分。
现在我们对权重参数 w11 进行一个值的微调,那么就会反向传播影响到最后的 C 的值,也就是损失值(从上图可以形象的看出这样的反向传播是一个链式的影响)。
另外从数学上说,求偏微分的本质简单理解也就是一个函数中的某一个自变量产生的微小变化(极微小)对于该函数的函数值所产生的影响,也可以说是函数变化率的大小。
为了方便理解,可以回忆一下导数的定义:
import numpy as np
from chapter_01.Forward_Propagation import *
import matplotlib.pyplot as plt
# 1、定义数据集
x = np.array([[1, 1]])
y = np.array([[0]])
# 2、随机初始化权重和偏置项
# 隐藏层中有 3 个单元,每个输入节点与每个隐藏层单元相连。
# 因此,总共有 6 个权重值和 3 个偏置项,其中 1 个偏置和 2 个权重(2 个权重来自 2个输入节点)对应每个隐藏单元。
# 另外,最后一层有 1 个单元连接到隐藏层的 3 个单元。因此,输出层的值由 3 个权重和 1 个偏置项决定。
# 这段代码是用 Python 和 NumPy 定义了一个简单的神经网络的权重和偏置项
# W 是一个包含四个元素的列表,每个元素分别对应神经网络中的不同层的权重矩阵或偏置向量。
W = [
# 第一个元素:输入层到隐藏层的权重矩阵,.T 表示进行转置操作,数组中的值是初始化的权重值,类型为 np.float32
np.array([[-0.0053, 0.3793], [-0.5820, -0.5204], [-0.2723, 0.1896]], dtype=np.float32).T,
# 第二个元素:隐藏层的偏置向量
np.array([-0.0140, 0.5607, -0.0628], dtype=np.float32),
# 第三个元素:隐藏层到输出层的权重矩阵
np.array([[0.1528, -0.1745, -0.1135]], dtype=np.float32).T,
# 第四个元素:输出层的偏置项
np.array([-0.5516], dtype=np.float32)
]
# 3、更新超过100轮的权重,并获取损失值和更新的权重值:
losses = []
for epoch in range(100):
W, loss = update_weights(x, y, W, 0.01)
losses.append(loss)
# 4、绘制损失值的图像:
plt.plot(losses)
plt.title('Loss over increasing number of epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss value')
plt.show()
# 5、 一旦有了更新的权重,就可以通过将输入传递给网络进行预测,并计算输出值
# 这里的代码是模拟了将输入数据传递给“神经网络”进行预测概率的行为
pre_hidden = np.dot(x, W[0]) + W[1]
hidden = 1/(1+np.exp(-pre_hidden))
pred_out = np.dot(hidden, W[2]) + W[3]
# 输出 -0.0174781
print(pred_out)
运行效果如下:
可以看出来,损失从 0.33 开始,稳步下降到 0.0001 左右。
这表明,权重是根据输入 – 输出数据进行调整的,当给定输入时,就可以期望它预测出与损失函数进行比较的输出。
理解学习率的影响
使用小学习率不会导致权重值大幅变化的原因是我们限制了权重更新的数值等于梯度 × 学习率,小的学习率本质上导致了权重的少量更新。然而,当学习率较大时,权重值的更新量也较大,之后损失的变化(权重值更新量较小时)非常小,使得权重值无法达到最优值。
总结:一般来说,学习率越小越好。这样,模型可以慢慢地学习,会有利于将权重调整到最优值。典型的学习率参数值范围是 0.0001 到 0.01。
总结神经网络的训练过程
训练神经网络是一个为神经网络架构构造最优权重的过程,通过重复在给定学习率下的前向传播和反向传播这两个关键步骤来实现。
在前向传播中,对输入数据施加一组权重,把它传递给隐藏层,并执行非线性激活函数实现隐藏层的输出,隐藏层到输出层则是使用隐藏层节点的值与另一组权重值相乘来估计输出值,最后计算出给定权重集对应的总体损失。对于第一次前向传播,权重值被随机初始化。
在反向传播中,通过在一个方向上调整权重来减小损失值(误差),以减少总体损失。此外,权重更新的大小是梯度乘以学习率。
前向传播和反向传播的过程不断重复,直到达到尽可能少的损失。这就意味着,在训练结束时,神经网络调整了它的权重θ,以便预测出希望的预测输出值。在上述例子中,网络模型经过训练后,当 {1, 1} 作为输入时,更新后的网络预测输出值为 0。
第二章:PyTorch基础
PyTorch 提供了多个辅助构建神经网络的功能:使用高级方法对各种组件进行抽象化处理,并提供了张量对象,利用 GPU 更快地训练神经网络。
PyTorch 张量
例如,可以将一幅彩色图像看作像素值的三维张量,因为一幅彩色图像由 height×width×3 像素组成—其中这三个通道对应于 RGB 通道。类似地,可以将灰度图像看成二维张量,因为它由 height×width 像素组成。
初始化张量
张量在很多方面都很有用,除了可以作为图像的基本数据结构之外,还有一个更加突出的用途,就是可以利用张量来初始化连接神经网络不同层的权重。
import numpy as np
import torch
# 导入 PyTorch 并通过调用 torch.tensor 在列表中初始化一个张量:
x = torch.tensor([[1, 2]])
y = torch.tensor([[1], [2]])
# 接下来,访问张量对象的形状和数据类型:
# torch.Size([1, 2])
print(x.shape)
# torch.Size([2, 1])
print(y.shape)
# torch.int64
print(x.dtype)
# 张量内所有元素的数据类型是相同的。
# 这就意味着如果一个张量包含不同数据类型的数据(比如布尔、整数和浮点数),
# 那么整个张量被强制转换为一种最为通用的数据类型:
x = torch.tensor([False, 1, 2.0])
# tensor([0., 1., 2.])
print(x)
# 类似于 NumPy,可以使用内置函数初始化张量对象
# 注意,这里画出的张量和神经网络权重之间的相似之处现在开始显现了:
# 这里初始化张量,使它们能够表示神经网络的权重初始化。
# 生成一个张量对象,其有三行四列,填充0
torch.zeros((3, 4))
# 生成一个张量对象,它有三行四列,填充1
torch.ones((3, 4))
# 生成值介于0和10之间(包括小值不包括大值)的三行四列
torch.randint(low=0, high=10, size=(3, 4))
# 生成具有 0 和 1 之间随机数的三行四列
torch.rand(3, 4)
# 生成数值服从正态分布的三行四列
torch.randn(3, 4)
# 直接使用torch.tensor(<Numpy-array>) 将Numpy数组转换位 Torch 张量
x = np.array([[10, 20, 30], [2, 3, 4]])
y = torch.tensor(x)
# 输出:<class 'numpy.ndarray'> <class 'torch.Tensor'>
print(type(x), type(y))
张量运算
与 NumPy 类似,你可以在张量对象上执行各种基本运算。与神经网络运算类似的是输入数据与权重之间的矩阵乘法,添加偏置项,并在需要的时候重塑输入数据或权重值。
import torch
# 可以使用下列代码将 x 中所有元素乘以 10:
x = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]])
# tensor([[10, 20, 30, 40],
# [50, 60, 70, 80]])
print(x * 10)
# 可以使用下列代码将 10 加到 x 中的元素,并将得到的张量存储到 y 中
x = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]])
y = x.add(10)
# tensor([[11, 12, 13, 14],
# [15, 16, 17, 18]])
print(y)
# 可以使用下列代码重塑一个张量:
y = torch.tensor([2, 3, 1, 0])
print(y.shape)
print(y)
# y.shape == (4)
y = y.view(4, 1)
print(y)
print(y.shape)
# y.shape == (4, 1)
# 重塑张量的另一种方法是使用 squeeze 方法,提供我们想要移除的指标轴。
# (squeeze() 方法,这个方法用于从张量中挤压(去除)维度为 1 的轴。)
# 注意,这只适用于要删除的轴在该维度中只有一个项的场合:
# 创建一个形状为 (10, 1, 10) 的张量 x,即三维张量
x = torch.randn(10, 1, 10)
# 使用 squeeze() 方法挤压掉维度为 1 的轴,注意该方法传递的是元组x的下标索引号
z1 = torch.squeeze(x, 1)
# 也可以直接调用 x.squeeze(1) 来实现相同的效果,1表示元组x的下标
z2 = x.squeeze(1)
# 使用 assert 语句检查两个张量 z1 和 z2 中的所有元素是否相等
assert torch.all(z1 == z2)
# 打印输出张量的形状信息
print('Squeeze: \n', x.shape, z1.shape)
# 在 PyTorch 中,unsqueeze() 方法和使用 [None] 索引是用来增加张量维度的两种方法。
# 它们的作用是在指定的位置增加一个新的维度
# 与 squeeze 相反的是 unsqueeze,
# 这意味着给矩阵增加一个维度,可以使用下列代码实现:
x = torch.randn(10, 10)
print(x.shape)
# torch.size(10,10)
# 使用 unsqueeze(0) 方法在第 0 维度(最外层维度)增加一个维度, unsqueeze方法内传递的同样是元组的下标索引
z1 = x.unsqueeze(0)
# 输出 torch.size(1,10,10)
print(z1.shape)
# 使用 [None] 索引方式增加维度,效果与 unsqueeze(0) 相同
# x[None] 等效于 unsqueeze(0),在第 0 维度增加一个维度,形状变为 (1, 10, 10)。
# x[:, None] 在第 1 维度增加一个维度,形状变为 (10, 1, 10)。
# x[:, :, None] 在第 2 维度增加一个维度,形状变为 (10, 10, 1)。
z2, z3, z4 = x[None], x[:, None], x[:, :, None]
print(z2.shape, z3.shape, z4.shape)
# 可以使用下列代码实现两个不同张量的矩阵乘法:
x = torch.tensor([[1, 2, 3, 4],
[5, 6, 7, 8]])
# 输出 tensor([[11],
# [35]])
print(torch.matmul(x, y))
# 或者,也可以使用 @ 运算符实现矩阵乘法:
print(x@y)
# 在 PyTorch 中,使用 torch.cat() 函数可以实现张量的拼接(连接)操作,
# 类似于 NumPy 中的 np.concatenate() 函数。这个函数可以在指定的轴上将多个张量拼接成一个更大的张量。
x = torch.randn(10, 10, 10)
z = torch.cat([x, x], axis=0)
# Cat axis 0: torch.Size([10, 10, 10]) torch.Size([20, 10, 10])
print(' Cat axis 0: ', x.shape, z.shape)
z = torch.cat([x, x], axis=1)
# Cat axis 1: torch.Size([10, 10, 10]) torch.Size([10, 20, 10])
print('Cat axis 1:', x.shape, z.shape)
# 可以使用下列代码提取张量中的最大值:
# torch.arange(25) 创建了一个包含 0 到 24 的整数序列的一维张量
# reshape(5, 5) 将这个一维张量重新形状为一个 5x5 的二维张量 x
x = torch.arange(25).reshape(5, 5)
# x.max() 是张量 x 的方法,用于计算张量中所有元素的最大值。
# 在这个例子中,由于张量 x 包含了从 0 到 24 的整数,因此最大值为 24。
print('Max:', x.shape, x.max())
# max 和 min 不是很明白是什么意思...
# 可以从存在最大值的行索引中提取最大值:
# 在 PyTorch 中,当您调用 max() 方法并传递 dim 参数时,可以沿着指定的维度计算张量的最大值。
# 这种操作可以帮助您在多维张量中沿着指定维度找到每列(或者其他维度)的最大值及其对应的索引。
x.max(dim=0)
# torch.return types.max(values=tensor([20, 21, 22, 23, 24]),
# indices=tensor([4, 4, 4, 4, 4]))
# 注意,在前面的输出中,获取的是第 0 号维度上的最大值,即张量在行上的最大值。
# 因此,所有行上的最大值都是第 4 个索引中出现的值,所以 indices 的输出也都是 4。
# 此外,.max 返回最大值和最大值的位置(argmax)。
m, argm = x.max(dim=1)
print('Max in axis 1:\n', m, argm)
# Max in axis 1: tensor([ 4, 9, 14, 19, 24])
# tensor([4, 4, 4, 4, 4])
# min 运算与 max 运算完全相同,但在适合的情况下返回最小值和最小值的位置(arg minimum)。
# 置换一个张量对象的维数:
x = torch.randn(10, 20, 30)
z = x.permute(2, 0, 1)
print('Permute dimensions:', x.shape, z.shape)
# Permute dimensions: torch.Size([10, 20, 30])
# torch.Size([30, 10, 20])
# 注意当对原始张量进行排列时,张量的形状就会发生改变。
因为本书很难涵盖所有可用的运算,所以重要的是要知道,你可以使用几乎与 NumPy 相同的语法在 PyTorch 中执行几乎所有的 NumPy 运算。标准的数学运算,如 abs、add、argsort、ceil、floo、sin、cos、tan、sum、cumprod、diag、eig、exp、log、
log2、log10、mean、median、mode、resize、round、sigmoid、softmax、square、sqrt、svd 和 transpose 等,均可以直接在任何有轴或没有轴的张量上被调用。你总是可以运行 dir(torch.Tensor) 来查看所有可能的 Torch 张量方法,并通过 help(torch.tensor.<method>) 来查看关于该方法的官方帮助和相关文档。
接下来,我们将学习如何利用张量在数据上执行梯度计算,这是神经网络执行反向传播的一个关键点。
张量对象的自动梯度
PyTorch 的张量对象自带了计算梯度的内置功能。
import torch
# 定义一个张量对象,并指定要为张量对象计算梯度:
x = torch.tensor([[2., -1.], [1, 1.]], requires_grad=True)
# 在上述代码中,requires_grad 参数指定要为张量对象计算梯度。
# 这意味着后续可以通过自动求导功能计算相对于这个张量的梯度。
print(x)
# 接下来,定义计算输出的方式,在这个特定的例子中,输出是所有输入的平方和:
# x.pow(2) 对张量 x 中的每个元素进行平方运算。
# .sum() 对平方后的张量元素进行求和操作,得到标量值,即所有元素的平方和
# 输出 out 为 7
out = x.pow(2).sum()
# 可以通过对某个值调用 backward() 方法来计算该值的梯度
# out.backward() 调用了自动求导的 backward() 方法,
# 用来计算 out 相对于所有 requires_grad=True 的张量的梯度。
out.backward()
# 现在可以得到 out 关于 x 的梯度
# 由于 out 是通过对 x 的平方和操作得到的,梯度 x.grad 将会是 out 相对于 x 的梯度。
print(x.grad)
运行结果如下:
具体原因解释如下:
PyTorch 的张量较 Numpy 的 ndarrays 的优势
在前文计算权重值的时候,对每个权重都进行了少量的改变,并考察其对减少总损失值的影响。注意到,在同一次迭代中,基于某个权重更新的损失计算并不影响其他权重更新的损失计算。因此,如果每个权重更新分别由不同的内核并行完成,而不是按顺序更新权重,则可以优化这个计算过程。在这种情况下,GPU 非常有用,因为与 CPU(通常情况下,CPU 可能有≤ 64 个内核)相比,GPU 由数千个内核组成。
与 NumPy 相比,Torch 张量对象被优化为与 GPU 一起工作。
使用 PyTorch 构建神经网络
前一章中学习了从零开始构建神经网络,其中神经网络的组件如下:
为了能够理解使用 PyTorch 实现神经网络,这里将解决一个简单的问题—两个数字相加。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.optim import SGD
# 定义输入值(x)和输出值(y):
# 注意:输入和输出是一个列表的列表,其中输入列表中的值之和就是输出列表中的值
x = [[1, 2], [3, 4], [5, 6], [7, 8]]
y = [[3], [7], [11], [15]]
# 将输入列表转换成张量对象:
# 将张量对象转换为浮点对象。
# 将张量对象作为浮点数或长整数是一个良好的实践,因为它们将与十进制值(权重)相乘。
X = torch.tensor(x).float()
Y = torch.tensor(y).float()
device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
# 定义神经网络架构:
# 创建一个类(MyNeuralNet),可以使用它构建神经网络架构。
# 在使用模块创建模型架构的时候,从 nn.Module 继承是强制性的,因为它是所有神经网络模块的基类:
class MyNeuralNet(nn.Module):
# 在该类中,使用 __init__ 方法初始化神经网络的所有组件。
# 必须调用 super().__init__() 来确保类继承 nn.Module:
def __init__(self):
super().__init__()
# 使用上述代码,通过适当指定 super().__init__(),就可以利用 nn.Module 中所有事先编写好的预制功能。
# 这些组件将在 init 方法中进行初始化,并将用于 MyNeuralNet 类中的多种不同方法。
# 定义神经网络的层:
self.input_to_hidden_layer = nn.Linear(2, 8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8, 1)
# 前述代码指定了神经网络的所有层—线性层(self.input_to_hidden_layer),
# 然后是 ReLU 激活(self.hidden_layer_activation),
# 最后是线性层(self.hidden_to_output_layer)。
# 注意,层数和激活的选择目前是任意的。我们将在下一章中更详细地了解层中单元的数量和层激活的影响。
# 在定义了神经网络组件之后,定义网络前向传播时就可以将这些组件连接起来:
# 注意:必须使用 forward 作为函数名,因为 PyTorch 保留了这个函数作为执行前向传播的方法。
# 使用任何其他名称都会引发错误。
def forward(self, x):
x = self.input_to_hidden_layer(x)
x = self.hidden_layer_activation(x)
x = self.hidden_to_output_layer(x)
return x
# 到目前为止,我们已经构建了神经网络模型架构,下一步将检查权重值的随机初始化。
# 你可以通过下列步骤获取每个组件的初始权重:
# 首先定义 MyNeuralNet 类对象的一个实例,并将其注册到 device:
mynet = MyNeuralNet().to(device)
# 定义用于最优化的损失函数。鉴于预测的是连续输出,这里将优化均方误差
# 其他重要的损失函数如下:
# CrossEntropyLoss(多项分类);
# BCELoss(二元分类的二元交叉熵损失)
loss_func = nn.MSELoss()
# 通过将输入值传递给 neuralnet 对象,然后计算给定输入的 MSELoss,
# 就可以计算出神经网络的损失值:
_Y = mynet(X)
loss_value = loss_func(_Y, Y)
print(loss_value)
# 在上述代码中,mynet(X) 在神经网络获得输入值时计算输出值。
# 此外,loss_func函数用于计算神经网络预测(_Y)和实际值(Y)对应的 MSELoss 值。
# 需要注意的是,在计算损失时,总是先发送预测,然后发送真实数据。这是一个PyTorch 约定。
# 定义了损失函数之后,下面将定义试图减少损失值的优化器。
# 优化器的输入是神经网络对应的参数(权重与偏置项)和更新权重时的学习率
# 对于这个实例,将考虑随机梯度下降
# 从 torch.optim 模块导入 SGD 方法,然后将神经网络对象(mynet)和学习率
# (lr)作为参数传递给 SGD 方法:
opt = SGD(mynet.parameters(), lr=0.001)
# 在下面的例子中,将在总共 50 轮执行权重更新过程。
# 此外,在列表 loss_history 中的每轮中存储损失值:
loss_history = []
for _ in range(50):
opt.zero_grad()
loss_value = loss_func(mynet(X), Y)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
# 遍历损失值列表,看是不是越来越小:
for loss in loss_history:
print(loss)
# 正如预期的那样,损失值随着轮数的增加而减少。
#------------------- 下面的内容都是演示使用,和神经网络的搭建没有关系 -------------------------
# 可以通过下列方式获取每一层的权重和偏置项:
# 注意:权重的输出值每一次都与前面的不同,因为神经网络每次都使用随机值进行初始化。
# print(mynet.input_to_hidden_layer.weight)
#
# 可以使用下列代码获得神经网络的所有参数:
# parameters方法返回一个生成器对象。
print(mynet.parameters())
# 通过循环遍历生成器,可以得到如下参数
for par in mynet.parameters():
print(par)
# 该模型将这些张量注册为特殊的对象,以保持对前向传播和反向传播的跟踪。
# 当在 __init__ 方法中定义任何 nn 层时,它将自动创建相应的张量并进行注册。
# 可以通过输出来理解上述代码中函数 nn.Linear 方法完成的功能:
print(nn.Linear(2, 7))
# 在上述代码中,线性方法有 2 个输入值,7 个输出值,还有一个与之相关的偏置项参数
# 在一轮中一起执行所有要做的步骤:
# ❍ 计算给定输入和输出所对应的损失值。
# ❍ 计算每个参数对应的梯度。
# ❍ 根据每个参数的学习率和梯度更新权重。
# ❍ 一旦权重被更新,就要确保在下轮计算梯度之前刷新上一步计算的梯度:
opt.zero_grad() # flush the previous epoch's gradients
loss_value = loss_func(mynet(X), Y) # compute loss
loss_value.backward() # perform back-propagation
opt.step() # update the weights according to the gradients
数据集、数据加载器和批大小
目前,神经网络还没有考虑到的一个超参数是批大小。批大小是指用于计算损失值或更新权重的数据点的数量。
这个超参数在有数百万个数据点的情况下特别有用,而将所有数据点用于一个权重的更新不是最佳的情形,因为内存不能存储这么多信息。另外,一个样本可以代表足够多的数据。批大小有助于获取具有足够代表性的多个数据样本,但不一定能 100% 代表全部数据。
在本节中,我们将提出一种方法来指定在计算权重梯度时要考虑的批大小,用于更新权重,进而用于计算更新的损失值:
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
from torch.optim import SGD
x = [[1, 2], [3, 4], [5, 6], [7, 8]]
y = [[3], [7], [11], [15]]
X = torch.tensor(x).float()
Y = torch.tensor(y).float()
device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
# 实例化一个数据集类 MyDataset:
# 在 MyDataset 类中,存储信息每次获取一个数据点,
# 以便可以将一批数据点捆绑在一起(使用 DataLoader),并通过一个前向和一个反向传播发送,以更新权重:
class MyDataset(Dataset):
# 定义一个 __init__ 方法,用于接收输入和输出对,并将它们转换为 Torch 浮点对象:
def __init__(self, x, y):
self.x = torch.tensor(x).float()
self.y = torch.tensor(y).float()
# 指定输入数据集的长度(__len__):
def __len__(self):
return len(self.x)
# 最后,用 __getitem__ 方法获取特定的行:
def __getitem__(self, ix):
return self.x[ix], self.y[ix]
# 在上述代码中,ix 指的是需要从数据集中获取的行的索引
# 创建已定义类的实例:
ds = MyDataset(X, Y)
# 通过 DataLoader 传递之前定义的数据集实例,
# 获取原始输入和输出张量对象中数据点的 batch_size:
dl = DataLoader(ds, batch_size=2, shuffle=True)
# 在上述代码中,还指定从原始输入数据集(ds)中获取两个数据点(通过batch_size=2)的一个随机样本(通过 shuffle=True)。
# 为了从 dl 中获取批数据,需要进行如下循环(每次从x和y中取两个数据,然后是随机抽取的):
for x, y in dl:
print(x, y)
# 现在,按照前文的定义来定义神经网络类:
class MyNeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.input_to_hidden_layer = nn.Linear(2, 8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8, 1)
def forward(self, x):
x = self.input_to_hidden_layer(x)
x = self.hidden_layer_activation(x)
x = self.hidden_to_output_layer(x)
return x
# 接下来,定义模型对象(mynet)、损失函数(loss_func)和优化器(opt),
# 这与前文所定义的一样:
mynet = MyNeuralNet().to(device)
loss_func = nn.MSELoss(mynet(X), Y)
opt = SGD(mynet.parameters(), lr=0.001)
# 最后,循环遍历各批数据点以最小化损失值,正如前一节第 6 步中所做的那样
loss_history = []
for _ in range(50):
for data in dl:
x, y = data
opt.zero_grad()
loss_value = loss_func(mynet(X), Y)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
# 虽然上述代码看起来与我们在前一部分中学习的代码非常相似,但与前一部分中权重更新的次数相比,
# 我们在每轮中执行的权重更新次数是 2X,因为本节的批大小是2,而前一部分的批大小是 4(数据点的总数)。
预测新的数据点
我们在前文中学习了如何在已知数据点上拟合模型。这一节将学习如何使用前文已训练模型 mynet 中定义的前向传播方法来预测新的数据点。下面将从上一节构建的代码继续:
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
from torch.optim import SGD
x = [[1, 2], [3, 4], [5, 6], [7, 8]]
y = [[3], [7], [11], [15]]
X = torch.tensor(x).float()
Y = torch.tensor(y).float()
device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
# 实例化一个数据集类 MyDataset:
# 在 MyDataset 类中,存储信息每次获取一个数据点,
# 以便可以将一批数据点捆绑在一起(使用 DataLoader),并通过一个前向和一个反向传播发送,以更新权重:
class MyDataset(Dataset):
# 定义一个 __init__ 方法,用于接收输入和输出对,并将它们转换为 Torch 浮点对象:
def __init__(self, x, y):
self.x = torch.tensor(x).float()
self.y = torch.tensor(y).float()
# 指定输入数据集的长度(__len__):
def __len__(self):
return len(self.x)
# 最后,用 __getitem__ 方法获取特定的行:
def __getitem__(self, ix):
return self.x[ix], self.y[ix]
# 在上述代码中,ix 指的是需要从数据集中获取的行的索引
# 创建已定义类的实例:
ds = MyDataset(X, Y)
# 通过 DataLoader 传递之前定义的数据集实例,
# 获取原始输入和输出张量对象中数据点的 batch_size:
dl = DataLoader(ds, batch_size=2, shuffle=True)
# 在上述代码中,还指定从原始输入数据集(ds)中获取两个数据点(通过batch_size=2)的一个随机样本(通过 shuffle=True)。
# 为了从 dl 中获取批数据,需要进行如下循环(每次从x和y中取两个数据,然后是随机抽取的):
for x, y in dl:
print(x, y)
# 现在,按照前文的定义来定义神经网络类:
class MyNeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.input_to_hidden_layer = nn.Linear(2, 8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8, 1)
def forward(self, x):
x = self.input_to_hidden_layer(x)
x = self.hidden_layer_activation(x)
x = self.hidden_to_output_layer(x)
return x
# 接下来,定义模型对象(mynet)、损失函数(loss_func)和优化器(opt),
# 这与前文所定义的一样:
mynet = MyNeuralNet().to(device)
loss_func = nn.MSELoss()
opt = SGD(mynet.parameters(), lr=0.001)
# 最后,循环遍历各批数据点以最小化损失值,正如前一节第 6 步中所做的那样
loss_history = []
for _ in range(1000):
for data in dl:
x, y = data
opt.zero_grad()
loss_value = loss_func(mynet(X), Y)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
# 虽然上述代码看起来与我们在前一部分中学习的代码非常相似,但与前一部分中权重更新的次数相比,
# 我们在每轮中执行的权重更新次数是 2X,因为本节的批大小是2,而前一部分的批大小是 4(数据点的总数)。
# ----------- 下面将学习如何使用前文已训练模型 mynet 中定义的前向传播方法来预测新的数据点 -----------
# 1、创建用于测试模型的数据点:
val_x = [[10, 11]]
# 2. 将新数据点转换为一个张量浮点对象并注册到设备:
val_x = torch.tensor(val_x).float().to(device)
# 3、把张量对象当作 Python 函数传递通过训练好的神经网络 mynet
print(mynet(val_x))
# 上述代码返回与输入数据点相关联的预测输出值。
运行结果如下:
到目前为止,我们已经能够训练用于映射输入和输出的神经网络模型,训练过程中通过执行反向传播的方式来更新权重值,以最小化损失值(使用预先定义的损失函数进行计算)。
获取中间层的值
在某些情况下,获取神经网络中间层的值是有帮助的。
PyTorch 提供了以下两种方式获取神经网络中间值。
一种方法是直接调用层,就像它们是函数一样。可以按如下方式完成:
input_to_hidden = mynet.input_to_hidden_layer(X)
print(input_to_hidden)
hidden_activation = mynet.hidden_layer_activation(input_to_hidden)
print(hidden_activation)
输出:
另一种方法是指定需要在 forward 方法中查看的层。
保存并加载 PyTorch 模型
训练网络模型的含义很简单,就是训练这个网络中的各个权重参数。
而训练好的模型的含义很简单,其实就是一个权重参数已经被分配的很合适(即训练过程)的一个神经网络,可以直接拿这个网络来进行任务的实现,如预测啊、分类等。
state dict
model.state_dict() 命令是理解保存和加载 PyTorch 模型如何工作的基础。在model.state_dict() 中的字典对应于模型相应的参数名(键)和值(权重和偏置项)。state 指的是模型的当前快照(快照是每个张量处的值集)。
它返回一个包含键和值的字典(OrderedDict)。
键是模型层的名称,值对应于这些层的权重。