据资料说这两个框架(LibTorch和PyTorch)是Torch的前端,前端的实现语言不同而已, 他们的后端都是基于ATen Tensor库,ATen是C++的写的Tensor库,构建在CUDA和CUDNN之上,它也可以运行在CPU上。所以说从理论上说,PyTorch和LibTorch功能是对等的,只是实现的语言不同而已。
这篇文章主要是看一下LibTorch库并且如何用其和C++实现设计深度学习模型。包括Tensor API的使用,自动微分的实现,简单的回归模型的实现,和稍微复杂点的深度网络的实现。
资料也说从性能的角度讲,LibTorch是端到端是C++实现,理论上有性能的优势,特别适合产品环境。性能是个复杂的话题,会牵扯方方面面。可以查看参考资料对比。
是否使用LibTorch还要看其他的因素,比如语言,C++的语法灵活但晦涩,需要构建编译才能执行,适合于需要低延迟,高性能或是多线程环境。而Python是动态语言语法较直观易学容易上手,库类丰富成熟。除非有很充分的理由否则构建深度学习模型还是建议用Python,因为会有很多资料可以参考,性能也不是那么不堪并且用户体验较好。
C++ Torch 前端库的设计目标是:
- 设计上尽量和PyTorch前端保持一致,比如命名,设计,功能和行为上保持一致。
- 强调灵活和用户体验高于微调优。C++中,经常可以调优代码,但是这些有时候是以牺牲用户体验。灵活和动态特性是PyTorch的核心特性,C++前端-LibTorch尽量会保留这些特性。
有一点需要说明,Python并不一定比C++慢。Python几乎会调用那些对应的C++的API完成高强度的计算,特别是那些数值操作,因此保持了用户体验和灵活性,其性能损失并不太大。如果你更偏好于C++,Torch的C++前端API,根据其设计哲学,也会尽量提供易用,灵活,友好和直观的,类似Python的API。
张量(Tensor)是torch核心数据结构
张量零维叫标量(Scalar),一维的张量称之为向量(Vector),二维张量为矩阵(Matrix),三维及以上称为为张量。 c++ torch命名空间包括了几乎所有的对tensor张量的的操作和函数。
张量的二维形式可以表示二维图像(h,w),三维张量可以用于表示彩色图像(channel,w,h),四维向量可以表示图片训练集(batch,channel,w,h).
以下是张量Tensor的基本操作,创建,加载,reshape,stack等等。在PyTorch上能实现的操作,在C++也可以借助于LibTorch实现。C++的LibTorch和PyTorch基本功能是对等的。
void tensor_funcs(){
//create ones
torch::Tensor ones=torch::ones({2,5});
std::cout<<"the Tensor ones created from torch"<<std::endl;
std::cout<<ones<<std::endl;
//created normailzied 3-dimensional tensor
torch::Tensor tensor = torch::randn({3, 4, 5});
std::cout<<"normal distributed tensor"<<std::endl;
std::cout<<tensor<<std::endl;
//load data from vector
std::vector<float> values{1,2,3,4,5,6,7,8,9,10};
torch::Tensor tensorValues=torch::from_blob(values.data(), { 2, 5});
std::cout<<"the data fron blob of vector"<<std::endl;
std::cout<<tensorValues<<std::endl;
//tensor slice indexing
torch::Tensor arange_tensor=torch::arange(0,100,1);
auto reshaped_tensor=arange_tensor.reshape({2,5,10});
std::cout<<reshaped_tensor<<std::endl;
//dstack the tensors
auto a=torch::tensor({1,2,3,4},torch::kFloat);
auto b=torch::tensor({5,6,7,8},torch::kFloat);
auto c=torch::dstack({a,b});
std::cout<<c<<std::endl;
/*
(1,.,.) =
1 5
2 6
3 7
4 8
[ CPUFloatType{1,4,2} ]
*/
//vstack the tensors
auto d=torch::vstack({a,b});
std::cout<<d<<std::endl;
/*
1 2 3 4
5 6 7 8
[ CPUFloatType{2,4} ]
*/
//hstack the tensors
auto e=torch::hstack({a,b});
std::cout<<e<<std::endl;
/*
1
2
3
4
5
6
7
8
[ CPUFloatType{8} ]
*/
}
Autograd自动微分计算功能
Torch有个非常优秀的功能是自动微分计算,导数和偏导数计算是内置在Tensor库中,关于自动导数和梯度计算可以参考这篇文章深度学习和PyTorch框架(一)。 在创建张量Tensor变量时指定需要梯度计算require_grad(true),然后再调用因变量backward(),比如y.backward()系统就可以自动回溯计算导数和偏导数或梯度,Torch系统就会回溯计算对应变量的导数或偏导数,这对学习模型设计来说提供了极大的帮助,是PyTorch如此受欢迎的原因之一。
void grad_basic() {
auto options =torch::TensorOptions().dtype(torch::kFloat32)
.layout(torch::kStrided)
.device(torch::kCPU, 1)
.requires_grad(true);
auto x = torch::tensor(1.0, torch::requires_grad(true));
auto w = torch::tensor(2.0, torch::requires_grad(true));
auto b = torch::tensor(3.0,options);
auto y = w * x + b; //y=2x+3
y.backward();
std::cout << x.grad() << '\n'; // x.grad()=dy/dx = 2
std::cout << w.grad() << '\n'; // w.grad()=dy/dw = 1
std::cout << b.grad() << "\n\n"; //b.grad()=dy/db= 1
}
注意到options指定设备Device是kCPU,当然可以直接指定kGPU如果你的系统里面GPU的话,这样可以使模型训练在GPU上完成。
生成线性模型
有了自动求导的功能,我们可以很容易的借助Torch的模块生成模型。 如下代码是借助于Torch框架。和PyTorch一样自定义模型需要继承自nn::Module.
//declare a simple linear model like y=w*x+b, and then train the model with the created data
struct Linearity:torch::nn::Module {
Linearity() {
// Construct and register one Linear submodule.
linear = register_module("linear", torch::nn::Linear(1, 1));
}
// Implement the linear forward
torch::Tensor forward(torch::Tensor x) {
x=linear->forward(x);
return x;
}
//declear the linear instance
torch::nn::Linear linear{nullptr};
};
以上代码是自定义线性模型,输入输出都是一个值类似有,比较简单的线性模型。可以通过训练数据拟合计算出参数
。以下是简单的数据准备和训练代码的实现。
void try_train_linear(){
auto net=std::make_shared<Linearity>();
//simulation data -created on the fly
std::vector<float> x={1.0,2.0,3.0,4.0,5.0};
std::vector<float> y={2.0,4.0,6.0,8.0,10.0};
auto x_train=torch::from_blob(x.data(),{5,1}) ;
auto y_train=torch::from_blob(y.data(),{5,1});
auto criterion=torch::nn::MSELoss();
torch::optim::SGD optimizer(net->parameters(), /*lr=*/0.01);
auto num_epochs=5000;
for(size_t epoch=1;epoch<=num_epochs;epoch++){
auto y_pred=net->forward((x_train));
auto loss=criterion(y_pred,y_train);
//reset it to zero_grad to avoid affect backpropgation
optimizer.zero_grad();
//backpropagation computing
loss.backward();
//update parameters(Weights and Bias)
optimizer.step();
if(epoch%100==0){
boost::format fmt=boost::format("Epoch: %1%/%2%,loss: %3%")%epoch%num_epochs%loss.item<float>();
std::cout<<fmt<<std::endl;
}
}
std::cout<<"the final model parameter is:"<<std::endl;
std::cout<<net->parameters()<<std::endl;
}
代码中,optimizer的默认行为是累积导数值,所以为了避免影响反向传播导数计算,每次调用loss.backgrad()前需要zero_grad复位其值。实例化SGD时,指定的是指定其学习速率参数,是深度学习模型训练的超参数(Hyperparameter)。其值的过大过小都会影响模型学习的质量。
loss损失函数是采用均方误差函数(mean squared error -squared L2 norm),是一种在深度学习网络中,常用的误差计算函数。
optimizer.step()是用于根据计算的导数和学习参数更新模型的参数,这里主要是,更新的方式是:
-
<-
-
<-
数字识别的深度网络C++实现
这个例子可以从这里Torch的官方网站看到手写数字识别神经网络。
非常经典的实现,简洁而优美。 这个网络包括三个全连接网络模块(fc-fully connected),relu激活函数,dropout网络和log_softmax。dropout层主要作用是消除过度拟合(overfitting)问题,过拟会造成新在新测试数据上模型训练时性能下降,dropout是一种矫正机制(Regulation),可以有效解决此种问题。
Net模型构造声明中,调用register_module注册三个全连接网络(fc),然后需要重载forward函数实现网络层之间的连接。
// Define a new hands-writing digits recognizing Module.
//x->fc1->relu->dropout->relu->log_softmax()
struct Net : torch::nn::Module {
Net() {
// Construct and register two Linear submodules.
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));
}
// Implement the Net's algorithm.
torch::Tensor forward(torch::Tensor x) {
// Use one of many tensor manipulation functions.
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;
}
// Use one of many "standard library" modules.
torch::nn::Linear fc1{nullptr}, fc2{nullptr}, fc3{nullptr};
};
手写数字的图片大小是28x28=784,作为input层fc1的输入,fc1层的输出是64个节点。在fc1输出端插入dropout层,其dropout率(rate)设置为0.5,这个设置参数和学习速率(learning rate) 参数都是深度学习中的超参数(Hyperparameter),需要在工程实践中指定其值,这个dropout层的dropout率为0.5. 注意的是这个dropout层,并不会改变fc输出的节点数量,它的输出和fc1的输出保持一致,只是有些节点用p的概率zero-out,这种方法可以有效的减少一些模式过度依赖某些几点,可以有效避免过拟,改善学习质量。
深度学习网络会有两个阶段,推理阶段(inference)和学习阶段(training)。推理主要是根据学习到的知识去根据输入推论其输出。学习阶段主要是通过反向传播和自动求导更新学习参数进而达到学习的目的。
以下代码是这个模型的学习代码。其实和上个例子的线性模型的学习步骤几乎一样,学习的参数变成了矩阵,输入参数从单维变成多维。
void use_sgd_learning() {
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;
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");
}
}
}
}
注意loss函数用的是torch::nll_loss(preds,target),这个之所以可这样,是因为神经网络的forward输出是log_softmax函数输出。 nll_loss(negative log likelihood)和交叉熵损失函数工作方式相似。 交叉熵损失函数组合了log_softmax和nll_loss以获取交叉熵的值。这样的话,可以定义神经网络的最后层为log_softmax,而不是softmax层。
总结
C++借助于LibTorch库API可以实现深度学习。实现代码和PyTorch写的代码结构非常相似,主要原因它们其实底层Tensor实现是用的同一个库加上现代C++的语法,基本神似PyTorch。只是前端API的语言实现不同而已。
LibTorch可以完全脱离Python环境,这点对硬件资源不是那么丰富的嵌入式系统颇具吸引力。
工程编译时采用的是Clang 16.0 ,CMake 3.30 和C++17标准。