动手学机器学习

pytorch

预备知识

数据操作

张量

张量表示由一个数值组成的数组,这个数组可能有多个维度,具有一个轴的张量对应数学上的向量(vector); 具有两个轴的张量对应数学上的矩阵(matrix); 具有两个轴以上的张量没有特殊的数学名称。

  • 可以使用arange创建向量,向量的范围是[0,num)
x=torch.arange(10)
#tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9])
  • 可以通过张量的shape属性访问张量的形状
x.shape
#torch.Size([10])
  • 查看张量中元素的总数
x.numel()
#10
  • 改变张量的形状
X=x.reshape(3,4)
#tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])

我们可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)x.reshape(3,-1)来取代

  • 初始化矩阵
torch.zeros((2, 3, 4))
torch.ones((2, 3, 4))

随机初始化张量。其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。

torch.randn(3, 4)

通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

运算符

x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y  
# **运算符是求幂运算
(tensor([ 3.,  4.,  6., 10.]),
 tensor([-1.,  0.,  2.,  6.]),
 tensor([ 2.,  4.,  8., 16.]),
 tensor([0.5000, 1.0000, 2.0000, 4.0000]),
 tensor([ 1.,  4., 16., 64.]))
  • 把多个张量连结(concatenate)在一起。需要提供张量列表,并给出沿哪个轴连结0是按行,1是按列
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X,Y),dim=0)
'''
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.],
        [ 2.,  1.,  4.,  3.],
        [ 1.,  2.,  3.,  4.],
        [ 4.,  3.,  2.,  1.]])
'''

torch.cat((X,Y),dim=1)
'''
tensor([[ 0.,  1.,  2.,  3.,  2.,  1.,  4.,  3.],
        [ 4.,  5.,  6.,  7.,  1.,  2.,  3.,  4.],
        [ 8.,  9., 10., 11.,  4.,  3.,  2.,  1.]])
'''
  • 对张量中所有元素求和X.sum()
  • 广播机制:由于ab分别是3×1和1×2矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵广播为一个更大的3×2矩阵,如下所示:矩阵a将复制列, 矩阵b将复制行,然后再按元素相加。
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a+b
'''
(tensor([[0],
         [1],
         [2]]),
 tensor([[0, 1]]))
'''
  • 可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素
X[-1], X[1:3]
(tensor([ 8.,  9., 10., 11.]),
 tensor([[ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.]]))

我们还可以通过指定索引来将元素写入矩阵

X[1,2]=90

执行原地操作非常简单。 我们可以使用切片表示法将操作的结果分配给先前分配的数组

可以使用X[:] = X + YX += Y减少操作的内存开销

before = id(X)
X += Y
id(X) == before
  • 转化类型

张量转换为NumPy张量(ndarray

A = X.numpy()
B = torch.tensor(A)

要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。

a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
  • 处理缺失值
inputs = inputs.fillna(inputs.mean())

巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。

pd.get_dummies相当于onehot编码,常用与把离散的类别信息转化为onehot编码形式。

inputs = pd.get_dummies(inputs, dummy_na=True)
'''
   NumRooms  Alley_Pave  Alley_nan
0       3.0           1          0
1       2.0           0          1
2       4.0           0          1
3       3.0           0          1

'''

线性代数

标量由只有一个元素的张量表示。

矩阵的转置X.T

图像以n维数组形式出现, 其中3个轴对应于高度、宽度,以及一个通道(channel)轴, 用于表示颜色通道(红色、绿色和蓝色)。

A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone()  # 通过分配新内存,将A的一个副本分配给B

降维求和

张量每一行求和X.sum(axis=0)

求平均值A.mean(), A.sum() / A.numel()

非降维求和

sum_A = A.sum(axis=1, keepdims=True)
#沿行方向累积求和
A.cumsum(axis=0)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  6.,  8., 10.],
        [12., 15., 18., 21.],
        [24., 28., 32., 36.],
        [40., 45., 50., 55.]])

点积

