第七周周报:动手深度学习(二)

目录

摘要

Abstract

一、深度学习计算

1.1 模型构造

1.1.1 继承Module类来构造模型

1.1.2 Module的子类来构造模型

1.1.3 构造复杂的模型

1.2 模型参数的访问、初始化和共享

1.3 自定义层

1.4 读取和存储

二、卷积神经网络

2.1 二维卷积层

2.2 填充和步幅

2.3 多输入通道和多输出通道

2.4 池化层

2.5 卷积神经网络(LeNet)

2.6 深度卷积神经网络(AlexNet)

2.7 使用重复元素的网络(VGG)

2.8 网络中的网络(NiN)

2.9 含并行连结的网络(GoogLeNet)

2.10 批量归一化

2.11 残差网络(ResNet)

2.12 稠密连接网络(DenseNet)

总结


摘要

本周跟着李沐老师的动手深度学习课程,主要学习了使用PyTorch实现深度学习的计算和卷积神经网络。详细学习了深度学习模型的构造、参数的访问初始化、自定义网络层等,以及卷积神经网络的整个发展历程,从最简单的二维卷积网络,到深度卷积网络,再到后面的一些变型的卷积网络。每一次的创新,都有助于卷积神经网络更好的拟合数据,减轻训练难度,得到更好的测试效果。

Abstract

This week, I followed Professor Li Mu's hands-on deep learning course and mainly learned about using PyTorch to implement deep learning computation and convolutional neural networks. I have studied in detail the construction of deep learning models, parameter access initialization, custom network layers, and the entire development process of convolutional neural networks, from the simplest two-dimensional convolutional network to deep convolutional networks, and then to some later variants of convolutional networks. Every innovation helps convolutional neural networks better fit data, reduce training difficulty, and achieve better testing results.

一、深度学习计算

1.1 模型构造

1.1.1 继承Module类来构造模型

Module类是nn模块里提供的一个模型构造类,是所有神经网络模块的基类,我们可以继承它来定义模型。自定义的类需重载Module类的__init__函数和forward函数,分别用于创建模型参数和定义前向计算(正向传播)。

直接继承Module类来构建模型,灵活性更好。

import torch
from torch import nn
from torch.nn import Sequential

class MLP(nn.Module):  # 构造多层感知机
    # 声明带有模型参数的层,这里声明了两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Module的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Linear(784, 256)  # 隐藏层
        self.act = nn.ReLU()  # 激活函数
        self.output = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        a = self.act(self.hidden(x))
        return self.output(a)

X = torch.rand(2, 784)
net = MLP()
print(net)
print(net(X))  # 2X10

代码运行结果如下:

1.1.2 Module的子类来构造模型
  • Sequential类

它可以接收一个子模块的有序字典或一系列子模块作为参数来逐一添加Module的实例,而模型的前向计算就是将这些实例按添加的顺序逐一计算。

net = Sequential(
        nn.Linear(784, 256),
        nn.ReLU(),
        nn.Linear(256, 10),
        )
print(net)
print(net(X))

代码运行结果如下:

  • ModuleList类

ModuleList接收一个子模块的列表作为输入,然后也可以类似List那样进行append和extend操作。

# 加入到ModuleList里面的所有模块的参数会被自动添加到整个网络中
net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
net.append(nn.Linear(256, 10))  # # 类似List的append操作,添加
print(net[-1])  # 类似List的索引访问
# ModuleList仅仅是一个储存各种模块的列表,这些模块之间没有联系也没有顺序,而且没有实现forward功能需要自己实现;而Sequential内的模块需要按照顺序排列,要保证相邻层的输入输出大小相匹配,内部forward功能已经实现。

代码运行结果如下:

  • ModuleDict类

 ModuleDict接收一个子模块的字典作为输入,然后也可以类似字典那样进行添加访问操作。

