PyTorch C++ 使用笔记
PyTorch 在 Python 下很好用,好写,稳定,速度也不差。但有时候一个项目除了神经网络,仍需要大量的计算,此时 Python 的运行速度就差了点意思。几个月前注意到 PyTorch 1.0 版本除了改进了很多特性(比如把我一直没弄懂区别的 Tensor
和 Variable
合并了),更好用了,还提出了对 C++ API 的支持(虽然 PyTorch 底层就是 C 写的,感觉怪怪的)。Anyway,正好一个项目需要 C++ 在神经网络的结果上做一些搜索计算,试试看用 C++ 写 PyTorch。
1 代码环境
家里一台12年 Macbook Air 10.14.1,集显,主要写写代码。 实验室一台工作站 Ubuntu 18.04,GTX 1070,主要运行代码。
2 遇到的问题和解决办法
做个记录以备日后翻阅,希望也能帮助一些人节省时间。
2.1 CUDA 9.1版本太低,升级到CUDA 10
因为装 Ubuntu 18.04 的时候,官方的 CUDA 9.2 不支持 18.04 所以我通过
apt-get install nvidia-cuda-toolkit
安装的 CUDA 9.1,因此卸载也方便。按着官网的步骤安装完 CUDA 10 后,我重启了之后才一切正常。另外,nvidia-cuda-toolkit
会把 nvcc
放在 /usr/bin
下,而 CUDA 10 的路径是 /usr/local/cuda/bin
,稍作注意。
cuDNN
也是一个必备的包,因为检测到了 CUDA 所以这个包也要同时安装。
2.2 Mac下的安装和测试
本想自己写 Makefile
链接库文件,但遇到的问题太多,就采用官方 CMakeLists.txt
,真香。
按照官方教程,cmake
和 make
都挺顺利,但运行时报错如下
dyld: Library not loaded: @rpath/libmklml.dylib
Referenced from: /Users/yq/Sources/libtorch/lib/libcaffe2.dylib
Reason: image not found
需要的是 mklml
的库文件,网上一搜信息很少,只看到和 Intel Math Kernel Library
相关。看了一个帖子说了 mklml
和 mkldnn
的区别,觉得 mklml
是我们想要的,于是从官网下了 mklml-mac
的包,文件很大,看起来支持很多,但安装后,只在 opt/intel
看到了一些其他的库文件,却没有我们想要的 libmklml.dylib
,可能我找的位置不对,找了十几分钟后我把它卸了。
回头看 mkl-dnn ,我找到了编译后会有 libmklml.dylib
文件,于是从源文件编译安装,搞定!
2.3 data_loader遍历报错
遵循例子里的 MNIST
数据库,我们这么定义 data_loader
的话,会报错。
auto train_loader = torch::data::make_data_loader(
torch::data::datasets::MNIST(
options.data_root, torch::data::datasets::MNIST::Mode::kTrain),
// .map(Normalize(0.1307, 0.3081))
// .map(torch::data::transforms::Stack<>()),
options.batch_size);
for (auto & batch: *train_loader) {
Tensor data = batch.data.to(device);
Tensor target = batch.target.to(device);
}
报错如下:
error:
reference to non-static member function must be called; did you mean to
call it with no arguments?
Tensor data = batch.data.to(device);
~~~~~~^~~~
()
注意,这里的遍历 train_loader
需要用 *train_loader
,否则有另一个编译报错:
error:
invalid range expression of type
'std::__1::unique_ptr<torch::data::DataLoader<CompatibilityDataset,
torch::data::samplers::RandomSampler>,
std::__1::default_delete<torch::data::DataLoader<CompatibilityDataset,
torch::data::samplers::RandomSampler> > >'; did you mean to dereference it
with '*'?
没有 data
的成员变量,是因为我们没有注释掉了 map(torch::data::transforms::Stack<>())
。注释前,batch
的子结构是 vector
(batch_size),每个元素的子结构是包含 data
和 target
的 Example<>
。因此自然没有相应的成员函数,使用了 map
之后,batch
的子结构是 data
和 target
,它们的子结构分别都是 vector
(batch_size)。
加上 map(torch::data::transforms::Stack<>())
后解决报错。
2.4 nll_loss报错
nll_loss(output, target)
具体是什么函数我就不赘述了,在 PyTorch 里,要求 target
的类型必须是 Long
。否则有如下报错:
libc++abi.dylib: terminating with uncaught exception of type c10::Error:
Expected object of scalar type Long but got scalar type Float for argument #2 'target'
(checked_tensor_unwrap at /Users/administrator/nightlies/2018_11_15/wheel_build_dirs/libtorch_2.7/pytorch/aten/src/ATen/Utils.h:74)
修改 Tensor
类型的方法参照文档 Tensor Creation API,一个简单的例子是:
Tensor a = torch::rand({64});
Tensor b = torch::randint(0, 10, {64}, dtype(kInt64));
Tensor c = nll_loss(a, b);
还有一个错误要注意,nll_loss
和 PyTroch 里的 CrossEntropyLoss
都是接受大小为 (mini-batch, C)
的,如果把上述代码放在 mini-batch
里面,会报错:
terminating with uncaught exception of type std::runtime_error:
multi-target not supported at /Users/administrator/nightlies/2018_11_15/wheel_build_dirs/libtorch_2.7/pytorch/aten/src/THNN/generic/ClassNLLCriterion.c:21
从 ClassNLLCriterion.c
中,可以看到报错的语句:
if (THIndexTensor_(nDimensionLegacyAll)(target) > 1) {
THError("multi-target not supported");
}
这是因为 target
要比 output
少一维,举个论坛中的例子,来自 ptrblck:
criterion = nn.CrossEntropyLoss()
output = Variable(torch.randn(10, 120).float())
target = Variable(torch.FloatTensor(10).uniform_(0, 120).long())
loss = criterion(output, target)
如果在自定义 Dataset
中的 get
函数返回的是仅有一个数字的 Tensor
,那么经过 batch
后,也依然是二维的,尽管是一个 mini-batch x 1
的 Tensor
,用 squeeze
函数即可:
target = squeeze(target, /*dim*/1);
2.5 Scalar转换成Tensor
将标量如 float
、int
转换成 Tensor
其实很简单,只需要:
int class_idx = 2;
Tensor target = torch::tensor(class_idx, dtype(kInt64));
注意这里的命名空间 torch::
必须显式给出,否则编译会因为歧义性报错。因为接受同样参数的构造函数还有 at::tensor
和 at::native::tensor
。如果用了后面这两个,会出现很奇怪的错误:
terminate called after throwing an instance of 'c10::Error'
what(): Expected object of type Variable but found type CUDALongType for argument #1 'target' (checked_cast_variable at /pytorch/torch/csrc/autograd/VariableTypeManual.cpp:186)
这是因为后两个构造函数创建出来的变量不是 Variable
类型的,它们可能是用在别的地方。按理说是可以用
inline Variable autograd::make_variable(at::Tensor data, bool requires_grad = false) {}
来转换成 Variable
,但我试了后有问题,可能姿势不对。。。所以最好的办法还是用 torch::tensor
来转换标量。
2.6 将OpenCV格式的图片转换成Tensor
这里要解决两个问题,一个是类型的问题,二是排列顺序的问题。
格式上,依照官方文档的 from_blob()
函数:
float data[] = { 1, 2, 3,
4, 5, 6 };
torch::Tensor f = torch::from_blob(data, {2, 3});
我们可以得到:
cv::Mat img = cv::imread("test.png");
Tensor img_tensor = torch::from_blob(img.data, {img.rows, img.cols, 3}, dtype(kInt8));
第二个要解决的问题是排列问题,在 OpenCV 里是按照 H x W x C
的顺序,在 PyTorch 的网络结构里,一般是按照 C x H x W
的顺序。因此,用 permute
即可:
img_tensor = img_tensor.permute({2, 0, 1});
将 OpenCV 的格式转为 PyTorch 格式。
2.7 网络输出nan
这是困扰了我六个小时的 bug!!!
首先遇到的情况是,网络在多 batch 的情况下,前几个还是正常输出,后面都是 nan。第一反应是学习率设置的不对,调整后,问题依旧。
于是一层一层输出看原因,发现是网络的输入就有 nan。于是我担心是并行读数据有问题,把构造 data_loader
用的 data::DataLoaderOptions
换回 batch_size
,问题依旧。
这个问题最大的麻烦在于,不是每次都出现,和图片也无关,图片的显示都是正常的。在看了官方的重载的 MNIST Dataset 后我猜测,是不是读硬盘文件的时候,没读取完成就输入到网络里面了(虽然以我对计算机的理解,这应该不可能,但新东西不能排除任何一个坑。。。并且官方代码也是这么做的),我改写了读取模式,先把所有图片读到内存里再使用 get
,问题依旧。
仔细研究 MNIST Dataset后,我觉得它的姿势和我不一样,对于输入图片,我是先从 uchar
转换成 float
,然后再使用 from_blob
转换成 Tensor
,类型设置的是 kFloat32
。而它的做法恰好和我相反,于是我就试了一下,先读进 Tensor
,再做类型转换,搞定。。贴代码:
data::Example<> CompatibilityDataset::get(size_t index) {
cv::Mat img = cv::imread(dataset_folder + to_string(data[index].first) + ".png");
Tensor img_tensor = torch::from_blob(img.data, {img_size.height, img_size.width, 3}, kByte);
img_tensor = img_tensor.permute({2, 0, 1}).toType(kFloat32).div_(255);
int class_idx = symbol2class[data[index].second];
Tensor target = torch::tensor(class_idx, dtype(kInt64));
return {img_tensor, target};
}
其中,data
是一个 vector< pair<int, char> >
的数组,保存了图片编号和所代表的字符。