经典的卷积神经网络的pytorch实现(LeNet、AlexNet、VGGNet、NiN、GoogleNet、ResNet、DenseNet)

LeNet

LeNet 是最先出现的计算机卷积神经网络,它交替采用卷积层和池化层,并在最后将元素展开为一维,通过全连接层进行分类。
它具有一下特点:

  1. 激活函数采用的是sigmoid,而不是后来广泛应用的relu函数
  2. 因为输入的图片很小,所以没有使用dropout等一系列的防止过拟合的手段

头文件

import torch
import torch.nn as nn
import torchvision as tv
import pandas as pd 
import numpy as np

模型搭建


class LeNet(nn.Module):
    def __init__(self):
        super(LeNet,self).__init__()  
        self.conv = nn.Sequential(nn.Conv2d(1,6,5), # input 26x26x1
                                nn.Sigmoid(),       # out 22x22x5
                                nn.MaxPool2d(2,2),  # 11x11x5
                                nn.Conv2d(6,16,5),  # 8x8x16
                                nn.Sigmoid(),       
                                nn.MaxPool2d(2,2))  #4x4x16
        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,x):
        features = self.conv(x)
        return self.fc(features.view(x.shape[0],-1))


    train_csv = pd.read_csv("./archive/fashion-mnist_train.csv")
    test_csv = pd.read_csv("./archive/fashion-mnist_test.csv")

自定义dataset

因为用的是校园网直连,所以不能通过使用torchvision.dataset直接获取Fashion_Minist数据集,所以只好从网上下载,然后手动导入。


class Fashion_dataset(torch.utils.data.Dataset):
    def __init__(self,data,transform=None):
        self.Fashion_Mnist = list(data.values)
        self.transform = transform
        label,image = [],[]
        for data in self.Fashion_Mnist:
            label.append(data[0])
            image.append(data[1:])
        self.labels = np.asarray(label)
        # print(image[1].shape)
        self.images = np.array(image,copy=False).reshape((-1,28,28,1)).astype("float32") # 两者等价
        # asarray 默认不拷贝 array默认拷贝
    def __getitem__(self,index):
        image = self.images[index]
        label = self.labels[index]
        
        if self.transform is not None:
            image = self.transform(image)
        return image, label
        
    def __len__(self):
        return len(self.images)

train_dataset = Fashion_dataset(train_csv,transform=tv.transforms.Compose([tv.transforms.ToTensor()]))
test_dataset = Fashion_dataset(test_csv,transform=tv.transforms.Compose([tv.transforms.ToTensor()]))
batch_size = 64
device = torch.device("cuda:0"if torch.cuda.is_available() else "cpu")
train_iter = torch.utils.data.DataLoader(train_dataset,batch_size=batch_size)
test_iter = torch.utils.data.DataLoader(test_dataset,batch_size=batch_size)

训练

def train(model,optimizer,train_loader,test_loader,criterion,epochs=50):
    
    train_ls, test_ls = [],[]
    model = model.to(device)
    for epoch in range(epochs):
        tot_loss = 0
        for i, data in enumerate(train_loader):
            X, y = data[0].to(device),data[1].to(device)
            loss = criterion(model(X),y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            tot_loss = loss.cpu().item()
            
        train_ls.append(tot_loss)
        if epoch % 10 == 0:
            total = 0
            correct = 0
        
            for images, labels in test_loader:
                images, labels = images.to(device), labels.to(device)
            
            
                outputs = model(images)
            
                predictions = torch.max(outputs, 1)[1].to(device)
                correct += (predictions == labels).sum()
            
                total += len(labels)
            
            accuracy = correct * 100 / total
                
            print(f"train_loss:{tot_loss},test_accuracy:{accuracy}")
    optimizer = torch.optim.Adam(model.parameters(),lr=0.001)
    criterion = torch.nn.CrossEntropyLoss()
    train(model,optimizer,train_iter,test_iter,criterion,50)

因为采用的数据集和数据的读入都是类似的过程,下面只给出模型的代码实现

AlexNet

在AlexNet的论文中,采用的是ImageNet作为数据集,训练需要花很长时间,所以这里仍然使用Fashion_Mnist作为数据集。

AlexNet的改进:

  1. 采用Relu函数作为卷积层的激活函数,可以避免数据接近0时,梯度很小的问题。
  2. 引入了Dropout,防止过拟合。
  3. 增大了卷积核的大小(因为ImageNet的图片大小约为之前的100倍),同时增加了通道数(channels)
class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            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),
            nn.Linear(4096, 4096),
            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