# 点积,相同位置的按元素乘积的和
torch.dot(x, y)
# 矩阵-向量积
torch.mv(A, x)
# 矩阵乘法
torch.mm(A, B)

范数

一个向量的范数告诉我们一个向量有多大

L2范数(默认)

在这里插入图片描述

u = torch.tensor([3.0, -4.0])
torch.norm(u) #tensor(5.)

L1范数

在这里插入图片描述

torch.abs(u).sum()

Frobenius范数(Frobenius norm)是矩阵元素平方和的平方根:

在这里插入图片描述

a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

概率

机器学习就是做出预测

# 得到每种情况出现的次数
#multinomial.Multinomial(试验次数, 事件概率).sample((实验组数,))
multinomial.Multinomial(10000, fair_probs).sample((5,))
multinomial.Multinomial(10, fair_probs).sample((5,))
# 进行500组实验,每组抽取10个样本。模拟骰子
counts = multinomial.Multinomial(10, fair_probs).sample((500,))
cum_counts = counts.cumsum(dim=0)
estimates = cum_counts / cum_counts.sum(dim=1, keepdims=True)

d2l.set_figsize((6, 4.5))
for i in range(6):
    d2l.plt.plot(estimates[:, i].numpy(),
                 label=("P(die=" + str(i + 1) + ")"))
d2l.plt.axhline(y=0.167, color='black', linestyle='dashed')
d2l.plt.gca().set_xlabel('Groups of experiments')
d2l.plt.gca().set_ylabel('Estimated probability')
d2l.plt.legend();

在这里插入图片描述

线性神经网络

损失函数

平方误差函数

在这里插入图片描述

为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值

在这里插入图片描述

在模型训练时寻找参数(w,b),使得训练样本的总损失最小化在这里插入图片描述

随机梯度下降

梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

算法的步骤如下:(1)初始化模型参数的值,如随机初始化;

​ (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。

在这里插入图片描述

|B|表示每个小批量中的样本数,这也称为批量大小(batch size)。 η表示学习率(learning rate)。

  • 对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)或称为稠密层(dense layer)
def sgd(params, lr, batch_size): 
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_() # “清除”x的梯度值

线性回归

神经网络的训练有时候可能希望保持一部分的网络参数不变,只对其中一部分的参数进行调整。或者训练部分分支网络,并不让其梯度对主网络的梯度造成影响.这时候我们就需要使用detach()函数来切断一些分支的反向传播.

tensor.detach()返回一个新的tensor,从当前计算图中分离下来。但是仍指向原变量的存放位置,不同之处只是requirse_grad为false.得到的这个tensor永远不需要计算器梯度,不具有grad.即使之后重新将它的requires_grad置为true,它也不会具有梯度grad.这样我们就会继续使用这个新的tensor进行计算,后面当我们进行反向传播时,到该调用detach()的tensor就会停止,不能再继续向前进行传播.

使用detach返回的tensor和原始的tensor共同一个内存,即一个修改另一个也会跟着改变.

no_grad与detach有异曲同工之妙,都是逃避autograd的追踪。

yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。

def linreg(X, w, b):  
    """线性回归模型"""
    return torch.matmul(X, w) + b

训练

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        l.sum().backward()
        sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

线性神经网络

from torch import nn
#nn.Linear(输入特征形状,输出特征形状)
net = nn.Sequential(nn.Linear(2, 1))
# 初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
# 定义损失函数,均方误差
loss = nn.MSELoss()
# 定义优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
# 训练
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

softmax回归

Softmax函数就可以将多分类的输出值转换为范围在***[0, 1]***和为1的概率分布。

softmax函数
s o f t m a x ( X ) i j = exp ⁡ ( X i j ) ∑ k exp ⁡ ( X i k ) . \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. softmax(X)ij=kexp(Xik)exp(Xij).
交叉熵损失函数为
l ( y , y ^ ) = − ∑ j = 1 q y j log ⁡ y j ^ l(y,\hat{y} )= \mathrm{-}\sum_{j=1}^q y_j\log \hat{y_j} l(y,y^)=j=1qyjlogyj^

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc
# 初始化参数模型
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
# 损失函数
loss = nn.CrossEntropyLoss(reduction='none')
#优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
#训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