net = nn.ModuleDict({
    'linear': nn.Linear(784, 256),
    'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10) # 添加
print(net['linear'])  # 访问
print(net.output)
print(net)

代码运行结果如下:

1.1.3 构造复杂的模型

虽然上面介绍的这些类可以使模型构造更加简单,且不需要定义forward函数,但直接继承Module类可以极大地拓展模型构造的灵活性。在前向计算中,除了使用创建的常数参数外,我们还使用Tensor的函数和Python的控制流,并多次调用相同的层。 

# 构造复杂网络,可以嵌套使用Sequential
class FancyMLP(nn.Module):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)
        self.rand_weight = torch.rand((20, 20), requires_grad=False)  # 不可训练参数(常数参数)
        self.linear = nn.Linear(20, 20)

    def forward(self, x):
        x = self.linear(x)
        # 使用创建的常数参数,以及nn.functional中的relu函数和mm函数
        x = nn.functional.relu(torch.mm(x, self.rand_weight.data) + 1)

        # 复用全连接层。等价于两个全连接层共享参数
        x = self.linear(x)
        # 控制流,这里我们需要调用item函数来返回标量进行比较
        while x.norm().item() > 1:
            x /= 2
        if x.norm().item() < 0.8:
            x *= 10
        return x.sum()

class NestMLP(nn.Module):
    def __init__(self, **kwargs):
        super(NestMLP, self).__init__(**kwargs)
        self.net = nn.Sequential(nn.Linear(40, 30), nn.ReLU())

    def forward(self, x):
        return self.net(x)

net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())  # 嵌套使用,都是继承的Module类

X = torch.rand(2, 40)
print(net)
print(net(X))

代码运行结果如下:

1.2 模型参数的访问、初始化和共享

本节主要通过代码展示如何访问和初始化模型参数,以及如何在多个层之间共享同一份模型参数。

import torch
from torch import nn
from torch.nn import init

print("构建模型")
net = nn.Sequential(
    nn.Linear(4, 3),
    nn.ReLU(),
    nn.Linear(3, 1)
)  # pytorch已进行默认初始化
print(net)
X = torch.rand(2, 4)
print(net(X).sum())

# 访问模型参数
print("访问模型参数")
print(type(net.named_parameters()))
for name, param in net.named_parameters():
    print(name, param.size())
# 对于使用Sequential类构造的神经网络,我们可以通过方括号[]来访问网络的任一层
for name, param in net[0].named_parameters():
    print(name, param.size(), type(param))
# 如果一个Tensor是Parameter,它会自动被添加到模型的参数列表里

# 初始化模型参数
print("初始化模型参数")
for name, param in net.named_parameters():
    if 'weight' in name:  # 初始化权重参数为(0,0.01)的正态分布
        init.normal_(param, mean=0, std=0.01)
    if 'bias' in name:  # 初始化权重参数为常数
        init.constant_(param, val=0)
    print(name, param.data)

# 自定义初始化
print("自定义初始化")
def init_weight_(tensor):
    with torch.no_grad():
        tensor.uniform_(-10, 10)  # (-10,10)均匀分布
        tensor *= (tensor.abs() >= 5).float()  # 绝对值大于等于5为1,反之为0

for name, param in net.named_parameters():
    if 'weight' in name:
        init_weight_(param)
        print(name, param.data)

# 共享模型参数,传入Sequential的模块是同一个Module实例的话参数也是共享的
print("共享模型参数")
linear = nn.Linear(1, 1, bias=False)
net = nn.Sequential(linear, linear)
for name, param in net.named_parameters():
    init.constant_(param, val=3)
    print(name, param.data)

代码运行结果如下:

1.3 自定义层

import torch
from torch import nn

# ------------------------------------不含模型参数的自定义层-------------------------------------
class CenteredLayer(nn.Module):
    def __init__(self, **kwargs):
        super(CenteredLayer, self).__init__(**kwargs)
    def forward(self, x):
        return x - x.mean()  # 每个值减去均值

layer = CenteredLayer()
print(layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)))

# 构造更复杂的函数
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
print(net(torch.rand(4, 8)))


# ------------------------------------含模型参数的自定义层-------------------------------------
# Parameter类其实是Tensor的子类,如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里
# ParameterList接收一个Parameter实例的列表作为输入然后得到一个参数列表,使用的时候可以用索引来访问某个参数,另外也可以使用append和extend在列表后面新增参数
class MyListDense(nn.Module):
    def __init__(self):
        super(MyListDense, self).__init__()
        self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])  # 生成3个4x4的矩阵
        self.params.append(nn.Parameter(torch.randn(4, 1)))

    def forward(self, x):
        for i in range(len(self.params)):
            x = torch.mm(x, self.params[i])
        return x
