图像分类学习笔记(一)——LeNet

CNN(Convolutional Neural Network)的雏形是LeNet网络结构

一、model.py

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

# 定义一个类,继承来自于nn.Module这个父类
class LeNet(nn.Module):
    # 定义初始化函数
    def __init__(self):
        # 解决在多重继承中继承父类方法可能出现的一系列问题,涉及到多继承一般都会使用到这个函数
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, 5) #in_channels,out_channels,kernal_size
        self.pool1 = nn.MaxPool2d(2, 2) # kernel_size,stride
        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 = x.view(-1, 32*5*5)       # output(32*5*5) 展平
        # view中第一个参数设定为-1,代表动态调整这个维度上的元素个数,以保证元素的总数不变
        x = F.relu(self.fc1(x))      # output(120)
        x = F.relu(self.fc2(x))      # output(84)
        x = self.fc3(x)              # output(10)
        return x

初始化函数

  • self.conv1 = nn.Conv2d(3, 16, 5)
    • 彩色图片有R、G、B,三个通道,所以输入通道数量是3
    • 采用了16个卷积核,所以输出通道是16个
    • 卷积核的尺寸是5 X 5 的
  • self.pool1 = nn.MaxPool2d(2, 2)
    • 池化核的尺寸是2×2的
    • 步长为2
  • self.conv2 = nn.Conv2d(16, 32, 5)
    • 接受上一个层的输出,输入通道数量是16
    • 采用了32个卷积核,所以输出通道是32个
    • 卷积核的尺寸是5 X 5 的
  • self.pool2 = nn.MaxPool2d(2, 2)
    • 池化核的尺寸是2×2的
    • 步长为2
  • self.fc1 = nn.Linear(32*5*5, 120)
    • 第一层全连接层的节点个数是120
  • self.fc2 = nn.Linear(120, 84)
    • 接受上一全连接层的输出
    • 上一层有120个节点,第二层自己有84个节点
  • self.fc3 = nn.Linear(84, 10)
    • 接受上一全连接层的输出
    • 上一层是84个节点,又由于是分十类,所以最后的输出必须是10

正向传播过程

x是pytorch tensor的通道排列顺序:[batch,channel,height,width]

  • x = F.relu(self.conv1(x))
    • 输入是(3,32,32)的图片
    • 用了16个卷积核,输出channel是16
    • 尺寸N=(32-5+0)/1+1=28
    • 输出是(16,28,28)
  • x = self.pool1(x)
    • 接受上一个卷积层的输出(16,28,28)
    • 池化层不影响channel,只改变宽高,缩小两倍
    • 输出是(16,14,14)
  • x = F.relu(self.conv2(x))
    • 接受上一个池化层的输出(16,14,14)
    • 用了32个卷积核,输出channel是32
    • 尺寸N=(14-5+0)/1+1=10
    • 输出是(32,10,10)
  • x = self.pool2(x)
    • 接受上一个卷积层的输出(32,10,10)
    • 池化层不影响channel,只改变宽高,缩小两倍
    • 输出是(32,5,5)
  • x = x.view(-1,32*5*5)
    • 全连接需要的数据格式是一维向量
    • 把大小为(32, 5, 5)的图片,展平成32*5*5长度的向量
    • -1代表第一个维度自动推理,即batch
  • x = F.relu(self.fc1(x))
  • x = F.relu(self.fc2(x))
  • x = self.fc3(x)

 为什么最后没使用softmax函数:因为在训练网络时,进行计算卷积交叉熵的过程中,在它的内部已经实现了更加高效的softmax方法

 二、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
