“在前文中,我们搭建了Alexnet网络并用于Cifar-10数据集的训练与分类,可是对验证数据分类的准确率只达到56.59%,这个准确率对于比Lenet-5网络更复杂的Alexnet网络来说并不理想,在本文中,我们将在前文的基础上,尝试采取一些措施来提高网络对Cifar-10数据集分类的准确性。”
基于libtorch的Alexnet深度学习网络实现——Alexnet网络结构与原理
基于libtorch的Alexnet深度学习网络实现——Cifar-10数据集分类
我们在本文中提升分类准确率的措施主要有:
1. 网络结构微调; 2. 增加batch normalization层; 3. 数据增强。 4. 增加网络的训练模式、测试模式切换。
01
—
网络结构的微调
如上图所示,在前文的基础上,我们对网络结构主要作以下调整(这里主要是个人多次尝试的结果):
1. 把三个最大值池化层的池化窗口都改成2*2,步长也都改为2;
2. 减少三个Affine层的神经元数:
3. 去掉fc3层的dropout。
02
—
batch normalization
我们使用梯度下降法对神经网络的参数进行优化时,数据输入网络之前一般都做一个normalization操作,把数据钳制到一定范围,确保不同样本的数据都属于同一量级,这样可提升训练模型的泛化能力(减缓过拟合)。然而存在这样的问题:数据虽然在输入端被normalize了,但是经过网络的中间层处理之后,极有可能又变成不同量级、不同分布的数据了。为解决该问题,人们在网络的中间层也作normalization操作,而batch normalization(简称BN)就是这样的一种很常用的方法。
1. 增加batch normalization操作的主要好处
(1)加速学习速度。
(2)使得网络对学习相关的参数不那么敏感,通俗说就是使调参过程更加傻瓜化。
比如合适的学习率对网络的学习速度、学习效果相当重要,我们调参时必须谨慎地调节学习率,但如果增加了BN层,则无需那么依赖学习率的合适性,即使设置一个偏大或偏小的学习率也影响不大。
(3)改善sigmoid、tanh等激活函数的饱和问题(缓解梯度消失)。
比如sigmoid函数的曲线如下图所示,当输入值很大时输出值将接近1,输入值很小时输出值将接近0,这两种情况的共同点是输出值变化很小,导致梯度消失,严重影响学习效率。但增加BN之后,可以把数据钳制到较小值的范围内,所以很大程度减缓梯度消失问题。
(4)一定程度地提升网络地泛化能力。
BN操作时使用到每个batch的均值、方差,而不同batch的均值、方差是不一样的,这就相当于给数据添加了一定的随机因子,提升了网络的泛化能力,这与dropout通过随机删除神经元来添加随机因子的原理类似。
2. BN操作添加的位置
BN操作也可以看作是神经网络中的一个层,该层通常加在卷积操作或Affine操作之后、激活函数之前,如下图所示:
3. BN层计算原理
前文我们讲过batch训练的方法,每次从训练数据集中随机取N(N> 1,N通常称为batch size)个样本,然后N个样本分别输入神经网络执行前向传播,得到对应的N个损失函数值Yi(0 ≤ i < N),再计算这N个损失函数值的均值Y作为本轮迭代的损失函数值,然后使用Y进行误反向传播,计算并使用梯度进行网络参数更新。
由于每个batch有N个样本,网络中每一层的每个神经元则有对应于N个样本的N个输出,BN就是针对每个神经元的这N个输出进行的。
(1) Affine输出的BN操作
对于Affine层,1个样本在1个神经元的输出为1个值,对应一个batch的N个样本该神经元总共有N个输出值,通过计算这N个值的均值、方差来进行normalize。假设某一层的第i个神经元输出为xi1、xi2、…、xiN,对其进行BN操作如下式所示,其中ε为一个较小值,用于防止σ为0时计算异常的情况。
经过以上计算之后,数据会丢失一定量的原有信息,也即丧失一定量的特征表达,为解决此问题,在以上计算的基础上再增加γ和β两个参数,对normalize之后的数据进行线性变换,如下式所示,该式就是Affine层的BN计算公式,其中γ和β的值是学习得来的,也即像网络的权重参数一样在训练过程中被一步步地调整。
假设某个Affine层有5个神经元,batch size为4(N=4),那么该层的BN操作示意图如下图所示:
(2) 卷积输出的BN操作
对于卷积层,1个样本在1个神经元的输出为1个m*n二维矩阵,对应一个batch的N个样本该神经元总共有N个m*n输出矩阵,通过计算这N个矩阵所包含所有值(N*m*n)的均值、方差来进行normalize。假设某一卷积层的第i个神经元输出为矩阵Ii1、Ii2、…、IiN,并假设第j个样本的输出矩阵中点(c, r)的值为Iij(c,r),那么该对该神经元输出的BN计算如下式所示。与Affine层的BN计算类似,其中ε为一个较小值,用于防止σ为0时计算异常的情况,γ和β的值也是通过学习得来的,一个batch中所有样本在同一神经元的全部输出值共用相同的γ和β值。
假设某个卷积层有5个神经元,batch size为4(N=4),那么该层的BN操作示意图如下图所示:
4. 测试阶段的BN计算
以上讲的BN计算原理属于训练过程中的BN计算,那么测试阶段BN层怎么计算呢?我们知道,训练阶段中一个batch通常有多个样本,然后通过计算这多个样本的神经元输出值的均值、方差来进行normalize。然而测试阶段通常只输入1个样本,这样一来计算1个样本输出值的均值、方差就没有意义了,于是人们改变了计算方法:
针对网络中某一层的某一个神经元,记录在训练过程中该神经元对所有batch的输出的均值、方差,然后使用这些记录的均值、方差来计算测试阶段该神经元的BN层需要的均值、方差。假设该神经元在训练过程中总共处理了A个batch样本(batch size=N),对应A个batch样本的输出值的均值、方差为:
那么测试阶段该神经元的BN层的均值、方差可按下式计算:
从而测试阶段对于该神经元的每个输出值x进行的BN计算如下式,其中γ和β参数在训练阶段已经确定下来:
在pytorch/libtorch的实际实现中,计算方式与上述公式又略有区别,它们通过滑动平均的方式来计算测试阶段的均值、方差,这样,从第1到第A个batch(1≤k≤A)计算结束之后,测试阶段的均值、方差也就确定了下来。计算公式如下式,其中momentum称为动量值,默认取0.1。
5. libtorch的BN层参数说明
使用libtorch搭建网络时,可以按照以下方式定义一个BN层:
//二维batch norm,通常加在卷积操作的后面BN1 = register_module("BN1", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(64).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true))));//一维batch norm,通常加载Affine操作的后面BN2=register_module("BN2",torch::nn::BatchNorm1d(torch::nn::BatchNorm1dOptions(64).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true))));
从以上代码可以看到,定义BN层的同时指定了5个输入参数,下面分别说明:
torch::nn::BatchNorm2dOptions(64):这里的64指的是本卷积层或Affine层的神经元的个数,又比如本层有256个神经元,对应1个样本有256个输出值(矩阵),那么BatchNorm2dOptions()中则填写256。
eps:这里的eps则是以上计算公式中的ε,用于防止方差为0时计算异常的情况。
momentum:这里的momentum则是计算测试阶段的均值、方差时使用的滑动平均算法的动量值。
affine:这个affine参数是一个bool值,为true表示加入γ和β参数进行计算,为false则不加入。
track_running_stats:该参数也是bool值,如果为true则在训练阶段采用针对当前处理的batch实时计算均值、方差,并采用滑动平均算法计算测试阶段的均值和方差。如果为false则训练阶段、测试阶段都采用实时计算的均值、方差。所以一般都设置为true,这样效果会更好。
03
—
数据增强
在深度学习领域中,经常通过对原有训练数据进行一些随机变换,是使得训练数据具有更加广泛的代表性,从而提升训练模型的泛化能力。常见的变换有随机平移、随机旋转、水平/垂直翻转、添加高斯噪声等,本文我们只做简单的数据增强:随机平移和随即旋转。使用Opencv就可以轻易实现(本文使用了BORDER_REPLICATE的边缘填充方式):
//平移函数,x、y分别为水平、竖直方向的平移量void my_pingyi(Mat src, Mat& dst, float x, float y){cv::Size dst_sz = src.size();cv::Mat t_mat = cv::Mat::zeros(2, 3, CV_32FC1); //定义平移矩阵t_mat.at<float>(0, 0) = 1;t_mat.at<float>(0, 2) = x; //水平平移量t_mat.at<float>(1, 1) = 1;t_mat.at<float>(1, 2) = y; //竖直平移量//根据平移矩阵进行仿射变换cv::warpAffine(src, dst, t_mat, dst_sz, INTER_CUBIC, BORDER_REPLICATE);}//旋转函数,angle为旋转角度,0~360void my_rotate(cv::Mat src, cv::Mat& dst, double angle){cv::Size src_sz = src.size();cv::Size dst_sz(src_sz.width, src_sz.height);cv::Point center = cv::Point(src.cols / 2, src.rows / 2);//旋转中心cv::Mat rot_mat = cv::getRotationMatrix2D(center, angle, 1.0);//获得仿射变换矩阵cv::warpAffine(src, dst, rot_mat, dst_sz, INTER_CUBIC, BORDER_REPLICATE);}//随机平移、旋转函数void data_ehance(Mat src, Mat& dst){int a = rand() % 3; //生成0、1、2的随机数if (a == 0) //a为0则旋转{double angle = 359.0*(rand() / double(RAND_MAX)); //0~359之间的随机数my_rotate(src,dst,angle);}elseif(a==1)//a为1则平移{floatx=10.0*(rand()/double(RAND_MAX))-5;//-5~5之间的随机数floaty=10.0*(rand()/double(RAND_MAX))-5; //-5~5之间的随机数my_pingyi(src, dst, x, y);}//a为2则不作处理}
本文实现的平移、旋转效果如下图所示:
04
—
网络训练模式、测试模式的切换
在前文我们并没有注意到这一点,网络中的一些模块/层在训练模式和测试模式下的计算方式是略有不同的,比如训练模式下需要dropout操作,但是测试时却不再需要,又比如上述讲的batch normalization操作在训练和测试时的计算方式是有区别的。
在libtorch中当然同时实现了这些模块/层的不同模式,我们需要做的很简单,只要在一行代码把网络对象设置为训练或测试模式即可:
AlexNet net1(10);net1.train();//设置为训练模式net1.eval(); //设置为测试模式
05
—
代码实现
本文的代码与前文的代码大同小异,所以本文我们只列出跟前文实现不一样的代码。
基于libtorch的Alexnet深度学习网络实现——Cifar-10数据集分类
1. batch样本的获取代码
//bin_path为tif文件的路径,注意文件名中的序号要替换成%d//比如:"cifar-10/img/%d.tif"//这里的shuffle_idx为数组train_image_shuffle_set中某一元素的地址://假如batch_size=32,传入train_image_shuffle_set,则取0~31地址中保存的顺序//如果传入&train_image_shuffle_set[32],则取32~63地址中保存的顺序,以此类推void read_cifar_batch(char *bin_path, Mat labels, size_t *shuffle_idx, int batch_size, vector<Mat> &img_list, vector<long long> &label_list){img_list.clear();label_list.clear();for (int i = 0; i < batch_size; i++){char str[200] = {0};sprintf(str, bin_path, shuffle_idx[i]);Mat img = imread(str, CV_LOAD_IMAGE_COLOR); //以BGR方式读取tif文件data_ehance(img,img);//数据增强,随机平移、旋转//将BGR转换RGBcvtColor(img, img, COLOR_BGR2RGB);img.convertTo(img, CV_32F, 1.0/255.0); img = (img - 0.5) / 0.5; //将图像数据值转换为-1.0~1.0之img_list.push_back(img.clone());//将图像保存到数组中//计算对应标签label.tif文件中的坐标int y = shuffle_idx[i] / labels.cols;int x = shuffle_idx[i] % labels.cols;//将标签保存到数组中,注意标签需要强制转换为long long型数据label_list.push_back((longlong)labels.ptr<uchar>(y)[x]);}}
2. 网络结构体定义代码
struct AlexNet : torch::nn::Module{AlexNet(int arg_padding = 10): conv1(register_module("conv1", torch::nn::Conv2d(torch::nn::Conv2dOptions(3, 64, { 3,3 }).padding(1).stride({ 1,1 }))))//这里是新加入的batchnormalization层,c1b(register_module("c1b",torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(64).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), conv2(register_module("conv2", torch::nn::Conv2d(torch::nn::Conv2dOptions(64, 192, { 3,3 }).padding(1).stride({ 1,1 }))))//这里是新加入的batch normalization层,c2b(register_module("c2b",torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(192).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), conv3(register_module("conv3", torch::nn::Conv2d(torch::nn::Conv2dOptions(192, 384, { 3,3 }).padding(1).stride({ 1,1 }))))//这里是新加入的batch normalization层, c3b(register_module("c3b", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(384).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), conv4(register_module("conv4", torch::nn::Conv2d(torch::nn::Conv2dOptions(384, 256, { 3,3 }).padding(1).stride({ 1,1 }))))//这里是新加入的batch normalization层, c4b(register_module("c4b", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(256).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), conv5(register_module("conv5", torch::nn::Conv2d(torch::nn::Conv2dOptions(256, 256, { 3,3 }).padding(1).stride({ 1,1 })))) //256*4*4//这里是新加入的batch normalization层,c5b(register_module("c5b",torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(256).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), fc1(register_module("fc1", torch::nn::Linear(256*4*4, 2048)))//这里是新加入的batch normalization层, f1b(register_module("f1b", torch::nn::BatchNorm1d(torch::nn::BatchNorm1dOptions(2048).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), fc2(register_module("fc2", torch::nn::Linear(2048, 1024)))//这里是新加入的batch normalization层, f2b(register_module("f2b", torch::nn::BatchNorm1d(torch::nn::BatchNorm1dOptions(1024).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)))), fc3(register_module("fc3", torch::nn::Linear(1024, arg_padding))){}~AlexNet(){}//前向传播函数torch::Tensor forward(torch::Tensor input){namespaceF=torch::nn::functional;autox=F::max_pool2d(F::relu(c1b(conv1(input))),F::MaxPool2dFuncOptions(2).stride({2,2}));x=F::max_pool2d(F::relu(c2b(conv2(x))),F::MaxPool2dFuncOptions(2).stride({2,2}));x=F::relu(c3b(conv3(x)));x=F::relu(c4b(conv4(x)));x=F::max_pool2d(F::relu(c5b(conv5(x))),F::MaxPool2dFuncOptions(2).stride({2,2}));x=x.view({x.size(0),-1});x=F::dropout(x,F::DropoutFuncOptions().p(0.5));x=F::relu(f1b(fc1(x)));x=F::dropout(x,F::DropoutFuncOptions().p(0.5));x=F::relu(f2b(fc2(x)));x = fc3(x);return x;}torch::nn::Conv2d conv1;torch::nn::BatchNorm2dc1b;//batchnorm在卷积层之后、激活函数之前torch::nn::Conv2d conv2;torch::nn::BatchNorm2dc2b;torch::nn::Conv2d conv3;torch::nn::BatchNorm2d c3b;torch::nn::Conv2d conv4;torch::nn::BatchNorm2d c4b;torch::nn::Conv2d conv5;torch::nn::BatchNorm2dc5b;torch::nn::Linear fc1;torch::nn::BatchNorm1d f1b; //batchnorm在Affine层之后、激活函数之前torch::nn::Linear fc2;torch::nn::BatchNorm1d f2b;torch::nn::Linear fc3;};
3. 训练代码
void tran_alexnet_cifar_10_batch(void){vector<Mat> train_img_total;vector<uchar> train_label_total;AlexNet net1(10); //定义Alexnet网络结构体net1.train(); //切换到训练模式net1.to(device_type); //将网络类型切换到GPU,以加速运行//定义交叉熵函数auto criterion = torch::nn::CrossEntropyLoss();//训练300个spochint kNumberOfEpochs = 300;//学习率double alpha = 0.001;int batch_size = 32; //batch sizevector<Mat> img_list;vector<long long> label_list;//读取存储标签的tif文件Mat label_mat = imread("cifar-10/label/label.tif", CV_LOAD_IMAGE_GRAYSCALE);//定义梯度下降优化器,momentum模式auto optimizer = torch::optim::SGD(net1.parameters(), torch::optim::SGDOptions(alpha).momentum(0.9));vector<float> loss_d; //保存每50个batch的loss平均值for (int epoch = 0; epoch < kNumberOfEpochs; epoch++){printf("epoch:%d\n", epoch + 1);srand((unsignedint)(time(NULL)));//设置数据增强的随机种子//batch训练之前打乱读取数据的顺序train_image_shuffle_set.clear();for (size_t i = 0; i < CIFAT_10_TOTAL_DATANUM; i++){train_image_shuffle_set.push_back(i);}std::random_device rd;std::mt19937_64 g(rd());std::shuffle(train_image_shuffle_set.begin(), train_image_shuffle_set.end(), g); //打乱顺序auto running_loss = 0.;//总共有CIFAT_10_TOTAL_DATANUM/batch_size个batchfor (int k = 0; k < CIFAT_10_TOTAL_DATANUM/batch_size; k++){//按照打乱之后的顺序读取batch样本、标签read_cifar_batch("cifar-10/img/%d.tif", label_mat, &train_image_shuffle_set[k*batch_size], batch_size, img_list, label_list);auto inputs = torch::ones({ batch_size, 3, 32, 32 });for (int b = 0; b < batch_size; b++){inputs[b] = torch::from_blob(img_list[b].data, { img_list[b].channels(), img_list[b].rows, img_list[b].cols }, torch::kFloat).clone();}torch::Tensor labels = torch::tensor(label_list);//将样本、标签张量由CPU类型切换到GPU类型,对应于GPU类型的网络inputs = inputs.to(device_type);labels = labels.to(device_type);auto outputs = net1.forward(inputs); //前向传播auto loss = criterion(outputs, labels); //计算交叉熵误差optimizer.zero_grad(); //清零梯度loss.backward(); //误反向传播optimizer.step(); //更新参数running_loss += loss.item().toFloat();if ((k + 1) % 50 == 0){float l = running_loss / 50;loss_d.push_back(l);//记录每50个batch的loss平均值printf("loss: %f\n", l);running_loss = 0.;}}}write_float_to_file("loss.bin",loss_d);//将记录的loss值写入文件remove("mnist_cifar_10_alexnet.pt");printf("Finish training!\n");torch::serialize::OutputArchive archive;net1.save(archive);archive.save_to("mnist_cifar_10_alexnet.pt"); //保存训练好的模型printf("Save the training result to mnist_cifar_10_alexnet.pt.\n");}
4. 测试代码
void test_alexnet_cifar_10(void){AlexNet net1(10);torch::serialize::InputArchive archive;archive.load_from("mnist_cifar_10_alexnet.pt"); //从文件加载训练模型net1.load(archive); //将训练模型加载到网络net1.eval();//切换到测试模式net1.to(device_type); //将网络类型切换到GPU,以加速运行vector<Mat> test_img;vector<uchar> test_label;read_cifar_bin_rgb("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++){//将样本、标签转换为Tensor张量torch::Tensor inputs = torch::from_blob(test_img[i].data, { 1, test_img[i].channels(), test_img[i].rows, test_img[i].cols }, torch::kFloat); //1*1*32*32torch::Tensor labels = torch::tensor({ (long long)test_label[i] });//将样本、标签张量由CPU类型切换到GPU类型,对应于GPU类型的网络inputs = inputs.to(device_type);labels = labels.to(device_type);// 用训练好的网络处理测试数据,也即前向传播auto outputs = net1.forward(inputs);// 得到预测值,0 ~ 9auto 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);}
06
—
运行结果
运行上述代码,执行300个epoch的训练,然后使用训练模型对测试数据进行分类,得到的准确率达到72.02%,相比之前的56.59%提高了很多,因此本文所做的措施还是相当有效的。我们记录每50个batch的loss平均值,如下图所示:
本文我们只训练300个epoch,其实还可以尝试更多的epoch看看,准确率是不是会提高。接下来我们也继续尝试实现别的深度学习网络,看看识别的准确率会有什么不同。
欢迎扫码关注本微信公众号,接下来会不定时更新更加精彩的内容,敬请期待~
点击进入留言区
如果觉得《基于libtorch的Alexnet深度学习网络实现——Cifar-10数据集分类(提升准确率)》对你有帮助,请点赞、收藏,并留下你的观点哦!