预备知识

数据操作

广播机制

对形状不同的张量进行相加操作,规则为a矩阵复制列,b矩阵复制行,将元素相加

1
2
3
4
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
a + b

1
2
3
4
5
6
7
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
tensor([[0, 1],
[1, 2],
[2, 3]])

节省内存

1
2
3
4
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))

发现Z的id未变化,减少了内存开销

转换numpy对象

1
2
3
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)

将大小为1的张量转换为python标量,调用item函数,或者float,int等函数进行类型转换

1
2
3
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)

练习

  1. 运行本节中的代码。将本节中的条件语句X == Y更改为X < YX > Y,然后看看你可以得到什么样的张量。
    1
    tensor([[False, False, False, False], [ True, True, True, True], [ True, True, True, True]])
    会发现同样得到了与原tensor大小相同的逻辑值
  2. 用其他形状(例如三维张量)替换广播机制中按元素操作的两个张量。结果是否与预期相同?
    如果两个张量在某个维度上的大小不同,其中一个的大小必须是1,这样它就可以在该维度上进行扩展。
    如果两个张量在某个维度上的大小都是1,或者其中一个张量在该维度上不存在(即它的大小在该维度上为1),则在该维度上的大小将被设置为较大的那个大小。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a = torch.tensor([1, 2, 3])
    b = torch.tensor([4, 5, 6])
    result = a + b
    print(result)
    a_3d = a.unsqueeze(0).unsqueeze(-1)
    b_3d = b.unsqueeze(0).unsqueeze(0)
    result_3d = a_3d + b_3d
    print(result_3d)

    tensor([5, 7, 9]) tensor([[[5, 6, 7], [6, 7, 8], [7, 8, 9]]])

    数据预处理

    数据集读取

    基本操作:创建文件、路径组合、文件写入
    1
    2
    3
    4
    5
    import os
    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
    data_file = os.path.join('..', 'data', 'house_tiny.csv')
    with open(data_file, 'w') as f:
    f.write('NumRooms,Alley,Price\n') # 列名
    读取csv方法
    1
    2
    import pandas as pd
    data = pd.read_csv(data_file)

    缺失值处理

    对于一组数据:
    1
    2
    3
    4
    5
       NumRooms Alley   Price
    0 NaN Pave 127500
    1 2.0 NaN 106000
    2 4.0 NaN 178100
    3 NaN NaN 140000
    均值插值法
    1
    inputs = inputs.fillna(inputs.mean())
    独热编码
    1
    inputs = pd.get_dummies(inputs, dummy_na=True)

    练习

    创建包含更多行和列的原始数据集。
  3. 删除缺失值最多的列。
  4. 将预处理后的数据集转换为张量格式。
    首先创建数据
    1
    data = { 'NumRooms': [np.nan, 2.0, 4.0, np.nan, 3.0], 'Alley': ['Pave', np.nan, np.nan, np.nan, 'Grvl'], 'Price': [127500, 106000, 178100, 140000, 165000], 'Garage': [1, 0, 2, 1, 2], 'YearBuilt': [2000, 1995, 2005, 2010, 2003] }
    1
    2
    3
    4
    df.dropna(axis=1, inplace=True) 
    imputer = SimpleImputer(strategy='mean')
    df_imputed = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)
    tensor_data = torch.tensor(df_imputed.values) #转为tensor

    线性代数

矩阵的转置

1
A.T

张量基本算法

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

.clone()类似于深拷贝,B的反向传播不影响A。
Hadamard积:数学符号⊙,指两个相同形状的矩阵(或向量)对应位置上元素相乘的结果。

1
A * B

降维

默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。 我们还可以指定张量沿哪一个轴来通过求和降低维度。 以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指定axis=0。 由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失。

1
2
3
4
5
6
7
8
9
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape

(tensor([40., 45., 50., 55.]), torch.Size([4]))

A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape

(tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))

沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。
1
A.sum(axis=[0, 1])  # 结果和A.sum()相同

非降维求和

如果我们想沿某个轴计算A元素的累积总和, 比如axis=0(按行计算),可以调用cumsum函数。 此函数不会沿任何轴降低输入张量的维度。

1
A.cumsum(axis=0)

点积

  • 点积也称为内积或数量积,是两个向量之间的运算。
  • 对于两个长度相同的向量a和b,它们的点积为a·b = a₁b₁ + a₂b₂ + … + aₙbₙ,其中aᵢ和bᵢ分别表示向量a和b的第i个元素。
  • 点积的结果是一个标量,表示了两个向量在同一方向上的投影的乘积。
    1
    torch.dot(x, y)

    矩阵乘法

    1
    torch.mm(A, B)

    范数

    范数(Norm)是一个函数,通常用来衡量向量的大小或长度。范数的定义通常满足以下性质:
  1. 非负性(Non-negativity):对于任意向量x,其范数必须为非负数,即∥x∥ ≥ 0,且当且仅当x=0时,范数为0。
  2. 齐次性(Homogeneity):对于任意标量α,向量x的范数乘以α等于向量αx的范数,即∥αx∥ = |α| ∥x∥。
  3. 三角不等式(Triangle Inequality):对于任意两个向量x和y,有∥x+y∥ ≤ ∥x∥ + ∥y∥。
    常见向量范数包括:
  4. L1范数:向量中所有元素的绝对值之和,表示为∥x∥₁ = |x₁| + |x₂| + … + |xₙ|。
  5. L2范数:向量中所有元素的平方和的平方根,表示为∥x∥₂ = √(x₁² + x₂² + … + xₙ²)。
  6. L∞范数:向量中所有元素的绝对值的最大值,表示为∥x∥₊ = max(|x₁|, |x₂|, …, |xₙ|)。
    范数在机器学习和优化问题中经常被用来作为正则化项,添加到损失函数中,帮助控制模型的复杂度并避免过拟合。

练习

  1. 对于任意方阵A,A+AT是对称的
  2. 对于形状为(2, 3, 4)的张量X,len(X)的输出结果是2、
  3. 不是。在Python中,len()函数用于获取对象的长度或大小,对于张量来说,len(X)返回的是张量的第一个维度的大小,而不是张量特定轴的长度。、
  4. 考虑一个具有形状(3, 4, 5)的张量,对它在轴0、1、2上求和,输出的形状为(1, 1, 1),即一个标量值。

    微积分

    练习

  5. 如果有函数u=f(x,y,z),其中 x=x(a,b),y=y(a,b),z=z(a,b),根据链式法则,u 对 a 的导数可以表示为:

    自动微分

    深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。 实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
    在我们计算y关于x的梯度之前,需要一个地方来存储梯度。 重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 注意,一个标量函数关于向量x的梯度是向量,并且与x具有相同的形状。
    举例说明:y=2x*x的反向传播
    1
    2
    3
    4
    5
    6
    7
    8
    9
    x = np.arange(4.0)
    # 通过调用attach_grad来为一个张量的梯度分配内存
    x # array([0., 1., 2., 3.])
    x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
    x.grad # 默认值是None
    y = 2 * np.dot(x, x)
    y # array(28.)
    y.backward()
    x.grad # tensor([ 0., 4., 8., 12.]) x.grad == 4 * x

    非标量变量的反向传播

    y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的yx,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。

1
2
3
4
5
6
7
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad # tensor([0., 2., 4., 6.])

分离计算

有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为yx的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到xy被计算后发挥的作用。

这里可以分离y来返回一个新变量u,该变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经ux。 因此,下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理, 而不是z=x*x*x关于x的偏导数。

1
2
3
4
5
6
7
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
x.grad == u # tensor([True, True, True, True])

由于记录了y的计算结果,我们可以随后在y上调用反向传播, 得到y=x*x关于的x的导数,即2*x
1
2
3
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x # tensor([True, True, True, True])

练习

  1. 为什么计算二阶导数比一阶导数的开销要更大?
    计算二阶导数需要首先计算一阶导数,然后再对一阶导数进行求导;计算二阶导数需要应用链式法则多次,涉及到更多的函数组合和嵌套。
  2. 在运行反向传播函数之后,立即再次运行它,看看会发生什么?
    会报错:RuntimeError: Trying to backward through the graph a second time
  3. 在控制流的例子中,我们计算d关于a的导数,如果将变量a更改为随机向量或矩阵,会发生什么?
    如果a是一个随机向量或矩阵,那么计算其导数的过程会考虑到a的每个元素,并计算相应的雅可比矩阵。(雅可比矩阵是一个将一个向量值函数的梯度向量(或梯度向量的转置)表示为每个自变量的偏导数的矩阵)

查阅文档

查找模块中的所有函数和类

了知道模块中可以调用哪些函数和类,可以调用dir函数。

1
2
3
import torch
print(dir(torch.distributions))
# ['AbsTransform', 'AffineTransform', 'Bernoulli', 'Beta', 'Binomial', 'CatTransform', 'Categorical', 'Cauchy', 'Chi2', 'ComposeTransform', 'ContinuousBernoulli', 'CorrCholeskyTransform', ....

通常可以忽略以“__”(双下划线)开始和结束的函数,它们是Python中的特殊对象, 或以单个“_”(单下划线)开始的函数,它们通常是内部函数。

查找特定函数和类的用法

1
help(torch.ones)

线性神经网络

线性回归

回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。

线性模型

权重与偏置

损失函数

损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。 回归问题中最常用的损失函数是平方误差函数。

从零实现线性回归

练习

  1. 如果我们将权重初始化为零,会发生什么。算法仍然有效吗?
    可能会影响网络训练速度,收敛性和最终性能。
  • 随机初始化:将权重初始化为随机值是一种常用的做法。通过随机初始化,可以避免权重对称性,从而使每个神经元学习到不同的特征。常见的做法是从某个分布(如均匀分布或正态分布)中随机采样权重值。
  • 全0初始化:将权重初始化为全0是一种简单的初始化方法。然而,如果所有权重都初始化为0,那么每个神经元在前向传播时计算的值将是相同的,这会导致每个神经元学习相同的特征。这种情况下,网络无法有效地学习复杂的特征,训练过程可能会出现问题。
  1. 假设试图为电压和电流的关系建立一个模型。自动微分可以用来学习模型的参数吗?
    V=R×I
    R 是电阻。我们希望通过给定的电压和电流的数据样本,学习到电阻 R 的值。
    我们可以将电阻 R 视为模型的参数,然后定义一个损失函数来衡量模型预测的电压与实际观测值之间的差异。
  2. 能基于普朗克定律使用光谱能量密度来确定物体的温度吗?可以
  3. 计算二阶导数时可能会遇到什么问题?这些问题可以如何解决?
    计算量大,计算复杂度高,由于计算机表示的精读限制,可能出现数值不稳定问题
    符号计算:对于简单的函数和表达式,可以使用符号计算来精确地计算二阶导数,避免数值计算的误差。在进行数值计算时,可以采用数值稳定的算法和技巧,例如使用高精度算法或避免数值不稳定的操作。
  4. 为什么在squared_loss(均方损失)函数中需要使用reshape函数?
    1
    2
    3
    def squared_loss(y_hat, y):  #@save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
    我们需要将真实值y的形状转换为和预测值y_hat的形状相同
  5. 尝试使用不同的学习率,观察损失函数值下降的快慢
  6. 如果样本个数不能被批量大小整除,data_iter函数的行为会有什么变化?
    最后一个批次的大小会小于设定的批量大小。

线性回归的简洁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)
next(iter(data_iter))

定义模型

正如我们在构造nn.Linear时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层, 然后使用weight.databias.data方法访问参数。 我们还可以使用替换方法normal_fill_来重写参数值。

1
2
3
4
5
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

多层感知机

多层感知机

激活函数

RELU

sigmoid函数

tanh函数

练习

  1. 计算pReLU激活函数的导数。导数为
  2. 假设我们有一个非线性单元,将它一次应用于一个小批量的数据。这会导致什么样的问题?
  • 过拟合:在小批量上应用非线性单元可能导致模型在训练集上过拟合,因为模型可能会过度记住小批量的特定模式或噪声。
  • 收敛速度:在小批量上应用非线性单元可能会影响模型的收敛速度,因为模型可能需要更多的迭代才能收敛到最优解。

    多层感知机简洁实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import torch
    from torch import nn
    from d2l import torch as d2l
    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)

    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

练习

  1. 尝试添加不同数量的隐藏层,修改学习率。
  2. 尝试不同的激活函数,哪个效果最好?
    MSEloss
  3. 尝试不同的方案来初始化权重,什么方法效果最好?
    正态分布初始化

模型选择,欠拟合,过拟合

将模型在训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting), 用于对抗过拟合的技术称为正则化(regularization)。
在训练参数化机器学习模型时,
权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2正则化_。 这项技术通过函数与零的距离来衡量函数的复杂度, 因为在所有函数f中,函数f=0(所有输入都得到值0) 在某种意义上是最简单的。这个正则化项通常是权重的平方和或绝对值和,以惩罚模型的复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())

练习

  1. 在本节的估计问题中使用$\lambda$的值进行实验。绘制训练和测试精度关于$\lambda$的函数。观察到了什么?
    λ 太大后,train和test的loss会变得很大,太小后,train的loss会低,但是test的loss会很高。
  2. 使用验证集来找到最佳值λ。它真的是最优值吗?这有关系吗?
    它是对于该验证集的最优值,但不是全局最优值,会随着训练集与验证集而变化。
  3. 回顾训练误差和泛化误差之间的关系。除了权重衰减、增加训练数据、使用适当复杂度的模型之外,还能想出其他什么方法来处理过拟合?
    Dropout,数据增强,模型集成

暂退法(Dropout)

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

从零开始实现

要实现单层的暂退法函数, 我们从均匀分布U[0,1]中抽取样本,样本数与这层神经网络的维度一致。 然后我们保留那些对应样本大于p的节点,把剩下的丢弃。

在下面的代码中,我们实现 dropout_layer 函数, 该函数以dropout的概率丢弃张量输入X中的元素, 如上所述重新缩放剩余部分:将剩余部分除以1.0-dropout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
from torch import nn
from d2l import torch as d2l

def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)

1
2
3
4
5
6
7
8
9
10
11
12
13
X= torch.arange(16, dtype = torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))
#tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
# [ 8., 9., 10., 11., 12., 13., 14., 15.]])
#tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
# [ 8., 9., 10., 11., 12., 13., 14., 15.]])
#tensor([[ 0., 2., 0., 6., 0., 0., 0., 14.],
# [16., 18., 0., 22., 0., 26., 28., 30.]])
#tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
# [0., 0., 0., 0., 0., 0., 0., 0.]])

定义模型

我们可以将暂退法应用于每个隐藏层的输出(在激活函数之后), 并且可以为每一层分别设置暂退概率: 常见的技巧是在靠近输入层的地方设置较低的暂退概率。 下面的模型将第一个和第二个隐藏层的暂退概率分别设置为0.2和0.5, 并且暂退法只在训练期间有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training = True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()

def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用dropout
if self.training == True:
# 在第一个全连接层之后添加一个dropout层
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个dropout层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out

net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)

简介实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

练习

  1. 如果更改第一层和第二层的暂退法概率,会发生什么情况?具体地说,如果交换这两个层,会发生什么情况?设计一个实验来回答这些问题,定量描述该结果,并总结定性的结论。
    在调换第一和第二层概率后,结果基本没有什么变化,但是改变第一层和第二层的概率总和后,结果会有明显的变化。
  2. 增加训练轮数,并将使用暂退法和不使用暂退法时获得的结果进行比较。
    没有dropout的训练效果会更好,但是泛化性不够好。
  3. 为什么在测试时通常不使用暂退法?
    为了保持模型的一致性和稳定性,在测试时,我们希望模型能够产生确定性的预测结果,而不是随机性的输出。
  4. 如果我们将暂退法应用到权重矩阵的各个权重,而不是激活值,会发生什么?
    train_loss会下降的比较慢。

数值稳定性与模型初始化

梯度消失和梯度爆炸

梯度消失:当网络的层数较多时,梯度在反向传播过程中可能会变得非常小,甚至接近于零。这样,深层网络中较早层的参数将无法得到有效更新,导致模型无法学习到有效的特征表示,从而影响模型的性能。
梯度爆炸:与梯度消失相反,梯度爆炸指的是在反向传播过程中,梯度变得非常大,甚至超出了计算机能够表示的范围。这样会导致参数更新过大,模型参数发散,无法收敛到有效的解决方案。
为了解决梯度消失和梯度爆炸问题,可以采取以下几种方法:

  1. 权重初始化:合适的权重初始化可以帮助减少梯度消失和爆炸的可能性。例如,使用较小的随机数来初始化权重。

  2. 梯度裁剪:在反向传播过程中,如果梯度的范数超过了设定的阈值,可以对梯度进行裁剪,将其限制在一个合理的范围内,防止梯度爆炸。

  3. 使用激活函数:合适的激活函数可以帮助缓解梯度消失和梯度爆炸问题。例如,ReLU等激活函数在一定程度上可以防止梯度消失。

  4. Batch Normalization:批量归一化可以减少内部协变量漂移,并有助于稳定训练过程,从而减少梯度消失和爆炸的可能性。

  5. 使用更深层次的结构:使用一些技术,如残差连接(Residual Connections),可以帮助信息在网络中更好地传播,减少梯度消失的影响。

    深度学习计算

    层和块

    (block)可以描述单个层、由多个层组成的组件或整个模型本身。从编程的角度来看,块由类(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数。

    自定义块

    一个多层感知器块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
    # 调用MLP的父类Module的构造函数来执行必要的初始化。
    # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
    super().__init__()
    self.hidden = nn.Linear(20, 256) # 隐藏层
    self.out = nn.Linear(256, 10) # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
    # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
    return self.out(F.relu(self.hidden(X)))

    使用:

    1
    2
    net = MLP()
    net(X)

    顺序块

    自己定义一个sequential类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MySequential(nn.Module):
    def __init__(self, *args):
    super().__init__()
    for idx, module in enumerate(args):
    # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
    # 变量_modules中。_module的类型是OrderedDict
    self._modules[str(idx)] = module

    def forward(self, X):
    # OrderedDict保证了按照成员添加的顺序遍历它们
    for block in self._modules.values():
    X = block(X)
    return X

    使用方法:

    1
    2
    net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
    net(X)

    我们可以混合搭配各种组合块的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class NestMLP(nn.Module):
    def __init__(self):
    super().__init__()
    self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
    nn.Linear(64, 32), nn.ReLU())
    self.linear = nn.Linear(32, 16)

    def forward(self, X):
    return self.linear(self.net(X))

    chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
    chimera(X)

    练习

  6. 如果将MySequential中存储块的方式更改为Python列表,会出现什么样的问题?
    print(net) 会报错,不能更清晰的看到网络结构。
  7. 实现一个块,它以两个块为参数,例如net1net2,并返回前向传播中两个网络的串联输出。这也被称为平行块。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class paarallelblock(nn.Module):
    def __init__(self,net1,net2):
    super(parallelblock,self).__init__()
    self.net1 = net1
    self.net2 = net2
    def forward(self,x):
    out1 = self.net1(x)
    out2 = self.net2(x)
    return torch.cat((out1,out2),dim = 1)

  8. 假设我们想要连接同一网络的多个实例。实现一个函数,该函数生成同一个块的多个实例,并在此基础上构建更大的网络。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
    nn.Linear(8, 4), nn.ReLU())

    def block2():
    net = nn.Sequential()
    for i in range(4):
    # 在这里嵌套
    net.add_module(f'block {i}', block1())
    return net

    rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
    rgnet(X)

    参数管理

    参数访问

    1
    2
    3
    4
    5
    6
    7
    8
    import torch
    from torch import nn

    net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
    X = torch.rand(size=(2, 4))
    net(X)
    print(net[2].state_dict())
    # OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])
    每个参数都表示为参数类的一个实例。 要对参数执行任何操作,首先我们需要访问底层的数值。 有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。 下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。
    1
    2
    3
    4
    5
    6
    7
    print(type(net[2].bias))
    print(net[2].bias)
    print(net[2].bias.data)
    # <class 'torch.nn.parameter.Parameter'>
    # Parameter containing:
    # tensor([0.0887], requires_grad=True)
    # tensor([0.0887])

    一次性访问所有参数

    递归整个树来提取每个子块的参数:
    1
    2
    3
    4
    print(*[(name, param.shape) for name, param in net[0].named_parameters()])
    # ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
    print(*[(name, param.shape) for name, param in net.named_parameters()])

    这为我们提供了另一种访问网络参数的方式,如下所示。
    1
    2
    net.state_dict()['2.bias'].data
    # tensor([0.0887])
    观察网络是如何工作的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
    nn.Linear(8, 4), nn.ReLU())

    def block2():
    net = nn.Sequential()
    for i in range(4):
    # 在这里嵌套
    net.add_module(f'block {i}', block1())
    return net

    rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
    rgnet(X)
    print(regnet)
    # Sequential(
    #(0): Sequential(
    #(block 0): Sequential(
    #(0): Linear(in_features=4, out_features=8, bias=True)
    #(1): ReLU()
    #(2): Linear(in_features=8, out_features=4, bias=True)
    #(3): ReLU()
    #)
    #(block 1): Sequential(
    #(0): Linear(...

    参数绑定

    我们希望在多个层间共享参数,可以定义一个稠密层,使用它的参数来设置另一个层的参数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 我们需要给共享层一个名称,以便可以引用它的参数
    shared = nn.Linear(8, 8)
    net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
    shared, nn.ReLU(),
    shared, nn.ReLU(),
    nn.Linear(8, 1))
    net(X)
    # 检查参数是否相同
    print(net[2].weight.data[0] == net[4].weight.data[0])
    net[2].weight.data[0, 0] = 100
    # 确保它们实际上是同一个对象,而不只是有相同的值
    print(net[2].weight.data[0] == net[4].weight.data[0])
    1
    2
    tensor([True, True, True, True, True, True, True, True])
    tensor([True, True, True, True, True, True, True, True])
    会发现$shared$层在net中被使用了两次,但它们依然拥有相同的参数,它们不仅值相等,而且由相同的张量表示。因此我们改变其中一个参数,另一个参数也会改变。问题:当参数绑定时,梯度会发生什么情况?答案时由于模型参数包含梯度,因此在反向传播期间死三个神经网络层和第五个神经网络层的梯度会加到一起。

