系列文章目录个人CNN学习记录之LeNet pytorch代码分析文章目录系列文章目录前言一、Lenet模型架构一、网络的历史地位与意义二、网络结构层次详解二、代码分析model.pytrain.pypredict.py总结前言在日常工作中我专注于并行计算领域主要依托GPGPU、NPU等高算力芯片进行开发。当前高算力与AI已深度融合计算与人工智能二者相辅相成底层计算为实现通用算法与算子提供基础而AI模型则能反哺并优化传统算法的决策效率与性能。为系统构建这方面的知识体系我在公司导师的推荐下跟随up主“霹雳吧啦Wz”的CNN系列视频进行学习并通过博客记录学习过程融入自己的理解与总结。一、Lenet模型架构一、网络的历史地位与意义CNN雏形由Yann LeCun等人于1998年提出是第一个成功应用于手写数字识别的卷积神经网络奠定了现代CNN的基础架构。经典结构其“卷积层 → 池化层 → 卷积层 → 池化层 → 全连接层”的交替模式成为后续几乎所有深度CNN的通用设计范式。二、网络结构层次详解LeNet-5的结构清晰地展示了从原始图像到最终分类结果的数据变换过程过程需要注意每一层的输入输出的尺寸变化尺寸公式以前课程有记录N(W-F2P)/S1W输入尺寸F卷积核大小步长Spadding像素数P输入层尺寸32x32的灰度图像。说明输入图像被归一化并置于网络中心。这个尺寸大于实际数字如MNIST数据集的28x28为特征提取留出边界。第一组特征提取卷积层C1 池化层S2卷积层C1使用6个​5x5的卷积核无填充。产生 6个​特征图每个尺寸为 28x28(32-5)/11 28。功能初步提取边缘、角点等低级视觉特征。池化层下采样层S2采用 2x2 最大池化步长为2。产生 6个​ 特征图每个尺寸为 14x14(28/2 14)。功能降低特征图的空间尺寸和计算量同时增强特征的平移不变性。第二组特征提取卷积层C3 池化层S4卷积层C3使用16个​ 5x5的卷积核。产生 16个​ 特征图每个尺寸为 10x10 ((14-5)/11 10)。注意此层并非简单的全连接卷积而是设计了稀疏连接以模拟生物视觉并减少参数。图中标注的165x5即表示16个5x5的特征图。池化层S4再次使用 2x2 最大池化步长为2。产生 16个​ 特征图每个尺寸为 5x5(10/2 5)。分类器部分全连接层层C5是一个具有 120个​ 神经元或120维特征向量的全连接层。它将S4层输出的16个5x5特征图共16 * 5 * 5400个值展平并连接。层F6是一个具有 84个​ 神经元的全连接层。输出层由 10个​ 神经元组成分别对应数字0-9通常使用Softmax函数输出每个类别的概率。二、代码分析model.pyimport 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(3, 16, 5) self.pool1 nn.MaxPool2d(2, 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 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根据模型再去看代码确实很简单基本就是按照网络结构按照python提供的类进行模型创建。和前面不一样的是代码里第二个卷积层个数为32所以后面的尺寸有所变化。首先导入必要的模块torch.nnPyTorch的神经网络模块包含各种层和函数。torch.nn.functional通常简写为F包含各种激活函数、损失函数等。定义LeNet类继承自nn.Module。definit(self)定义初始化值得注意的是初始化中使用了super(LeNet, self).init()super().init()​ 确保了LeNet正确继承了nn.Module的全部功能在定义网络时通常都需要这个函数。nn.Conv2d(3, 16, 5)定义了第一层卷积层需要注意这个函数的参数根据图片可以看到Conv2d默认的参数顺序为in_channles输入特征矩阵的深度如第一层通常为RGB图像有三个分量深度为3)out_channels为卷积核的个数kernel_size为卷积核的尺寸。想要更详细的参数说明可以参考官方手册 https://docs.pytorch.org/docs/stable/index.htmlnn.MaxPool2d定义了池化层同样需要注意池化层的参数第一个参数为池化层尺寸第二个参数为步长如果不设置步长默认和尺寸相等。nn.Linear定义了全连接层同样需要注意全连接层的参数第一个参数为输入节点尺寸第二个参数为输出节点尺寸。第一个全连接层的输入节点尺寸即为特征提取后的特征矩阵展平得来后续的全连接层输入节点等于前一层的输出节点尺寸。最后一个全连接层的输出节点与分类类别种类个数相同。def forward(self, x)定义前向传播过程x代表输入的数据注意pytorch的数据通道排序为[batch,channel,height,width]。x F.relu(self.conv1(x)) 表示将数据通过第一个卷积层后再经过一个relu激活函数x self.pool1(x) 表示数据经过第一个池化层x F.relu(self.conv2(x)) 表示数据经过第二个卷积层后再经过一个relu激活函数x self.pool2(x) 表示数据经过第二个池化层x x.view(-1, 3255) 表示将数据通过view函数做展平处理变成一维向量x F.relu(self.fc1(x)) 表示将展平后的数据通过第一个全连接层再经过一个relu激活函数x F.relu(self.fc2(x)) 表示数据经过第二个全连接层再经过一个relu激活函数x self.fc3(x) 表示数据经过最后一个全连接层这里代码没有通过softmax做归一化是因为训练那里计算会有体现。因此这里不需要。train.pydef 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, trainTrue, downloadTrue, transformtransform) train_loader torch.utils.data.DataLoader(train_set, batch_size36, shuffleTrue, num_workers0) # 10000张验证图片 # 第一次使用时要将download设置为True才会自动去下载数据集 val_set torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform) val_loader torch.utils.data.DataLoader(val_set, batch_size5000, shuffleFalse, num_workers0) 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) net LeNet() loss_function nn.CrossEntropyLoss() optimizer optim.Adam(net.parameters(), lr0.001) for epoch in range(5): # loop over the dataset multiple times running_loss 0.0 for step, data in enumerate(train_loader, start0): # 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 with torch.no_grad(): outputs net(val_image) # [batch, 10] predict_y torch.max(outputs, dim1)[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()代码里通过train_set torchvision.datasets.CIFAR10(root‘./data’, trainTrue,downloadTrue, transformtransform)来进行训练数据集的下载root表示下载的目录traintrue表示下载的是训练数据集download可以控制是否下载transorm可以用来做数据预处理。代码里的transform首先通过transforms.ToTenso对输入图像做tensor转换然后通过transforms.Normalize对数据做归一化处理。torch.utils.data.DataLoader进行数据集的批次导入第一个参数为上面输出的训练数据集第二个参数为批次数第三个参数shuffle表示数据是否打乱一般都为true第四个参数num_workers为线程数。接下来同样的方法导入测试集代码里导入了10000张图片用于测试准确率不同的是测试集通过val_data_iter iter(val_loader)创建了一个迭代器然后通过next(): 从迭代器中获取下一批数据。由于val_loader的batch_size5000这里获取的是5000个样本。返回的val_image: 形状为[5000, 3, 32, 32]的张量返回的val_label: 形状为[5000]的张量net LeNet() 进行模型实例化loss_function nn.CrossEntropyLoss()定义损失函数为交叉熵损失函数optimizer optim.Adam(net.parameters(), lr0.001)定义优化器为Adam优化器lr为学习率。接下来进入训练过程代码最外层for循环定义了5次训练每一次训练通过enumerate函数去遍历训练集然后通过optimizer.zero_grad()清除历史梯度。outputs net(val_image) 经过网络模型得到输出loss loss_function(outputs, labels)计算损失loss.backward()进行反向传播optimizer.step()通过优化器进行参数的更新。 running_loss loss.item()进行损失累计计算。if step % 500 499:定义每500个batch验证一次with torch.no_grad():可以禁用梯度计算节省内存。outputs net(val_image)通过将数据集送入网络得到输出predict_y torch.max(outputs, dim1)[1] 获得预测的类别accuracy torch.eq(predict_y, val_label).sum().item() / val_label.size(0)计算准确率。最后打印这些数据。predict.pyimport 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) im transform(im) # [C, H, W] im torch.unsqueeze(im, dim0) # [N, C, H, W] with torch.no_grad(): outputs net(im) predict torch.max(outputs, dim1)[1].numpy() print(classes[int(predict)]) if __name__ __main__: main()验证脚本先通过transforms.Compose定义了数据预处理方式与训练脚本不同的是增加了Resize((32, 32))将输入图片调整为32×32。原因是LeNet网络要求输入为32×32但测试图片可能是任意尺寸。classes (‘plane’, ‘car’, ‘bird’, ‘cat’,‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’)定义标签CIFAR-10的10个类别名称顺序必须与训练时一致net LeNet() 实例化模型结构net.load_state_dict(torch.load(‘Lenet.pth’))加载训练好的权重im Image.open(‘1.jpg’) 打开图片文件im transform(im) 图片预处理transform处理后的维度变化原始图片: PIL.Image对象↓ transforms.Resize((32, 32))调整大小: (3, 32, 32) 但仍为PIL.Image↓ transforms.ToTensor()转换为Tensor: torch.Tensor [3, 32, 32] 值范围[0,1]↓ transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))标准化: torch.Tensor [3, 32, 32] 值范围[-1,1]im torch.unsqueeze(im, dim0) 调整维度原始维度[3, 32, 32]添加批次维度[1, 3, 32, 32]网络期望输入格式[batch_size, channels, height, width]with torch.no_grad(): 禁用梯度计算outputs net(im) 输入网络得到输出predict torch.max(outputs, dim1)[1].numpy() 获取预测类别总结以上就是今天要讲的内容