深度强化学习(DRL)算法 附录 5 —— CV 基础回顾篇

因为强化学习里面有些状态是图片表示的,需要用 CNN 抽取信息,所以对 CNN 要有基础的了解。

卷积层

原理

1200 万像素,RGB 3通道图片 3600万元素,使用 FC(全连接) size 100 的隐藏层,模型 36 亿元素,所以需要新的模型表征图片。
新模型需要一些特性:平移不变性和局部性,平移和局部抽取都不改变图片特性,把 FC 从平移角度和局部抽取角度来设计新模型,就是卷积层,卷积是特殊的 FC 操作。
卷积核

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} hi,j=k,lwi,j,k,lxk,l=a,bvi,j,a,bxi+a,j+b
(从 FC 推出)
平移不变性:
v i , j , a , b = v a , b v_{i, j, a, b}=v_{a, b} vi,j,a,b=va,b

局部性: 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} hi,j=a=ΔΔb=ΔΔva,bxi+a,j+b  当  ∣ a ∣ , ∣ b ∣ > Δ  时, 使得  v a , b = 0 \text { 当 }|a|,|b|>\Delta \text { 时, 使得 } v_{a, b}=0   a,b>Δ 使得 va,b=0
所以 a,b 即卷积核的大小,这里比较抽象,可以看下参考第二个视频,比较好理解。
二维交叉相关:
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} yi,j=a=1hb=1wwa,bxi+a,j+b
(图片)
二维卷积:
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} yi,j=a=1hb=1wwa,bxi+a,j+b

实际上实现的是二维交叉相关,因为实现起来方便。同理有:
一维交叉相关:
y i = ∑ a = 1 h w a x i + a y_i=\sum_{a=1}^h w_a x_{i+a} yi=a=1hwaxi+a
(文本,时序序列)
三维交叉相关:
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} yi,j,k=a=1hb=1wc=1dwa,b,cxi+a,j+b,k+c
(视频,医学图像,气象地图)
可学习参数:卷积核矩阵 w 和 偏移 b
超参数:卷积核大小
填充和步幅
图片是有边缘的,为了考虑边缘信息,引入了填充概念。那么二维卷积后的输出为:

( 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) (nhkh+ph+1)×(nwkw+pw+1)p代表padding

一般情况下: p h = k h − 1  和  p w = k w − 1 p_h=k_h-1 \text { 和 } p_w=k_w-1 ph=kh1  pw=kw1 (保证输入输出的维度不改变),k 是奇数,这样可以对称填充 p 2 \frac{p}{2} 2p
还可以通过改变步幅(移动窗口的格数)加宽卷积速度,之前的步幅都是 1。那么二维卷积后的输出为:

⌊ ( 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 (nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw

多输入通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道( c i c_i ci)数的卷积核,以便与输入数据进行互相关运算。
多输出通道
输入数据具有相同输入通道数的卷积核 * c o c_o co c o c_o co 就是多输出通道的数目。
那么就有(batch = 1)
输入 X:
c i × n h × n w c_i \times n_h \times n_w ci×nh×nw

卷积核 W:
c o × c i × k h × k w c_o \times c_i \times k_h \times k_w co×ci×kh×kw

偏差 B:
c o × c i c_o \times c_i co×ci

输出 Y:
c o × m h × m w c_o \times m_h \times m_w co×mh×mw

池化层(汇聚层)
平均池化层和最大池化层,也有对应的填充和步幅。

代码

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

网络结构


代码

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

代码

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 块的数目。

代码

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

代码

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 那样强行构造和分类数一样的输出通道,看完的感觉:炼丹技术又提高了。

代码

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 不和 丢弃法混用。

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

代码

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

参考

https://www.bilibili.com/list/1567748478?sid=358497&spm_id_from=333.999.0.0&desc=0&oid=758101639&bvid=BV1L64y1m7Nh
从“卷积”、到“图像卷积操作”、再到“卷积神经网络”,“卷积”意义的3次改变_哔哩哔哩_bilibili
6.6. 卷积神经网络(LeNet) - 动手学深度学习 2.0.0 documentation

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿姆姆姆姆姆姆姆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值