练习

为什么共享参数是个好主意?

  1. 可以减少模型的参数量,降低复杂度
  2. 提高泛化能力
  3. 在循环神经网络/卷积神经网络中参数共享可以帮助模型更好地捕捉数据的时空特征,提高学习效率

    自定义层

    首先构造一个没有任何参数的层(减去均值)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import torch
    import torch.nn.functional as F
    from torch import nn

    class CenteredLayer(nn.Module):
    def __init__(self):
    super().__init__()

    def forward(self, X):
    return X - X.mean()
    将层作为组件合并到更复杂的模型中:
    1
    net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

    设计带参数的层

    (其实是自定义了参数)
    1
    2
    3
    4
    5
    6
    7
    8
    class MyLinear(nn.Module):
    def __init__(self, in_units, units):
    super().__init__()
    self.weight = nn.Parameter(torch.randn(in_units, units))
    self.bias = nn.Parameter(torch.randn(units,))
    def forward(self, X):
    linear = torch.matmul(X, self.weight.data) + self.bias.data
    return F.relu(linear)
    初始化
    1
    2
    3
    linear = MyLinear(5,3)
    linear.weight
    linear(torch.rand(2,5))

    练习

    设计一个返回输入数据的傅里叶前半部分的层
    1
    2
    3
    4
    5
    6
    class FourierFrontHalf(nn.Module): 
    def __init__(self): super(FourierFrontHalf, self).__init__()
    def forward(self, x): # 计算傅里叶变换
    x_fft = torch.fft.fft(x) # 获取前半部分
    front_half = x_fft[:, :x_fft.shape[1] // 2]
    return front_half

    读写文件

    基本load,save操作

    对于单个张量可以调用load和save函数分别读写它们。
    这两个函数都要求我们提供一个名称,save要求将要保存的变量作为输入。
    1
    2
    3
    4
    5
    import torch
    form torch import nn
    from torch.nn import functional as F
    x = torch.arrange(4)
    torch.save(x,'x-file')
    将存储在文件中的数据读回内存:
    1
    x2 = torch.load('x-file')
    存储一个张量列表,然后把它们(分别)读回内存。
    1
    2
    3
    4
    y = torch.zeros(4)
    torch.save([x, y],'x-files')
    x2, y2 = torch.load('x-files')
    (x2, y2)
    可以写入或读取从字符串映射到张量的字典。 当我们要读取或写入模型中的所有权重时,这很方便
    1
    2
    3
    4
    5
    mydict = {'x': x, 'y': y}
    torch.save(mydict, 'mydict')
    mydict2 = torch.load('mydict')
    mydict2
    # {'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}

    模型的加载与保存

    深度学习框架提供了内置函数来保存和加载整个网络。 需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。
    多层感知机模型参数存储:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MLP(nn.Module):
    def __init__(self):
    super().__init__()
    self.hidden = nn.Linear(20, 256)
    self.output = nn.Linear(256, 10)

    def forward(self, x):
    return self.output(F.relu(self.hidden(x)))

    net = MLP()
    X = torch.randn(size=(2, 20))
    Y = net(X)
    1
    torch.save(net.state_dict(), 'mlp.params')
    为了恢复模型,我们实例化了原始多层感知机模型的一个备份。 这里我们不需要随机初始化模型参数,而是直接读取文件中存储的参数:
    1
    2
    3
    clone = MLP()
    clone.load_state_dict(torch.load('mlp.params'))
    clone.eval()

    练习

  4. 即使不需要将经过训练的模型部署到不同的设备上,存储模型参数还有什么实际的好处?
    节省内存,模型压缩,快速部署,方便下一次训练
  5. 假设我们只想复用网络的一部分,以将其合并到不同的网络架构中。比如想在一个新的网络中使用之前网络的前两层,该怎么做?
  • 提取需要复用的部分:从原始网络中提取前两层的参数。
  • 构建新网络:构建一个新的网络,将提取的部分作为其中的一部分。(以resnet为例)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import torch
    import torch.nn as nn
    from torchvision.models import resnet18
    # 加载预训练的ResNet模型
    pretrained_model = resnet18(pretrained=True)
    # 提取前两层
    conv1 = pretrained_model.conv1
    bn1 = pretrained_model.bn1
    # 构建新网络
    class NewNetwork(nn.Module):
    def __init__(self):
    super(NewNetwork, self).__init__()
    self.conv1 = conv1
    self.bn1 = bn1
    # 添加新的层
    self.conv2 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
    self.bn2 = nn.BatchNorm2d(128)
    self.relu = nn.ReLU(inplace=True)
    self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
    # 其他层...

    def forward(self, x):
    x = self.conv1(x)
    x = self.bn1(x)
    x = self.relu(x)
    x = self.conv2(x)
    x = self.bn2(x)
    x = self.relu(x)
    x = self.pool(x)
    # 其他层的前向传播...
    return x
    # 创建新网络的实例
    new_network = NewNetwork()
  1. 如何同时保存网络架构和参数?需要对架构加上什么限制?
    将参数与架构同时保存在同一文件中,(实例化模型并保存)加载时分开加载:
    1
    2
    3
    # 假设我有一个模型SimpleModel()
    model = SimpleModel()
    torch.save({'model_state_dict': model.state_dict(), 'model_architecture': SimpleModel}, 'model.pth')
    加载时:
    1
    2
    3
    4
    5
    # 加载模型
    checkpoint = torch.load('model.pth')
    model_architecture = checkpoint['model_architecture']
    model = model_architecture()
    model.load_state_dict(checkpoint['model_state_dict'])

GPU

PyTorch中,CPU和GPU可以用torch.device('cpu')torch.device('cuda')表示。 应该注意的是,cpu设备意味着所有物理CPU和内存, 这意味着PyTorch的计算将尝试使用所有CPU核心。 然而,gpu设备只代表一个卡和相应的显存。 如果有多个GPU,我们使用torch.device(f'cuda:{i}') 来表示第�块GPU(�从0开始)。 另外,cuda:0cuda是等价的。

1
2
3
4
import torch
from torch import nn

torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')

查询gpu数量
1
torch.cuda.device_count()

可以定义一个函数,可以在不存在所有所需gpu的情况下运行代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
def try_gpu(i=0):  #@save
"""如果存在,则返回gpu(i),否则返回cpu()"""
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')

def try_all_gpus(): #@save
"""返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
devices = [torch.device(f'cuda:{i}')
for i in range(torch.cuda.device_count())]
return devices if devices else [torch.device('cpu')]

try_gpu(), try_gpu(10), try_all_gpus()

默认创建张量是在CPU上的
1
2
X = torch.ones(2, 3, device=try_gpu())
X

可以在指定的gpu上创建张量:
1
2
Y = torch.rand(2, 3, device=try_gpu(1))
Y

这时,X在’cuda:0’上,Y在‘cuda:1’上,如果我们要计算X + Y,我们需要决定在哪里执行这个操作。们可以将X传输到第二个GPU并在那里执行操作。 不要简单地X加上Y,因为这会导致异常, 运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。 由于Y位于第二个GPU上,所以我们需要将X移到那里, 然后才能执行相加运算。
可以将X复制到与Y相同的GPU进行相加
1
2
Z = X.cuda(1)
Y + Z

将模型参数放在GPU上:
1
2
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())

练习

  1. 尝试一个计算量更大的任务,比如大矩阵的乘法,看看CPU和GPU之间的速度差异。再试一个计算量很小的任务呢?
  • 大任务测试:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import torch
    import time
    # 设置矩阵大小
    size = 5000
    # 创建两个大矩阵
    a = torch.rand(size, size)
    b = torch.rand(size, size)
    # 在CPU上进行矩阵乘法
    start_time = time.time()
    result_cpu = torch.matmul(a, b)
    cpu_time = time.time() - start_time
    print(f"CPU time: {cpu_time:.6f} seconds")
    # 如果可以使用GPU,进行相同的测试
    if torch.cuda.is_available():
    a = a.cuda()
    b = b.cuda()
    start_time = time.time()
    result_gpu = torch.matmul(a, b)
    gpu_time = time.time() - start_time
    print(f"GPU time: {gpu_time:.6f} seconds")
    CPU time: 1.693618 seconds
    GPU time: 0.643845 seconds
    小任务:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 设置矩阵大小
    size = 100
    # 创建两个小矩阵
    a = torch.rand(size, size)
    b = torch.rand(size, size)
    # 在CPU上进行矩阵乘法
    start_time = time.time()
    result_cpu = torch.matmul(a, b)
    cpu_time = time.time() - start_time
    print(f"CPU time (small task): {cpu_time:.6f} seconds")
    # 如果可以使用GPU,进行相同的测试
    if torch.cuda.is_available():
    a = a.cuda()
    b = b.cuda()
    start_time = time.time()
    result_gpu = torch.matmul(a, b)
    gpu_time = time.time() - start_time
    print(f"GPU time (small task): {gpu_time:.6f} seconds")
    CPU time (small task): 0.010012 seconds
    GPU time (small task): 0.002001 seconds
  1. 我们应该如何在GPU上读写模型参数?
    可以通过model.parameters获取模型终端 参数
    1
    for param in model.parameters(): print(param)
  2. 测量计算1000个100×100矩阵的矩阵乘法所需的时间,并记录输出矩阵的Frobenius范数,一次记录一个结果,而不是在GPU上保存日志并仅传输最终结果。
    首先,Frobenius范数可以看作是矩阵中所有元素的平方和的平方根,类似于向量的欧几里得范数(L2范数)在矩阵上的推广。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import torch
    import time
    # 设置矩阵大小和数量
    matrix_size = 100
    num_matrices = 1000
    device = torch.device('cuda')
    frobenius_norms = []
    # 开始计时
    start_time = time.time()
    # 进行矩阵乘法并记录Frobenius范数
    for i in range(num_matrices):
    # 创建两个随机矩阵并移动到GPU
    a = torch.rand(matrix_size, matrix_size, device=device)
    b = torch.rand(matrix_size, matrix_size, device=device)
    # 在GPU上进行矩阵乘法
    result = torch.matmul(a, b)
    # 计算Frobenius范数并移动到CPU
    frob_norm = torch.norm(result, p='fro').cpu().item()
    # 记录结果
    frobenius_norms.append(frob_norm)
    # 结束计时
    end_time = time.time()
    total_time = end_time - start_time
    print(f"Total time for {num_matrices} matrix multiplications: {total_time:.6f} seconds")
    print("Frobenius norms of output matrices:", frobenius_norms)

    Total time for 1000 matrix multiplications: 4.377934 seconds

    卷积神经网络

    全连接到卷积

  3. 为什么平移不变性可能也不是好主意呢?
    平移不变性意味着卷积层对输入数据的平移具有不变性,这在很多情况下是有益的,因为它允许模型识别对象,无论它们在图像中的位置如何。然而,在某些情况下,平移不变性可能不是一个好主意。例如,如果图像中对象的位置对于任务非常重要,那么平移不变性可能会导致信息丢失。
  4. 当从图像边界像素获取隐藏表示时,我们需要思考哪些问题?
    我们需要考虑边界效应和填充策略。由于卷积核在边界处可能无法完全覆盖所有像素,这可能导致边界像素的信息不足。为了解决这个问题,我们可以采用填充(padding)策略
  5. 描述一个类似的音频卷积层的架构。
    音频数据通常以一维时间序列的形式出现,因此音频卷积层通常使用一维卷积(1D convolution)。这种卷积层在时间轴上滑动卷积核,提取音频信号的局部特征。例如,一个音频卷积层可以由一系列一维卷积层组成,每个层使用不同大小的卷积核和步长来捕捉不同时间尺度上的特征。这类似于图像卷积层使用二维卷积提取空间特征。
  6. 卷积层也适合于文本数据吗?为什么?
    在文本处理中,卷积层可以用来捕捉词汇或短语的局部模式。例如,通过在嵌入层(embedding layer)之后应用一维卷积层,模型可以学习到文本中词语的组合特征。

    图像卷积

    互相关运算

    在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。

    卷积层

    卷积层中的两个被训练的参数是卷积核权重和标量偏置。 就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。

    感受野

    在卷积神经网络中,对于某一层的任意元素x,其_感受野(receptive field)是指在前向传播期间可能影响x计算的所有元素(来自所有先前层)。

    练习

  7. 在我们创建的Conv2D自动求导时,有什么错误消息?
    没有使用PyTorch的操作来确保梯度可以被追踪
  8. 如何通过改变输入张量和卷积核张量,将互相关运算表示为矩阵乘法?
    把输入和卷积展平,卷积核每滑动一个步幅取一个样本。
  9. 手工设计一些卷积核。
    1. 二阶导数的核的形式是什么?
      二阶导数卷积核就是拉普拉斯算子[[0,1,0],[1,-4,1],[0,1,0]]
    2. 积分的核的形式是什么?
      d+1

      填充与步幅

      通用公式

      其中:
  • i:输入尺寸input
  • o:输出output
  • s:步长stride、
  • p:填充padding(一般都是零)
  • k:卷积核(kernel)大小

    展开公式

    多输入多输出通道

    多输入通道

    当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。

    多输出通道

    在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。用$c_i$和$c_o$分别表示输入和输出通道的数目,并让$k_h$和$k_w$为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为$c_ik_hk_w$的卷积核张量,这样卷积核的形状是$c_oc_ik_h*k_w$。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

    1 * 1卷积

    汇聚层(pooling池化层)

    它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

    最大汇聚层和平均汇聚层

    与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。

在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。

练习

除了平均汇聚层和最大汇聚层,是否有其它函数可以考虑(提示:回想一下softmax)?为什么它不流行?
除了平均汇聚层(Average Pooling)和最大汇聚层(Max Pooling),还可以考虑使用Softmax汇聚层。Softmax汇聚层是一种在汇聚操作中应用Softmax函数的方法,它可以为汇聚区域内的每个像素分配一个权重,这些权重根据像素的相对大小进行加权和。
Softmax汇聚层的一个潜在优点是它可以提供一种更加灵活和可学习的汇聚机制,因为它根据输入的特征自适应地调整汇聚区域内像素的权重。不流行的原因有:计算复杂度,训练困难(梯度小时或梯度爆炸),解释性差。

卷积神经网络(LeNet)

  • 卷积编码器:由两个卷积层组成;
  • 全连接层密集块:由三个全连接层组成。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import torch
    from torch import nn
    from d2l import torch as d2l

    net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))
    X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
    for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)

练习

  1. 将平均汇聚层替换为最大汇聚层,会发生什么?
    导致模型捕捉更加显著的特征,因为最大汇聚层倾向于保留最强的信号,而忽略较弱的信号。这种变化可能会提高模型的性能,特别是在处理具有明显特征的图像时。
  2. 尝试构建一个基于LeNet的更复杂的网络,以提高其准确性。
    1. 调整卷积窗口大小。
    2. 调整输出通道的数量。
    3. 调整激活函数(如ReLU)。
    4. 调整卷积层的数量。
    5. 调整全连接层的数量。
    6. 调整学习率和其他训练细节(例如,初始化和轮数)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      import torch
      import torch.nn as nn
      import torch.optim as optim

      class ImprovedLeNet(nn.Module):
      def __init__(self):
      super(ImprovedLeNet, self).__init__()
      self.conv1 = nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2)
      self.conv2 = nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2)
      self.conv3 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
      self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
      self.fc1 = nn.Linear(64 * 7 * 7, 120)
      self.fc2 = nn.Linear(120, 84)
      self.fc3 = nn.Linear(84, 10)
      self.relu = nn.ReLU()
      def forward(self, x):
      x = self.pool(self.relu(self.conv1(x)))
      x = self.pool(self.relu(self.conv2(x)))
      x = self.pool(self.relu(self.conv3(x)))
      x = x.view(-1, 64 * 7 * 7)
      x = self.relu(self.fc1(x))
      x = self.relu(self.fc2(x))
      x = self.fc3(x)
      return x

      model = ImprovedLeNet()

      criterion = nn.CrossEntropyLoss()
      optimizer = optim.Adam(model.parameters(), lr=0.001)

现代卷积网络

深度卷积神经网络

在计算机视觉中,直接将神经网络与其他机器学习方法进行比较也许不公平。这是因为,卷积神经网络的输入是由原始像素值或是经过简单预处理(例如居中、缩放)的像素值组成的。但在使用传统机器学习方法时,从业者永远不会将原始像素作为输入。在传统机器学习方法中,计算机视觉流水线是由经过人的手工精心设计的特征流水线组成的。对于这些传统方法,大部分的进展都来自于对特征有了更聪明的想法,并且学习到的算法往往归于事后的解释。

Alexnet

在AlexNet的第一层,卷积窗口的形状是11×11。 由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 第二层中的卷积窗口形状被缩减为5×5,然后是3×3。 此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为3×3、步幅为2的最大汇聚层。 而且,AlexNet的卷积通道数目是LeNet的10倍。
在最后一个卷积层后有两个全连接层,分别有4096个输出。 这两个巨大的全连接层拥有将近1GB的模型参数。
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。 一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。
AlexNet通过暂退法(dropout)控制全连接层的模型复杂度,而LeNet只使用了权重衰减。

练习

  1. AlexNet对Fashion-MNIST数据集来说可能太复杂了。
    1. 尝试简化模型以加快训练速度,同时确保准确性不会显著下降。
    2. 设计一个更好的模型,可以直接在28×28图像上工作。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      import torch
      import torch.nn as nn
      import torch.nn.functional as F
      class SimpleCNN(nn.Module):
      def __init__(self):
      super(SimpleCNN, self).__init__()
      self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
      self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
      self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
      self.fc1 = nn.Linear(64 * 7 * 7, 128)
      self.fc2 = nn.Linear(128, 10)
      def forward(self, x):
      x = F.relu(self.conv1(x))
      x = self.pool(x)
      x = F.relu(self.conv2(x))
      x = self.pool(x)
      x = x.view(-1, 64 * 7 * 7)
      x = F.relu(self.fc1(x))
      x = self.fc2(x)
      return x
      model = SimpleCNN()
      print(model)
  2. 分析了AlexNet的计算性能。
    1. 在AlexNet中主要是哪部分占用显存?
      全连接的权重和偏置
    2. 在AlexNet中主要是哪部分需要更多的计算?
      卷积层
    3. 计算结果时显存带宽如何?
      显存带宽决定了数据(如权重、激活值和梯度)在GPU内存和计算单元之间传输的速度

      使用块的网络(VGG)

      VGG块

      经典卷积神经网络基本组成部分:
  3. 卷积层
  4. 激活函数
  5. pooling汇聚层
    vggblock的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import torch
    from torch import nn
    from d2l import torch as d2l
    def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
    layers.append(nn.Conv2d(in_channels, out_channels,
    kernel_size=3, padding=1))
    layers.append(nn.ReLU())
    in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

    接受三个参数,分别对应于卷积层的数量num_convs、输入通道的数量in_channels和输出通道的数量out_channels。由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def vgg(conv_arch):
    conv_blks = []
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
    conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
    in_channels = out_channels

    return nn.Sequential(
    *conv_blks, nn.Flatten(),
    # 全连接层部分
    nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
    nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
    nn.Linear(4096, 10))

    net = vgg(conv_arch)

    可以看出,在定义网络时,可以先用列表来存储(append)块卷积层,最后再return nn.sequential(*list)。
    其中超参数conv_arch定义了每个VGG块李卷积层的个数和输出通道数。

    1
    conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
  6. 打印层的尺寸时,我们只看到8个结果,而不是11个结果。剩余的3层信息去哪了?
    后三层的2个卷积看成了一个块,所以只有8层

  7. 与AlexNet相比,VGG的计算要慢得多,而且它还需要更多的显存。分析出现这种情况的原因。
    VGG的网络更深,参数量更大,并且VGG的线性层参数更多
  8. 尝试将Fashion-MNIST数据集图像的高度和宽度从224改为96。这对实验有什么影响?

  9. 请参考VGG论文中的表1构建其他常见模型,如VGG-16或VGG-19。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
    layers.append(nn.Conv2d(in_channels, out_channels,
    kernel_size=3, padding=1))
    layers.append(nn.ReLU())

    in_channels = out_channels
    if out_channels >= 256:
    layers.append(nn.Conv2d(out_channels, out_channels, kernel_size=1))
    layers.append(nn.ReLU())
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
    return nn.Sequential(*layers)
    def vgg16(conv_arch):
    conv_blks = []
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
    conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
    in_channels = out_channels

    return nn.Sequential(
    *conv_blks, nn.Flatten(),
    # 全连接层部分
    nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
    nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
    nn.Linear(4096, 10))
    conv_arch = ((2, 64), (2, 128), (2, 256), (2, 512), (2, 512))
    ratio = 4
    small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
    net = vgg16(small_conv_arch)

    网络中的网络(NiN)

    LeNet、AlexNet和VGG都有一个共同的设计模式:通过一系列的卷积层与汇聚层来提取空间结构特征;然后通过全连接层对特征的表征进行处理。 AlexNet和VGG对LeNet的改进主要在于如何扩大和加深这两个模块。 或者,可以想象在这个过程的早期使用全连接层。然而,如果使用了全连接层,可能会完全放弃表征的空间结构。 网络中的网络NiN)提供了一个非常简单的解决方案:在每个像素的通道上分别使用多层感知机

    NiN块

    卷积层的输入和输出由四维张量组成,张量的每个轴分别对应样本,通道,高度和宽度。另外全连接额层的输入和输出时对应于样本和特征的二维张量。

  • NiN使用由一个卷积层和多个1×1卷积层组成的块。该块可以在卷积神经网络中使用,以允许更多的每像素非线性。
  • NiN去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层(即在所有位置上进行求和)。该汇聚层通道数量为所需的输出数量(例如,Fashion-MNIST的输出为10)。
  • 移除全连接层可减少过拟合,同时显著减少NiN的参数。
  • NiN的设计影响了许多后续卷积神经网络的设计。

    练习

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import torch
    from torch import nn
    from d2l import torch as d2l

    def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    return nn.Sequential(
    nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
    nn.ReLU(),
    nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
    nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

    net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    nn.MaxPool2d(3, stride=2),
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

含并行连结的网络(googleNet)

在GoogLeNet中,基本的卷积块被称为Inception块(Inception block)。
![[Pasted image 20240402145836.png]]
inception块由四条并行路径组成,前三条路径使用1 1,3 3,5 5,卷积层,从不同空间大小中提取信息。中间的两条路径在输入上执行1 1卷积,以减少通道数,降低模型复杂性。第四条路径使用3 3最大汇聚层,然后使用1 1卷积层来改变通道数。这四条路径使用合适的填充使输入和输出的高和宽一直,最后我们将每条线路的输出在通道维度上连接,构成Inception块的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

class Inception(nn.Module):
# c1--c4是每条路径的输出通道数
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
# 线路2,1x1卷积层后接3x3卷积层
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最大汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维度上连结输出
return torch.cat((p1, p2, p3, p4), dim=1)

googlenet模型

GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块之间的最大汇聚层可降低维度。
![[Pasted image 20240402150750.png]]
模块逐渐实现:

1
2
3
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

练习

  1. 使用GoogLeNet的最小图像大小是多少?
    224 * 224
  2. 将AlexNet、VGG和NiN的模型参数大小与GoogLeNet进行比较。后两个网络架构是如何显著减少模型参数大小的?
    AlexNet:6000万参数
    VGG16:1.38亿参数
    NiN:很少(1 1卷积)
    相比之下,*GoogLeNet
    引入了Inception模块,这些模块可以在不增加太多参数的情况下增加网络的深度和宽度。

    批量规范化

    这是一种流行且有效的技术,可持续加速深层网络的收敛速度。结合残差块,可以训练100层以上的网络。
    在训练神经网络时出现的一些实际挑战:
    首先,数据预处理的方式会对最终结果产生巨大影响。
    第二,对于典型的多层感知机或卷积神经网络,当我们训练时,中间层的变量可能有更广的变化范围。不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数会随着训练更新而变换莫测。假设这些变量分布中的这种偏移可能会阻碍网络的收敛,直观来说,如果一个层的可变值是另一个层的100倍,者可能需要对学习率进行补偿调整。
    第三,更深层的网络很复杂,容易过拟合。这意味着正则化变得重要。

批量规范化用于单个可选层(也可以应用到所有的层),原理:在每次训练迭代中,我们首先规范化输入,同伙减去其均值并除以标准差,其中两者均基于当前小批量处理。接下来,我们应用比例系数和比例偏移.正是由于这个基于批量统计的标准化,才有了批量规范化的名称。
请注意,如果我们尝试使用大小为1的小批量应用批量规范化,我们将无法学到任何东西,在减去均值后,这个单元将为0。所以,只有使用足够大的小批量,批量规范化才是有效且稳定的。在应用批量规范化时,批量的大小选择可能比没有批量规范化时重要。
从形式上来说,用$x$表示来自一个小批量的输入,批量规范化BN根据以下表达式转换x:

从零实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import torch
from torch import nn
from d2l import torch as d2l

def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data

简明实现

使用Batchnorm2d(num_feature)

1
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),

使用方法为输入上一层通道数即可。

争议

直观地说,批量规范化被认为可以使优化更加平滑。 然而,我们必须小心区分直觉和对我们观察到的现象的真实解释。 回想一下,我们甚至不知道简单的神经网络(多层感知机和传统的卷积神经网络)为什么如此有效。 即使在暂退法和权重衰减的情况下,它们仍然非常灵活,因此无法通过常规的学习理论泛化保证来解释它们是否能够泛化到看不见的数据。

注:

  • 批量规范化在全连接层和卷积层的使用略有不同。
  • 批量规范化层和暂退层一样,在训练模式和预测模式下计算不同。

    练习

  1. 在使用批量规范化之前,我们是否可以从全连接层或卷积层中删除偏置参数?为什么?
    可以,因为批量规范化的一个作用就是对每个特征通道进行归一化,使得偏置的影响减小。因此,如果我们使用批量规范化,偏置参数的作用可能会变得不那么重要,甚至可以被省略。

  2. 我们是否需要在每个层中进行批量规范化?尝试一下?
    尝试在每个层中都进行批量规范化可能会导致网络过度约束,使得网络学习能力下降。

  3. 可以通过批量规范化来替换暂退法吗?行为会如何改变?
    它们是两种不同的正则化技术,不能简单替换。但可以结合使用。暂退法是在训练过程中随机将一部分神经元的输出置为0,可以减少过拟合问题。批量规范化可以额对每个特征通道的激活值进行归一化,有助于模型加速收敛,减少梯度消失问题。但如果把所有批量归一化转换为暂退法回事模型训练变慢或者不稳定。
  4. 查看高级API中有关BatchNorm的在线文档,以查看其他批量规范化的应用。
    在PyTorch中,BatchNorm是通过torch.nn.BatchNorm模块实现的。这个模块可以对输入的特征图(feature maps)进行规范化,具体来说,它会计算每个通道的均值和方差,并使用这些统计数据来规范化输入数据。BatchNorm的参数包括num_features(输入特征的数量)、eps(数值稳定性参数)、momentum(动量,用于更新均值和方差的移动平均)、affine(是否进行可学习的缩放和偏移变换)等
  5. 研究思路:可以应用的其他“规范化”转换?可以应用概率积分变换吗?全秩协方差估计可以么?
    面临复杂度高和对大规模数据不适用的问题,因此在深度学习中并不常见。

残差网络

函数类

嵌套函数类可以避免创造的新体系偏离原来的近似。
残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。 于是,残差块(residual blocks)便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。

残差块

要求残差块的输入于输出通道数相同,使得它们可以相加,如果想改变通道数,需要引入一个额外的1 * 1卷积来讲输入变换成需要的形状后在做相加运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

class Residual(nn.Module): #@save
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)

def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)

resnet模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk

b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

最后在Resnet中加入全局平均汇聚层,以及全连接输出。

1
2
3
net = nn.Sequential(b1,b2,b3,b4,b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(),nn.Linear(512,10))

将模型每一层输入形状打印出来的方法:
1
2
3
4
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)

练习

  1. Inception块与残差块之间的主要区别是什么?在删除了Inception块中的一些路径之后,它们是如何相互关联的?
    Inception将输入的特征都进行了处理,最后在通道上拼接。resnet的残差块将输入保留不进行运算(保证用1 * 1卷积使尺寸一致),最后直接相加。即使在简化后的Inception块中,多尺度特征提取的概念仍然存在,这是它与纯残差块的主要区别。在实际应用中,可以根据具体任务的需求和网络设计的考虑,灵活地调整Inception块的结构,以达到最佳的性能和效率。同时,也有研究者尝试将Inception块和残差块结合起来,创造出新的网络结构,以利用两者的优势。
  2. 对于更深层次的网络,ResNet引入了“bottleneck”架构来降低模型复杂性。请试着去实现它。
    在ResNet中,”bottleneck”架构是为了减少网络中的参数数量和计算复杂性而设计的。这种架构通过引入一个较小的卷积层(通常是1x1卷积)来降低维度,从而在进行较大的卷积操作(如3x3或5x5卷积)之前和之后减少输入和输出的通道数。
  3. 在ResNet的后续版本中,作者将“卷积层、批量规范化层和激活层”架构更改为“批量规范化层、激活层和卷积层”架构。请尝试做这个改进。详见resnet论文中的图1。
  4. 为什么即使函数类是嵌套的,我们仍然要限制增加函数的复杂性呢?
    可读性与可维护性,调试难度,性能影响,递归深度,模块化和重用性,测试难度。

稠密连接网络

从resnet到densenet

联系泰勒展开式,将一个函数分解成越来越高阶的项,同样将resnet展开为:

一个简单线性项和一个复杂的非线性项,想将f(x)拓展为超过两部分的信息?
DenseNet与ResNet的区别在于,Densenet输出是连接,而不是简单相加。
![[Pasted image 20240403195132.png]]

稠密块体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
from torch import nn
from d2l import torch as d2l

def conv_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))

class DenseBlock(nn.Module):
def __init__(self, num_convs, input_channels, num_channels):
super(DenseBlock, self).__init__()
layer = []
for i in range(num_convs):
layer.append(conv_block(
num_channels * i + input_channels, num_channels))
self.net = nn.Sequential(*layer)

def forward(self, X):
for blk in self.net:
Y = blk(X)
# 连接通道维度上每个块的输入和输出
X = torch.cat((X, Y), dim=1)
return X

ResNet和DenseNet的关键区别在于,DenseNet输出是连接,而不是如ResNet的简单相加。
稠密完工罗主要由两部分组成,稠密快,过渡层,前者定义如何连接和输入,而后者控制通道数量,使其不太会复杂。

练习

  1. DenseNet的优点之一是其模型参数比ResNet小。为什么呢?
    • 特征重用,每一层输出会作为下一层的输入,意味着网络中的每一层都可以访问到之前所有层的特征图。这种设计显著减少了需要学习的参数数量,因为网络可以重用之前层的特征,而不是每次都从头开始学习新的特征。
    • 参数共享,可以在多个层之间共享权重。
    • densenet的设计允许在不牺牲性能的情况的下对网络进行压缩,通过一处或合并一些层,可以在保持网络性能的同时减少参数量。
  2. DenseNet一个诟病的问题是内存或显存消耗过多。有另一种方法来减少显存消耗吗?需要改变框架么?
    DenseNet由于其密集连接的特性,在训练时确实会消耗较多的内存或显存。这是因为在DenseNet中,每一层都需要接收前面所有层的特征图作为输入,这导致特征图数量随着层数的增加而呈平方级增长。
    • 共享内存分配
    • 压缩特征图(1x1卷积减少特征图通道数)
    • 混合精度训练,将一些变量存储为低精度,可以减少显存消耗。

      循环神经网络

      序列模型

      马尔可夫模型

      我们使用当前序列前一段时间内的状态进行估计当前状态。一阶马尔可夫模型(First-Order Markov Model):最简单的马尔可夫模型,它假设下一个状态的概率仅依赖于当前状态。一阶马尔可夫模型通常用状态转移矩阵来描述。

      练习

  3. 一位投资者依赖过去的回报来决定购买哪种证券的策略可能会遇到几个问题:
    证券回报可能具有时间序列特性,如季节性、趋势和周期性。单纯依赖历史回报可能无法充分捕捉这些特性,而需要更复杂的时间序列分析方法。
  4. 时间是向前推进的因果模型在文本分析中的适用程度有限。文本数据通常是静态的,不具有时间序列数据的动态特性。然而,因果模型可以用于理解文本内容之间的逻辑关系和因果链,例如在法律文件、历史文档或科学论文中分析不同概念和论点之间的联系。
  5. 隐变量自回归模型(如隐马尔可夫模型)可能在以下情况下用于捕捉数据的动力学模型:
  • 序列数据:当数据表现为一系列状态,并且状态转换可能依赖于之前的状态时,隐变量自回归模型可以用来捕捉状态转换的动态过程。
  • 不完全观测:当数据中的某些状态或变量无法直接观测到,或者观测到的数据存在噪声时,隐变量模型可以通过观测到的数据来推断隐状态的动态变化。

文本预处理

步骤

  1. 将文本作为字符串加载到内存中。
  2. 将字符串拆分为词元(如单词和字符)。
  3. 建立一个词表,将拆分的词元映射到数字索引。
  4. 将文本转换为数字索引序列,方便模型操作。

    数据集读取与词元化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',                       '090b5e7e70c295757f55df93cb0a180b9691891a')
    def read_time_machine(): #@save
    """将时间机器数据集加载到文本行的列表中"""
    with open(d2l.download('time_machine'), 'r') as f:
    lines = f.readlines()
    return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]

    lines = read_time_machine()
    print(f'# 文本总行数: {len(lines)}')
    print(lines[0])
    print(lines[10])
    def tokenize(lines, token='word'): #@save
    """将文本行拆分为单词或字符词元"""
    if token == 'word':
    return [line.split() for line in lines]
    elif token == 'char':
    return [list(line) for line in lines]
    else:
    print('错误:未知词元类型:' + token)

    tokens = tokenize(lines)
    for i in range(11):
    print(tokens[i])

    词表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    class Vocab:  #@save
    """文本词表"""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
    if tokens is None:
    tokens = []
    if reserved_tokens is None:
    reserved_tokens = []
    # 按出现频率排序
    counter = count_corpus(tokens)
    self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
    reverse=True)
    # 未知词元的索引为0
    self.idx_to_token = ['<unk>'] + reserved_tokens
    self.token_to_idx = {token: idx
    for idx, token in enumerate(self.idx_to_token)}
    for token, freq in self._token_freqs:
    if freq < min_freq:
    break
    if token not in self.token_to_idx:
    self.idx_to_token.append(token)
    self.token_to_idx[token] = len(self.idx_to_token) - 1
    def __len__(self):
    return len(self.idx_to_token)
    def __getitem__(self, tokens):
    if not isinstance(tokens, (list, tuple)):
    return self.token_to_idx.get(tokens, self.unk)
    return [self.__getitem__(token) for token in tokens]
    def to_tokens(self, indices):
    if not isinstance(indices, (list, tuple)):
    return self.idx_to_token[indices]
    return [self.idx_to_token[index] for index in indices]
    @property
    def unk(self): # 未知词元的索引为0
    return 0
    @property
    def token_freqs(self):
    return self._token_freqs
    def count_corpus(tokens): #@save
    """统计词元的频率"""
    # 这里的tokens是1D列表或2D列表
    if len(tokens) == 0 or isinstance(tokens[0], list):
    # 将词元列表展平成一个列表
    tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)
    使用时光机器数据集作为语料库来构建此表,然后打印前几个高频词元及其索引。
    1
    2
    vocab = Vocab(tokens)
    print(list(vocab.token_to_idx.items())[:10])

功能整合

在使用上述函数时,我们将所有功能打包到load_corpus_time_machine函数中, 该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def load_corpus_time_machine(max_tokens=-1):  #@save
"""返回时光机器数据集的词元索引列表和词表"""
lines = read_time_machine()
tokens = tokenize(lines, 'char')
vocab = Vocab(tokens)
# 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,
# 所以将所有文本行展平到一个列表中
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens]
return corpus, vocab

corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)

练习

  1. 词元化是一个关键的预处理步骤,它因语言而异。尝试找到另外三种常用的词元化文本的方法。
  • BPE(Byte-Pair Encoding) WordPiece SentencePiece
  1. 在本节的实验中,将文本词元为单词和更改Vocab实例的min_freq参数。这对词表大小有何影响?
    1
    2
    3
    4
    5
    6
    7
    lines = read_time_machine()
    tokens = tokenize(lines, 'word')
    vocab = Vocab(tokens, min_freq=3)
    corpus = [vocab[token] for line in tokens for token in line]
    if -1 > 0:
    corpus = corpus[:-1]
    len(corpus), len(vocab)
    词表大小变为(32775, 1420)

    语言模型和数据集

    语言模型的目标是估计序列的联合概率
    只需要抽取一个词元
    一个理想的语言模型能够基于模型本身生成自然文本。

    练习

  2. 假设训练数据集中有100,000个单词。一个四元语法需要存储多少个词频和相邻多词频率?
    (N-3) (N-2) (N-1) N词频和(N-3) (N-2) * (N-1) 相邻多词词频
  3. 我们如何对一系列对话建模?
    将每一段对话视为一个序列数据,用RNN处理这些序列。
  4. 一元语法、二元语法和三元语法的齐普夫定律的指数是不一样的,能设法估计么?
    齐普夫定律描述了词频与词序之间的幂律关系。对于一元语法、二元语法和三元语法,齐普夫定律的指数可能会有所不同,这取决于语言的特性和语料库的特定性质。估计这些指数通常需要对大量文本数据进行统计分析,通过拟合词频分布的尾部来确定。
  5. 想一想读取长序列数据的其他方法?
    使用滑动窗口技术,将长序列分割成多个较短的子序列,或者使用分段技术,将序列分成多个部分分别处理。
  6. 考虑一下我们用于读取长序列的随机偏移量。
    1. 为什么随机偏移量是个好主意?
      它允许模型在每次迭代中从长序列中随机选择一个子序列,从而增加训练过程中的多样性,并可能提高模型的泛化能力。
    2. 它真的会在文档的序列上实现完美的均匀分布吗?
      可能不会在文档的序列上实现完美的均匀分布,因为某些区域可能会被更频繁地采样。
    3. 要怎么做才能使分布更均匀?
      限制每个序列的重叠区域,或者使用更复杂的采样策略来引导模型关注序列的不同部分。
  7. 如果我们希望一个序列样本是一个完整的句子,那么这在小批量抽样中会带来怎样的问题?如何解决?
    一个句子可能被分割成两个或多个小批量,从而破坏了句子的完整性。可以使用桶排序,它将具有相似长度的序列分组到同一个小批量中,以保持句子的完整性。

循环神经网络

练习

  1. 如果我们使用循环神经网络来预测文本序列中的下一个字符,那么任意输出所需的维度是多少?
    等于词汇表的大小
  2. 为什么循环神经网络可以基于文本序列中所有先前的词元,在某个时间步表示当前词元的条件概率?
    它的设计允许网络在每个实践部接受新的输入并更新其内部状态。这种状态能够捕捉并保留序列中的历史信息,使得网络可以根据先前观察到的词元来预测下一个词元。
  3. 如果基于一个长序列进行反向传播,梯度会发生什么状况?
    可能会梯度消失或者梯度爆炸。在反向传播时,梯度需要通过序列的每个时间步进行多次链式求导,如果序列很长,这些连续的乘积操作可能导致梯度非常小或非常大,影响网络学习过程。
  4. 与本节中描述的语言模型相关的问题有哪些?
  • 如何选择和设计合适的词汇表,以便更好地捕捉语言的特性。
  • 如何确定最佳的模型架构,包括隐藏层的大小和层数,以及激活函数的选择。
  • 如何处理未知词汇,以及如何平衡词表的大小和模型的泛化能力。

循环神经网络的从零开始实现

独热编码

1
F.one_hot(torch.tensor([0,2]),len(vocab))

我们每次采样的小批量数据形状是二维张量: (批量大小,时间步数)。 one_hot函数将这样一个小批量数据转换成三维张量, 张量的最后一个维度等于词表大小(len(vocab))。 我们经常转换输入的维度,以便获得形状为 (时间步数,批量大小,词表大小)的输出。 这将使我们能够更方便地通过最外层的维度, 一步一步地更新小批量数据的隐状态。

初始化模型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
# 隐藏层参数
W_xh = normal((num_inputs, num_hiddens))
W_hh = normal((num_hiddens, num_hiddens))
b_h = torch.zeros(num_hiddens, device=device)
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params

网络模型

1
2
3
4
5
6
7
8
9
10
11
12
13
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
def rnn(inputs, state, params):
# inputs的形状:(时间步数量,批量大小,词表大小)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)

rnn函数定义了如何在一个时间步内计算因状态和输出。循环神经网络模型通过inputs最外层的维度实现循环,一边逐时间步更新批量数据的隐状态H,此外,这里使用tanh函数作为激活函数。
接下来需要创建一个类来包装这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RNNModelScratch: #@save
"""从零开始实现的循环神经网络模型"""
def __init__(self, vocab_size, num_hiddens, device,
get_params, init_state, forward_fn):
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
self.params = get_params(vocab_size, num_hiddens, device)
self.init_state, self.forward_fn = init_state, forward_fn

def __call__(self, X, state):
X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
return self.forward_fn(X, state, self.params)

def begin_state(self, batch_size, device):
return self.init_state(batch_size, self.num_hiddens, device)

训练

  1. 序列数据的不同采样方法(随机采样和顺序分区)将导致隐状态初始化的差异。
  2. 我们在更新模型参数之前裁剪梯度。 这样的操作的目的是,即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。
  3. 我们用困惑度来评价模型。所述, 这样的度量确保了不同长度的序列具有可比性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #@save
    def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练网络一个迭代周期(定义见第8章)"""
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2) # 训练损失之和,词元数量
    for X, Y in train_iter:
    if state is None or use_random_iter:
    # 在第一次迭代或使用随机抽样时初始化state
    state = net.begin_state(batch_size=X.shape[0], device=device)
    else:
    if isinstance(net, nn.Module) and not isinstance(state, tuple):
    # state对于nn.GRU是个张量
    state.detach_()
    else:
    # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
    for s in state:
    s.detach_()
    y = Y.T.reshape(-1)
    X, y = X.to(device), y.to(device)
    y_hat, state = net(X, state)
    l = loss(y_hat, y.long()).mean()
    if isinstance(updater, torch.optim.Optimizer):
    updater.zero_grad()
    l.backward()
    grad_clipping(net, 1)
    updater.step()
    else:
    l.backward()
    grad_clipping(net, 1)
    updater(batch_size=1)
    metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

