前言

机器人上小电脑计算环境太杂了,LibTorch在arm架构上无法直接使用官方编译好的包。我之前也尝试过自己编译,踩了不少坑也没解决。此外,LibTorch作为C++接口居然跑得比PyTorch还慢,github上有人说慢2倍,我上机测试时有时单帧推理非常诡异的要跑几百毫秒,而用PyTorch推理只需要2ms。
总之各种大问题小错误不断,室友推荐我尝试一下Darknet,几乎没有依赖库,完全由C和Cuda构成,速度也很快。

安装

官方文档写得非常简洁明了:https://pjreddie.com/darknet/install/

直接安装

从Github上下载项目然后直接make即可,得到可执行文件 ./darknet

  1. git clone https://github.com/pjreddie/darknet.git
  2. cd darknet
  3. make

与CUDA联合编译

训练还是直接上GPU要快不少。需要先装好CUDA环境。
打开项目根目录下的 Makefile 并修改成一下内容,然后执行make命令。

  1. GPU=1

后续训练时默认使用GPU0,也可以通过命令行参数指定,作者官网有详细说明。(反正我只有一个GPU,略过)

训练

作者官网对于图片分类器训练的一些说明:https://pjreddie.com/darknet/imagenet/
我使用的是mnist数据集。

制作数据集

mnist数据集直接提供的是二进制文件,需要将其转换成普通的图片文件。darknet训练时对于图片的名称有要求,需要包含标签名,且不可重复出现多次,否则会带来歧义。我采用如下命名格式: <图片序号>_<标签名>
改了一下以前的程序,用到了PyTorch的接口。

  1. import os
  2. import torchvision.datasets.mnist as mnist
  3. from skimage import io
  4. print(os.getcwd())
  5. root = os.getcwd()
  6. train_set = (
  7. mnist.read_image_file(os.path.join(
  8. root+"/src_data", 'train-images.idx3-ubyte')),
  9. mnist.read_label_file(os.path.join(
  10. root+"/src_data", 'train-labels.idx1-ubyte'))
  11. )
  12. test_set = (
  13. mnist.read_image_file(os.path.join(
  14. root+"/src_data", 't10k-images.idx3-ubyte')),
  15. mnist.read_label_file(os.path.join(
  16. root+"/src_data", 't10k-labels.idx1-ubyte'))
  17. )
  18. print("train set:", train_set[0].size())
  19. print("test set:", test_set[0].size())
  20. labels = ["zero", "one", "two", "three", "four",
  21. "five", "six", "seven", "eight", "nine"]
  22. root = root+"/dataset"
  23. def convert_to_img(train=True):
  24. if (train):
  25. data_path = root + '/train/'
  26. if (not os.path.exists(data_path)):
  27. os.makedirs(data_path)
  28. for i, (img, label) in enumerate(zip(train_set[0], train_set[1])):
  29. img_path = data_path + str(i)+'_'+labels[label.item()] + '.jpg'
  30. io.imsave(img_path, img.numpy())
  31. else:
  32. data_path = root + '/test/'
  33. if (not os.path.exists(data_path)):
  34. os.makedirs(data_path)
  35. for i, (img, label) in enumerate(zip(test_set[0], test_set[1])):
  36. img_path = data_path + str(i)+'_'+labels[label.item()] + '.jpg'
  37. io.imsave(img_path, img.numpy())
  38. convert_to_img(True)
  39. convert_to_img(False)

处理后得到训练集train和测试集test两个文件夹,内容大致如下。
image.png
数据集概览

将两个数据集移动到darknet项目的data目录下,然后也在该目录下,用以下命令行生成图片的路径描述文件。

  1. find `pwd`/train -name \*.jpg > train.list
  2. find `pwd`/test -name \*.jpg > test.list

创建标签文件 labels.list ,存放标签名,内容如下

  1. zero
  2. one
  3. two
  4. three
  5. four
  6. five
  7. six
  8. seven
  9. eight
  10. nine

创建类别名文件 names.list ,在预测阶段有用,训练阶段不需要,可以将标签名映射成你想要显示的名称,这里我保持不变。

  1. zero
  2. one
  3. two
  4. three
  5. four
  6. five
  7. six
  8. seven
  9. eight
  10. nine

至此,data目录下有以下内容

  1. .
  2. ├── labels.list
  3. ├── names.list
  4. ├── test
  5. ├── test.list
  6. ├── train
  7. └── train.list
  8. 2 directories, 4 files

