AlexNet代码详解


\一、开发背景**

AlexNet由Hinton和他的学生Alex Krizhevsky设计,模型名字来源于论文第一作者的姓名Alex。该模型以很大的优势获得了2012年ISLVRC竞赛的冠军网络,分类准确率由传统的 70%+提升到 80%+,自那年之后,深度学习开始迅速发展。

ImageNet是一个在2009年创建的图像数据集,从2010年开始到2017年举办了七届的ImageNet 挑战赛——ImageNet Large Scale Visual Recognition ChallengeI (LSVRC),在这个挑战赛上诞生了AlexNet、ZFNet、OverFeat、VGG、Inception、ResNet、WideResNet、FractalNet、DenseNet、ResNeXt、DPN、SENet 等经典模型。

二、网络结构

Alexnet模型为8层深度网络,由5个卷积层和3个全连接层构成,不计LRN层和池化层。AlexNet 跟 LeNet 结构类似,但使用了更多的卷积层和更大的参数空间来拟合大规模数据集 ImageNet。它是浅层神经网络和深度神经网络的分界线,如下图所示:

img

网络详解:AlexNet网络结构详解(含各层维度大小计算过程)与PyTorch实现

三、模型特点

  1. 使用CUDA加速深度卷积网络的训练,利用GPU强大的并行计算能力,处理神经网络训练时大量的矩阵运算;
  2. 使用大数据训练,是百万级ImageNet图像数据,提升算法的准确率,避免过拟合;
  3. 使用ReLU作为激活函数,解决了SIgmoid在网络较深时的梯度消失问题,使收敛更快;
  4. 使用随机丢弃技术(dropout)以0.5的概率选择性地将隐藏层神经元的输出设置为零,以这种方式“dropped out”的神经元既不参与前向传播,也不参与反向传播,避免模型的过拟合;
  5. 重叠最大池化(overlapping max pooling),池化的步长小于核尺寸,使得输出之间会有重叠和覆盖,提升了特征的丰富性,并且避免平均池化的模糊化效果;
  6. 使用 LRN 局部响应归一化(Local Response Normalization)层,对局部神经元的活动创建竞争机制,使得响应较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力,使准确率更高;
  7. 进行数据增强,随机从256256的原始图像中截取224224大小的区域(以及水平翻转的镜像),相当于增强了(256-224)(256-224)2=2048倍的数据量,减轻过拟合,提升泛化能力。

为什么Dropout有效?

Dropout背后理念和集成模型很相似。在Drpout层,不同的神经元组合被关闭,这代表了一种不同的结构,所有这些不同的结构使用一个的子数据集并行地带权重训练,而权重总和为1。如果Dropout层有 n 个神经元,那么会形成 2n 个不同的子结构。在预测时,相当于集成这些模型并取均值。这种结构化的模型正则化技术有利于避免过拟合。Dropout有效的另外一个视点是:由于神经元是随机选择的,所以可以减少神经元之间的相互依赖,从而确保提取出相互独立的重要特征。

四、代码实现

  • model.py :定义AlexNet网络模型
  • train.py:加载数据集并训练,计算loss和accuracy,保存训练好的网络参数
  • predict.py:用自己的数据集进行分类测试
  • spilit_data.py:划分给定的数据集为训练集和测试集

注意:代码实现没有还原两个小型GPU同时运算的设计特点,而是在一个模型中运行

\1. model.py**

# 导入pytorch库
import torch
# 导入torch.nn模块
from torch import nn
# nn.functional:(一般引入后改名为F)有各种功能组件的函数实现,如:F.conv2d
import torch.nn.functional as F
 
