目录
这里以vgg分类网络为例,vgg网络简单:多层卷积提取特征,最大池化下采样,全连接层进行分类,最后接一个sotmax归一化输出。
- #ifndef VGG_H
- #define VGG_H
- #include
- #include
- #include
-
- // 二维卷积参数配置
- // 注意:关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用。
- inline torch::nn::Conv2dOptions conv_options(int64_t in_planes, int64_t out_planes, int64_t kerner_size,
- int64_t stride = 1, int64_t padding = 0, bool with_bias = false) {
- torch::nn::Conv2dOptions conv_options = torch::nn::Conv2dOptions(in_planes, out_planes, kerner_size);
- conv_options.stride(stride);
- conv_options.padding(padding);
- conv_options.bias(with_bias);
- return conv_options;
- }
-
- // 最大池化参数配置
- inline torch::nn::MaxPool2dOptions maxpool_options(int kernel_size, int stride){
- torch::nn::MaxPool2dOptions maxpool_options(kernel_size);
- maxpool_options.stride(stride);
- return maxpool_options;
- }
-
- // 工具函数:生成多个卷积层,作为特征提取层
- // 参数cfg: 中间特征层的通道数,batch_norm:是否使用bn
- torch::nn::Sequential make_features(std::vector<int> &cfg, bool batch_norm);
-
- // 创建VGG类,必须继承Module类
- // vgg网络:卷积层+pooling层+全连接层
- class VGGImpl: public torch::nn::Module
- {
- public:
- VGGImpl(std::vector<int>& cfg, int num_classes = 1000, bool batch_norm = false); // 1个构造函数,初始化各个层操作的参数
- torch::Tensor forward(torch::Tensor x); // 1个forward函数
- private:
- torch::nn::Sequential features_{nullptr}; // 卷积层
- torch::nn::AdaptiveAvgPool2d avgpool{nullptr}; // pooling
- torch::nn::Sequential classifier; // 全连接层
- };
- TORCH_MODULE(VGG);
-
- #endif // VGG_H
- #include "vgg.h"
-
- // 工具函数:生成多个卷积层,作为特征提取层
- // 参数cfg: 中间特征层的通道数,batch_norm:是否使用bn
- torch::nn::Sequential make_features(std::vector<int> &cfg, bool batch_norm){
- torch::nn::Sequential features;
- int in_channels = 3;
- for(auto v : cfg){ // v是通道数
- if(v==-1){ // 遇到-1,则接一个池化层
- features->push_back(torch::nn::MaxPool2d(maxpool_options(2,2)));
- }
- else{
- // conv2 + bn + relu
- auto conv2d = torch::nn::Conv2d(conv_options(in_channels,v,3,1,1)); // k=3,s=1,p=1
- features->push_back(conv2d);
- if(batch_norm){
- features->push_back(torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(v))); // or torch::nn::BatchNorm2d(v)
- }
- features->push_back(torch::nn::ReLU(torch::nn::ReLUOptions(true))); // relu层
- in_channels = v;
- }
- }
- return features;
- }
-
- // 初始化私有成员: features_,avgpool,classifier
- VGGImpl::VGGImpl(std::vector<int> &cfg, int num_classes, bool batch_norm){
- features_ = make_features(cfg,batch_norm); // 1.初始化卷积层(包含了池化层)
- avgpool = torch::nn::AdaptiveAvgPool2d(torch::nn::AdaptiveAvgPool2dOptions(7)); // 2.初始化池化层
- classifier->push_back(torch::nn::Linear(torch::nn::LinearOptions(512 * 7 * 7, 4096))); // 3.初始化全连接层
- classifier->push_back(torch::nn::ReLU(torch::nn::ReLUOptions(true)));
- classifier->push_back(torch::nn::Dropout());
- classifier->push_back(torch::nn::Linear(torch::nn::LinearOptions(4096, 4096)));
- classifier->push_back(torch::nn::ReLU(torch::nn::ReLUOptions(true)));
- classifier->push_back(torch::nn::Dropout());
- classifier->push_back(torch::nn::Linear(torch::nn::LinearOptions(4096, num_classes))); // 两个linear+relu+dropout, 再加一个linear.
-
- features_ = register_module("features",features_); // module必须注册
- classifier = register_module("classifier",classifier);
- }
-
- torch::Tensor VGGImpl::forward(torch::Tensor x){
- x = features_->forward(x); // 1,先卷积(包含了池化层),注册的module需要通过forward使用
- x = avgpool(x); // 2,再池化,直接使用
- x = torch::flatten(x,1); // 3,二维变一维
- x = classifier->forward(x); // 4,再接全连接层
- return torch::log_softmax(x, 1); // 5,最后softmax
- }
使用预训练权重训练模型,可以更快的收敛。为了使c++训练模型时能够使用pytorch版的预训练权重,c++代码搭建的vgg必须和pytorch一致。感兴趣的可以看下官方的pytorch代码torchvision.models.vgg16_bn
注意要使用jit.trace函数。
- import torch
- from torchvision.models import vgg16, vgg16_bn
-
- # 在c++中搭建一个和pytorch下完全一致的vgg16bn。如果不一致的话其实不影响正常的模型训练和预测,
- # 但是影响初始化状态,模型加载从ImageNet数据集训练好的权重以后,训练收敛的速度和收敛后的精度都会好很多。
-
- model = vgg16_bn(pretrained=True)
- model = model.to(torch.device("cpu"))
- model.eval()
- var = torch.ones((1, 3, 224, 224))
- # 保存pytorch模型的权重不能直接用torch.save保存模型,这样存下来的模型不能被c++加载。我们利用部署时常用的torch.jit.script模型来保存。
- traced_script_module = torch.jit.trace(model, var)
- traced_script_module.save("vgg16bn.pt")
- # 这样,模型的卷积层,归一化层,线性层的权重就保存到.pt文件中了。
基于VGG类,再打包一次得到Classifier类。
classification.h
- #ifndef CLASSIFICATION_H
- #define CLASSIFICATION_H
- #include
- #include
- #include
-
- class Classifier
- {
- private:
- torch::Device device = torch::Device(torch::kCPU); // 默认使用cpu
- VGG vgg = VGG{nullptr}; // 自定义网络对象
- public:
- Classifier(int gpu_id = 0); // 构造函数,初始化device私用成员
- void Initialize(int num_classes, std::string pretrained_path); // 加载预训练权重
- void Train(int epochs, int batch_size, float learning_rate, std::string train_val_dir, std::string image_type, std::string save_path);
- int Predict(cv::Mat &image); // preprocess + infer + postprocess
- void LoadWeight(std::string weight); // 加载权重
- };
-
- #endif // CLASSIFICATION_H
先看看权重参数的特点。
- # pytorch
- import torch
- from torchvision.models import vgg16, vgg16_bn
-
- model = vgg16_bn(pretrained=True)
- for k, v in model.named_parameters():
- print(k)
-
-
- // c++
- std::vector<int> cfg_dd = {64, 64, -1, 128, 128, -1, 256, 256, 256, -1, 512, 512, 512, -1, 512, 512, 512, -1};
- auto vgg_dd = VGG(cfg_dd,1000,true); // 直接实例化VGG即可
- auto dictdd = vgg_dd->named_parameters();
- for (auto n = dictdd.begin(); n != dictdd.end(); n++) // 打印出模型每一层(有权重的层,不包括类似激活函数层)的名称
- {
- std::cout<<(*n).key()<<std::endl;
- }
左边是pytorch打印的信息,右边是c++打印的信息,可以发现c++的少了部分bias 权重,这是因为有些卷积没有设置bias(可以设置)。初始化的时候,根据权重参数名称初始化右边对应的值。
利用pytorch版的预训练权重(前面有保存下来),初始化c++网络权重参数。
classification.cpp
- // 使用预训练权重,初始化自定义的模型权重
- void Classifier::Initialize(int _num_classes, std::string _pretrained_path){
- std::vector<int> cfg_d = {64, 64, -1, 128, 128, -1, 256, 256, 256, -1, 512, 512, 512, -1, 512, 512, 512, -1};
- auto net_pretrained = VGG(cfg_d,1000,true); // 注意这里是1000个类别,实例化预训练网络,为了取出权重
- vgg = VGG(cfg_d,_num_classes,true); // 注意这里的类别个数
- torch::load(net_pretrained, _pretrained_path); // 预训练的网络载入预训练的权重。
- torch::OrderedDict
pretrained_dict = net_pretrained->named_parameters(); - torch::OrderedDict
model_dict = vgg->named_parameters(); -
- // 将训练的权重值提取出来,其中后面的分类层参数值丢弃不用。
- for (auto n = pretrained_dict.begin(); n != pretrained_dict.end(); n++)
- {
- if (strstr((*n).key().data(), "classifier")) { // 不使用分类层的权重
- continue;
- }
- model_dict[(*n).key()] = (*n).value();
- }
-
- torch::autograd::GradMode::set_enabled(false); // make parameters copying possible
- auto new_params = model_dict; // implement this
- auto params = vgg->named_parameters(true /*recurse*/);
- auto buffers = vgg->named_buffers(true /*recurse*/);
- for (auto& val : new_params) { // 功能:将屏蔽分类层后的新权重,复制到自定义的vgg网络模型权重中。
- auto name = val.key();
- auto* t = params.find(name); // 查看自定义网络中是否有该权重参数名
- if (t != nullptr) {
- t->copy_(val.value()); // 有,则将预训练得到的val复制到自定义网络参数中。
- }
- else {
- t = buffers.find(name); // 没有,则看看buffer里面有没有,都没有则跳过。
- if (t != nullptr) {
- t->copy_(val.value());
- }
- }
- }
- torch::autograd::GradMode::set_enabled(true);
- try
- {
- vgg->to(device);
- }
- catch (const std::exception&e)
- {
- std::cout << e.what() << std::endl;
- }
-
- return;
- }
- #include
-
- int main(int argc, char *argv[])
- {
- //2,加载预训练权重,测试权重是否可用。
- std::vector<int> cfg_16bn = { 64, 64, -1, 128, 128, -1, 256, 256, 256, -1, 512, 512, 512, -1, 512, 512, 512, -1 };
- auto vgg16bn = VGG(cfg_16bn, 1000, true);
- torch::load(vgg16bn, "E:\\code\\c++\\LibtorchTutorials-main\\vgg16bn.pt");
- vgg16bn->to(torch::Device(torch::kCUDA));
- }
最后一句报错:PyTorch is not linked with support for cuda devices
解决办法:属性->链接器->命令行:
- libtorch 1.5: -INCLUDE:THCudaCharTensor_zero
-
- libtorch 1.6/1.7/1.9/1.9.1/1.10/1.1:-INCLUDE:?warp_size@cuda@at@@YAHXZ
-
-
- libtorch 1.8.1: -INCLUDE:?wait@Future@ivalue@c10@@QEAAXXZ
-
- 或者:-INCLUDE:?mutate@OptOutMutator@cuda@fuser@jit@torch@@UEAAPEAVStatement@2345@PEAVForLoop@kir@2345@@Z
这里有个骚操作,前8个epoch保留前面的特征提取层参数不变(即使用预训练权重值),只更新分类层参数。
- void Classifier::Train(int num_epochs, int batch_size, float learning_rate, std::string train_val_dir, std::string image_type, std::string save_path){
- std::string path_train = train_val_dir+ "\\train"; // 数据路径
- std::string path_val = train_val_dir + "\\val";
-
- auto custom_dataset_train = dataSetClc(path_train, image_type).map(torch::data::transforms::Stack<>()); // dataset
- auto custom_dataset_val = dataSetClc(path_val, image_type).map(torch::data::transforms::Stack<>());
-
- auto data_loader_train = torch::data::make_data_loader
(std::move(custom_dataset_train), batch_size); // dataloader - auto data_loader_val = torch::data::make_data_loader
(std::move(custom_dataset_val), batch_size); -
- float loss_train = 0; float loss_val = 0; // 累加当前epoch内所有的loss,求平均loss
- float acc_train = 0.0; float acc_val = 0.0; float best_acc = 0.0; // 累加准去率,和最佳准确率
- for (size_t epoch = 1; epoch <= num_epochs; ++epoch) { // epoch
- size_t batch_index_train = 0; // 记录当前是第几个batch
- size_t batch_index_val = 0;
- if (epoch == int(num_epochs / 2)) { learning_rate /= 10; } // 每两个epoch下降一次学习率
- torch::optim::Adam optimizer(vgg->parameters(), learning_rate); // Learning Rate
- if (epoch < int(num_epochs / 8)) // 前8个epoch只更新分类层参数
- {
- for (auto mm : vgg->named_parameters())
- {
- if (strstr(mm.key().data(), "classifier")) // 只更新分类层的参数
- {
- mm.value().set_requires_grad(true);
- }
- else
- {
- mm.value().set_requires_grad(false);
- }
- }
- }
- else { // 后面epoch次更新全部参数
- for (auto mm : vgg->named_parameters())
- {
- mm.value().set_requires_grad(true);
- }
- }
- // Iterate data loader to yield batches from the dataset
- for (auto& batch : *data_loader_train) {
- auto data = batch.data; // b,c,h,w
- auto target = batch.target.squeeze(); // b,1
- data = data.to(torch::kF32).to(device).div(255.0); // data: kf32,cuda,/255
- target = target.to(torch::kInt64).to(device); // target: kint64, cuda
- optimizer.zero_grad();
- // Execute the model
- torch::Tensor prediction = vgg->forward(data);
- //cout << prediction << endl;
- auto acc = prediction.argmax(1).eq(target).sum();
- acc_train += acc.template item<float>() / batch_size;
- // Compute loss value
- torch::Tensor loss = torch::nll_loss(prediction, target);
- // Compute gradients
- loss.backward();
- // Update the parameters
- optimizer.step();
- loss_train += loss.item<float>(); // 累加loss
- batch_index_train++;
- std::cout << "Epoch: " << epoch << " |Train Loss: " << loss.item<float>() << " |Train Acc:" << acc_train / batch_index_train << "\r";
- }
- std::cout << std::endl;
-
- //validation part
- vgg->eval();
- for (auto& batch : *data_loader_val) {
- auto data = batch.data;
- auto target = batch.target.squeeze();
- data = data.to(torch::kF32).to(device).div(255.0);
- target = target.to(torch::kInt64).to(device);
- torch::Tensor prediction = vgg->forward(data);
- // Compute loss value
- torch::Tensor loss = torch::nll_loss(prediction, target);
- auto acc = prediction.argmax(1).eq(target).sum(); // val准确率计算方法
- acc_val += acc.template item<float>() / batch_size;
- loss_val += loss.item<float>(); // 累加loss
- batch_index_val++;
- std::cout << "Epoch: " << epoch << " |Val Loss: " << loss_val / batch_index_val << " |Valid Acc:" << acc_val / batch_index_val << "\r";
- }
- std::cout << std::endl;
-
-
- if (acc_val > best_acc) {
- torch::save(vgg, save_path);
- best_acc = acc_val;
- }
- loss_train = 0; loss_val = 0; acc_train = 0; acc_val = 0; batch_index_train = 0; batch_index_val = 0;
- }
- }
其中 dataset-dataSetClc的定义与上一篇博客一样都是图像分类数据集。
- #include
-
- int main(int argc, char *argv[])
- {
-
- //2,使用预训练权重,进行推理
- std::string vgg_weight_path = "E:\\code\\c++\\LibtorchTutorials-main\\vgg16bn.pt";
- std::string train_val_dir = "F:\\zxq\\data\\hymenoptera_data";
- Classifier classifier(0); // gpu id: 0
- classifier.Initialize(2, vgg_weight_path); // 使用预训练权重初始化权重,
- classifier.Train(300, 4, 0.0003, train_val_dir, ".jpg", "classifer.pt");
- }
验证集的准确率是0.91。
换成猫狗大战数据集(分出4000张训练),训练和测试集loss收敛的都很快。
测试的时候发现一个问题,在2080ti训练的模型,在3080显卡下预测会报错:
Microsoft C++ 异常: c10::Error,位于内存位置 0x000000301E6F3BF0 处。
可以在高端显卡训练,低端显卡部署。
- // 3, infer
- Classifier classifier(1); // gpu id: 0
- classifier.Initialize(2, "E:\\code\\c++\\LibtorchTutorials-main\\vgg16bn.pt");
- classifier.LoadWeight("E:\\code\\c++\\LibtorchLearning\\classifer.pt");
-
- std::string train_val_dir = "F:\\zxq\\data\\custom\\dog";
- cv::Mat image = cv::imread(train_val_dir+"\\dog.2344.jpg");
- int cls_id = classifier.Predict(image); // preprocess + forward
- std::cout << "cls_id: " << cls_id << std::endl;
换成cat图片。
- int Classifier::Predict(cv::Mat& image){
- // preprocess: resize, to_tensor, cuda, kf32, /255
- cv::resize(image, image, cv::Size(448, 448));
- torch::Tensor img_tensor = torch::from_blob(image.data, { image.rows, image.cols, 3 }, torch::kByte).permute({ 2, 0, 1 }); // c,h,w
- img_tensor = img_tensor.to(device).unsqueeze(0).to(torch::kF32).div(255.0);
- auto prediction = vgg->forward(img_tensor); // raw output
- prediction = torch::softmax(prediction,1);
- auto class_id = prediction.argmax(1);
- std::cout<
- int ans = int(class_id.item().toInt());
- float prob = prediction[0][ans].item().toFloat();
- return ans;
- }
-
- void Classifier::LoadWeight(std::string weight){
- torch::load(vgg,weight); // 载入训练好的权重
- vgg->eval();
- return;
- }
参考:LibtorchTutorials/lesson5-TrainingVGG at main · AllentDan/LibtorchTutorials · GitHub