配置文件

在darknet的cfg目录下,写两个 .data.cfg 配置文件。
.data 文件主要用于描述数据集信息,内容如下

  1. classes=10
  2. train = /home/luzhan/My-Project/RM2020/deploy/darknet_gpu/data/train.list
  3. valid = /home/luzhan/My-Project/RM2020/deploy/darknet_gpu/data/test.list
  4. backup = /home/luzhan/My-Project/RM2020/deploy/darknet_gpu/backup/
  5. labels = /home/luzhan/My-Project/RM2020/deploy/darknet_gpu/data/labels.list
  6. names = /home/luzhan/My-Project/RM2020/deploy/darknet_gpu/data/names.list
  7. top=1

属性说明

  • classes:类别数
  • train:训练集图片路径描述文件
  • vaild:测试集图片路径描述文件
  • backup:模型参数保存路径
  • labels:标签名文件
  • names:类别名文件
  • top:top=n表示概率最高的前n个输出中有正确标签就视为分类正确

.cfg 文件描述网络,我直接复制了darknet提供的 cifar.cfg

  1. [net]
  2. batch=128
  3. subdivisions=1
  4. height=28
  5. width=28
  6. channels=3
  7. max_crop=32
  8. min_crop=32
  9. hue=.1
  10. saturation=.75
  11. exposure=.75
  12. learning_rate=0.1
  13. policy=poly
  14. power=4
  15. max_batches = 5000
  16. momentum=0.9
  17. decay=0.0005
  18. [convolutional]
  19. batch_normalize=1
  20. filters=32
  21. size=3
  22. stride=1
  23. pad=1
  24. activation=leaky
  25. [maxpool]
  26. size=2
  27. stride=2
  28. [convolutional]
  29. batch_normalize=1
  30. filters=16
  31. size=1
  32. stride=1
  33. pad=1
  34. activation=leaky
  35. [convolutional]
  36. batch_normalize=1
  37. filters=64
  38. size=3
  39. stride=1
  40. pad=1
  41. activation=leaky
  42. [maxpool]
  43. size=2
  44. stride=2
  45. [convolutional]
  46. batch_normalize=1
  47. filters=32
  48. size=1
  49. stride=1
  50. pad=1
  51. activation=leaky
  52. [convolutional]
  53. batch_normalize=1
  54. filters=128
  55. size=3
  56. stride=1
  57. pad=1
  58. activation=leaky
  59. [convolutional]
  60. batch_normalize=1
  61. filters=64
  62. size=1
  63. stride=1
  64. pad=1
  65. activation=leaky
  66. [convolutional]
  67. filters=10
  68. size=1
  69. stride=1
  70. pad=1
  71. activation=leaky
  72. [avgpool]
  73. [softmax]

至此,在cfg目录下有以下文件

  1. .
  2. ├── mnist_cifar10.cfg
  3. └── mnist.data
  4. 0 directories, 2 files

开始训练

先进入darknet根目录,执行以下命令,指定配置文件即可开始训练。

  1. ./darknet classifier train cfg/mnist.data cfg/mnist_cifar10.cfg

每训练到一定阶段,会在指定的backup目录下保存 .weights 后缀的权重参数文件,也会有 .backup 后缀的备份文件。本次训练中断或结束,可以使用这两类文件进行继续训练。如使用下列命令

  1. ./darknet classifier train cfg/mnist.data cfg/mnist_cifar10.cfg backup/mnist_cifar10_10.weights

训练过程中以 avg 值随着模型的迭代逐渐向0 收敛为优,如果不符合应考虑修改网络结构及训练超参数。
可以使用 valid 参数统计在测试集上的准确率。

  1. ./darknet classifier valid cfg/mnist.data cfg/mnist_cifar10.cfg backup/mnist_cifar10_10.weights

推理

指定配置文件,权重参数文件,待分类图片进行推理。

  1. ./darknet classifier predict cfg/mnist.data cfg/mnist_cifar10.cfg backup/mnist_cifar10_10.weights data/test/0_seven.jpg