训练和预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
"""训练模型(定义见第8章)"""
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
# 初始化
if isinstance(net, nn.Module):
updater = torch.optim.SGD(net.parameters(), lr)
else:
updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# 训练和预测
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8(
net, train_iter, loss, updater, device, use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('time traveller'))
animator.add(epoch + 1, [ppl])
print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))


num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

练习

  1. 尝试说明独热编码等价于为每个对象选择不同的嵌入表示。
    在文本处理中,每个词元(如单词或字符)都会被分配一个唯一的索引,独热编码就是根据这个索引创建一个与词汇表大小相同的向量,其中对应索引位置的元素为1,其余为0。这种表示方法等价于为每个对象选择一个不同的嵌入向量,其中嵌入向量是预先定义好的,而不是通过学习得到的。
  2. 在不裁剪梯度的情况下运行本节中的代码会发生什么?
    可能会导致数值不稳定,如梯度爆炸,这会影响模型的训练过程和最终性能。
  3. 更改顺序划分,使其不会从计算图中分离隐状态。运行时间会有变化吗?困惑度呢?
    可能会影响模型的训练效率和稳定性。这种改变可能会使隐状态在每次迭代中保持连续,从而有助于梯度的传播和模型的学习。
  4. 用ReLU替换本节中使用的激活函数,并重复本节中的实验。我们还需要梯度裁剪吗?为什么?
    可能会影响模型的性能。ReLU激活函数在正区间内保持线性,而在负区间内输出为0。这种特性使得ReLU在正向传播时能够保持梯度不衰减,有助于缓解梯度消失问题。在某些情况下,使用ReLU可以减少对梯度裁剪的需求,因为它相对于其他激活函数(如sigmoid或tanh)在正区间内具有恒定的梯度。

