ncnn加速效果验证
前言
本文记录ncnn神经网络前向计算框架(ncnn项目地址)加速效果的验证过程,进一步探究ncnn加速原理并学习利用ncnn框架自定义推理加速。
流程:
- 使用pytorch实现的人脸识别网络
MobileFaceNet
(参考代码地址)和对应预训练权重生成onnx格式中间文件,再转成ncnn需要的*.bin
和*.param
格式 - 使用lfw人脸数据集作为验证数据集
- 对比
MobileFaceNet
使用pytorch推理使用ncnn推理所用时长和精度
关于模型部署
参考:深度学习之模型部署
推理引擎
- 转化:预训练模型转化、量化、剪枝
- 推理:用精简的推理框架,加载模型并计算
部署平台
- 在线服务器端:精度优先。大模型和分布式部署,对延迟不敏感
- 离线嵌入式端:兼顾精度和速度。小模型和延迟敏感。
部署方式
- 原始训练框架:pytorch、tf等。缺点需要安装整个框架,功能冗余、内存占用大。
- 训练框架自带的部署引擎:TF-Lite、Pytorch_mobile。缺点只支持自己框架下的模型,支持的硬件/OS有限
- 手动模型重构:手写c/c++实现计算图,导入权重
- 高性能推理引擎:ncnn、mnn、Tengine等。可以移植主流框架的模型文件,只依赖c/c++库,支持多种os,方便python、c、app不同语言调用
移植细节
pytorch-onnx-ncnn步骤
主要记录生成*.bin
和*.param
文件以后ncnn框架下的代码实现
正则化
pytorch代码中的正则化:
'val': transforms.Compose([
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
参考ncnn官网对substract_mean_normalize
方法的说明,c++实现如下:
const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
input.substract_mean_normalize(mean_vals, norm_vals);
解读:模型输入乘mean_vals
,再乘norm_vals
,norm_vals
包含1/255.f
对应transforms.ToTensor()
。
模型输入
pytorch代码中模型输入为2个batch:
imgs[0] = transform(img.copy(), False)
imgs[1] = transform(img.copy(), True)
with torch.no_grad():
output = model(imgs)#将翻转与未翻转的图片合并输入模型
ncnn::Mat
没有batch维,官方建议是用将每一维分别输入再对输出做处理。c++实现:
// 分别在旋转和不旋转的情况下获得模型输出
get_output(img2,net,x1,false);
get_output(img2,net,x2,true);
计时
仅对模型推理过程计时:
#pytorch
start = time.time()
with torch.no_grad():
output = model(imgs) # 将翻转与未翻转的图片合并输入模型
end = time.time()
elapsed += end - start
//ncnn
clock_t start,finish;
start=clock();
// ncnn前向计算
ncnn::Extractor extractor = net.create_extractor();
extractor.input("input", input);
ncnn::Mat output;
extractor.extract("output", output);
finish=clock();
duration+=(double)(finish-start);
ubuntu部署结果
\ | 耗时(ms) | 错判(共500对) |
---|---|---|
pytorch | 57658.20217132568 | 1 |
ncnn(线程数1) | 20581.8 | 5 |
ncnn(线程数2) | 28762 | 5 |
结论:ncnn可以提升推理速度,但是可能由于和原生网络输入不一致等原因导致精度不如原来的网络。开启多线程以后耗时增加的原因有待探究。
线程数增加,耗时也增加的问题
这是由于用clock()函数导致的,所记录的为cpu时间,也就是当开启两个线程时将两个线程分别耗时相加,实际人体验是小于这个时间的。
改为使用std::chrono
(官方例子)计时,避免了这个问题。
auto t1 = std::chrono::high_resolution_clock::now();
// ncnn前向计算
ncnn::Extractor extractor = net.create_extractor();
extractor.input("input", input);
ncnn::Mat output;
extractor.extract("output", output);
std::chrono::duration<double, std::milli> fp_ms;
auto t2 = std::chrono::high_resolution_clock::now();
fp_ms = t2 - t1;
duration+=fp_ms.count();
重新得到计算时间:
\ | 耗时(ms) | 错判(共500对) |
---|---|---|
pytorch | 57658.20217132568 | 1 |
ncnn(线程数1) | 19660.9 | 5 |
ncnn(线程数2) | 13833 | 5 |
ncnn(线程数3) | 12918.7 | 5 |
ncnn(线程数4) | 12009.7 | 5 |
代码地址
安卓部署
在Android Studio工程中使用opencv-mobile代替Opencv,将上面c++代码改为转化为JNI,并和Java接口对接,最后在Android上运行。
故障排除
参数太旧问题
- 前面生成的param和bin文件都在c++下的ncnn框架下成功加载并运行,但是Android版ncnn记载过程中却报错
param is too old, please regenerate
,加载失败。 - 网络上的资料显示是由于param文件第一行magic number不等于767517,但上述生成的param第一行就是767517。
- 使用ncnn官网的一个安卓部署项目(ncnn-android-squeezenet)中的param和bin文件,依旧报这个错误。
- 为了不在这个报错上一直纠结,接下来在成功项目ncnn-android-squeezenet的基础上改造成MobileFaceNet。
安卓项目迁移问题
- build过程中报错:
No variants found for ':app'. Check build files to ensure at least one variant exists. at
。查资料需要在build.gradle和gradle.properties中修改sdk有关的版本参数,尝试过后无效。 - 进入
File->Project Structure
,将Project和Modules中的参数设置成和build成功的项目一致,比如前面报param is too old
那个版本,虽然参数加载失败,但是build成功。ncnn-android-squeezenet成功运行。
图片格式转换问题
Android读取图片格式为bitmap
,输入ncnn网络的格式是ncnn::Mat
,输入之前用opencv对图片进行一些处理,格式为cv::Mat
。下面记录各种格式的读取和转换:
- bitmap
Uri selectedImage = data.getData();
Bitmap bitmap = decodeUri(selectedImage);
Bitmap rgba = bitmap.copy(Bitmap.Config.ARGB_8888, true);
yourSelectedImage = Bitmap.createScaledBitmap(rgba, 112, 112, false);//图像格式为rgba
- bitmap转cv::Mat
void *pixels = 0;
cv::Mat &dst = mat;
CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0);
CV_Assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
info.format == ANDROID_BITMAP_FORMAT_RGB_565);//此处为ANDROID_BITMAP_FORMAT_RGBA_8888
CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
CV_Assert(pixels);
dst.create(info.height, info.width, CV_8UC4);
cv::Mat tmp(info.height, info.width, CV_8UC4, pixels);//注意这两处由于是4通道,因此设为CV_8UC4
tmp.copyTo(dst);//保存到dst
- cv::Mat转ncnn::Mat
//注意此处第二个参数type设置为 ncnn::Mat::PIXEL_RGBA2BGR
ncnn::Mat input = ncnn::Mat::from_pixels(img2.data, ncnn::Mat::PIXEL_RGBA2BGR, img2.cols, img2.rows);
安卓部署结果
项目地址
检测精度和在PC端一致。检测相同的图片,GPU耗时比CPU更长,根据官方解释,这是因为很多针对GPU的优化还没有完成(例如winograd卷积,算子融合,fp16存储和算术等),而且arm架构下的CPU优化已经做够充分,所以CPU下更快。
相同(cpu):
相同(gpu):
不同(cpu):