动手深度学习

数据操作 + 数据预处理

N维数组

N维数组是机器学习和神经网络的主要数据结构。

  1. 0维 标量

    1.0

    表示一个类别

  2. 维 向量

    [1.0, 2.7, 3.4]

    表示一个特征向量

  3. 2维 矩阵

    [[1.0, 2.7, 3.4]

    [5.0, 0.2, 4.6]

    [4.3, 8.5, 0.2]]

    表示一个样本—特征矩阵

  4. 3维

    [ [ [0.1, 2.7, 3.4]

    ​ [5.0, 0.2, 4.6]

    ​ [4.3, 8.5, 0.2] ]

    [ [3.2, 5.7, 3.4]

    [5.4, 6.2, 3.2]

    [4.1, 3.5, 6.2] ] ]

    如RGB图片(w * H * channels )

  5. 4维

    一个RGB图片的批量 (批量大小 * 宽 * 高 * 通道)

  6. 5维

    一个视频批量 (批量大小 * 时间 * 宽 * 高 * 通道)


数据操作

1
import torch

张量(tensor)表示一个数值组成的数组,这个数据可能有多个维度。

  • shape属性:访问张量的形状。
  • numel()函数:访问张量中元素的总数。
  • reshape()函数:改变张量的形状而不改变元素数量和元素值。
1
2
3
x = torch.arange(12)
x.shape
x.numel()

reshape()


  • torch.zeros() 使用全0填充指定形状的张量
  • torch.ones() 使用全1填充指定形状的张量
  • torch.tensor() 为张量中的每个元素赋予确定值
1
2
3
torch.zeros((234))

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

  • 常见的标准运算符(+-*/和求幂**)都可以被升级为按元素运算

    1
    2
    3
    x = torch.tensor([1.0, 2, 4, 8])
    y = torch.tensor([2, 2, 2, 2])
    x + y, x - y, x * y, x / y, x**y

    image-20240131222451661


  • torch.cat() 连结张量

    image-20240201160930619


  • 通过逻辑运算符构建二元张量

    image-20240201161036811


  • x.sum() 对张量中所有元素进行求和,会产生只有一个元素的张量

  • 即使形状不同,仍然可以通过调用广播机制维度的尺寸要么相等,要么其中一个维度为1)来执行按元素操作。

    image-20240201162656769


  • 可以用[-1]选择最后一个元素

  • 可以用[1:3]选择第二个和第三个元素

  • 可以指定索引将元素写入矩阵

    image-20240201163611717

  • 为多个元素赋值相同的元素,只需要索引所有元素,为其赋值。

    image-20240201163810436


  • Numpy类型的张量转换为pytorch类型的张量

    1
    2
    A = X.numpy()
    B = torch.tensor(A)
  • 将大小为1的张量转换为Python标量

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

数据预处理

  1. 创建人工数据集,并存储在csv文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import os
    # 创建 ../data文件夹
    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
    # CSV 文件的路径 ../data/house_tiny.csv
    data_file = os.path.join('..', 'data', 'house_tiny.csv')

    with open(data_file, 'w') as f:
    f.write('NumRooms,Alley,Price\n') # 列名
    f.write('NA,Pave,127500\n') # 每行表示一个数据样本
    f.write('2,NA,106000\n')
    f.write('4,NA,178100\n')
    f.write('NA,NA,140000\n')
  2. 从csv文件中加载原始数据集

    image-20240201213545427

  3. 为了处理缺失的数据,典型的方法包括插值删除

    下例通过插入数值,消除缺失的数值。

    1
    2
    3
    4
    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
    # 用 非空数据的平均值 填充NaN
    inputs = inputs.fillna(inputs.mean(numeric_only=True))
    inputs

    image-20240201224423732

  4. 对于类别值或离散值,将NaN视为一个类别。

    1
    2
    3
    4
    5
    6
    7
    # 独热编码(将分类变量转换为二进制向量)
    # 每个分类变量的每个可能取值都被编码为一个二进制特征
    # 当分类变量有多个不同取值时,会生成更多的列。这可能会导致维度灾难(curse of dimensionality)的问题
    inputs = pd.get_dummies(inputs, dummy_na=True)
    # 将inputs中的所有值转换为数值类型(True/False --> 1/0)
    inputs *= 1
    inputs

    image-20240201234546437

  5. 将dataframe转换为张量

    1
    2
    3
    4
    5
    6
    7
    import torch

    # 把所有条目都是数值类型的 dataframe格式的inputs和outputs
    # 转换为张量格式
    X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)

    X, y

    image-20240201235240914