循环神经网络的简介实现

1
2
3
4
5
6
7
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

模型定义

1
2
3
4
5
6
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
# 我们使用张量来初始化隐状态,它的形状是(隐藏层数,批量大小,隐藏单元数)。
state = torch.zeros((1, batch_size, num_hiddens))
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)

定义一个RNN类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#@save
class RNNModel(nn.Module):
"""循环神经网络模型"""
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)

def forward(self, inputs, state):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.rnn(X, state)
# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state

def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,batch_size, self.num_hiddens),
device=device)
else:
# nn.LSTM以元组作为隐状态
return (torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),device=device),
torch.zeros((
self.num_directions *self.rnn.num_layers,batch_size, self.num_hiddens), device=device))

训练与预测

1
2
3
4
device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
d2l.predict_ch8('time traveller', 10, net, vocab, device)

练习

  1. 尝试使用高级API,能使循环神经网络模型过拟合吗?
    尤其是当数据集相对较小或者模型过于复杂时。高级API通常提供了更多的模型配置选项和优化技术,如正则化、dropout等,这些技术可以帮助减少过拟合的风险。然而,如果不正确使用这些技术,或者在训练过程中没有适当的验证和调整,模型仍然可能会在训练数据上过度拟合,导致泛化能力下降。
  2. 如果在循环神经网络模型中增加隐藏层的数量会发生什么?能使模型正常工作吗?
    可以增加模型的复杂度和学习能力,这在处理复杂任务时可能是有益的。然而,增加隐藏层的数量也可能导致几个问题:首先,它可能会增加模型训练的难度,因为更多的参数需要被优化;其次,它可能会增加过拟合的风险,因为模型可能会学习到数据中的噪声而不是潜在的模式。为了使增加隐藏层的模型正常工作,需要仔细调整超参数,可能还需要更多的训练数据和更复杂的正则化技术。

现代循环神经网络

门控循环单元(GRU)

练习

  1. 假设我们只想使用时间步t′的输入来预测时间步t>t′的输出。对于每个时间步,重置门和更新门的最佳值是什么?
  • 重置门:rt​ 应该接近0,这表示我们不希望将过去的隐藏状态信息融入到当前的候选隐藏状态中。
  • 更新门:zt​ 应该接近1,这表示我们希望保留大部分的当前输入信息,而不是过去的隐藏状态。
  1. 调整和分析超参数对运行时间、困惑度和输出顺序的影响。
  • 学习率:较高的学习率可能导致模型快速收敛,但也可能导致在最优解附近的震荡或发散。较低的学习率则可能导致模型收敛速度缓慢。
  • 隐藏层大小:较大的隐藏层可以捕捉更复杂的特征,但可能导致过拟合和更长的训练时间。较小的隐藏层可能无法捕捉所有必要的信息,导致欠拟合。
  • 批次大小:较大的批次可以提供更稳定的梯度估计,但需要更多的内存,并且可能需要更多的迭代来训练模型。较小的批次可能需要更多的迭代,但每次迭代的计算量较小。
  • 迭代次数:更多的迭代次数可以给模型更多的时间来学习数据,但也可能导致过拟合和不必要的计算。较少的迭代次数可能导致模型未能充分学习。
  1. 比较rnn.RNNrnn.GRU的不同实现对运行时间、困惑度和输出字符串的影响。
  • 运行时间:GRU通常具有更少的参数和更简单的结构,这可能导致在某些情况下训练速度更快。
  • 困惑度:GRU由于其门控机制,通常能够更好地捕捉长距离依赖关系,这可能导致较低的困惑度和更好的模型性能。
  • 输出字符串:GRU的输出可能更加流畅和连贯,因为它能够更有效地避免梯度消失问题,从而在长序列中保持信息的传递。
  1. 如果仅仅实现门控循环单元的一部分,例如,只有一个重置门或一个更新门会怎样?
    没有更新门,模型可能无法有效地决定何时应该更新其隐藏状态;没有重置门,模型可能无法决定何时应该忘记过去的信息。这可能导致模型无法捕捉到序列中的重要动态,从而影响其性能和预测能力。

    长短期记忆网络(LSTM)

    LSTM的每个单元包含以下几个关键的组成部分:
  2. 遗忘门(Forget Gate):决定哪些信息应该被丢弃或保留。它通过一个sigmoid函数来输出一个介于0到1之间的值,0表示完全遗忘,而1表示完全保留。
  3. 输入门(Input Gate):决定哪些新的信息应该被添加到细胞状态中。它由两部分组成:一个sigmoid层决定哪些值我们将要更新,和一个tanh层创建一个新的候选值向量,这些值将会被加入到状态中。
  4. 细胞状态(Cell State):是LSTM的核心,它在整个序列中传递相关的信息。细胞状态的更新是通过遗忘门和输入门的组合来实现的。
  5. 输出门(Output Gate):基于当前的细胞状态和输入,决定最终的输出。输出门的输出通过tanh函数处理,并将结果与sigmoid门的输出相乘,以决定最终输出的活跃度。
    LSTM的这些门控机制使得网络能够有选择性地保留或遗忘信息,从而有效地处理长期依赖关系。这使得LSTM在许多序列数据任务中,如语言模型、机器翻译、语音识别等领域,都取得了显著的成功。
    ![[Pasted image 20240406190204.png]]

    练习

  6. 调整和分析超参数对运行时间、困惑度和输出顺序的影响。
    增加隐藏层的神经元数量可能会导致模型的训练时间增加,因为需要更多的时间来计算和更新更多的权重。同时,这也可能影响模型的困惑度,即模型对测试数据的不确定性的度量。如果模型过于复杂,可能会导致过拟合,从而在训练数据上表现良好但在测试数据上表现不佳,从而增加困惑度。
  7. 如何更改模型以生成适当的单词,而不是字符序列?
    可以通过更改模型的输出层和训练数据来实现。
    首先,需要将文本数据预处理为单词级别的表示,而不是字符级别的表示。这意味着需要构建一个单词到整数的映射,并在训练数据中将每个单词转换为相应的整数序列。然后,模型的输出层应该设计为预测下一个单词的概率分布,而不是下一个字符。这通常通过在RNN的顶部添加一个全连接层来实现,该层的输出维度等于词汇表的大小。在训练过程中,使用单词级别的标签(即正确的下一个单词)作为目标,而不是字符级别的标签。
  8. 在给定隐藏层维度的情况下,比较门控循环单元、长短期记忆网络和常规循环神经网络的计算成本。要特别注意训练和推断成本。
    门控循环单元(GRU)和长短期记忆网络(LSTM)都是为了解决常规循环神经网络(RNN)在处理长序列时遇到的梯度消失问题而设计的。在隐藏层维度给定的情况下,LSTM和GRU通常比常规RNN具有更多的参数和更复杂的结构,因为它们引入了门控机制来控制信息的流动。这可能会导致它们的训练成本高于常规RNN,因为需要更多的计算资源来更新和优化这些额外的参数。然而,这种增加的计算成本可能会通过减少训练时间(因为模型能够更快地学习长距离依赖关系)和提高模型性能来补偿。在推断阶段,LSTM和GRU可能会比常规RNN慢,因为门控机制需要额外的计算来决定信息的保留和遗忘。
  9. 既然候选记忆元通过使用tanh函数来确保值范围在(−1,1)之间,那么为什么隐状态需要再次使用tanh函数来确保输出值范围在(−1,1)之间呢?
    在LSTM中,候选记忆元是通过tanh函数处理的,确保其值范围在[-1, 1]之间。然而,LSTM的隐状态是记忆元的一个加权和,其中包括旧的隐状态和新的候选记忆元。隐状态需要通过tanh函数处理,以确保其值范围在[0, 1]之间,这是因为隐状态在LSTM中充当着网络的“记忆”,并且需要反映为一个概率分布,表示信息的重要性和应该被保留的程度。tanh函数的输出范围与LSTM的设计目标相匹配,即保留重要的信息并遗忘不重要的信息。
  10. 实现一个能够基于时间序列进行预测而不是基于字符序列进行预测的长短期记忆网络模型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    import torch
    import torch.nn as nn
    import torch.optim as optim
    # x_train的形状应该是(样本数量,时间步长,特征数量)
    # y_train的形状应该是(样本数量,输出长度)
    # 定义LSTM模型
    class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
    super(LSTMModel, self).__init__()
    self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
    self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
    lstm_out, (hidden, cell) = self.lstm(x)
    # 只使用最后一个时间步的输出
    output = self.fc(lstm_out[:, -1, :])
    return output
    # 设定超参数
    input_size = 4 # 特征数量
    hidden_size = 50 # LSTM隐藏层大小
    num_layers = 1 # LSTM层数
    output_size = 1 # 输出长度,对于回归问题通常为1
    learning_rate = 0.01
    batch_size = 32
    num_epochs = 100
    # 创建模型实例
    model = LSTMModel(input_size, hidden_size, num_layers, output_size)
    # 损失函数和优化器
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    # 训练模型
    for epoch in range(num_epochs):
    for i in range(0, len(x_train), batch_size):
    batch_x = x_train[i:i+batch_size]
    batch_y = y_train[i:i+batch_size]
    inputs = torch.tensor(batch_x, dtype=torch.float32)
    targets = torch.tensor(batch_y, dtype=torch.float32)
    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, targets)
    loss.backward()
    optimizer.step()
    if (epoch+1) % 10 == 0:
    print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')
    # 测试模型
    # 假设x_test是你要进行预测的时间序列数据
    x_test = np.random.rand(1, 1, 4)
    inputs = torch.tensor(x_test, dtype=torch.float32)
    predicted = model(inputs)
    print(predicted)

