抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

CodingStudio

努力进步

0 引言

1 课程安排

  • 内容
  • 深度学习基础:线性神经网络,多路感知机
  • 卷积神经网络:LeNet,AlexNet,VGG,Inception,ResNet
  • 循环神经网络: RNN, GRU, LSTM, seq2seq
  • 注意力机制: Attention, Transformer
  • 优化算法: SGD, Momentum, Adam
  • 高性能计算: 并行, 多GPU, 分布式
  • 计算机视觉: 目标检测, 语义分割
  • 自然语言处理: 词嵌入, BERT

3 安装

1
2
3
4
5
6
7
8
9
10
11
sudo apt install build-essential    # 安装编译环境
sudo apt install zip

# conda环境下安装
pip install jupyter d2l

# 下载代码
wget https://courses.d2l.ai/zh-v2/

# 将服务器端的端口8888映射到本地
ssh -L8888:localhost:8888 ubuntu@远端服务器ip

4 数据操作+数据预处理

4.1 数据操作

  • N维数组是机器学习和神经网络的主要数据结构
    • 0-d标量:一个类别
    • 1-d向量:一个特征向量
    • 2-d矩阵:一个样本-特征矩阵
    • 3-d: RGB图片(*宽*高*通道)
    • 4-d:一个RGB图像批量(批量大小*宽*高*通道)
    • 5-d: 一个视频批量(批量大小*时间*宽*高*通道)
  • python中 : 代表的区间为前开后闭的区间
    • id()类似C++中指针
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
68
[1:3, 1:]       # 访问子区域,表示访问1-2行,1到最后一列
[::3, ::2] # 跳跃访问:表示每三行一跳,每2列一跳

# 导入模块
import torch

# 张量表示一个数值组成的数组,这个数组可能有多个维度
x = torch.arrange(12) # 创建序列0-11

# 可通过张量的shape属性访问张量的形状和张量中元素的总数
x.shape() # torch.Size([12])
x.numel() # 元素总数

# 可以使用reshape()改变张量的形状而不改变元素数量和元素值
x = x.reshape(3, 4)

# 使用全0, 全1, 其他常量或从特定分布中随机采样的数字
torch.zeros((2,3,4))
torch.ones((2,3,4))

# 通过提供包含数值的Python列表来为所需张量中的每个元素赋予确定值
torch.tensor(array)

# 常见的标准算数运算符(+,-*,/,**)都可以被升级为按元素运算
torch.exp(x)

# 将多个张量连结在一起
X = torch.arrange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X,Y),dim=0), torch.cat((X,Y), dim=1) # dim=0为按行拼接,dim=1按列拼接

# 通过逻辑运算符构建二元张量
X == Y

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

# 即使形状不同,仍然可以通过调用广播机制来执行按元素操作
a = torch.arrange(3).reshape((3,1))
b = torch.arrange(2).reshape((1,2))
a + b

# 可以使用[-1]访问最后一个元素,[1:3]访问第1,2个元素
x[-1],x[1:3]

# 运行一些操作可能会导致为新结果分配内存(内存析构)
before = id(Y)
Y = Y+X
id(Y) = before

# 执行原地操作
z = torch.zeros_like(Y)
print(id(z))
z[:] = X+Y
print(id(z))

# 后续计算没有重复使用X,减少内存开销
before = id(X)
X+=Y
id(X) == before

# 转换为numpy张量
A = x.numpy()
B = torch.tensor(A)

# 将大小为1的张量转换为python标量
a = torch.tensor([3.5])
a, a.item, float(a), int(a)

4.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
# 创建一个人工数据集,并存储在csv文件
import os

os.makedirs(os.path.join('..','data'), exist_ok=True)
data_file = os.makedirs(os.path.join('..','data'), 'house_tiny.csv')
with open(data_file, 'w') as f:
f.write('NumRomms,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')

# 从创建的csv文件中加载原始数据集
import pandas as pd

data = pd.read_csv(data_file)

# 为了处理缺失的数据,典型的方法包括插值和删除
# 使用插值
inputs, outputs = data.iloc[:,0:2], data.iloc[:,2]
inputs = inputs.fillna(inputs.mean()) # 使用均值进行填充
print(inputs)

# 对于inputs中的类别值或离散值,将Nan视为一个类别
inputs = pd.get_dummies(inputs, dummy_na=True) # 将类别值或离散值映射为值

# input和outputs中的所有条目都是数值类型,可以转换为张量格式
immport torch

X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
# 使用float32浮点数进行计算速度较快

4.3 数据操作

1
2
3
4
5
6
# reshape 和 view的 区别
a = torch.arange(12)
b = a.reshape((3, 4))
b[:] = 2
a
# reshape为浅拷贝方式
  • tensor为张量,为数学上的定义,array为数学上的数组的定义

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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 标量
import torch

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

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

# 向量
x = torch.arange(4)

# 长度,维度和形状
# 访问张量的长度
len(x)
# 张量的形状
x.shape

# 矩阵
A = torch.arange(20).reshape(5, 4)

A.T # 矩阵的转置

B == B.T # 对称矩阵,B等于其转置

# 张量
X = torch.arange(24).reshape(2, 3, 4)

# 张量算法的基本性质
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # 通过分配新内存,将A的一个副本分配给B
A, A + B

# 两个矩阵按元素乘法为哈达玛积
A * B

# 将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape

# 计算其元素的和
x = torch.arange(4, dtype=torch.float32)
x, x.sum()

# 表示任意形状张量的元素和
A = torch.arange(20*2).reshape(2,5,4)
A.shape, A.sum()

# 指定求和汇总张量的轴
# 对哪个维度求和,得到的维度为丢掉该维度,若keepdims则将丢掉的维度设置为 1
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape()

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

# 一个就和相关的量是平均值
A.mean(), A.sum() / A.numel()

A.mean(axis=0), A.sum(axis=0)/A.shape[0]

# 计算总和或均值的保持轴数不变
sum_A = A.sum(axis=1,keepdims=True)
sum_A

# 通过广播将A除以sum_A
A/sum_A

# 某个轴计算A元素的累积总和
A.cumsum(axis=0)

# 点积是相同位置的按元素乘积的和
y = torch.ones(4, dtype=torch.float32)
x, y, torch.dot(x, y)

# 可通过执行元素乘法,然后进行求和来表示两向量的点击
torch(x*y)

# 矩阵向量积Ax
A.shape, x.shape, torch.mv(A, x)

# 矩阵矩阵乘法
B = torch.ones(4,3)
torch.mm(A, B)

# L2范数(对向量)
u = torch.tensor([3.0, -4.0])
torch.norm(u)

# L1范数
torch.abs(u).sum()

# F范数(矩阵元素的平方和的平方根)
torch.norm(torch.ones((4,9)))
  • 对指定维度求和 sum:对哪个维度求和,得到的维度为丢掉该维度,若keepdims则将丢掉的维度设置为 1

6 矩阵计算

矩阵计算

7 自动求导

7.1 自动求导

  • 自动求导计算一个函数在指定值上的导数
  • 计算图
    • 将代码分解成操作子
    • 将计算表示成一个无环图

计算图

  • 反向累计总结
    • 构造计算图
    • 前向: 执行图,存储中间结果
    • 反向: 从相反方向执行图,去除不需要的枝
  • 复杂度
    • 计算复杂度:
      • 反向:O(n)O(n),n是操作子个数,正向和反向的代价类似
      • 正向:O(n)O(n),计算复杂度用来计算一个变量的梯度
    • 内存复杂度:
      • 反向:O(n)O(n),需要存储正向的所有中间结果,消耗GPU资源
      • 正向:O(1)O(1)

7.2 自动求导实现

  • 实现对函数y=2xTxy=2x^Tx求导
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch

# 关于列向量x求导
x = torch.arange(4.0)

# 需要一个地方存储梯度
x.requires_grad_(True) # 等价于x = torch.arange(4.0, requires_grad=True)
x.grad # 默认值None

# 计算 y
y = 2*torch.dot(x,x)
# tensor(28., grad_fn=<MulBackward0>),grad_fn求梯度函数的属性

# 通过反向传播函数来自动计算y关于x每个分量的梯度
y.backward()
x.grad()

# 默认情况下,pytorch回累计梯度,需要清除之前的值
x.grad.zero_() # 清除之前的值
y = x.sum()
y.backward()
x.grad()
  • 非标量变量的反向传播
    • y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵
    • 对于高阶和高维的yx,求导的结果可以是一个高阶张量
    • 然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当调用向量的反向计算时,通常会试图计算一批训练样本中每个组成部分的损失函数的导数(我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。)
1
2
3
4
5
6
7
# 深度学习中,目的不是计算微分矩阵,而是批量中每个样本单独计算的偏微分之和
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
  • 分离计算:将某些计算移动到记录的计算图之外]
    • 例如,假设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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 将某些计算移动到记录的计算图之外
x.grad.zero_()
y = x*x
u = y.detach() # 将y当作常数,而不是关于x的函数
z = u*x

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

x.grad.zero_()
y.sum().backward()
x.grad == 2*x

# 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),仍然可以计算得到的变量的梯度
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

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

a.grad == d/a
  • pytorch为隐式构造,对控制流的计算较好,但计算速度较显示构造慢
  • 多个损失函数分别反向时需要累计梯度,因此pytorch默认为累计梯度的

8 线性回归+基础优化算法

  • 线性总结
    • 线性回归是对n维输入的加权,外加偏差
    • 使用平方损失来衡量预测值和真实值的差异
    • 线性回归有显示解
    • 线性回归可以看作单层神经网络
  • 梯度下降
    • 挑选一个初始值w0w_0
    • 重复迭代参数t=1,2,3,wt=wt1ηwt1t=1,2,3, \mathbf{w}_t=\mathbf{w}_{t-1}-\eta \frac{\partial \ell}{\partial \mathbf{w}_{t-1}}
    • 沿梯度方向将增加损失函数值
    • 学习率:步长的超参数
  • 小批量随机梯度下降
    • 在整个训练集上算梯度太贵,一个深度神经网络模型可能需要数分钟至数小时
    • 可以随机采样b个样本i1,u2,...,ibi_1,u_2,...,i_b来近似损失,1biIb(xi,yi,w)\frac{1}{b} \sum_{i \in I_b} \ell\left(\mathbf{x}_i, y_i, \mathbf{w}\right)
      • b是批量大小,另一个重要的超参数
        • 批量太小,计算量太小不适合并行来最大利用计算资源
        • 批量太大内存消耗增加,浪费计算
  • 总结
    • 梯度下降通过不断沿着繁体都方向更新参数求解
    • 小批量随机梯度下降是深度学习默认的求解算法
    • 两个重要的超参数是批量大小和学习率
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# ----------------------
# 1 线性回归的从零开始实现
# ----------------------

# 从零开始实现整个方法,包括数据流水线,模型,损失函数和小批量随机梯度下降优化器
%matplotlib inline
import random
import torch
from d2l import torch as d2l

# 根据带有噪声的线性模型构造一个人造数据集
# 使用线性模型参数2=[2,-3,4]T,b=4.2和噪声生成数据集及其标签
# 函数
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) #均值为0,方差为1,大小为(num_examples, len(w))的随机数
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
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)

print('features:', features[0],'\nlabel:', labels[0])
# 图像观察数据
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1)

# 读取数据集
# 定义一个data_iter函数,接受批量大小,特征矩阵和标签向量操作为输入,生成大小为batch_size的小批量
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices] # yield为生成器,每次产生并返回一个随机顺序的特征x和值y

batch_size = 10
for X, y in date_iter(batch_size, features, labels):
print(X, '\n', y)
break

# 定义初始化模型参数
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

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

# 定义损失函数
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

# 定义优化函数
def sgd(params, lr, batch_size): #@save
# params为输入的数据,lr为学习率,batch_size为批量大小
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

# 训练过程
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

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

# 比较真实参数和通过训练学到的参数来评估训练的成功程度
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
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
# ----------------
# 线性回归的简洁实现
# ----------------
# 通过使用深度学习框架来简洁实现线性回归模型生成数据集
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)

# 调用框架中现成的API来读取数据
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个大小的数据

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

# 使用 iter 构造Python迭代器,并使用 next 从迭代器中获取第一项
next(iter(data_iter))

# 使用框架的预定义好的层
# nn是神经网络的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2, 1)) # 将该层放在Sequential的容器中,在容器中各层以流水线连接

# 初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

# 计算均方误差使用的是MSELoss类,也称为平方范数
loss = nn.MSELoss()

# 实例化SGD实例
trainer = torch.optim.SGD(net.parameters(),lr=0.03)

# 训练代码与从零开始实现时所做的非常相似
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter: # 提取数据集X,y
l = loss(net(X) ,y) # 计算损失函数, 将数据集X输入给网络
trainer.zero_grad() # 梯度清零,防止累加
l.backward() # 计算梯度,反向传播过程
trainer.step() # 进行模型更新
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')

w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
  • 为什么使用平方损失而不是绝对差值
    • 平方损失和绝对差值的差别不大,绝对差值是一个不可导的函数,在0点时绝对差值的导数不好求解
  • 损失为什么取平均
    • 本质上没有太大差别,取平均之后梯度数值较小,便于计算。不使用损失取平均,使用学习率除以n也可以
  • GD和SGD的差别
    • GD在每次迭代都需要用到全部训练数据
    • SGD每次迭代可以只用一个训练数据来更新参数
  • 使用生成器生成数据的优势
    • 相比return,需要一个batch运行一遍,并不需要完全设置好

9 Softmax 回归 + 损失函数 + 图片分类数据集

9.1 Softmax 回归

  • 回归 VS 分类
    • 回归估计一个连续值
      • 单连续数值输出,输出为自然区间R\R, 跟真实值的区别作为损失
    • 分类预测一个离散类别
      • 通常多个输出,输出i是预测为第i类的置信度
  • 从回归到多类分类——均方损失
    • 对类别进行1为有效编码 y=[y1,y2,...,yn]T,yi={1 if i=y0 otherwise y = [y_1, y_2, ..., y_n]^T, y_i=\left\{\begin{array}{l}1 \text { if } i=y \\ 0 \text { otherwise }\end{array}\right.
    • 使用均方损失训练,y^=argmaxi oi\hat{y} ={argmax}_i \space o_i
    • 无校验比例: 需要更置信的识别正确类(大余类),oyoiΔ(y,i)o_y-o_i \geq \Delta(y, i)
    • 校验比例
      • 输出匹配概率(非负,和为1): y^=softmax(o)\hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o})
        y^i=exp(oi)kexp(ok)\hat{y}_i=\frac{\exp \left(o_i\right)}{\sum_k \exp \left(o_k\right)}
      • 概率 yyy^\hat{y} 的区别作为损失
  • softmax和交叉熵损失
    • 交叉熵常用来衡量两个概率的区别H(p,q)=pilog(qi)H(p,q) = \sum -p_i \log(q_i)
    • 损失函数l(y,y^)=iyilogy^i=logy^yl(\mathbf{y}, \hat{\mathbf{y}})=-\sum_i y_i \log \hat{y}_i=-\log \hat{y}_y
    • 梯度是真实概率和预测概率的区别 oil(y,y^)=softmax(0)iyi\partial_{o_i} l(\mathbf{y}, \hat{\mathbf{y}})=\operatorname{softmax}(\mathbf{0})_i-y_i
  • 总结
    • softmax是一个多分类模型
    • 使用softmax操作子得到每个类的预测置信度
    • 使用交叉熵来衡量预测和标号的区别

多类分类