VGG

对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核优于采用大的卷积核,因为可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。例如,在VGG中,使用了3个3x3卷积核来代替7x7卷积核,使用了2个3x3卷积核来代替5*5卷积核,这样做的主要目的是在保证具有相同感知野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果。

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)

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(cnov_arch,fc_features,fc_hidden_units):
    net = nn.Sequential()
    for i, (num_convs,in_channels,out_channels) in enumerate(conv_arch):
        net.add_module("vgg"+str(i),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  

NiNet

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


NiN块是NiN中的基础块。它由一个卷积层加两个充当全连接层的1×1 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

除使用NiN块以外,NiN还有一个设计与AlexNet显著不同:NiN去掉了AlexNet最后的3个全连接层,取而代之地,NiN使用了输出通道数等于标签类别数的NiN块,然后使用全局平均池化层对每个通道中所有元素求平均并直接用于分类。这里的全局平均池化层即窗口形状等于输入空间维形状的平均池化层。NiN的这个设计的好处是可以显著减小模型参数尺寸,从而缓解过拟合。然而,该设计有时会造成获得有效模型的训练时间的增加。

import torch.nn.functional as F
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:])

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(), 
   # 将四维的输出转成二维的输出,其形状为(批量大小, 10)
   d2l.FlattenLayer())

  • NiN重复使用由卷积层和代替全连接层的1×11×1卷积层构成的NiN块来构建深层网络。
  • NiN去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数的NiN块和全局平均池化层。

GooLeNet

  • Inception块相当于一个有4条线路的子网络。它通过不同窗口形状的卷积层和最大池化层来并行抽取信息,并使用1×11×1卷积层减少通道数从而降低模型复杂度。
  • GoogLeNet将多个设计精细的Inception块和其他层串联起来。其中Inception块的通道数分配之比是在ImageNet数据集上通过大量的实验得来的。
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 Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        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)  # 在通道维上连结输出

GooLeNet主要分为5个模块,b1,b2,b3,b4,b5。b5仍然是沿用VGG模型的flatten之后全连接层分类。

b1

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

b2

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


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

b3开始采用了Inception模块,第一个Inception各个传输路线的通道比值为2:4:1:1,第二个为4:6:3:2

b4

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

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

第五模块有输出通道数为256+320+128+128=832256+320+128+128=832和384+384+128+128=1024384+384+128+128=1024的两个Inception块。其中每条线路的通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均池化层来将每个通道的高和宽变成1。最后我们将输出变成二维数组后接上一个输出个数为标签类别数的全连接层。

Batch_Normalize Layer

from scratch

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 batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 判断当前模式是训练模式还是预测模式
    if not is_training:
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        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, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=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, moving_var

class BatchNorm(nn.Module):
    def __init__(self, num_features, num_dims):
        super(BatchNorm, self).__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 不参与求梯度和迭代的变量,全在内存上初始化成0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.zeros(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, Module实例的traning属性默认为true, 调用.eval()后设成false
        Y, self.moving_mean, self.moving_var = batch_norm(self.training, 
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y


pytorch

net = nn.Sequential(
            nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
            nn.BatchNorm2d(6),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2), # kernel_size, stride
            nn.Conv2d(6, 16, 5),
            nn.BatchNorm2d(16),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),
            d2l.FlattenLayer(),
            nn.Linear(16*4*4, 120),
            nn.BatchNorm1d(120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.BatchNorm1d(84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )

BN层实际上是对变量做了如下的线性变换。
x = ( x − μ ) σ x=\frac{(x-\mu)}{\sigma} x=σ(xμ)
其中 μ \mu μ, σ \sigma σ分别为平均数和方差,如果具有多个通道,那么会对每一个通道独立计算。BN层在测试的时候, μ \mu μ, σ \sigma σ会采用全局的均值和方差。

ResNet


class Residual(nn.Module):  
    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:
            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)

在这里插入图片描述

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东风中的蒟蒻

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

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

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

打赏作者

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

抵扣说明:

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

余额充值