多层感知机

通过在网络中添加一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系模型。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层输出到上面的层,直到生成最后的输出。可以把前L-1层看作表示,把最后一层看作线性预测器,这种架构称为多层感知机(multilayer perceptron,MLP)

在这里插入图片描述

这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2
H = σ ( X W ( 1 ) + b ( 1 ) ) , O = H W ( 2 ) + b ( 2 ) . \begin{aligned} \mathbf{H} & = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}), \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}.\\ \end{aligned} HO=σ(XW(1)+b(1)),=HW(2)+b(2).

激活函数

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的。

ReLU函数

修正线性单元(Rectified linear unit,ReLU)。实现简单,同时在各种预测任务中表现良好。它提供了一种非常简单的非线性变换。

R e L U ( x ) = m a x ( x , 0 ) ReLU(x)=max(x,0) ReLU(x)=max(x,0)

ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。当x特别大时也不会饱和。

img

R e L U ReLU ReLU 函数的微分是一个分段函数:
r e l u ′ ( x ) = { 1 x > 0 0 : o t h e r w i s e relu^\prime(x)=\begin{cases} 1&x>0\\ 0&:otherwise \end{cases} relu(x)={10x>0:otherwise
要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题.

ReLU变种

Leaky RelU:传统的ReLU 单元当 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noO1HwfH-1670308960424)(https://www.zhihu.com/equation?tex=z)] 的值小于 0 时,是不会反向传播误差 [公式] 改善了这一点,当 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnEHtGdK-1670308960425)(https://www.zhihu.com/equation?tex=z)] 的值小于0时,仍然会有一个很小的误差反向传播回去。

[公式]

其中0<k<1.img

[公式] 函数的微分是一个分段函数:

在这里插入图片描述

Sigmoid

对于一个定义域在ℝ中的输入, sigmoid函数将输入变换为区间(0, 1)上的输出。 因此,sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:

sigmoid ⁡ ( x ) = 1 1 + exp ⁡ ( − x ) . \operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}. sigmoid(x)=1+exp(x)1.

img

导数为:
d d x sigmoid ⁡ ( x ) = exp ⁡ ( − x ) ( 1 + exp ⁡ ( − x ) ) 2 = sigmoid ⁡ ( x ) ( 1 − sigmoid ⁡ ( x ) ) . \frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right). dxdsigmoid(x)=(1+exp(x))2exp(x)=sigmoid(x)(1sigmoid(x)).

Tanh

tanh与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上
tanh函数的公式如下:

tanh ⁡ ( x ) = 1 − exp ⁡ ( − 2 x ) 1 + exp ⁡ ( − 2 x ) . \operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}. tanh(x)=1+exp(2x)1exp(2x).

img

梯度为:

d d x tanh ⁡ ( x ) = 1 − tanh ⁡ 2 ( x ) . \frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x). dxdtanh(x)=1tanh2(x).

Hard tanh:有时候 hard tanh 函数有时比 tanh 函数的选择更为优先,因为它的计算量更小。然而当 z 的值大于 1 时,函数的数值会饱和(如下图所示会恒等于 1)。tanh 激活函数为:

在这里插入图片描述

img

hard tanh这个函数的微分也可以用分段函数的形式表示:

在这里插入图片描述

Soft sign: 是另外一种非线性激活函数,它可以是tanh 的另外一种选择,因为它 hard clipped functions 那样过早地饱和:

在这里插入图片描述

img

函数的微分表达式为:

在这里插入图片描述

其中 sgn 是符号函数,根据 z 的符号返回 1 或者 -1 。

多层感知机简单实现

net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

模型选择

训练误差(training error)是指, 模型在训练数据集上计算得到的误差。 泛化误差(generalization error)是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

影响模型泛化的因素:

  1. 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
  2. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
  3. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。

除了训练和测试数据集之外,还增加一个验证数据集(validation dataset), 也叫验证集(validation set)

过拟合:训练误差明显低于验证误差