9.2 损失函数

  • 均方损失L2 Loss: l(y,y)=12(yy)2l\left(y, y^{\prime}\right)=\frac{1}{2}\left(y-y^{\prime}\right)^2
  • 绝对值损失函数L1 Loss : l(y,y)=yyl(y,y^{'})=|y-y^{'}|
    • 当预测值和真值相距较远时,梯度仍然较大,参数更新速度不变,但 0 点处不可导,不平滑,优化末期时会不稳定
  • Huber损失函数l(y,y)={yy12 if yy>112(yy)2 otherwise l\left(y, y^{\prime}\right)= \begin{cases}\left|y-y^{\prime}\right|-\frac{1}{2} & \text { if }\left|y-y^{\prime}\right|>1 \\ \frac{1}{2}\left(y-y^{\prime}\right)^2 & \text { otherwise }\end{cases}
    • 综合上述两个损失函数的特点,当预测值和真值相差较大时参数更新速度不变,在优化末期,梯度更新速度会越来越小,优化较为平滑

损失函数

9.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# MNIST数据集是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单
# 将使用类似但更复杂的Fashion-MNIST数据集
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()

# 通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0到1之间
trans = transforms.ToTensor()
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)

# Fashion-MNIST由10个类别的图像组成,每个类别由训练数据集train dataset中的6000张图像和测试数据集test dataset中的1000张图像组成
len(mnist_train), len(mnist_test)

# 每个输入图像的高度和宽度均为28像素,数据集由灰度图像组成,其通道数为1
mnist_train[0][0].shape

# 可视化数据集的函数
def get_fashion_mnist_labels(labels): #@save
"""返回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): #@save
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
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

# 几个样本的图像及其标签
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));

# 读取一小批量数据,大小为batch_size
batch_size = 256

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

train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())

timer = d2l.Timer()
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec'

# 整合成 load_data_fashion_minist函数
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
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()))

9.4 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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# softmax回归的从零开始实现
import torch
from IPython import display
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# 将展平每个图像,将它们视为长度为784的向量
# 因为数据集有10个类别,所以网络输出维度为10
num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

# 定义softmax操作
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True) # 按行求和[784,1]
return X_exp / partition # 这里应用了广播机制

# 验证softmax操作:根据概率原理,每行总和为1
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)

# 实现softmax回归模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

# 定义交叉熵损失函数
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])

# 将预测类别与真实y元素进行比较
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())

# 评估在任意模型net的准确度
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

# Accumulator实例中创建了2个变量,用于分别存储正确预测的数量和预测的总数量
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n

def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):
self.data = [0.0] * len(self.data)

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

# softmax回归的训练
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
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)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
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())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]

# 定义一个在动画中绘制数据的实用程序类
class Animator: #@save
"""在动画中绘制数据"""
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 = []
d2l.use_svg_display()
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)

# 训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc

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

def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)

# 训练模型10个迭代周期
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

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

predict_ch3(net, test_iter)

9.5 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
# 通过深度学习框架的高级API能够很容易的实现softmax回归
import torch
from torch import nn
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# softmax回归的输出层是一个全连接层
# PyTorch不会隐式地调整输入的形状。因此,在线性层前定义了展平层(flatten),来调整网络输入的形状
# 展平层(flatten)将任何维度的tensor转换为2维tensor, 将第0维保留,其他维度转换为向量
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
if type(m) == nn.Linear: # 如果为线性层
nn.init.normal_(m.weight, std=0.01) # 将weight设置为均值为0,方差为0.01的随机值

net.apply(init_weights); # 在net的每一层使用init_weights函数

# 在交叉熵损失函数中传递未归一化的预测,同时计算softmax及其对数
loss = nn.CorssEntropyLoss()

# 使用学习率为0.1的小批量随机梯度下降作为优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)

# 调用之前定义的训练函数来训练模型
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

9.6 QA

softlabel和hardlabel

    1. hard label 更容易标注,但会丢失类内,类间的关联并且引入噪声
    • softlabel 给模型带来更返回能力,携带更多信息,对噪声鲁棒
    1. softlabel训练策略
    • 在hard label监督下,由于softmax的作用,one-hot的最大值位置无限往1优化,但永远不可能等于1,达到一定优化程度后达到饱和区,可以保证优化过程处于优化效率最高的中间区域,避免进入饱和区
    1. 使用one-hot编码会使不正确的类变为0, 因此损失函数不计不正确的类,实际上是关心不正确的类
    1. 最小化损失等价于最大化似然函数,统计学中的概念
    1. 在计算精度时使用eval()将模型设置为评估模式可以避免在评估模式时计算梯度

10 多层感知机+代码实现

10.1 感知机

  • 给定输入xx, 权重ww, 和偏移b, 感知机输出o=σ(w,x+b)σ(x)={1 if x>01 otherwise o=\sigma(\langle\mathbf{w}, \mathbf{x}\rangle+b) \quad \sigma(x)= \begin{cases}1 & \text { if } x>0 \\ -1 & \text { otherwise }\end{cases}
    • 二分类:-1或1
      • 回归输出实数,Softmax输出概率
  • 训练感知机
    • 等价于使用批量大小为1的梯度下降,并使用损失函数l(y,x,w)=max(0,y<w,x>)l(y,x,w)=max(0, -y<w,x>)
      • 当分类正确时,相当于y<w,x>-y<w,x>小于0,则损失函数为0,不进行更新

训练感知机

  • 收敛定理
    • 数据在半径rr
    • 余量 ρ分类两类:y(xTw+b)>=ρ,对于w2+b2<=1\rho 分类两类: y(x^Tw+b) >= \rho, 对于||w||^2+b^2 <= 1
    • 感知机保证在r2+1ρ2\frac{r^2+1}{\rho^2}步后收敛

10.2 多层感知机

多层感知机

  • 上述问题需要实现异或操作,一个分类器无法实现,则使用多个分类器组合

单隐层

  • 隐藏层大小是超参数
  • 单隐藏层——单分类
    • 输入: xRnx \in \R^{n}
    • 隐藏层: W1Rmn,b1RmW_1 \in \R^{m*n}, b_1 \in \R^m
    • 输出层: w2Rm,b2Rw_2 \in \R^m, b_2 \in \R
      -h=σ(W1x+b1),σh = \sigma(W_1x+b_1),\sigma是按元素的激活函数,激活函数为非线性函数
      -o=w2Th+b2o=w_2^Th+b_2
  • 激活函数
    • sigmoid函数:将输入投影到(0, 1)
    • tanh激活函数: 将输入投影到(-1, 1)
    • ReLu函数: ReLu = max(x, 0)
  • 多类分类: y1,...yk=softmax(o1,...ok)y_1,...y_k=softmax(o_1,...o_k)
    • 与softmax回归的唯一区别,添加了隐藏层
    • 输入: xRnx \in \R^{n}
    • 隐藏层: W1Rmn,b1RmW_1 \in \R^{m*n}, b_1 \in \R^m
    • 输出层: w2Rmk,b2Rkw_2 \in \R^{m*k}, b_2 \in \R^k
      -h=σ(W1x+b1)h = \sigma(W_1x+b_1)
      -o=w2Th+b2o=w_2^Th+b_2
      -y=softmax(o)y=softmax(o)
    • 可以使用多隐藏层:
      -h1=σ(W1x+b1)h_1 = \sigma(W_1x+b_1)
      -h2=σ(W2h1+b2)h_2 = \sigma(W_2h_1+b_2)
      -h2=σ(W3h2+b3)h_2 = \sigma(W_3h_2+b_3)
      -o=w4Th3+b4o=w_4^Th_3+b_4
    • 超参数
      • 隐藏层数
      • 每层隐藏层的大小
    • 多隐藏层时,隐藏层的大小越往后越压缩,最好是慢慢做压缩,不断对信息做提炼,在一开始可以比输入层稍大一点,扩充数据,(但在CNN中会有先压缩再扩张的模型

10.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
33
34
35
36
37
38
39
40
# 1 多层感知机的从零开始实现
import torch
from torch import nn
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# 实现一个具有单隐藏层的多层感知机,包含256个隐藏单元
num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = nn.Parameter(torch.randn(
num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

# 实现ReLu激活函数
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)

# 实现模型
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X @ W1 + b1) # @表示矩阵乘法
return (H @ W2 + b2)

loss = nn.CrossEntropyLoss(reduction='none')

# 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

# 预测
d2l.predict_ch3(net, test_iter)
  • 多层感知机的精度和softmax回归的精度差别不大,损失减小
  • 使用MLP(多层感知机)而不是SVM的原因:MLP方便之后效果不好时调整模型,代码方便实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2 多层感知机的简洁实现
import torch
from torch import nn
from d2l import torch as d2l

# 隐藏层包含256个隐藏单元,并使用了ReLu激活函数
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)

10.4 QA

  • 神经网络中一般使用增加隐藏层的层数,而不是神经元个数
    • 因为一层中神经元个数太多,网络较难训练
  • 不同任务下的激活函数本质上区别不大,该超参数的调节意义较小
    • 一般使用ReLu即可

11 模型选择 + 过拟合和欠拟合

11.1 模型选择

  • 训练误差和泛化误差
    • 训练误差:模型在训练数据上的误差
    • 泛化误差:模型在新数据上的误差
  • 验证数据集和测试数据集
    • 验证数据集:一个用来评估模型好坏的数据集
      • 经常称为test_data,但是是错误的
    • 测试数据集:只用一次的数据集
  • 解决数据集数量不足的问题:K-折交叉验证
    • 在没有足够多数据时使用
    • 算法
      • 将驯良数据分割成K块
      • For i=1,…,K
        • 使用第i块作为验证数据集,其余的作为训练数据集
      • 报告K个验证集误差的平均
    • 常用K=5,或K=10

11.2 过拟合和欠拟合

过拟合和欠拟合

  • 模型容量
    • 拟合各种函数的能力
    • 低容量的模型难以拟合训练数据
    • 高容量的模型可以记住所有的训练数据

模型容量的影响

  • 在模型足够大的前提下,控制模型容量是泛化误差向下降,泛化误差和训练误差的gap变小,可以容忍一定程度的过拟合
  • 模型容量的两个主要因素:参数的个数和参数值的选择范围
  • VC维
    • 对于一个分类模型,VC等于一个最大的数据集的大小,不管如何给定标号,都存在一个模型来对它进行完美分类
    • 二维输入的感知机,VC维=3
      • 能够分类任何3个点,而不是4个(xor)
    • 支持N维输入的感知机的VC维是N+1
    • 一些多层感知机的VC维是O(Nlog2N)O(Nlog_2N)
    • 深度学习中很少使用,衡量不是很准确,且计算较难
  • 数据复杂度
    • 多个重要因素:样本个数,每个样本的元素个数,时间和空间结构,多样性

11.3 代码

  • 使用多项式来生成训练和测试数据的标签:y=5+1.2x3.4x22!+5.6x33!+ϵ where ϵN(0,0.12).y = 5 + 1.2x - 3.4\frac{x^2}{2!} + 5.6 \frac{x^3}{3!} + \epsilon \text{ where }\epsilon \sim \mathcal{N}(0, 0.1^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
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
68
69
70
71
72
73
74
75
76
77
# 1 模型选择,欠拟合和过拟合
# 通过多项式拟合来交互探索这些概念
import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l

# 生成训练和测试数据标签
max_degree = 20 # 多项式的最大阶数
n_train, n_test = 100, 100 # 训练和测试数据集大小
true_w = np.zeros(max_degree) # 分配大量的空间
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])

features = np.random.normal(size=(n_train + n_test, 1))
np.random.shuffle(features)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)

# 看一下前2个样本
# NumPy ndarray转换为tensor
true_w, features, poly_features, labels = [torch.tensor(x, dtype=
torch.float32) for x in [true_w, features, poly_features, labels]]
features[:2], poly_features[:2, :], labels[:2]

# 实现一个函数来评估模型在给定数据集上的损失
def evaluate_loss(net, data_iter, loss): #@save
"""评估给定数据集上模型的损失"""
metric = d2l.Accumulator(2) # 损失的总和,样本数量
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]

# 定义训练函数
def train(train_features, test_features, train_labels, test_labels,
num_epochs=400):
loss = nn.MSELoss(reduction='none')
input_shape = train_features.shape[-1]
# 不设置偏置,因为我们已经在多项式中实现了它
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, train_labels.shape[0])
train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
batch_size)
test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
batch_size, is_train=False)
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
xlim=[1, num_epochs], ylim=[1e-3, 1e2],
legend=['train', 'test'])
for epoch in range(num_epochs):
d2l.train_epoch_ch3(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
print('weight:', net[0].weight.data.numpy())

# 三阶多项式函数拟合(正确)
# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])

# 线性函数拟合(欠拟合)
# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])

# 高阶多项式函数拟合(过拟合)
# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)

12 权重衰退

12.1 权重衰退

  • 使用均方范数作为硬性限制
    • 权重衰退,通过限制参数值的选择范围来控制模型容量: min l(w,b) subjecttow2<=θmin \space l(w,b) \space subject to ||w||^2 <= \theta
      • 通常不限制偏移 b (限制不限制都差不多)
      • 小的 θ\theta 意味着更强的正则项
  • 使用均方单数作为柔性限制
    • 对于每个 θ\theta,都可以找到 λ\lambda 使之前的目标函数等价于 min l(w,b)+λ2w2min \space l(w,b)+\frac{\lambda}{2}||w||^2,(可通过拉格朗日乘子来证明)
    • 超参数 λ\lambda 控制了正则项的重要程度
      -λ=0\lambda=0:无作用
      -λinf,w0\lambda \rightarrow \inf, w^* \rightarrow 0
  • 参数更新法则
    • 计算梯度:w((w,b)+λ2w2)=(w,b)w+λw\frac{\partial}{\partial \mathbf{w}}\left(\ell(\mathbf{w}, b)+\frac{\lambda}{2}\|\mathbf{w}\|^2\right)=\frac{\partial \ell(\mathbf{w}, b)}{\partial \mathbf{w}}+\lambda \mathbf{w}
    • 时间t更新参数:wt+1=(1ηλ)wtη(wt,bt)wt\mathbf{w}_{t+1}=(1-\eta \lambda) \mathbf{w}_t-\eta \frac{\partial \ell\left(\mathbf{w}_t, b_t\right)}{\partial \mathbf{w}_t}
      • 通常ηλ<1\eta \lambda < 1,在深度学习中通常叫做权重衰退
  • 权重衰退用来可解决欠拟合问题

12.2 代码实现

  • 生成数据的公式: y=0.05+i=1d0.01xi+ϵ where ϵN(0,0.012).y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where }\epsilon \sim \mathcal{N}(0, 0.01^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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 1 权重衰退的从零开始实现
# 权重衰退是最广泛使用的正则化技术之一
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

# 生成数据集
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5 # 训练数据容易发生过拟合
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

# 初始化模型参数
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]

# 定义L2范数惩罚
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2

# 定义训练代码实现
def train(lambd):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
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:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
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范数是:', torch.norm(w).item())

# 忽略正则化训练
train(lambd=0)

# 使用权重衰减
train(lambd=3)


# 2 权重衰减的简洁实现
# 其中使用weight-decay实现权重衰退,即W每次减小
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())

# 忽略正则化训练
train_concise(0)

# 使用权重衰退
train_concise(3)

12.3 QA

  • 一般权重衰减的值取10e0,10e-1,10e-2,10e-3,10e-4
    • 权重衰退的效果有限,使用时初始设置10e-3即可

13 丢弃法

13.1 丢弃法(dropout)

  • 动机
    • 一个好的模型需要对输入数据的扰动鲁棒
      • 使用有噪声的数据等价于Tikhonov正则
      • 丢弃法:在层之间加入噪音
  • 无偏差的加入噪音
    • xx 加入噪音得到 xx^{'},但希望 E(x)=xE(x^{'}) = x
    • 丢弃法对每个元素进行扰动:xi={0 with probablity pxi1p otherise x_i^{\prime}= \begin{cases}0 & \text { with probablity } p \\ \frac{x_i}{1-p} & \text { otherise }\end{cases},不改变数据的期望
      • dropout参数p为丢弃的节点的概率
  • 使用丢弃法:通常将丢弃法作用在隐藏全连接层的输出上
    -h=σ(W1x+b1)h=\sigma(W_1x+b_1)
    -h=dropout(h)h^{'} = dropout(h)
    -o=W2h+b2o = W_2h^{'}+b_2
    • y = softmax(o)

dropout

  • 推理中的丢弃法
    • 正则项只在训练中使用:影响模型参数的更新
    • 在推理过程中,丢弃法直接返回输入: h=dropout(h)h = dropout(h)
      • 保证确定性的输出
  • 总结
    • 丢弃法将一些输出项随机置0来控制模型复杂度
    • 常作用在多层感知机的隐藏层输出上
    • 丢弃概率是控制模型复杂度的超参数

13.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 1 Dropout的从零开始实现
# 实现dropout_layer函数,该函数以dropout的概率丢弃张量输入x中的元素
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()
# 在GPU或CPU中做乘法比选元素的效率快
return mask * X / (1.0 - dropout)

# 测试dropout_layer函数
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.))

# 定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

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)

# 训练和测试
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)


# 2 Dropout的简洁实现
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)

13.3 QA

  • dropout随机置0的地方梯度也为0,dropout随机置0的地方的权重在该轮不被更新
  • BN为卷积层使用的,dropout为全连接层使用的
    • BN层,可以加快网络的训练和收敛的速度,控制梯度爆炸防止梯度消失,防止过拟合
  • dropout改变的为隐藏层的输出
    • 也可以改变标签实现正则化(CV中会使用)
  • dropout和权重衰减为两种解决过拟合的方法
    • 权重衰减weight_weak更为常用可以在CNN等中使用
    • dropout更容易调参
      • 训练一个单隐藏层的全连接MLP,隐藏层不使用dropout为64,而使用dropout=0.5隐藏层为128,效果可能较好,需要让模型更强而使用正则使模型学偏

14 数值稳定性 + 模型初始化和激活函数

14.1 数值稳定性

  • 神经网络的梯度
    • 考虑有d层的神经网络: h^t=f_t(h^{t-1}) \space and \space y=l_d_d_...*f_1(x)
    • 计算损失关于参数的梯度: Wt=hdhdhd1ht+1hthtWt\frac{\partial \ell}{\partial \mathbf{W}^t}=\frac{\partial \ell}{\partial \mathbf{h}^d} \frac{\partial \mathbf{h}^d}{\partial \mathbf{h}^{d-1}} \ldots \frac{\partial \mathbf{h}^{t+1}}{\partial \mathbf{h}^t} \frac{\partial \mathbf{h}^t}{\partial \mathbf{W}^t}
      • 上述表达式中间计算了dtd-t次矩阵乘法
      • 太多矩阵乘法会带来梯度爆炸和梯度消失的问题
  • 梯度爆炸的问题
    • 值超出值域(对float16尤为严重,GPU对float16运算速度较快)
    • 对学习率敏感
      • 学习率太大->大参数值->更大的梯度
      • 学习率太小->训练无进展
      • 可能需要在训练过程不断调整学习率

梯度消失

  • 梯度消失的问题
    • 梯度值变成0(对float16尤为严重)
    • 训练没有进展,不管如何选择学习率
    • 对于底部层尤为严重,仅仅顶部层训练的较好,无法让神经网络更深

14.2 模型初始化和激活函数

  • 让训练更加稳定
    • 目标:让梯度值在合理的范围内
    • 让乘法变成加法: ResNet,LSTM
    • 归一化: 梯度归一化,梯度裁剪
    • 合理的权重初始和激活函数
  • 将神经网络设计的每层设计为如下形式可以使训练更加稳定
    • 让每层的方差都是一个常数
      • 让每层的输出和梯度都看作随机变量
      • 让均值和方差都保持一致
        - 正向  反向 E[hit]=0E[hit]=0Var[hit]=bi,tVar[hit]=a\begin{array}{cc}\text { 正向 } & \text { 反向 } \\ \mathbb{E}\left[h_i^t\right]=0 & \mathbb{E}\left[\frac{\partial \ell}{\partial h_i^t}\right]=0 \quad \operatorname{Var}\left[\frac{\partial \ell}{\partial h_i^t}\right]=b \quad \forall i, t \\ \operatorname{Var}\left[h_i^t\right] =a \end{array},a和b都是常数
  • 具体做法
    • 权重初始化:在合理区间里随机初始参数
    • 训练开始的时候更容易有数值不稳定
      • 远离最优解的地方损失函数表面可能很复杂
      • 最优解附近表面比较平
    • 使用N(0,0.01)N(0,0.01)来初始可能对小网络没问题,但不能保证深度神经网络
  • 例子MLP
    • 假设
      -wi,jtw_{i, j}^t 是 i.i.d, 那么E [wi,jt]=0,Var[wi,jt]=γt\left[w_{i, j}^t\right]=0, \operatorname{Var}\left[w_{i, j}^t\right]=\gamma_t
      -hit1h_i^{t-1} 独立于 wi,jtw_{i, j}^t
    • 假设没有激活函数 ht=Wtht1\mathbf{h}^t=\mathbf{W}^t \mathbf{h}^{t-1}, 这里 WtRnl×nt1\mathbf{W}^t \in \mathbb{R}^{n_l \times n_{t-1}}
      -E[hit]=E[jwi,jthjt1]=jE[wi,jt]E[hjt1]=0\mathbb{E}\left[h_i^t\right]=\mathbb{E}\left[\sum_j w_{i, j}^t h_j^{t-1}\right]=\sum_j \mathbb{E}\left[w_{i, j}^t\right] \mathbb{E}\left[h_j^{t-1}\right]=0
    • 正向方差:Var[(hit)2]=nt1γtVar[hjt1]\operatorname{Var}\left[(h_{i}^t)^2\right]=n_{t-1} \gamma_t \operatorname{Var}\left[h_j^{t-1}\right]
      • 假设输入方差和输出方差一致,则nt1γt=1n_{t-1} \gamma_t = 1
    • 反向均值和方差
      -ht1=htWt(ht1)T=(Wt)T(ht)T\frac{\partial \ell}{\partial \mathbf{h}^{t-1}}=\frac{\partial \ell}{\partial \mathbf{h}^t} \mathbf{W}^t \rightarrow \left(\frac{\partial \ell}{\partial \mathbf{h}^{t-1}}\right)^T=\left(W^t\right)^T\left(\frac{\partial \ell}{\partial \mathbf{h}^t}\right)^T
      -E[hit1]=0\mathbb{E}\left[\frac{\partial \ell}{\partial h_i^{t-1}}\right]=0
      -Var[hit1]=ntγtVar[hjt]ntγt=1\operatorname{Var}\left[\frac{\partial \ell}{\partial h_i^{t-1}}\right]=n_t \gamma_t \operatorname{Var}\left[\frac{\partial \ell}{\partial h_j^t}\right] \rightarrow n_t \gamma_t=1
    • Xavier初始化(权重初始化的方法,设置权重的方差)
      • 难以满足ntγt=1nt1γt=1n_t \gamma_t=1 和 n_{t-1} \gamma_t=1
      • Xavier 使得 γt(nt1+nt)/2=1γt=2/(nt1+nt)\gamma_t\left(n_{t-1}+n_t\right) / 2=1 \quad \rightarrow \gamma_t=2 /\left(n_{t-1}+n_t\right)
        • 正态分布 N(0,2/(nt1+nt))\mathcal{N}\left(0, \sqrt{2 /\left(n_{t-1}+n_t\right)}\right)
        • 均匀分布 U(6/(nt1+nt),6/(nt1+nt))U\left(-\sqrt{6 /\left(n_{t-1}+n_t\right)}, \sqrt{6 /\left(n_{t-1}+n_t\right)}\right)
          • 分布 U[a,a]U[-a, a] 和方差是 a2/3a^2 / 3
      • 适配权重形状更新,特别是ntn_t
  • 激活函数的选取
    • 假设线性的激活函数σ(x)=αx+β,h=Wtht1\sigma(x)=\alpha x+\beta, \mathbf{h}^{\prime}=\mathbf{W}^t \mathbf{h}^{t-1} \quad and ht=σ(h)\quad \mathbf{h}^t=\sigma\left(\mathbf{h}^{\prime}\right)
      • 正向
        -E[hit]=E[αhi+β]=ββ=0\mathbb{E}\left[h_i^t\right]=\mathbb{E}\left[\alpha h_i^{\prime}+\beta\right]=\beta \rightarrow \beta=0
        -Var[hit]=a2Var[hit]α=1\mathbb{Var}\left[h_i^t\right]=a^2 \mathbb{Var}\left[h_i^t\right] \rightarrow \alpha=1
      • 反向h=ht(Wt)T\frac{\partial \ell}{\partial \mathbf{h}^{\prime}}=\frac{\partial \ell}{\partial \mathbf{h}^t}\left(W^t\right)^T and ht1=αh\frac{\partial \ell}{\partial \mathbf{h}^{t-1}}=\alpha \frac{\partial \ell}{\partial \mathbf{h}^{\prime}}
        -E[hit1]=0β=0\mathbb{E}\left[\frac{\partial \ell}{\partial h_i^{t-1}}\right]=0 \rightarrow \beta=0
        -Var[hit1]=α2Var[hj]α=1\operatorname{Var}\left[\frac{\partial \ell}{\partial h_i^{t-1}}\right]=\alpha^2 \operatorname{Var}\left[\frac{\partial \ell}{\partial h_j^{\prime}}\right] \rightarrow \alpha=1
    • 常用的激活函数,使用泰勒展开
      -sigmoid(x)=12+x4x348+O(x5)tanh(x)=0+xx33+O(x5)relu(x)=0+x for x0\begin{aligned} \operatorname{sigmoid}(x) &=\frac{1}{2}+\frac{x}{4}-\frac{x^3}{48}+O\left(x^5\right) \\ \tanh (x) &=0+x-\frac{x^3}{3}+O\left(x^5\right) \\ \operatorname{relu}(x) &=0+x \quad \text { for } x \geq 0 \end{aligned}
      • sigmoid的一次项不满足要求,则需要调整sigmoid为4sigmoid(x)24*sigmoid(x)-2即可满足要求

激活函数

14.3 QA

  • nan和inf的产生原因和解决办法
    • nan的产生原因:除以0出现,一般梯度过小会出现
    • inf的产生原因:learning rate过大,或权重初始时值太大出现
    • 解决办法:学习率选择不太大(优先调整学习率使不产生nan和inf),合理初始化权重,激活函数的选择
  • 在训练过程中,如果网络层的输出中间层特征元素的值突然变成nan,一般是发生了梯度爆炸问题
  • 梯度消失的原因可能由sigmoid激活函数引起,但不仅仅是由该原因引起

15 实战:Kaggle房价预测 + 课程竞赛:加州2020年房价预测

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# 实现几个函数来方便下载数据
import hashlib
import os
import tarfile
import zipfile
import requests

#@save
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

def download(name, cache_dir=os.path.join('..', 'data')): #@save
"""下载一个DATA_HUB中的文件,返回本地文件名"""
assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
url, sha1_hash = DATA_HUB[name]
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split('/')[-1])
if os.path.exists(fname):
sha1 = hashlib.sha1()
with open(fname, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
return fname # 命中缓存
print(f'正在从{url}下载{fname}...')
r = requests.get(url, stream=True, verify=True)
with open(fname, 'wb') as f:
f.write(r.content)
return fname

def download_extract(name, folder=None): #@save
"""下载并解压zip/tar文件"""
fname = download(name)
base_dir = os.path.dirname(fname)
data_dir, ext = os.path.splitext(fname)
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
else:
assert False, '只有zip/tar文件可以被解压缩'
fp.extractall(base_dir)
return os.path.join(base_dir, folder) if folder else data_dir

def download_all(): #@save
"""下载DATA_HUB中的所有文件"""
for name in DATA_HUB:
download(name)

# 使用pandas读入并处理数据
%matplotlib inline
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l

DATA_HUB['kaggle_house_train'] = ( #@save
DATA_URL + 'kaggle_house_pred_train.csv',
'585e9cc93e70b39160e7921475f9bcd7d31219ce')

DATA_HUB['kaggle_house_test'] = ( #@save
DATA_URL + 'kaggle_house_pred_test.csv',
'fa19780a7b011d9b009e8bff8e99922a8ee2eb90')

train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))

print(train_data.shape)
print(test_data.shape)

# 前四个和最后两个特征,以及相应标签
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])

# 在每个样本中,第一个特征是ID,将其从数据集中删除
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

# 将所有缺失的值替换成相应特征的平均值,通过将特征重新缩放到零均值和单位方差来标准化数据
# 若无法获得测试数据,则可根据训练数据计算均值和标准差
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0
all_features[numeric_features] = all_features[numeric_features].fillna(0)

# 处理离散值,使用一次独热编码替换它们
# “Dummy_na=True”将“na”(缺失值)视为有效的特征值,并为其创建指示符特征
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape

# 从pandas格式中提取Numpy格式,并将其转换为张量
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)
train_labels = torch.tensor(
train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)

# 训练
loss = nn.MSELoss()
in_features = train_features.shape[1]

def get_net():
net = nn.Sequential(nn.Linear(in_features,1))
return net

# 更关心相对误差,解决问题的一种方法就是用价格预测的对数来衡量差异
def log_rmse(net, features, labels):
# 为了在取对数时进一步稳定该值,将小于1的值设置为1
clipped_preds = torch.clamp(net(features), 1, float('inf'))
rmse = torch.sqrt(loss(torch.log(clipped_preds),
torch.log(labels)))
return rmse.item()

# 训练函数借助Adam优化器
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
train_iter = d2l.load_array((train_features, train_labels), batch_size)
# 这里使用的是Adam优化算法
optimizer = torch.optim.Adam(net.parameters(),
lr = learning_rate,
weight_decay = weight_decay)
for epoch in range(num_epochs):
for X, y in train_iter:
optimizer.zero_grad()
l = loss(net(X), y)
l.backward()
optimizer.step()
train_ls.append(log_rmse(net, train_features, train_labels))
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
return train_ls, test_ls

# K折交叉验证
def get_k_fold_data(k, i, X, y):
assert k > 1
fold_size = X.shape[0] // k
X_train, y_train = None, None
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size)
X_part, y_part = X[idx, :], y[idx]
if j == i:
X_valid, y_valid = X_part, y_part
elif X_train is None:
X_train, y_train = X_part, y_part
else:
X_train = torch.cat([X_train, X_part], 0)
y_train = torch.cat([y_train, y_part], 0)
return X_train, y_train, X_valid, y_valid

# 返回训练和验证误差的平均值
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,
batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(k):
data = get_k_fold_data(k, i, X_train, y_train)
net = get_net()
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
weight_decay, batch_size)
train_l_sum += train_ls[-1]
valid_l_sum += valid_ls[-1]
if i == 0:
d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
legend=['train', 'valid'], yscale='log')
print(f'折{i + 1},训练log rmse{float(train_ls[-1]):f}, '
f'验证log rmse{float(valid_ls[-1]):f}')
return train_l_sum / k, valid_l_sum / k

# 模型验证
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
weight_decay, batch_size)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
f'平均验证log rmse: {float(valid_l):f}')

# 提交kaggle预测
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
net = get_net()
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
print(f'训练log rmse:{float(train_ls[-1]):f}')
# 将网络应用于测试集。
preds = net(test_features).detach().numpy()
# 将其重新格式化以导出到Kaggle
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv('submission.csv', index=False)

train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)

16 PyTorch 神经网络基础

16.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 层和块
# 回顾多层感知机
import torch
from torch import nn
from torch.nn import functional as F # 定义了一些函数

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

X = torch.rand(2, 20)
net(X)
# nn.Sequential定义了一种特殊的Module

# 自定义定义块
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)))

net = MLP()
net(X)

# 顺序块
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

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

# 在正向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 不计算梯度的随机权重参数。因此其在训练期间保持不变
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)

def forward(self, X):
X = self.linear(X)
# 使用创建的常量参数以及relu和mm函数
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 复用全连接层。这相当于两个全连接层共享参数
X = self.linear(X)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()

net = FixedHiddenMLP()
net(X)

# 混合搭配各种组合块的方法
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)

16.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 参数管理
# 首先关注具有单隐藏层的多层感知机
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())

# 目标参数
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
# class 'torch.nn.parameter.Parameter'说明类型为可更新的参数

net[2].weight.grad == None # 访问梯度

# 一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])

net.state_dict()['2.bias'].data # 直接使用名字访问参数

# 从嵌套块收集参数
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(rgnet)

rgnet[0][1][0].bias.data

# 内置初始化
def init_normal(m):
if type(m) == nn.Linear:
# _下滑线为一种内置写法,表示的是替换操作,而不是返回一个值
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal) # 遍历net里面的层使用init_normal
net[0].weight.data[0], net[0].bias.data[0]

def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1) # 将weight全部初始化为 1, 但实际中不可以
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]

# 对某些快应用不同的初始化方法
def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

# 自定义初始化
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]

# 始终可以手动设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]

# 参数绑定
# 在多个层间共享参数:可以定义一个稠密层,然后使用它的参数来设置另一个层的参数
# 我们需要给共享层一个名称,以便可以引用它的参数
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])

16.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
33
34
35
36
37
38
39
40
41
# 构造一个没有任何参数的自定义层
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()

layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))

# 将层作为组件合并到构建更加复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

Y = net(torch.rand(4, 8))
Y.mean()

# 带参数的图层
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)

linear = MyLinear(5, 3)
linear.weight

# 使用自定义层执行前向计算
linear(torch.rand(2, 5))

# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

16.4 读写文件

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
# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4)
torch.save(x, 'x-file')

x2 = torch.load('x-file')
x2

# 存储一个张量列表,然后把它们读回内存
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)

# 导入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2

# 加载和保存模型参数
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)

# 将模型参数存储在mlp.params文件中
torch.save(net.state_dict(), 'mlp.params')

# 实例化了原始多层感知机模型的一个备份
# 直接读取文件中存储的参数
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()

Y_clone = clone(X)
Y_clone == Y

16.5 QA

  • 将特征字符串变成tensor,使用one-hot方法进行转换,内存不足时的解决办法
    • 方法一:使用稀疏矩阵进行解决
    • 方法二:使用其他方法进行解决,使用summary
  • net(x)实现的原理
    • 其继承的nn.module实现了__call__方法,类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用

17 使用和购买 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
# 计算设备
import torch
from torch import nn

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

# 查询可用gpu的数量
torch.cuda.device_count()

# 允许在请求的GPU不存在的情况下运行代码
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()

# 查询张量所在的设备
x = torch.tensor([1, 2, 3])
x.device

# 存储在GPU上
X = torch.ones(2, 3, device=try_gpu())
X

# 第二个GPU上创建一个随机张量
Y = torch.rand(2, 3, device=try_gpu(1))
Y

# 要计算X+Y,需要决定在哪里执行这个操作
# 必须保证运算的对象在同一个GPU上
Z = X.cuda(1)
print(X)
print(Z)

Y + Z

Z.cuda(1) is Z
# 数据在CPU和GPU之间传递,尤其消耗资源

# 神经网络与GPU
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu()) # 将网络迁移到GPU上

net(X)

# 确认模型参数存储在同一个GPU上
net[0].weight.data.device

18 预测房价竞赛总结

  • 使用的方法:
  • 在该竞赛中,排名靠前的都是用了集成学习
    • 集成学习为刷榜的常用方法
  • 特征预处理和超参数是取得好成绩的基础
  • 房价数据集的难点
    • 数值较大
    • 有文本特征(地址,介绍)
    • 训练数据是前6个月,公榜是后3个月,私榜是再后3个月
  • 关于automl
    • 数据科学家80%时间在处理数据,20%调模型
    • automl目前节省10%时间

19 卷积层

19.1 从全连接到卷积

  • 重新考察全连接层
    • 将输入和输出变形为矩阵(宽度,高度)
    • 将权重变形为4-D张量(h,w)(h,w)(h,w)(h^{'},w^{'}):hi,j=wi,j,k,lxk,l=vi,j,a,bxi+a,j+bh_{i,j} = \sum w_{i,j,k,l}x_{k,l} = \sum v_{i,j,a,b}x_{i+a,j+b}
    • V是W的重新索引vi,j,a,b=wi,j,i+a,j+bv_{i,j,a,b}=w_{i,j,i+a,j+b}
  • 原则#1-平移不变性
    • x的平移导致h的平移hi,j=vi,j,a,bxi+a,j+bh_{i,j} = \sum v_{i,j,a,b}x_{i+a,j+b}
    • v不应该依赖于(i,j)(i,j)
    • 解决方案:vi,j,a,b=va,bv_{i,j,a,b}=v_{a,b},hi,j=va,bxi+a,j+bh_{i,j} = \sum v_{a,b}x_{i+a,j+b}
    • 这就是二维卷积(实际为二维交叉相关)
  • 原则#2-局部性
    -hi,j=va,bxi+a,j+bh_{i,j} = \sum v_{a,b}x_{i+a,j+b}
    • 当评估hi,jh_{i,j}时,不应该用远离xi,jx_{i,j}的参数
    • 解决方案:当a,b>Δ|a|,|b|>\Delta时,使得va,b=0v_{a,b}=0
      -hi,j=a=ΔΔb=ΔΔva,bxi+a,j+bh_{i,j}=\sum_{a=-\Delta}^\Delta\sum_{b=-\Delta}^\Delta v_{a,b}x_{i+a,j+b}
  • 卷积层是特殊的全连接层,其使用了平移不变性和局部性原理

19.2 卷积层

二维卷积

  • 二维卷积层
    • 输入X:nhnwn_h*n_w
    • 核W:khkwk_h*k_w
    • 偏差bRb\in\R
    • 输出Y:(nhkh+1)(nwkw+1)(n_h-k_h+1)*(n_w-k_w+1)
      -Y=XW+b\mathbf{Y}=\mathbf{X} \star \mathbf{W}+b
    • 核矩阵 W 和 偏移 b是可学习的参数, 核矩阵的大小是超参数
    • 卷积层实际上计算的是交叉相关
  • 交叉相关和卷积由于对称性,实际在使用中没有区别
    • 二维交叉相关:yi,j=a=1hb=1wwa,bxi+a,j+by_{i, j}=\sum_{a=1}^h \sum_{b=1}^w w_{a, b} x_{i+a, j+b}
    • 卷积:yi,j=a=1hb=1wwa,bxi+a,j+by_{i, j}=\sum_{a=1}^h \sum_{b=1}^w w_{-a,-b} x_{i+a, j+b}
    • 一维交叉相关:文本,语言,时序序列
    • 三维:视频,医学图像,气象图片

19.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
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
# 互相关运算
import torch
from torch import nn
from d2l import torch as d2l

def corr2d(X, K): #@save
"""计算二维互相关运算"""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y

# 验证上述互相关运算的输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)

# 实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))

def forward(self, x):
return corr2d(x, self.weight) + self.bias

# 卷积层的一个简单应用:检测图像中不同颜色的边缘
X = torch.ones((6, 8))
X[:, 2:6] = 0
X

# 构建卷积核
K = torch.tensor([[1.0, -1.0]])

# 输出Y中的1代表从白到黑的边缘,-1表示从黑到白
Y = corr2d(X, K)
Y

# 卷积核K只能检测垂直边缘
corr2d(X.t(), K)


# 学习由X生成Y的卷积核
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率

for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')

conv2d.weight.data.reshape((1, 2))

19.4 QA

  • 感受野不是越大越好,最好将网络设计的深一点,而不是大卷积核,类似全连接层为什么不做的很大的问题

20 卷积层里的填充和步幅

20.1 填充和步幅

  • 填充
    • 给定32*32输入图像
    • 应用5*5大小的卷积核
      • 第1层得到输出大小28*28
      • 第7层得到输出大小4*4
    • 更大的卷积核可以更快地减小输出大小
      • 形状从 n_k_n_w 减小到 (njkh+1)(nwkw+1)(n_j-k_h+1)_(n_w-k_w+1)
  • 填充:在输入周围添加额外的行/列
    • 填充php_h行和pwp_w列,输出形状为(nhkh+ph+1)(nwkw+pw+1)(n_h-k_h+p_h+1)*(n_w-k_w+p_w+1)
    • 通常取ph=kh1,pw=kw1p_h=k_h-1,p_w=k_w-1
      • khk_h为奇数:在上下两侧填充ph2\frac{p_h}{2}
      • khk_h为偶数:在上侧填充[ph2][\frac{p_h}{2}], 在下侧填充[ph2][\frac{p_h}{2}]
  • 步幅
    • 填充减小的输出大小与层数线性相关
      • 给定输入大小224*224,在使用5*5卷积核的情况下,需要44层将输出降低到4*4
      • 需要大量计算才能得到较小输出
    • 步幅是指行/列的滑动步长
      • 给定高度 shs_h 和宽度 sws_w 的步幅,输出形状是 [(nhkh+ph+sh)/sh][(nwkw+pw+sw)/sw][(n_h-k_h+p_h+s_h)/s_h]*[(n_w-k_w+p_w+s_w)/s_w]
      • 如果ph=kh1,pw=kw1p_h=k_h-1,p_w=k_w-1[(nh+sh1)/sh][(nw+sw1)/sw][(n_h+s_h-1)/s_h]*[(n_w+s_w-1)/s_w]
      • 如果输入高度和宽度都可被步幅整除:(nhsh)(nwsw)(\frac{n_h}{s_h}) * (\frac{n_w}{s_w})
  • 总结
    • 填充和步幅是卷积层的超参数
    • 填充在输入周围添加额外的行/列,来控制输出形状的减小量
    • 步幅是每次滑动核窗口时的行/列的步长,可以成倍的减少输出形状

20.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
# 填充和步幅
# 在所有侧边填充1个像素
import torch
from torch import nn


# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道
return Y.reshape(Y.shape[2:])

# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape

# 上下各填充2行,左右各填充1列
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

# 将高度和宽度的步幅设置为 2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape

# 稍微复杂的例子
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape

20.3 QA

  • 超参数:核大小,填充,步幅
    • 一般而言,填充保证输入和输出的大小相同,填充为核大小减一,为默认值
    • 一般而言,步幅,步幅为 1 最好,当计算量太大时,步幅较大,取 2
    • 一般而言,核大小为最关键的
    • 填充,步幅和通道数一般是网络架构的一部分,一般不改变
  • 卷积核的边长一般取奇数,一般选择 3*3
  • 使用自动机学习,NAS方法可以让超参数一起训练

21 卷积层里的多输入多输出通道

  • 多个输入通道
    • 彩色图片可能有RGB三个通道,转换为灰度会丢失信息
    • 每个通道都有一个卷积核,结果是所有通道卷积结果的和
    • 输入X:c_i_n_h_n_w
    • 核W:c_i_k_h_k_w
    • 输出Y:mhmwm_h*m_w,表达式为Y=i=0ciXi,:,:Wi,:,:\mathbf{Y}=\sum_{i=0}^{c_i} \mathbf{X}_{i,:,:} \star \mathbf{W}_{i,:,:}
  • 多个输出通道
    • 无论多少个输入通道目前只用到单输出通道
    • 可以有多个三维卷积核,每个核生成一个输出通道
    • 输入X:c_i_n_h_n_w
    • 核W:c_o_c_i_k_h*k_w
    • 输出Y:c_o_m_h_m_w,表达式为Yi,:,:=XWi,:,:,:\mathbf{Y}_{i,:,:}=\mathbf{X} \star \mathbf{W}_{i,:,:,:}
  • 多个输入和输出通道
    • 每个输出通道可以识别特定模式
    • 输入通道核识别并组合输入中的模式
  • 1*1卷积核
    -kh=kw=1k_h=k_w=1是一个受欢迎的选择,不识别空间模式,只是融合通道
    • 相当于输入形状为n_hn_w_c_i,权重为c_o_c_i的全连接层

1*1卷积层

  • 二维卷积层
    • 输入X:c_i_n_h_n_w
    • 核W:c_o_c_i_k_h*k_w
    • 偏差B:cocic_o*c_i
    • 输出Y:c_o_m_h_m_w,表达式为Yi,:,:=XW+B\mathbf{Y}_{i,:,:}=\mathbf{X} \star \mathbf{W} + \mathbf{B}
    • 计算复杂度(浮点计算书FLOP) O(cicokhkwmhmw)O(c_ic_ok_hk_wm_hm_w)
  • 每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道结果;每个输出通道有独立的三维卷积核
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
# 实现以下多输入通道互相关运算
import torch
from d2l import torch as d2l

def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))

# 验证互相关运算的输出
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)

# 计算多个通道的输出的互相关函数
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0)
K.shape

corr2d_multi_in_out(X, K)


# 1*1卷积核
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))

X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
  • QA
    • 通常情况下,输入和输出大小减半时,输出的通道数会加一倍(空间信息压缩使用更多通道存储信息)
    • Padding 0很多不会影响卷积神经网络
    • 每个通道的卷积核不一样,同一层不同通道的卷积核大小一样,为了计算方便,若不一样需要进行两个卷积分开操作,计算效率更高
    • 在计算卷积时,偏差的作用性越来越低,尤其是在batch normalization时
    • MobileNet:使用3*3*3和1*1*n的卷积层叠加,来分别进行空间信息的检测和信息融合以及输出通道的调整

22 池化层

  • 池化层
    • 积对位置敏感
    • 需要一定程度的平移不变性,物体稍微改动,输出不变,因此需要一个池化层
    • 池化层的目的为:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性
  • 二维最大池化层
    • 返回滑动窗口中的最大值
  • 填充,步幅,和多个通道
    • 池化层与卷积层类似,都具有填充、步幅和窗口大小作为超参数
    • 没有可学习的参数
    • 在每个输入通道应用池化层以获得相应的输出通道,不会像卷积层将多个通道融合
    • 输出通道数=输入通道数
  • 平均池化层
    • 最大池化层:每个窗口中最强的模式信号
    • 平均池化层:将最大池化层中最大操作替换成平均
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
# 实现池化层的正向传播
import torch
from torch import nn
from d2l import torch as d2l

def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y

# 验证二维最大池化层的输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))

# 验证平均池化层
pool2d(X, (2, 2), 'avg')

# 填充和步幅
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X

# 深度学习框架中的步幅与池化窗口的大小相同 (3*3的窗口)
pool2d = nn.MaxPool2d(3)
pool2d(X)

# 填充和步幅可以手动设定
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

# 设定一个任意大小的矩形池化窗口,并设置填充和步幅,2行*3列的padding窗口
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)

# 池化层在每个输入通道上单独运算
X = torch.cat((X, X + 1), 1)

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
  • QA
    • 池化层一般放在卷积层之后,降低卷积对位置的敏感性
    • 池化时,窗口重叠与没有重叠几乎没有影响
      • 目前池化层的使用越来越少,通过其他操作实现

23 经典卷积神经网络 LeNet

LeNet

  • LeNet其中使用5*5的卷积核和2*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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# LeNet由两个部分组成:卷积编码器和全连接层密集块
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(), #为了获得更多信息,padding了 2
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)

# LeNet在Fashion-MNIST数据集上的表现
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

# 使用GPU对evaluate_accuracy函数进行轻微的修改
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度"""
if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

# 为了使用GPU,需要修改train函数
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss() # 交叉熵
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,样本数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step() # 进行模型更新
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')

# 训练和评估LeNet模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • 卷积神经网络设计思想:

    • 使用不同的卷积层获取空间信息并不断进行压缩,将压缩后的信息增加到不同通道中,不断减小高和宽,最后通过感知机实现
  • QA

    • 常见做法:高宽减半,通道数翻倍(使通道能匹配的模式更多)

24 深度卷积神经网络 AlexNet

AlexNet

  • AlexNet
    • AlexNet赢了2012年ImageNet竞赛
    • 更深更大的LeNet
    • 主要改进
      • 丢弃法:方便做更大的模型
      • ReLu:让Sigmoid激活函数梯度可以更大,支持更深的模型
      • MaxPooling:使得输出值比较大,梯度值相应更大,使得训练更加容易
    • 计算机视觉方法论的改变
  • 对比
    • 传统机器学习方法:输入图片->人工特征提取->SVM
    • 深度学习方法:输入图片->通过CNN学习特征->Softmax回归
      • 构造CNN相对简单, Softmax更加高效
      • 转换为端到端的学习

AlexNet架构

AlexNet架构

AlexNet架构

  • AlexNet相较于LeNet的架构变化
    • 更大的核窗口和步长,因为图片更大了
    • 使用3*3的池化窗口,2*2运行一个像素往一边移动一下不影响输出,而3*3允许往左右各移一点不影响输出
    • 输出通道变为256,用更大的输出通道,识别更多的模式
    • 新加了3层卷积层和一个池化层
    • 从120增加到了4096,输出1000类
    • 窗口更大,增加了三个卷积层,最后的全连接层更大
    • 激活函数从sigmoid变到了ReLu(减缓梯度消失)
    • 隐藏全连接层后加入了丢弃层(两个4096全连接层增加dropout丢弃)
    • 数据增强

AlexNet复杂度

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
# AlexNet深度神经网络
import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(
# 这里,我们使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))

# 构造一个单通道数据,观察每一层的输出
X = torch.randn(1, 1, 224, 224)
for layer in net:
X=layer(X)
print(layer.__class__.__name__,'output shape:\t',X.shape)

# Fashion-MNIST图像的分辨率低于ImageNet图像,将它们增加到224*224
# (图片拉大并不会增加信息量)
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

# 训练
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • QA
    • 卷积神经网络卷积层提取的信息不够充足,需要两个全连接Dense(4096)层,若只是用一个,效果会变差
    • 网络要求输入size固定,对于不同的图片需要保持长宽比,将短边resize到指定的尺寸再进行裁剪

25 使用块的网络 VGG

VGG

AlexNet与LeNet架构

  • AlexNet最大问题是结构不够规则,结构不够清晰,设计更深的网络需要更好的设计思想,将框架设计的更加规则
    • 使用更多的全连接层(太贵)
    • 更多的卷积层
    • 将卷积层组合成块(VGG的重要思想)

VGG块与AlexNet

  • VGG块
    • 同样的计算开销的情况下,深但窄效果更好
    • 3*3卷积(填充1),n层,m通道
    • 2*2最大池化层(步幅2)
  • VGG结构
    • 多个VGG块后接全连接层
    • 不同次数的重复块得到不同的架构,VGG-16,VGG-19,…
    • VGG对AlexNet最大的贡献:将AlexNet中的卷积和池化层抽离成VGG块

VGG与AlexNet

  • 总结
    • VGG 使用可重复使用的卷积块来构建深度卷积神经网络
    • 不同的卷积块个数和超参数可以得到不同复杂度的变种
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
# VGG块
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)

# 原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层
# 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512
# 由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))

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)