# 定义AlexNet网络模型
# MyLeNet5(子类)继承nn.Module(父类)
class MyAlexNet(nn.Module):
    # 子类继承中重新定义Module类的__init__()和forward()函数
    # init():进行初始化,申明模型中各层的定义
    def __init__(self):
        # super:引入父类的初始化方法给子类进行初始化
        super(MyAlexNet, self).__init__()
        # 卷积层,输入大小为224*224,输出大小为55*55,输入通道为3,输出为96,卷积核为11,步长为4
        self.c1 = nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2)
        # 使用ReLU作为激活函数
        self.ReLU = nn.ReLU()
        # MaxPool2d:最大池化操作
        # 最大池化层,输入大小为55*55,输出大小为27*27,输入通道为96,输出为96,池化核为3,步长为2
        self.s1 = nn.MaxPool2d(kernel_size=3, stride=2)
        # 卷积层,输入大小为27*27,输出大小为27*27,输入通道为96,输出为256,卷积核为5,扩充边缘为2,步长为1
        self.c2 = nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2)
        # 最大池化层,输入大小为27*27,输出大小为13*13,输入通道为256,输出为256,池化核为3,步长为2
        self.s2 = nn.MaxPool2d(kernel_size=3, stride=2)
        # 卷积层,输入大小为13*13,输出大小为13*13,输入通道为256,输出为384,卷积核为3,扩充边缘为1,步长为1
        self.c3 = nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1)
        # 卷积层,输入大小为13*13,输出大小为13*13,输入通道为384,输出为384,卷积核为3,扩充边缘为1,步长为1
        self.c4 = nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1)
        # 卷积层,输入大小为13*13,输出大小为13*13,输入通道为384,输出为256,卷积核为3,扩充边缘为1,步长为1
        self.c5 = nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1)
        # 最大池化层,输入大小为13*13,输出大小为6*6,输入通道为256,输出为256,池化核为3,步长为2
        self.s5 = nn.MaxPool2d(kernel_size=3, stride=2)
        # Flatten():将张量(多维数组)平坦化处理,神经网络中第0维表示的是batch_size,所以Flatten()默认从第二维开始平坦化
        self.flatten = nn.Flatten()
        # 全连接层
        # Linear(in_features,out_features)
        # in_features指的是[batch_size, size]中的size,即样本的大小
        # out_features指的是[batch_size,output_size]中的output_size,样本输出的维度大小,也代表了该全连接层的神经元个数
        self.f6 = nn.Linear(6*6*256, 4096)
        self.f7 = nn.Linear(4096, 4096)
        # 全连接层&softmax
        self.f8 = nn.Linear(4096, 1000)
        self.f9 = nn.Linear(1000, 2)
 
    # forward():定义前向传播过程,描述了各层之间的连接关系
    def forward(self, x):
        x = self.ReLU(self.c1(x))
        x = self.s1(x)
        x = self.ReLU(self.c2(x))
        x = self.s2(x)
        x = self.ReLU(self.c3(x))
        x = self.ReLU(self.c4(x))
        x = self.ReLU(self.c5(x))
        x = self.s5(x)
        x = self.flatten(x)
        x = self.f6(x)
         # Dropout:随机地将输入中50%的神经元激活设为0,即去掉了一些神经节点,防止过拟合
        # “失活的”神经元不再进行前向传播并且不参与反向传播,这个技术减少了复杂的神经元之间的相互影响
        x = F.dropout(x, p=0.5)
        x = self.f7(x)
        x = F.dropout(x, p=0.5)
        x = self.f8(x)
        x = F.dropout(x, p=0.5)
        x = self.f9(x)
        return x
 
# 每个python模块(python文件)都包含内置的变量 __name__,当该模块被直接执行的时候,__name__ 等于文件名(包含后缀 .py )
# 如果该模块 import 到其他模块中,则该模块的 __name__ 等于模块名称(不包含后缀.py)
# “__main__” 始终指当前执行模块的名称(包含后缀.py)
# if确保只有单独运行该模块时,此表达式才成立,才可以进入此判断语法,执行其中的测试代码,反之不行
if __name__ == '__main__':
    # rand:返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数,此处为四维张量
    x = torch.rand([1, 3, 224, 224])
    # 模型实例化
    model = MyAlexNet()
    y = model(x)

2. train.py

import torch
from torch import nn
from model import MyAlexNet
from torch.optim import lr_scheduler
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
import os
import matplotlib.pyplot as plt
 
# 解决中文显示问题
# 运行配置参数中的字体(font)为黑体(SimHei)
plt.rcParams['font.sans-serif'] = ['simHei']
# 运行配置参数总的轴(axes)正常显示正负号(minus)
plt.rcParams['axes.unicode_minus'] = False
 
 
ROOT_TRAIN = 'D:/pycharm/AlexNet/data/train'
ROOT_TEST = 'D:/pycharm/AlexNet/data/val'
 
# 将图像的像素值归一化到[-1,1]之间
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
 
# Compose():将多个transforms的操作整合在一起
train_transform = transforms.Compose([
    # Resize():把给定的图像随机裁剪到指定尺寸
    transforms.Resize((224, 224)),
    # RandomVerticalFlip():以0.5的概率竖直翻转给定的PIL图像
    transforms.RandomVerticalFlip(),
    # ToTensor():数据转化为Tensor格式
    transforms.ToTensor(),
    normalize])
 
val_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    normalize])
 
# 加载训练数据集
# ImageFolder:假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
# ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
# root:在root指定的路径下寻找图像,transform:对输入的图像进行的转换操作
train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
# DataLoader:将读取的数据按照batch size大小封装给训练集
# dataset (Dataset):加载数据的数据集
# batch_size (int, optional):每个batch加载多少个样本(默认: 1)
# shuffle (bool, optional):设置为True时会在每个epoch重新打乱数据(默认: False)
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
 
# 加载训练数据集
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
val_dataloader = DataLoader(val_dataset, batch_size=32, shuffle=True)
 
