因为强化学习里面有些状态是图片表示的,需要用 CNN 抽取信息,所以对 CNN 要有基础的了解。
卷积层
原理
1200 万像素,RGB 3通道图片 3600万元素,使用 FC(全连接) size 100 的隐藏层,模型 36 亿元素,所以需要新的模型表征图片。 新模型需要一些特性:平移不变性和局部性,平移和局部抽取都不改变图片特性,把 FC 从平移角度和局部抽取角度来设计新模型,就是卷积层,卷积是特殊的 FC 操作。 卷积核
<math xmlns="http://www.w3.org/1998/Math/MathML"> h i , j = ∑ k , l w i , j , k , l x k , l = ∑ a , b v i , j , a , b x i + a , j + b h_{i, j}=\sum_{k, l} w_{i, j, k, l} x_{k, l}=\sum_{a, b} v_{i, j, a, b} x_{i+a, j+b} </math>hi,j=∑k,lwi,j,k,lxk,l=∑a,bvi,j,a,bxi+a,j+b (从 FC 推出) 平移不变性: <math xmlns="http://www.w3.org/1998/Math/MathML"> v i , j , a , b = v a , b v_{i, j, a, b}=v_{a, b} </math>vi,j,a,b=va,b
局部性: <math xmlns="http://www.w3.org/1998/Math/MathML"> h i , j = ∑ a = − Δ Δ ∑ b = − Δ Δ v a , b x i + a , j + b h_{i, j}=\sum_{a=-\Delta}^{\Delta} \sum_{b=-\Delta}^{\Delta} v_{a, b} x_{i+a, j+b} </math>hi,j=∑a=−ΔΔ∑b=−ΔΔva,bxi+a,j+b <math xmlns="http://www.w3.org/1998/Math/MathML"> 当 ∣ a ∣ , ∣ b ∣ > Δ 时, 使得 v a , b = 0 \text { 当 }|a|,|b|>\Delta \text { 时, 使得 } v_{a, b}=0 </math> 当 ∣a∣,∣b∣>Δ 时, 使得 va,b=0 所以 a,b 即卷积核的大小,这里比较抽象,可以看下参考第二个视频,比较好理解。 二维交叉相关: <math xmlns="http://www.w3.org/1998/Math/MathML"> y i , j = ∑ a = 1 h ∑ b = 1 w w a , b x i + a , j + b y_{i, j}=\sum_{a=1}^h \sum_{b=1}^w w_{a, b} x_{i+a, j+b} </math>yi,j=∑a=1h∑b=1wwa,bxi+a,j+b (图片) 二维卷积: <math xmlns="http://www.w3.org/1998/Math/MathML"> y i , j = ∑ a = 1 h ∑ b = 1 w w − a , − b x i + a , j + b y_{i, j}=\sum_{a=1}^h \sum_{b=1}^w w_{-a,-b} x_{i+a, j+b} </math>yi,j=∑a=1h∑b=1ww−a,−bxi+a,j+b
实际上实现的是二维交叉相关,因为实现起来方便。同理有: 一维交叉相关: <math xmlns="http://www.w3.org/1998/Math/MathML"> y i = ∑ a = 1 h w a x i + a y_i=\sum_{a=1}^h w_a x_{i+a} </math>yi=∑a=1hwaxi+a (文本,时序序列) 三维交叉相关: <math xmlns="http://www.w3.org/1998/Math/MathML"> y i , j , k = ∑ a = 1 h ∑ b = 1 w ∑ c = 1 d w a , b , c x i + a , j + b , k + c y_{i, j, k}=\sum_{a=1}^h \sum_{b=1}^w \sum_{c=1}^d w_{a, b, c} x_{i+a, j+b, k+c} </math>yi,j,k=∑a=1h∑b=1w∑c=1dwa,b,cxi+a,j+b,k+c (视频,医学图像,气象地图) 可学习参数:卷积核矩阵 w 和 偏移 b 超参数:卷积核大小 填充和步幅 图片是有边缘的,为了考虑边缘信息,引入了填充概念。那么二维卷积后的输出为:
<math xmlns="http://www.w3.org/1998/Math/MathML"> ( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) ( p 代表 p a d d i n g ) \left(n_h-k_h+p_h+1\right) \times\left(n_w-k_w+p_w+1\right) (p 代表 padding) </math>(nh−kh+ph+1)×(nw−kw+pw+1)(p代表padding)
一般情况下: <math xmlns="http://www.w3.org/1998/Math/MathML"> p h = k h − 1 和 p w = k w − 1 p_h=k_h-1 \text { 和 } p_w=k_w-1 </math>ph=kh−1 和 pw=kw−1 (保证输入输出的维度不改变),k 是奇数,这样可以对称填充 <math xmlns="http://www.w3.org/1998/Math/MathML"> p 2 \frac{p}{2} </math>2p 。 还可以通过改变步幅(移动窗口的格数)加宽卷积速度,之前的步幅都是 1。那么二维卷积后的输出为:
<math xmlns="http://www.w3.org/1998/Math/MathML"> ⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ \left\lfloor\left(n_h-k_h+p_h+s_h\right) / s_h\right\rfloor \times\left\lfloor\left(n_w-k_w+p_w+s_w\right) / s_w\right\rfloor </math>⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋
多输入通道 当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道( <math xmlns="http://www.w3.org/1998/Math/MathML"> c i c_i </math>ci)数的卷积核,以便与输入数据进行互相关运算。 多输出通道 输入数据具有相同输入通道数的卷积核 * <math xmlns="http://www.w3.org/1998/Math/MathML"> c o c_o </math>co , <math xmlns="http://www.w3.org/1998/Math/MathML"> c o c_o </math>co 就是多输出通道的数目。 那么就有(batch = 1) 输入 X: <math xmlns="http://www.w3.org/1998/Math/MathML"> c i × n h × n w c_i \times n_h \times n_w </math>ci×nh×nw
卷积核 W: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o × c i × k h × k w c_o \times c_i \times k_h \times k_w </math>co×ci×kh×kw
偏差 B: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o × c i c_o \times c_i </math>co×ci
输出 Y: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o × m h × m w c_o \times m_h \times m_w </math>co×mh×mw
池化层(汇聚层) 平均池化层和最大池化层,也有对应的填充和步幅。
代码
python
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K):
"""
二维交叉相关
X:输入
K:卷积核
"""
h, w = K.shape
# Y[i][j] = sum(X[i:i+h, j:j+w] * K)
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
def pool2d(X, pool_size, mode='max'):
"""
二维最大(平均)池化层
X:输入
pool_size:池化层大小
"""
p_h, p_w = pool_size
Y = np.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
class Conv2D(nn.Module):
"""
二维卷积层
weight:卷积核矩阵
bias:偏移
"""
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
LeNet
网络结构
代码
python
import torch
from torch import nn
from d2l import torch as d2l
# LeNet
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
# 打印模型信息 用 summary 也可以,但是模型结构太复杂的时候 手动打印最关键层信息即可
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)
# 训练和评估
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
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]
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)
nn.init.xavier_uniform_(m.bias) # 这里视频里没有 一般 bias 也初始化一下比较好
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)}')
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
AlexNet
网络结构
更深更大的 LeNet 主要改进:丢弃法/ReLu/MaxPooling
代码
python
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))
# 训练
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())
VGG
网络结构
更大更深的 AlexNet,拉开了炼丹的序幕。 结构更合理,有了 vgg 块,扩展模型,可以通过增加 vgg 块的数目。
代码
python
import torch
from torch import nn
from d2l import torch as d2l
# vgg 块
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 block 结构(8 个卷积层)
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
# vgg11 (8 + 3)
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(),
# 全连接层部分(3个)
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)
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())
NiN
对全连接层参数很多的问题,提出了 NiN 块 LeNet: 16x5x5x120 = 48k AlexNet: 256x5x5x4096 = 26M VGG: 512x7x7x4096 = 102M
网络结构
NiN 块:一个卷积层后面跟了两个全连接层(1*1 卷积,用于通道融合,减少 FC 参数)
代码
python
import torch
from torch import nn
from d2l import torch as d2l
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
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())
GooLeNet
网络结构
什么是最好的卷积层超参数,不知道,所以把各种卷积合并到一起,提出了 Inception 块。 GooLeNet 全局平均汇聚层相当于提取出特征,不用象 NiN 那样强行构造和分类数一样的输出通道,看完的感觉:炼丹技术又提高了。
代码
python
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)
# 第一个模块使用64个通道、7x7 卷积层
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))
# 第二个模块使用两个卷积层:第一个卷积层是64个通道、1x1 卷积层;第二个卷积层使用将通道数量增加三倍的 3x3 卷积层。
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))
"""
第三个模块串联两个完整的Inception块。第一个inception块的输出通道数为 64+128+32+32=256, 四个路径之间的输出通道数量
比为64: 128: 32: 32=2: 4: 1: 1。第二个和第三个路径首先将输入通道的数量分别减少到 96/192=1/2和 16/192=1/12,
然后连接第二个卷积层。第二个inception块的输出通道数增 加到 $128+192+96+64=480$, 四个路径之间的输出通道数量比为
128: 192: 96: 64=4: 6: 3: 2。第二条和第三条路径首先将输入通道的数量分别减少到 128/256=1/2和 32/256=1/8。
"""
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))
"""
第四模块更加复杂, 它串联了5个Inception块, 其输出通道数分别是192+208+48+64=512、160+224+64+64=512、128+256+64+64=512
、112+288+64+64=528 和 256+320+128+128=832。其中第二、第 三条路径都会先按比例减小通道数。这些比例在各个inception块中都略有不同。
"""
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))
"""
第五模块包含输出通道数为 256+320+128+128=832 和384+384+128+128=1024 的两个Inception块。
其中每条路径通道数的分配思路和第三、第四模块中的一致, 只是在具体数值上有所不同。
需要注意的是, 第五模块的后面紧跟输出层, 该模块同 NiN 一样使用全局平均汇聚层, 将每个通道的高和宽变成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))
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())
批量归一化
Inception-v2 使用了 Batch Normalization 训练深层神经网络是十分困难的(因为梯度消失的问题,使得越深层的网络训练越慢),特别是在较短的时间内使他们收敛更加棘手。 这是一种流行且有效的技术,可持续加速深层网络的收敛速度。 再结合 ResNet 里的残差块,批量归一化使得研究人员能够训练100层以上的网络。 可学习的参数是 gamma 和 beta。 对于全连接层作用在特征维,对于卷积层作用在通道维(1x1 卷积那个通道上)。 简而言之,有特征输入的地方,都可以用到批量归一化,让特征变化不要太过于剧烈,从而加速了模型收敛。 gamma 和 beta 的学习减小的特征的随机性和偏移,所以也可以缓解过拟合,增加了泛化性,所以一般 BN 不和 丢弃法混用。
python
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
ResNet
网络结构
网络越深,拟合的函数越复杂,怎么保证添加的网络结构不对原有网络结构的效果造成损害呢?何恺明等人提出了残差网络。残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。 于是,残差块(residual blocks)便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。 其实直觉上理解很简单:f(x) = x + f(x)。所以每次添加新结构 f,输入都包含原始的输入 x 和之前结构的输出 f。 ResNet-18
代码
python
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)
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):
# 后面的 3 个块,只有每个块的第一个残差块使用 1x1 卷积
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))
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())