线性代数

标量

  • 简单操作
    • $c = a + b$
    • $c = a \cdot b$
    • $c = \sin a$
  • 长度
    • $|a|=\begin{cases} a, & \text{if } a > 0 \ -a, & \text{if } x \leq 0 \end{cases}$
    • $|a+b| \leq |a| + |b|$
    • $|a\cdot b|=|a| \cdot |b|$

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

1
2
3
4
5
6
import torch

x = torch.tensor([3.0])
y = torch.tensor([2.0])

x + y, x - y, x * y, x / y, x**y

向量

  • 简单操作

    • $c = a + b \text{ where } c_i = a_i + b_i$
    • $c = \alpha \cdot b \text{ where } c_i = \alpha b_i$ (α是标量
    • $c = \sin a \text{ where } c_i = \sin a_i$
  • 长度

    • $||a||2=\sqrt{\sum\limits{i=1}^ma_i^2}$
    • $||a||\geq 0 \text{ for all } a$
    • $||a+b|| \leq ||a|| + ||b||$
    • $||\alpha\cdot b||=|\alpha| \cdot ||b||$ (α是标量
  • 点乘

    $a^Tb = \sum\limits_ia_ib_i$

    如果a与b正交,则$a^Tb = 0$

1
2
3
4
5
6
7
# 可以把向量视为标量值组成的列表
x = torch.arange(4)

# 张量的长度
len(x)
# 只有一个轴的张量,形状只有一个元素。
x.shape # torch.Size([4])

矩阵

  • 简单操作

    • $C = A + B \text{ where }C_{ij}=A_{ij}+B_{ij}$
    • $C=\alpha\cdot B \text{ where } C_{ij}=\alpha B_{ij}$
    • $C=\sin A \text{ where } C_{ij} = \sin A_{ij}$
  • 乘法(矩阵 x 向量

    $C = Ab \text{ where } C_i=\sum\limits_jA_{ij}b_j$ (矩阵A的每一行和向量b做内积)

    image-20240202081512116

  • 乘法(矩阵 x 矩阵)

    $C = AB \text{ where } C_{ik}=\sum\limits_jA_{ij}B_{jk}$

  • 范数(norm)

    F范数:把所有元素的模取平方和,再开方。

    $||A||2=\sqrt{|A{11}|^2+… + |A_{nn}|^2}=\sqrt{\sum\limits_{i,j}|a_{ij}|^2}$

    1
    torch.norm(torch.ones((4, 9)))

    image-20240203005800817

    平方范数$L_2$,


特殊矩阵

  • 对称和反对称矩阵

    • $A_{ij}=A_{ji}$
    • $A_{ij}=-A_{ji}$
  • 正定矩阵

    $x$为任意向量,$x^TAx \geq 0$ ,就称A为正定矩阵。

  • 正交矩阵

    • 所有行/列都是单位向量,且两两正交
    • 可以写成$AA^T=E$ (对角线全为1的单位矩阵)
  • 置换矩阵


  • 特征向量

    不被矩阵改变方向的向量,称为特征向量。对称矩阵总能找到特征向量。

    $Ax = \lambda x$


代码举例

通过指定两个分量$m$和$n$来创建一个形状为$m\times n$的矩阵。

1
2
3
4
A = torch.arange(20).reshape(5, 4)

# 得到矩阵的转置
A.T

哈达玛积 $\odot$

两个矩阵的按元素乘法称为哈达玛积$\odot$,不常用。

1
2
3
4
5
6
7
import torch

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

A * B

image-20240202224121178

1
2
3
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape

image-20240202224400195


按指定轴求和

1
2
A = torch.arange(20 * 2).reshape(2, 5, 4)
A

image-20240203000156266

  • 针对第1个维度求和

    1
    2
    A_sum_axis0 = A.sum(axis = 0)
    A.shape, A_sum_axis0.shape, A_sum_axis0

    image-20240203000257268

  • 针对第2个维度求和

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

    image-20240203000435931

  • 针对1、2两个维度求和

    1
    A.sum(axis = [0, 1])

    image-20240203000524694

  • 针对第1个维度计算累加和

    1
    A.cumsum(axis = 0)

    image-20240203003928627


平均值

  • A.mean()
  • A.mean(axis=0)

计算总和或均值时 保持轴数不变

1
2
3
4
A = torch.arange(20 * 2.).reshape(2, 5, 4)

# 保持第2维的存在(只有1行),对于广播机制是一个很好的操作
A.sum(axis=1, keepdims=True)

image-20240203003249355


点积 $\cdot$ (点乘、内积)

点积是相同位置的按元素乘积的

1
2
3
x = torch.arange(4.)
y = torch.ones(4, dtype=torch.float32)
x, y, torch.dot(x, y)

image-20240203004516282

可以通过执行按元素乘法(哈达玛积$\odot$),然后求和来表示两个向量的点积。

1
torch.sum(x * y)

矩阵向量积

1
A.shape, x.shape, torch.mv(A, x)

image-20240203005305511


矩阵乘法

1
2
B = torch.ones(4, 3)
torch.mm(A, B)

image-20240203005453450


梯度

梯度是导数在向量上的拓展,指向值变化最大的方向

  1. y是标量,x是向量

    image-20240203010527048

    x是列向量,求导之后,会变成行向量

    image-20240203010607386

    image-20240203010856496

  2. y是向量,x是标量

    image-20240204155019314

    y是列向量,求导之后,还是列向量

    image-20240204155124157

  3. y和x都是向量

    求导之后,是一个矩阵

    image-20240204155431906


自动求导

自动求导计算一个函数在指定值上的导数。自动求导有两种模式:

  1. 正向累积

    $\frac{\partial y}{\partial x}=\frac{\partial y}{\partial u_n}(\frac{\partial u_n}{\partial u_{n-1}}(\dots(\frac{\partial u_2}{\partial u_1}\frac{\partial u_1}{\partial x})))$

  2. 反向累积(反向传递)

    $\frac{\partial y}{\partial x}=(((\frac{\partial y}{\partial u_n}\frac{\partial u_n}{\partial u_{n-1}})\dots)\frac{\partial u_2}{\partial u_1})\frac{\partial u_1}{\partial x}$

反向累积求导的复杂度

  • 时间复杂度 $O(n)$,n是操作子个数

    通常和正向累积代价类似。

  • 空间复杂度$O(n)$

    需要存储正向计算的所有中间结果。正向累积的空间复杂度是$O(1)$


对函数$y = 2x^Tx$ 关于列向量x求导

1
2
3
4
5
6
7
8
9
10
11
import torch

x = torch.arange(4., requires_grad=True)
x, x.grad # 默认值是None

y = 2 * torch.dot(x, x)

# 通过调用反向传播函数,自动计算y关于x每个分量的梯度。
# 执行backward前,y必须是个标量
y.backward()
x.grad

重新计算梯度时,要清除之前的值

如继续对函数$y = \sum\limits_{i=1}^nx_i$关于列向量x求导。

1
2
3
4
5
# 默认情况下,pytorch会累积梯度,需要清除之前的值
x.grad.zero_()
y = x.sum() # y = x_1 + x_2 + ... + x_n
y.backward()
x.grad

将某些计算移动到记录的计算图之外

1
2
3
4
5
6
7
8
9
x.grad.zero_()
y = x * x
# 使u不是一个关于x的函数,而是看做一个常数
u = y.detach()
# 会被看做 常数 * x
z = u * x

z.sum().backward()
x.grad

image-20240204192352450

1
2
3
4
x.grad.zero_()
y.sum().backward()

x.grad

image-20240204192422844


即使构建函数的计算图需要通过Python控制流,仍然可以计算得到变量的梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def f(a):
b = a * 2
# F范数:把所有元素的模取平方和,再开方。
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

# 随机数,size为空,即标量
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

a.grad == d / a

线性回归 + 基础优化算法

线性回归模型

  • 给定n维输入$\mathbf{x} = [x_1, x_2, \dots, x_n]^T$

  • 线性回归模型有一个n维权重$\mathbf{w}$和一个标量偏差b

    $\mathbf{w}=[w_1, w_2, \dots, w_n]^T, \ b$

  • 输出是输入的加权和

    $y = w_1x_1 +w_2x_2 +\dots+w_nx_n+b$

    向量版本:$y=\langle\mathbf{w},\mathbf{x}\rangle+b$

线性回归模型可以看做单层神经网络,有显式解。


衡量预估质量

使用平方损失(均方损失)衡量预测值和真实值的差异。

$\mathscr{l}(y,\hat{y})=\frac{1}{2}(y-\hat{y})^2$ ($\frac{1}{2}$是为了求导时,方便消去)


参数学习

  • 训练损失

    $\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum\limits_{i=1}^{n}(y_i-\langle\mathbf{x}_i,\mathbf{w}\rangle-b)^2$

  • 通过最小化损失函数来学习参数

    $\mathbf{w}^*,\mathbf{b}^*=\arg\min\limits_{\mathbf{w},b}\ell(\mathbf{X},\mathbf{y},\mathbf{w},b)$

均方误差(Mean Squared Error, MSE)是一种常用的损失函数,用于衡量预测值与真实值之间的差异:

$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$

1
2
3
from torch import nn

loss = nn.MSELoss()

基础优化方法

梯度下降

当一个模型没有显式解时,如何操作?

  1. 挑选一个初始值$\mathbf{w}_0$

  2. 重复迭代参数 t = 1, 2, 3

    $\mathbf{w}t= \mathbf{w}{t-1} -\eta \frac{\partial \ell}{\partial \mathbf{w}_{t-1}}$

    $\frac{\partial \ell}{\partial \mathbf{w}{t-1}}$:损失函数$\ell$关于$\mathbf{w}{t-1}$的梯度

    • 沿梯度方向将增加损失函数值
    • 学习率$\eta$:步长超参数(需要人为指定的值)。学习率不能太小,也不能太大

在实际中,很少直接使用梯度下降,最长使用的形式是小批量随机梯度下降

  • 在整个训练集上算梯度成本高昂

    一个深度神经网络模型可能需要数分钟至数小时

  • 可以随机采样 b 个样本$i_1, i_2, …, i_b$来近似损失
    $\frac{1}{b}\sum\limits_{i\in I_b}\ell(\mathbf{x}_i, y_i, \mathbf{w})$

b是批量大小,另一个重要的超参数。


  • 梯度下降通过不断沿着反梯度方向更新参数求解
  • 小批量随机梯度下降是深度学习默认的求解算法
  • 两个重要的超参数批量大小学习率

pytorch实现

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

def synthetic_data(w, b, num_examples):
"""生成 y = Xw + b + 噪音。"""
# 从均值为0,标准差为1的正态分布中随机采样 形状为(num_examples, len(w)) 的张量x
x = torch.normal(0, 1, (num_examples, len(w)))
# torch.matmul:进行矩阵乘法
y = torch.matmul(x, w) + b
# 添加噪音,从均值为0,标准差为0.01的正态分布中随机采样
y += torch.normal(0, 0.01, y.shape)
# -1:表示根据张量的总元素数量自动确定有多少行
# 1:表示每行的大小为 1。
return x, y.reshape((-1, 1))


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

调用框架中现有的api来读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个pytorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
# 按batch_size来随机读取样本
return data.DataLoader(dataset, batch_size, shuffle=is_train)


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

# iter() 将可迭代对象data_iter转换为迭代器对象,
# next() 函数从迭代器中获取下一个元素。
next(iter(data_iter))

使用框架的预定义好的层

1
2
3
4
5
from torch import nn

# Linear 线性回归(全连接层),输入维度是2,输出是1
# Sequential 看作 神经网络层的list
net = nn.Sequential(nn.Linear(2, 1))

初始化模型参数

1
2
3
4
# normal_ 对权重参数的数据进行正态分布初始化,均值为 0,标准差为 0.01
net[0].weight.data.normal_(0, 0.01)
# bias 偏差
net[0].bias.data.fill_(0)

计算均方误差使用MSELoss

$\text{MSE} = \frac{1}{n}\sum\limits_{i=1}^n(y_i-\hat{y})^2$

1
loss = nn.MSELoss()

实例化SGD实例

随机梯度下降(SGD, Stochastic Gradient Descent)

1
2
# torch.optim.SGD是一个优化器类
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
num_epochs = 3
for epoch in range(num_epochs):
# 遍历训练数据集的批次
for X, y in data_iter:
# 计算预测值,计算预测值与真实标签之间的损失
l = loss(net(X), y)
# 梯度清零
trainer.zero_grad()
# 反向传播,计算损失函数关于模型参数的梯度s
l.backward()
# 根据计算得到的梯度值和SGD的规则 更新模型参数
trainer.step()
# 对 所有训练数据 进行预测,计算损失值
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')

Softmax回归

Softmax回归其实是一个分类问题。

  • 回归估计一个连续值
    • 跟真实值的区别作为损失
  • 分类预测一个离散类别
    • 通常多个输出
    • 输出i是预测为第i类的置信度

Softmax函数

$softmax(o)=\hat{y}$

$softmax(o)_i = \hat{y}_i=\frac{\exp(o_i)}{\sum_j\exp(o_j)}$

  • $softmax(o)_i$ 是模型输出 o 经过 softmax 函数后的概率分布的第 i 个元素。
  • $\sum_j\exp(o_j)$:所有向量指数的和

使用Softmax函数得到每个类的预测置信度


交叉熵

交叉熵常用来衡量两个概率的区别:$H(p,q)=\sum\limits_i -p_i\log (q_i)$

将它作为损失:
$\ell(y,\hat{y})= -\sum\limits_i y_i\log \hat{y}_i = -\log \hat{y}_y$

将每个类别的真实标签的概率 $y_i$ 乘以预测概率 $\hat{y}_i$ 的负对数,并将所有类别的结果求和。交叉熵衡量了预测概率分布 $\hat{y}_i$ 在真实概率分布 $y_i$ 上的不确定性和差异

交叉熵损失函数对模型输出 o 求梯度,即真实概率和预测概率的区别
$\partial_{o_i}\ell(y, \hat{y}) = softmax(o)_i - y_i$


损失函数

均方损失 $L_2$ Loss

$\ell(y,y^{‘})=\frac{1}{2}(y-y^{‘})^2$


绝对值损失 $L_1$ Loss

$\ell(y, y^{‘})=|y-y^{‘}|$

优化到末期时,就不那么稳定


Softmax回归的从零开始实现

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
from IPython import display

def get_dataloader_workers():
"""使用4个进程来读取的数据"""
return 4


def load_data_fashion_mnist(batch_size, resize=None):
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
# 接受一个可选参数resize,用来将图像大小调整为另一种形状。
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)

# 这个函数返回训练集和验证集的数据迭代器。
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))


batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
  • softmax的输入需要是一个向量,展平每个图像,将它们视为长度为784(1*28*28)的向量。
  • 因为我们的数据集有10个类别,所以网络输出维度为10
1
2
3
4
5
6
num_inputs = 1 * 28 * 28
num_outputs = 10

# 从均值为0,标准差为0.01的正态分布中随机采样
w = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

实现Softmax回归模型

对于一个矩阵来说,就是按行来做softmax。

$softmax(X){ij}=\frac{\exp (X{ij})}{\sum_{k}\exp (X_{ik})}$

1
2
3
4
5
6
7
8
9
10
11
12
def softmax(X):
# 对每个元素做指数计算
X_exp = torch.exp(X)
# 按照维度为1(行)进行求和
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 应用了广播机制

def net(X):
# torch.matmul:进行矩阵乘法
# -1:表示根据张量的总元素数量自动确定有多少行
# 最终得到1个 所有的元素值>0,行和为1的输出
return softmax(torch.matmul(X.reshape((-1, w.shape[0])), w) + b)

实现交叉熵损失函数

1
2
3
4
def cross_entropy(y_hat, y):
# log实际是ln
# len() 能获取矩阵的行数
return -torch.log(y_hat[range(len(y_hat)), y])

将预测类别与真实y元素进行比较