**欠拟合:**训练和验证误差之间的泛化误差很小

𝐾折交叉验证

当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。 这个问题的一个流行的解决方案是采用𝐾折交叉验证。 这里,原始训练数据被分成𝐾个不重叠的子集。 然后执行𝐾次模型训练和验证,每次在𝐾−1个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对𝐾次实验的结果取平均来估计训练和验证误差。

权重衰减

在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为 L 2 L_2 L2正则化。

​ 一种简单的方法是通过线性函数 𝑓(𝐱)=𝐰⊤𝐱中的权重向量的某个范数来度量其复杂性。要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数 ∣ ∣ w ∣ ∣ 2 ||w||^2 w2

使用 L 2 L_2 L2 范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。 在实践中,这可能使它们对单个变量中的观测误差更为稳定。 相比之下, L 1 L_1 L1 惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。 这称为特征选择(feature selection),这可能是其他场景下需要的。

原来的损失函数

L ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( i ) ) 2 . L(\mathbf{w}, b) = \frac{1}{n}\sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2. L(w,b)=n1i=1n21(wx(i)+by(i))2.

回想一下, x ( i ) \mathbf{x}^{(i)} x(i)是样本 i i i的特征, y ( i ) y^{(i)} y(i)是样本 i i i的标签, ( w , b ) (\mathbf{w}, b) (w,b)是权重和偏置参数。为了惩罚权重向量的大小,
我们必须以某种方式在损失函数中添加 ∥ w ∥ 2 \| \mathbf{w} \|^2 w2,但是模型应该如何平衡这个新的额外惩罚的损失?实际上,我们通过正则化常数 λ \lambda λ来描述这种权衡,这是一个非负超参数,我们使用验证数据拟合:

L ( w , b ) + λ 2 ∥ w ∥ 2 , L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2, L(w,b)+2λw2,

L 2 L_2 L2正则化回归的小批量随机梯度下降更新如下式:

w ← ( 1 − η λ ) w − η ∣ B ∣ ∑ i ∈ B x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) . \begin{aligned} \mathbf{w} & \leftarrow \left(1- \eta\lambda \right) \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} w(1ηλ)wBηiBx(i)(wx(i)+by(i)).

dropout

使用 dropout 的方式是我们取每个神经元层的输出 h ,并保持概率 p 的神经元是激活的,否则将神经元设置为 0 。然后,在反向传播中我们仅对在前向传播中激活的神经元回传梯度。最后,在测试过程,我们使用神经网络中全部的神经元进行前向传播计算。然而,有一个关键的微妙之处,为了使 dropout 有效地工作,测试阶段的神经元的预期输出应与训练阶段大致相同-否则输出的大小可能会有很大的不同

暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。 这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。 在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零。

当我们将暂退法应用到隐藏层,以 p p p的概率将隐藏单元置为零时,结果可以看作是一个只包含原始神经元子集的网络。比如,删除了 h 2 h_2 h2 h 5 h_5 h5,因此输出的计算不再依赖于 h 2 h_2 h2 h 5 h_5 h5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算不能过度依赖于 h 1 , … , h 5 h_1, \ldots, h_5 h1,,h5的任何一个元素。

在这里插入图片描述

通常,我们在测试时不用暂退法。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。然而也有一些例外:一些研究人员在测试时使用暂退法,用于估计神经网络预测的“不确定性”:如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。

net = nn.Sequential(nn.Flatten(),
        nn.Linear(784, 256),
        nn.ReLU(),
        # 在第一个全连接层之后添加一个dropout层
        nn.Dropout(dropout1),
        nn.Linear(256, 256),
        nn.ReLU(),
        # 在第二个全连接层之后添加一个dropout层
        nn.Dropout(dropout2),
        nn.Linear(256, 10))

前向传播、反向传播

前向传播

前向传播:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。

为了简单起见,我们假设输入样本是 x ∈ R d \mathbf{x}\in \mathbb{R}^d xRd,并且我们的隐藏层不包括偏置项。这里的中间变量是:

z = W ( 1 ) x , \mathbf{z}= \mathbf{W}^{(1)} \mathbf{x}, z=W(1)x, 其中 W ( 1 ) ∈ R h × d \mathbf{W}^{(1)} \in \mathbb{R}^{h \times d} W(1)Rh×d 是隐藏层的权重参数。将中间变量 z ∈ R h \mathbf{z}\in \mathbb{R}^h zRh通过激活函数 ϕ \phi ϕ后,我们得到长度为 h h h的隐藏激活向量: h = ϕ ( z ) . \mathbf{h}= \phi (\mathbf{z}). h=ϕ(z).隐藏变量 h \mathbf{h} h也是一个中间变量。

假设输出层的参数只有权重 W ( 2 ) ∈ R q × h \mathbf{W}^{(2)} \in \mathbb{R}^{q \times h} W(2)Rq×h,我们可以得到输出层变量,它是一个长度为 q q q的向量: o = W ( 2 ) h . \mathbf{o}= \mathbf{W}^{(2)} \mathbf{h}. o=W(2)h.

假设损失函数为 l l l,样本标签为 y y y,我们可以计算单个数据样本的损失项,

L = l ( o , y ) . L = l(\mathbf{o}, y). L=l(o,y).

根据 L 2 L_2 L2正则化的定义,给定超参数 λ \lambda λ,正则化项为

s = λ 2 ( ∥ W ( 1 ) ∥ F 2 + ∥ W ( 2 ) ∥ F 2 ) , s = \frac{\lambda}{2} \left(\|\mathbf{W}^{(1)}\|_F^2 + \|\mathbf{W}^{(2)}\|_F^2\right), s=2λ(W(1)F2+W(2)F2),

其中矩阵的Frobenius范数是将矩阵展平为向量后应用的 L 2 L_2 L2范数。
最后,模型在给定数据样本上的正则化损失为:

J = L + s . J = L + s. J=L+s.

在下面的讨论中,我们将 J J J称为目标函数(objective function)。

在这里插入图片描述

反向传播

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。
简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。假设我们有函数 Y = f ( X ) \mathsf{Y}=f(\mathsf{X}) Y=f(X) Z = g ( Y ) \mathsf{Z}=g(\mathsf{Y}) Z=g(Y),其中输入和输出 X , Y , Z \mathsf{X}, \mathsf{Y}, \mathsf{Z} X,Y,Z是任意形状的张量。利用链式法则,我们可以计算 Z \mathsf{Z} Z关于 X \mathsf{X} X的导数

∂ Z ∂ X = prod ( ∂ Z ∂ Y , ∂ Y ∂ X ) . \frac{\partial \mathsf{Z}}{\partial \mathsf{X}} = \text{prod}\left(\frac{\partial \mathsf{Z}}{\partial \mathsf{Y}}, \frac{\partial \mathsf{Y}}{\partial \mathsf{X}}\right). XZ=prod(YZ,XY).

在这里,我们使用 prod \text{prod} prod运算符在执行必要的操作(如换位和交换输入位置)后将其参数相乘。对于向量,这很简单,它只是矩阵-矩阵乘法。对于高维张量,我们使用适当的对应项。运算符 prod \text{prod} prod指代了所有的这些符号。

单隐藏层简单网络的参数是 W ( 1 ) \mathbf{W}^{(1)} W(1) W ( 2 ) \mathbf{W}^{(2)} W(2)。反向传播的目的是计算梯度 ∂ J / ∂ W ( 1 ) \partial J/\partial \mathbf{W}^{(1)} J/W(1) ∂ J / ∂ W ( 2 ) \partial J/\partial \mathbf{W}^{(2)} J/W(2)。为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。
计算的顺序与前向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。第一步是计算目标函数 J = L + s J=L+s J=L+s相对于损失项 L L L和正则项 s s s的梯度。 ∂ J ∂ L = 1    and    ∂ J ∂ s = 1. \frac{\partial J}{\partial L} = 1 \; \text{and} \; \frac{\partial J}{\partial s} = 1. LJ=1andsJ=1.