# 观察每个层输出的形状
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
X = blk(X)
print(blk.__class__.__name__,'output shape:\t',X.shape)

# 由于VGG-11比AlexNet计算量更大,因此构建了一个通道数较少的网络
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)

# 模型训练
lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

26 网络中的网络 NiN

  • 网络中的网络NiN目前使用较少,但提出很多思想
  • 全连接层的问题
    • 卷积层需要较少的参数 c_i_c_o_k^2
    • 但卷积层后的第一个全连接层的参数
      • LeNet 16_5_5*120=48k
      • AlexNet 255_5_5*4096=26M
      • VGG 512_7_7*4096=102M,占用空间较大,空间的大部分占用都在卷积层后的第一个全连接层
  • 为了解决VGG中参数较多,占用较大的问题,提出NiN
    • NiN的思想:完全不要全连接层
  • NiN块
    • 一个卷积层后跟两个全连接层
      • 步幅1,无填充,输出形状跟卷积层输出一样
      • 1*1卷积层起到了全连接层的作用
    • NiN块类似最简单的卷积神经网络

NiN块

  • NiN架构
    • 无全连接层
    • 交替使用NiN块和步幅为2的最大池化层
      • 逐步减小高宽和增大通道数,步幅为2的最大池化层为高宽减半
    • 最后使用全局平均池化层(高宽等于输入的高宽)得到输出
      • 其输入通道数是类别数

NiN

  • 总结
    • NiN块使用卷积层加两个1*1卷积层
      • 后者对每个像素增加了非线性性(两个卷积层相当于做了两个隐含层的MLP,MLP的ReLu函数增加了线性性)
    • NiN使用全局平均池化层来替代VGG和AlexNet中的全连接层
      • 不容易过拟合,更少的参数个数
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
# NiN块
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())