深度循环神经网络

  1. 尝试从零开始实现两层循环神经网络。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import torch
    import torch.nn as nn

    class TwoLayerLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
    super(TwoLayerLSTM, self).__init__()
    self.hidden_size = hidden_size
    self.num_layers = num_layers
    self.lstm_layers = nn.ModuleList([nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) for _ in range(2)])
    self.output_layer = nn.Linear(hidden_size, output_size)

    def forward(self, x):
    lstm_out, (hidden, cell) = self.lstm_layers[0](x)
    lstm_out, (hidden, cell) = self.lstm_layers[1](lstm_out)
    output = self.output_layer(lstm_out[:, -1, :])
    return output, (hidden, cell)

    # 假设输入数据的维度为10,隐藏层大小为20,输出大小为1
    model = TwoLayerLSTM(input_size=10, hidden_size=20, num_layers=2, output_size=1)
  2. 在本节训练模型中,比较使用门控循环单元替换长短期记忆网络后模型的精确度和训练速度。
    在实际应用中,GRU通常比LSTM有更快的训练速度,因为GRU的门控机制相对简单,参数数量较少。然而,LSTM通常在处理长期依赖关系方面表现得更好,因为它具有更复杂的门控机制。在比较两者的精确度和训练速度时,需要根据具体任务和数据集进行实验。一般来说,如果任务对长期依赖关系的要求不高,GRU可能是一个更快、更高效的选择。如果任务需要捕捉复杂的长期依赖关系,LSTM可能是更好的选择。
  3. 如果增加训练数据,能够将困惑度降到多低?
    增加训练数据通常有助于提高模型的性能,包括降低困惑度。困惑度是衡量模型对测试数据的不确定性的指标,较低的困惑度意味着模型对数据的预测更准确。理论上,随着训练数据的增加,模型可以学习到更多的模式和依赖关系,从而提高其对测试数据的预测能力。然而,降低困惑度的幅度也受到数据质量、模型容量和训练策略等因素的影响。
  4. 在为文本建模时,是否可以将不同作者的源数据合并?有何优劣呢?
    可以增加数据的多样性和丰富性,有助于提高模型的泛化能力。然而,不同作者的写作风格和用词习惯可能存在显著差异,这可能会影响模型的训练效果。

双向循环神经网络

练习

  1. 设计一个具有多个隐藏层的双向循环神经网络。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import torch
    import torch.nn as nn

    class MultiLayerBiLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
    super(MultiLayerBiLSTM, self).__init__()
    self.hidden_size = hidden_size
    self.num_layers = num_layers
    self.bi_lstm = nn.ModuleList([
    nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
    for _ in range(num_layers)
    ])
    self.output_layer = nn.Linear(hidden_size * 2, output_size) # 双向输出需要合并

    def forward(self, x):
    outputs = []
    for lstm in self.bi_lstm:
    lstm_out, (hidden, cell) = lstm(x)
    outputs.append(lstm_out)
    x = lstm_out
    # 合并所有隐藏层的输出
    combined_output = torch.cat(outputs, dim=-1)
    output = self.output_layer(combined_output[:, -1, :])
    return output

    # 假设输入数据的维度为10,隐藏层大小为20,输出大小为1
    model = MultiLayerBiLSTM(input_size=10, hidden_size=20, num_layers=2, output_size=1)
  2. 在自然语言中一词多义很常见。例如,“bank”一词在不同的上下文“i went to the bank to deposit cash”和“i went to the bank to sit down”中有不同的含义。如何设计一个神经网络模型,使其在给定上下文序列和单词的情况下,返回该单词在此上下文中的向量表示?哪种类型的神经网络架构更适合处理一词多义?
    可以使用编码器-解码器(Encoder-Decoder)架构,其中编码器负责生成上下文的表示,解码器则利用这个表示来生成目标单词的向量。
    模型设计:
    • 上下文编码器:使用Bi-LSTM或Transformer架构来编码输入句子的上下文信息。这种双向结构能够捕捉到每个词前后的依赖关系,从而为每个词生成一个包含上下文信息的向量表示。
    • 注意力机制:在解码器中引入注意力机制,它可以动态地聚焦于编码器输出的上下文表示中最相关的部分。这样,模型可以根据当前处理的单词来调整其对上下文的关注点。
    • 解码器:解码器可以是另一个LSTM或GRU层,它使用注意力加权的上下文向量来生成目标单词的表示。

机器翻译与数据集

练习

  1. load_data_nmt函数中尝试不同的num_examples参数值。这对源语言和目标语言的词表大小有何影响?
    load_data_nmt函数中尝试不同的num_examples参数值会影响源语言和目标语言的词表大小。num_examples参数决定了从数据集中抽取多少个样本来构建词表。如果num_examples的值较小,那么词表可能会不够全面,因为它只考虑了较少的样本。这可能导致一些单词没有被包含在词表中,特别是那些较少出现的单词。相反,如果num_examples的值较大,词表将更加全面,因为它考虑了更多的样本,从而可能包含更多的单词。然而,一个非常大的num_examples值也可能导致词表过于庞大,包含许多在特定任务中并不重要的单词。
  2. 某些语言(例如中文和日语)的文本没有单词边界指示符(例如空格)。对于这种情况,单词级词元化仍然是个好主意吗?为什么?
    子词级词元化可以帮助模型更好地理解文本的结构,同时避免了将单词错误地分割的问题。此外,使用基于词典或基于统计的分词方法可以有效地处理这些语言的文本,因为这些方法能够识别出有意义的词汇单元,而不是简单地基于字符进行分割。这样,模型可以更准确地捕捉到语言的特征,并提高处理效果。

编码器-解码器结构

机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出, 我们可以设计一个包含两个主要组件的架构: 第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。 这被称为编码器-解码器(encoder-decoder)架构。

编码器

在编码器接口中,我们只指定长度可变的序列作为编码器的输入X。 任何继承这个Encoder基类的模型将完成代码实现。

1
2
3
4
5
6
7
8
from torch import nn
#@save
class Encoder(nn.Module):
"""编码器-解码器架构的基本编码器接口"""
def __init__(self, **kwargs):
super(Encoder, self).__init__(**kwargs)
def forward(self, X, *args):
raise NotImplementedError

解码器
1
2
3
4
5
6
7
8
9
#@save
class Decoder(nn.Module):
"""编码器-解码器架构的基本解码器接口"""
def __init__(self, **kwargs):
super(Decoder, self).__init__(**kwargs)
def init_state(self, enc_outputs, *args):
raise NotImplementedError
def forward(self, X, state):
raise NotImplementedError

合并编码器和解码器

1
2
3
4
5
6
7
8
9
10
11
12
#@save
class EncoderDecoder(nn.Module):
"""编码器-解码器架构的基类"""
def __init__(self, encoder, decoder, **kwargs):
super(EncoderDecoder, self).__init__(**kwargs)
self.encoder = encoder
self.decoder = decoder

def forward(self, enc_X, dec_X, *args):
enc_outputs = self.encoder(enc_X, *args)
dec_state = self.decoder.init_state(enc_outputs, *args)
return self.decoder(dec_X, dec_state)

练习

  1. 假设我们使用神经网络来实现“编码器-解码器”架构,那么编码器和解码器必须是同一类型的神经网络吗?
    不一定是同一类型的网络
  2. 除了机器翻译,还有其它可以适用于”编码器-解码器“架构的应用吗?
    语音识别,图像描述,视频内容理解,聊天对话模型

序列到序列学习(seq2seq)

遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。 为了连续生成输出序列的词元, 独立的循环神经网络解码器是基于输入序列的编码信息 和输出序列已经看见的或者生成的词元来预测下一个词元。

练习

  1. 重新运行实验并在计算损失时不使用遮蔽,可以观察到什么结果?为什么会有这个结果?
    模型可能会在预测时错误地将注意力放在填充的位置上。这会导致模型学习到错误的依赖关系,因为它把填充的部分也当作了有效的输入。结果通常是模型性能下降,因为它不能正确地区分真实的序列数据和用于保持序列长度一致性的填充数据。遮蔽的目的是告诉模型哪些位置是真实的数据,哪些是填充数据,从而确保模型只在有效的数据上进行学习。
  2. 如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐状态?
    解码器的隐状态通常可以从编码器的最后一个时间步的隐状态进行初始化。这样做是因为编码器的最终状态被认为包含了整个输入序列的上下文信息,它可以作为解码器的初始上下文。
  3. 在训练中,如果用前一时间步的预测输入到解码器来代替强制教学,对性能有何影响?
    这可能会导致模型性能下降。teacher forcing是一种训练策略,它迫使模型在每个时间步使用真实的目标词作为输入,即使这些目标词在推理时可能不可用。如果移除这个策略,模型可能会生成不准确的预测,因为它不再依赖于真实的目标序列进行学习,而是依赖于自己的预测。这可能导致模型陷入错误累积的循环,从而降低生成序列的准确性。
  4. 有没有其他方法来设计解码器的输出层?
  • 注意力机制:通过注意力机制,输出层可以聚焦于输入序列中的相关部分来生成每个输出。
  • 层归一化:在输出层之前使用层归一化可以帮助稳定训练过程。
  • 残差连接:引入残差连接可以帮助信息在网络中更有效地流动,防止梯度消失。
  • 不同的激活函数:根据任务的不同,可以尝试使用不同的激活函数,如tanh或ReLU,来改善模型的性能。

束搜索

贪心搜索

它在每一步都选择当前看起来最优的选择,而不考虑长远的后果。这种方法简单且易于实现,但在某些情况下可能无法找到全局最优解,因为它可能在早期步骤中就锁定了次优的路径。
在机器翻译中,贪心搜索可能会在每个时间步选择最有可能的单词作为翻译的一部分,直到生成完整的句子。然而,这种方法可能会导致输出的连贯性和准确性问题,因为它没有考虑整个句子的全局最优性。
在训练过程中,贪心搜索可以用于解码器的输出,其中模型在每个时间步生成一个输出,而不是等待整个序列生成完成。这种方法的优点是速度快,但可能导致生成的序列质量不高,因为它没有利用后续时间步的信息来优化当前的输出。

穷举搜索

如果目标是获得最优序列, 我们可以考虑使用穷举搜索(exhaustive search): 穷举地列举所有可能的输出序列及其条件概率, 然后计算输出条件概率最高的一个。

束搜索

束搜索(beam search)是贪心搜索的一个改进版本。 它有一个超参数,名为束宽(beam size)k。 在时间步1,我们选择具有最高条件概率的k个词元。 这k个词元将分别是k个候选输出序列的第一个词元。 在随后的每个时间步,基于上一时间步的k个候选输出序列, 我们将继续从k|y个可能的选择中 挑出具有最高条件概率的k个候选输出序列。
![[Pasted image 20240406202313.png]]

练习

  1. 我们可以把穷举搜索看作一种特殊的束搜索吗?为什么?
    穷举搜索可以被看作是束宽为1的特殊束搜索。在穷举搜索中,搜索算法会考虑所有可能的候选解,直到找到最优解或满足某些条件为止。相比之下,束搜索在每一步只保留一定数量的最佳候选解(即束宽),并在此基础上继续搜索。束宽为1时,束搜索在每一步只保留一个候选解,这与穷举搜索中考虑所有可能解的方式非常相似。
  2. 9.7节的机器翻译问题中应用束搜索。 束宽是如何影响预测的速度和结果的?
    束宽决定了在每一步保留的候选解的数量:
    • 较小的束宽(如1)可以加快搜索速度,因为需要评估的候选解数量较少。但这也可能导致搜索过程错过一些优质的候选解,从而影响翻译质量。
    • 较大的束宽可以增加找到高质量翻译的概率,因为它考虑了更多的候选解。然而,这也意味着需要更多的计算资源和时间来评估这些候选解,从而减慢搜索速度。 因此,选择合适的束宽需要在搜索质量和效率之间做出权衡。

      注意力机制

      “是否包含自主性提示”将注意力机制与全连接层或汇聚层区别开来。 在注意力机制的背景下,自主性提示被称为查询(query)。 给定任何查询,注意力机制通过注意力汇聚(attention pooling) 将选择引导至感官输入(sensory inputs,例如中间特征表示)。 在注意力机制中,这些感官输入被称为值(value)。 更通俗的解释,每个值都与一个键(key)配对, 这可以想象为感官输入的非自主提示。

      练习

  3. 在机器翻译中通过解码序列词元时,其自主性提示可能是什么?非自主性提示和感官输入又是什么?
    在机器翻译的上下文中,自主性提示通常指的是模型在生成翻译时依赖于其先前生成的输出,而不是外部输入。例如,在解码过程中,模型可能会根据已经生成的词序列来决定下一个最合适的词。这种提示体现了模型在生成翻译时的自主性和连贯性,因为它完全依赖于内部状态和已生成的序列。
  4. 随机生成一个10×10矩阵并使用softmax运算来确保每行都是有效的概率分布,然后可视化输出注意力权重。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import numpy as np
    import matplotlib.pyplot as plt

    # 随机生成一个3x5的矩阵,代表注意力权重
    attention_weights = np.random.rand(3, 5)

    # 使用softmax运算来归一化每行,确保它们是有效的概率分布
    attention_distribution = np.exp(attention_weights) / np.sum(np.exp(attention_weights), axis=1, keepdims=True)

    # 可视化注意力权重
    plt.figure(figsize=(10, 5))
    for i in range(attention_distribution.shape[0]):
    plt.plot(range(1, attention_distribution.shape[1] + 1), attention_distribution[i, :], label=f'Decoder Position {i+1}')
    plt.xlabel('Position')
    plt.ylabel('Attention Weight')
    plt.title('Attention Weights Distribution')
    plt.legend()
    plt.show()

    注意力汇聚:Nadaraya-Watson核回归

    生成数据集

    给定的成对的输入数据集,根据某非线性函数生成一个人工数据集,加入噪声项。生成50个训练样本和50个测试样本。

    平均汇聚

    先使用最简单的估计器来解决回归问题。 基于平均汇聚来计算所有训练样本输出值的平均值

    非参数注意力汇聚

    显然,平均汇聚忽略了输入xi。 于是Nadaraya 和 Watson提出了一个更好的想法,根据输入的位置对输出yi进行加权。其中KI是核(kernel)

    带参数注意力汇聚

    非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点: 如果有足够的数据,此模型会收敛到最优结果。 尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。

    批量矩阵乘法

    为了更有效地计算小批量数据的注意力, 我们可以利用深度学习开发框架中提供的批量矩阵乘法。
    1
    2
    3
    X = torch.ones((2, 1, 4))
    Y = torch.ones((2, 4, 6))
    torch.bmm(X, Y).shape
    在注意力机制的背景中,我们可以使用小批量矩阵乘法来计算小批量数据中的加权平均值。
    1
    2
    3
    weights = torch.ones((2, 10)) * 0.1
    values = torch.arange(20.0).reshape((2, 10))
    torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1))

    练习

  5. 增加训练数据的样本数量,能否得到更好的非参数的Nadaraya-Watson核回归模型?
    增加训练数据的样本数量通常能够提升非参数的Nadaraya-Watson核回归模型的性能。非参数方法不对数据的基础结构做任何先验假设,因此它们能够从更多的数据中学习并捕捉到更复杂的模式。
  6. 在带参数的注意力汇聚的实验中学习得到的参数w的价值是什么?为什么在可视化注意力权重时,它会使加权区域更加尖锐?
    在带参数的注意力汇聚中,学习到的参数可以捕捉输入数据中的重要特征和结构。这些参数有助于模型更好地理解哪些部分的输入对于预测任务最为关键。注意力权重的可视化通常会显示出一个加权区域,这个区域突出了输入数据中与预测目标最相关的部分。当注意力权重集中在一个较小的、尖锐的区域时,这意味着模型已经学习到专注于输入数据中的特定部分,从而提高了预测的准确性和解释性。
  7. 如何将超参数添加到非参数的Nadaraya-Watson核回归中以实现更好地预测结果?
    可以考虑引入正则化项或者调整核函数的带宽。正则化可以帮助防止过拟合,而带宽参数则控制了模型对数据局部波动的平滑程度。通过调整这些超参数,可以优化模型以适应特定的预测任务。
  8. 为本节的核回归设计一个新的带参数的注意力汇聚模型。训练这个新模型并可视化其注意力权重。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    	import torch
    import torch.nn as nn
    import torch.nn.functional as F

    class ParameterizedAttentionModel(nn.Module):
    def __init__(self, input_dim, attention_dim):
    super(ParameterizedAttentionModel, self).__init__()
    self.query LinearLayer(input_dim, attention_dim)
    self.key LinearLayer(input_dim, attention_dim)
    self.value LinearLayer(input_dim, attention_dim)
    self.output LinearLayer(attention_dim, 1)
    def forward(self, inputs):
    queries = self.query(inputs)
    keys = self.key(inputs)
    values = self.value(inputs)
    attention_scores = torch.matmul(queries, keys.transpose(-2, -1))
    attention_weights = F.softmax(attention_scores, dim=-1)
    attended_values = torch.matmul(attention_weights, values)
    output = self.output(attended_values)
    return output
    # 假设输入维度为10,注意力维度为5
    model = ParameterizedAttentionModel(input_dim=10, attention_dim=5)

    注意力评分函数

    (10.2.6)中的 高斯核指数部分可以视为注意力评分函数(attention scoring function), 简称评分函数(scoring function), 然后把这个函数的输出结果输入到softmax函数中进行运算。 通过上述步骤,将得到与键对应的值的概率分布(即注意力权重)。 最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。
    ![[Pasted image 20240408043455.png]]

    掩蔽softmax操作

    softmax操作用于输出一个概率分布作为注意力权重。 在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #@save
    def masked_softmax(X, valid_lens):
    """通过在最后一个轴上掩蔽元素来执行softmax操作"""
    # X:3D张量,valid_lens:1D或2D张量
    if valid_lens is None:
    return nn.functional.softmax(X, dim=-1)
    else:
    shape = X.shape
    if valid_lens.dim() == 1:
    valid_lens = torch.repeat_interleave(valid_lens, shape[1])
    else:
    valid_lens = valid_lens.reshape(-1)
    # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
    X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
    value=-1e6)
    return nn.functional.softmax(X.reshape(shape), dim=-1)
    为了演示此函数是如何工作的, 考虑由两个2×4矩阵表示的样本, 这两个样本的有效长度分别为2和3。 经过掩蔽softmax操作,超出有效长度的值都被掩蔽为0。
    1
    masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))

    加性注意力

    一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #@save
    class AdditiveAttention(nn.Module):
    """加性注意力"""
    def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
    super(AdditiveAttention, self).__init__(**kwargs)
    self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
    self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
    self.w_v = nn.Linear(num_hiddens, 1, bias=False)
    self.dropout = nn.Dropout(dropout)

    def forward(self, queries, keys, values, valid_lens):
    queries, keys = self.W_q(queries), self.W_k(keys)
    # 在维度扩展后,
    # queries的形状:(batch_size,查询的个数,1,num_hidden)
    # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
    # 使用广播方式进行求和
    features = queries.unsqueeze(2) + keys.unsqueeze(1)
    features = torch.tanh(features)
    # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
    # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
    scores = self.w_v(features).squeeze(-1)
    self.attention_weights = masked_softmax(scores, valid_lens)
    # values的形状:(batch_size,“键-值”对的个数,值的维度)
    return torch.bmm(self.dropout(self.attention_weights), values)
    用一个小例子来演示上面的AdditiveAttention类, 其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小), 实际输出为(2,1,20)、(2,10,2)和(2,10,4)。 注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。

    练习

  9. 修改小例子中的键,并且可视化注意力权重。可加性注意力和缩放的“点-积”注意力是否仍然产生相同的结果?为什么?
    我们首先需要了解注意力权重是如何计算的。在典型的注意力机制中,如“点-积”注意力,权重是通过查询(Q)和键(K)的点积来计算的,然后通常会除以一个缩放因子,最后应用softmax函数来获得概率分布。如果我们要修改键,我们需要确保修改后的键仍然能够与查询进行有效的交互,以便计算出有意义的注意力权重。可视化注意力权重通常涉及到将权重矩阵以图形的形式展示出来,这样可以直观地看到模型在处理输入序列时对不同位置的关注度。
  10. 只使用矩阵乘法,能否为具有不同矢量长度的查询和键设计新的评分函数?
    只使用矩阵乘法可能不足以处理具有不同矢量长度的查询和键。在标准的注意力机制中,查询和键的长度通常是相同的,以便进行点积操作。如果查询和键的长度不同,可能需要引入额外的步骤来调整它们的长度,或者设计一个能够处理不同长度输入的评分函数。例如,可以通过嵌入层将查询和键映射到相同长度的空间,或者设计一个基于对齐或匹配的评分函数,而不是简单的点积。
  11. 当查询和键具有相同的矢量长度时,矢量求和作为评分函数是否比“点-积”更好?为什么?
    当查询和键具有相同的矢量长度时,矢量求和作为评分函数可能不如“点-积”注意力有效。原因在于点-积能够更好地捕捉查询和键之间的相似性,因为它考虑了查询和键的每个维度之间的相互作用。而矢量求和可能会忽略这些维度之间的相互作用,导致无法有效地捕捉查询和键之间的关系。

    Bahdanau 注意力

    模型

    ![[Pasted image 20240408043946.png]]

    定义注意力解码器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #@save
    class AttentionDecoder(d2l.Decoder):
    """带有注意力机制解码器的基本接口"""
    def __init__(self, **kwargs):
    super(AttentionDecoder, self).__init__(**kwargs)

    @property
    def attention_weights(self):
    raise NotImplementedError
    接下来,让我们在接下来的Seq2SeqAttentionDecoder类中 实现带有Bahdanau注意力的循环神经网络解码器。 首先,初始化解码器的状态,需要下面的输入:
  12. 编码器在所有时间步的最终层隐状态,将作为注意力的键和值;
  13. 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态;
  14. 编码器有效长度(排除在注意力池中填充词元)。
    在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作查询。 因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    class Seq2SeqAttentionDecoder(AttentionDecoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
    dropout=0, **kwargs):
    super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
    self.attention = d2l.AdditiveAttention(
    num_hiddens, num_hiddens, num_hiddens, dropout)
    self.embedding = nn.Embedding(vocab_size, embed_size)
    self.rnn = nn.GRU(
    embed_size + num_hiddens, num_hiddens, num_layers,
    dropout=dropout)
    self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, enc_valid_lens, *args):
    # outputs的形状为(batch_size,num_steps,num_hiddens).
    # hidden_state的形状为(num_layers,batch_size,num_hiddens)
    outputs, hidden_state = enc_outputs
    return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)
    def forward(self, X, state):
    # enc_outputs的形状为(batch_size,num_steps,num_hiddens).
    # hidden_state的形状为(num_layers,batch_size,
    # num_hiddens)
    enc_outputs, hidden_state, enc_valid_lens = state
    # 输出X的形状为(num_steps,batch_size,embed_size)
    X = self.embedding(X).permute(1, 0, 2)
    outputs, self._attention_weights = [], []
    for x in X:
    # query的形状为(batch_size,1,num_hiddens)
    query = torch.unsqueeze(hidden_state[-1], dim=1)
    # context的形状为(batch_size,1,num_hiddens)
    context = self.attention(
    query, enc_outputs, enc_outputs, enc_valid_lens)
    # 在特征维度上连结
    x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
    # 将x变形为(1,batch_size,embed_size+num_hiddens)
    out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
    outputs.append(out)
    self._attention_weights.append(self.attention.attention_weights)
    # 全连接层变换后,outputs的形状为
    # (num_steps,batch_size,vocab_size)
    outputs = self.dense(torch.cat(outputs, dim=0))
    return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,
    enc_valid_lens]
    @property
    def attention_weights(self):
    return self._attention_weights

    多头注意力

    多头注意力(Multi-head Attention)是一种用于增强神经网络模型对序列数据(如文本或时间序列)建模能力的注意力机制。它是 Transformer 模型中的关键组件之一。

