“ 前文我们使用libtorch实现的Resnet34网络对Cifar-10进行分类,测试集的分类准确率仅有74.95%,本文我们在前文的基础上做了一些改进,使得测试集的分类准确率达到94.15%。”
深度学习这玩意儿就像炼丹一样,很多时候并不是按照纸面上的配方来炼就好了,还需要在实践中多多尝试,比如各种调节火候、调整配方、改进炼丹炉等。
我们在前文的基础上,我们通过以下措施来提高Cifar-10测试集的分类准确率,下面将分别详细说明:
1. 对Resnet34网络的结构做了一点调节;
2. 增加全局对比度归一化的数据预处理;
3. 修改了Tensor张量的维度顺序(这一点最重要,之前犯了这个低级错误,导致准确率一直上不去)。
前文连接:
基于libtorch的Resnet34残差网络实现——Cifar-10分类
1. 首先是残差模块的调整:
原结构
修改之后的结构
我们知道,使用梯度下降法对神经网络的参数进行优化时,数据输入网络之前一般都做一个normalization操作,把数据钳制到一定范围,确保不同样本的数据都属于同一量级,加快训练速度。然而数据虽然在输入端被normalize了,但是经过网络的中间层处理之后,极有可能又变成不同量级、不同分布的数据了。所以在卷积层或Affine层之后、激活函数之前增加batch normalization层(简称BN层)就是为了把中间层的数据也钳制到一定范围。
基于以上原因,本人微调了残差模块的结构,使得数据在残差模块最后的Relu函数之前得到batch normalize,如上图所示:
把1*1卷积层后面的BN层去掉;
把conv2卷积层后面的BN层去掉;
在“out+Residual”后面增加BN层(也即把out+Residual信号通过BN层处理之后再输入Relu激活函数)。
2. 其次是残差模块的调整:
前文我们已经讲过,Resnet34网络可以分为6个大模块:
其中模块1由1个卷积层、1个Batchnorm层、1个Relu层、1个pool层构成,如下图所示:
原结构
本人在尝试的过程中,发现把以上模块1的Max池化层去掉,分类的准确率会提高不少,想了一下应该是Cifar-10数据集的32*32图像本来就不大,一开始就使用Max池化会丢失不少信息,从而导致准确率下降。因此把该Max池化层去掉,如下图所示:
修改之后的结构
在分析或处理不同量纲、不同取值范围的不同系列数据时,通常对不同系列数据分别做标准化,使它们的均值为0、标准差为1,同时保留了原始数据中各数据之间的相对大小和分布。我们在前文已详细讲过的全局对比度归一化正是这样一种数据预处理方法:
深度学习的数据预处理——全局对比度归一化(GCN)
增加GCN操作之后,我们数据预处理的基本流程如下:
libtorch处理数据的基本单位是Tensor张量,而Opencv读取的图像为Mat格式的BGR图像(后来转换为Mat格式的RGB图像),所以需要把Mat格式图像数据转换为Tensor张量。
之前本人犯了一个很严重的低级错误,就是把Mat格式转换为Tensor张量时,把维度顺序弄错了:
Opencv Mat存储三通道图像的顺序为[Height, Width, Channels],比如RGB图像展开成一维来看就是下面这种形式:
然而libtorch要求输入神经网络的Tensor张量存储三通道图像的顺序为[Channels, Height, Width],比如RGB图像展开成一维来看就是下面这种形式:
我没有转换Mat格式的顺序就直接将其数据赋值给Tensor张量,导致网络因为维度顺序不对而不能准确捕获图像特征,因此分类准确率低下。
基于以上原因,我们该错误纠正过来:首先把[Height, Width, Channels]的Mat格式数据转换为[Height, Width, Channels]的Tensor张量,然后再调用Tensor张量的permute函数把数据的维度顺序调整为[Channels, Height, Width]。这样一来就没问题了。
训练网络的时候,我们通常使用batch训练的方式,也即每次同时训练batch size个样本。测试验证的时候则没有这个限制,既可以batch批量测试(将多个样本同时输入网络),也可以每次只将一个样本输入网络,两者的区别在于以batch测试的方式测试完10000张图像所花时间更少。
之前我们都是训练完所有epoch之后再使用模型对测试集进行分类,并查看准确率。为了在训练过程中实时观察测试集分类准确率的变化情况,我们现在每训练完5个epoch就对测试集进行一次分类,同时为了节省时间,我们使用batch测试的方式。
本文我们改变学习率调整策略:前文每个epoch之后都降低学习率,其实没有必要,可以间隔一定频率再降低学习率,这样效果相对更好一点:
1. 在前30个epoch内学习率每隔5个epoch乘以0.98;
2. 在前30~70个epoch内学习率每隔5个epoch乘以0.95;
3. 在前70~100个epoch内学习率每隔5个epoch乘以0.925;
4. 在前100~200个epoch内学习率每隔5个epoch乘以0.9;
5. 在前200个epoch以上学习率每隔5个epoch乘以0.88。
此外需要注意,不仅训练的时候需要按照上述第3章所述调整数据维度顺序,测试的时候同样需要调整顺序。
struct Residual: torch::nn::Module
{
int in_channel_r;
int out_channel_r;
int _stride_r;
Residual(int in_channel, int out_channel, int _stride = 1)
{
conv1 = register_module("conv1", torch::nn::Conv2d(torch::nn::Conv2dOptions(in_channel, out_channel, { 3, 3 }).padding(1).stride({ _stride, _stride }).bias(false)));
c1b = register_module("c1b", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(out_channel).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)));
conv2 = register_module("conv2", torch::nn::Conv2d(torch::nn::Conv2dOptions(out_channel, out_channel, { 3,3 }).padding(1).stride({ 1, 1 }).bias(false)));
c2b = register_module("c2b", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(out_channel).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)));
conv3 = register_module("conv3", torch::nn::Conv2d(torch::nn::Conv2dOptions(in_channel, out_channel, { 1, 1 }).stride({ _stride, _stride }).bias(false)));
in_channel_r = in_channel;
out_channel_r = out_channel;
_stride_r = _stride;
}
~Residual()
{
}
torch::Tensor forward(torch::Tensor input)
{
namespace F = torch::nn::functional;
auto x = conv1->forward(input); //(32+1*2-3)/1+1 = 32
x = c1b->forward(x);
x = F::relu(x, F::ReLUFuncOptions().inplace(true));
x = conv2->forward(x); //(32+1*2-3)/1+1 = 32
Tensor x1;
if (_stride_r != 1 || in_channel_r != out_channel_r)
{
x1 = conv3->forward(input); //(32-1)/1+1 = 32
}
else
{
x1 = input;
}
x = x + x1;
x = c2b->forward(x);
x = F::relu(x, F::ReLUFuncOptions().inplace(true));
return x;
}
torch::nn::Conv2d conv1{ nullptr };
torch::nn::BatchNorm2d c1b{ nullptr }; //batchnorm在卷积层之后、激活函数之前
torch::nn::Conv2d conv2{ nullptr };
torch::nn::BatchNorm2d c2b{ nullptr };
torch::nn::Conv2d conv3{ nullptr };
};
struct Resnet34 : torch::nn::Module
{
Resnet34(int in_channel, int num_class = 10)
{
conv1 = register_module("conv1", torch::nn::Sequential(
torch::nn::Conv2d(torch::nn::Conv2dOptions(in_channel, 64, { 3, 3 }).padding(1).stride({ 1, 1 }).bias(false)), //(32+1*2-3)/1+1=32
torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(64).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)),
torch::nn::ReLU(torch::nn::ReLUOptions(true)))
//将这里的Max池化层去掉
//torch::nn::MaxPool2d(torch::nn::MaxPool2dOptions({ 3, 3 }).stride({ 1, 1 }).padding(1))) //(32+1*2-3)/1+1=32
);
conv2 = register_module("conv2", torch::nn::Sequential(
Residual(64, 64),
Residual(64, 64),
Residual(64, 64))
);
conv3 = register_module("conv3", torch::nn::Sequential(
Residual(64, 128, 2), //(32+1*2-3)/2+1=16
Residual(128, 128),
Residual(128, 128),
Residual(128, 128))
);
conv4 = register_module("conv4", torch::nn::Sequential(
Residual(128, 256, 2), //(16+1*2-3)/2+1=8
Residual(256, 256),
Residual(256, 256),
Residual(256, 256),
Residual(256, 256),
Residual(256, 256))
);
conv5 = register_module("conv5", torch::nn::Sequential(
Residual(256, 512, 2), //(8+1*2-3)/2+1=4
Residual(512, 512),
Residual(512, 512))
);
fc = register_module("fc", torch::nn::Linear(512, num_class));
}
~Resnet34()
{
}
//前向传播
Tensor forward(Tensor input)
{
namespace F = torch::nn::functional;
auto x = conv1->forward(input);
x = conv2->forward(x);
x = conv3->forward(x);
x = conv4->forward(x);
x = conv5->forward(x);
x = F::avg_pool2d(x, F::AvgPool2dFuncOptions(4)); //默认步长与窗口尺寸一致,(4-4)/4+1=1
x = x.view({ x.size(0), -1 });
x = fc->forward(x);
return x;
}
// Use one of many "standard library" modules.
torch::nn::Sequential conv1{ nullptr };
torch::nn::Sequential conv2{ nullptr };
torch::nn::Sequential conv3{ nullptr };
torch::nn::Sequential conv4{ nullptr };
torch::nn::Sequential conv5{ nullptr };
torch::nn::Linear fc{ nullptr };
};
其中数据增强函数data_ehance在前文已贴出,此处不再重复。
//gcn对比度全局归一化
void cal_gcn(Mat &src)
{
float m = 0;
float m_2 = 0;
for (int i = 0; i < src.rows; i++)
{
Vec3f *p = src.ptr(i);
for (int j = 0; j < src.cols; j++)
{
m += (p[j][0] + p[j][1] + p[j][2]);
m_2 += (p[j][0] * p[j][0] + p[j][1] * p[j][1] + p[j][2] * p[j][2]);
}
}
float total_cnt = src.rows * src.cols * 3.0;
m /= total_cnt;
m_2 /= total_cnt;
float std = sqrt(m_2 - m*m); //标准差
for (int i = 0; i < src.rows; i++)
{
Vec3f *p = src.ptr(i);
for (int j = 0; j < src.cols; j++)
{
p[j][0] = (p[j][0] - m) / max(1e-8, std);
p[j][1] = (p[j][1] - m) / max(1e-8, std);
p[j][2] = (p[j][2] - m) / max(1e-8, std);
}
}
}
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
data_ehance(img, img); //数据增强
cvtColor(img, img, COLOR_BGR2RGB); //转换为RGB
img.convertTo(img, CV_32F, 1.0 / 255.0); //转换为0~1浮点数
cal_gcn(img); //全局对比度归一化
img_list.push_back(img.clone());
int y = shuffle_idx[i] / labels.cols;
int x = shuffle_idx[i] % labels.cols;
label_list.push_back((long long)labels.ptr(y)[x]);
}
}
float test_resnet_cifar_10_after_one_epoch_batch(Resnet34 net, vector test_img, vector test_label, int batch_size)
{
int total_test_items = 0, passed_test_items = 0;
double total_time = 0.0;
for (int i = 0; i < test_img.size()/batch_size; i++)
{
vector label_list;
//[batch_size, Height, Width, Channels]
auto inputs = torch::ones({ batch_size, 32, 32, 3 });
for (int j = 0; j < batch_size; j++)
{
int idx = i * batch_size + j;
inputs[j] = torch::from_blob(test_img[idx].data, { test_img[idx].rows, test_img[idx].cols, test_img[idx].channels() }, torch::kFloat).clone();
label_list.push_back((long long)test_label[idx]);
}
//将[batch_size, Height, Width, Channels]调整为[batch_size, Channels, Height, Width]
inputs = inputs.permute({ 0, 3, 1, 2 });
torch::Tensor labels = torch::tensor(label_list);
inputs = inputs.to(device_type);
labels = labels.to(device_type);
// 用训练好的网络处理测试数据
auto outputs = net.forward(inputs);
// 得到预测值,0 ~ 9
auto predicted = (torch::max)(outputs, 1);
// 比较预测结果和实际结果,并更新统计结果
for (int k = 0; k < batch_size; k++) //分别统计一个batch中所有样本的预测是否准确
{
if (labels[k].item() == std::get<1>(predicted)[k].item())
passed_test_items++;
}
total_test_items += batch_size;
}
float acc = passed_test_items * 1.0 / total_test_items;
printf("total_test_items=%d, passed_test_items=%d, pass rate=%f\n", total_test_items, passed_test_items, acc);
return acc;
}
//更新学习率函数
void updata_learn_rate(torch::optim::SGD &optimizer, double alpha)
{
for (auto& pg : optimizer.param_groups())
{
if (pg.has_options())
{
auto& options = pg.options();
static_cast(pg.options()).lr() = alpha;
}
}
}
//写文件函数
void write_float_to_file(char *filepath, vector d)
{
FILE *fp = fopen(filepath, "wb+");
for (int i = 0; i < d.size(); i++)
{
fprintf(fp, "%f, ", d[i]);
}
fclose(fp);
}
//训练函数
void tran_resnet_cifar_10_test_one_epoch(void)
{
vector train_img_total;
vector train_label_total;
Resnet34 net1(3, 10);
net1.train(); //训练状态
net1.to(device_type);
int kNumberOfEpochs = 150;
double alpha = 0.01;
int batch_size = 100;
vector img_list;
vector label_list;
Mat label_mat = imread("D:/Program Files (x86)/Microsoft Visual Studio/2017/Community/prj/libtorch_test1_gpu2/libtorch_test/cifar-10/label/label.tif", CV_LOAD_IMAGE_GRAYSCALE);
vector test_img0;
vector test_label0;
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_img0, test_label0);
auto criterion = torch::nn::CrossEntropyLoss();
auto optimizer = torch::optim::SGD(net1.parameters(), torch::optim::SGDOptions(alpha).momentum(0.9));//.weight_decay(5e-4)); //weight_decay表示L2正则化
vector acc_list;
vector loss_list;
float l;
for (int epoch = 0; epoch < kNumberOfEpochs; epoch++)
{
train_image_shuffle_set.clear();
for (size_t i = 0; i < CIFAT_10_TOTAL_DATANUM; i++)
{
train_image_shuffle_set.push_back(i);
}
std::mt19937_64 g((unsigned int)time(NULL));
//打乱顺序
std::shuffle(train_image_shuffle_set.begin(), train_image_shuffle_set.end(), g); //打乱顺序
auto running_loss = 0.;
for (int k = 0; k < CIFAT_10_TOTAL_DATANUM / batch_size; k++)
{
//获取一个batch的样本
read_cifar_batch("D:/Program Files (x86)/Microsoft Visual Studio/2017/Community/prj/libtorch_test1_gpu2/libtorch_test/cifar-10/img/%d.tif", label_mat, &train_image_shuffle_set[k*batch_size], batch_size, img_list, label_list);
//[batch_size, Height, Width, Channels]
auto inputs = torch::ones({ batch_size, 32, 32, 3 });
for (int b = 0; b < batch_size; b++)
{
inputs[b] = torch::from_blob(img_list[b].data, { img_list[b].rows, img_list[b].cols, img_list[b].channels() }, torch::kFloat).clone();
}
//将[batch_size, Height, Width, Channels]调整为[batch_size, Channels, Height, Width]
inputs = inputs.permute({ 0, 3, 1, 2 });
torch::Tensor labels = torch::tensor(label_list);
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)
{
srand((unsigned int)(time(NULL)));
l = running_loss / 50; //计算50个epoch的损失函数值的平均值
printf("alpha=%f, loss: %f\n", alpha, l);
loss_list.push_back(l);
running_loss = 0.;
}
}
if ((epoch + 1) % 5 == 0)
{
net1.eval(); //切换到测试状态
//每训练5个epoch之后对测试集进行一次分类
float acc = test_resnet_cifar_10_after_one_epoch_batch(net1, test_img0, test_label0, batch_size);
net1.train(); //切换回训练状态
printf("********************acc:%f\n", acc);
acc_list.push_back(acc); //记录分类准确率
}
printf("epoch:%d\n", epoch + 1);
//调整学习率
if (epoch <= 30 && (epoch + 1) % 5 == 0)
{
alpha *= 0.98;
updata_learn_rate(optimizer, alpha); //更新学习率
}
else if (epoch > 30 && epoch <= 70 && (epoch + 1) % 5 == 0)
{
alpha *= 0.95;
updata_learn_rate(optimizer, alpha);
}
else if (epoch > 70 && epoch <= 100 && (epoch + 1) % 5 == 0)
{
alpha *= 0.925;
updata_learn_rate(optimizer, alpha);
}
else if (epoch > 100 && epoch <= 200 && (epoch + 1) % 5 == 0)
{
alpha *= 0.9;
updata_learn_rate(optimizer, alpha);
}
else if(epoch > 200 && (epoch + 1) % 5 == 0)
{
alpha *= 0.88;
updata_learn_rate(optimizer, alpha);
}
}
//将训练过程中的损失函数值、测试集分类准确率写到文件中
write_float_to_file("acc.bin", acc_list);
write_float_to_file("loss.bin", loss_list);
remove("mnist_cifar_10_resnet.pt");
printf("Finish training!\n");
torch::serialize::OutputArchive archive;
net1.save(archive);
//将训练模型写到文件中
archive.save_to("mnist_cifar_10_resnet.pt");
printf("Save the training result to mnist_cifar_10_resnet.pt.\n");
}
在main函数中运行上述tran_resnet_cifar_10_test_one_epoch函数,得到分类结果如下,可以看到对测试集分类的准确率达到了94.15%,相比之前提升了很多。
训练过程中损失函数的变化情况如下图所示,发现我们使用上述学习率调整策略,效果还是不错的,损失函数值没有之前震荡得那么厉害了:
训练过程中测试集分类准确率的变化情况:
之前分类得准确率一直没有提升,挺纳闷的,到底时什么原因。找到原因之后才发现时一个低级错误,也是一个基础知识的错误,怪自己岁基础知识没有理解透吧,引以为戒~
分类模型我们就讲到这里了,接下来我们将进入深度学习的目标检测系列,敬请期待!
欢迎扫码关注本微信公众号,接下来会不定时更新更加精彩的内容,敬请期待~