前言:
今天各位老铁们对“lenet5python3”都比较关心,姐妹们都想要知道一些“lenet5python3”的相关知识。那么小编在网上搜集了一些关于“lenet5python3””的相关文章,希望你们能喜欢,看官们快快来了解一下吧!上篇文章中我们使用libtorch实现了LeNet-5卷积神经网络,并对Minst数据集进行训练与分类。本文我们尝试使用该实现的网络对更加复杂的Cifar-10数据集进行训练、分类。
基于libtorch的LeNet-5卷积神经网络实现
LeNet-5网络地总体结构如下,详细请参考上方地链接。
1. Cifar-10数据集介绍
Cifar-10是一个专门用于测试图像分类的公开数据集,其包含的彩色图像分为10种类型:飞机、轿车、鸟、猫、鹿、狗、蛙、马、船、货车。且这10种类型图像的标签依次为0、1、2、3、4、5、6、7、8、9。
该数据集分为Python、Matlab、C/C++三个不同的版本,顾名思义,三个版本分别适用于对应的三种编程语言。因为我们使用的是C/C++语言,所以使用对应的C/C++版本就好,该版本的数据集包含6个bin文件,如下图所示,其中data_batch_1.bin~data_batch_5.bin通常用于训练,而test_batch.bin则用于训练之后的识别测试。
如下图所示,每个bin文件包含10000*3073个字节数据,在每个3073数据块中,第一个字节是0~9的标签,后面3072字节则是彩色图像的三通道数据:红通道 --> 绿通道 --> 蓝通道 (1024 --> 1024 --> 1024)。其中每1024字节的数据就是一帧单通道的32*32图像,3帧32*32字节的单通道图像则组成了一帧彩色图像。所以总体来说,每一个bin文件包含了10000帧32*32的彩色图像。
2. 训练策略
(1) epoch
首先我们讲一下epoch的概念:一个epoch就是将所有的训练数据都输入神经网络,并完成前向传播、反向传播、参数更新的过程。比如我们使用Cifar-10数据集进行训练的时候,训练数据包含于5个bin文件中,每个bin文件有10000张32*32图像,因此总共有5*10000张训练图像,当我们把这5*10000张训练图像都输入网络并完成训练的过程,就是一个epoch过程。
然而,往往一个epoch过程过程达不到参数收敛的目的,因此需要执行多个epoch过程,也就是说:使用这5*10000张训练图像完成一次训练之后,在此次训练得到参数模型的基础上,再重复使用这5*10000张训练图像进行下一轮训练。
(2) 学习率的改变
我们把学习率α的初始值设置为0.001,每完成一个epoch,参数都进一步接近收敛状态,因此这个时候我们需要适当比例地减小α,以缩短步长:
α = α*0.8
3. 数据格式转换
(1) 图像格式转换
我们使用Opencv来读取Cifar-10图像为Mat格式,但是libtorch框架处理的数据格式为Tensor格式,因此首先需要把Mat格式的图像转换为Tensor格式。调用from_blob函数可方便进行转换,不过要注意转换时需指定数据维度为1*1*row*col,其中row、col分别为图像的高、宽。
//test_img[i]为Mat格式,test_img[i].data为Mat的数据首地址//{ 1, 1, test_img[i].rows, test_img[i].cols }指定Tensor张量的维度为1*1*row*col//torch::kFloat表示以float格式转换数据,该类型要与Mat本来的数据类型相一致,否则会出错torch::Tensor inputs = torch::from_blob(test_img[i].data, { 1, 1, test_img[i].rows, test_img[i].cols }, torch::kFloat);
(1) 标签格式转换
我们读取的标签为单个uchar型数据,但是libtorch框架处理的数据格式为Tensor格式,因此首先需要把单个uchar数据转换为Tensor格式。调用from_blob函数也可方便转换,同样要注意转换时需指定数据维度为1,也就是只有一个数据的张量。
//test_label[i]为一个uchar数据,&test_label[i]表示该数据的地址//{ 1 }表示该张量的维度为1//torch::kByte表示以uchar类型读取该数据,该参数需要与数据本身的类型一致//toType(torch::kLong)表示把Tensor张量转换为long int类型,因为要求标签的类型为long inttorch::Tensor labels = torch::from_blob(&test_label[i], { 1 }, torch::kByte).toType(torch::kLong);
4. 主要代码实现
(1) 读取Cifar-10数据与标签代码
读取到的图像为uchar型的三通道彩色图像,因此需要将其转换为单通道灰度图,并转换为-1~1之间的浮点型数据,方便后续的训练、分类。
void read_cifar_bin(char *bin_path, vector<Mat> &img_liat, vector<uchar> &label_list){ const int img_num = 10000; const int img_size = 3073; //第一字节是标签 const int img_size_1 = 1024; const int data_size = img_num * img_size; const int row = 32; const int col = 32; uchar *cifar_data = (uchar *)malloc(data_size); if (cifar_data == NULL) { cout << "malloc failed" << endl; return; } FILE *fp = fopen(bin_path, "rb"); if (fp == NULL) { cout << "fopen file failed" << endl; free(cifar_data); return; } fread(cifar_data, 1, data_size, fp); img_liat.clear(); label_list.clear(); for (int i = 0; i < img_num; i++) { long int offset = i * img_size; long int offset0 = offset + 1; //红 long int offset1 = offset0 + img_size_1; //绿 long int offset2 = offset1 + img_size_1; //蓝 uchar label = cifar_data[offset]; //标签 Mat img(row, col, CV_8UC3); for (int y = 0; y < row; y++) { for (int x = 0; x < col; x++) { int idx = y * col + x; img.at<Vec3b>(y, x) = Vec3b(cifar_data[offset2 + idx], cifar_data[offset1 + idx], cifar_data[offset0 + idx]); //BGR } } Mat gray; cvtColor(img, gray, COLOR_BGR2GRAY); //三通道彩色图转换为单通道灰度图 gray.convertTo(gray, CV_32F); //uchar转换为float gray = gray / 255.0; //范围0~1 gray = (gray - 0.5) / 0.5; //范围-1~1 img_liat.push_back(gray.clone()); //float label_list.push_back(label); //uchar } fclose(fp); free(cifar_data);}
(2) LeNet-5网络定义
struct LeNet5 : torch::nn::Module{ //arg_padding为C1层的padding参数,当输入图像为28*28时,需要将其填充为32x32的图像 //这里可能有人会有疑惑,为什么没有定义S2、S4层,这是因为池化层放在前向传播函数中执行即可,不需要再定义了,详细见forward函数 LeNet5(int arg_padding = 0) //C1层 : C1(register_module("C1", torch::nn::Conv2d(torch::nn::Conv2dOptions(1, 6, 5).padding(arg_padding)))) //C3层 , C3(register_module("C3", torch::nn::Conv2d(6, 16, 5))) //C5层 , C5(register_module("C5", torch::nn::Conv2d(16, 120, 5))) //F6层 , F6(register_module("F6", torch::nn::Linear(120, 84))) //OUTPUT层 , OUTPUT(register_module("OUTPUT", torch::nn::Linear(84, 10))) { } ~LeNet5() { } //该函数用于将多维数据一维展开成一维向量 int64_t num_flat_features(torch::Tensor input) { int64_t num_features = 1; auto sizes = input.sizes(); for (auto s : sizes) { num_features *= s; } return num_features; } //前向传播函数 torch::Tensor forward(torch::Tensor input) { namespace F = torch::nn::functional; //这一步其实包含了3个操作,首先是C1层的卷积,其次是将卷积结果输入Relu函数,接着是将Relu函数的结果做最大值的池化操作 auto x = F::max_pool2d(F::relu(C1(input)), F::MaxPool2dFuncOptions({ 2,2 })); //这一步也包含了3个操作,首先是C3层的卷积,其次是将卷积结果输入Relu函数,接着是将Relu函数的结果做最大值的池化操作 x = F::max_pool2d(F::relu(C3(x)), F::MaxPool2dFuncOptions({ 2,2 })); //将C5层的卷积结果输入Relu函数,Relu函数的结果作为本层输出 x = F::relu(C5(x)); //120张1*1的卷积结果图像按顺序展开成长度为120的一维向量 x = x.view({ -1, num_flat_features(x) }); //F6层的Affine计算 x = F::relu(F6(x)); //OUTPUT层的Affine计算,注意这里不包括Softmax层计算,Softmax层计算放到后面的交叉熵误差函数中去做 x = OUTPUT(x); return x; } //要求这里的各层定义与本结构体开头处的定义保持一致 int m_padding = 0; torch::nn::Conv2d C1; torch::nn::Conv2d C3; torch::nn::Conv2d C5; torch::nn::Linear F6; torch::nn::Linear OUTPUT;};
(3) 训练代码
Cifar-10图像本来就是32*32大小,因此不需要像Minst数据集那样填充数据。
void tran_lenet_5_cifar_10(void){ vector<Mat> train_img_total; vector<uchar> train_label_total; //定义一个LeNet-5网络结构体,输入的图像是32x32图像,不需要填充 LeNet5 net1(0); //使用交叉熵误差函数 auto criterion = torch::nn::CrossEntropyLoss(); int kNumberOfEpochs = 8; //训练8个epoch int data_file_num = 5; //总共5个训练文件 double alpha = 0.001; //学习率初始值0.001 for (int epoch = 0; epoch < kNumberOfEpochs; epoch++) { printf("epoch:%d\n", epoch+1); //定义梯度下降法优化器 auto optimizer = torch::optim::SGD(net1.parameters(), torch::optim::SGDOptions(alpha).momentum(0.9)); for (int k = 1; k <= data_file_num; k++) { printf("data_file_num:%d\n", k); auto running_loss = 0.; char str[200] = { 0 }; sprintf(str, "D:/Program Files (x86)/Microsoft Visual Studio 14.0/prj/KNN_test/KNN_test/cifar-10-batches-bin/data_batch_%d.bin", k); //读取10000张cifar-10训练图像 read_cifar_bin(str, train_img_total, train_label_total); for (int i = 0; i < train_img_total.size(); i++) { //Mat转换为Tensor torch::Tensor inputs = torch::from_blob(train_img_total[i].data, { 1, 1, train_img_total[i].rows, train_img_total[i].cols }, torch::kFloat); //1*1*32*32 //uchar转换为Tensor torch::Tensor labels = torch::from_blob(&train_label_total[i], { 1 }, torch::kByte).toType(torch::kLong); //1 //清零梯度 optimizer.zero_grad(); //前向传播 auto outputs = net1.forward(inputs); //计算交叉熵误差 auto loss = criterion(outputs, labels); //误差反向传播 loss.backward(); //更新参数 optimizer.step(); //累加每1000个误差值,方便查看训练时交叉熵误差函数的下降情况 running_loss += loss.item().toFloat(); if ((i + 1) % 1000 == 0) { printf("loss: %.3f\n", running_loss / 1000); running_loss = 0.; } } } alpha *= 0.8; //完成一个epoch,减小学习率 } printf("Finish training!\n"); torch::serialize::OutputArchive archive; net1.save(archive); archive.save_to("mnist_cifar_10.pt"); printf("Save the training result to mnist.pt.\n");}
(4) 分类测试代码
void test_lenet_5_cifar_10(void){ LeNet5 net1(0); torch::serialize::InputArchive archive; archive.load_from("mnist_cifar_10.pt"); //加载上一步骤训练好的模型 net1.load(archive); //读取测试数据与标签 vector<Mat> test_img; vector<uchar> test_label; read_cifar_bin("D:/Program Files (x86)/Microsoft Visual Studio 14.0/prj/KNN_test/KNN_test/cifar-10-batches-bin/test_batch.bin", test_img, test_label); int total_test_items = 0, passed_test_items = 0; double total_time = 0.0; for (int i = 0; i < test_img.size(); i++) { //将Mat格式转换为Tensor格式 torch::Tensor inputs = torch::from_blob(test_img[i].data, { 1, 1, test_img[i].rows, test_img[i].cols }, torch::kFloat); //1*1*32*32 //uchar转换为Tensor torch::Tensor labels = torch::from_blob(&test_label[i], { 1 }, torch::kByte).toType(torch::kLong); //1 //使用训练好的模型对测试数据进行分类,也即前向传播过程 auto outputs = net1.forward(inputs); //得到分类值,0 ~ 9 auto predicted = (torch::max)(outputs, 1); //比较分类结果和对应的标签是否一致,如果一致则认为分类正确 if (labels[0].item<int>() == std::get<1>(predicted).item<int>()) passed_test_items++; total_test_items++; printf("label: %d.\n", labels[0].item<int>()); printf("predicted label: %d.\n", std::get<1>(predicted).item<int>()); } printf("total_test_items=%d, passed_test_items=%d, pass rate=%f\n", total_test_items, passed_test_items, passed_test_items*1.0 / total_test_items);}
(5) main函数
int main(){ tran_lenet_5_cifar_10(); //训练 test_lenet_5_cifar_10(); //测试 return EXIT_SUCCESS;}
5. 运行结果
运行上述代码,先训练模型、保存模型,然后再加载模型(实际使用时如果已经训练好模型,可直接加载模型而不需要再训练)。
训练时目标函数(损失函数)的变化如下图所示,可以看到其值逐步减小:
待训练完成以及保存模型之后,就可以加载模型对数据进行分类预测啦,我们的分类结果如下图所示,准确率达到了60.3%,这个分类结果并不理想,这是因为LeNet-5网络还是相对简单了,对相对Minst更复杂的数据分类效果并不好。因此在接下来的文章中我们将使用libtorch来实现更加复杂的网络,敬请期待~
欢迎关注“萌萌哒程序猴”微信公众号,接下来会不定时更新更加精彩的内容噢~
标签: #lenet5python3