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

    技术2022-07-10  155

    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 ..

    CMake的详细使用参考博主的另一篇博客

    https://blog.csdn.net/Luo_LA/article/details/107057414

    Torch C++编程

    Torch C++ 文档

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

    数据集

    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("第d轮训练\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; } 运行展示


    Processed: 0.019, SQL: 9