net = MyListDense()
print(net)

# ParameterDict接收一个Parameter实例的字典作为输入然后得到一个参数字典,然后可以按照字典的规则使用
class MyDictDense(nn.Module):
    def __init__(self):
        super(MyDictDense, self).__init__()
        self.params = nn.ParameterDict({
                'linear1': nn.Parameter(torch.randn(4, 4)),
                'linear2': nn.Parameter(torch.randn(4, 1))
        })
        self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))})  # 新增

    def forward(self, x, choice='linear1'):
        return torch.mm(x, self.params[choice])  # 选定和某一层做计算

net = MyDictDense()
print(net)

x = torch.ones(1, 4)
print(net(x, 'linear1'))
print(net(x, 'linear2'))
print(net(x, 'linear3'))

# 还可以嵌套使用
net = nn.Sequential(
    MyDictDense(),
    MyListDense(),
)
print(net)
print(net(x))

代码运行结果如下:

1.4 读取和存储

在实际中,有时需要把训练好的模型部署到很多不同的设备。在这种情况下,可以把内存中训练好的模型参数存储在硬盘上供后续读取使用。

import torch
from torch import nn

x = torch.ones(3)
torch.save(x, './data/Read&Store.pt')  # 存入
x2 = torch.load('./data/Read&Store.pt')  # 读取
print(x2)

y = torch.zeros(4)
torch.save({'x': x, 'y': y}, './data/Read&Store.pt')
xy = torch.load('./data/Read&Store.pt')
print(xy)

class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        self.hidden = nn.Linear(3, 2)
        self.act = nn.ReLU()
        self.output = nn.Linear(2, 1)

    def forward(self, x):
        a = self.act(self.hidden(x))
        return self.output(a)

net = MLP()
print(net.state_dict())  # state_dict是一个从参数名称隐射到参数Tesnor的字典对象,只有具有可学习参数的层(卷积层、线性层等)才有state_dict中的条目

# 优化器(optim)也有一个state_dict,其中包含关于优化器状态以及所使用的超参数的信息
optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
print(optimizer.state_dict())

# 保存和加载模型
X = torch.randn(2, 3)
Y = net(X)
PATH = "./data/Read&Store.pt"
torch.save(net.state_dict(), PATH)  # 保存state_dict
net2 = MLP()
net2.load_state_dict(torch.load(PATH))  # 加载state_dict
Y2 = net2(X)
print(Y2)
# 保存和加载整个模型
# torch.save(model, PATH)
# model = torch.load(PATH)

代码运行结果如下:

二、卷积神经网络

CNN理论知识可移步这篇博客

2.1 二维卷积层

二维即只有高和宽两个空间维度,常用来处理图像数据。

卷积层的核心就在于二维互相关运算,如下图所示。即卷积核对输入进行特征的提取。

import torch
from torch import nn

def corr2d(X, K):  # 本函数已保存在d2lzh_pytorch包中方便以后使用
    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, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
print('corr2d:', corr2d(X, K))

# corr2d函数来实现一个自定义的二维卷积层
class Conv2D(nn.Module):
    def __init__(self, kernel_size):  # kernel_size卷积核大小
        super(Conv2D, self).__init__()
        self.weight = nn.Parameter(torch.randn(kernel_size))
        self.bias = nn.Parameter(torch.randn(1))

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

# 图像中物体边缘检测,即找到像素变化的位置
X = torch.ones(6, 8)
X[:, 2:6] = 0
print('X:', X)
# 构造一个高和宽分别为1和2的卷积核K
K = torch.tensor([[1, -1]])
Y = corr2d(X, K)  # 如果横向相邻元素相同为0,否则为非0
print('Y:', Y)