以下为示例结果。

  1. layer filters size input output
  2. 0 conv 32 3 x 3 / 1 28 x 28 x 3 -> 28 x 28 x 32 0.001 BFLOPs
  3. 1 max 2 x 2 / 2 28 x 28 x 32 -> 14 x 14 x 32
  4. 2 conv 16 1 x 1 / 1 14 x 14 x 32 -> 14 x 14 x 16 0.000 BFLOPs
  5. 3 conv 64 3 x 3 / 1 14 x 14 x 16 -> 14 x 14 x 64 0.004 BFLOPs
  6. 4 max 2 x 2 / 2 14 x 14 x 64 -> 7 x 7 x 64
  7. 5 conv 32 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 32 0.000 BFLOPs
  8. 6 conv 128 3 x 3 / 1 7 x 7 x 32 -> 7 x 7 x 128 0.004 BFLOPs
  9. 7 conv 64 1 x 1 / 1 7 x 7 x 128 -> 7 x 7 x 64 0.001 BFLOPs
  10. 8 conv 10 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 10 0.000 BFLOPs
  11. 9 avg 7 x 7 x 10 -> 10
  12. 10 softmax 10
  13. Loading weights from backup/mnist_cifar10_10.weights...Done!
  14. data/test/0_seven.jpg: Predicted in 0.147498 seconds.
  15. 99.96%: seven

部署至OpenCV项目

这一步实现提供一张 cv::Mat 类型的图片,进行推理并返回分类结果。
作者官网上并没有提供关于C/C++接口的说明文档,但是通过命令行执行的函数和examples目录下的各个文件息息相关。比如处理 classifier 命令的相关函数可以在 classifier.c 文件中找到。根据这个文件中的 predict_classifier 函数和网上查到的资料,可以实现自己调用推理接口。
此外,darknet作者自己定义了图片文件类型,与OpenCV中常用的 Mat 不同,需要进行额外的转换。

编写CMakeLists.txt

  1. cmake_minimum_required(VERSION 3.7)
  2. project(deploy)
  3. set(CMAKE_CXX_STANDARD 11)
  4. # 指定文件夹位置
  5. set(OPENCV_DIR /usr/local/share/OpenCV)
  6. # 自动查找包
  7. find_package(OpenCV REQUIRED)
  8. # 添加源程序
  9. add_executable(deploy
  10. src/main.cpp
  11. src/timer.cpp
  12. src/Classifier.cpp)
  13. # 添加头文件
  14. include_directories(${OpenCV_INCLUDE_DIRS})
  15. include_directories(./include)
  16. include_directories(darknet_gpu/include)
  17. link_directories(darknet_gpu/)
  18. # 加入库文件位置
  19. target_link_libraries(deploy
  20. ${OpenCV_LIBS}
  21. -pthread
  22. -lMVSDK
  23. /lib/libMVSDK.so
  24. )
  25. target_link_libraries(deploy
  26. libdarknet_gpu.so
  27. )

添加动态库

直接建立CMake工程会出现以下报错。

  1. /usr/bin/ld: 找不到 -ldarknet

一般Linux把/lib和/usr/lib两个目录作为默认的库搜索路径。在这里我们需要用到 libdarknet.so 这个动态链接库,这个文件是之前编译库时得到的(注意CPU版和GPU版编译产生的文件是不同的)。可以通过 locate 命令直接找到这个库。

  1. $ locate libdarknet.so
  2. /home/luzhan/My-Project/RM2020/darknet/darknet_cpu/libdarknet.so
  3. /home/luzhan/My-Project/RM2020/darknet/darknet_gpu/libdarknet.so

进入/usr/lib/目录中,输入以下命令(CPU版本同理),创建一个软链接,链接到已有的库路径。

  1. sudo ln -s /home/luzhan/My-Project/RM2020/darknet/darknet_cpu/libdarknet.so libdarknet_gpu.so

实现Classifier类

Classifier.h

  1. //
  2. // Created by luzhan on 2020/10/3.
  3. //
  4. #ifndef CLASSIFIER_H
  5. #define CLASSIFIER_H
  6. #include <opencv2/opencv.hpp>
  7. #include <darknet.h>
  8. class Classifier {
  9. public:
  10. std::vector<std::string> labels;
  11. private:
  12. network *net;
  13. constexpr static int TOP = 1;
  14. float *input;
  15. public:
  16. Classifier(char *cfg_file, char *weight_file, const char *name_list);
  17. int predict(const cv::Mat &src);
  18. private:
  19. void imgConvert(const cv::Mat &img, float *dst);
  20. };
  21. #endif //CLASSIFIER_H

