DenseNet-稠密连接网络

 

核心:

  • 建立的是前面所有层与后面所有层的密集连接(dense connection),加强了前后层之间的信息流通,在一定程度上缓解了梯度消失现象,从而可以将神经网络搭建得很深
  • 通过特征在channel上的连接来实现特征重用(feature reuse)

DenseNet的前向过程

特征传递方式是直接将前面所有层的特征concat后传到下一层,而不是前面层都要有一个箭头指向后面的所有层

DenseBlock

CNN网络一般要经过Pooling或者stride>1的Conv来降低特征图的大小,而DenseNet的密集连接方式需要特征图大小保持一致。为了解决这个问题,DenseNet网络中使用DenseBlock+Transition的结构,

 

  • 其中DenseBlock是包含很多层的模块,每个层的特征图大小相同,层与层之间采用密集连接方式。结构:BN+ReLU+3x3 Conv
  • 而Transition模块是连接两个相邻的DenseBlock,并且通过Pooling使特征图大小降低
    • 它主要是连接两个相邻的DenseBlock,并且降低特征图大小,结构为BN+ReLU+1x1 Conv+2x2 AvgPooling
    • 通过1×1卷积层来减小通道数,并使用步幅为2的平均池化层减半高和宽,从而进一步降低模型复杂度。
    • 还可以起到压缩模型的作用,通过压缩系数实现,进一步提高网络的紧密度

Growth rate

所有DenseBlock中各个层卷积之后均输出k个特征图,k在DenseNet称为growth rate,K的实际含义就是这层新提取出的特征

一般情况下使用较小的k(比如12),就可以得到较佳的性能假定输入层的特征图的channel数为k0,那么l层的输入channel数为k0+k(l-1),因此随着层数增加,尽管k设定的非常小,但DenseBlock的输入会非常多,不过这是由于特征重用所造成的,每个层仅有k个特征是自己的(每个层所独有的特征图比较小)。

Bottleneck

尽管每前进一层,只产生K张新特征图,但还是嫌多,于是在进行3×3卷积之前先用一个 1×1卷积将输入的特征图个数降低到 4*k

DenseNet-B结构:BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv (3x3卷积搭配padding=1的方式可保证特征图大小维持不变)

增加了1x1的卷积的Dense Block也称为Bottleneck结构

  • 每一个Bottleneck输出的特征通道数是相同的,经过concat操作后的通道数是按K的增长量增加的
  • 这里1×1卷积的作用是固定输出通道数,达到降维的作用,1×1卷积输出的通道数通常是GrowthRate的4倍
  • Dense Block采用了激活函数在前、卷积层在后的顺序,即BN-ReLU-Conv的顺序,这种方式也被称为pre-activation

 DenseNet优缺点

优点:

  • 更强的梯度流动
    • 每一层都建立起了与前面层的连接,误差信号可以很容易地传播到较早的层,所以较早的层可以从最终分类层获得直接监管
  • 减少参数总量

  • 保存了低维度的特征
    • 在标准的卷积网络中,最终输出只会利用提取最高层次的特征
    • 在DenseNet中,它使用了不同层次的特征,倾向于给出更平滑的决策边界。这也解释了为什么训练数据不足时DenseNet表现依旧良好

缺点

  • 由于需要进行多次Concatnate操作,数据需要被复制多次,显存容易增加得很快,需要一定的显存优化技术
  • DenseNet是一种更为特殊的网络,ResNet则相对一般化一些,因此ResNet的应用范围更广泛

代码实现

import torch
from torch import nn, optim
import torchvision
import sys
from time import time
import torch.nn.functional as F

'''实现DenseBlock的基本结构:批量归一化、激活和卷积'''
def conv_block(in_channels, out_channels):
    blk = nn.Sequential(
        nn.BatchNorm2d(in_channels),
        nn.ReLU(),
        nn.Conv2d(in_channels, out_channels,kernel_size=3, padding=1)
    )
    return blk

'''实现DenseBlock,DenseBlock由多个 conv_block 组成,每个Block使用相同的输出通道数'''
class DenseBlock(nn.Module):
    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  #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


'''过渡层'''
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模型'''
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 的形状转换 '''
class FlattenLayer(nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x):
        return x.view(x.shape[0], -1)

'''使用同ResNet一样的单卷积层和最大池化层'''
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)
)
'''使用的是4个稠密块,卷积层为4,卷积层通道数(即增长率)设为32,使用过渡层来减半高和宽,并减半通道数'''
num_channels, growth_rate = 64, 32
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)
    net.add_module('DenseBlock_%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', GlobalAvgPool2d())
net.add_module('fc', nn.Sequential(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)

 

'''读取数据集'''
def load_data_fashion_mnist(batch_size, resize=None):
    if sys.platform.startswith('win'):
        num_workers = 0  # 0表示不用额外的进程来加速读取数据
    else:
        num_workers = 4
        
    '''定义数据预处理的转换函数列表'''    
    trans = []
    if resize: #判断是否需要进行图像尺寸调整(resize)
        trans.append(torchvision.transforms.Resize(size=resize)) 
        #将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
    trans.append(torchvision.transforms.ToTensor())
    # 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
    transform = torchvision.transforms.Compose(trans) 
    #通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
    
    mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
                                                    train=True,
                                                    download=True,
                                                    transform=transform)
    mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
                                                   train=False,
                                                   download=True,
                                                   transform=transform)
    train_iter = torch.utils.data.DataLoader(mnist_train,
                                             batch_size=batch_size,
                                             shuffle=True,
                                             num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(mnist_train,
                                             batch_size=batch_size,
                                             shuffle=False,
                                             num_workers=num_workers) 
    return train_iter, test_iter


'''用于GPU的准确率评估函数'''
def evaluate_accuracy(data_iter, net, device=None): 
    if device is None and isinstance(net, torch.nn.Module):
        device = list(net.parameters())[0].device #如果没指定device就使用net的device
    acc_sum, n = 0.0, 0
    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:
            if ('is_training' in net.__code__.co_varnames):
                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= 0.0, 0.0, 0,0
        # 分别表示训练损失总和、训练准确度总和、样本数
        start = time()
        for X, y in train_iter:
            X = X.to(device)
            y = y.to(device)
            '''前向传播'''
            y_hat = net(X) 
            #由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
            '''计算损失'''
            l = loss(y_hat, y).sum()
            '''梯度清零'''
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            '''反向传播'''
            l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
            
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else: 
                '''更新参数'''
                optimizer.step()
            '''计算精度'''  
            train_l_sum += l.cpu().item()  
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().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()-start))

'''使用fashion_mnist来训练DenseNet模型'''
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size,resize=96)
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)

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值