1
2
3
4
5
6
7
8
9
10
11
def accuracy(y_hat, y):
"""计算预测正确的数量"""
# y_hat 是一个二维张量且有多个列(即表示多类别分类任务的预测结果)
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
# argmax 函数用于在给定轴上找到张量中最大元素的索引
# 使用 argmax 函数沿着列的维度(每行)计算出每个样本的预测标签
y_hat = y_hat.argmax(axis=1)
# y.dtype 获取 y 张量的数据类型,使用 y_hat.type(y.dtype) 将 y_hat 的数据类型转换为与 y 相同的类型。
cmp = y_hat.type(y.dtype) == y
# 计算布尔张量 cmp 中值为 True 的元素的数量,并将结果转换为浮点数。返回预测正确的数量。
return float(cmp.type(y.dtype).sum())

评估准确率

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
class Accumulator:
"""For accumulating sums over `n` variables."""

def __init__(self, n):
# 使用列表推导式创建了一个名为 data 的列表,初始化为长度为 n 的全零列表。
self.data = [0.0] * n

def add(self, *args):
# zip 函数将 data 列表与参数 args 逐个配对,并使用列表推导式将它们相加
self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):
# 将 data 列表的每个元素都设置为 0.0。
self.data = [0.0] * len(self.data)

def __getitem__(self, idx):
return self.data[idx]