Classifier.cpp

  1. //
  2. // Created by luzhan on 2020/10/3.
  3. //
  4. #include "Classifier.h"
  5. using namespace cv;
  6. using namespace std;
  7. Classifier::Classifier(char *cfg_file, char *weight_file, const char *name_file) {
  8. net = load_network(cfg_file, weight_file, 0);
  9. set_batch_network(net, 1);
  10. srand(2222222);
  11. // 标签文件
  12. ifstream f_label(name_file);
  13. if (f_label.is_open()) {
  14. string label;
  15. while (getline(f_label, label)) {
  16. labels.emplace_back(label);
  17. }
  18. }
  19. size_t srcSize = 28 * 28 * 3 * sizeof(float);
  20. input = (float *) malloc(srcSize);
  21. }
  22. int Classifier::predict(const cv::Mat &src) {
  23. // 将图像转为yolo形式
  24. imgConvert(src, input);
  25. // 网络推理
  26. float *predictions = network_predict(net, input);
  27. if (net->hierarchy) {
  28. hierarchy_predictions(predictions, net->outputs, net->hierarchy, 1, 1);
  29. }
  30. int *indexes = (int *) calloc(TOP, sizeof(int));
  31. top_k(predictions, net->outputs, TOP, indexes);
  32. for (int i = 0; i < TOP; ++i) {
  33. int index = indexes[i];
  34. printf("%5.2f%%: %s\n", predictions[index] * 100, labels[index].c_str());
  35. }
  36. return indexes[0];
  37. }
  38. void Classifier::imgConvert(const cv::Mat &img, float *dst) {
  39. uchar *data = img.data;
  40. int h = img.rows;
  41. int w = img.cols;
  42. int c = img.channels();
  43. for (int k = 0; k < c; ++k) {
  44. for (int i = 0; i < h; ++i) {
  45. for (int j = 0; j < w; ++j) {
  46. dst[k * w * h + i * w + j] = data[(i * w + j) * c + k] / 255.;
  47. }
  48. }
  49. }
  50. }

使用Classifier类进行推理

推理单张图片

main.cpp

  1. #include <vector>
  2. #include <opencv2/opencv.hpp>
  3. #include <iostream>
  4. #include "timer.h"
  5. #include "Classifier.h"
  6. using namespace cv;
  7. using namespace std;
  8. int main() {
  9. Classifier classifier("../darknet_gpu/cfg/mnist_cifar10.cfg",
  10. "../darknet_gpu/backup/mnist_cifar10_10.weights",
  11. "../darknet_gpu/data/names.list");
  12. classifier.predict(src);
  13. }

输出结果

  1. layer filters size input output
  2. 0 conv 32 3 x 3 / 1 28 x 28 x 3 -> 28 x 28 x 32 0.001 BFLOPs
  3. 1 max 2 x 2 / 2 28 x 28 x 32 -> 14 x 14 x 32
  4. 2 conv 16 1 x 1 / 1 14 x 14 x 32 -> 14 x 14 x 16 0.000 BFLOPs
  5. 3 conv 64 3 x 3 / 1 14 x 14 x 16 -> 14 x 14 x 64 0.004 BFLOPs
  6. 4 max 2 x 2 / 2 14 x 14 x 64 -> 7 x 7 x 64
  7. 5 conv 32 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 32 0.000 BFLOPs
  8. 6 conv 128 3 x 3 / 1 7 x 7 x 32 -> 7 x 7 x 128 0.004 BFLOPs
  9. 7 conv 64 1 x 1 / 1 7 x 7 x 128 -> 7 x 7 x 64 0.001 BFLOPs
  10. 8 conv 10 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 10 0.000 BFLOPs
  11. 9 avg 7 x 7 x 10 -> 10
  12. 10 softmax 10
  13. Loading weights from ../darknet_gpu/backup/mnist_cifar10_10.weights...Done!
  14. 99.96%: seven

检验模型正确率

更新:用 valid 命令给出top1的准确率。

  1. ./darknet classifier valid cfg/hero.data cfg/hero.cfg backup/hero.backup