在传统的注意力机制中,模型通过计算查询(query)、键(key)和值(value)之间的关联来为每个查询选择相关的值。而多头注意力通过引入多组查询-键-值投影矩阵,从而使模型能够在不同的表示空间中学习关注不同方面的信息。每个头产生的注意力权重矩阵被独立地计算,然后这些矩阵被拼接在一起并经过另一个线性变换,最终得到多头注意力的输出。

多头注意力的主要优势在于它可以让模型在不同的表示子空间中学习到不同的语义信息,从而提高模型对复杂关系的建模能力。在 Transformer 中,多头注意力被用来同时捕捉不同位置的关系和不同层次的语义信息,使得模型能够更好地处理长距离依赖性。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#@save
class MultiHeadAttention(nn.Module):
"""多头注意力"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
num_heads, dropout, bias=False, **kwargs):
super(MultiHeadAttention, self).__init__(**kwargs)
self.num_heads = num_heads
self.attention = d2l.DotProductAttention(dropout)
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)

def forward(self, queries, keys, values, valid_lens):
# queries,keys,values的形状:
# (batch_size,查询或者“键-值”对的个数,num_hiddens)
# valid_lens 的形状:
# (batch_size,)或(batch_size,查询的个数)
# 经过变换后,输出的queries,keys,values 的形状:
# (batch_size*num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
queries = transpose_qkv(self.W_q(queries), self.num_heads)
keys = transpose_qkv(self.W_k(keys), self.num_heads)
values = transpose_qkv(self.W_v(values), self.num_heads)

if valid_lens is not None:
# 在轴0,将第一项(标量或者矢量)复制num_heads次,
# 然后如此复制第二项,然后诸如此类。
valid_lens = torch.repeat_interleave(
valid_lens, repeats=self.num_heads, dim=0)

# output的形状:(batch_size*num_heads,查询的个数,
# num_hiddens/num_heads)
output = self.attention(queries, keys, values, valid_lens)

# output_concat的形状:(batch_size,查询的个数,num_hiddens)
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat)

同时定义transpose_qkv,转置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#@save
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads,
# num_hiddens/num_heads)
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)
# 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3)
# 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数,
# num_hiddens/num_heads)
return X.reshape(-1, X.shape[2], X.shape[3])
#@save
def transpose_output(X, num_heads):
"""逆转transpose_qkv函数的操作"""
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
X = X.permute(0, 2, 1, 3)
return X.reshape(X.shape[0], X.shape[1], -1)

练习

  1. 分别可视化这个实验中的多个头的注意力权重。

  2. 假设有一个完成训练的基于多头注意力的模型,现在希望修剪最不重要的注意力头以提高预测速度。如何设计实验来衡量注意力头的重要性呢?
    为了衡量注意力头的重要性并设计实验进行修剪,我们可以采用以下方法:
    首先,我们需要定义一个性能指标,如准确率、F1分数或模型的损失值,来评估模型在不同任务上的表现。
    逐个或逐步关闭(修剪)注意力头,并观察性能指标的变化。每次关闭一个或多个头后,重新训练模型,并记录性能变化。
    分析性能变化与修剪的头之间的关系。如果关闭某些头后性能下降不明显,这可能表明这些头不是很重要。相反,如果性能显著下降,则表明这些头对模型的预测至关重要。
    根据重要性分析的结果,迭代地调整模型结构,移除不重要的头,并重新训练模型。这个过程可能需要多次迭代,直到找到一个性能和效率之间的最佳平衡点。

自注意力与位置编码

想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中, 以便同一组词元同时充当查询、键和值。 具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。 由于查询、键和值来自同一组输入,因此被称为 自注意力(self-attention)

位置编码

在处理词元序列时,循环神经网络是逐个的重复地处理词元的, 而自注意力则因为并行计算而放弃了顺序操作。 为了使用序列的顺序信息,通过在输入表示中添加 位置编码(positional encoding)来注入绝对的或相对的位置信息。 位置编码可以通过学习得到也可以直接固定得到。基于正弦函数和余弦函数的固定位置编码:
假设输入表示$X∈R^{nd}$ 包含一个序列中n个词元的d维嵌入表示。 位置编码使用相同形状的位置嵌入矩阵 $P∈R^{nd}$输出X+P, 矩阵第i行、第2j列和2j+1列上的元素为:

在位置嵌入矩阵P中, 行代表词元在序列中的位置,列代表位置编码的不同维度。 从下面的例子中可以看到位置嵌入矩阵的第6列和第7列的频率高于第8列和第9列。 第6列和第7列之间的偏移量(第8列和第9列相同)是由于正弦函数和余弦函数的交替。

练习

  1. 假设设计一个深度架构,通过堆叠基于位置编码的自注意力层来表示序列。可能会存在什么问题?
    位置编码的固定性,难以捕捉序列中的长期依赖关系,计算复杂度增加,过拟合风险。
  2. 请设计一种可学习的位置编码方法。
    在可学习的位置编码中,位置编码不再是固定的,而是作为模型参数的一部分。这意味着位置编码可以通过训练过程进行优化。在上述代码示例中,self.positional_embedding是一个可学习的参数,其形状为[max_position, num_features]。这个矩阵存储了从位置0到max_position的每个位置的编码。
    位置编码的自适应性: 由于位置编码是可学习的,模型可以根据数据中的模式和序列的特点自适应地调整位置编码。这使得模型能够更好地处理各种长度的序列,并且能够捕捉到序列中位置信息的复杂性。

    Transformer

    ![[Pasted image 20240408050453.png]]

基于位置的前馈网络

基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP)。输入X的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,ffn_num_outputs)的输出张量。

1
2
3
4
5
6
7
8
9
10
11
12
#@save
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs):
super(PositionWiseFFN, self).__init__(**kwargs)
self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)

def forward(self, X):
return self.dense2(self.relu(self.dense1(X)))

用同一个多层感知机对所有位置上的输入进行变换,当这些位置的输入相同时,输出也是相同的。
1
2
3
ffn = PositionWiseFFN(4, 4, 8)
ffn.eval()
ffn(torch.ones((2, 3, 4)))[0]

残差连接和层规范化

以下代码对比不同维度的层规范化和批量规范化的效果。

1
2
3
4
5
ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))

现在可以使用残差连接和层规范化来实现AddNorm类。暂退法也被作为正则化方法使用。
1
2
3
4
5
6
7
8
9
10
#@save
class AddNorm(nn.Module):
"""残差连接后进行层规范化"""
def __init__(self, normalized_shape, dropout, **kwargs):
super(AddNorm, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
self.ln = nn.LayerNorm(normalized_shape)

def forward(self, X, Y):
return self.ln(self.dropout(Y) + X)

残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。
1
2
3
add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape

编码器

有了组成Transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的EncoderBlock类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#@save
class EncoderBlock(nn.Module):
"""Transformer编码器块"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, use_bias=False, **kwargs):
super(EncoderBlock, self).__init__(**kwargs)
self.attention = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout,
use_bias)
self.addnorm1 = AddNorm(norm_shape, dropout)
self.ffn = PositionWiseFFN(
ffn_num_input, ffn_num_hiddens, num_hiddens)
self.addnorm2 = AddNorm(norm_shape, dropout)

def forward(self, X, valid_lens):
Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
return self.addnorm2(Y, self.ffn(Y))

验证Transformer编码器中任何层都不会改变骑输入的形状:
1
2
3
4
5
6
X = torch.ones((2, 100, 24))
valid_lens = torch.tensor([3, 2])
encoder_blk = EncoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5)
encoder_blk.eval()
encoder_blk(X, valid_lens).shape
# torch.Size([2, 100, 24])

下面实现的Transformer编码器的代码中,堆叠了num_layersEncoderBlock类的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#@save
class TransformerEncoder(d2l.Encoder):
"""Transformer编码器"""
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
EncoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, use_bias))

def forward(self, X, valid_lens, *args):
# 因为位置编码值在-1和1之间,
# 因此嵌入值乘以嵌入维度的平方根进行缩放,
# 然后再与位置编码相加。
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self.attention_weights = [None] * len(self.blks)
for i, blk in enumerate(self.blks):
X = blk(X, valid_lens)
self.attention_weights[
i] = blk.attention.attention.attention_weights
return X

下面我们指定了超参数来创建一个两层的Transformer编码器。 Transformer编码器输出的形状是(批量大小,时间步数目,num_hiddens)。
1
2
3
4
5
encoder = TransformerEncoder(
200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5)
encoder.eval()
encoder(torch.ones((2, 100), dtype=torch.long), valid_lens).shape
# torch.Size([2, 100, 24])

解码器

