Torch C++实现Lenet-5网络训练与测试

Torch C++开发环境与第一个程序

头文件

编译环境

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

# 1.指定项目名
project(main)

# 2.执行Torch的cmake配置的位置
set(CMAKE_PREFIX_PATH  "C:/libtorch")

# 3.直接加载Torch C++提供cmake配置
find_package(Torch REQUIRED)

# 4.直接使用预先定义的变量(服务于Torch项目的编译链接)
set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FALAG} ${TORCH_CXX_FLAGS}")


# 5.输出的执行文件
add_executable(main main.cpp)

# 6.指定编译库
target_link_libraries(main "${TORCH_LIBRARIES}")

# 7.只对指定的编译目标指定编译器的C++语言标准版本
set_property(TARGET main PROPERTY CXX_STANDARD 14)
  • build.bat命令文件
@rem 创建工程构建工作目录
@mkdir build
@rem 进入构建目录
@cd build
@rem 生成本地工程
@cmake ..
@rem 直接编译visual studio工程
@cmake --build . --config Debug
@rem 拷贝执行文件到图像所在目录
@copy .\Debug\main.exe  ..\
@rem 回到代码目录
@cd  ..

Torch C++编程

Torch C++ 文档

  • https://pytorch.org/cppdocs/api/library_root.html
    • at命名空间Tensor
    • torch命名:数据集/模型/函数/优化器

数据集

MNIST
Dataset

// 数据集
auto  ds_train = torch::data::datasets::MNIST(".\\data", torch::data::datasets::MNIST::Mode::kTrain);
auto  ds_valid = torch::data::datasets::MNIST(".\\data", torch::data::datasets::MNIST::Mode::kTest);

torch::data::transforms::Normalize<> norm(0.1307, 0.3081);
torch::data::transforms::Stack<> stack;

数据集批次处理

DataLoader

// 数据批次加载
auto n_train = ds_train.map(norm);
auto s_train = ds_train.map(stack);
auto train_loader = torch::data::make_data_loader<torch::data::samplers::SequentialSampler>(std::move(s_train), 10); 

auto n_valid = ds_valid.map(norm);
auto s_valid = ds_valid.map(stack);
auto valid_loader = torch::data::make_data_loader<torch::data::samplers::SequentialSampler>(std::move(s_valid), 10); 

Lenet-5 模型

在这里插入图片描述

  • 代码
class Lenet5 : public torch::nn::Module{
private:
    // 卷积特征运算
    torch::nn::Conv2d  conv1;
    torch::nn::Conv2d  conv2;
    torch::nn::Conv2d  conv3;
    torch::nn::Linear  fc1;
    torch::nn::Linear  fc2;

public:
    Lenet5():
    conv1(torch::nn::Conv2dOptions(1, 6, 5).stride(1).padding(2)),  // 1 * 28 * 28 -> 6 * 28 * 28 -> 6 * 14 * 14
    conv2(torch::nn::Conv2dOptions(6, 16, 5).stride(1).padding(0)),  // 6 * 14 * 14 -> 16 * 10 * 10 -> 16 * 5 * 5
    conv3(torch::nn::Conv2dOptions(16, 120, 5).stride(1).padding(0)), // 16 * 5 * 5 -> 120 * 1 * 1 (不需要池化)
    fc1(120, 84),  // 120 -> 84
    fc2(84, 10){  // 84 -> 10 (分量最大的小标就是识别的数字)
        // 注册需要学习的矩阵(Kernel Matrix)
        register_module("conv1", conv1);
        register_module("conv2", conv2);
        register_module("conv3", conv3);
        register_module("fc1", fc1);
        register_module("fc2", fc2);
    }

    // override
    torch::Tensor forward(torch::Tensor x){  // {n * 1 * 28 * 28}
        // 1. conv
        x = conv1->forward(x);   // {n * 6 * 28 * 28}
        x = torch::max_pool2d(x, 2);   // {n * 6 * 14 * 14}
        x = torch::relu(x); // 激活函数 // {n * 6 * 14 * 14}
        // 2. conv
        x = conv2->forward(x);   // {n * 16 * 10 * 10}
        x = torch::max_pool2d(x, 2);   // {n * 16 * 5 * 5}
        x = torch::relu(x); // 激活函数 // {n * 16 * 5 * 5}
        // 3. conv
        x = conv3->forward(x);   // {n * 120 * 1 * 1}
        x = torch::relu(x); // 激活函数 // {n * 120 * 1 * 1}
        // 做数据格式转换
        x = x.view({-1, 120});   // {n * 120}
        // 4. fc
        x = fc1->forward(x);
        x = torch::relu(x);
        
        // 5. fc 
        x = fc2->forward(x);
        return  torch::log_softmax(x, 1);   // CrossEntryLoss = log_softmax + nll
    }

};
  • 输出模型
     // 1. 创建模型对象
    std::shared_ptr<Lenet5> model = std::make_shared<Lenet5>();
    // 2. 向前计算
    for(auto &batch: *train_loader){
        // 3. 观察结果
        auto data = batch.data;
        auto target = batch.target;
        // 保证数据格式
        data = data.view({-1, 1, 28, 28});
        auto pred = model3->forward(data);
        // std::cout << pred.sizes() << std::endl; 
        auto digit= pred.argmax(1);
        std::cout << "预测:\n" << digit << std::endl;
        std::cout << "真实:\n" << target << std::endl;
        break;
    }