接下来,我们根据链式法则计算目标函数关于输出层变量 o \mathbf{o} o的梯度:

∂ J ∂ o = prod ( ∂ J ∂ L , ∂ L ∂ o ) = ∂ L ∂ o ∈ R q . \frac{\partial J}{\partial \mathbf{o}} = \text{prod}\left(\frac{\partial J}{\partial L}, \frac{\partial L}{\partial \mathbf{o}}\right) = \frac{\partial L}{\partial \mathbf{o}} \in \mathbb{R}^q. oJ=prod(LJ,oL)=oLRq.

接下来,我们计算正则化项相对于两个参数的梯度:

∂ s ∂ W ( 1 ) = λ W ( 1 )    and    ∂ s ∂ W ( 2 ) = λ W ( 2 ) . \frac{\partial s}{\partial \mathbf{W}^{(1)}} = \lambda \mathbf{W}^{(1)} \; \text{and} \; \frac{\partial s}{\partial \mathbf{W}^{(2)}} = \lambda \mathbf{W}^{(2)}. W(1)s=λW(1)andW(2)s=λW(2).

现在我们可以计算最接近输出层的模型参数的梯度

∂ J / ∂ W ( 2 ) ∈ R q × h \partial J/\partial \mathbf{W}^{(2)} \in \mathbb{R}^{q \times h} J/W(2)Rq×h
使用链式法则得出:

∂ J ∂ W ( 2 ) = prod ( ∂ J ∂ o , ∂ o ∂ W ( 2 ) ) + prod ( ∂ J ∂ s , ∂ s ∂ W ( 2 ) ) = ∂ J ∂ o h ⊤ + λ W ( 2 ) . \frac{\partial J}{\partial \mathbf{W}^{(2)}}= \text{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{W}^{(2)}}\right) + \text{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(2)}}\right)= \frac{\partial J}{\partial \mathbf{o}} \mathbf{h}^\top + \lambda \mathbf{W}^{(2)}. W(2)J=prod(oJ,W(2)o)+prod(sJ,W(2)s)=oJh+λW(2).

为了获得关于 W ( 1 ) \mathbf{W}^{(1)} W(1)的梯度,我们需要继续沿着输出层到隐藏层反向传播。关于隐藏层输出的梯度 ∂ J / ∂ h ∈ R h \partial J/\partial \mathbf{h} \in \mathbb{R}^h J/hRh由下式给出:

∂ J ∂ h = prod ( ∂ J ∂ o , ∂ o ∂ h ) = W ( 2 ) ⊤ ∂ J ∂ o . \frac{\partial J}{\partial \mathbf{h}} = \text{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{h}}\right) = {\mathbf{W}^{(2)}}^\top \frac{\partial J}{\partial \mathbf{o}}. hJ=prod(oJ,ho)=W(2)oJ.

由于激活函数 ϕ \phi ϕ是按元素计算的,计算中间变量 z \mathbf{z} z的梯度 ∂ J / ∂ z ∈ R h \partial J/\partial \mathbf{z} \in \mathbb{R}^h J/zRh需要使用按元素乘法运算符,我们用 ⊙ \odot 表示:
∂ J ∂ z = prod ( ∂ J ∂ h , ∂ h ∂ z ) = ∂ J ∂ h ⊙ ϕ ′ ( z ) . \frac{\partial J}{\partial \mathbf{z}} = \text{prod}\left(\frac{\partial J}{\partial \mathbf{h}}, \frac{\partial \mathbf{h}}{\partial \mathbf{z}}\right) = \frac{\partial J}{\partial \mathbf{h}} \odot \phi'\left(\mathbf{z}\right). zJ=prod(hJ,zh)=hJϕ(z).

