动手学深度学习——稠密连接网络DenseNet(原理解释+代码详解)

CIFAR 和 SVHN 数据集上的错误率 (%)。DenseNet 比 ResNet 使用更少的参数,同时实现了更低的错误率。在没有数据增强的情况下,DenseNet 的性能大幅提高。
在这里插入图片描述

1. 从ResNet到DenseNet

稠密连接网络在某种程度上是ResNet的逻辑扩展。
回想一下任意函数的泰勒展开式,它把这个函数分解成越来越高阶的项。在x接近0时,
在这里插入图片描述
ResNet将函数展开为
在这里插入图片描述
ResNet将 f 分解为两部分:一个简单的线性项和一个复杂的非线性项。
那么再向前拓展一步,如果我们想将 f 拓展成超过两部分的信息呢? 一种方案便是DenseNet,使用连结。
在这里插入图片描述
执行从x到其展开式的映射
在这里插入图片描述
最后,将这些展开式结合到多层感知机中,再次减少特征的数量。
在这里插入图片描述
稠密网络主要由2部分构成:

  • 稠密块(dense block):定义如何连接输入和输出
  • 过渡层(transition layer):控制通道数量,使其不会太复杂。

2. 稠密块体

稠密块每一层都将所有前面的特征图作为输入
在这里插入图片描述
DenseNet使用了ResNet改良版的“批量规范化、激活和卷积”架构

# 泰勒公式
"""
稠密网络:
        1、稠密块:定义如果连接输入和输出
        2、过渡层:后者控制通道数
"""
import torch
from torch import nn
from d2l import torch as d2l


def conv_block(input_channels, num_channels):
    return nn.Sequential(
        nn.BatchNorm2d(input_channels), nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))

一个稠密块由多个卷积块组成,每个卷积块使用相同数量的输出通道。在前向传播中,将每个卷积块的输入和输出在通道维上连结。

# 稠密块
class DenseBlock(nn.Module):
    def __init__(self, num_convs, input_channels, num_channels):
        super(DenseBlock, self).__init__()
        layer = []
        for i in range(num_convs):
            # 稠密连接:执行从x到其展开式的映射,即每个卷积块的输入和输出在通道维度上连接
            layer.append(conv_block(i * num_channels + input_channels, num_channels))
        self.net = nn.Sequential(*layer)
    
    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            # 连接通道维度上每个卷积块的输入和输出
            X = torch.cat((X, Y), dim=1)
        return X

定义一个有2个输出通道数为10的DenseBlock。 使用通道数为3的输入时,我们会得到通道数为3+2x10=23的输出。

卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为增长率(growth rate)。

# 定义有2个输出通道为10的DenseBlock
# 使用通道数为3的输入,会得到3+2x10=23
# 卷积块的通道数控制了输出通道数相对于输入通道数的增长程度,因此被称为增长率
blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
Y.shape

在这里插入图片描述

3. 过渡层

每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。

过渡层可以用来控制模型复杂度,它通过1x1卷积层来减小通道数,并使用步幅为2的平均汇聚层减半高和宽,从而进一步降低模型复杂度。

# 过渡层:用来控制模型复杂度
# 1x1卷积层减少通道数,使用步幅为2的平均汇聚层减半高度和宽度
def transition_block(input_channels, num_channels):
    return nn.Sequential(
        nn.BatchNorm2d(input_channels), nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=1),
        nn.AvgPool2d(kernel_size=2, stride=2))

对于上一个例子,将通道数从23变为10

# 对于上一个例子,将通道数从23变为10
blk = transition_block(23, 10)
blk(Y).shape

在这里插入图片描述

4. DenseNet模型

DenseNet首先使用同ResNet一样的单卷积层和最大汇聚层

# 构建DenseNet
# 首先,使用和ResNet一样的卷积层和最大汇聚层
b1 = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
    nn.BatchNorm2d(64), nn.ReLU(),
    nn.AvgPool2d(kernel_size=3, stride=2, padding=1))

DenseNet使用4个稠密块,稠密块里的卷积层通道数(即增长率)设为32,所以每个稠密块将增加128个通道。

在每个模块之间,ResNet通过步幅为2的残差块减小高和宽,DenseNet则使用过渡层来减半高和宽,并减半通道数。

"""
DenseNet使用4个稠密块:
1、稠密块里的卷积层通道数(即增长率)设为32,每个稠密块将增加4x32=128个通道
2、每个模块之间,DenseNet使用过渡层减半高度和宽度,并减半通道数
"""
# num_channels为当前的通道数
# growth_rate为增长率,即输出通道相对于输入通道的增长程度
num_channels, growth_rate = 64, 32
# 4个稠密块,每个稠密块4个卷积层,每个稠密块增加4x32=128个通道
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks):
    blks.append(DenseBlock(num_convs, num_channels, growth_rate))
    # 上一个稠密块的输出通道数
    num_channels += num_convs * growth_rate
    # 在稠密块之间添加一个过渡层,使通道数减半
    if i != len(num_convs_in_dense_blocks) - 1:
        blk.append(transition_block(num_channels, num_channels // 2))
        num_channels // 2

最后接上全局汇聚层和全连接层来输出结果。

# 最后连接全局汇聚层和全连接层来输出结果
net = nn.Sequential(
    b1, *blks,
    nn.BatchNorm2d(num_channels), nn.ReLU(),
    nn.AdaptiveAvgPool2d((1, 1)),
    nn.Flatten(),
    nn.Linear(num_channels, 10))

5. 训练模型

定义精度评估函数

"""
    定义精度评估函数:
    1、将数据集复制到显存中
    2、通过调用accuracy计算数据集的精度
"""
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    # 判断net是否属于torch.nn.Module类
    if isinstance(net, nn.Module):
        net.eval()
        
        # 如果不在参数选定的设备,将其传输到设备中
        if not device:
            device = next(iter(net.parameters())).device
    
    # Accumulator是累加器,定义两个变量:正确预测的数量,总预测的数量。
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            # 将X, y复制到设备中
            if isinstance(X, list):
                # BERT微调所需的(之后将介绍)
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            
            # 计算正确预测的数量,总预测的数量,并存储到metric中
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

定义GPU训练函数

"""
    定义GPU训练函数:
    1、为了使用gpu,首先需要将每一小批量数据移动到指定的设备(例如GPU)上;
    2、使用Xavier随机初始化模型参数;
    3、使用交叉熵损失函数和小批量随机梯度下降。
"""
#@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)
    
    # 在设备device上进行训练
    print('training on', device)
    net.to(device)
    
    # 优化器:随机梯度下降
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    
    # 损失函数:交叉熵损失函数
    loss = nn.CrossEntropyLoss()
    
    # Animator为绘图函数
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    
    # 调用Timer函数统计时间
    timer, num_batches = d2l.Timer(), len(train_iter)
    
    for epoch in range(num_epochs):
        
        # Accumulator(3)定义3个变量:损失值,正确预测的数量,总预测的数量
        metric = d2l.Accumulator(3)
        net.train()
        
        # enumerate() 函数用于将一个可遍历的数据对象
        for i, (X, y) in enumerate(train_iter):
            timer.start() # 进行计时
            optimizer.zero_grad() # 梯度清零
            X, y = X.to(device), y.to(device) # 将特征和标签转移到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)}')

在这里插入图片描述

由于模型较深,这里将输入高度和宽度从224降为96

# 训练模型
# 由于模型较深,这里将输入高度和宽度从224降为96
lr, num_epochs, batch_size = 0.1, 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())

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值