训练

  • 在main函数中定义模型对象和优化器:
// 创建模型对象
std::shared_ptr<Lenet5> model = std::make_shared<Lenet5>();

// 优化器(管理模型中可训练矩阵)
torch::optim::Adam  optimizer = torch::optim::Adam(model->parameters(), torch::optim::AdamOptions(0.001)); // 根据经验一般设置为10e-4 
  • 代码

template <typename  DataLoader>  // 训练集自动推导
// (模型对象,训练集,优化器)
void train(std::shared_ptr<Lenet5> &model,  DataLoader &loader,  torch::optim::Adam &optimizer){
    model->train();
    // 迭代数据
    int n = 0;
    for(torch::data::Example<torch::Tensor, torch::Tensor> &batch: loader){
        torch::Tensor data   = batch.data;
        auto target          = batch.target;
        optimizer.zero_grad(); // 清空上一次的梯度
        // 计算预测值
        torch::Tensor y = model->forward(data);
        // 计算误差
        torch::Tensor loss = torch::nll_loss(y, target);
        // 计算梯度: 前馈求导
        loss.backward();
        // 根据梯度更新参数矩阵
        optimizer.step();
        // 为了观察效果,输出损失
        std::cout << "\t|--批次:" << std::setw(2) << std::setfill(' ')<< ++n 
        << ",\t损失值:" << std::setw(8) << std::setprecision(4) << loss.item<float>() << std::endl;
    }

}

验证与测试

  • 代码
template <typename DataLoader>
void  valid(std::shared_ptr<Lenet5> &model, DataLoader &loader) {
    model->eval();
    // 禁止求导的图跟踪
    torch::NoGradGuard  no_grad;
    // 循环测试集
    double sum_loss = 0.0;
    int32_t num_correct = 0;
    int32_t num_samples = 0;
    for(const torch::data::Example<> &batch: loader){
        // 每个批次预测值
        auto data = batch.data;
        auto target = batch.target;
        num_samples += data.sizes()[0];
        auto y = model->forward(data);
        // 计算纯预测的结果
        auto pred = y.argmax(1);
        // 计算损失值
        sum_loss += torch::nll_loss(y, target, {}, at::Reduction::Sum).item<double>();
        // 比较预测结果与真实的标签值
        num_correct += pred.eq(target).sum().item<int32_t>();
    }
    // 输出正确值
    std::cout << std::setw(8) << std::setprecision(4) 
        << "平均损失值:" << sum_loss / num_samples 
        << ",\t准确率:" << 100.0 * num_correct / num_samples << " %" << std::endl;
}
在主函数中调用训练与测试函数
    std::cout<< "开始训练" << std::endl;
    int epoch = 1;
    int interval = 1;   // 从测试间隔
    for(int e = 0; e < epoch; e++){
        std::printf("第%02d轮训练\n", e+1);
        // 调用训练函数
        train(model, *train_loader, optimizer);
        if (e  % interval == 0){
            valid(model, *valid_loader);
        }
    }
    std:: cout << "训练结束" << std::endl;
运行展示
  • 训练一轮的部分结果展示
    在这里插入图片描述

模型保存

  • // 保存
    torch::save(model, "lenet5.pt");
    

模型加载与识别

  • 代码
// 训练好的模型文件
// lenet5.pt
int main(){
    
    const char * data_filename = ".\\data";
    // 加载模型
    std::shared_ptr<Lenet5> model = std::make_shared<Lenet5>();
    torch::load(model, "lenet5.pt");

    //1.使用测试集中数据识别
    auto imgs = torch::data::datasets::MNIST(data_filename, torch::data::datasets::MNIST::Mode::kTest);
    
    for(int i = 0; i < 10; i++){
        // 取一张图像
        torch::data::Example<> example = imgs.get(i);
        //std::cout << "识别的数字是:" << example.target.item<int32_t>() << std::endl;  
        // 获取图像
        torch::Tensor  a_img = example.data;
        // 预测
        a_img = a_img.view({-1, 1, 28, 28});  // 我们的模型只接受4为的固定的数据格式(N * C * H * W)(NCHW格式)
        torch::Tensor  y = model->forward(a_img);
        int32_t result = y.argmax(1).item<int32_t>();
        std::cout << "识别的结果是:" << result << "->" << example.target.item<int32_t>() <<  std::endl;
    }
    
    std::cout << "------------------------------" << std::endl;
    
    
    //2.使用图像文件来识别
    // 读取图像
    cv::Mat im = cv::imread("5.png"); // 引号内为一个手写数字图片
    std::cout << im.size() << std::endl;
    // 转换为Tensor,处理成0-1之间的数字
    im.convertTo(im, CV_32FC1, 1.0f / 255.0f);
    torch::Tensor  t_img = torch::from_blob(im.data, {28, 28});
    t_img = t_img.view({-1, 1, 28, 28});
    // std::cerr << "错误日志" << std::endl;
    // 识别
    torch::Tensor  y_ = model->forward(t_img);
    int32_t pred = y_.argmax(1).item<int32_t>();
    std::cout << "识别的结果是:" << pred <<  std::endl;

    return 0;
}
  • 运行展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HcXyD2wA-1593567329906)(Snipaste_2020-06-30_22-08-45.png)]


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值