最后,我们可以得到最接近输入层的模型参数的梯度 ∂ J / ∂ W ( 1 ) ∈ R h × d \partial J/\partial \mathbf{W}^{(1)} \in \mathbb{R}^{h \times d} J/W(1)Rh×d
根据链式法则,我们得到:
∂ J ∂ W ( 1 ) = prod ( ∂ J ∂ z , ∂ z ∂ W ( 1 ) ) + prod ( ∂ J ∂ s , ∂ s ∂ W ( 1 ) ) = ∂ J ∂ z x ⊤ + λ W ( 1 ) . \frac{\partial J}{\partial \mathbf{W}^{(1)}}= \text{prod}\left(\frac{\partial J}{\partial \mathbf{z}}, \frac{\partial \mathbf{z}}{\partial \mathbf{W}^{(1)}}\right) + \text{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(1)}}\right)= \frac{\partial J}{\partial \mathbf{z}} \mathbf{x}^\top + \lambda \mathbf{W}^{(1)}. W(1)J=prod(zJ,W(1)z)+prod(sJ,W(1)s)=zJx+λW(1).

在训练神经网络时,前向传播和反向传播相互依赖。 对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。 然后将这些用于反向传播,其中计算顺序与计算图的相反。

一方面,在前向传播期间计算正则项取决于模型参数𝐖(1)和 𝐖(2)的当前值。 它们是由优化算法根据最近迭代的反向传播给出的。 另一方面,反向传播期间参数的梯度计算, 取决于由前向传播给出的隐藏变量𝐡的当前值。因此,在训练神经网络时,在初始化模型参数后, 我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。 注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。 带来的影响之一是我们需要保留中间值,直到反向传播完成。 这也是训练比单纯的预测需要更多的内存(显存)的原因之一。 此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。 因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)错误。

梯度消失或爆炸

  • 根据反向传播算法和链式法则, 梯度的计算可以简化为以下公式:

avatar

  • 其中sigmoid的导数值域是固定的, 在[0, 0.25]之间, 而一旦公式中的w也小于1, 那么通过这样的公式连乘后, 最终的梯度就会变得非常非常小, 这种现象称作梯度消失. 反之, 如果我们人为的增大w的值, 使其大于1, 那么连乘够就可能造成梯度过大, 称作梯度爆炸.

  • 梯度消失或爆炸的危害:
    • 如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败; 梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值).
  • 梯度爆炸的解决方法
    1. 为了解决梯度爆炸的问题,Thomas Mikolov首先引入了一个简单的启发式解决方案,当梯度爆炸时,将梯度裁剪为一个小数值。也就是说,当它们达到某个阈值时,就会被重新设置为一个小数值

    2. 为了解决梯度消失问题,引入两种技术。

      • 第一种技术从一个单位矩阵初始化W(hh),而不是随机初始化W(hh)。
      • 第二种方法是使用校正线性单元(ReLU)代替sigmoid函数。ReLU的导数不是0就是1。通过这种方式,梯度将流经导数为1的神经元,而不会在向后传播时衰减时间步长。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《用Python动手机器学习》是一本介绍利用Python编程语言进行机器学习习的书籍,主要适用于对机器学习感兴趣的初者。通过阅读这本书,你将了解机器学习的基本概念、算法和工具,并通过Python实践来应用这些知识。 这本书的作者在书中详细介绍了Python中常用的机器学习库,例如NumPy、Pandas和Scikit-learn等。这些库提供了丰富的机器学习算法和工具,方便用户快速应用。书中每个章节都配有实例和练习,可以帮助读者理解和巩固所的知识。 在《用Python动手机器学习》这本书中,你将习到如何使用Python进行数据预处理、特征工程、模型选择和评估。你将会如何利用Python将数据集进行切分,建立模型并进行训练和预测。同时,书中还将介绍常用的机器学习算法,如线性回归、逻辑回归、决策树和支持向量机等。 此外,书中还提供了一些实际案例,如图像识别和文本分类等。这些案例可以帮助读者将所的知识应用到实际问题中,并加深对机器学习原理的理解。 总之,《用Python动手机器学习》这本书通过结合理论和实践,帮助读者掌握机器学习的基本概念和应用,同时也培养了读者使用Python进行机器学习的能力。无论你是初者还是有一定编程基础的人,这本书都能帮助你轻松入门机器学习,并在实际应用中取得良好的成果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值