# NiN模型(模型的许多参数都是从AlexNet而来)
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())

# 查看每个块的输出形状
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)

# 训练模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • QA
    • 使用pytorch的部署到C++生产环境中,可以使用torchScript或者onnx部署
    • 分类使用softmax,softmax函数写在training函数的loss函数中,没有写在神经网络中
    • 加入全局池化层,将输入降低,且没有可学习的参数,使学习变得简单,最大的好处是使模型复杂度降低,提升泛化性,缺点是使收敛变慢

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

  • googleNet

googleNet

  • 最好的卷积层超参数

卷积层超参数

Inception块

  • Inception块:4个路径从不同层面抽取信息,然后再输出通道维合并
    • 结构
      • 第一条路径使用1*1的卷积层
      • 第二条路先使用1*1的卷积层对通道做变换,然后使用3*3的卷积层
      • 第三条路先使用1*1的卷积层对通道做变换,然后使用5*5的卷积层
      • 第四条路先使用3*3的池化层,然后使用1*1的卷积层
      • 最后使用concatenation做合并,跟输入等同高宽
    • 跟单3*3或5*5卷积层比,Inception块由更少的参数个数和计算复杂度

Inception块
上图中白色的框用来变换通道数,其他蓝色框用来抽取空间信息

  • GoogLeNet
    • 5段,9个Inception块
    • stage:高宽减半为一个stage
    • GoogLeNet stage1 为 7*7卷积核3*3MaxPool
    • GoogLeNet stage2 为 1*1卷积 3*3卷积核 3*3MaxPool
    • GoogLeNet stage3 为 2 个Inception block(不改变高宽只改变通道数) 3*3MaxPool
    • GoogLeNet stage4 为 5 个Inception block(不改变高宽只改变通道数) 3*3MaxPool
    • GoogLeNet stage5 为 2 个Inception block(不改变高宽只改变通道数) 3*3MaxPool
    • 最后使用全局池化层和全连接层
    • GoogLeNet大量使用1*1卷积层增加通道数,并且使用全局池化层

GoogLeNet

  • 段1 & 段2:更小的窗口,更多的通道
    • 快速减小图像尺寸并迅速增大通道数,使用更小的卷积层高宽保留更流

段1 & 段2

  • 段3: 2个Inception block的通道数的分配不同

段3

  • 段4 & 段5: 5个Inception block

段4 & 段5

  • Inception由各种后续变种
  • Inception-BN(V2):使用batch normalization
  • Inception-V3:修改了Inception块
    • 替换5*5为多个3*3卷积层
    • 替换5*5为1*7和7*1卷积层
    • 替换3*3为1*3和3*1卷积层
    • 更深
  • Inception-V4:使用残差连接

InceptionV3

InceptionV3

InceptionV3

上图右侧为InceptionV1的版本,左图为InceptionV3的版本

  • 总结
    • Inception块由4条由不同超参数的卷积层和池化层的路来抽取不同的信息
      • 一个主要优点就是模型参数小,计算复杂度低
    • GoogLeNet使用了9个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
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
68
69
70
# Inception块
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)

# 实现每一个stage
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))

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))

# 为了使Fashion-MNIST上的训练短小精悍,将输入的高和宽从224降到96
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)

# 训练
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • QA
    • 1*1的卷积层用来降低通道数,减小计算量
    • 在神经网络搭建中,最好不要修改经典的模型,或者将通道数整体减少
    • 通道数通常设置为2n2^n,主要是为了方便GPU计算
    • 目前主要使用GoogleNetV3

28 批量归一化(Batch norm)

  • 批量归一化
    • 问题
      • 损失出现在最后,后面的层训练较快
      • 数据在最底部
        • 底部的层训练较慢
        • 底部层一变化,所有都得跟着变
        • 最后的那些层需要重新学习多次
        • 导致收敛变慢
      • 批量归一化可以在学习底部层的时候避免变化顶部层
    • 原理
      • 固定小批量里面的均值和方差:μB=1BiBxi\mu_B=\frac{1}{|B|} \sum_{i \in B} x_i and σB2=1BiB(xiμB)2+ϵ\sigma_B^2=\frac{1}{|B|} \sum_{i \in B}\left(x_i-\mu_B\right)^2+\epsilon
      • 然后再做额外的调整(可学习的参数):xi+1=γxiμBσB+β,β为均值,γ为方差x_{i+1}=\gamma \frac{x_i-\mu_B}{\sigma_B}+\beta, \beta为均值,\gamma为方差
    • 批量归一化层
      • 可学习的参数γβ\gamma \beta
      • 作用在
        • 全连接层和卷积层输出上,激励函数前
        • 全连接层和卷积层输入上
      • 对全连接层,作用在特征维
        • 二维输入,每一行为样本,每一列为特征
      • 对卷积层,作用在通道维
        • 在每个批量里,一个像素是一个样本,与像素(样本)对应的通道维为就是特征维
    • 批量归一化在做什么
      • 最初论文是向用它来减少内部协变量转移
      • 后续有论文指出它可能就是通过在每个小批量里加入噪声来控制模型变量:xi+1=γxiμBσB+β,μbσbx_{i+1}=\gamma \frac{x_i-\mu_B}{\sigma_B}+\beta,\mu_b和\sigma_b为随机偏移和随机缩放,为噪声,然后通过一个学习到的方差和均值进行归一化
      • 没必要和dropout丢弃法混合使用
  • 批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放
    • 可以加速收敛速度,但一般不改变模型精度
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# batch norm的从零开始实现
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

class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数。
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)

def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y

# 应用BatchNorm于LeNet模型上
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
nn.Linear(84, 10))

# 训练
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

# 查看gamma和beta
net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,))

# -------
# 简洁实现
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
  • 代码实现中:momentum=0.9, eps=1e-5, 1e-6, 1e-7 都可以,不同的 eps 对输出有一定影响

    • pytorch中默认momentum=0.1
  • QA

    • xavier保证在参数初始化时初始化正常避免梯度爆炸和消失;BN保证在训练过程中均值和方差,可以保证较大的梯度,保证可使用较大的学习率,保证训练速度

29 残差网络 ResNet

  • ResNet是实际使用中较好用的一个神经网络
  • 对于非嵌套函数类,较复杂(由较大区域表示)的函数类不能保证更接近“真”函数( ff^* ),在嵌套函数类中不会发生,只有当较复杂的函数类包含较小的函数类时才能确保提高它们的性能
    • 对于深度神经网络,如果我们能将新添加的层训练成_恒等映射_f(x)=xf(\mathbf{x}) = \mathbf{x},新模型和原模型将同样有效;同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差
      • 对于非嵌套函数类(图左边),较复杂的函数类并不总是向“真”函数ff^*靠拢(复杂度由F1\mathcal{F}_1F6\mathcal{F}_6递增),虽然F3\mathcal{F}_3F1\mathcal{F}_1更接近ff^*,但F6\mathcal{F}_6却离的更远了
      • 相反对于图右侧的嵌套函数类F1F6\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6,可以避免上述问题

函数网络

  • 残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一

  • 残差块

    • 串联一个层改变函数类,希望能扩大函数类
    • 残差块加入快速通道(右边)来得到 f(x)=x+g(x)f(x) = x+g(x) 的结构
    • 左边的ResNet的高宽不变,右边的ResNet块高宽减半,靠近输入的3*3卷积核和1*1卷积核的步幅为2

残差块

  • ResNet块是从VGG过来的

ResNet块细节

  • 不同的残差块

不同的残差块

  • ResNet网络:类似VGG和GoogLeNet的总体架构,有5个stage,最后有一个全局池化层,但替换成了ResNet块

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
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
68
69
70
71
72
73
74
75
76
# 从零开始实现
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)

# 输入和输出形状一致
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape

# 增加输出通道数的同时,减半输出的高和宽
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape

# ResNet模型
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))

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

# 观察不同模块的输入形状是如何变化的
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)

# 训练
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

31 深度学习硬件:CPU 和 GPU

  • 提升CPU利用率

    • 在计算a+b之前,需要准备数据
      • 主内存->L3->L2->L1->寄存器
    • 提升空间和时间的内存本地性
      • 时间:重用数据使得保持它们在缓存
      • 空间:按序读写数据使得可以预读取
    • 如果一个矩阵是按列存储,访问一行会比访问一列要快
      • CPU会一次读取64字节(缓存线)
      • CPU会提前读取下一个(缓存线)
    • 并行来利用所有核:超线程不一定提升性能,因为它们共享寄存器
  • 提升GPU利用率

    • 并行:使用数千个线程
    • 内存本地性:缓存更小,架构更加简单
    • 少用控制语句:支持有限,同步开销很大
  • CPU/GPU带宽

    • 带宽受限,并且需要同步开销
    • 不要频繁在CPU和GPU之间传数据
  • CPU/GPU高性能计算编程

    • CPU:C++或任何高性能语言
    • GPU:
      • Nvidia:Cuda,编译器和驱动成熟
      • OpenCL:质量取决于硬件厂商
  • QA

    • 全连接层较为耗费内存和性能,且模型占用空间更大,模型的计算复杂度和模型的占用空间不成正比
    • w-=lr*w.grad计算前后的w的地址相同,而w=w-lr*w.grad计算后右式赋值给一个新的变量,然后赋值给变量 w,地址会发生变化,不推荐使用

32 深度学习硬件:TPU和其他

  • DSP:数字信号处理
    • 为数字信号处理算法设计:点积,卷积,FFT
    • 低功耗,高性能,比移动GPU快5X,功耗更低
    • VLIW:一条指令加u四年上百次乘累加
    • 编程和调试困难,编译器质量良莠不齐
  • 可编程阵列(FPGA)
    • 有大量可编程逻辑单元和可配置的连接
    • 可配置成计算复杂函数:VHDL,Verilog
    • 通常比通用硬件更高效
    • 工具链质量良莠不齐
    • 一次编译需要数小时
  • AI ASIC
    • google TPU是标志性芯片
      • 能够媲美Nvidia GPU性能
      • 核心是systolic array
        • 计算单元(PE)阵列
        • 特别适合做矩阵乘法
        • 设计和制造相对简单

33 单机多卡并行

  • 单机多卡并行
    • 一台机器可以安装多个GPU
    • 在训练和预测时,将小批量计算切分到多个GPU上来达到加速目的
    • 常用切分方案
      • 数据并行:将小批量分成n块,每个GPU拿到完整参数计算一块数据的梯度(通常性能更好)
      • 模型并行:将模型分成n块,每个GPU拿到一块模型计算它的前向和方向结果(通常用于模型大到单GPU放不下)
      • 通道并行(数据+模型并行)
  • 数据并行
    1. 读一个数据块
    2. 拿回参数
    3. 计算梯度
    4. 发出梯度
    5. 更新梯度

34 多GPU训练实习

34.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 多GPu训练
%matplotlib inline
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 使用LeNet网络进行实现
# 初始化模型参数
scale = 0.01
W1 = torch.randn(size=(20, 1, 3, 3)) * scale
b1 = torch.zeros(20)
W2 = torch.randn(size=(50, 20, 5, 5)) * scale
b2 = torch.zeros(50)
W3 = torch.randn(size=(800, 128)) * scale
b3 = torch.zeros(128)
W4 = torch.randn(size=(128, 10)) * scale
b4 = torch.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]

# 定义模型
def lenet(X, params):
h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1])
h1_activation = F.relu(h1_conv)
h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2))
h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3])
h2_activation = F.relu(h2_conv)
h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2))
h2 = h2.reshape(h2.shape[0], -1)
h3_linear = torch.mm(h2, params[4]) + params[5]
h3 = F.relu(h3_linear)
y_hat = torch.mm(h3, params[6]) + params[7]
return y_hat

# 交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction='none')

# 向多个设备分发参数
def get_params(params, device):
new_params = [p.to(device) for p in params]
for p in new_params:
p.requires_grad_()
return new_params

new_params = get_params(params, d2l.try_gpu(0))
print('b1 权重:', new_params[1])
print('b1 梯度:', new_params[1].grad)

# allreduce函数将所有向量相加,并将结果广播给所有GPU
def allreduce(data):
for i in range(1, len(data)):
data[0][:] += data[i].to(data[0].device)
for i in range(1, len(data)):
data[i][:] = data[0].to(data[i].device)

data = [torch.ones((1, 2), device=d2l.try_gpu(i)) * (i + 1) for i in range(2)]
print('allreduce之前:\n', data[0], '\n', data[1])
allreduce(data)
print('allreduce之后:\n', data[0], '\n', data[1])

# 将小批量数据均匀地分布在多个GPU上
data = torch.arange(20).reshape(4, 5)
devices = [torch.device('cuda:0'), torch.device('cuda:1')]
split = nn.parallel.scatter(data, devices)
print('input :', data)
print('load into', devices)
print('output:', split)

#@save
def split_batch(X, y, devices):
"""将X和y拆分到多个设备上"""
assert X.shape[0] == y.shape[0]
return (nn.parallel.scatter(X, devices),
nn.parallel.scatter(y, devices))

# 在一个小批量上实现多GPU训练
def train_batch(X, y, device_params, devices, lr):
X_shards, y_shards = split_batch(X, y, devices)
# 在每个GPU上分别计算损失
ls = [loss(lenet(X_shard, device_W), y_shard).sum()
for X_shard, y_shard, device_W in zip(
X_shards, y_shards, device_params)]
for l in ls: # 反向传播在每个GPU上分别执行
l.backward()
# 将每个GPU的所有梯度相加,并将其广播到所有GPU
with torch.no_grad():
for i in range(len(device_params[0])):
allreduce(
[device_params[c][i].grad for c in range(len(devices))])
# 在每个GPU上分别更新模型参数
for param in device_params:
d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量

# 训练函数
def train(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)]
# 将模型参数复制到num_gpus个GPU
device_params = [get_params(params, d) for d in devices]
num_epochs = 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
timer = d2l.Timer()
for epoch in range(num_epochs):
timer.start()
for X, y in train_iter:
# 为单个小批量执行多GPU训练
train_batch(X, y, device_params, devices, lr)
torch.cuda.synchronize()
timer.stop()
# 在GPU0上评估模型
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(
lambda x: lenet(x, device_params[0]), test_iter, devices[0]),))
print(f'测试精度:{animator.Y[0][-1]:.2f}{timer.avg():.1f}秒/轮,'
f'在{str(devices)}')

# 在单个GPU上运行
train(num_gpus=1, batch_size=256, lr=0.2)

# 增加为2个GPU
train(num_gpus=2, batch_size=256, lr=0.2)
  • 多GPU训练不变快的原因,GPU增加但batch_size不增加,每一次计算不能高效的使用GPU的线程,最好保证每个GPU能拿到与相同相同的batch_size,但收敛速度很有可能会变慢,精度会降低,需要提高学习率

34.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
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
68
69
70
71
72
73
74
75
import torch
from torch import nn
from d2l import torch as d2l

# 使用ResNet18网络进行
#@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)
# 多GPU训练必备函数,并行获取数据,并且将数据分发给多个GPU,然后并行算梯度,然后更新
# 相当于重新包装forward函数
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)}')

# 单GPU训练
train(net, num_gpus=1, batch_size=256, lr=0.1)

# 增加到2个GPU
train(net, num_gpus=2, batch_size=512, lr=0.2)
  • QA
    • learning rate可能会导致nan问题,导致准确率震荡

35 分布式训练

  • 分布式计算
    • 数据放在分布式文件系统上->通过网络读取数据->多个worker<->多个参数服务器
    • 其中的带宽主要受网络带宽的限制
  • 计算一个小批量
    • 每个计算服务器读取小批量中的一部分
    • 进一步将数据切分到每个GPU上
    • 每个worker从参数服务器获取模型参数
    • 复制参数到每个GPU上
    • 每个GPU计算梯度
    • 将所有GPU上的梯度求和
    • 梯度传回服务器
    • 每个服务器对梯度求和并更新参数

同步SGD

性能

权衡

实践时的建议

  • 模型需要有好的计算(FLOP)通信(model size)比:Inception>ResNet>AlexNet
    • Inception 比 ResNet更好做并行计算

36 数据增广

  • 数据增强:增加一个已有数据集,使得有更多的多样性
    • 在语言里面加入各种不同的背景噪音
    • 改变图片的颜色和形状
    • 在线生成增强后的数据,随机生成,随后使用增强数据进行训练,可认为一种正则项
    • 常用方法:
      • 左右翻转
      • 上下翻转(不一定都可以)
      • 切割(从图像中切割一块,然后变形到固定形状,使用随机高宽比,随机大小,随机位置)
      • 颜色(改变色调,饱和度,明亮度)
      • github上图片增强的各种方法
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 图片增广
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

d2l.set_figsize()
img = d2l.Image.open('../img/cat1.jpg')
d2l.plt.imshow(img);

def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
d2l.show_images(Y, num_rows, num_cols, scale=scale)

# 左右翻转图像
apply(img, torchvision.transforms.RandomHorizontalFlip())

# 上下翻转图像
apply(img, torchvision.transforms.RandomVerticalFlip())

# 随机裁剪
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)

# 改变颜色
apply(img, torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0, saturation=0, hue=0))

# 改变色调
apply(img, torchvision.transforms.ColorJitter(
brightness=0, contrast=0, saturation=0, hue=0.5))

# 随机更改图像的亮度、对比度、饱和度和色调
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)

# 结合多种图像增广方法
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)

# 使用图像增广进行训练
all_images = torchvision.datasets.CIFAR10(train=True, root="../data",
download=True)
d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8);

# 只是用最简单的随机左右翻转
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])

test_augs = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])

# 定义一个辅助函数,以便于读取图像和应用图像增广
def load_cifar10(is_train, augs, batch_size):
dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train,
transform=augs, download=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=is_train, num_workers=d2l.get_dataloader_workers())
return dataloader


# 定义一个函数,使用多GPU对模型进行评估和训练
#@save
def train_batch_ch13(net, X, y, loss, trainer, devices):
"""用多GPU进行小批量训练"""
if isinstance(X, list):
# 微调BERT中所需(稍后讨论)
X = [x.to(devices[0]) for x in X]
else:
X = X.to(devices[0])
y = y.to(devices[0])
net.train()
trainer.zero_grad()
pred = net(X)
l = loss(pred, y)
l.sum().backward()
trainer.step()
train_loss_sum = l.sum()
train_acc_sum = d2l.accuracy(pred, y)
return train_loss_sum, train_acc_sum

#@save
def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices=d2l.try_all_gpus()):
"""用多GPU进行模型训练"""
timer, num_batches = d2l.Timer(), len(train_iter)
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['train loss', 'train acc', 'test acc'])
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
# 4个维度:储存训练损失,训练准确度,实例数,特点数
metric = d2l.Accumulator(4)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = train_batch_ch13(
net, features, labels, loss, trainer, devices)
metric.add(l, acc, labels.shape[0], labels.numel())
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[3],
None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {metric[0] / metric[2]:.3f}, train acc '
f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
f'{str(devices)}')

# 定义train_with_data_aug函数,使用图像增广来训练模型
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)

def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.xavier_uniform_(m.weight)

net.apply(init_weights)

def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = nn.CrossEntropyLoss(reduction="none")
trainer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)

# 训练模型
train_with_data_aug(train_augs, test_augs, net)

37 微调(迁移学习之一)

  • 微调是CV中深度学习最重要的技术,可以使用迁移学习
  • 网络架构
    • 一个神经网络一般可以分成两块
      • 特征抽取将原始像素变成容易线性分割的特征
      • 线性分类器来做分类

网络架构

  • 微调
    • 网络架构中,使用源数据集训练好的模型,特征抽取部分可能仍然对目标数据集做特征抽取,线性分类器部分由于标号可能变了故该部分不能使用
  • 微调中的权重初始化:在目标数据集模型初始化中,特征抽取部分的权重的初始化使用源数据集模型的权重,而线性分类器使用随机初始化

微调中的权重初始化

  • 训练:是一个目标数据集上的正常训练任务,但使用更强的正则化
    • 使用更小的学习率
    • 使用更小的数据迭代
    • 源数据集远复杂于目标数据集,通常微调效果更好
  • 重用分类器权重
    • 源数据集可能也有目标数据中的部分标号
    • 可以使用预训练好模型分类器中对应标号对应的向量来做初始化
  • 固定一些层
    • 神经网络通常学习有层次地特征表示
      • 低层次的特征更加通用
      • 高层次的特征则更跟数据集相关
    • 可以固定底部一些层的参数,不参与更新:更强的正则
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 微调
%matplotlib inline
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

# 获取数据集,热狗数据集
#@save
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip',
'fba480ffa8aa7e0febbb511d181409f899b9baa5')

data_dir = d2l.download_extract('hotdog')

train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))

# 图像的大小和纵横比各有不同
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);

# 数据增广(匹配ImgNet上的模型)
# 使用RGB通道的均值和标准差,以标准化每个通道
normalize = torchvision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomResizedCrop(224),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
normalize])

test_augs = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
normalize])

# 定义和初始化模型
pretrained_net = torchvision.models.resnet18(pretrained=True) # 将模型下载,并且pretrained=True也下载模型的数据

pretrained_net.fc

finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2) # 将最后一层的输出通道改为2
nn.init.xavier_uniform_(finetune_net.fc.weight); # 最后一层随机初始化

# 微调模型
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
param_group=True):
train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train'), transform=train_augs),
batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'test'), transform=test_augs),
batch_size=batch_size)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss(reduction="none")
if param_group:
params_1x = [param for name, param in net.named_parameters()
if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x},
{'params': net.fc.parameters(),
'lr': learning_rate * 10}],
lr=learning_rate, weight_decay=0.001)
else:
trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
weight_decay=0.001)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)

# 训练模型
train_fine_tuning(finetune_net, 5e-5)

# 为了进行比较,所有模型参数初始化为随机值
scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
train_fine_tuning(scratch_net, 5e-4, param_group=False)
  • 最好都是从微调(fine tuning)开始进行训练,而不是从零开始训练

39 实战 Kaggle 比赛:图像分类(CIFAR-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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

# 提供包含前1000个训练图像和5个随机测试图像的数据集的小规模样本
#@save
d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip',
'2068874e4b9a9f0fb07ebe0ad2b29754449ccacd')

# 如果你使用完整的Kaggle竞赛的数据集,设置demo为False
demo = True

if demo:
data_dir = d2l.download_extract('cifar10_tiny')
else:
data_dir = '../data/cifar-10/'

# 整理数据集
#@save
def read_csv_labels(fname):
"""读取fname来给标签字典返回一个文件名"""
with open(fname, 'r') as f:
# 跳过文件头行(列名)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
return dict(((name, label) for name, label in tokens))

labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# 训练样本 :', len(labels))
print('# 类别 :', len(set(labels.values())))