# 如果有NVIDA显卡,可以转到GPU训练,否则用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
 
# 模型实例化,将模型转到device
model = MyAlexNet().to(device)
 
# 定义损失函数(交叉熵损失)
loss_fn = nn.CrossEntropyLoss()
 
# 定义优化器(随机梯度下降法)
# params(iterable):要训练的参数,一般传入的是model.parameters()
# lr(float):learning_rate学习率,也就是步长
# momentum(float, 可选):动量因子(默认:0),矫正优化率
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
 
# 学习率每隔10轮变为原来的0.5
# StepLR:用于调整学习率,一般情况下会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果
# optimizer (Optimizer):更改学习率的优化器
# step_size(int):每训练step_size个epoch,更新一次参数
# gamma(float):更新lr的乘法因子
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
 
# 定义训练函数
def train(dataloader, model, loss_fn, optimizer):
    loss, current, n = 0.0, 0.0, 0
    # dataloader: 传入数据(数据包括:训练数据和标签)
    # enumerate():用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在for循环当中
    # enumerate返回值有两个:一个是序号,一个是数据(包含训练数据和标签)
    # x:训练数据(inputs)(tensor类型的),y:标签(labels)(tensor类型)
    for batch, (x, y) in enumerate(dataloader):
        # 前向传播
        image, y = x.to(device), y.to(device)
        # 计算训练值
        output = model(image)
        # 计算观测值(label)与训练值的损失函数
        cur_loss = loss_fn(output, y)
        # torch.max(input, dim)函数
        # input是具体的tensor,dim是max函数索引的维度,0是每列的最大值,1是每行的最大值输出
        # 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引
        _, pred = torch.max(output, axis=1)
        # 计算每批次的准确率
        # output.shape[0]为该批次的多少,output的一维长度
        # torch.sum()对输入的tensor数据的某一维度求和
        cur_acc = torch.sum(y == pred)/output.shape[0]
 
        # 反向传播
        # 清空过往梯度
        optimizer.zero_grad()
        # 反向传播,计算当前梯度
        cur_loss.backward()
        # 根据梯度更新网络参数
        optimizer.step()
        # item():得到元素张量的元素值
        loss += cur_loss.item()
        current += cur_acc.item()
        n = n + 1
 
    train_loss = loss / n
    train_acc = current / n
    # 计算训练的错误率
    print('train_loss==' + str(train_loss))
    # 计算训练的准确率
    print('train_acc' + str(train_acc))
    return train_loss, train_acc
 
# 定义验证函数
def val(dataloader, model, loss_fn):
    loss, current, n = 0.0, 0.0, 0
    # eval():如果模型中有Batch Normalization和Dropout,则不启用,以防改变权值
    model.eval()
    with torch.no_grad():
        for batch, (x, y) in enumerate(dataloader):
            # 前向传播
            image, y = x.to(device), y.to(device)
            output = model(image)
            cur_loss = loss_fn(output, y)
            _, pred = torch.max(output, axis=1)
            cur_acc = torch.sum(y == pred)/output.shape[0]
            loss += cur_loss.item()
            current += cur_acc.item()
            n = n+1
 
    val_loss = loss / n
    val_acc = current / n
    # 计算验证的错误率
    print('val_loss=' + str(val_loss))
    # 计算验证的准确率
    print('val_acc=' + str(val_acc))
    return val_loss, val_acc
 
# 定义画图函数
# 错误率
def matplot_loss(train_loss, val_loss):
    # 参数label = ''传入字符串类型的值,也就是图例的名称
    plt.plot(train_loss, label='train_loss')
    plt.plot(val_loss, label='val_loss')
    # loc代表了图例在整个坐标轴平面中的位置(一般选取'best'这个参数值)
    plt.legend(loc='best')
    plt.xlabel('loss')
    plt.ylabel('epoch')
    plt.title("训练集和验证集的loss值对比图")
    plt.show()
 
# 准确率
def matplot_acc(train_acc, val_acc):
    plt.plot(train_acc, label = 'train_acc')
    plt.plot(val_acc, label = 'val_acc')
    plt.legend(loc = 'best')
    plt.xlabel('acc')
    plt.ylabel('epoch')
    plt.title("训练集和验证集的acc值对比图")
    plt.show()
 
#开始训练
loss_train = []
acc_train = []
loss_val = []
acc_val = []
 
