PyTorch 在 Python 下很好用,好写,稳定,速度也不差。但有时候一个项目除了神经网络,仍需要大量的计算,此时 Python 的运行速度就差了点意思。几个月前注意到 PyTorch 1.0 版本除了改进了很多特性(比如把我一直没弄懂区别的 TensorVariable 合并了),更好用了,还提出了对 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,真香。

按照官方教程,cmakemake 都挺顺利,但运行时报错如下

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 相关。看了一个帖子说了 mklmlmkldnn 的区别,觉得 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),每个元素的子结构是包含 datatargetExample<>。因此自然没有相应的成员函数,使用了 map 之后,batch 的子结构是 datatarget,它们的子结构分别都是 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 1Tensor,用 squeeze 函数即可:

target = squeeze(target, /*dim*/1);

2.5 Scalar转换成Tensor

将标量如 floatint 转换成 Tensor 其实很简单,只需要:

int class_idx = 2;
Tensor target = torch::tensor(class_idx, dtype(kInt64));

注意这里的命名空间 torch::必须显式给出,否则编译会因为歧义性报错。因为接受同样参数的构造函数还有 at::tensorat::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> > 的数组,保存了图片编号和所代表的字符。

砖家说:扫码能带给人愉悦