基于Pytorch的LeNet实现CIFAR数据集分类(俺的代码注解)

开始了解深度学习有一小段时间了,期间我看了几个up主讲的LeNet实现,从最开始的每个函数都要百度到现在基本知道每部分的作用,还能比较熟练的搭建网络模型,我想记录一下一个菜鸟的成长经历。

代码是由b站up【霹雳吧啦Wz】的源码进行注解和稍微改动过的,初学者可以去看up的视频,讲的非常透彻

一、认识网络结构

首先我们开看一下LeNet的网络结构:

 卷积+池化(下采样)+卷积+池化+三个全连接层

计算卷积输出大小的公式:N = (W − F + 2P )/S+1

N:图片输出的大小       W:原宽度       F:卷积核大小       P:padding     S:stride

每层的参数:大家对照上面的图和下面的表格 就会发现网络的结构非常清晰

实际使用时改动了 c1的深度为16 c3深度为32 

但是也有好多人把第一个全连接层也用卷积层来写,还是使用5*5的卷积核,正好展成一行,我们使用的激活函数是ReLU,我看的第一个视频是b站的炮哥的lenet实现手写数字识别,他就是用了三个卷积层,而且因为第三层卷积是把它变成线性的了就没有再加激活函数,包括全连接层也没有加激活函数(全连接层本来就是线性的,还需要激活函数吗?俺现在也不太明白)但是由于我使用那个网络结构训练的结果并不理想,这里我采用标准的网络模型,三层全连接层来实现,并在前两个全连接层上使用了激活函数

二、搭建网络结构模型(model.py)

要做的解释:

  • 只写了第一次卷积层和池化层的参数名称 后面的都省略了,没用标准lenet,图片中的第一层通道数应为6,第二层为16,但是在我这里训练效果没那么好,就是用了up的16和32的写法
  • 全连接层之前还需要把输入的tensor展平 我之前使用flatten函数,这里用的view函数
  • flatten是不用考虑参数直接展开,view则是相当于对这个张量进行一个reshape,关于-1的理解我在下面的注释中也写到了

【看up的视频学到了:搭好网络模型后,可以采用打断点的方式debug,步进看每一层的参数大小和深度如何变化】


import torch.nn as nn
import torch.nn.functional as F


class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5)
        self.pool1 = nn.MaxPool2d(kernel_size=2,stride=2)
        self.conv2 = nn.Conv2d(16, 32, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))    # input(3, 32, 32) output(16, 28, 28)
        x = self.pool1(x)            # output(16, 14, 14)
        x = F.relu(self.conv2(x))    # output(32, 10, 10)
        x = self.pool2(x)            # output(32, 5, 5)
        # x.view() 对tensor进行reshape -1表示规定另一个参数 这个参数自己计算 我们规定列数为32*5*5(展平 行由view函数自己算(参数的总数除规定的列数 这里也就是一行
        x = x.view(-1, 32*5*5)       # output(32*5*5)
        x = F.relu(self.fc1(x))      # output(120)
        x = F.relu(self.fc2(x))      # output(84)
        x = self.fc3(x)              # output(10)
        return x

三、训练(train.py)

先贴源码,后面解释

import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt


def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)
    transform = transforms.Compose(
        [transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
                                             download=False, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
                                               shuffle=True, num_workers=0)

    test_set = torchvision.datasets.CIFAR10(root='./data', train=False,
                                            download=False, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=5000,
                                              shuffle=False, num_workers=0)

    test_data_iter = iter(test_loader)
    test_image, test_label = test_data_iter.next() 
    test_image, test_label = test_image.to(device), test_label.to(device)
    # 迭代器中的测试集图片和label也要to(device)

    classes = ('plane', 'car', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # def imshow(img):
    #     img = img /2 +0.5 #反标准化处理(为了图像能正常展示
    #     npimg = img.numpy() #转成图像格式
    #     plt.imshow(np.transpose(npimg,(1, 2, 0))) #tensor格式[b,c,h,w]的纬度转换成正常纬度[h,w,c]:
    #     plt.show()
    #
    # print(' '.join("%5s" % classes[test_label[j]] for j in range(4))) #打印标签
    # imshow(torchvision.utils.make_grid(test_image)) #展示图片



    net = LeNet().to(device)
    loss_function = nn.CrossEntropyLoss() #内置了softmax函数
    optimizer = optim.Adam(net.parameters(), lr=0.001)

    save_path = './Lenet.pth'

    maxAcc = 0.0
    for epoch in range(5):  # loop over the dataset multiple times

        running_loss = 0.0

        for step, data in enumerate(train_loader, start=0):
            # get the inputs; data is a list of [inputs, labels]
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)

            # 训练时 反向传播用的
            optimizer.zero_grad()
            outputs = net(inputs) 
            loss = loss_function(outputs, labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item()
            if step % 500 == 499:  # print every 500 mini-batches 训练五百次测试一次
                with torch.no_grad(): #测试过程中不计算梯度
                    outputs = net(test_image)  # [batch, 10]
                    predict_y = torch.max(outputs, dim=1)[1] #dim(轴 0表示列 1表示行
                    accuracy = torch.eq(predict_y, test_label).sum().item() / test_label.size(0) #size(0)行数 size(1)列数 item():tensor转成数值

                    print('[%d, %5d] train_loss: %.3f  test_accuracy: %.3f' %
                          (epoch + 1, step + 1, running_loss / 500, accuracy))

                    if accuracy>maxAcc:
                        maxAcc = accuracy
                        torch.save(net.state_dict(), save_path)
                        print('save best model')
                    running_loss = 0.0

    print('Finished Training')


if __name__ == '__main__':
    main()
训练结果,准确率在0.68左右(没做任何优化处理)

对其中的一些代码进行说明:


up本来使用cpu训练的,我拿着源码修改的时候以为还是跟之前一样只需要加这两行就行了,结果一直报错如:Input type (torch.FloatTensor) and weight type (torch.cuda.FloatTensor) should be the same

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net = LeNet().to(device)

查了一下,是因为有些数据没放到gpu里面,这次的代码使用了iter迭代(之前没用过),迭代器中的数据(测试集的)没有to(device)所以报错了,一定要注意,所有放到网络里处理的数据都要放到gpu中【语言表达的不准确,只是说了我理解的意思】

如果使用cpu训练,只需要查找 .to(device) ,然后全部删除就好了


使用的device和数据预处理

ToTensor()和Normalize()作为经典的两个数据预处理函数,解释参考俺贴的这个博客

预处理看这个

ToTensor简单的说就是把图片变成tensor格式,(注意里面的维度顺序发生了编号)而Normalize就是数据归一化,0均值,1方差

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

# 设置使用cpu还是gpu 并打印显示 如果使用的gpu会打印cuda


transform = transforms.Compose(
    [transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 数据进行预处理,因为使用的是CIFAR数据集中的数据,大小本来就是32了,而lenet网络的输入也是32,所以不需要再修改

数据集加载和处理

CIFAR数据集包括10个种类,50000张训练集和10000张测试集,第一次使用先下载,后面再运行就可把download设置成false了

  • DataLoader相当于对数据进行初始化,参数分别是要放进去的数据集,每一批次的大小,是否打乱 和是否使用多线程(windows中只能是0,或者直接不写num_workers参数)
  • datesets 四个参数分别为 root要下载到的路径(./是在本目录下) ,train是否为训练集, download是否下载, transform数据怎么处理(之前写好的预处理方法)
    train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
                                             download=False, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
                                               shuffle=True, num_workers=0)

   # 测试集打不打乱没有影响 直接用了5000为一个批次(本来是使用10000的 但是好像效果没有5000好)
    test_set = torchvision.datasets.CIFAR10(root='./data', train=False,
                                            download=False, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=5000,
                                              shuffle=False, num_workers=0)
    # iter:迭代器,使用next可以调用下一批数据 我们设置为迭代器之后只用了一个next,
    #也就是调用了第一批数据5000张作为测试集的输入【我自己的理解,不知道对不对】
    test_data_iter = iter(test_loader)
    test_image, test_label = test_data_iter.next()

#to(device)是为了后面的测试做准备 把测试集的图片和标签都传到gpu中 不然就会报我上面提到那个错误
    test_image, test_label = test_image.to(device), test_label.to(device)

    net = LeNet().to(device) # 网络模型扔到gpu
    loss_function = nn.CrossEntropyLoss() #损失函数使用交叉熵损失函数 内置了softmax函数
    optimizer = optim.Adam(net.parameters(), lr=0.001) #优化器使用Adam,第一个参数是网络参数,第二个是学习率

    save_path = './Lenet.pth' #训练好的模型要保存到本目录下的这个路径

    maxAcc = 0.0 #设置一个最优精确度 初始化0 
    for epoch in range(5):  # epoch设置成5 也就是训练的数据集50000张跑5次 每跑一次是一个epoch

        running_loss = 0.0 #设置一个损失率 初值为0 

        # 对一个batch的数据训练的过程称为 一个 iteration 或 step 训练集中用step 测试用迭代器iter 所以下面开始迭代训练集中的数据,step是迭代的次数 (多少个batch) data里面是图片和标签

        for step, data in enumerate(train_loader, start=0):
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device) #训练集放到gpu中

            optimizer.zero_grad() #历史损失梯度清零 选择在哪用可以变相增大batch??还是不太理解
            outputs = net(inputs) #前向传播 
            loss = loss_function(outputs, labels) # 计算损失函数
            loss.backward() #反向传播
            optimizer.step() # 设置权重 梯度更新

            # 训练集有50000个样本 每批训练batch_size=32张照片 完整的训练完50000 step为1562次 所以使用32时会输出三次 但是这里使用的36 50000/36=1388 所以只print两次
            running_loss += loss.item() # 计算每次的损失函数累计,是本来是tensor形式的,要求数值就用item()
            if step % 500 == 499:  # 训练五百次测试一次并打印精确率(一次只使用5000张照片)
                with torch.no_grad(): #测试过程中不计算梯度(测试过程不需要反向传播,浪费算力
                    outputs = net(test_image)  # [batch, 10]大小的矩阵 36行是一批照片,每张一行 10列是每个类的对应特征值
                    predict_y = torch.max(outputs, dim=1)[1] #dim(轴 0表示列 1表示行 所以就是求每行最大的 [1] 本来的输出是一个二元组 我们只有后面的label 前面是真实值
                    accuracy = torch.eq(predict_y, test_label).sum().item() / test_label.size(0) #size(0)行数 size(1)列数 item():tensor转成数值

                    print('[%d, %5d] train_loss: %.3f  test_accuracy: %.3f' %
                          (epoch + 1, step + 1, running_loss / 500, accuracy))

                    if accuracy>maxAcc:
                        maxAcc = accuracy
                        torch.save(net.state_dict(), save_path)
                        # torch.save()用来保存网络模型,如果只保存网络参数,两个参数是(net.state_dict(),PATH) 
                        # 如果是历史最优的就保存路径并打印这句话
                        print('save best model')
                    running_loss = 0.0 # 每一次测试重新计算损失率 清零

四、测试(test.py)

先贴代码

import torch
import torchvision.transforms as transforms
from PIL import Image
import matplotlib.pyplot as plt

from model import LeNet

def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)
    transform = transforms.Compose([
        transforms.Resize((32,32)),
        transforms.ToTensor(),
        transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
    ])
    classes = ('plane', 'car', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # net = LeNet()
    net = LeNet().to(device)
    net.load_state_dict(torch.load('Lenet.pth'))

    im = Image.open('1.jpg')
    plt.imshow(im) #接受一张图像 但是不展示出来 后续还可以进行draw等操作等用plt.show才会展示出来 比如本代码中后来给图片加了title
    im = transform(im) #转化成img或者numpy格式 [c h w]
    im = torch.unsqueeze(im, dim=0) #转化成tensor[b,c,h,w] 在最前面(在dim=0时)增加一个纬度b 

    with torch.no_grad():
        outputs = net(im.to(device))
        # numpy格式是cpu-only的 所以使用之前要先恢复到cpu 不然会报错TypeError: can’t convert CUDA tensor to numpy
        predict = torch.max(outputs, dim=1)[1].cpu().numpy() #是一个数组类型[6] 所以要转成int才能用 后面这个[1]是因为max函数输出两个值[val,index] 我们只需要index
        # max函数是输出一个data 一个index  maxavg函数只输出一个index
        predict_1 = torch.softmax(outputs, dim=1) #softmax本来输出是tensor格式 但是是高维
        predict_1 = torch.squeeze(predict_1).cpu().numpy() #在tensor的形式下去掉b纬度 然后转化成numpy数组 同样需要转回cpu
        plt.title(classes[int(predict)]) #给图片加上title 他的对应类名
        plt.show() #展示图片
    # print(predict_1) 
    print(classes[int(predict)])
    print('%.3f' %predict_1[int(predict)])

if __name__ == '__main__':
    main()

图片可视化,识别结果为frog
是青蛙的概率为0.953

测试集要说明的几个地方:

  • 预处理:除了train时的两个操作还多了resize,因为我们随便找到的图片大小不定,不符合lenet网络的输入规范,所以要给他resize成32*32的大小
  • 关于网络模型的保存和加载,一般我们只是保存网络模型的参数,所以:
# 保存网络中的参数, 速度快,占空间少 在train中
torch.save(net.state_dict(),PATH)
# 加载保存的部分参数 在test中
model_dict=model.load_state_dict(torch.load(PATH)) 
  •  最后输出概率的地方我可能写的比较繁琐,因为是我试出来的写法(写这篇之前我还没有看过其他类似结构的输出如何处理),我选择了查看这张图片的softmax输出值,把他转成numpy格式,并输出我们已经找到的index对应的值
  • numpy格式是只支持cpu的,所以使用转换的时候要先转到cpu

 完结撒花~~写这个好累,因为中途发现好多我自己的错误点,报了好多错,差点没运行起来,纪念一下,虽然我语言描述不准确,因为好多地方我也是按自己的想法来写的,并不是一定正确,如果有大佬看到了这篇请指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值