引言

文通过代码实现了AlexNet算法,使用的是pytorch框架,版本为1.7.1。另外本专栏的所有算法都有对应的Libtorch版本(Libtorch版本的AlexNet地址),算法原理本文不做过多阐述。本文针对小白对代码以及相关函数进行讲解,建议配合代码进行阅读,代码中我进行了详细的注释,因此读者可以更加容易理解代码的含义,本文只展示了部分代码,全部代码可以通过GitHub下载

本文使用的数据集为0~9的手写数据集,全部代码主要分为以下几个部分:

1、定义AlexNet网络结构(model.py)

2、训练卷积神经网络(train.py)

3、输入图片预测结果(predict.py)

4、将保存的模型参数 .pth文件(Pythorch用)转变为 .pt文件(Libtorch用) (pytocpp.py)

model.py

AlexNet的网络结构如下所示:

首先是特征提取部分的网络结构,其中每一次卷积后都需要加ReLu激活函数。



层名\参数

输入通道数

输出通道数

卷积核大小

步长

填充数

备注

卷积层

3

96

11

4

2

后接ReLu

最大池化层



3

2

0


卷积层

96

256

5

1

2

后接ReLu

最大池化层



3

2

0


卷积层

256

384

3

1

1

后接ReLu

卷积层

384

384

3

1

1

后接ReLu

卷积层

384

256

3

1

1

后接ReLu

最大池化层



3

2

0




此部分代码如下

self.features = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(3, 2),
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(3, 2),
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(3, 2)
        )

然后是线性分类部分的网络结构:



层名\参数

输入通道数

输出通道数

备注

Dropout层



0.5

全连接层

256*6*6

2048

后接ReLu

Dropout层



0.5

全连接层

2048

2048

后接ReLu

全连接层

2048

NUM_CLASS




此部分代码如下:

self.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(256*6*6, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            nn.Linear(2048, NUM_CLASS) # NUM_CLASS为类别总数
        )

先介绍以下用到的函数吧

import torch.nn as nn
import torch
# 卷积层  计算公式为 -> 卷积后图像尺寸=(原图像尺寸+2*填充大小-卷积核大小)/步长+1
# in_channels为输入通道数
# out_channels为输出通道数
# kernel_size为卷积核尺寸 比如11代表(11*11)
# stride卷积的步长
# padding为零填充数
nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2)

# 最大池化层
# kernel_size为卷积核大小
# stride为步长
nn.MaxPool2d(kernel_size=3, stride=2)

# ReLu激活函数
# inplace=True会改变输入数据的值,节省反复申请与释放内存的空间与时间,效率更好
nn.ReLU(inplace=True)

nn.Dropout(0.5),  # Dropout随机丢弃神经元,0.5代表随机丢弃50%

torch.flatten(x, start_dim=1) # (将矩阵x展开成一维行向量)

# 全连接层
# in_features 输入部分神经元个数
# out_features 输出部分神经元个数
nn.Linear(in_features,out_features)

以0~9的手写数据集为例,在AlexNet中要求输入224*224*3的彩色三通道RGB图片,经过特征提取部分的神经网络输出256*6*6大小的张量,再经过线性分类部分的神经网络得到1*10的张量,假设这10个数中第4个数最大,则最终预测此图像为数字3。至于权重初始化代码,是官方例程中给出的,参考一下就好。

train.py

定义完网络结构之后就可以正式开始训练了!

具体分为以下几个步骤:

1、定义数据集

2、定义网络结构

3、定义损失函数以及优化器

4、开始训练

首先是数据集的定义,本文用到了torchvision中datasets.ImageFolder函数,此函数对数据集的摆放格式有一定要求,以0~9的手写数据集为例,具体布置格式如下图所示:




encoder decoder代码 pytorch pytorch alexnet代码_数据集


encoder decoder代码 pytorch pytorch alexnet代码_python_02


在定义数据集的同时需要对所有的图片进行预处理,包括将图片resize成(224,224)的大小,转变为张量,进行标准化等等。