from matplotlib import pyplot as plt
import numpy as np

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

    # 50000张训练图片
    # 第一次使用时要将download设置为True才会自动去下载数据集
    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)

    # 10000张验证图片
    # 第一次使用时要将download设置为True才会自动去下载数据集
    val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
                                           download=False, transform=transform)
    val_loader = torch.utils.data.DataLoader(val_set, batch_size=10000,
                                             shuffle=False, num_workers=0)
    val_data_iter = iter(val_loader)  #迭代器
    val_image, val_label = next(val_data_iter)
    
    classes = ('plane', 'car', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # def imshow(img):
    #     img = img / 2 + 0.5  #unnormalize反标准化
    #     npimg = img.numpy()
    #     plt.imshow(np.transpose(npimg,(1,2,0)))  #0,1,2可看成索引
    #     plt.show()
    # 
    # # print labels
    # print(' '.join('%5s' % classes[val_label[j]] for j in range(4)))
    # # show images
    # imshow(torchvision.utils.make_grid(val_image))

    net = LeNet()
    loss_function = nn.CrossEntropyLoss()
    optimizer = optim.Adam(net.parameters(), lr=0.001)

    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

            # zero the parameter gradients
            optimizer.zero_grad()
            # forward + backward + optimize
            outputs = net(inputs)
            loss = loss_function(outputs, labels)
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()
            if step % 500 == 499:    # print every 500 mini-batches
                # 上下文管理器,可以禁用PyTorch自动求导机制,从而避免在评估模型时浪费内存和计算资源
                with torch.no_grad():
                    outputs = net(val_image)  # [batch, 10]
                    predict_y = torch.max(outputs, dim=1)[1]
                    accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)

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

    print('Finished Training')

    save_path = './Lenet.pth'
    torch.save(net.state_dict(), save_path)


if __name__ == '__main__':
    main()

  • transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    • 第一个(0.5, 0.5, 0.5)表示将每个通道的像素值都减去0.5,从而使其均值为0
    • 第二个(0.5, 0.5, 0.5)表示将每个通道的像素值都除以0.5,从而使其标准差为1
    • 这行代码的作用是对输入图像的每个通道进行归一化,使其像素值在[-1,1]之间
    • 注意:进行归一化操作时,需要保证输入图像的像素值为浮点数类型,且范围在[0,1]之间

思考:

(1)归一化就是要把图片3个通道中的数据整理到[-1, 1]区间,即x = (x - mean(x))/std(x),那么只要输入数据集x确定了,mean(x)和std(x)也就是确定的数值了,为什么Normalize()函数还需要输入mean和std的数值?

mean 和 std 要在normalize()之前自己先算好再传进去的,不然每次normalize()就得把所有的图片都读取一遍算mean和std

(2)RGB单个通道的值是[0, 255],所以一个通道的均值应该在127附近。如果Normalize()函数去计算 x = (x - mean)/std ,算出来的x就不可能落在[-1, 1]区间了。

  两种情况:

(a)如果是ImageNet数据集,那么ImageNet的数据在加载的时候就已经转换成了[0, 1]
(b) 应用了torchvision.transforms.ToTensor,其作用是将数据归一化到[0,1](是将数据除以255),transforms.ToTensor()会把HWC会变成C *H *W(拓展:格式为(h,w,c),像素顺序为RGB)

  •  torch.utils.data.DataLoader(train_set, batch_size=36,shuffle=True, num_workers=0)
    • 将训练数据集train_set按照batch_size分成若干个小批次

    • 在每个epoch开始时,将训练数据集打乱(shuffle=True),从而使得每个小批次的样本都是随机选择的

    • 使用num_workers参数指定加载数据所需的进程数,以加快数据加载速度(windows 系统下必须为0,否则报错)

    • 这行代码的作用是针对训练数据集train_set定义了一个数据加载器(Data Loader),用于将训练数据按批次加载到模型中进行训练