# 通过数据学习核数组
# 构造一个核数组形状是(1, 2)的二维卷积层
c2d = Conv2D(kernel_size=(1, 2))
step = 20
lr = 0.01
for i in range(step):
    Y_hat = c2d(X)
    l = ((Y_hat - Y) ** 2).sum()  # Y为直接计算的真实值
    l.backward()

    # 梯度下降
    c2d.weight.data -= lr * c2d.weight.grad
    c2d.bias.data -= lr * c2d.bias.grad
    # 梯度清0
    c2d.weight.grad.fill_(0)
    c2d.bias.grad.fill_(0)
    if (i + 1) % 5 == 0:
        print('Step %d, loss %.3f' % (i + 1, l.item()))

print("weight: ", c2d.weight.data)
print("bias: ", c2d.bias.data)

 代码运行结果如下:

2.2 填充和步幅

  • 填充

填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。如下图所示,在高和宽上分别填充1行和1列元素,在进行卷积提取特征。

  • 步幅

卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。将每次滑动的行数和列数称为步幅(stride)。如下图所示,展示在高上步幅为3、在宽上步幅为2的二维互相关运算。

代码会在下文中展现。 

2.3 多输入通道和多输出通道

  • 多输入通道

当输入数据含多个通道时,需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。如下图所示:

  • 多输出通道

当输入通道有多个时,因为我们对各个通道的结果做了累加,所以不论输入通道数是多少,输出通道数总是为1。所以需要增加卷积核的个数,以达到需要的输出通道数。

import torch
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

# 多输入通道
# 当输入数据含多个通道时,我们需要构造一个输入通道数与输入数据的通道数相同的卷积核
def corr2d_multi_in(X, K):
    # 沿着X和K的第0维(通道维)分别计算再相加
    res = d2l.corr2d(X[0, :, :], K[0, :, :])
    for i in range(1, X.shape[0]):
        res += d2l.corr2d(X[i, :, :], K[i, :, :])
    return res  # 输出为2维tensor,通道数为1

X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]])  # 2x2x2卷积核
print(corr2d_multi_in(X, K))

# 互相关运算函数来计算多个通道的输出
def corr2d_multi_in_out(X, K):
    # 对K的第0维遍历,每次同输入X做互相关计算。所有结果使用stack函数合并在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K])

K = torch.stack([K, K + 1, K + 2])  # 3个2x2x2的卷积核叠加(torch.stack)
print(K)
print(corr2d_multi_in_out(X, K))

 代码运行结果如下:

2.4 池化层

池化(pooling)层的提出是为了缓解卷积层对位置的过度敏感性。池化层每次对输入数据的一个固定形状窗口中的元素计算输出,直接计算池化窗口内元素的最大值或者平均值。

池化层对每个输入通道分别池化,而不是像卷积层那样将各通道的输入按通道相加。意味着池化层的输出通道数与输入通道数相等

2x2最大池化层如下图所示:

# 池化层,为了缓解卷积层对位置的过度敏感性
import torch
from torch import nn

# 二维最大池化层和平均池化层
def pool2d(X, pool_size, mode='max'):  # 最大池化层
    X = X.float()
    p_h, p_w = pool_size
    Y = torch.zeros(X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)  # 输出的维度,2x2
    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, 1, 2], [3, 4, 5], [6, 7, 8]])
print(pool2d(X, (2, 2)))
print(pool2d(X, (2, 2), 'avg'))  # 平均池化层



# 填充和步幅
X = torch.arange(16, dtype=torch.float).view((1, 1, 4, 4))  # 1个4x4的二维tensor
Pool2d = nn.MaxPool2d(3)  # nn.MaxPool2d():输入整数或一个元组 (h, w),h是池化窗口的高,w是池化窗口的宽;如整数,则高度和宽度相同
print(Pool2d(X))

Pool2d = nn.MaxPool2d(3, padding=1, stride=2)  # 填补为1,步幅为2
print(Pool2d(X))
# 也可以指定非正方形的池化窗口,并分别指定高和宽上的填充和步幅
Pool2d = nn.MaxPool2d((2, 4), padding=(1, 2), stride=(2, 3))
print(Pool2d(X))



# 多通道输入
# 池化层对每个输入通道分别池化,而不是像卷积层那样将各通道的输入按通道相加。池化层的输出通道数=输入通道数
X = torch.cat((X, X + 1), dim=1)
print(X)

Pool2d = nn.MaxPool2d(3, padding=1, stride=2)
print(Pool2d(X))

 代码运行结果如下:

2.5 卷积神经网络(LeNet)