def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
# 将模型设置为评估模式(不计算梯度)
net.eval()
# 正确预测数、预测总数
metric = Accumulator(2)
for X, y in data_iter:
# accuracy 预测正确的个数
# `numel()`函数:访问张量中元素的总数。
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

# 未经训练时的准确率
evaluate_accuracy(net, test_iter)

训练的一个迭代周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 训练模型一个迭代周期
def train_epoch(net, train_iter, loss, updater):
if isinstance(net, torch.nn.Module):
net.train()
metric = Accumulator(3)
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
# 如果updater是pytorch的optimizer
if isinstance(updater, torch.optim.Optimizer):
# 使用pytorch内置的优化器,将梯度设为0
updater.zero_grad()
# 计算梯度
l.mean().backward()
# 对参数进行一次更新
updater.step()
else:
l.sum().backward()
updater(X.shape[0])
# 训练损失总和,正确的分类数,样本数 放入累加器
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# loss的累加 / 样本数,分类正确的样本数 / 样本数(训练损失和训练精度)
return metric[0] / metric[2], metric[1] / metric[2]

定义一个在动画中绘制数据的类

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
from matplotlib_inline.backend_inline import set_matplotlib_formats


class Animator:
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
set_matplotlib_formats('svg')
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts

def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)

训练函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def train(net, train_iter, test_iter, loss, num_epochs, updater):
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
# 将当前训练轮次的指标(训练损失、训练准确率和测试准确率)添加到 animator 对象中,用于可视化展示。
# 如果 train_metrics 的值是 (0.2, 0.8),而 test_acc 的值是 0.75,那么 train_metrics + (test_acc,) 的结果将是 (0.2, 0.8, 0.75)。
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
# 检查最后一轮训练的损失是否小于 0.5,如果不满足条件,则抛出异常。
assert train_loss < 0.5, train_loss
# 检查最后一轮训练的准确率是否在 0.7 到 1 之间,如果不满足条件,则抛出异常。
assert 1 >= train_acc > 0.7, train_acc
# 检查最后一轮测试的准确率是否在 0.7 到 1 之间,如果不满足条件,则抛出异常。
assert 1 >= test_acc > 0.7, test_acc


