“在前文中,我们搭建了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(0, 0) = 1;
t_mat.at(0, 2) = x; //水平平移量
t_mat.at(1, 1) = 1;
t_mat.at(1, 2) = y; //竖直平移量
//根据平移矩阵进行仿射变换
cv::warpAffine(src, dst, t_mat, dst_sz, INTER_CUBIC, BORDER_REPLICATE);
}
//旋转函数,angle为旋转角度,0~360
void 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);
}
else if(a == 1) //a为1则平移
{
float x = 10.0*(rand() / double(RAND_MAX)) - 5; //-5~5之间的随机数
float y = 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 &img_list, vector &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转换RGB
cvtColor(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((long long)labels.ptr(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 }))))
//这里是新加入的batch normalization层
, 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)
{
namespace F = torch::nn::functional;
auto x = 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::BatchNorm2d c1b; //batchnorm在卷积层之后、激活函数之前
torch::nn::Conv2d conv2;
torch::nn::BatchNorm2d c2b;
torch::nn::Conv2d conv3;
torch::nn::BatchNorm2d c3b;
torch::nn::Conv2d conv4;
torch::nn::BatchNorm2d c4b;
torch::nn::Conv2d conv5;
torch::nn::BatchNorm2d c5b;
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 train_img_total;
vector train_label_total;
AlexNet net1(10); //定义Alexnet网络结构体
net1.train(); //切换到训练模式
net1.to(device_type); //将网络类型切换到GPU,以加速运行
//定义交叉熵函数
auto criterion = torch::nn::CrossEntropyLoss();
//训练300个spoch
int kNumberOfEpochs = 300;
//学习率
double alpha = 0.001;
int batch_size = 32; //batch size
vector img_list;
vector 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 loss_d; //保存每50个batch的loss平均值
for (int epoch = 0; epoch < kNumberOfEpochs; epoch++)
{
printf("epoch:%d\n", epoch + 1);
srand((unsigned int)(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个batch
for (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 test_img;
vector 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*32
torch::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 ~ 9
auto predicted = (torch::max)(outputs, 1);
// 比较预测结果和实际结果,并更新统计结果
if (labels[0].item() == std::get<1>(predicted).item())
passed_test_items++;
total_test_items++;
printf("label: %d.\n", labels[0].item());
printf("predicted label: %d.\n", std::get<1>(predicted).item());
}
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看看,准确率是不是会提高。接下来我们也继续尝试实现别的深度学习网络,看看识别的准确率会有什么不同。
欢迎扫码关注本微信公众号,接下来会不定时更新更加精彩的内容,敬请期待~
点击进入留言区