LeNet分为卷积层块和全连接层块两个部分,如下图所示:

卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使用5×5的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6,第二个卷积层输出通道数则增加到16。这是因为第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使两个卷积层的参数尺寸类似。卷积层块的两个最大池化层的窗口形状均为2×2,且步幅为2。由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域互不重叠。

卷积层块的输出形状为(批量大小, 通道, 高, 宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。也就是说,全连接层的输入形状将变成二维,其中第一维是小批量中的样本,第二维是每个样本变平后的向量表示,且向量长度为通道、高和宽的乘积。全连接层块含3个全连接层。它们的输出个数分别是120、84和10,其中10为输出的类别个数。

import time
import torch
from torch import nn
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv = nn.Sequential(  # 卷积层
            nn.Conv2d(1, 6, 5),  # 输入通道数为1,5x5的窗口
            nn.Sigmoid(),  # 激活函数
            nn.MaxPool2d(2, 2),
            # 第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使两个卷积层的参数尺寸类似
            nn.Conv2d(6, 16, 5),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2)
        )
        # 卷积层由于使用高和宽均为5的卷积核,从而将高和宽分别减小4,而池化层则将高和宽减半,但通道数则从1增加到16。

        self.fc = nn.Sequential(  # 全连接层
            nn.Linear(16*4*4, 120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output
net = LeNet()

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

def evaluate_accuracy(data_iter, net, device=None):
    if device is None and isinstance(net, torch.nn.Module):
        # 如果没指定device就使用net的device
        device = list(net.parameters())[0].device
    acc_sum, n = 0.0, 0
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(net, torch.nn.Module):
                net.eval()  # 评估模式, 这会关闭dropout
                acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
                net.train()  # 改回训练模式
            else:  # 自定义的模型, 3.13节之后不会用到, 不考虑GPU
                if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
                    # 将is_training设置成False
                    acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
                else:
                    acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
            n += y.shape[0]
    return acc_sum / n

def train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs):
    net = net.to(device)
    print("training on ", device)
    loss = torch.nn.CrossEntropyLoss()
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n, batch_count, start = 0.0, 0.0, 0, 0, time.time()
        for X, y in train_iter:
            X = X.to(device)
            y = y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            optimizer.zero_grad()  # 梯度清零
            l.backward()
            optimizer.step()  # 梯度更新
            train_l_sum += l.cpu().item()
            # y_hat的每一行代表了一个样本在各个类别上的预测概率,argmax(dim=1)的结果为模型预测的每个样本的类别索引,这个操作的输出形状为[batch_size]
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()  # .sum()返回一个标量张量;.item()用于获取这个张量中的值
            n += y.shape[0]
            batch_count += 1
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec' % (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

 代码运行结果如下:

2.6 深度卷积神经网络(AlexNet)

AlexNet网络结构,如下图所示: 

AlexNet与LeNet的设计理念非常相似,但也有显著的区别。

第一,与相对较小的LeNet相比,AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。下面我们来详细描述这些层的设计。AlexNet第一层中的卷积窗口形状是11×11。第一、第二和第五个卷积层之后都使用了窗口形状为3×3、步幅为2的最大池化层。而且,AlexNet使用的卷积通道数也大于LeNet中的卷积通道数数十倍。紧接着最后一个卷积层的是两个输出个数为4096的全连接层。这两个巨大的全连接层带来将近1GB的模型参数。

第二,AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,例如它并没有sigmoid激活函数中的求幂运算。另一方面,ReLU激活函数在不同的参数初始化方法下使模型更容易训练。这是由于当sigmoid激活函数输出极接近0或1时,这些区域的梯度几乎为0,从而造成反向传播无法继续更新部分模型参数;而ReLU激活函数在正区间的梯度恒为1。因此,若模型参数初始化不当,sigmoid函数可能在正区间得到几乎为0的梯度,从而令模型无法得到有效训练。

第三,AlexNet通过丢弃法来控制全连接层的模型复杂度。而LeNet并没有使用丢弃法。

第四,AlexNet引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。

# AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层
import torch
from torch import nn
import torchvision
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(  # AlexNet能够捕捉图像中更深层次的特征,提高了模型对复杂图像的识别能力;卷积窗口从11x11、5x5、到3x3
            nn.Conv2d(1, 96, 11, 4),  # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2),  # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),  # 丢弃法,50%概率的丢弃
            nn.Linear(4096, 4096),  # 两个巨大的全连接层带来将近1GB的模型参数
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

net = AlexNet()
print(net)

# 将图像高和宽扩大到AlexNet使用的图像高和宽224
def load_data_fashion_mnist(batch_size, resize=None):
    """Download the fashion mnist dataset and then load into memory."""
    trans = []
    if resize:
        trans.append(torchvision.transforms.Resize(size=resize))  # .Resize进行缩放操作
    trans.append(torchvision.transforms.ToTensor())  # 将其转化为tensor,并加入trans[]

    transform = torchvision.transforms.Compose(trans)  # 将trans[]中的所有转换操作组合成一个转换流程
    mnist_train = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform)
    mnist_test = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform)

    train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True)  # num_workers线程
    test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False)

    return train_iter, test_iter