# 将验证集从原始的训练集中拆分出来
def copyfile(filename, target_dir):
"""将文件复制到目标目录"""
os.makedirs(target_dir, exist_ok=True)
shutil.copy(filename, target_dir)

def reorg_train_valid(data_dir, labels, valid_ratio):
"""将验证集从原始的训练集中拆分出来"""
# 训练数据集中样本最少的类别中的样本数
n = collections.Counter(labels.values()).most_common()[-1][1]
# 验证集中每个类别的样本数
n_valid_per_label = max(1, math.floor(n * valid_ratio))
label_count = {}
for train_file in os.listdir(os.path.join(data_dir, 'train')):
label = labels[train_file.split('.')[0]]
fname = os.path.join(data_dir, 'train', train_file)
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train_valid', label))
if label not in label_count or label_count[label] < n_valid_per_label:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train', label))
return n_valid_per_label

# 在预测掐尖整理测试集,以方便读取
def reorg_test(data_dir):
"""在预测期间整理测试集,以方便读取"""
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(data_dir, 'train_valid_test', 'test',
'unknown'))

# 调用前面定义的函数
def reorg_cifar10_data(data_dir, valid_ratio):
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
reorg_train_valid(data_dir, labels, valid_ratio)
reorg_test(data_dir)

batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)

# 图像增广
transform_train = torchvision.transforms.Compose([
# 在高度和宽度上将图像放大到40像素的正方形
torchvision.transforms.Resize(40),
# 随机裁剪出一个高度和宽度均为40像素的正方形图像,
# 生成一个面积为原始图像面积0.64到1倍的小正方形,
# 然后将其缩放为高度和宽度均为32像素的正方形
torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
ratio=(1.0, 1.0)),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])

transform_test = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])

# 读取由原始图像组成的数据集
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]

valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]

# 指定上面定义的所有图像增广操作
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]

valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)

test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)

# 模型
def get_net():
num_classes = 10
net = d2l.resnet18(num_classes, 3)
return net

loss = nn.CrossEntropyLoss(reduction="none")

# 训练函数
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9,
weight_decay=wd)
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)
num_batches, timer = len(train_iter), d2l.Timer()
legend = ['train loss', 'train acc']
if valid_iter is not None:
legend.append('valid acc')
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
for epoch in range(num_epochs):
net.train()
metric = d2l.Accumulator(3)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = d2l.train_batch_ch13(net, features, labels,
loss, trainer, devices)
metric.add(l, acc, labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[2], metric[1] / metric[2],
None))
if valid_iter is not None:
valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter)
animator.add(epoch + 1, (None, None, valid_acc))
scheduler.step()
measures = (f'train loss {metric[0] / metric[2]:.3f}, '
f'train acc {metric[1] / metric[2]:.3f}')
if valid_iter is not None:
measures += f', valid acc {valid_acc:.3f}'
print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')

# 训练和验证模型
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 2e-4, 5e-4
lr_period, lr_decay, net = 4, 0.9, get_net()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)

# 在Kaggle上对测试集进行分类并提交结果
net, preds = get_net(), []
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
lr_decay)

for X, _ in test_iter:
y_hat = net(X.to(devices[0]))
preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort(key=lambda x: str(x))
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x])
df.to_csv('submission.csv', index=False)

40 实战 Kaggle 比赛:狗的品种识别

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

# 下载数据集
d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip',
'0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d')

# 如果你使用Kaggle比赛的完整数据集,请将下面的变量更改为False
demo = True
if demo:
data_dir = d2l.download_extract('dog_tiny')
else:
data_dir = os.path.join('..', 'data', 'dog-breed-identification')

# 整理数据集
def reorg_dog_data(data_dir, valid_ratio):
labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv'))
d2l.reorg_train_valid(data_dir, labels, valid_ratio)
d2l.reorg_test(data_dir)


batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_dog_data(data_dir, valid_ratio)

# 图像增广
transform_train = torchvision.transforms.Compose([
# 随机裁剪图像,所得图像为原始面积的0.08到1之间,高宽比在3/4和4/3之间。
# 然后,缩放图像以创建224x224的新图像
torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
ratio=(3.0/4.0, 4.0/3.0)),
torchvision.transforms.RandomHorizontalFlip(),
# 随机更改亮度,对比度和饱和度
torchvision.transforms.ColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4),
# 添加随机噪声
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])

transform_test = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
# 从图像中心裁切224x224大小的图片
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])

# 读取数据集
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]

valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]

train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]

valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)

test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)

# 微调预训练模型
def get_net(devices):
finetune_net = nn.Sequential()
finetune_net.features = torchvision.models.resnet34(pretrained=True)
# 定义一个新的输出网络,共有120个输出类别
finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256),
nn.ReLU(),
nn.Linear(256, 120))
# 将模型参数分配给用于计算的CPU或GPU
finetune_net = finetune_net.to(devices[0])
# 冻结(卷积层)参数
for param in finetune_net.features.parameters():
param.requires_grad = False
return finetune_net

# 计算损失
loss = nn.CrossEntropyLoss(reduction='none')

def evaluate_loss(data_iter, net, devices):
l_sum, n = 0.0, 0
for features, labels in data_iter:
features, labels = features.to(devices[0]), labels.to(devices[0])
outputs = net(features)
l = loss(outputs, labels)
l_sum += l.sum()
n += labels.numel()
return (l_sum / n).to('cpu')

# 训练函数
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
# 只训练小型自定义输出网络
net = nn.DataParallel(net, device_ids=devices).to(devices[0])
trainer = torch.optim.SGD((param for param in net.parameters()
if param.requires_grad), lr=lr,
momentum=0.9, weight_decay=wd)
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay)
num_batches, timer = len(train_iter), d2l.Timer()
legend = ['train loss']
if valid_iter is not None:
legend.append('valid loss')
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=legend)
for epoch in range(num_epochs):
metric = d2l.Accumulator(2)
for i, (features, labels) in enumerate(train_iter):
timer.start()
features, labels = features.to(devices[0]), labels.to(devices[0])
trainer.zero_grad()
output = net(features)
l = loss(output, labels).sum()
l.backward()
trainer.step()
metric.add(l, labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(metric[0] / metric[1], None))
measures = f'train loss {metric[0] / metric[1]:.3f}'
if valid_iter is not None:
valid_loss = evaluate_loss(valid_iter, net, devices)
animator.add(epoch + 1, (None, valid_loss.detach().cpu()))
scheduler.step()
if valid_iter is not None:
measures += f', valid loss {valid_loss:.3f}'
print(measures + f'\n{metric[1] * num_epochs / timer.sum():.1f}'
f' examples/sec on {str(devices)}')

# 训练和验证模型
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 10, 1e-4, 1e-4
lr_period, lr_decay, net = 2, 0.9, get_net(devices)
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay)

# 对测试集分类并在Kaggle提交结果
net = get_net(devices)
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, lr_decay)

preds = []
for data, label in test_iter:
output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0)
preds.extend(output.cpu().detach().numpy())
ids = sorted(os.listdir(
os.path.join(data_dir, 'train_valid_test', 'test', 'unknown')))
with open('submission.csv', 'w') as f:
f.write('id,' + ','.join(train_valid_ds.classes) + '\n')
for i, output in zip(ids, preds):
f.write(i.split('.')[0] + ',' + ','.join(
[str(num) for num in output]) + '\n')

41 物体检测和数据集

  • 目标检测中:边缘框
    • 一个边缘框可以通过4个数字定义:
      • (左上x, 左上y, 右下x, 右下y)
      • (左上x, 左上y, 宽, 高)
    • 图像处理从左上到右下遍历图片
  • 目标检测数据集
    • 每行表示一个物体:图片文件名,物体类别,边缘框
    • COCO
      • 80个物体,330K图片,1.5M物体
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
# 目标检测和边界框
%matplotlib inline
import torch
from d2l import torch as d2l

d2l.set_figsize()
img = d2l.plt.imread('../img/catdog.jpg')
d2l.plt.imshow(img)


# 定义在这两种表示之间进行转换的函数
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes

def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes

# 定义图像中狗和猫的边界框
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]

boxes = torch.tensor((dog_bbox, cat_bbox))
box_center_to_corner(box_corner_to_center(boxes)) == boxes

# 将边界框在图中画出
def bbox_to_rect(bbox, color):
# 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
# ((左上x,左上y),宽,高)
return d2l.plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)

fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'))
  • 数据集
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
# 目标检测数据集
# 一个小型数据集 下载数据集
%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
from d2l import torch as d2l

d2l.DATA_HUB['banana-detection'] = (
d2l.DATA_URL + 'banana-detection.zip',
'5de26c8fce5ccdea9f91267273464dc968d20d72')

# 读取香蕉检测数据集
def read_data_bananas(is_train=True):
"""读取香蕉检测数据集中的图像和标签"""
data_dir = d2l.download_extract('banana-detection')
csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
else 'bananas_val', 'label.csv')
csv_data = pd.read_csv(csv_fname)
csv_data = csv_data.set_index('img_name')
images, targets = [], []
for img_name, target in csv_data.iterrows():
images.append(torchvision.io.read_image(
os.path.join(data_dir, 'bananas_train' if is_train else
'bananas_val', 'images', f'{img_name}')))
# 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
# 其中所有图像都具有相同的香蕉类(索引为0)
targets.append(list(target))
return images, torch.tensor(targets).unsqueeze(1) / 256

# 创建一个自定义Dataset实例
class BananasDataset(torch.utils.data.Dataset):
"""一个用于加载香蕉检测数据集的自定义数据集"""
def __init__(self, is_train):
self.features, self.labels = read_data_bananas(is_train)
print('read ' + str(len(self.features)) + (f' training examples' if
is_train else f' validation examples'))

def __getitem__(self, idx):
return (self.features[idx].float(), self.labels[idx])

def __len__(self):
return len(self.features)

# 为了训练集和测试集返回两个数据加载器实例
def load_data_bananas(batch_size):
"""加载香蕉检测数据集"""
train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True),
batch_size, shuffle=True)
val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),
batch_size)
return train_iter, val_iter

# 读取一个小批量,并打印其中的图像和标签的形状
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
batch[0].shape, batch[1].shape

# 实例
imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
axes = d2l.show_images(imgs, 2, 5, scale=2)
for ax, label in zip(axes, batch[1][0:10]):
d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])

42 锚框

跳转至教材

锚框

  • 锚框
    • 一类目标检测算法是基于锚框(目前仍为主流算法)
      • 提出多个被称为锚框的区域(边缘框)
      • 预测每个锚框里是否含有有关注的物体
      • 如果是,预测从这个锚框到真实边缘框的偏移
  • IoU交并比:用来计算两个框之间的相似度
    • 0表示无重叠,1表示重合
    • 是Jacquard指数的一个特殊情况
      • 给定两个集合A和B:J(A,B)=ABABJ(A, B)=\frac{|A \cap B|}{|A \cup B|}

交并比

  • 赋予锚框标号(一种常用算法)
    • 每个锚框是一个训练样本
    • 将每个锚框,要么标注成背景,要么关联上一个真实边缘框
    • 可能会产生大量的锚框,这样会导致大量的负类样本
    • 流程:
      • 首先选取锚框值IoU中间的最大值,随后将其对应的边缘框给锚框,并将锚框所在的行和列的元素删除,重复上述操作,选出所有边缘框对应的锚框

赋予锚框标号

  • 使用非极大抑制(NMS)输出
    • 每个锚框预测一个边缘框
    • NMS可以合并相似的预测
      • 选中非背景类的最大预测值
      • 去掉所有其它和它的IoU值大于 θ\theta 的预测
      • 重复上述过程知道所有预测要么被选中,要么被去掉

NMS

  • 总结
    • 一类目标检测算法基于锚框来预测
      • 首先生成大量锚框,并赋予标号,每个锚框作为一个样本进行训练
    • 在预测时,使用NMS来去掉冗余的预测
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
%matplotlib inline
import torch
from d2l import torch as d2l

torch.set_printoptions(2) # 精简输出精度

# 锚框的宽度和高度分别为 ws*sqrt(r) 和 hs*sqrt(r)
# s 为锚框占图片的比例, r 为高宽的高低比
# 组合为(s1,r1),(s1,r2),...,(s1,rm),(s2,r1),(s3,r1),...,(sn,r1)
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)

# 返回锚框变量Y的形状
img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]

print(h, w)
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape

# 访问以(250,250)为中心的第一个锚框
boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]

# 显示以图像中的一个像素为中心的所有锚框
def show_bboxes(axes, bboxes, labels=None, colors=None):
"""显示所有边界框"""
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj

labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = d2l.bbox_to_rect(bbox.detach().numpy(), color)
axes.add_patch(rect)
if labels and len(labels) > i:
text_color = 'k' if color == 'w' else 'w'
axes.text(rect.xy[0], rect.xy[1], labels[i],
va='center', ha='center', fontsize=9, color=text_color,
bbox=dict(facecolor=color, lw=0))

# 以(250,250)为中心的所有锚框
d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
's=0.75, r=0.5'])

# 交并比(IoU)
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
(boxes[:, 3] - boxes[:, 1]))
# boxes1,boxes2,areas1,areas2的形状:
# boxes1:(boxes1的数量,4),
# boxes2:(boxes2的数量,4),
# areas1:(boxes1的数量,),
# areas2:(boxes2的数量,)
areas1 = box_area(boxes1)
areas2 = box_area(boxes2)
# inter_upperlefts,inter_lowerrights,inters的形状:
# (boxes1的数量,boxes2的数量,2)
inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
# inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量)
inter_areas = inters[:, :, 0] * inters[:, :, 1]
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas

# 将真实边界框分配给锚框
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
"""将最接近的真实边界框分配给锚框"""
num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
# 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU
jaccard = box_iou(anchors, ground_truth)
# 对于每个锚框,分配的真实边界框的张量
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
device=device)
# 根据阈值,决定是否分配真实边界框
max_ious, indices = torch.max(jaccard, dim=1)
anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
box_j = indices[max_ious >= iou_threshold]
anchors_bbox_map[anc_i] = box_j
col_discard = torch.full((num_anchors,), -1)
row_discard = torch.full((num_gt_boxes,), -1)
for _ in range(num_gt_boxes):
max_idx = torch.argmax(jaccard)
box_idx = (max_idx % num_gt_boxes).long()
anc_idx = (max_idx / num_gt_boxes).long()
anchors_bbox_map[anc_idx] = box_idx
jaccard[:, box_idx] = col_discard
jaccard[anc_idx, :] = row_discard
return anchors_bbox_map

# 标记类和偏移
def offset_boxes(anchors, assigned_bb, eps=1e-6):
"""对锚框偏移量的转换"""
"""书中有详细介绍,最好使offest好预测,将值分散开而不是聚在一起,较好预测"""
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
offset = torch.cat([offset_xy, offset_wh], axis=1)
return offset

def multibox_target(anchors, labels):
"""使用真实边界框标记锚框"""
batch_size, anchors = labels.shape[0], anchors.squeeze(0)
batch_offset, batch_mask, batch_class_labels = [], [], []
device, num_anchors = anchors.device, anchors.shape[0]
for i in range(batch_size):
label = labels[i, :, :]
anchors_bbox_map = assign_anchor_to_bbox(
label[:, 1:], anchors, device)
bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
1, 4)
# 将类标签和分配的边界框坐标初始化为零
class_labels = torch.zeros(num_anchors, dtype=torch.long,
device=device)
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
device=device)
# 使用真实边界框来标记锚框的类别。
# 如果一个锚框没有被分配,我们标记其为背景(值为零)
indices_true = torch.nonzero(anchors_bbox_map >= 0)
bb_idx = anchors_bbox_map[indices_true]
class_labels[indices_true] = label[bb_idx, 0].long() + 1
assigned_bb[indices_true] = label[bb_idx, 1:]
# 偏移量转换
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
batch_offset.append(offset.reshape(-1))
batch_mask.append(bbox_mask.reshape(-1))
batch_class_labels.append(class_labels)
bbox_offset = torch.stack(batch_offset)
bbox_mask = torch.stack(batch_mask)
class_labels = torch.stack(batch_class_labels)
return (bbox_offset, bbox_mask, class_labels)

# 在图像中绘制这些地面真相边界框和锚框
ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
[1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
[0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
[0.57, 0.3, 0.92, 0.9]])

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);

# 根据狗和猫的真实边界框,标注这些锚框的分类和偏移量
labels = multibox_target(anchors.unsqueeze(dim=0),
ground_truth.unsqueeze(dim=0))

labels[2]

labels[1]

labels[0]

# 应用逆偏移变换来返回预测的边界框坐标
def offset_inverse(anchors, offset_preds):
"""根据带有预测偏移量的锚框来预测边界框"""
anc = d2l.box_corner_to_center(anchors)
pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
predicted_bbox = d2l.box_center_to_corner(pred_bbox)
return predicted_bbox

# 以下num函数按降序对置信度进行排序并返回其索引
def nms(boxes, scores, iou_threshold):
"""对预测边界框的置信度进行排序"""
B = torch.argsort(scores, dim=-1, descending=True)
keep = [] # 保留预测边界框的指标
while B.numel() > 0:
i = B[0]
keep.append(i)
if B.numel() == 1: break
iou = box_iou(boxes[i, :].reshape(-1, 4),
boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
inds = torch.nonzero(iou <= iou_threshold).reshape(-1)
B = B[inds + 1]
return torch.tensor(keep, device=boxes.device)

# 将极大值抑制应用于预测边界框
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
pos_threshold=0.009999999):
"""使用非极大值抑制来预测边界框"""
device, batch_size = cls_probs.device, cls_probs.shape[0]
anchors = anchors.squeeze(0)
num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
out = []
for i in range(batch_size):
cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
conf, class_id = torch.max(cls_prob[1:], 0)
predicted_bb = offset_inverse(anchors, offset_pred)
keep = nms(predicted_bb, conf, nms_threshold)

# 找到所有的non_keep索引,并将类设置为背景
all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
combined = torch.cat((keep, all_idx))
uniques, counts = combined.unique(return_counts=True)
non_keep = uniques[counts == 1]
all_id_sorted = torch.cat((keep, non_keep))
class_id[non_keep] = -1
class_id = class_id[all_id_sorted]
conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
# pos_threshold是一个用于非背景预测的阈值
below_min_idx = (conf < pos_threshold)
class_id[below_min_idx] = -1
conf[below_min_idx] = 1 - conf[below_min_idx]
pred_info = torch.cat((class_id.unsqueeze(1),
conf.unsqueeze(1),
predicted_bb), dim=1)
out.append(pred_info)
return torch.stack(out)

# 将上述算法应用到一个带四个锚框的具体示例中
anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
[0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = torch.tensor([0] * anchors.numel())
cls_probs = torch.tensor([[0] * 4, # 背景的预测概率
[0.9, 0.8, 0.7, 0.1], # 狗的预测概率
[0.1, 0.2, 0.3, 0.9]]) # 猫的预测概率

# 在图像上绘制这些预测边界框和置信度
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
['dog=0.9', 'dog=0.8', 'dog=0.7', 'cat=0.9'])

output = multibox_detection(cls_probs.unsqueeze(dim=0),
offset_preds.unsqueeze(dim=0),
anchors.unsqueeze(dim=0),
nms_threshold=0.5)
output

fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
if i[0] == -1:
continue
label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

43 树叶分类竞赛技术总结

  • 相比于课程介绍的代码,主要做了下面的加强
    • 数据增强,在测试时多次使用稍弱的增强然后取平均
    • 使用多个模型预测,最后结果加权平均
    • 训练算法和学习率
    • 清理数据
  • 数据方面
    • 有重复图片,可以手动去除
    • 图片背景较多,而且树叶没有方向性,可以做更多增强
    • 跨图片增强
      • Mixup:随机叠加两张图片
      • CutMix:随机组合来自不同图片的块
  • 模型方面
    • 模型多为ResNet变种
      • DenseNet,ResNeXt,ResNest,…
      • EfficientNet
    • 优化算法多为Adam或其变种
    • 学习率一般使Cosine或者训练不同时往下调(存在相关代码)
  • AutoGluon
    • 15行代码,安装加训练化花时100分钟.跳转
    • 精度96%
      • 可以通过定制化提升精度
      • 下一版本将搜索更多的模型超参数
      • AG目前仍是关注工业界上,非比赛
  • 总结
    • 提升精度思路:根据数据挑选增强,使用新模型,新优化算法,多个模型融合,测试时使用增强
    • 在工业界应用中:
      • 少使用模型融合和测试时增强,计算代价过高
      • 通常固定模型超参数,而将精力主要花在提升数据质量

44 物体检测算法:R-CNN,SSD,YOLO

  • R-CNN
    • 使用启发式搜索算法来选择锚框
    • 使用与训练模型来对每个锚框抽取特征
    • 训练一个SVM来对类别分类
    • 训练一个线性回归模型来预测边缘框偏移

R-CNN

  • 兴趣区域(RoI)池化层
    • 给定一个锚框,均匀分割成nmn*m块,输出每块里的最大值
    • 不管锚框多大,总是输出 nmnm 个值

兴趣区域池化层

  • Fast RCNN
    • 上述模型每次一张图片需要抽取特征,一张图片需要抽取特征变成大量的小图片,而Fast RCNN在一整张图片上抽取特征给定锚框,在锚框内使用RoI池化,最后输出一个矩阵
    • 不对每个锚框进行CNN抽取特征,而是对整张图片抽取特征
    • 使用CNN对图片抽取特征
    • 使用RoI池化层对每个锚框生成固定长度特征

Fast CNN

  • Faster R-CNN:使用一个区域提议网络(RPN)来替代启发式搜索来获得更好的锚框
    • 使用一个神经网络来代替上述的选择性搜索算法,相当于进行一次粗糙的目标检测

Faster RCNN

  • Mask R-CNN
    • 如果有像素级别的标号,使用FCN来利用这些信息
    • RoI池化层改为RoI align 在像素级别预测时,切分为等分,像素值为加权平均数

Mask R-CNN

  • 总结

    • Fast/Faster RCNN持续提升性能
    • Fast/Faster RCNN和Mask RCNN是在追求高精度场景下的常用算法
  • 单发多框检测(SSD)

    • 生成锚框
    • 对每个像素,生成多个以它为中心的锚框
    • 给定n个大小x1,s2,...snx_1,s_2,...s_nmm个高宽比,那么生成n+m1n+m-1个锚框,其大小和高宽比分别为(s1,r1),(s2,r1),...,(sn,r1),(s1,r2),...,(s1,rm)(s_1,r_1),(s_2,r_1),...,(s_n,r_1),(s_1,r_2),...,(s_1,r_m)

