用 cpp 来写一个神经网络(libtorch版)(1)

1,547 阅读3分钟

本以为安装 libTorch 会费一番周折,结果根据官方 guide 一步一步很顺利地安装成功。其实主要原因就是自己对 cmake 如何来编译一个 c++ 项目并不熟悉。首先是在官方网站下载 libtorch 下面给出地址是我随后分享中用到的 libtorch 下载地址。

libtorch 用途

暂时个人认识 libtorch 用途是将训练好模型加载到 c++ 环境然后进行推理或者一些额外的工作,因为很多网络实现都是用 python 版本 torch 也就是 pytorch 实现的。

wget https://download.pytorch.org/libtorch/nightly/cpu/libtorch-shared-with-deps-latest.zip
unzip libtorch-shared-with-deps-latest.zip

在官网给的示例就是用 CMakeLists 来构建的项目,当然你可以用你顺手的工具来构建项目。其实个人也是一名 c++ 入门级的开发人员。

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(example-app)

find_package(Torch REQUIRED)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")

add_executable(example-app main.cpp)
target_link_libraries(example-app "${TORCH_LIBRARIES}")
set_property(TARGET example-app PROPERTY CXX_STANDARD 14)

关于 cmake 这个高级编译配置工具,随后随着个人对其进行深入了解,也会推出相关的分享。

  • cmake_minimum_required 这里主要指定了最低支持的 cmake 版本
  • add_executable 添加可执行文件
  • find_package 可以找到 torch 安装
  • set 可以设置一些参数
  • target_link_libraries
  • set_property 设置属性
#include <iostream>

int main

{
std::cout << "hello world" std::endl;
}
mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/absolute/path/to/libtorch ..
cmake --build . --config Release

这里注意是/absolute/path/to/libtorch 是绝对路径,然后

-D 接下里参数是自定义参数, CMAKE_PREFIX_PATH 表示要编译的安装目录,需要/absolute/path/to 替换为你刚刚解压 libtorch 的目录

下载数据集

做一些准备工作,也就是先需要去下载一下数据库,不然在运行程序会报错。这里准备一段 python 代码

import torch
import torchvision
from torchvision import transforms

if __name__ == "__main__":
    train_dataset = torchvision.datasets.MNIST(root='./data',
                    train=True,
                    transform=transforms.ToTensor(),
                    download=True
                    )

下载完成后,我们需要将 train-labels-idx3-ubyte 和 train-images-idx3-ubyte 复制到 build 文件夹下 data 不然在运行时候会报错。

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)

project(NNet)

set(CMAKE_PREFIX_PATH /home/ubuntu16/Downloads/libtorch)

find_package(Torch REQUIRED)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")

add_executable(main src/main.cpp include/network.h)

target_include_directories(main PRIVATE include)

target_link_libraries(main ${TORCH_LIBRARIES})

set_property(TARGET main PROPERTY CXX_STANDARD 14)

当我们配置好如何编译一个项目之后,我们就可以做更多有趣的事,其实也就是将 c++ 作为前端来开发一个神经网络,首先我们来看一看如何在 c++ 中定义和操作一个模块(module),想必大家都有一定开发神经网络经验,可以用 python 在 pytorch 框架下可以开发一个小型神经网络,或者自己有过经验去实现一个经典神经网络,例如 VGG ResNet。

这个与 pytorch 提供 python 的接口类似,因为为 C++ 提供可重复使用,用于搭建神经网络基础设施 module (模块)。所有其他的模块都是从这个基础 module 派生出来的。在 python 中,这个类是torch.nn.Module,而在 C++ 对应的是 torch::nn::Module。模块所封装了前向传播的方法 forward() 外,还包括 parameter (参数)、buffers(缓冲器)和submodules(子模块)。

参数和缓冲区以张量的形式存储状态。参数记录梯度,而缓冲区不记录。参数通常是你的神经网络的可训练权重。缓冲区的例子包括用于批量规范化的平均值和方差。为了重复使用特定的逻辑和状态块,PyTorch的API允许模块嵌套。一个嵌套的模块被称为子模块。

#pragma once

#include <iostream>
#include <torch/torch.h>

struct NetImpl:torch::nn::Module{

    NetImpl(int fc1_dims, int fc2_dims):fc1(fc1_dims,fc1_dims),fc2(fc1_dims,fc2_dims),out(fc2_dims,1){

    register_module("fc1",fc1);
    register_module("fc2",fc2);
    register_module("out",out);

}

torch::Tensor forward(torch::Tensor x){
    x = torch::relu(fc1(x));
    x = torch::relu(fc2(x));
    x = out(x);

    return x;

}

torch::nn::Linear fc1{nullptr}, fc2{nullptr}, out{nullptr};

};

TORCH_MODULE(Net);
#include "network.h"

#include <iostream>

#include <torch/torch.h>

\


using namespace std;

using namespace torch;



int main()

{

Net network(50,10);

cout << network << endl;

cout << "hello world" << endl;

}

搭建网络

#include <torch/torch.h>
#include <iostream>


// 定义一个简单全连接层组成的网络

struct Net: torch::nn::Module{
  Net(){
    // 构造并且注册网络结构中层
    fc1 = register_module("fc1",torch::nn::Linear(784,64));
    fc2 = register_module("fc2",torch::nn::Linear(64,32));
    fc3 = register_module("fc3",torch::nn::Linear(32,10));
  }
  // 实现前向传播
  torch::Tensor forward(torch::Tensor x){
    x = torch::relu(fc1->forward(x.reshape({x.size(0),784})));
    x = torch::dropout(x, /*p=*/0.5, /*train=*/is_training());
    x = torch::relu(fc2->forward(x));
    x = torch::log_softmax(fc3->forward(x), /*dim=*/1);
    return x;
  }

  torch::nn::Linear fc1{nullptr}, fc2{nullptr}, fc3{nullptr};
};

int main() {
  // 创建一个新的网络
  auto net = std::make_shared<Net>();

  auto data_loader = torch::data::make_data_loader(
      torch::data::datasets::MNIST("./data").map(
          torch::data::transforms::Stack<>()),
      /*batch_size=*/64);

  torch::optim::SGD optimizer(net->parameters(), /*lr=*/0.01);

  for (size_t epoch = 1; epoch <= 10; ++epoch) {
    size_t batch_index = 0;
    // Iterate the data loader to yield batches from the dataset.
    for (auto& batch : *data_loader) {
      // 重置梯度
      optimizer.zero_grad();
      // 在输入数据上运行模型
      torch::Tensor prediction = net->forward(batch.data);
      // 根据模型预测值与真实值之间差距来计算损失值
      torch::Tensor loss = torch::nll_loss(prediction, batch.target);
      //在反向传播中计算参数的梯度
      loss.backward();

      // 基于计算得到梯度来更新参数
      optimizer.step();
      if (++batch_index % 100 == 0) {
        std::cout << "Epoch: " << epoch << " | Batch: " << batch_index
                  << " | Loss: " << loss.item<float>() << std::endl;
        // 保存模型
        torch::save(net, "net.pt");
      }
    }
  }
}