batch_size = 128
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

2.7 使用重复元素的网络(VGG)

VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为3×3 3×3的卷积层后接上一个步幅为2、窗口形状为2×2的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。

VGG网络有5个卷积块,前2块使用单卷积层,而后3块使用双卷积层。第一块的输入输出通道分别是1(因为使用Fashion-MNIST数据的通道数为1)和64,之后每次对输出通道数翻倍,直到变为512。

# VGG:连续使用数个相同的填充为1、窗口形状为3×3的卷积层后接上一个步幅为2、窗口形状为2×2的最大池化层。
# 卷积层保持输入的高和宽不变,而池化层则对其减半
import time
import torch
from torch import nn, optim
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def vgg_block(num_convs, in_channels, out_channels):
    blk = []
    for i in range(num_convs):
        if i == 0:  # 只第一层改变通道数量
            blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
        else:
            blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
        blk.append(nn.ReLU())
    blk.append(nn.MaxPool2d(kernel_size=2, stride=2))  # 这里会使宽高减半
    return nn.Sequential(*blk)

# 前2块使用单卷积层,而后3块使用双卷积层
conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256), (2, 256, 512), (2, 512, 512))  # 储存每层信息
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 = 7
fc_features = 512 * 7 * 7  # c * w * h
fc_hidden_units = 4096  # 任意

def vgg(conv_arch, fc_features, fc_hidden_units=4096):
    net = nn.Sequential()
    # 卷积层部分
    for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
        # 每经过一个vgg_block都会使宽高减半
        net.add_module("vgg_block_" + str(i+1), vgg_block(num_convs, in_channels, out_channels))

    net.add_module("fc", nn.Sequential(   # 全连接层
                                 d2l.FlattenLayer(),
                                 nn.Linear(fc_features, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, 10)
                                ))
    return net

net = vgg(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)

# named_children获取一级子模块及其名字(named_modules会返回所有子模块,包括子模块的子模块)
for name, blk in net.named_children():
    X = blk(X)
    print(name, 'output shape: ', X.shape)  # 输出(样本,通道,高,宽)

ratio = 8
small_conv_arch = [(1, 1, 64//ratio), (1, 64//ratio, 128//ratio), (2, 128//ratio, 256//ratio), (2, 256//ratio, 512//ratio), (2, 512//ratio, 512//ratio)]
net = vgg(small_conv_arch, fc_features//ratio, fc_hidden_units//ratio)
print(net)

batch_size = 64
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

2.8 网络中的网络(NiN)

卷积层的输入和输出通常是四维数组(样本,通道,高,宽),而全连接层的输入和输出则通常是二维数组(样本,特征)。如果想在全连接层后再接上卷积层,则需要将全连接层的输出变换为四维。1×1卷积层可以看成全连接层,其中空间维度(高和宽)上的每个元素相当于样本,通道相当于特征。因此,NiN使用1×1卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去。

NIN模型是由NIN块组成,NIN块由一个卷积层加两个充当全连接层的1×1卷积层串联而成。其中第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的。

NiN模型网络结构局部,如下图所示:

# NiN:串联多个由卷积层和“全连接”层构成的小网络来构建一个深层网络
# 卷积层的输入和输出通常是四维数组(样本,通道,高,宽),而全连接层的输入和输出则通常是二维数组(样本,特征)
# NiN使用1×1卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去
import time
import torch
from torch import nn, optim
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
import torch.nn.functional as F

# NiN块是NiN中的基础块。它由一个卷积层加两个充当全连接层的1×1卷积层串联而成
# 其中第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
    blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, 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())
    return blk

class GlobalAvgPool2d(nn.Module):
    # 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
    def __init__(self):
        super(GlobalAvgPool2d, self).__init__()
    def forward(self, x):
        return F.avg_pool2d(x, kernel_size=x.size()[2:])  # x.size()[2:]即x的长宽

net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, stride=4, padding=0),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(96, 256, kernel_size=5, stride=1, padding=2),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(256, 384, kernel_size=3, stride=1, padding=1),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, stride=1, padding=1),
    GlobalAvgPool2d(),  # 使用全局平均池化层对每个通道中所有元素求平均并直接用于分类
    d2l.FlattenLayer())  # 扁平化:将四维的输出转成二维的输出,其形状为(批量大小, 10)