SSD

  • SSD模型
    • 一个基础网络来抽取特征,然后多个卷积层块来减半高宽
    • 在每段都生成锚框:底部段来拟合小物体,顶部段来拟合大物体
    • 对每个锚框预测类别和边缘框
  • 总结
    • SSD通过单神经网络来检测模型
    • 以每个像素为中心的产生多个锚框
    • 在多个段的输出上进行多尺度的检测

SSD模型

  • YOLO模型
    • SSD中锚框大量重叠,因此浪费了很多计算
    • YOLO将图片均匀分成sss*s个锚框
    • 每个锚框预测B个边缘框

45 SSD实现

跳转至Bilibili视频

45.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
# 多尺度目标检测
%matplotlib inline
import torch
from d2l import torch as d2l

img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]
h, w

# 在特征图(fmap)上生成锚框(anchors),每个单位(像素)作为锚框中心
def display_anchors(fmap_w, fmap_h, s):
d2l.set_figsize()
# 前两个维度上的值不影响输出
fmap = torch.zeros((1, 10, fmap_h, fmap_w))
anchors = d2l.multibox_prior(fmap, sizes=s, ratios=[1, 2, 0.5])
bbox_scale = torch.tensor((w, h, w, h))
d2l.show_bboxes(d2l.plt.imshow(img).axes,
anchors[0] * bbox_scale)

# 探测小目标
display_anchors(fmap_w=4, fmap_h=4, s=[0.15])

# 将特征图的高宽减半,然后使用较大的锚框来检测较大的目标
display_anchors(fmap_w=2, fmap_h=2, s=[0.4])

# 将特征图的高宽减半,然后使用较大的锚框来检测较大的目标
display_anchors(fmap_w=1, fmap_h=1, s=[0.8])

45.2 SSD

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 类别预测层(每个锚框都需要预测(num_classes + 1)类)
def cls_predictor(num_inputs, num_anchors, num_classes):
return nn.Conv2d(num_inputs, num_anchors * (num_classes + 1),
kernel_size=3, padding=1) # 该组合不会改变图像的尺寸
# 预测的类别数为H*w*num_anchors*(num_class+1)
# 需要对每一个像素生成的锚框进行检测,3*3的视野

# 边界框预测层
def bbox_predictor(num_inputs, num_anchors):
return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)
# 输出通道数:每个像素的锚框大小*4(锚框有4个值)

# 连接多尺度的预测
def forward(x, block):
return block(x)

Y1 = forward(torch.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10))
Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10))
Y1.shape, Y2.shape

# 将4-D转换为1-D
# 将通道数丢到最后,将后面的3个维度拉成一个向量
def flatten_pred(pred):
return torch.flatten(pred.permute(0, 2, 3, 1), start_dim=1)

# 在宽上contact
def concat_preds(preds):
return torch.cat([flatten_pred(p) for p in preds], dim=1)

concat_preds([Y1, Y2]).shape

# 高和宽减半块
def down_sample_blk(in_channels, out_channels):
blk = []
for _ in range(2):
blk.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
blk.append(nn.BatchNorm2d(out_channels))
blk.append(nn.ReLU())
in_channels = out_channels
blk.append(nn.MaxPool2d(2))
return nn.Sequential(*blk)

forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape

# 基本网络层
def base_net():
blk = []
num_filters = [3, 16, 32, 64]
for i in range(len(num_filters) - 1):
blk.append(down_sample_blk(num_filters[i], num_filters[i+1]))
return nn.Sequential(*blk)

forward(torch.zeros((2, 3, 256, 256)), base_net()).shape

# 完整的模型
# 在5个尺度上进行目标检测
def get_blk(i):
if i == 0:
blk = base_net()
elif i == 1:
blk = down_sample_blk(64, 128)
elif i == 4:
blk = nn.AdaptiveMaxPool2d((1,1)) #global max pooling
else:
blk = down_sample_blk(128, 128)
return blk

# 为每个块定义前向传播
# 与图像分类任务不同,此处的输出包括:CNN特征图Y;在当前尺度下根据Y生成的锚框;预测的这些锚框的类别和偏移量(基于Y)
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio)
cls_preds = cls_predictor(Y)
bbox_preds = bbox_predictor(Y)
return (Y, anchors, cls_preds, bbox_preds)

# 超参数
sizes = [[0.2, 0.272],
[0.37, 0.447],
[0.54, 0.619],
[0.71, 0.79],
[0.88, 0.961]]
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1

# 定义完整的模型TinySSD
class TinySSD(nn.Module):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
idx_to_in_channels = [64, 128, 128, 128, 128]
for i in range(5):
# 即赋值语句self.blk_i=get_blk(i)
setattr(self, f'blk_{i}', get_blk(i))
setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i],
num_anchors, num_classes))
setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i],
num_anchors))

def forward(self, X):
anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
for i in range(5):
# getattr(self,'blk_%d'%i)即访问self.blk_i
X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
anchors = torch.cat(anchors, dim=1)
cls_preds = concat_preds(cls_preds)
cls_preds = cls_preds.reshape(
cls_preds.shape[0], -1, self.num_classes + 1)
bbox_preds = concat_preds(bbox_preds)
return anchors, cls_preds, bbox_preds

# 创建一个模型实例,然后使用它执行前向计算
net = TinySSD(num_classes=1)
X = torch.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)

print('output anchors:', anchors.shape)
print('output class preds:', cls_preds.shape)
print('output bbox preds:', bbox_preds.shape)

# ---------
# 2 训练模型
# 读取数据集并初始化
batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)

# 初始化其参数并定义优化函数
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)

# 定义损失函数和评价函数
cls_loss = nn.CrossEntropyLoss(reduction='none')
bbox_loss = nn.L1Loss(reduction='none') # 不使用L2的原因,当预测偏差太大时,损失函数较小

def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
cls = cls_loss(cls_preds.reshape(-1, num_classes),
cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)
bbox = bbox_loss(bbox_preds * bbox_masks,
bbox_labels * bbox_masks).mean(dim=1)
# mask=0时不需要计算loss函数,只需要计算mask=1时的loss函数
return cls + bbox

def cls_eval(cls_preds, cls_labels):
# 由于类别预测结果放在最后一维,argmax需要指定最后一维。
return float((cls_preds.argmax(dim=-1).type(cls_labels.dtype) == cls_labels).sum())

def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())

# 训练模型
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
# 训练精确度的和,训练精确度的和中的示例数
# 绝对误差的和,绝对误差的和中的示例数
metric = d2l.Accumulator(4)
net.train()
for features, target in train_iter:
timer.start()
trainer.zero_grad()
X, Y = features.to(device), target.to(device)
# 生成多尺度的锚框,为每个锚框预测类别和偏移量
anchors, cls_preds, bbox_preds = net(X)
# 为每个锚框标注类别和偏移量
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)
# 根据类别和偏移量的预测和标注值计算损失函数
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.mean().backward()
trainer.step()
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.numel())
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')

# 预测目标
X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()
img = X.squeeze(0).permute(1, 2, 0).long()

def predict(X):
net.eval()
anchors, cls_preds, bbox_preds = net(X.to(device))
cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]

output = predict(X)

# 筛选所有置信度不低于0.9的边界框,作为最终输出
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img)
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[0:2]
bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')

display(img, output.cpu(), threshold=0.9)

46 语义分割和数据集

  • 语义分割将图片中的每个像素分类到对应的类别
    • 应用:背景虚化,路面分割
    • 实例分割:区别每一个实例,目标检测的进化版本
  • 最重要的语义分割数据集之一时Pascal VOC2012
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
%matplotlib inline
import os
import torch
import torchvision
from d2l import torch as d2l

#@save
d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar',
'4e443f8a2eca6b1dac8a6c57641b67dd40621a49')

voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')

# 将所有输入的图像和标签读入内存
#@save
def read_voc_images(voc_dir, is_train=True):
"""读取所有VOC图像并标注"""
txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation',
'train.txt' if is_train else 'val.txt')
mode = torchvision.io.image.ImageReadMode.RGB
with open(txt_fname, 'r') as f:
images = f.read().split()
features, labels = [], []
for i, fname in enumerate(images):
features.append(torchvision.io.read_image(os.path.join(
voc_dir, 'JPEGImages', f'{fname}.jpg')))
labels.append(torchvision.io.read_image(os.path.join(
voc_dir, 'SegmentationClass' ,f'{fname}.png'), mode))
return features, labels

train_features, train_labels = read_voc_images(voc_dir, True)

# 绘制前5个输入图像及其标签
n = 5
imgs = train_features[0:n] + train_labels[0:n]
imgs = [img.permute(1,2,0) for img in imgs]
d2l.show_images(imgs, 2, n);

# 列举RGB颜色值和类名
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
[0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
[64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
[64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
[0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
[0, 64, 128]]

VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
'diningtable', 'dog', 'horse', 'motorbike', 'person',
'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

# 查找标签中每个像素的类标签
def voc_colormap2label():
"""构建从RGB到VOC类别索引的映射"""
colormap2label = torch.zeros(256 ** 3, dtype=torch.long)
for i, colormap in enumerate(VOC_COLORMAP):
colormap2label[
(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i
return colormap2label

def voc_label_indices(colormap, colormap2label):
"""将VOC标签中的RGB值映射到它们的类别索引"""
colormap = colormap.permute(1, 2, 0).numpy().astype('int32')
idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256
+ colormap[:, :, 2])
return colormap2label[idx]

# 例如
y = voc_label_indices(train_labels[0], voc_colormap2label())
y[105:115, 130:140], VOC_CLASSES[1]

# 使用图片增广中的随机裁剪,裁剪输入图像和标签的相同区域
def voc_rand_crop(feature, label, height, width):
"""随机裁剪特征和标签图像"""
rect = torchvision.transforms.RandomCrop.get_params(
feature, (height, width))
feature = torchvision.transforms.functional.crop(feature, *rect)
label = torchvision.transforms.functional.crop(label, *rect)
return feature, label

imgs = []
for _ in range(n):
imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)

imgs = [img.permute(1, 2, 0) for img in imgs]
d2l.show_images(imgs[::2] + imgs[1::2], 2, n);

# 自定义语义分割数据集类
class VOCSegDataset(torch.utils.data.Dataset):
"""一个用于加载VOC数据集的自定义数据集"""

def __init__(self, is_train, crop_size, voc_dir):
self.transform = torchvision.transforms.Normalize(
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
self.crop_size = crop_size
features, labels = read_voc_images(voc_dir, is_train=is_train)
self.features = [self.normalize_image(feature)
for feature in self.filter(features)]
self.labels = self.filter(labels)
self.colormap2label = voc_colormap2label()
print('read ' + str(len(self.features)) + ' examples')

def normalize_image(self, img):
return self.transform(img.float() / 255)

def filter(self, imgs):
return [img for img in imgs if (
img.shape[1] >= self.crop_size[0] and
img.shape[2] >= self.crop_size[1])]

def __getitem__(self, idx):
feature, label = voc_rand_crop(self.features[idx], self.labels[idx],
*self.crop_size)
return (feature, voc_label_indices(label, self.colormap2label))

def __len__(self):
return len(self.features)

# 读取数据集
crop_size = (320, 480)
voc_train = VOCSegDataset(True, crop_size, voc_dir)
voc_test = VOCSegDataset(False, crop_size, voc_dir)

batch_size = 64
train_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True,
drop_last=True,
num_workers=d2l.get_dataloader_workers())
for X, Y in train_iter:
print(X.shape)
print(Y.shape)
break

# 整合所有组件
def load_data_voc(batch_size, crop_size):
"""加载VOC语义分割数据集"""
voc_dir = d2l.download_extract('voc2012', os.path.join(
'VOCdevkit', 'VOC2012'))
num_workers = d2l.get_dataloader_workers()
train_iter = torch.utils.data.DataLoader(
VOCSegDataset(True, crop_size, voc_dir), batch_size,
shuffle=True, drop_last=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(
VOCSegDataset(False, crop_size, voc_dir), batch_size,
drop_last=True, num_workers=num_workers)
return train_iter, test_iter

47 转置卷积

47.1 转置卷积

转置卷积

  • 转置卷积(在语义分割中常用)
    • 卷积不会增大输入的高宽,通常要么不变,要么减半
    • 转置卷积则可以用来增大输入高宽
  • 为什么称为转置
    • 对于卷积Y=XWY = X \star W,可以对W构造一个V使得卷积等价于Y=VX,Y,XY^{'} = VX^{'},Y^{'},X^{'}对应Y,XY,X的向量版本
    • 转置卷积则等价于Y=VTXY^{'} = V^TX^{'}
    • 如果卷积将输入从(h,w)(h,w)变成了(h,w)(h^{'},w^{'}),同样超参数下它将(h,w)(h^{'},w^{'})变成(h,w)(h,w)

47.2 转置卷积也是一种卷积

  • 转置卷积是一种卷积
    • 将输入和核进行了重新排列
    • 同卷积一般是做下采样不同,通常用作上采样
    • 如果卷积将输入从(h,w)(h,w)变成了(h,w)(h^{'},w^{'}),同样超参数下它将(h,w)(h^{'},w^{'})变成(h,w)(h,w)
  • 重新排列输入和核
    • 当填充为0步幅为1时
      • 将输入填充k-1(k是核窗口)
      • 将核矩阵上下,左右翻转
      • 然后正常卷积(填充0,步幅1)
    • 当填充为p步幅为1时
      • 将输入填充k-p-1(k是核窗口)
      • 将核矩阵上下,左右翻转
      • 然后正常卷积(填充0,步幅1)
    • 当填充为p步幅为s时
      • 在行和列之间插入s-1行或列
      • 将输入填充k-p-1(k是核窗口)
      • 将核矩阵上下,左右翻转
      • 然后正常卷积(填充0,步幅1)

转置卷积

  • 形状换算
    • 输入高(宽)为n,核k,填充p,步幅s
    • 转置卷积:n=sn+k2psn^{'}=sn+k-2p-s
      • 卷积:n=[(nk2p+s)/s]n>=sn+k2psn^{'}=[(n-k-2p+s)/s] \rightarrow n >= sn^{'} +k-2p-s
      • (nk2p+s)/s(n-k-2p+s)/s可以整除,则n>=sn+k2psn >= sn^{'} +k-2p-s 与转置卷积公式互为逆公式,当卷积在不能整除时,转置卷积时的nn为能整除的最小的nn
    • 如果让高宽成倍增加,那么k=2p+sk=2p+s
  • 同反卷积的关系
    • 数学上的反卷积是卷积的逆运算:若Y=conv(X,K)Y = conv(X,K)X=deconv(Y,K)X=deconv(Y,K)
    • 反卷积与转置卷积并相同,反卷积很少用在深度学习中,反卷积神经网络指用了转置卷积的神经网络
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
# 转置卷积的实现
import torch
from torch import nn
from d2l import torch as d2l

def trans_conv(X, K):
h, w = K.shape
Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Y[i: i + h, j: j + w] += X[i, j] * K
return Y

# 验证上述实现输出
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
trans_conv(X, K)

# 使用高级API获得相同的结果
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
tconv(X)

# 填充,步幅和多通道
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
tconv(X)

tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=2, bias=False)
tconv.weight.data = K
tconv(X)

X = torch.rand(size=(1, 10, 16, 16))
conv = nn.Conv2d(10, 20, kernel_size=5, padding=2, stride=3)
tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3)
tconv(conv(X)).shape == X.shape

# 与矩阵变换的关系
X = torch.arange(9.0).reshape(3, 3)
K = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
Y = d2l.corr2d(X, K)
Y

def kernel2matrix(K):
k, W = torch.zeros(5), torch.zeros((4, 9))
k[:2], k[3:5] = K[0, :], K[1, :]
W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
return W

W = kernel2matrix(K)
W

Y == torch.matmul(W, X.reshape(-1)).reshape(2, 2)

Z = trans_conv(Y, K)
Z == torch.matmul(W.T, Y.reshape(-1)).reshape(3, 3)
  • 转置卷积不是进行上采样,可以对卷积核的参数进行训练

48 全连接卷积神经网络 FCN

语义分割

  • 全连接神经网络(FCN)
    • 是用深度神经网络来做语义分割的奠基性工作
    • 用转置卷积层替换CNN最后的全连接层,从而可以实现每个像素的预测
      • 将CNN中的最后的全连接层和全局池化层去除
      • 1*1卷积层合并通道,降低通道数,减小计算量
      • 转置卷积层,将图片扩大

FCN

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 使用在ImageNet数据集上与训练的ResNet18模型来提取图像特征
pretrained_net = torchvision.models.resnet18(pretrained=True)
list(pretrained_net.children())[-3:]

# 创建一个全卷积网络实例net
net = nn.Sequential(*list(pretrained_net.children())[:-2])

X = torch.rand(size=(1, 3, 320, 480))
net(X).shape

# 使用1*1卷基层将输出通道转换为Pascal VOC2012数据集的类数
# 将要素地图的高宽增加32倍
num_classes = 21
net.add_module('final_conv', nn.Conv2d(512, num_classes, kernel_size=1))
net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes,
kernel_size=64, padding=16, stride=32))

# 初始化转置卷积层
def bilinear_kernel(in_channels, out_channels, kernel_size):
factor = (kernel_size + 1) // 2
if kernel_size % 2 == 1:
center = factor - 1
else:
center = factor - 0.5
og = (torch.arange(kernel_size).reshape(-1, 1),
torch.arange(kernel_size).reshape(1, -1))
filt = (1 - torch.abs(og[0] - center) / factor) * \
(1 - torch.abs(og[1] - center) / factor)
weight = torch.zeros((in_channels, out_channels,
kernel_size, kernel_size))
weight[range(in_channels), range(out_channels), :, :] = filt
return weight

# 双线性插值的上采样实验
conv_trans = nn.ConvTranspose2d(3, 3, kernel_size=4, padding=1, stride=2,
bias=False)
conv_trans.weight.data.copy_(bilinear_kernel(3, 3, 4));

img = torchvision.transforms.ToTensor()(d2l.Image.open('../img/catdog.jpg'))
X = img.unsqueeze(0)
Y = conv_trans(X)
out_img = Y[0].permute(1, 2, 0).detach()

d2l.set_figsize()
print('input image shape:', img.permute(1, 2, 0).shape)
d2l.plt.imshow(img.permute(1, 2, 0));
print('output image shape:', out_img.shape)
d2l.plt.imshow(out_img);

# 用双线性插值的上采样初始化转置卷积层
# 对于卷积层,使用Xavier初始化参数
W = bilinear_kernel(num_classes, num_classes, 64)
net.transpose_conv.weight.data.copy_(W);

# 读取数据集
batch_size, crop_size = 32, (320, 480)
train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size)

# 训练
def loss(inputs, targets):
return F.cross_entropy(inputs, targets, reduction='none').mean(1).mean(1)

num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus()
trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)

# 预测
def predict(img):
X = test_iter.dataset.normalize_image(img).unsqueeze(0)
pred = net(X.to(devices[0])).argmax(dim=1)
return pred.reshape(pred.shape[1], pred.shape[2])

# 可视化预测的类别
def label2image(pred):
colormap = torch.tensor(d2l.VOC_COLORMAP, device=devices[0])
X = pred.long()
return colormap[X, :]

voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')
test_images, test_labels = d2l.read_voc_images(voc_dir, False)
n, imgs = 4, []
for i in range(n):
crop_rect = (0, 0, 320, 480)
X = torchvision.transforms.functional.crop(test_images[i], *crop_rect)
pred = label2image(predict(X))
imgs += [X.permute(1,2,0), pred.cpu(),
torchvision.transforms.functional.crop(
test_labels[i], *crop_rect).permute(1,2,0)]
d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2);

49 样式迁移

  • 样式迁移:将样式图片中的样式迁移到内容图片上,得到合成图片(类似滤镜)
  • 基于CNN的样式迁移
    • 内容图片计算其CNN的特征的内容损失
    • 样式图片计算其CNN的特征的样式损失
    • 训练的生成的图片的CNN的特征的内容损失和样式损失,同时计算图像上的噪点
    • 训练内容为图片,不是损失函数

基于CNN的样式迁移

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

d2l.set_figsize()
content_img = d2l.Image.open('../img/rainier.jpg')
d2l.plt.imshow(content_img);

style_img = d2l.Image.open('../img/autumn-oak.jpg')
d2l.plt.imshow(style_img);

# 预处理和后处理
rgb_mean = torch.tensor([0.485, 0.456, 0.406])
rgb_std = torch.tensor([0.229, 0.224, 0.225])

def preprocess(img, image_shape):
transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize(image_shape),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=rgb_mean, std=rgb_std)])
return transforms(img).unsqueeze(0)

def postprocess(img): #上述过程的反过程
img = img[0].to(rgb_std.device)
img = torch.clamp(img.permute(1, 2, 0) * rgb_std + rgb_mean, 0, 1)
return torchvision.transforms.ToPILImage()(img.permute(2, 0, 1))

# 抽取图像特征,使用VGG19(VGG系列对抽取特征效果较好)
pretrained_net = torchvision.models.vgg19(pretrained=True)

style_layers, content_layers = [0, 5, 10, 19, 28], [25]

net = nn.Sequential(*[pretrained_net.features[i] for i in
range(max(content_layers + style_layers) + 1)])

# 抽取特征的函数
def extract_features(X, content_layers, style_layers):
contents = []
styles = []
for i in range(len(net)):
X = net[i](X)
if i in style_layers:
styles.append(X)
if i in content_layers:
contents.append(X)
return contents, styles

def get_contents(image_shape, device):
content_X = preprocess(content_img, image_shape).to(device)
contents_Y, _ = extract_features(content_X, content_layers, style_layers)
return content_X, contents_Y

def get_styles(image_shape, device):
style_X = preprocess(style_img, image_shape).to(device)
_, styles_Y = extract_features(style_X, content_layers, style_layers)
return style_X, styles_Y

# 定义损失函数
def content_loss(Y_hat, Y):
# 我们从动态计算梯度的树中分离目标:
# 这是一个规定的值,而不是一个变量。
return torch.square(Y_hat - Y.detach()).mean()

def gram(X):
num_channels, n = X.shape[1], X.numel() // X.shape[1]
X = X.reshape((num_channels, n))
return torch.matmul(X, X.T) / (num_channels * n)

def style_loss(Y_hat, gram_Y):
return torch.square(gram(Y_hat) - gram_Y.detach()).mean()

def tv_loss(Y_hat):
return 0.5 * (torch.abs(Y_hat[:, :, 1:, :] - Y_hat[:, :, :-1, :]).mean() +
torch.abs(Y_hat[:, :, :, 1:] - Y_hat[:, :, :, :-1]).mean())

# 风格转移的损失函数是内容损失,风格损失和总变化损失的加权和
content_weight, style_weight, tv_weight = 1, 1e3, 10