Transformer解码器也是由多个相同的层组成。在DecoderBlock类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class DecoderBlock(nn.Module):
"""解码器中第i个块"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, i, **kwargs):
super(DecoderBlock, self).__init__(**kwargs)
self.i = i
self.attention1 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm1 = AddNorm(norm_shape, dropout)
self.attention2 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm2 = AddNorm(norm_shape, dropout)
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens,
num_hiddens)
self.addnorm3 = AddNorm(norm_shape, dropout)

def forward(self, X, state):
enc_outputs, enc_valid_lens = state[0], state[1]
# 训练阶段,输出序列的所有词元都在同一时间处理,
# 因此state[2][self.i]初始化为None。
# 预测阶段,输出序列是通过词元一个接着一个解码的,
# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
if state[2][self.i] is None:
key_values = X
else:
key_values = torch.cat((state[2][self.i], X), axis=1)
state[2][self.i] = key_values
if self.training:
batch_size, num_steps, _ = X.shape
# dec_valid_lens的开头:(batch_size,num_steps),
# 其中每一行是[1,2,...,num_steps]
dec_valid_lens = torch.arange(
1, num_steps + 1, device=X.device).repeat(batch_size, 1)
else:
dec_valid_lens = None
# 自注意力
X2 = self.attention1(X, key_values, key_values, dec_valid_lens)
Y = self.addnorm1(X, X2)
# 编码器-解码器注意力。
# enc_outputs的开头:(batch_size,num_steps,num_hiddens)
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.addnorm2(Y, Y2)
return self.addnorm3(Z, self.ffn(Z)), state

为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是num_hiddens
1
2
3
4
5
6
decoder_blk = DecoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5, 0)
decoder_blk.eval()
X = torch.ones((2, 100, 24))
state = [encoder_blk(X, valid_lens), valid_lens, [None]]
decoder_blk(X, state)[0].shape
# torch.Size([2, 100, 24])

将多个block拼接成decoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class TransformerDecoder(d2l.AttentionDecoder):
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, **kwargs):
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
DecoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, i))
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, enc_valid_lens, *args):
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
for i, blk in enumerate(self.blks):
X, state = blk(X, state)
# 解码器自注意力权重
self._attention_weights[0][
i] = blk.attention1.attention.attention_weights
# “编码器-解码器”自注意力权重
self._attention_weights[1][
i] = blk.attention2.attention.attention_weights
return self.dense(X), state
@property
def attention_weights(self):
return self._attention_weights

练习

  1. 在实验中训练更深的Transformer将如何影响训练速度和翻译效果?
    可能会增加训练时间和计算资源的需求。随着层数的增加,模型的参数数量也会增加,这可能导致梯度下降过程中的优化变得更加复杂和困难。同时,更多的层意味着更多的注意力计算,这会增加内存消耗和计算时间。
  2. 在Transformer中使用加性注意力取代缩放点积注意力是不是个好办法?为什么?
    加性注意力通过一个加性而非点积的方式来计算注意力分数。这种方法可能有助于模型捕捉不同类型的依赖关系,因为它提供了一种不同的机制来组合查询和键的信息。
  3. 对于语言模型,应该使用Transformer的编码器还是解码器,或者两者都用?如何设计?
    对于语言模型,通常使用Transformer的解码器部分,因为解码器能够处理序列数据并生成下一个词元的预测。然而,在某些情况下,也可以结合编码器和解码器,例如在机器翻译任务中。编码器可以处理源语言文本,而解码器则生成目标语言的翻译。
  4. 如果输入序列很长,Transformer会面临什么挑战?为什么?
    首先,长序列会增加自注意力层的计算复杂度,因为每个时间步都需要与序列中的所有其他时间步进行交互。这会导致显著的计算和内存开销。其次,长序列可能导致梯度消失或爆炸问题,影响模型的训练稳定性和效率。
  5. 如何提高Transformer的计算速度和内存使用效率?提示:可以参考论文 (Tay et al., 2020)。
    可以使用量化来减少模型参数的位宽,从而减少计算和存储需求。还可以使用模型剪枝技术去除不重要的权重,简化模型结构。或使用局部注意力或稀疏注意力。
  6. 如果不使用卷积神经网络,如何设计基于Transformer模型的图像分类任务?提示:可以参考Vision Transformer
    可以设计Vision Transformer(ViT)来进行图像分类任务。ViT将图像分割成一系列的小块(patches),然后将这些小块线性嵌入到一个序列中,就像处理文本序列一样。然后,使用标准的Transformer架构来处理这个序列,包括自注意力层和前馈网络层。ViT能够捕捉图像中的全局依赖关系,并且在多个图像分类任务上取得了良好的性能。设计时,可以调整块的大小、序列的长度以及Transformer层的配置来适应不同的图像和任务需求。

优化算法

优化和深度学习

优化的目标

主要关注最小化目标,由于优化算法发目标函数通常是基于训练数据集发损失函数,因此优化发目标时减少训练误差。但深度学习的目标是减少泛化误差。为了实现后者,除了使用优化算法来减少训练误差之外,还要注意过拟合。

深度学习中的优化挑战

对于任何目标函数f(x),如果在x处对应的f(x)值小于在x附近任意其他点的f(x)值,那么f(x)可能是局部最小值。如果f(x)在x处的值是整个域中目标函数的最小值,那么f(x)是全局最小值。

鞍点

除了局部最小值之外,鞍点是梯度消失的另一个原因。鞍点(saddle point)是指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。考虑这个函数$f(x)=x^3$。它的一阶和二阶导数在x=0时消失。这时优化可能会停止,尽管它不是最小值。

练习

  1. 考虑一个简单的MLP,它有一个隐藏层,比如,隐藏层中维度为d和一个输出。证明对于任何局部最小值,至少有d’个等效方案。
    我们至少可以构造 d 个等效方案,每个方案对应于隐藏层权重的一种调整。但由于输出层的权重和偏置也可以调整,实际上存在无限多个等效方案。这里的 d′ 可以理解为 d 个隐藏单元提供的基础对称性,而实际的等效方案数量是无限的。
  2. 你能想到深度学习优化还涉及哪些其他挑战?
    非凸优化问题,训练与验证的偏差,计算资源限制,超参数调整
  3. 假设你想在(真实的)鞍上平衡一个(真实的)球。
    为什么这很难?
    不稳定的平衡点:鞍点是一个既不是最高点也不是最低点的位置,球在这一点上容易受到扰动而滚动到其他位置,类似于优化过程中的鞍点,模型参数的微小变化可能导致性能显著下降。
    难以识别:鞍点可能在参数空间中不明显,难以被识别和区分,特别是在高维空间中。
    局部最小值的干扰:鞍点附近可能存在局部最小值,优化算法可能会被这些局部最小值吸引,从而陷入鞍点并难以逃脱。
    能利用这种效应来优化算法吗?
    使用动量:动量可以帮助模型在参数空间中保持速度,避免在鞍点附近停滞不前。
    学习率调整策略:通过调整学习率,可以在鞍点附近进行更细致的搜索,有助于模型越过鞍点继续向全局最小值前进。
    正则化技术:正则化可以帮助模型学习更平滑的权重配置,减少在鞍点附近震荡的可能性。

    凸性

    凸集

    对于任何$a,b\in X$连接a和b的线段也位于X中,则向量空间中的集合X是凸的。

    凸函数

    给定一个凸集X,对于所有x,x‘属于X,和所有$\lambda \in [0,1]$,函数f是凸的,我们可以得到:余弦函数是非凸的,抛物线和指数函数是凸的。

    詹森不等式

    凸函数的期望不小于期望的凸函数,其中后者通常是一个更简单的表达式。
    应用:用一个较简单的表达式约束一个较复杂的表达式。

    性质

    局部极小值是全局极小值
    凸函数的下水平集是凸的

    拉格朗日函数

    通常,求解一个有约束的优化问题是困难的,解决这个问题的一种方法来自物理中相当简单的直觉。 想象一个球在一个盒子里,球会滚到最低的地方,重力将与盒子两侧对球施加的力平衡。 简而言之,目标函数(即重力)的梯度将被约束函数的梯度所抵消(由于墙壁的“推回”作用,需要保持在盒子内)。 请注意,任何不起作用的约束(即球不接触壁)都将无法对球施加任何力。
    这里我们简略拉格朗日函数L的推导,上述推理可以通过以下鞍点优化问题来表示:

    练习

  4. 假设我们想要通过绘制集合内点之间的所有直线并检查这些直线是否包含来验证集合的凸性。i.证明只检查边界上的点是充分的。ii.证明只检查集合的顶点是充分的。
    i. 证明只检查边界上的点是充分的:
    集合的凸性意味着对于集合中的任意两点,这两点之间的线段都完全位于集合内。要验证一个集合是否为凸集,我们可以从集合内任选两点,并检查这两点之间的线段是否完全在集合内。边界上的点是集合中最接近“边缘”的点,如果集合是凸的,那么边界上的任意两点之间的线段都将完全在集合内,因为任何偏离这条线段的点都将在集合外。因此,如果边界上任意两点之间的线段都在集合内,那么集合内任意两点之间的线段也必然在集合内,因为它们是由边界上的点“夹”在中间的。所以,只检查边界上的点足以验证集合的凸性。
    ii. 证明只检查集合的顶点是充分的:
    顶点是集合边界上的特定点,它们是集合边界的“角”或最尖锐的部分。在凸集的定义中,如果集合是凸的,那么通过集合中任意两点的线段都将完全位于集合内。顶点是确定集合形状的关键点,因为它们定义了集合边界的转向。
    如果只检查顶点,我们可以观察顶点之间的连接线段是否完全在集合内。如果所有顶点之间的线段都在集合内,那么可以推断出集合是凸的,因为这些线段覆盖了集合的所有边界。此外,任何不在这些顶点线段上的点都位于这些线段之间,因此也必然在集合内。因此,只检查顶点足以验证集合的凸性。

梯度下降

随机梯度下降

小批量随机梯度下降

动量法

本节将探讨更有效的优化算法,尤其是针对实验中常见的某些类型的优化问题。
它旨在帮助加速梯度下降算法在相关方向上的收敛,并抑制在不相关方向上的震荡。动量法通过累积过去梯度的指数衰减平均来实现这一目标。

从零开始实现

相比于小批量随机梯度下降,动量方法需要维护一组辅助变量,即速度。 它与梯度以及优化问题的变量具有相同的形状。 在下面的实现中,我们称这些变量为states

1
2
3
4
5
6
7
8
9
10
11
def init_momentum_states(feature_dim):
v_w = torch.zeros((feature_dim, 1))
v_b = torch.zeros(1)
return (v_w, v_b)

def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
with torch.no_grad():
v[:] = hyperparams['momentum'] * v + p.grad
p[:] -= hyperparams['lr'] * v
p.grad.data.zero_()

在实验中运作:
1
2
3
4
5
6
7
def train_momentum(lr, momentum, num_epochs=2):
d2l.train_ch11(sgd_momentum, init_momentum_states(feature_dim),
{'lr': lr, 'momentum': momentum}, data_iter,
feature_dim, num_epochs)

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
train_momentum(0.02, 0.5)

![[Pasted image 20240408230734.png]]

简介实现

1
2
trainer = torch.optim.SGD
d2l.train_concise_ch11(trainer, {'lr': 0.005, 'momentum': 0.9}, data_iter)

练习

  1. 当我们执行带动量法的随机梯度下降时会有什么变化?当我们使用带动量法的小批量随机梯度下降时会发生什么?试验参数如何?
    带动量法的随机梯度下降(SGD)和小批量随机梯度下降(Mini-batch SGD)是两种常见的优化算法,它们在标准SGD的基础上引入了动量项,以加速训练过程并提高收敛性。下面是这两种方法的特点和变化:
  • 带动量法的随机梯度下降(SGD with Momentum)
    • 在每次迭代中,动量法不仅考虑当前梯度,还考虑之前梯度的加权平均,这有助于平滑梯度更新路径。
    • 动量项 vt​ 根据当前梯度和之前动量的加权平均进行更新,然后用于更新参数。
    • 这种方法可以减少SGD在优化过程中的震荡,特别是在面对噪声或非平稳目标函数时。
    • 动量法的SGD通常需要调整额外的超参数,即动量系数 γ,以及学习率 α。
  • 带动量法的小批量随机梯度下降(Momentum SGD with Mini-batches)
    • 在这种方法中,每次迭代使用一个小批量数据来计算梯度,然后结合动量项来更新参数。
    • 小批量方法可以提供更稳定的梯度估计,并且可以更有效地利用并行计算资源,如GPU。
    • 动量项同样有助于平滑梯度更新,减少震荡,并加速训练过程。
    • 使用小批量数据时,动量项的更新可能会受到批量大小的影响,因此可能需要根据批量大小调整动量系数 γ 和学习率 α。

      AdaGrad算法

      AdaGrad的核心思想是根据每个参数的历史梯度信息来调整其学习率,使得模型在训练过程中能够针对不同的参数采取不同的更新策略。
      AdaGrad算法的主要特点如下:
  1. 参数特定的学习率:AdaGrad会为模型中的每个参数分配一个单独的学习率,这样不同的参数可以以不同的速度进行更新。
  2. 累积梯度平方和:对于每个参数,AdaGrad会累积其梯度的平方和,这有助于捕捉到每个参数的更新频率和幅度。
  3. 自适应调整:随着训练的进行,每个参数的学习率会根据其累积的梯度信息自动调整。如果一个参数的梯度在训练过程中变化很大,其学习率会降低;反之,如果梯度变化较小,学习率会增加。
  4. 适用性:AdaGrad特别适合处理稀疏数据集,因为它可以针对数据中出现频率不同的特征采取不同的更新策略。

    简介实现

    1
    2
    trainer = torch.optim.Adagrad
    d2l.train_concise_ch11(trainer, {'lr': 0.1}, data_iter)

    练习

  5. 要如何修改AdaGrad算法,才能使其在学习率方面的衰减不那么激进?
    引入衰减因子,使用学习率预热,动态调整学习率

    RMSProp算法

    ![[Pasted image 20240408231934.png]]

    简介实现

    1
    2
    3
    trainer = torch.optim.RMSprop
    d2l.train_concise_ch11(trainer, {'lr': 0.01, 'alpha': 0.9},
    data_iter)

    Adadelta

    Adadelta是AdaGrad的另一种变体, 主要区别在于前者减少了学习率适应坐标的数量。 此外,广义上Adadelta被称为没有学习率,因为它使用变化量本身作为未来变化的校准。

    简介实现

    1
    2
    trainer = torch.optim.Adadelta
    d2l.train_concise_ch11(trainer, {'rho': 0.9}, data_iter)

    练习

  6. 将Adadelta的收敛行为与AdaGrad和RMSProp进行比较。
    Adadelta算法是对AdaGrad算法的改进,旨在解决AdaGrad学习率单调递减可能导致过早收敛的问题。
    AdaGrad通过累积梯度平方来自适应学习率,但可能导致学习率过早下降;RMSProp通过指数加权移动平均来调整学习率,改善了AdaGrad的这一缺点;而Adadelta则进一步改进了自适应学习率机制,通过两个指数加权移动平均来同时控制学习和更新的幅度,使得算法在训练过程中更加稳定和有效。

    Adam算法

    Adam(Adaptive Moment Estimation)算法是一种用于优化神经网络的随机梯度下降算法的变种,它结合了动量法(momentum)和自适应学习率的思想。
    Adam算法的核心思想是根据梯度的一阶矩估计(mean)和二阶矩估计(uncentered variance)来动态调整每个参数的学习率。具体而言,Adam算法会维护两个指数加权移动平均变量,分别表示梯度的一阶矩估计和二阶矩估计。这两个变量分别用来校正梯度的偏差和动态调整学习率。
    算法步骤如下:
  7. 初始化参数θ,一阶矩估计变量m和二阶矩估计变量v。
  8. 在每个时间步t:
    • 计算当前时间步的梯度gt。
    • 更新一阶矩估计变量m和二阶矩估计变量v:
      1
      2
      mt = β1 * mt-1 + (1 - β1) * gt
      vt = β2 * vt-1 + (1 - β2) * gt^2
  • 其中,β1和β2分别是控制一阶矩估计和二阶矩估计的指数衰减率。
  • 根据一阶矩估计的偏差修正mt和vt:
    1
    2
    m̂t = mt / (1 - β1^t)
    v̂t = vt / (1 - β2^t)
    更新参数θ:
    1
    θt+1 = θt - α * m̂t / (sqrt(v̂t) + ε)

    练习

  1. 试着重写动量和二次矩更新,从而使其不需要偏差校正。
  2. 收敛时为什么需要降低学习率$\eta$?
    当模型接近收敛时,学习率的降低有助于使模型在最优解附近更加稳定地收敛。在开始阶段,较大的学习率可以帮助模型更快地接近最优解,但随着训练的进行,学习率的减小可以使模型在最优解周围更精细地调整参数,避免在最优解附近波动。因此,逐渐降低学习率可以提高模型的收敛速度和稳定性。

    学习率调度器

  • 首先,学习率的大小很重要。如果它太大,优化就会发散;如果它太小,训练就会需要过长时间,或者我们最终只能得到次优的结果。直观地说,这是最不敏感与最敏感方向的变化量的比率。
  • 其次,衰减速率同样很重要。如果学习率持续过高,我们可能最终会在最小值附近弹跳,从而无法达到最优解。
  • 在训练期间逐步降低学习率可以提高准确性,并且减少模型的过拟合。
  • 在实验中,每当进展趋于稳定时就降低学习率,这是很有效的。从本质上说,这可以确保我们有效地收敛到一个适当的解,也只有这样才能通过降低学习率来减小参数的固有方差。
  • 余弦调度器在某些计算机视觉问题中很受欢迎。
  • 优化之前的预热期可以防止发散。
  • 优化在深度学习中有多种用途。对于同样的训练误差而言,选择不同的优化算法和学习率调度,除了最大限度地减少训练时间,可以导致测试集上不同的泛化和过拟合量。

    练习

  1. 如果改变学习率下降的指数,收敛性会如何改变?在实验中方便起见,使用PolyScheduler
    如果改变学习率下降的指数,收敛性会受到影响。较大的指数会导致学习率下降得更快,可能会导致模型在训练过程中跳过最优解附近的区域,从而影响最终的收敛性。较小的指数会导致学习率下降得更慢,可能需要更多的训练时间才能达到最优解。因此,选择合适的学习率下降指数是重要的,通常需要根据具体问题和模型来调整。
  2. 将余弦调度器应用于大型计算机视觉问题,例如训练ImageNet数据集。与其他调度器相比,它如何影响性能?
    将余弦调度器应用于大型计算机视觉问题(如训练ImageNet数据集)可以带来一些好处。余弦调度器在训练初期使用较大的学习率,有助于快速收敛到一个比较好的解,然后在训练后期逐渐降低学习率,以更精细地调整参数并提高模型的泛化能力。与其他调度器相比,余弦调度器可以在一定程度上提高模型的性能和泛化能力。
  3. 预热应该持续多长时间?
    预热的持续时间应该根据具体情况来确定。预热的目的是在训练开始时使用较大的学习率,有助于快速收敛到一个比较好的解。预热时间不宜过长,通常在几个epoch内即可。预热时间过长可能会导致模型在训练初期过度调整参数,影响最终的收敛性能。
  4. 可以试着把优化和采样联系起来吗?首先,在随机梯度朗之万动力学上使用的结果。
    优化和采样可以通过随机梯度朗之万动力学(SGRLD)来联系。SGRLD是一种融合了随机梯度下降和朗之万动力学的优化算法,它将梯度下降的更新规则与朗之万动力学的随机性相结合,可以更好地处理带噪声的优化问题。在SGRLD中,采样被用来引入随机性,以避免陷入局部最优解,并且可以更好地探索参数空间。

    计算性能

    python是一种解释性语言,按顺序执行函数体的操作。通过对e = add(a,b)求值,将结果存储为变量和e。
    尽管命令式编程很方便,但可能效率不高,一方面因为python会单独执行这三个函数的调用,而没有考虑add函数在fancy_func中倍重复调用。如果在一个GPU上执行这些命令,那么python解释器产生的凯西奥可能会非常大。此外,它需要保存e和f的值,指导函数中所有语句都执行完毕,这是因为程序不知道在执行语句e = add(a,b)和f = add(c,d)之后,其他部分是否会使用变量e和f。

    符号式编程

    代码只有在完全定义了过程之后才执行计算:
  5. 定义计算流程
  6. 将流程编译成可执行的程序
  7. 给定输入,调用编译好的程序执行
  • 命令式编程使得新模型的设计变得容易,因为可以依据控制流编写代码,并拥有相对成熟的Python软件生态。
  • 符号式编程要求我们先定义并且编译程序,然后再执行程序,其好处是提高了计算性能。

    异步计算

  • 深度学习框架可以将Python前端的控制与后端的执行解耦,使得命令可以快速地异步插入后端、并行执行。

  • 异步产生了一个相当灵活的前端,但请注意:过度填充任务队列可能会导致内存消耗过多。建议对每个小批量进行同步,以保持前端和后端大致同步。

  • 芯片供应商提供了复杂的性能分析工具,以获得对深度学习效率更精确的洞察。

    自动并行

    基于GPU的并行计算

    1
    2
    3
    4
    5
    6
    devices = d2l.try_all_gpus()
    def run(x):
    return [x.mm(x) for _ in range(50)]

    x_gpu1 = torch.rand(size=(4000, 4000), device=devices[0])
    x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1])

    练习

  1. 设计一个实验,在CPU和GPU这两种设备上使用并行计算和通信。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import numpy as np
    import time
    import torch

    # 创建随机矩阵
    size = 1000
    A = np.random.rand(size, size)
    B = np.random.rand(size, size)

    # CPU上的串行矩阵乘法
    start_time = time.time()
    C_cpu_serial = np.dot(A, B)
    cpu_serial_time = time.time() - start_time

    # GPU上的并行矩阵乘法
    A_gpu = torch.tensor(A, dtype=torch.float32).cuda()
    B_gpu = torch.tensor(B, dtype=torch.float32).cuda()
    start_time = time.time()
    C_gpu_parallel = torch.mm(A_gpu, B_gpu)
    gpu_parallel_time = time.time() - start_time

    # 将结果从GPU复制回CPU
    C_gpu_parallel = C_gpu_parallel.cpu().numpy()

    print(f"CPU串行计算时间:{cpu_serial_time}秒")
    print(f"GPU并行计算时间:{gpu_parallel_time}秒")

    多GPU训练

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    import torch
    from torch import nn
    from d2l import torch as d2l
    #@save
    def resnet18(num_classes, in_channels=1):
    """稍加修改的ResNet-18模型"""
    def resnet_block(in_channels, out_channels, num_residuals,
    first_block=False):
    blk = []
    for i in range(num_residuals):
    if i == 0 and not first_block:
    blk.append(d2l.Residual(in_channels, out_channels,
    use_1x1conv=True, strides=2))
    else:
    blk.append(d2l.Residual(out_channels, out_channels))
    return nn.Sequential(*blk)

    # 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
    net = nn.Sequential(
    nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
    nn.BatchNorm2d(64),
    nn.ReLU())
    net.add_module("resnet_block1", resnet_block(
    64, 64, 2, first_block=True))
    net.add_module("resnet_block2", resnet_block(64, 128, 2))
    net.add_module("resnet_block3", resnet_block(128, 256, 2))
    net.add_module("resnet_block4", resnet_block(256, 512, 2))
    net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1)))
    net.add_module("fc", nn.Sequential(nn.Flatten(),
    nn.Linear(512, num_classes)))
    return net

    net = resnet18(10)
    # 获取GPU列表
    devices = d2l.try_all_gpus()
    # 我们将在训练代码实现中初始化网络

    def train(net, num_gpus, batch_size, lr):
    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    devices = [d2l.try_gpu(i) for i in range(num_gpus)]
    def init_weights(m):
    if type(m) in [nn.Linear, nn.Conv2d]:
    nn.init.normal_(m.weight, std=0.01)
    net.apply(init_weights)
    # 在多个GPU上设置模型
    net = nn.DataParallel(net, device_ids=devices)
    trainer = torch.optim.SGD(net.parameters(), lr)
    loss = nn.CrossEntropyLoss()
    timer, num_epochs = d2l.Timer(), 10
    animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
    for epoch in range(num_epochs):
    net.train()
    timer.start()
    for X, y in train_iter:
    trainer.zero_grad()
    X, y = X.to(devices[0]), y.to(devices[0])
    l = loss(net(X), y)
    l.backward()
    trainer.step()
    timer.stop()
    animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
    print(f'测试精度:{animator.Y[0][-1]:.2f}{timer.avg():.1f}秒/轮,'
    f'在{str(devices)}')
    # 使用1个
    train(net, num_gpus=1, batch_size=256, lr=0.1)
    # 使用2个
    train(net, num_gpus=2, batch_size=512, lr=0.2)

    参数服务器

    当我们从一个GPU迁移到多个GPU时,以及再迁移到包含多个GPU的多个服务器时(可能所有服务器的分布跨越了多个机架和多个网络交换机),分布式并行训练算法也需要变得更加复杂。
  • 同步需要高度适应特定的网络基础设施和服务器内的连接,这种适应会严重影响同步所需的时间。
  • 环同步对于p3和DGX-2服务器是最佳的,而对于其他服务器则未必。
  • 当添加多个参数服务器以增加带宽时,分层同步策略可以工作的很好。

    练习

  1. 请尝试进一步提高环同步的性能吗。(提示:可以双向发送消息。)
    可以尝试使用双向发送消息。在传统的环同步中,每个进程在每个阶段只能发送或接收消息,而双向发送消息可以使得进程在每个阶段既可以发送也可以接收消息,从而提高了通信的效率。这样可以减少通信的次数,加快算法的收敛速度。
  2. 在计算仍在进行中,可否允许执行异步通信?它将如何影响性能?
    允许在计算仍在进行中执行异步通信可以提高性能,因为它可以使得计算和通信重叠进行,减少了计算和通信之间的等待时间。但是,需要注意的是,在使用异步通信时需要确保通信操作不会影响计算的正确性。
  3. 怎样处理在长时间运行的计算过程中丢失了一台服务器这种问题?尝试设计一种容错机制来避免重启计算这种解决方案?
    可以通过设计容错机制来避免重启计算。一种常见的容错机制是使用检查点和恢复技术,定期保存计算状态到持久存储器中,以便在发生故障时能够重新启动计算。另一种方法是使用冗余计算节点,在计算过程中同时在多个节点上执行相同的计算任务,当某个节点发生故障时,可以从其他节点恢复计算。

    计算机视觉

图像增广

我们可以以不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,减少模型对于对象出现位置的依赖。 我们还可以调整亮度、颜色等因素来降低模型对颜色的敏感度。

常用的方法

翻转和裁剪
改变颜色:亮度,对比度,饱和度,色调。

使用图像增广进行训练

练习

  1. 在不使用图像增广的情况下训练模型:train_with_data_aug(no_aug, no_aug)。比较使用和不使用图像增广的训练结果和测试精度。这个对比实验能支持图像增广可以减轻过拟合的论点吗?为什么?
    对比实验的结果可以支持图像增广可以减轻过拟合的论点。在实验中,使用图像增广的模型通常会在训练集上表现更好,同时在测试集上也能取得更好的泛化性能,即使在没有使用图像增广的情况下,模型可能会出现过拟合的现象。
    图像增广可以减轻过拟合的原因在于它可以增加训练数据的多样性,从而使得模型更加鲁棒。通过对训练图像进行随机变换,图像增广可以生成更多样化的训练样本,使得模型不容易记住训练集中的特定样本,从而降低过拟合的风险。
  2. 在基于CIFAR-10数据集的模型训练中结合多种不同的图像增广方法。它能提高测试准确性吗?
    在基于CIFAR-10数据集的模型训练中结合多种不同的图像增广方法可以提高测试准确性。通过使用多种不同的图像增广方法,可以进一步增加训练数据的多样性,使得模型更加鲁棒,从而提高模型在测试集上的泛化能力。
  3. 参阅深度学习框架的在线文档。它还提供了哪些其他的图像增广方法?
    平移(仿射变换),随即擦除,尺度变换

    微调

    使用迁移学习,从元数据学到的知识迁移到目标数据集。尽管Imagenet上数据集大多数与意思无关,但在此数据集上训练的模型可能会提取更通用的图像特征,这有助于识别边缘、纹理、形状和对象组合。
    微调包含以下步骤:
  4. 在源数据集(例如ImageNet数据集)上预训练神经网络模型,即源模型。
  5. 创建一个新的神经网络模型,即目标模型。这将复制源模型上的所有模型设计及其参数(输出层除外)。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出层与源数据集的标签密切相关;因此不在目标模型中使用该层。
  6. 向目标模型添加输出层,其输出数是目标数据集中的类别数。然后随机初始化该层的模型参数。
  7. 在目标数据集(如椅子数据集)上训练目标模型。输出层将从头开始进行训练,而所有其他层的参数将根据源模型的参数进行微调。

    目标检测和边界框

    边界框

    在目标检测中,我们通常使用边界框来描述对象的空间位置。边界框是举行的,由鞠总左上角和右下角的xy坐标来决定另一种是边界框中心坐标和框的宽度和高度。
    可以设计函数将两种表示方法进行转换,并在图像中可视化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    x = torch.tensor([1, 2, 3])
    y = torch.tensor([4, 5, 6])
    # 在新维度上堆叠张量
    stacked = torch.stack([x, y])
    # 输出: tensor([[1, 2, 3],
    # [4, 5, 6]])
    stacked = torch.stack([x, y], dim=1)
    print(stacked)
    # 输出: tensor([[1, 4],
    # [2, 5],
    # [3, 6]])
  8. torch.stack会在新维度上堆叠张量,而torch.cat会在现有维度上拼接张量。
  9. torch.stack要求所有要堆叠的张量具有相同的形状,而torch.cat要求除了沿着指定维度之外的其他维度具有相同的形状。

    锚框

    目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边界从而更准确地预测目标的真实边界框(ground-truth bounding box)。 不同的模型使用的区域采样方法可能不同。 这里我们介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。

    生成多个锚框

    要生成多个不同形状的锚框,让我们设置许多缩放比(scale)取值s1,…,sn和许多宽高比(aspect ratio)取值r1,…,rm。 当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有wℎnm个锚框。 尽管这些锚框可能会覆盖所有真实边界框,但计算复杂性很容易过高。 在实践中,我们只考虑包含s1或r1的组合:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #@save
    def multibox_prior(data, sizes, ratios):
    """生成以每个像素为中心具有不同形状的锚框"""
    in_height, in_width = data.shape[-2:]
    device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
    boxes_per_pixel = (num_sizes + num_ratios - 1)
    size_tensor = torch.tensor(sizes, device=device)
    ratio_tensor = torch.tensor(ratios, device=device)

    # 为了将锚点移动到像素的中心,需要设置偏移量。
    # 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
    offset_h, offset_w = 0.5, 0.5
    steps_h = 1.0 / in_height # 在y轴上缩放步长
    steps_w = 1.0 / in_width # 在x轴上缩放步长

    # 生成锚框的所有中心点
    center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)

    # 生成“boxes_per_pixel”个高和宽,
    # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
    w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
    sizes[0] * torch.sqrt(ratio_tensor[1:])))\
    * in_height / in_width # 处理矩形输入
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
    sizes[0] / torch.sqrt(ratio_tensor[1:])))
    # 除以2来获得半高和半宽
    anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
    in_height * in_width, 1) / 2

    # 每个中心点都将有“boxes_per_pixel”个锚框,
    # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
    dim=1).repeat_interleave(boxes_per_pixel, dim=0)
    output = out_grid + anchor_manipulations
    return output.unsqueeze(0)

    交并比(IoU)

    相交面积除以相并面积

    练习

  10. 在multibox_prior函数中更改sizes和ratios的值。生成的锚框有什么变化?
    multibox_prior函数中更改sizesratios的值会改变生成的锚框的大小和长宽比。sizes控制锚框的大小,ratios控制锚框的长宽比。更改这些参数会导致生成的锚框在大小和形状上有所变化。
  11. 构建并可视化两个IoU为0.5的边界框。它们是怎样重叠的?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import matplotlib.pyplot as plt
    import numpy as np
    import matplotlib.patches as patches
    # 创建两个边界框的坐标
    bbox1 = [0.3, 0.3, 0.6, 0.6] # (xmin, ymin, xmax, ymax)
    bbox2 = [0.5, 0.5, 0.8, 0.8]
    # 计算两个边界框的重叠部分
    overlap_xmin = max(bbox1[0], bbox2[0])
    overlap_ymin = max(bbox1[1], bbox2[1])
    overlap_xmax = min(bbox1[2], bbox2[2])
    overlap_ymax = min(bbox1[3], bbox2[3])
    # 计算重叠部分的面积
    overlap_area = max(0, overlap_xmax - overlap_xmin) * max(0, overlap_ymax - overlap_ymin)
    # 计算两个边界框的面积
    area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
    area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
    # 计算IoU
    iou = overlap_area / (area1 + area2 - overlap_area)
    # 可视化边界框
    fig, ax = plt.subplots()
    ax.add_patch(patches.Rectangle((bbox1[0], bbox1[1]), bbox1[2] - bbox1[0], bbox1[3] - bbox1[1], fill=False))
    ax.add_patch(patches.Rectangle((bbox2[0], bbox2[1]), bbox2[2] - bbox2[0], bbox2[3] - bbox2[1], fill=False))
    plt.xlim(0, 1)
    plt.ylim(0, 1)
    plt.gca().invert_yaxis() # 反转y轴,使得原点在左上角
    plt.show()
    print(f"IoU: {iou}")
  12. 非极大值抑制是一种贪心算法,它通过移除来抑制预测的边界框。是否存在一种可能,被移除的一些框实际上是有用的?
    对于非极大值抑制(NMS),确实存在一些情况下被移除的边界框实际上是有用的。为了柔和地抑制,可以使用Soft-NMS算法,该算法通过减少重叠边界框的得分而不是完全删除它们来实现更平滑的抑制。Soft-NMS的关键思想是根据重叠的程度逐渐降低边界框的得分,而不是直接将其移除。
  13. 如何修改这个算法来柔和地抑制?可以参考Soft-NMS (Bodla et al., 2017)。 如果非手动,非最大限度的抑制可以被学习吗?
    非手动、非最大限度的抑制可以被学习,这通常通过训练一个边界框回归器来实现。边界框回归器的目标是预测每个边界框的位置和大小,从而使得最终的检测结果更加准确。这种方法可以在训练过程中学习到更有效的抑制策略,而不是简单地使用固定的阈值或规则来选择最佳边界框。

    多尺度目标检测

    如果为每个像素都生成的锚框,我们最终可能会得到太多需要计算的锚框。
    减少图像上的锚框数量并不困难。 比如,我们可以在输入图像中均匀采样一小部分像素,并以它们为中心生成锚框。 此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。 直观地说,比起较大的目标,较小的目标在图像上出现的可能性更多样。 例如,1×1、1×2和2×2的目标可以分别以4、2和1种可能的方式出现在2×2图像上。 因此,当使用较小的锚框检测较小的物体时,我们可以采样更多的区域,而对于较大的物体,我们可以采样较少的区域。
  • 在多个尺度下,我们可以生成不同尺寸的锚框来检测不同尺寸的目标。
  • 通过定义特征图的形状,我们可以决定任何图像上均匀采样的锚框的中心。
  • 我们使用输入图像在某个感受野区域内的信息,来预测输入图像上与该区域位置相近的锚框类别和偏移量。
  • 我们可以通过深入学习,在多个层次上的图像分层表示进行多尺度目标检测。

    练习

  1. 给定形状为1×c×ℎ×w的特征图变量,其中c、ℎ和w分别是特征图的通道数、高度和宽度。怎样才能将这个变量转换为锚框类别和偏移量?输出的形状是什么?
    使用一个1×1的卷积层将特征图变量转换为形状为1×(4k+c)×h×w的特征图。这个卷积层的输出通道数应为4k+c。将这个特征图展平为形状为1×((4k+c)×h×w)的向量。
    使用一个全连接层将展平后的向量转换为形状为1×((4k+1)×h×w)的向量。这个全连接层的输出大小应为(4k+1)×h×w,其中4k是偏移量的数量,1是类别预测的数量。
    将全连接层的输出重新整形为形状为1×(4k+1)×h×w的特征图,其中4k+1是每个位置的类别和偏移量的总数。
    最终输出的形状是1×(4k+1)×h×w,其中1表示批次大小为1。这个特征图包含每个位置的锚框类别和偏移量预测。

    目标检测数据集

    单发多框检测

    ![[Pasted image 20240409202220.png]]

    区域卷积神经网络

    区域卷积神经网络(R-CNN)系列是一系列用于目标检测的深度学习模型,主要包括R-CNN、Fast R-CNN、Faster R-CNN和Mask R-CNN等。这些模型在目标检测领域取得了重大突破,成为了目标检测任务中的经典模型。
  2. R-CNN(Region-based Convolutional Neural Network):R-CNN是最早的一种区域卷积神经网络模型,它通过选择性搜索(Selective Search)算法提取候选区域,并对每个候选区域进行卷积神经网络的特征提取和目标分类,从而实现目标检测。但是,R-CNN的速度较慢,因为它需要对每个候选区域分别进行卷积运算。
  3. Fast R-CNN:Fast R-CNN对R-CNN进行了改进,提出了候选区域池化(RoI Pooling)层,将整个图像的特征图输入到卷积神经网络中,然后通过RoI Pooling层将每个候选区域映射到特征图上,并提取固定大小的特征。这样可以避免对每个候选区域都进行卷积运算,提高了速度和效率。
  4. Faster R-CNN:Faster R-CNN在Fast R-CNN的基础上进一步改进,引入了区域提议网络(Region Proposal Network,RPN),用于生成候选区域。RPN通过在特征图上滑动一个小窗口来生成候选区域,并利用分类分支和回归分支对候选区域进行分类和精细化定位。这样可以将目标检测任务分为两个阶段:生成候选区域和目标分类定位,从而进一步提高了速度和效率。
  5. Mask R-CNN:Mask R-CNN在Faster R-CNN的基础上添加了一个额外的分支用于实例分割。在目标检测的基础上,Mask R-CNN可以生成每个检测到的目标的精确掩码,从而实现目标的精确分割。
    R-CNN系列模型的发展,使得目标检测在准确率和效率上取得了巨大的提升,成为了目标检测领域的重要里程碑。

    练习

  6. 我们能否将目标检测视为回归问题(例如预测边界框和类别的概率)?可以参考YOLO模型 (Redmon et al., 2016)的设计。
    目标检测可以被视为回归问题,其中目标是预测边界框的位置和大小,以及每个边界框内物体类别的概率。YOLO模型(You Only Look Once)是一个将目标检测视为回归问题的经典模型,它在单个神经网络中同时预测多个边界框的位置和类别概率。

    与YOLO相比,单发多框检测(Single Shot MultiBox Detection,SSD)也是一种将目标检测视为回归问题的方法,但它采用了不同的设计思路。主要区别包括:

    1. 网络结构:YOLO采用全卷积网络结构,将整个图像作为输入并直接输出预测边界框和类别概率的特征图。而SSD在特征图上应用一系列卷积和池化操作,然后在不同层次上预测不同尺度和长宽比的边界框。

    2. 多尺度特征:SSD利用不同层次的特征图来检测不同尺度的物体,从而提高了检测的准确性。而YOLO将所有预测都放在单个特征图上,可能会导致对小物体的检测效果不佳。

    3. 预测方式:YOLO使用单个全连接层来预测边界框和类别概率,而SSD在不同层次上使用卷积层来预测,这样可以提高模型对不同尺度物体的检测能力。

语义分割和数据集

计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation)。 我们在这里将它们同语义分割简单区分一下。

  • 图像分割:将图像划分为若干组成区域,这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。
  • 实例分割也叫同时检测并分割(simultaneous detection and segmentation),它研究如何识别图像中各个目标实例的像素级区域。与语义分割不同,实例分割不仅需要区分语义,还要区分不同的目标实例。例如,如果图像中有两条狗,则实例分割需要区分像素属于的两条狗中的哪一条。

    数据集

    Pascal VOC2012 语义分割数据集

    练习

  1. 如何在自动驾驶和医疗图像诊断中应用语义分割?还能想到其他领域的应用吗?
    语义分割可以帮助自动驾驶系统理解道路上不同区域的含义,如车道线、行人、车辆和路标等。这对于决策制定和车辆行驶路径规划非常重要。
    语义分割可以帮助医生准确地识别和分割出不同的组织结构或病变区域,如肿瘤、器官等,从而提高诊断的准确性和效率。
    农业,建筑与城市规划,环境保护
  2. 回想一下 13.1节中对数据增强的描述。图像分类中使用的哪种图像增强方法是难以用于语义分割的?
    随机裁剪和随机翻转,对于语义分割来说并不适用。因为这些方法改变了图像的空间信息和像素分布,可能会导致语义分割结果不准确。

    转置卷积

    用于逆转下采样导致的空间尺寸减少
    1
    2
    3
    tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
    tconv.weight.data = K
    tconv(X)
    与常规卷积不同,在转置卷积中,填充被应用于的输出(常规卷积将填充应用于输入)。 例如,当将高和宽两侧的填充数指定为1时,转置卷积的输出中将删除第一和最后的行与列。

    练习

  3. 13.10.3节中,卷积输入X和转置的卷积输出Z具有相同的形状。他们的数值也相同吗?为什么?
    不相同
  4. 使用矩阵乘法来实现卷积是否有效率?为什么?
    使用矩阵乘法来实现卷积在某些情况下可能是有效率的,但在一般情况下通常不是最优的选择。这是因为卷积操作通常涉及大量的参数和稀疏权重,而矩阵乘法则会将这些稀疏性转换为密集计算,导致计算量增加。

    全卷积网络

    13.9节中所介绍的那样,语义分割是对图像中的每个像素分类。 全卷积网络(fully convolutional network,FCN)采用卷积神经网络实现了从图像像素到像素类别的变换 (Long et al., 2015)。 与我们之前在图像分类或目标检测部分介绍的卷积神经网络不同,全卷积网络将中间层特征图的高和宽变换回输入图像的尺寸:这是通过在 13.10节中引入的转置卷积(transposed convolution)实现的。 因此,输出的类别预测与输入图像在像素级别上具有一一对应关系:通道维的输出即该位置对应像素的类别预测。

    模型

    全卷积最基本的设计:
    全卷积网络先使用卷积神经网络抽取图像特征,然后通过1×1卷积层将通道数变换为类别个数。

    练习

  5. 如果将转置卷积层改用Xavier随机初始化,结果有什么变化?
    通过使用Xavier随机初始化,转置卷积层的权重将以一种更合理的方式初始化,有助于加速模型的收敛速度,并可能提高模型的性能。具体效果取决于网络的结构、数据集和其他超参数的设置。

    风格迁移

    首先,我们初始化合成图像,例如将其初始化为内容图像。 该合成图像是风格迁移过程中唯一需要更新的变量,即风格迁移所需迭代的模型参数。 然后,我们选择一个预训练的卷积神经网络来抽取图像的特征,其中的模型参数在训练中无须更新。 这个深度卷积神经网络凭借多个层逐级抽取图像的特征,我们可以选择其中某些层的输出作为内容特征或风格特征。这里选取的预训练的神经网络含有3个卷积层,其中第二层输出内容特征,第一层和第三层输出风格特征。
    ![[Pasted image 20240409223638.png]]

    练习

  6. 选择不同的内容和风格层,输出有什么变化?
    选择更靠近网络底层的内容层可以保留更多的图像内容,而选择更靠近网络顶层的风格层可以强调更多的风格特征。
  7. 调整损失函数中的权重超参数。输出是否保留更多内容或减少更多噪点?
    增加内容损失的权重可能会使输出更接近于内容图像,减少噪点;而增加风格损失的权重可能会使输出更接近于风格图像,但也可能导致一些失真或不自然的效果。
  8. 替换实验中的内容图像和风格图像,能创作出更有趣的合成图像吗?
    可以
  9. 我们可以对文本使用风格迁移吗?
    在文本领域,风格迁移可以应用于文本生成、翻译和修改等任务,以生成具有不同风格的文本。
    我的实验:
    希望将如下两张图片进行风格迁移:
    ![[3.png]]
    ![[2.jpg]]
    ![[Pasted image 20240410004055.png]]