小批量随机梯度下降来优化模型的损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def sgd(params, lr, batch_size):
"""随机梯度下降(Stochastic Gradient Descent)
小批量梯度下降"""
# 更新时不参与梯度计算
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()


lr = 0.1


def updater(batch_size):
return sgd([w, b], lr, batch_size)

训练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
num_epochs = 10
train(net, train_iter, test_iter, cross_entropy, num_epochs, updater)


def get_fashion_mnist_labels(labels):
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]


def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""绘制图像列表"""
fig_size = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=fig_size)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes

def predict(net, test_iter, n=6):
"""预测标签"""
for X, y in test_iter:
break
trues = get_fashion_mnist_labels(y)
preds = get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])


predict(net, test_iter)

多层感知机

感知机

给定输入$\mathbf{x}$,权重$\mathbf{w}$,偏移$b$,感知机输出:

$o=\sigma(\langle\mathbf{w},\mathbf{x}\rangle+b)$

$\sigma(x)=\begin{cases}1 \ \ \text{if}\ x > 0\0 \ \ \text{otherwise}\end{cases}$

感知机其实就是二分类的问题。

  • 和线性回归相比,线性回归输出实数
  • 和Softmax回归相比,Softmax是多分类

模型选择

模型容量

模型容量指的是拟合各种函数的能力

  • 低容量的模型难以拟合训练数据
  • 高容量的模型可以记住所有的训练数据。

网络中的网络 NiN

全连接层的问题

卷积层只需要较少的参数,但卷积层之后的第一个全连接层需要的参数量很大

  • 卷积层的参数量: $c_i\times c_o \times k^2$ (输入通道数 x 输出通道数 x 卷积核的高 x 卷积核的宽)
  • 全连接层的参数量:$c_i\times w_i \times h_i \times c_o \times w_o \times h_i$ (输入通道数 x 输入宽度 x 输入高度 x 输出通道数 x 输出宽度 x 输出高度)

参数量大,会占用很多内存、计算带宽、容易过拟合


NiN块

1个卷积层后跟2个1 * 1的卷积层,对每个像素增加了非线性

  • 步幅1,无填充,输出形状跟卷积层输出一样
  • 起到全连接层的作用

NiN架构

  • 无全连接层
  • 交替使用NiN块和步幅为2的最大池化层,逐步减小宽高、增大通道数
  • 最后使用全局平均池化层得到输出,不容易过拟合,更少的参数个数。
    • 输入通道数是类别数

含并行连结的网络 GoogLeNet / Inception V3

Inception块用4条有不同超参数的卷积层和池化层的路来抽取不同的信息,它的一个主要优点是模型参数小,计算复杂度低。

image-20240222140852981

GoogLeNet使用了9个lnception块,是第一个达到上百层的网络,后续有一系列改进。


批量归一化

损失出现在最后,后面的层训练较快。

数据在最底部

  • 底部的层训练较慢
  • 底部层一变化,所有都得跟着变
  • 最后的那些层需要重新学习多次
  • 导致收敛变慢

我们可以在学习底部层的时候避免变化顶部层吗?


  • 固定小批量B里的样本x的均值

    $\mu_B=\frac {1}{|B|}\sum\limits_{x\in B}x_i$

  • 固定小批量B里的样本x的方差

    $\sigma^2_B=\frac{1}{|B|}\sum\limits_{i\in B}(x_i-\mu_B)^2+\epsilon$

    $\epsilon$是为了防止方差为0的一个很小的数。

批量归一化就是通过可学习的参数$\gamma、\beta$得到的结果:

$x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta$


批量归一化层

  • 含有可学习的参数为$\gamma$和$\beta$
  • 作用范围:
    • 全连接层和卷积层输出上,激活函数前
    • 全连接层和卷积层输入上
  • 对全连接层,作用在特征
  • 对于卷积层,作用在通道

批量归一化在做什么

后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度,因此没必要跟丢弃法混合使用。

$x_{i+1}=\gamma\frac{x_i-\hat{\mu}_B}{\hat{\sigma}_B}+\beta$

把$\hat{\mu}_B$和$\hat{\sigma}_B$看作随机偏移随机缩放

批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放。可以加速收敛速度,但一般不改变模型精度


残差网络 ResNet

残差块

  • 串联一个层改变函数类,希望能扩大函数类。
  • 残差块加入快速通道,来得到$f(x)=x+g(x)$的结构

image-20240223134806237

有很多调整变形的排列组合。

有两种残差块:

  1. 高宽减半、通道数翻倍的残差块(步幅stride=2)
  2. 接多个高宽不变的残差块(步幅stride=1)

残差块使得很深的网络更加容易训练,甚至可以训练一千层的网络。


数据增广

数据增强就是在一个已有的数据集中,通过变形数据,使得数据集有更多的多样性,模型繁华性能更好。例如:

  1. 在语言里加入各种不同的背景噪音
  2. 改变图片的形状和颜色

常见的图片的数据增强的形式:

  1. 翻转
  2. 切割
  3. 变色

微调

  • 微调通过使用在大数据上得到的预训练好的模型,来初始化模型权重,来完成提升精度。
  • 预训练模型质量很重要。
  • 微调通常速度更快、精度更高。

序列模型

  • 时序模型中,当前数据跟之前观察到的数据相关
  • 自回归模型使用自身过去数据来预测未来
  • 马尔科夫模型假设当前只跟最近少数数据相关,从而简化模型
  • 潜变量模型使用潜变量来概括历史信息