部分代码如下所示:

from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# transforms.Compose可以对张量进行一系列操作,各种操作存储在列表内
transform = {
# ToTensor()能够把像素的值域范围从0-255变换到0-1之间,
# 而后面的transform.Normalize()则把0-1变换到(-1,1).
    'train': transforms.Compose([transforms.Resize((224, 224)),
                                 transforms.RandomHorizontalFlip(),  # 随机翻转
                                 transforms.ToTensor(),  # 转换为张量
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 标准化处理
                                 ]),
    'test': transforms.Compose([transforms.Resize((224, 224)),
                                transforms.ToTensor(),
                                transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
}
train_dataset = datasets.ImageFolder(root='./AlexNet/Project1/DATASET/TRAIN', transform=transform['train'])
test_dataset = datasets.ImageFolder(root='./AlexNet/Project1/DATASET/TEST', transform=transform['test'])

# 参数shuffle代表是否打乱数据集(以乱序排列)
train_loader = DataLoader(train_dataset, batch_size=2, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=2, shuffle=False)

定义完数据集后,接下来是定义网络结构:(如果使用GPU进行训练,需要设置为CUDA),代码如下:

# 2、定义网络结构并设置为CUDA
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 这个AlexNet是我们在model.py中定义的类(网络结构)
net = AlexNet(NUM_CLASS=10, init_weight=True)
net.to(device)  # 转化为CUDA

接下来是定义损失函数以及优化器,我们这里使用的是交叉熵损失函数(CrossEntropyLoss)以及SGD优化器(随机梯度下降),如果使用GPU进行训练损失函数也要设置为CUDA,具体代码如下:

# 3、定义损失函数及优化器,损失函数设置为CUDA
loss_function = nn.CrossEntropyLoss()
optimizer = optioms.SGD(params=net.parameters(), lr=learning_rate)
loss_function.to(device)

好的,最后就开始训练了,在训练过程中学习率(learning rate)逐步减小会比较好,更改学习率则需要对优化器(optimizer)进行设置,在optimizer中存有字典(param_groups),在字典中可以设置学习率,设置方法如下:

for param_group in optimizer.param_groups:   # 其中的元素是2个字典;optimizer.param_groups[0]: 长度为6的字典,包括[‘amsgrad’, ‘params’, ‘lr’, ‘betas’, ‘weight_decay’, ‘eps’]这6个参数;
                                                # optimizer.param_groups[1]: 好像是表示优化器的状态的一个字典;
    param_group['lr'] = learning_rate      # 更改全部的学习率

然后就需要从上文定义的train_loader中取数据(image与target)进行训练(若使用GPU训练则image与target都要设置为CUDA),具体步骤为:1、将图片输入网络得到1*10的张。2、将神经网络的输出与标签(target)进行对比并计算其损失。3、通过优化器进行梯度下降和反向传播更新参数。4、保存模型参数。5、不断重复(迭代)以上步骤直到达到要求(损失值小于设定值或者完成设定的迭代次数)。部分代码如下所示。

# 4、开始训练
for epoch in range(num_epochs):
    net.train()  # 网络有Dropout,BatchNorm层时一定要加
    if epoch == 4:
        learning_rate = 0.0001  # 第四次迭代时学习率设置为0.0001
    if epoch == 6:
        learning_rate = 0.00001
    for param_group in optimizer.param_groups:   # 其中的元素是2个字典;optimizer.param_groups[0]: 长度为6的字典,包括[‘amsgrad’, ‘params’, ‘lr’, ‘betas’, ‘weight_decay’, ‘eps’]这6个参数;
                                                # optimizer.param_groups[1]: 好像是表示优化器的状态的一个字典;
        param_group['lr'] = learning_rate      # 更改全部的学习率
    print('\n\nStarting epoch %d / %d' % (epoch + 1, num_epochs))
    print('Learning Rate for this epoch: {}'.format(learning_rate))

    total_loss = 0.
    for i, (images, target) in enumerate(train_loader):  # image为图片,target为其对应的标签(类别名)
        images, target = images.cuda(), target.cuda()  # 设置为CUDA
        pred = net(images)    # 图片输入网络得到预测结果
        loss = loss_function(pred, target)  # 将预测结果与实际标签比对(计算两者之间的损失值)
        total_loss += loss.item()

        optimizer.zero_grad()   # 将梯度归零,有助于梯度下降
        loss.backward()    # 反向传播 计算梯度
        optimizer.step()   # 根据梯度 更新模型参数
        if (i + 1) % 5 == 0: # 打印训练的信息
            print('Epoch [%d/%d], Iter [%d/%d] Loss: %.4f, average_loss: %.4f' % (epoch + 1, num_epochs,
                                                                                 i + 1, len(train_loader), loss.item(), total_loss / (i + 1)))
    validation_loss = 0.0
    net.eval()
    for i, (images, target) in enumerate(test_loader):  # 导入dataloader 说明开始训练了  enumerate 建立一个迭代序列
        images, target = images.cuda(), target.cuda()
        pred = net(images)    # 将图片输入
        loss = loss_function(pred, target)
        validation_loss += loss.item()   # 累加loss值  (固定搭配)
    validation_loss /= len(test_loader)  # 计算平均loss
    if best_test_loss > validation_loss:
        best_test_loss = validation_loss
        print('get best test loss %.5f' % best_test_loss)
        torch.save(net.state_dict(), 'AlexNet.pth')  # 保存模型参数

predict.py

知道了如何训练那预测阶段也十分容易了,大致思路为,使用opencv读取一张图片然后经过和训练阶段一样的预处理操作,将其输入神经网络得到1*10维的张量,通过判断10个数中第几个数最大,就能知道该图片所属的类别。废话不多说,代码如下

import cv2
import torch

transform = transforms.Compose([ transforms.ToTensor(),  # 转换为张量
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 进行预测
class Predict:
    def __init__(self, image_root, net1): # image_root->需要预测的图片路径,net1->网络结构
        self.img_root = image_root
        self.model = net1

    def result(self):
        img = cv2.imread(img_root)  # opencv读取图片
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # BGR->RGB
        img = cv2.resize(img, (224, 224))  # 将图片尺寸变为224*224
        img = transform(img)   # 将图片对应的像素矩阵变为张量并且标准化  size=(3,224,224))
        img = torch.unsqueeze(img, 0)  # 执行完后尺寸为(1,3,224,224)
        result = self.model(img)  # 将图片输入神经网络得到结果
        return result

注意事项:

通过opencv读取到的图像彩色三通道的BGR格式的图像,而上文我们说到AlexNet需要输入的是RGB格式的图片,所以需要执行img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)这段代码,值得注意的是 transforms.ToTensor() 这段代码 将尺寸为 (224,224,3)的图像矩阵变成了 (3,224,224)。在Pytorch中神经网络处理图片的格式应为 [ batch_size , channel , height , width ] ,所以需要通过torch.unsqueeze来实现这个要求,另外这个操作在训练时是由 train_loader = DataLoader(train_dataset, batch_size=2, shuffle=True)

另外在此文件中我还附上了绘制混淆矩阵的方法,只需要将代码83行之后的注释全部清除即可(运行会花费一定时间)。

最后附上效果图:


encoder decoder代码 pytorch pytorch alexnet代码_计算机视觉_03


结尾

由于本专栏所有代码都有C++版本,所以提供了一段代码用于将Python版本的模型参数文件转变为C++版本的,如果需要请注意Pytorch与Libtorch版本要一致(在这里是都为1.7.1),读者也可以使用C++进行训练。本文的代码也可以使用其他数据集进行训练,但是需要对代码进行小小的更改,更改方法放在了GitHub中,另外本文用到的手写数据集也可以一并在GitHub中下载