darknet训练的时候不显示推理准确率,也没有提供相关接口(可能是我没有找到)。部署时也验证一下整个模型的准确率。

  1. #include <vector>
  2. #include <opencv2/opencv.hpp>
  3. #include <iostream>
  4. #include "timer.h"
  5. #include "Classifier.h"
  6. using namespace cv;
  7. using namespace std;
  8. int main() {
  9. Classifier classifier("../darknet_gpu/cfg/mnist_cifar10.cfg",
  10. "../darknet_gpu/backup/mnist_cifar10_10.weights",
  11. "../darknet_gpu/data/names.list");
  12. //Mat src = imread("../darknet_gpu/data/test/0_seven.jpg");
  13. //classifier.predict(src);
  14. int cnt = 0;
  15. vector<string> image_set;
  16. ifstream f_path("../darknet_gpu/data/test.list");
  17. Timer timer;
  18. timer.start();
  19. if (f_path.is_open()) {
  20. string img_path;
  21. while (getline(f_path, img_path)) {
  22. int pos1 = img_path.find_last_of('_');
  23. int pos2 = img_path.find_last_of('.');
  24. string label = img_path.substr(pos1 + 1, pos2 - pos1 - 1);
  25. Mat src = imread(img_path);
  26. int ans = classifier.predict(src);
  27. //cout << label << ' ' << ans << '\n';
  28. if (classifier.labels[ans] == label) {
  29. cnt++;
  30. }
  31. }
  32. }
  33. cout << cnt << '\n';
  34. cout << cnt / 10000.0 << '\n';
  35. timer.printTime("分类推理");
  36. }

输出结果(我把Classifier类中的标准输出部分注释了)

  1. /home/luzhan/My-Project/RM2020/deploy/cmake-build-default/deploy
  2. layer filters size input output
  3. 0 conv 32 3 x 3 / 1 28 x 28 x 3 -> 28 x 28 x 32 0.001 BFLOPs
  4. 1 max 2 x 2 / 2 28 x 28 x 32 -> 14 x 14 x 32
  5. 2 conv 16 1 x 1 / 1 14 x 14 x 32 -> 14 x 14 x 16 0.000 BFLOPs
  6. 3 conv 64 3 x 3 / 1 14 x 14 x 16 -> 14 x 14 x 64 0.004 BFLOPs
  7. 4 max 2 x 2 / 2 14 x 14 x 64 -> 7 x 7 x 64
  8. 5 conv 32 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 32 0.000 BFLOPs
  9. 6 conv 128 3 x 3 / 1 7 x 7 x 32 -> 7 x 7 x 128 0.004 BFLOPs
  10. 7 conv 64 1 x 1 / 1 7 x 7 x 128 -> 7 x 7 x 64 0.001 BFLOPs
  11. 8 conv 10 1 x 1 / 1 7 x 7 x 64 -> 7 x 7 x 10 0.000 BFLOPs
  12. 9 avg 7 x 7 x 10 -> 10
  13. 10 softmax 10
  14. Loading weights from ../darknet_gpu/backup/mnist_cifar10_10.weights...Done!
  15. 9794
  16. 0.9794
  17. 分类推理用时: 2452.01ms

10000张测试集图片一张张从文件读入并完成推理,共花了2452ms,速度上相当可观。准确率为97.94%。

效果小结

训练得到的 .weights 权重参数文件是通用的,无论是CPU还是GPU,只需要修改CMake项目链接的动态库即可。
单就推理任务计时,CPU推理单张图片耗时1.5ms左右,GPU推理单张图片在0.2-0.3ms左右(刚启动会需要不少时间,但是到第二张就迅速降下来,最终稳定在0.2ms左右)
本人测试机器配置:

  • CPU:i7-8750H 2.20GHz*12Core
  • GPU:GeForce GTX 1050

在NUC上部署(CPU推理),单张1.8ms左右;在妙算2上部署(GPU推理),单张1.2ms左右。
从最近的实际测试中,装甲板数字分类选用彩色图像作为样本训练效果较好,可以有效分类出误识别的目标。

遭遇问题

Q1 GPU推理出错

在妙算2上部署,直接执行编译好的项目文件出现以下报错。

  1. $ ./deploy
  2. layer filters size input output
  3. 0 CUDA Error: unknown error
  4. deploy: ./src/cuda.c:36: check_error: Assertion `0' failed.
  5. Aborted (core dumped)

解决方法:执行时加上 sudo

  1. sudo ./deploy

Q2 实际部署与命令行测试结果不同

在实际项目中部署后推理结果与使用命令行推理同一张图片的结果不同。因为OpenCV默认的通道顺序为BGR,而Darknet使用的通道顺序为RGB,需要先进行转换。
而且Darknet将图片均视为3通道图。

参考链接