数据加载器中的每个小批次的样本数量通常会影响模型的训练效果。如果batch_size设置过小,每个小批次的样本数量太少,可能会导致模型欠拟合;如果batch_size设置过大,每个小批次的样本数量太多,可能会导致模型过拟合。因此,在进行模型训练时,需要根据实际情况适当调整batch_size的大小。

  •  val_data_iter = iter(val_loader) 
     val_image, val_label = next(val_data_iter)
    • 使用iter()函数将val_loader转换为一个可迭代的对象val_data_iter

    • 使用next()函数从val_data_iter中获取下一个小批次的数据

    • 将获取到的小批次的图像数据val_image和标签数据val_label分别存储到变量中

  • optimizer = optim.Adam(net.parameters(), lr=0.001)

    • 使用optim.Adam()函数创建一个Adam优化器对象optimizer
    • 将神经网络模型net的参数net.parameters()传递给优化器对象optimizer,用于优化模型的参数
    • 设置优化器的学习率(learning rate)为0.001,即在每次迭代中更新参数时的步长大小
  • for epoch in range(5):.....
    • 用于训练和验证神经网络模型
    • 使用一个外层循环来控制训练的epoch数,即将整个训练集遍历多次
    • 在每个epoch开始时,初始化训练损失running_loss为0
    • 使用for循环遍历训练数据集train_loader,每次获取一个小批次的数据inputs和标签labels
    • 将优化器optimizer中的参数梯度清零optimizer.zero_grad(),避免上一次迭代的残余梯度对本次迭代造成影响
    • 将小批次数据inputs输入到神经网络模型net中,得到模型在小批次数据上的输出outputs
    • 使用损失函数loss_function计算模型在小批次数据上的损失loss
    • 使用反向传播算法计算损失关于模型参数的梯度loss.backward(),并使用优化器optimizer更新模型参数optimizer.step()

    • 计算训练损失running_loss,用于后续的训练过程可视化

    • 在每个epoch的末尾,对验证集进行评估,计算模型在验证集上的准确率accuracy,并打印出当前epoch和训练过程中的训练损失与测试准确度

(1)loss.backward() 的作用:

  • 计算当前小批次数据上的损失函数loss关于神经网络模型中每个可训练参数(如权重和偏置)的梯度

  • 将计算得到的梯度存储在各个参数的.grad属性中

  • 计算完成后,可以使用优化器的step()函数来更新模型中的参数

(2)optimizer.step()的作用:

  • 根据前者算出的梯度更新得到一个新的参数,然后根据这两个参数对应的loss值来决定怎么更新参数,即取当前的权重还是取更新后的权重

(3) running_loss += loss.item()的作用:

  • 将当前小批次数据上的损失loss使用loss.item()函数转换为一个标量值,存储在变量loss_value中
  • 将loss_value加到训练损失running_loss中
  • 在训练过程中,running_loss记录了所有小批次数据的损失之和,用于后续的训练过程可视化

(4)predict_y = torch.max(outputs, dim=1)[1]:

  • dim=1指定沿着第1个维度(即列维度)进行最大值的查找,这样就可以得到每个输出向量中的最大值和对应的类别索引
  • [1]表示取得每个最大值对应的类别索引,即输出矩阵outputs中每行最大值所在的列索引,得到一个一维张量predict_y

(5)accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0):

  • 使用torch.eq()函数计算预测值predict_y与验证集标签val_label之间的匹配情况,得到正确预测的样本数量

  • 将正确预测的样本数量除以验证集的样本数量val_label.size(0),得到模型在验证集上的准确率accuracy

三、predict.py

import torch
import torchvision.transforms as transforms
from PIL import Image

from model import LeNet


def main():
    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.load_state_dict(torch.load('Lenet.pth'))

    im = Image.open('1.jpg') # [H, W ,C]
    im = transform(im)  # [C, H, W]
    im = torch.unsqueeze(im, dim=0)  # [N, C, H, W]

    with torch.no_grad():
        outputs = net(im)
        predict = torch.max(outputs, dim=1)[1].numpy()
        # predict = torch.softmax(outputs,dim=1)
    # print(predict)
    print(classes[int(predict)])


if __name__ == '__main__':
    main()
  • torch.unsqueeze(im, dim=0)
    • 作用:扩展维度
    • 在图像张量的第0维(批量维度)上增加一个维度,就是转换成标准的pytorch tensor的格式,以便将其作为一个批量输入到模型中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值