def compute_loss(X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram):
# 分别计算内容损失、风格损失和全变分损失
contents_l = [content_loss(Y_hat, Y) * content_weight for Y_hat, Y in zip(
contents_Y_hat, contents_Y)]
styles_l = [style_loss(Y_hat, Y) * style_weight for Y_hat, Y in zip(
styles_Y_hat, styles_Y_gram)]
tv_l = tv_loss(X) * tv_weight
# 对所有损失求和
l = sum(10 * styles_l + contents_l + [tv_l])
return contents_l, styles_l, tv_l, l

# 初始化合成图像
class SynthesizedImage(nn.Module):
def __init__(self, img_shape, **kwargs):
super(SynthesizedImage, self).__init__(**kwargs)
self.weight = nn.Parameter(torch.rand(*img_shape))

def forward(self):
return self.weight

def get_inits(X, device, lr, styles_Y):
gen_img = SynthesizedImage(X.shape).to(device)
gen_img.weight.data.copy_(X.data)
trainer = torch.optim.Adam(gen_img.parameters(), lr=lr)
styles_Y_gram = [gram(Y) for Y in styles_Y]
return gen_img(), styles_Y_gram, trainer

# 训练模型
def train(X, contents_Y, styles_Y, device, lr, num_epochs, lr_decay_epoch):
X, styles_Y_gram, trainer = get_inits(X, device, lr, styles_Y)
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_decay_epoch, 0.8)
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[10, num_epochs],
legend=['content', 'style', 'TV'],
ncols=2, figsize=(7, 2.5))
for epoch in range(num_epochs):
trainer.zero_grad()
contents_Y_hat, styles_Y_hat = extract_features(
X, content_layers, style_layers)
contents_l, styles_l, tv_l, l = compute_loss(
X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram)
l.backward()
trainer.step()
scheduler.step()
if (epoch + 1) % 10 == 0:
animator.axes[1].imshow(postprocess(X))
animator.add(epoch + 1, [float(sum(contents_l)),
float(sum(styles_l)), float(tv_l)])
return X

# 训练模型
device, image_shape = d2l.try_gpu(), (300, 450)
net = net.to(device)
content_X, contents_Y = get_contents(image_shape, device)
_, styles_Y = get_styles(image_shape, device)
output = train(content_X, contents_Y, styles_Y, device, 0.3, 500, 50)
  • VGG系列对抽取特征效果较好

51 序列模型

  • 序列数据:实际中很多数据是有时序结构
  • 统计工具
    • 在时间tt观察到xtx_t,那么得到T个不独立的随机变量(x1,xT)p(x)\left(x_1, \ldots x_T\right) \sim p(\mathbf{x})
    • 使用条件概率展开:p(a,b)=p(a)p(ba)=p(b)p(ab)p(a, b)=p(a) p(b \mid a)=p(b) p(a \mid b)

统计工具

  • 序列模型
    -p(x)=p(x1)p(x2x1)p(x3x1,x2)p(xTx1,xT1)p(\mathbf{x})=p\left(x_1\right) \cdot p\left(x_2 \mid x_1\right) \cdot p\left(x_3 \mid x_1, x_2\right) \cdot \ldots p\left(x_T \mid x_1, \ldots x_{T-1}\right)
    • 对条件概率建模:p(xtx1,xt1)=p(xtf(x1,xt1))p\left(x_t \mid x_1, \ldots x_{t-1}\right)=p\left(x_t \mid f\left(x_1, \ldots x_{t-1}\right)\right)
      • 对见过的数据建模,也称自回归模型
  • 方案A——马尔可夫假设
    • 假设当前数据只跟τ\tau个数据点相关:p(xtx1,xt1)=p(xtxtτ,xt1)=p(xtf(xtτ,xt1))p\left(x_t \mid x_1, \ldots x_{t-1}\right)=p\left(x_t \mid x_{t-\tau}, \ldots x_{t-1}\right)=p\left(x_t \mid f\left(x_{t-\tau}, \ldots x_{t-1}\right)\right)
  • 方案B——潜变量模型
    • 引入潜变量hth_t来表示过去信息ht=f(x1,...,xt1)h_t=f(x_1,...,x_{t-1})xt=p(xtht)x_t=p(x_t|h_t)

潜变量模型

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 使用正弦函数和一些可加性噪声来生成序列数据
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

T = 1000 # 总共产生1000个点
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3))

# 将数据映射为数据对y_t=x_t和x_t=[...]
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
features[:, i] = x[i: T - tau + i]
labels = x[tau:].reshape((-1, 1))

batch_size, n_train = 16, 600
# 只有前n_train个样本用于训练
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),
batch_size, is_train=True)

# 使用一个相当简单的结构:只是一个拥有两个全连接层的多层感知机
# 初始化网络权重的函数
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)

# 一个简单的多层感知机
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net

# 平方损失。注意:MSELoss计算平方误差时不带系数1/2
loss = nn.MSELoss(reduction='none')

# 训练模型
def train(net, train_iter, loss, epochs, lr):
trainer = torch.optim.Adam(net.parameters(), lr)
for epoch in range(epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.sum().backward()
trainer.step()
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')

net = get_net()
train(net, train_iter, loss, 5, 0.01)

# 模型预测下一个时间步
onestep_preds = net(features)
d2l.plot([time, time[tau:]],
[x.detach().numpy(), onestep_preds.detach().numpy()], 'time',
'x', legend=['data', '1-step preds'], xlim=[1, 1000],
figsize=(6, 3))

# 进行多步预测
multistep_preds = torch.zeros(T)
multistep_preds[: n_train + tau] = x[: n_train + tau]
for i in range(n_train + tau, T):
multistep_preds[i] = net(
multistep_preds[i - tau:i].reshape((1, -1)))

d2l.plot([time, time[tau:], time[n_train + tau:]],
[x.detach().numpy(), onestep_preds.detach().numpy(),
multistep_preds[n_train + tau:].detach().numpy()], 'time',
'x', legend=['data', '1-step preds', 'multistep preds'],
xlim=[1, 1000], figsize=(6, 3))
# 每次预测有误差,导致误差累积,预测效果较差

# 更仔细地看k预测
max_steps = 64

features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 列i(i<tau)是来自x的观测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau):
features[:, i] = x[i: i + T - tau - max_steps + 1]

# 列i(i>=tau)是来自(i-tau+1)步的预测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau, tau + max_steps):
features[:, i] = net(features[:, i - tau:i]).reshape(-1)

steps = (1, 4, 16, 64)
d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps],
[features[:, (tau + i - 1)].detach().numpy() for i in steps], 'time', 'x',
legend=[f'{i}-step preds' for i in steps], xlim=[5, 1000],
figsize=(6, 3))

52 文本预处理

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# 文本预处理
import collections
import re
from d2l import torch as d2l

# 将数据集读取到由多条文本行组成的列表中
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')

def read_time_machine():
"""将时间机器数据集加载到文本行的列表中"""
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])

# 构建一个字典,通常也叫做词汇表,用来将字符串类型的标记映射到从0开始的数字索引中
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)

# 构建词汇表
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])

# 将每一条文本行转换成一个数字索引列表
for i in [0, 10]:
print('文本:', tokens[i])
print('索引:', vocab[tokens[i]])

# 将所有功能打包到函数中
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)

53 语言模型

  • 语言模型
    • 给定文本序列x1,...xtx_1,...x_t,语言模型的目的是估计联合概率p(x1,...xT)p(x_1,...x_T)
    • 应用:
      • 做与训练模型(BERT,GPT-3)
      • 生成文本,给定前面几个词,不断地生成后续文本
      • 判断多个序列中哪个更常见
    • 使用计数来建模
      • 假定序列长度为2,预测p(x,x)=p(x)p(xx)=n(x)nn(x,x)n(x)p\left(x, x^{\prime}\right)=p(x) p\left(x^{\prime} \mid x\right)=\frac{n(x)}{n} \frac{n\left(x, x^{\prime}\right)}{n(x)}
        • n是总词数,n(x),n(x,x)n(x),n(x,x^{'})是单个单词和连续单词对的出现次数
      • 很容易扩展到长为3的情况p(x,x,x)=p(x)p(xx)p(xx,x)=n(x)nn(x,x)n(x)n(x,x,x)n(x,x)p\left(x, x^{\prime}, x^{\prime \prime}\right)=p(x) p\left(x^{\prime} \mid x\right) p\left(x^{\prime \prime} \mid x, x^{\prime}\right)=\frac{n(x)}{n} \frac{n\left(x, x^{\prime}\right)}{n(x)} \frac{n\left(x, x^{\prime}, x^{\prime \prime}\right)}{n\left(x, x^{\prime}\right)}
    • N元语法
      • 当序列很长时,因为文本量不够大,很可能n(x1,...xT)<=1n(x_1,...x_T) <= 1
      • 使用马尔可夫假设缓解这个问题
        • 一元语法:p(x1,x2,x3,x4)=p(x1)p(x2)p(x3)p(x4)=n(x1)nn(x2)nn(x3)nn(x4)np\left(x_1, x_2, x_3, x_4\right)=p\left(x_1\right) p\left(x_2\right) p\left(x_3\right) p\left(x_4\right)=\frac{n\left(x_1\right)}{n} \frac{n\left(x_2\right)}{n} \frac{n\left(x_3\right)}{n} \frac{n\left(x_4\right)}{n}
        • 二元语法:p(x1,x2,x3,x4)=p(x1)p(x2x1)p(x3x2)p(x4x3)=n(x1)nn(x1,x2)n(x1)n(x2,x3)n(x2)n(x3,x4)n(x3)p\left(x_1, x_2, x_3, x_4\right)=p\left(x_1\right) p\left(x_2 \mid x_1\right) p\left(x_3 \mid x_2\right) p\left(x_4 \mid x_3\right)=\frac{n\left(x_1\right)}{n} \frac{n\left(x_1, x_2\right)}{n\left(x_1\right)} \frac{n\left(x_2, x_3\right)}{n\left(x_2\right)} \frac{n\left(x_3, x_4\right)}{n\left(x_3\right)}
        • 三元语法:p(x1,x2,x3,x4)=p(x1)p(x2x1)p(x3x1,x2)p(x4x2,x3)p\left(x_1, x_2, x_3, x_4\right)=p\left(x_1\right) p\left(x_2 \mid x_1\right) p\left(x_3 \mid x_1, x_2\right) p\left(x_4 \mid x_2, x_3\right)
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import random
import torch
from d2l import torch as d2l

tokens = d2l.tokenize(d2l.read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]

# 最流行的次词被称为停用词 画出词频图
freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
xscale='log', yscale='log')

# 其他的次元组合,比如二元语法
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]

# 三元语法
trigram_tokens = [triple for triple in zip(
corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
trigram_vocab.token_freqs[:10]

# 直观对比一元语法,二元语法,三元语法
bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',
ylabel='frequency: n(x)', xscale='log', yscale='log',
legend=['unigram', 'bigram', 'trigram'])

# 随机地生成一个小批量数据的特征和标签以供读取
# 在随机采样中,每个样本都是在原始的长序列上任意获取的子序列
def seq_data_iter_random(corpus, batch_size, num_steps): #@save
"""使用随机抽样生成一个小批量子序列"""
# 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
corpus = corpus[random.randint(0, num_steps - 1):]
# 减去1,是因为我们需要考虑标签
num_subseqs = (len(corpus) - 1) // num_steps
# 长度为num_steps的子序列的起始索引
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 在随机抽样的迭代过程中,
# 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
random.shuffle(initial_indices)

def data(pos):
# 返回从pos位置开始的长度为num_steps的序列
return corpus[pos: pos + num_steps]

num_batches = num_subseqs // batch_size
for i in range(0, batch_size * num_batches, batch_size):
# 在这里,initial_indices包含子序列的随机起始索引
initial_indices_per_batch = initial_indices[i: i + batch_size]
X = [data(j) for j in initial_indices_per_batch]
Y = [data(j + 1) for j in initial_indices_per_batch]
yield torch.tensor(X), torch.tensor(Y)

# 生成一个0到34的序列
my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):
print('X: ', X, '\nY:', Y)

# 保证两个相邻的小批量中的子序列在原始序列上时连续的
def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save
"""使用顺序分区生成一个小批量子序列"""
# 从随机偏移量开始划分序列
offset = random.randint(0, num_steps)
num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
Xs = torch.tensor(corpus[offset: offset + num_tokens])
Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
num_batches = Xs.shape[1] // num_steps
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i: i + num_steps]
Y = Ys[:, i: i + num_steps]
yield X, Y

for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
print('X: ', X, '\nY:', Y)

# 将上面的两个采样函数包装到一个类中
class SeqDataLoader: #@save
"""加载序列数据的迭代器"""
def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
if use_random_iter:
self.data_iter_fn = d2l.seq_data_iter_random
else:
self.data_iter_fn = d2l.seq_data_iter_sequential
self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
self.batch_size, self.num_steps = batch_size, num_steps

def __iter__(self):
return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

# 定义总函数,同时返回数据迭代器和词汇表
def load_data_time_machine(batch_size, num_steps, #@save
use_random_iter=False, max_tokens=10000):
"""返回时光机器数据集的迭代器和词表"""
data_iter = SeqDataLoader(
batch_size, num_steps, use_random_iter, max_tokens)
return data_iter, data_iter.vocab

54 循环神经网络 RNN

  • 潜变量自回归模型:使用潜变量hth_t总结过去信息

潜变量自回归模型

  • 循环神经网络
    • 更新隐藏状态:ht=ϕ(Whhht1+Whxxt1+bh)\mathbf{h}_t=\phi\left(\mathbf{W}_{h h} \mathbf{h}_{t-1}+\mathbf{W}_{h x} \mathbf{x}_{t-1}+\mathbf{b}_h\right)
    • 输出ot=ϕ(Whoht+bo)\mathbf{o}_t=\phi\left(\mathbf{W}_{h o} \mathbf{h}_t+\mathbf{b}_o\right)

循环神经网络

使用卷积神经网络

  • 困惑度
    • 衡量一个语言模型的好坏可以用平均交叉熵:π=1ni=1nlogp(xtxt1,)\pi=\frac{1}{n} \sum_{i=1}^n-\log p\left(x_t \mid x_{t-1}, \ldots\right),p时语言模型预测概率,xtx_t是真实词
    • 历史原因NLP使用困惑度exp(π)exp(\pi)来衡量,是平均每次可能选项:1表示完美,无穷大是最差情况
  • 梯度裁剪
    • 迭代中计算这T个时间步上的梯度,在反向传播过程中产生长度为O(T)O(T)的矩阵乘法链,导致数值不稳定
    • 梯度剪裁能有效预防梯度爆炸,如果梯度长度超过θ\theta,那拖影回长度gmin(1,θg)g\mathbf{g} \leftarrow \min \left(1, \frac{\theta}{\|\mathbf{g}\|}\right) \mathbf{g}

55 循环神经网络 RNN 的实现

55.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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
%matplotlib inline
import math
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)

# one-hot编码
F.one_hot(torch.tensor([0, 2]), len(vocab))

# 小批量数据形状是(批量大小,时间步数)
X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape
# (时间步数, 批量大小, 字典长度)

# 初始化循环神经网络模型的模型参数
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

# 在初始化时返回隐藏状态
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,) # 行数为批量大小*时间

# 创建一个类包装这些函数
class RNNModelScratch:
"""从零开始实现的循环神经网络模型"""
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)

# 检查输出是否具有正确的形状
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape

# 定义预测函数来生成prefix之后的新字符
def predict_ch8(prefix, num_preds, net, vocab, device): #@save
"""在prefix后面生成新字符"""
state = net.begin_state(batch_size=1, device=device)
outputs = [vocab[prefix[0]]]
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
for y in prefix[1:]: # 预热期
_, state = net(get_input(), state)
outputs.append(vocab[y])
for _ in range(num_preds): # 预测num_preds步
y, state = net(get_input(), state)
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])

predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())

# 梯度剪裁
def grad_clipping(net, theta):
"""裁剪梯度"""
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm

# 定义一个函数在一个迭代周期内训练模型
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() #为什么将输出contact成一个维度,在loss角度上是一个多分类问题
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.backward()
grad_clipping(net, 1)
updater.step()
else:
l.backward()
grad_clipping(net, 1)
# 因为已经调用了mean函数
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

# 训练函数即支持从零开始实现,也可以使用高级API来实现
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())

net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,init_rnn_state, rnn)

train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
use_random_iter=True)

55.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
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
68
69
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)

# 定义模型
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

# 使用张量来初始化隐藏状态
state = torch.zeros((1, batch_size, num_hiddens))
state.shape

# 通过一个隐藏状态和一个输入,就可以用更新后的隐藏状态计算输出
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape

# 一个完整的循环网络模型定义了一个RNNModel类
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))

# 训练
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)

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

56 门控循环单元(GRU)

  • 关注一个序列
    • 不是每个观察值都是同等重要
    • 想只记住相关的观察需要
      • 能关注的机制(更新门)
      • 能遗忘的机制(重置门)

门

  • 候选隐藏状态:H~t=tanh(XtWxh+(RtHt1)Whh+bh)\tilde{\boldsymbol{H}}_t=\tanh \left(\boldsymbol{X}_t \boldsymbol{W}_{x h}+\left(\boldsymbol{R}_t \odot \boldsymbol{H}_{t-1}\right) \boldsymbol{W}_{h h}+\boldsymbol{b}_h\right)

候选隐藏状态

  • 隐状态:Ht=ZtHt1+(1Zt)H~t\boldsymbol{H}_t=\boldsymbol{Z}_t \odot \boldsymbol{H}_{t-1}+\left(1-\boldsymbol{Z}_t\right) \odot \tilde{\boldsymbol{H}}_t

隐状态

总结

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
import torch
from torch import nn
from d2l import torch as d2l

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

# 初始化模型参数
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

def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))

W_xz, W_hz, b_z = three() # 更新门参数
W_xr, W_hr, b_r = three() # 重置门参数
W_xh, W_hh, b_h = three() # 候选隐状态参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params

# 定义隐藏状态的初始化函数
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )

# 定义门控循环单元模型
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)

# 训练GRU
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

# 简洁实现
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

57 长短期记忆网络(LSTM)

  • 长短期记忆网络
    • 忘记门:将值朝0减少,\boldsymbol{F}_t &=\sigma\left(\boldsymbol{X}_t \boldsymbol{W}_{x f}+\boldsymbol{H}_{t-1} \boldsymbol{W}_{h f}+\boldsymbol{b}_f\right)
    • 输入门:决定不是忽略掉输入数据,\begin{aligned} \boldsymbol{I}_t &=\sigma\left(\boldsymbol{X}_t \boldsymbol{W}_{x i}+\boldsymbol{H}_{t-1} \boldsymbol{W}_{h i}+\boldsymbol{b}_i\right)
    • 输出门:决定是不是使用隐状态,\boldsymbol{O}_t &=\sigma\left(\boldsymbol{X}_t \boldsymbol{W}_{x \jmath}+\boldsymbol{H}_{t-1} \boldsymbol{W}_{h o}+\boldsymbol{b}_o\right) \end{aligned}

门

  • 候选记忆单元:C~t=tanh(XtWxc+Ht1Whc+bc)\tilde{\boldsymbol{C}}_t=\tanh \left(\boldsymbol{X}_t \boldsymbol{W}_{x c}+\boldsymbol{H}_{t-1} \boldsymbol{W}_{h c}+\boldsymbol{b}_c\right)

候选记忆单元

  • 记忆单元:Ct=FtCt1+ItC~t\boldsymbol{C}_t=\boldsymbol{F}_t \odot \boldsymbol{C}_{t-1}+\boldsymbol{I}_t \odot \tilde{\boldsymbol{C}}_t

记忆单元

  • 隐藏状态:Ht=Ottanh(Ct)\boldsymbol{H}_t=\boldsymbol{O}_t \odot \tanh \left(\boldsymbol{C}_t\right)

隐藏状态

总结

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
68
69
import torch
from torch import nn
from d2l import torch as d2l

# 从零开始实现
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

# 初始化模型参数
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size

def normal(shape):
return torch.randn(size=shape, device=device)*0.01

def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))

W_xi, W_hi, b_i = three() # 输入门参数
W_xf, W_hf, b_f = three() # 遗忘门参数
W_xo, W_ho, b_o = three() # 输出门参数
W_xc, W_hc, b_c = three() # 候选记忆元参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
b_c, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params

# 初始化函数
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))

# 实际模型
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
C = F * C + I * C_tilda
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H, C)

# 训练
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

# 简洁实现
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

58 深层循环神经网络

回顾循环神经网络

  • 更深
    • 浅RNN:输入,隐层,输出
    • 深RNN:输入,隐层,隐层,…,输出
      -Ht1=f1(Ht11,Xt)\mathbf{H}_t^1=f_1\left(\mathbf{H}_{t-1}^1, \mathbf{X}_t\right)\quad \cdotsHtj=fj(Ht1j,Htj1)\mathbf{H}_t^j=f_j \left(\mathbf{H}_{t-1}^j, \mathbf{H}_t^{j-1}\right)\quad \cdotsOt=g(HtL)\mathbf{O}_t=g\left(\mathbf{H}_t^L\right)

更深

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

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

# 通过num_layers的值来设定隐藏层数
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)

# 训练
num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

59 双向循环神经网络

  • 双向RNN
    • 一个前向RNN隐层
    • 一个方向RNN隐层
    • 合并两个隐状态得到输出
      -Ht=ϕ(XtWxh(f)+Ht1Whh(f)+bh(f)),Ht=ϕ(XtWxh(b)+Ht+1Whh(b)+bh(b)),Ht=[HtHt],Ot=HtWhq+bq\overrightarrow{\mathbf{H}}_t=\phi\left(\mathbf{X}_t \mathbf{W}_{x h}^{(f)}+\overrightarrow{\mathbf{H}}_{t-1} \mathbf{W}_{h h}^{(f)}+\mathbf{b}_h^{(f)}\right), \overleftarrow{\mathbf{H}}_t=\phi\left(\mathbf{X}_t \mathbf{W}_{x h}^{(b)}+\overleftarrow{\mathbf{H}}_{t+1} \mathbf{W}_{h h}^{(b)}+\mathbf{b}_h^{(b)}\right), \mathbf{H}_t=\left[\overrightarrow{\mathbf{H}}_t \overleftarrow{\mathbf{H}}_t\right], \mathbf{O}_t=\mathbf{H}_t \mathbf{W}_{h q}+\mathbf{b}_q

双向RNN

  • 总结
    • 双向循环神经网络通过反向更新的隐藏层来利用方向时间信息
    • 通常用来对序列抽取特征,填空,而不是预测未来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 双向神经网络的错误应用