X = torch.rand(1, 1, 224, 224)
for name, blk in net.named_children():
    X = blk(X)
    print(name, 'output shape: ', X.shape)

batch_size = 128
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.002, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

 代码运行结果如下:

2.9 含并行连结的网络(GoogLeNet)

GoogLeNet中的基础卷积块叫作Inception块,如下图所示:

Inception块里有4条并行的线路。前3条线路使用窗口大小分别是1×1、3×3和5×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1×1卷积来减少输入通道数,以降低模型复杂度。第四条线路则使用3×3最大池化层,后接1×1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并输入接下来的层中去。 

import time
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# Inception块中可以自定义的超参数是每个层的输出通道数,以此来控制模型复杂度;通过不同窗口形状的卷积层和最大池化层来并行抽取信息
class Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):  # 4条线路都使用了合适的填充来使输入与输出的高和宽一致
        super(Inception, self).__init__()
        # 线路1,单1 x 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
        # 线路2,1 x 1卷积层后接3 x 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1 x 1卷积层后接5 x 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3 x 3最大池化层后接1 x 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)

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

# GoogLeNet跟VGG一样,在主体卷积部分中使用5个模块(block)
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))  # 第一个Inception块的输出通道数为64+128+32+32=256
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   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),
                   d2l.GlobalAvgPool2d())

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

X = torch.rand(1, 1, 224, 224)
for blk in net.children():
    X = blk(X)
    print('output shape: ', X.shape)

batch_size = 128
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

2.10 批量归一化

对于深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。

批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。

# 批量归一化层,标准化处理输入数据使各个特征的分布相近,能让较深的神经网络的训练变得更加容易
# 批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定
import torch
from torch import nn
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 使用批量归一化的LeNet
net = nn.Sequential(
            nn.Conv2d(1, 6, 5),  # in_channels, out_channels, kernel_size
            nn.BatchNorm2d(6),  # BatchNorm2d用于卷积层,需要对通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),  # kernel_size, stride
            nn.Conv2d(6, 16, 5),
            nn.BatchNorm2d(16),  # 16对于卷积层来说则为输出通道数,上下对应
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),
            d2l.FlattenLayer(),  # 扁平化
            nn.Linear(16*4*4, 120),
            nn.BatchNorm1d(120),  # BatchNorm1d用于全连接层的批量归一化
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.BatchNorm1d(84),  # 批量归一化在激活函数之前,网络层之后
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )

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

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

2.11 残差网络(ResNet)

理论上,原模型解的空间只是新模型解的空间的子空间。也就是说,如果我们能将新添加的层训练成恒等映射f(x)=x,新模型和原模型将同样有效。由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。然而在实践中,添加过多的层后训练误差往往不降反升。即使利用批量归一化带来的数值稳定性使训练深层模型更加容易,该问题仍然存在。于是提出了残差网络,残差网络的核心是残差块,如下图所示:

残差网络需要拟合出有关恒等映射的残差映射f(x)−x,残差映射在实际中往往更容易优化。 