# 训练次数
epoch = 20
# 用于判断最佳模型
min_acc = 0
for t in range(epoch):
    lr_scheduler.step()
    print(f"epoch{t+1}\n----------")
    # 训练模型
    train_loss, train_acc = train(train_dataloader, model, loss_fn, optimizer)
    # 验证模型
    val_loss, val_acc = val(val_dataloader, model, loss_fn)
 
    loss_train.append(train_loss)
    acc_train.append(train_acc)
    loss_val.append(val_loss)
    acc_val.append(val_acc)
 
    # 保存最好的模型权重
    if val_acc > min_acc:
        folder = 'save_model'
        # path.exists:判断括号里的文件是否存在的意思,括号内可以是文件路径,存在为True
        if not os.path.exists(folder):
            # os.mkdir() 方法用于以数字权限模式创建目录
            os.mkdir('save_model')
        min_acc = val_acc
        print(f"save best model,第{t+1}轮")
        # torch.save(state, dir):保存模型等相关参数,dir表示保存文件的路径+保存文件名
        # model.state_dict():返回的是一个OrderedDict,存储了网络结构的名字和对应的参数
        torch.save(model.state_dict(), 'save_model/best_model.pth')
 
    # 保存最后一轮权重
    if t == epoch-1:
        torch.save(model.state_dict(), 'save_model/best_model.pth')
 
matplot_loss(loss_train, loss_val)
matplot_acc(acc_train, acc_val)
 
print('done')

3. predict.py

import torch
from model import MyAlexNet
from torch.autograd import Variable
from torchvision import transforms
from torchvision.transforms import ToPILImage
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
 
ROOT_TRAIN = 'D:/pycharm/AlexNet/data/train'
ROOT_TEST = 'D:/pycharm/AlexNet/data/val'
 
# 将图像的像素值归一化到[-1,1]之间
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
 
val_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    normalize
])
 
# 加载训练数据集
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
 
# 如果有NVIDA显卡,转到GPU训练,否则用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
 
# 模型实例化,将模型转到device
model = MyAlexNet().to(device)
 
# 加载train.py里训练好的模型
model.load_state_dict(torch.load(r'D:\pycharm\AlexNet\save_model\best_model.pth'))
 
# 结果类型
classes = [
    "cat",
    "dog"
]
 
# 把Tensor转化为图片,方便可视化
show = ToPILImage()
 
# 进入验证阶段
model.eval()
for i in range(1):
    x, y = val_dataset[i][0], val_dataset[i][1]
    # show():显示图片
    show(x).show()
    # torch.unsqueeze(input, dim),input(Tensor):输入张量,dim (int):插入维度的索引,最终扩展张量维度为4维
    x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=False).to(device)
    with torch.no_grad():
        pred = model(x)
        # argmax(input):返回指定维度最大值的序号
        # 得到预测类别中最高的那一类,再把最高的这一类对应classes中的那一类
        predicted, actual = classes[torch.argmax(pred[0])], classes[y]
        # 输出预测值与真实值
        print(f'predicted:"{predicted}", actual:"{actual}"')

4. spilit_data.py

import os
from shutil import copy
import random
 
# 如果file不存在,创建file
def mkfile(file):
    if not os.path.exists(file):
        os.makedirs(file)
 
 
# 获取data文件夹下所有除.txt文件以外所有文件夹名(即需要分类的类名)
# os.listdir():用于返回指定的文件夹包含的文件或文件夹的名字的列表
file_path = 'D:/pycharm/AlexNet/data_name'
pet_class = [cla for cla in os.listdir(file_path) if ".txt" not in cla]
 
# 创建训练集train文件夹,并由类名在其目录下创建子目录
mkfile('data/train')
for cla in pet_class:
    mkfile('data/train/' + cla)
 
# 创建验证集val文件夹,并由类名在其目录下创建子目录
mkfile('data/val')
for cla in pet_class:
    mkfile('data/val/' + cla)
 
# 划分比例,训练集 : 验证集 = 8 : 2
split_rate = 0.2
 
# 遍历所有类别的图像并按比例分成训练集和验证集
for cla in pet_class:
    # 某一类别的子目录
    cla_path = file_path + '/' + cla + '/'
    # iamges列表存储了该目录下所有图像的名称
    images = os.listdir(cla_path)
    num = len(images)
    # 从images列表中随机抽取k个图像名称
    # random.sample:用于截取列表的指定长度的随机数,返回列表
    # eval_index保存验证集val的图像名称
    eval_index = random.sample(images, k=int(num * split_rate))
    for index, image in enumerate(images):
        if image in eval_index:
            image_path = cla_path + image
            new_path = 'data/val/' + cla
            # copy():将源文件的内容复制到目标文件或目录
            copy(image_path, new_path)
 
        # 其余图像保存在训练集train中
        else:
            image_path = cla_path + image
            new_path = 'data/train/' + cla
            copy(image_path, new_path)
        # '\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出,本行以前的内容会被逐一覆盖
        # <模板字符串>.format(<逗号分隔的参数>)
        # end="":将print自带的换行用end中指定的str代替
        print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="")
    print()
 
print("processing done!")

文章作者: Oliver
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Oliver !
评论
  目录