# bidirectional = True 即可使用双向循环神经网络
import torch
from torch import nn
from d2l import torch as d2l

# 加载数据
batch_size, num_steps, device = 32, 35, d2l.try_gpu()
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 通过设置“bidirective=True”来定义双向LSTM模型
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectional=True)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
# 训练模型
num_epochs, lr = 500, 1
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

60 机器翻译数据集

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import os
import torch
from d2l import torch as d2l

# 下载和预处理数据集
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')

def read_data_nmt():
"""载入“英语-法语”数据集"""
data_dir = d2l.download_extract('fra-eng')
with open(os.path.join(data_dir, 'fra.txt'), 'r',
encoding='utf-8') as f:
return f.read()

raw_text = read_data_nmt()
print(raw_text[:75])

# 几个预处理步骤
def preprocess_nmt(text):
"""预处理“英语-法语”数据集"""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '

# 使用空格替换不间断空格
# 使用小写字母替换大写字母
text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
# 在单词和标点符号之间插入空格
out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
for i, char in enumerate(text)]
return ''.join(out)

text = preprocess_nmt(raw_text)
print(text[:80])

# 词元化
def tokenize_nmt(text, num_examples=None):
"""词元化“英语-法语”数据数据集"""
source, target = [], []
for i, line in enumerate(text.split('\n')):
if num_examples and i > num_examples:
break
parts = line.split('\t')
if len(parts) == 2:
source.append(parts[0].split(' '))
target.append(parts[1].split(' '))
return source, target

source, target = tokenize_nmt(text)
source[:6], target[:6]

# 绘制每个文本序列所包含的标记数量的直方图
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):
"""绘制列表长度对的直方图"""
d2l.set_figsize()
_, _, patches = d2l.plt.hist(
[[len(l) for l in xlist], [len(l) for l in ylist]])
d2l.plt.xlabel(xlabel)
d2l.plt.ylabel(ylabel)
for patch in patches[1].patches:
patch.set_hatch('/')
d2l.plt.legend(legend)

show_list_len_pair_hist(['source', 'target'], '# tokens per sequence',
'count', source, target);

# 词汇表
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)

# 序列样本都有一个固定的长度 截断或填充文本序列
def truncate_pad(line, num_steps, padding_token):
"""截断或填充文本序列"""
if len(line) > num_steps:
return line[:num_steps] # 截断
return line + [padding_token] * (num_steps - len(line)) # 填充

truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])

# 转换成小批量数据集用于训练
def build_array_nmt(lines, vocab, num_steps):
"""将机器翻译的文本序列转换成小批量"""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = torch.tensor([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
return array, valid_len

# 训练模型
def load_data_nmt(batch_size, num_steps, num_examples=600):
"""返回翻译数据集的迭代器和词表"""
text = preprocess_nmt(read_data_nmt())
source, target = tokenize_nmt(text, num_examples)
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
tgt_vocab = d2l.Vocab(target, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
data_iter = d2l.load_array(data_arrays, batch_size)
return data_iter, src_vocab, tgt_vocab

# 读出"英语-法语"数据集中的第一个小批量数据
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
print('X:', X.type(torch.int32))
print('X的有效长度:', X_valid_len)
print('Y:', Y.type(torch.int32))
print('Y的有效长度:', Y_valid_len)
break

61 编码器-解码器架构

  • 重新考察CNN
    • 编码器:将输入编程成中间表达形式(特征)
      • 将文本表示成向量
    • 解码器:将中间表示解码成输出
      • 向量表示成输出
  • 编码器-解码器架构
    • 一个模型被分为两块:编码器处理输出,解码器生成输出

重新考虑CNN

重新考察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
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

#@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

#@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)

62 序列到序列学习(seq2seq)

  • Seq2Seq
    • 编码器是一个RNN,读取输入句子:可以是双向的
      • 双向RNN经常用在解码器中
    • 解码器使用另一个RNN来输出
      • 上一个时刻的翻译输出传递给下一时刻,同时结合此时刻的输入进行输出

Seq2Seq

  • 编码器-解码器细节
    • 编码器是没有输出的RNN
    • 编码器最后时间步的隐状态用作解码器的初始隐状态
    • 编码器将最后一层的RNN,最后时间步的隐状态结合解码器的Embedding用作解码器的初始隐状态

编码器-解码器细节

  • 训练:训练时解码器使用目标句子作为输入
    • 推理时使用上一时刻的输出作为输入

训练

  • 衡量生成序列的好坏
    -pnp_n是预测中所有n-gram的精度
    • 标签序列 A B C D E F 和预测序列 A B B C D ,有p1=4/5,p2=3/4,p3=1/3,p4=0p_1=4/5, p_2=3/4, p_3=1/3, p_4=0
    • BLEU定义:exp(min(0,1lenlabel lenpred ))n=1kpn1/2n\exp \left(\min \left(0,1-\frac{\operatorname{len}_{\text {label }}}{\operatorname{len}_{\text {pred }}}\right)\right) \prod_{n=1}^k p_n^{1 / 2^n},该值越大越好
      -\frac{\operatorname{len}_{\text {label }}为惩罚过短的预测,pn1/2np_n^{1 / 2^n}长匹配有高权重

跳转至教材

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l

# 实现循环神经网络编码器
class Seq2SeqEncoder(d2l.Encoder):
"""用于序列到序列学习的循环神经网络编码器"""
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
# 嵌入层
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
dropout=dropout)

def forward(self, X, *args):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X)
# 在循环神经网络模型中,第一个轴对应于时间步
X = X.permute(1, 0, 2)
# 如果未提及状态,则默认为0
output, state = self.rnn(X)
# output的形状:(num_steps,batch_size,num_hiddens)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state

# 上述编码器的实现
encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape

state.shape

# 解码器
class Seq2SeqDecoder(d2l.Decoder):
"""用于序列到序列学习的循环神经网络解码器"""
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
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, *args):
return enc_outputs[1]

def forward(self, X, state):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X).permute(1, 0, 2)
# 广播context,使其具有与X相同的num_steps
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), 2)
output, state = self.rnn(X_and_context, state)
output = self.dense(output).permute(1, 0, 2)
# output的形状:(batch_size,num_steps,vocab_size)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state

# 实例化解码器
decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape

# 通过零值化屏蔽不相关的项
def sequence_mask(X, valid_len, value=0):
"""在序列中屏蔽不相关的项"""
maxlen = X.size(1)
mask = torch.arange((maxlen), dtype=torch.float32,
device=X.device)[None, :] < valid_len[:, None]
X[~mask] = value
return X

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))

X = torch.ones(2, 3, 4)
sequence_mask(X, torch.tensor([1, 2]), value=-1)

# 通过扩展softmax交叉熵损失函数来屏蔽不相关的预测
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
"""带遮蔽的softmax交叉熵损失函数"""
# pred的形状:(batch_size,num_steps,vocab_size)
# label的形状:(batch_size,num_steps)
# valid_len的形状:(batch_size,)
def forward(self, pred, label, valid_len):
weights = torch.ones_like(label)
weights = sequence_mask(weights, valid_len)
self.reduction='none'
unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
pred.permute(0, 2, 1), label)
weighted_loss = (unweighted_loss * weights).mean(dim=1)
return weighted_loss

# 代码健全性检查
loss = MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),
torch.tensor([4, 2, 0]))

# 训练
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
"""训练序列到序列模型"""
def xavier_init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
if type(m) == nn.GRU:
for param in m._flat_weights_names:
if "weight" in param:
nn.init.xavier_uniform_(m._parameters[param])

net.apply(xavier_init_weights)
net.to(device)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = MaskedSoftmaxCELoss()
net.train()
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[10, num_epochs])
for epoch in range(num_epochs):
timer = d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失总和,词元数量
for batch in data_iter:
optimizer.zero_grad()
X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
device=device).reshape(-1, 1)
dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学
Y_hat, _ = net(X, dec_input, X_valid_len)
l = loss(Y_hat, Y, Y_valid_len)
l.sum().backward() # 损失函数的标量进行“反向传播”
d2l.grad_clipping(net, 1)
num_tokens = Y_valid_len.sum()
optimizer.step()
with torch.no_grad():
metric.add(l.sum(), num_tokens)
if (epoch + 1) % 10 == 0:
animator.add(epoch + 1, (metric[0] / metric[1],))
print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
f'tokens/sec on {str(device)}')

# 创建和训练一个循环卷积神经网络"编码器-解码器"模型
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

# 预测
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
device, save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为评估模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
src_vocab['<eos>']]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
# 添加批量轴
enc_X = torch.unsqueeze(
torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
enc_outputs = net.encoder(enc_X, enc_valid_len)
dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
# 添加批量轴
dec_X = torch.unsqueeze(torch.tensor(
[tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
output_seq, attention_weight_seq = [], []
for _ in range(num_steps):
Y, dec_state = net.decoder(dec_X, dec_state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['<eos>']:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

# BLEU的代码实现
def bleu(pred_seq, label_seq, k): #@save
"""计算BLEU"""
pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
len_pred, len_label = len(pred_tokens), len(label_tokens)
score = math.exp(min(0, 1 - len_label / len_pred))
for n in range(1, k + 1):
num_matches, label_subs = 0, collections.defaultdict(int)
for i in range(len_label - n + 1):
label_subs[' '.join(label_tokens[i: i + n])] += 1
for i in range(len_pred - n + 1):
if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
num_matches += 1
label_subs[' '.join(pred_tokens[i: i + n])] -= 1
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score

# 将几个英语句子翻译成法语
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
translation, attention_weight_seq = predict_seq2seq(
net, eng, src_vocab, tgt_vocab, num_steps, device)
print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

63 束搜索

  • 贪心搜索
    • Seq2Seq中使用了贪心搜索来预测序列:将当时时刻预测概率最大的词输出
    • 但贪心很可能不是最优的:当前选取的在全局时间上可能不是最优的

贪心搜索

  • 穷举搜索
    • 最优算法:对所有可能的序列,计算它的概率,然后选取最好的那个
    • 如果输出字典大小为n, 序列最长为T,那么需要考虑nTn^T个序列:计算上不可行
  • 束搜索
    • 做法
      • 保存最好的k个候选
      • 在每个时刻,对每个候选新加一项(n种可能),在knkn个选项中选出最好的k个
    • 时间复杂度O(knT)O(knT)
    • 每个候选的最终分数:1Lαlogp(y1,,yL)=1Lαt=1Llogp(yty1,,yt1,c)\frac{1}{L^\alpha} \log p\left(y_1, \ldots, y_L\right)=\frac{1}{L^\alpha} \sum_{t^{\prime}=1}^L \log p\left(y_{t^{\prime}} \mid y_1, \ldots, y_{t^{\prime}-1}, \boldsymbol{c}\right)
      • 通常:α=0.75\alpha=0.75

示例


64 注意力机制

  • 注意力机制
    • 卷积,全连接,池化层都只考虑不随意线索
    • 注意力机制则显示的考虑随意线索
      • 随意线索被称之为查询(query)
      • 每个输入是一个值(value)和不随意线索(key)的对
      • 通过注意力池化层来有偏向性的选项选择某些输入
  • 非参注意力池化层
    • 给定数据(xi,yi),i=1,...,n(x_i,y_i),i=1,...,n
    • 平均池化是最简单的方案:f(x)=1niyif(x)=\frac{1}{n} \sum_i y_i
    • 更好的方案是Nadaraya-Watson核回归f(x)=i=1nK(xxi)j=1nK(xxj)yif(x)=\sum_{i=1}^n \frac{K\left(x-x_i\right)}{\sum_{j=1}^n K\left(x-x_j\right)} y_i
      • K是核为衡量xxix与x_i距离的量,yiy_i为value,xjx_j为key
  • Nadaraya-Watson核回归
    • 使用高斯核:K(u)=12πexp(u22)K(u)=\frac{1}{\sqrt{2 \pi}} \exp \left(-\frac{u^2}{2}\right)
    • 则:f(x)=i=1nexp(12(xxi)2)j=1nexp(12(xxj)2)yi=i=1nsoftmax(12(xxi)2)yi\begin{aligned} f(x) &=\sum_{i=1}^n \frac{\exp \left(-\frac{1}{2}\left(x-x_i\right)^2\right)}{\sum_{j=1}^n \exp \left(-\frac{1}{2}\left(x-x_j\right)^2\right)} y_i \\ &=\sum_{i=1}^n \operatorname{softmax}\left(-\frac{1}{2}\left(x-x_i\right)^2\right) y_i \end{aligned}
  • 参数化的注意力机制
    • 在之前基础上引入可学习的 w :f(x)=i=1nsoftmax(12((xxi)w)2)yif(x)=\sum_{i=1}^n \operatorname{softmax}\left(-\frac{1}{2}\left(\left(x-x_i\right) w\right)^2\right) y_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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 注意力汇集Nadaraya-Watson核回归
import torch
from torch import nn
from d2l import torch as d2l

# 生成数据集
n_train = 50 # 训练样本数
x_train, _ = torch.sort(torch.rand(n_train) * 5) # 排序后的训练样本

def f(x):
return 2 * torch.sin(x) + x**0.8

y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) # 训练样本的输出
x_test = torch.arange(0, 5, 0.1) # 测试样本
y_truth = f(x_test) # 测试样本的真实输出
n_test = len(x_test) # 测试样本数
n_test

def plot_kernel_reg(y_hat):
d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'],
xlim=[0, 5], ylim=[-1, 5])
d2l.plt.plot(x_train, y_train, 'o', alpha=0.5);

y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)

# 非参数注意力汇聚
# X_repeat的形状:(n_test,n_train),
# 每一行都包含着相同的测试输入(例如:同样的查询)
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)

# 注意力权重
d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')

# 带参数注意力汇聚,假定两个张量的形状分别为(n,a,b)和(n,b,c),它们的批量矩阵乘法输出形状为(n,a,c)
X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
torch.bmm(X, Y).shape

# 使用小批量矩阵乘法来计算小批量数据中的加权平均值
weights = torch.ones((2, 10)) * 0.1
values = torch.arange(20.0).reshape((2, 10))
torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1))

# 带参数的注意力汇聚
class NWKernelRegression(nn.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = nn.Parameter(torch.rand((1,), requires_grad=True))

def forward(self, queries, keys, values):
# queries和attention_weights的形状为(查询个数,“键-值”对个数)
queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1]))
self.attention_weights = nn.functional.softmax(
-((queries - keys) * self.w)**2 / 2, dim=1)
# values的形状为(查询个数,“键-值”对个数)
return torch.bmm(self.attention_weights.unsqueeze(1),
values.unsqueeze(-1)).reshape(-1)

# 将训练数据集转换为键和值
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = x_train.repeat((n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = y_train.repeat((n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))

# 训练带参数的注意力汇聚模型
net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])

for epoch in range(5):
trainer.zero_grad()
l = loss(net(x_train, keys, values), y_train)
l.sum().backward()
trainer.step()
print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
animator.add(epoch + 1, float(l.sum()))

# 预测结果绘制
# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = x_train.repeat((n_test, 1))
# value的形状:(n_test,n_train)
values = y_train.repeat((n_test, 1))
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)

d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')

65 注意力分数

  • 回顾:f(x)=iα(x,xi)yi=i=1nsoftmax(12(xxi)2)yi,αf(x)=\sum_i \alpha\left(x, x_i\right) y_i=\sum_{i=1}^n \operatorname{softmax}\left(-\frac{1}{2}\left(x-x_i\right)^2\right) y_i, \alpha为注意力权重,12(xxi)2-\frac{1}{2}\left(x-x_i\right)^2为注意力分数

注意力分数

  • 拓展到高维度
    • 假设queryqRqq \in \R^q, m对key-value(k1,v1),kiRk,viRv(k_1,v_1),k_i \in \R^k, v_i \in \R^v
    • 注意力池化层:
      -f(q,(k1,v1),,(km,vm))=i=1mα(q,ki)viRvf\left(\mathbf{q},\left(\mathbf{k}_1, \mathbf{v}_1\right), \ldots,\left(\mathbf{k}_m, \mathbf{v}_m\right)\right)=\sum_{i=1}^m \alpha\left(\mathbf{q}, \mathbf{k}_i\right) \mathbf{v}_i \in \mathbb{R}^v
      -α(q,ki)=softmax(a(q,ki))=exp(a(q,ki))j=1mexp(a(q,kj))R\alpha\left(\mathbf{q}, \mathbf{k}_i\right)=\operatorname{softmax}\left(a\left(\mathbf{q}, \mathbf{k}_i\right)\right)=\frac{\exp \left(a\left(\mathbf{q}, \mathbf{k}_i\right)\right)}{\sum_{j=1}^m \exp \left(a\left(\mathbf{q}, \mathbf{k}_j\right)\right)} \in \mathbb{R}
      • 此处a为注意力分数
  • Additive Attention(加性注意力)
    • 可学参数:WkRh×k,WqRh×q,vRh,a(k,q)=vTtanh(Wkk+Wqq)\mathbf{W}_k \in \mathbb{R}^{h \times k}, \mathbf{W}_q \in \mathbb{R}^{h \times q}, \mathbf{v} \in \mathbb{R}^h, a(\mathbf{k}, \mathbf{q})=\mathbf{v}^T \tanh \left(\mathbf{W}_k \mathbf{k}+\mathbf{W}_q \mathbf{q}\right)
    • 等价于将query和key合并起来后放入到一个隐藏大小为h输出大小为1的单隐藏层MLP
    • 好处:key和value可以为任意长度
  • scaled dot-product attention
    • 如果query和key都是同样的长度q,kiRdq,k_i \in \R^d,那么可以a(q,ki)=q,ki/da\left(\mathbf{q}, \mathbf{k}_{\mathbf{i}}\right)=\left\langle\mathbf{q}, \mathbf{k}_{\mathbf{i}}\right\rangle / \sqrt{d}
    • 向量化版本
      -QRn×d,KRm×d,VRm×v\mathbf{Q} \in \mathbb{R}^{n \times d}, \mathbf{K} \in \mathbb{R}^{m \times d}, \mathbf{V} \in \mathbb{R}^{m \times v}
      • 注意力分数:a(Q,K)=QKT/dRn×ma(\mathbf{Q}, \mathbf{K})=\mathbf{Q K}^T / \sqrt{d} \in \mathbb{R}^{n \times m}
      • 注意力池化:f=softmax(a(Q,K))VRn×vf=\operatorname{softmax}(a(\mathbf{Q}, \mathbf{K})) \mathbf{V} \in \mathbb{R}^{n \times v}
  • 总结
    • 注意力分数是query和key的相似度,注意力权重是分数的softmax的结果
    • 两种常见的分数计算
      • 将query和key合并起来放入一个单输出单隐层的MLP
      • 直接将query和key做内积
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import math
import torch
from torch import nn
from d2l import torch as d2l

# 遮掩softmax操作
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)

masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))

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

# 加性注意力
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)

queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2))
# values的小批量,两个值矩阵是相同的
values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat(
2, 1, 1)
valid_lens = torch.tensor([2, 6])

attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8,
dropout=0.1)
attention.eval()
attention(queries, keys, values, valid_lens)

d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),
xlabel='Keys', ylabel='Queries')

# 缩放点和注意力
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)

# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,“键-值”对的个数,d)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)

queries = torch.normal(0, 1, (2, 1, 2))
attention = DotProductAttention(dropout=0.5)
attention.eval()
attention(queries, keys, values, valid_lens)

d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),
xlabel='Keys', ylabel='Queries')

66 使用注意力机制的seq2seq

跳转至Bilibili视频
跳转至教材

67 自注意力

跳转至Bilibili视频
跳转至教材

68 Transformer

跳转至Bilibili视频
跳转至教材

69 BERT预训练

跳转至Bilibili视频
跳转至教材

70 BERT微调

跳转至Bilibili视频
跳转至教材

71 优化算法

跳转至教材

  • 动量法:使用平滑过的梯度对权重更新
    -gt=1biIti(xt1)\mathbf{g}_t=\frac{1}{b} \sum_{i \in I_t} \nabla \ell_i\left(\mathbf{x}_{t-1}\right)
    -vt=βvt1+gtwt=wt1ηvt\mathbf{v}_t=\beta \mathbf{v}_{t-1}+\mathbf{g}_t \quad \mathbf{w}_t=\mathbf{w}_{t-1}-\eta \mathbf{v}_t
    • 梯度平滑:vt=gt+βgt1+β2gt2+β3gt3+\mathbf{v}_t=\mathbf{g}_t+\beta \mathbf{g}_{t-1}+\beta^2 \mathbf{g}_{t-2}+\beta^3 \mathbf{g}_{t-3}+\ldots
      -β\beta常见取值[0.5,0.9,0.95,0.99][0.5, 0.9, 0.95, 0.99]
    • 最简单的算法中都实现了该方法,pytorchSGD 中具有 momentum 选项即为动量法
  • Adam
    • 记录vt=β1vt1+(1β1)gt\mathbf{v}_t=\beta_1 \mathbf{v}_{t-1}+\left(1-\beta_1\right) \mathbf{g}_t通常β1=0.9\beta_1=0.9
    • 展开vt=(1β1)(gt+β1gt1+β12gt2+β13gt3+\mathbf{v}_t=\left(1-\beta_1\right)\left(\mathbf{g}_t+\beta_1 \mathbf{g}_{t-1}+\beta_1^2 \mathbf{g}_{t-2}+\beta_1^3 \mathbf{g}_{t-3}+\ldots\right.
    • 因为i=0β1i=11β1\sum_{i=0}^{\infty} \beta_1^i=\frac{1}{1-\beta_1}, 所以权重和为 1
    • 由于v0=0\mathbf{v}_0=0, 且i=0tβ1t=1β1t1β1\sum_{i=0}^t \beta_1^t=\frac{1-\beta_1^t}{1-\beta_1}, 修正v^t=vt1β1t\hat{\mathbf{v}}_t=\frac{\mathbf{v}_t}{1-\beta_1^t}
    • 类似记录st=β2st1+(1β2)gt2\mathbf{s}_t=\beta_2 \mathbf{s}_{t-1}+\left(1-\beta_2\right) \mathbf{g}_t^2, 通常β2=0.999\beta_2=0.999, 且修正s^t=st1β2t\hat{\mathbf{s}}_t=\frac{\mathbf{s}_t}{1-\beta_2^t}
    • 计算重新调整后的梯度gt=v^ts^t+ϵ\mathbf{g}_t^{\prime}=\frac{\hat{\mathbf{v}}_t}{\sqrt{\hat{\mathbf{s}}_t+\epsilon}}
    • 最后更新wt=wt1ηgt\mathbf{w}_t=\mathbf{w}_{t-1}-\eta \mathbf{g}_t^{\prime}

评论