# 添加过多的层后训练误差往往不降反升,于是提出残差网络
import time
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class Residual(nn.Module):  # 本类已保存在d2lzh_pytorch包中方便以后使用
    def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
        super(Residual, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            # 将输入跳过前两个卷积运算后直接加在最后的ReLU激活函数前
            self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_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)
        return F.relu(Y + X)

# ResNet后面接4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):  # 模块
    if first_block:
        assert in_channels == out_channels  # 第一个模块的通道数同输入通道数一致
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
        else:
            blk.append(Residual(out_channels, out_channels))
    return nn.Sequential(*blk)

net = 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))
# 每个模块使用两个残差块
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", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))

X = torch.rand((1, 1, 224, 224))
for name, layer in net.named_children():
    X = layer(X)
    print(name, ' output shape:\t', X.shape)

batch_size = 256
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

2.12 稠密连接网络(DenseNet)

与ResNet的主要区别在于,DenseNet里模块B的输出不是像ResNet那样和模块A的输出相加,而是在通道维上连结。这样模块A的输出可以直接传入模块B后面的层。相当于模块A直接跟模块B后面的所有层连接在了一起。如下图所示:

DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。 

# 与ResNet的主要区别在于,DenseNet(稠密连接)的输出不是像ResNet那样相加,而是在通道维上连结
import time
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def conv_block(in_channels, out_channels):
    blk = nn.Sequential(nn.BatchNorm2d(in_channels),   # 将输入归一化为in_channels的通道数
                        nn.ReLU(),
                        nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
    return blk

class DenseBlock(nn.Module):  # 稠密块由多个conv_block组成,每块使用相同的输出通道数
    def __init__(self, num_convs, in_channels, out_channels):
        super(DenseBlock, self).__init__()
        net = []
        for i in range(num_convs):
            in_c = in_channels + i * out_channels  # 随着i改变in_c的值
            net.append(conv_block(in_c, out_channels))
        self.net = nn.ModuleList(net)
        self.out_channels = in_channels + num_convs * out_channels  # 计算输出通道数

    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            X = torch.cat((X, Y), dim=1)  # 在通道维上将输入和输出连结
        return X

# 由于每个稠密块都会带来通道数的增加,使用过多则会带来过于复杂的模型。
# 过渡层用来控制模型复杂度,通过1×1卷积层来减小通道数,并使用步幅为2的平均池化层减半高和宽,从而进一步降低模型复杂度。
def transition_block(in_channels, out_channels):
    blk = nn.Sequential(
            nn.BatchNorm2d(in_channels),
            nn.ReLU(),
            nn.Conv2d(in_channels, out_channels, kernel_size=1),
            nn.AvgPool2d(kernel_size=2, stride=2))
    return blk

# 构造DenseNet模型
net = 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))

num_channels, growth_rate = 64, 32  # num_channels为当前的通道数,growth_rate稠密块里的卷积层通道数(即增长率)
num_convs_in_dense_blocks = [4, 4, 4, 4]

for i, num_convs in enumerate(num_convs_in_dense_blocks):
    DB = DenseBlock(num_convs, num_channels, growth_rate)  # 每个稠密块有4个conv_block块
    net.add_module("DenseBlosk_%d" % i, DB)
    # 上一个稠密块的输出通道数
    num_channels = DB.out_channels
    # 在稠密块之间加入通道数减半的过渡层
    if i != len(num_convs_in_dense_blocks) - 1:
        net.add_module("transition_block_%d" % i, transition_block(num_channels, num_channels // 2))
        num_channels = num_channels // 2

net.add_module("BN", nn.BatchNorm2d(num_channels))
net.add_module("relu", nn.ReLU())
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d())  # GlobalAvgPool2d的输出: (Batch, num_channels, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(num_channels, 10)))

X = torch.rand((1, 1, 96, 96))
for name, layer in net.named_children():
    X = layer(X)
    print(name, ' output shape:\t', X.shape)

batch_size = 256
# 如出现“out of memory”的报错信息,可减小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

代码运行结果如下:

总结

本周的学习到此结束,主要通过代码具体了解了卷积神经网络由易到难的一个发展过程,以及通过PyTorch进行训练实现。下周将继续学习优化算法、计算性能的章节。

如有错误,请各位大佬指出,谢谢! 

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值