基于libtorch的LeNet-5卷积神经网络实现(2)--Cifar-10数据分类

上篇文章中我们使用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 int
torch::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来实现更加复杂的网络,敬请期待~

欢迎扫码关注以下微信公众号,接下来会不定时更新更加精彩的内容噢~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌萌哒程序猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值