第 2 章:NCNN 部署 AlexNet
1. 什么是 AlexNet
AlexNet 是一个非常经典的卷积神经网络,应用于分类任务。Alex 等人在 2012 年提出了 AlexNet 网络结构模型,并在 ILSVRC-2012 上以巨大的优势获得第一名,引爆了神经网络的应用热潮,使得卷积神经网络 CNN 成为在图像分类上的核心算法模型。
因此,AlexNet 的被认为是计算机视觉领域最有影响力的研究之一,它刺激了更多使用卷积神经网络和 GPU 来加速深度学习的论文的出现。
AlexNet 包含八层。前五层是卷积层,之后一些层是最大池化层,最后三层是全连接层。它使用了非饱和的 ReLU
激活函数,显示出比 tanh
和 sigmoid
更好的训练性能。
2. 转换 Caffe 模型
AlexNet 的官方版本是 Caffe 模型,所以我们需要将 Caffe 格式的网络模型转换为 NCNN 格式的模型。
使用 NCNN 需要下面这两个模型文件:
deploy.prototxt
:https://github.com/BVLC/caffe/blob/master/models/bvlc_alexnet/deploy.prototxtbvlc_alexnet.caffemodel
:http://dl.caffe.berkeleyvision.org/bvlc_alexnet.caffemodel
Caffe 官方版本也包含了将旧版本的 Caffe 模型转换成新版本的工具,如果我们下载的模型是旧版本的,需要进行转换:
upgrade_net_proto_text [old_prototxt] [new_prototxt]
upgrade_net_proto_binary [old_caffemodel] [new_caffemodel]
如果你的计算机内没有安装 Caffe,可以直接下载编译好的 Caffe 的 GPU 或者 CPU 版本,Caffe 已经不再维护,所以编译起来也很困难。其 Windows 版本的下载地址为:https://github.com/Coderx7/Caffe_1.0_Windows/releases/tag/caffe_1.0_windows_build_all_archs_py36,解压后 bin/
文件夹内有我们需要的工具,我们可以将这个路径添加到 PATH 上。
我们需要修改 .prototxt
文件,这里的每个 layer { ... }
都代表神经网络的一层,我们只需要修改第一层。将输入层改用 Input
,因为每次只需要做一个图片,所以第一个 dim
设为 1
:
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 1 dim: 3 dim: 227 dim: 227 } }
}
最后使用 NCNN 自带的工具 caffe2ncnn
将 Caffe 模型转换为 NCNN:
caffe2ncnn deploy.prototxt bvlc_alexnet.caffemodel alexnet.param alexnet.bin
如果你需要将模型本身转换为代码,可以使用 ncnn2mem
工具将模型转换为代码,这样可以降低模型的可见性,防止明文保存 .param
文件:
ncnn2mem alexnet.param alexnet.bin alexnet.id.h alexnet.mem.h
以上命令将生成 alexnet.param.bin
文件和两个头文件(.h
文件),这样我们可以在生产环境中使用。
3. 加载网络模型
结合上一节的代码框架,我们进行快速验证。
3.1 快速验证
直接加载 .param
和 .bin
,适合快速验证效果使用:
ncnn::Net net;
net.load_param("alexnet.param");
net.load_model("alexnet.bin");
3.2 构建生产版本
加载二进制的 .param.bin
和 .bin
,没有可见字符串,适合 APP 分发模型资源:
ncnn::Net net;
net.load_param_bin("alexnet.param.bin");
net.load_model("alexnet.bin");
3.3 从内存加载
从内存引用加载网络和模型,没有可见字符串,模型和数据全在代码中,没有任何外部文件:
#include "alexnet.mem.h"
ncnn::Net net;
net.load_param(alexnet_param_bin);
net.load_model(alexnet_bin);
3.4 从单个文件中加载模型
Net
类有从 FILE*
文件描述加载的接口,可以利用这点把多个网络和模型文件合并为一个:
合并网络模型文件:
cat alexnet.param.bin alexnet.bin > alexnet-all.bin
使用网络模型:
#include "net.h"
FILE* fp = fopen("alexnet-all.bin", "rb");
ncnn::Net net;
net.load_param_bin(fp);
net.load_model(fp);
fclose(fp);
3.5 卸载模型
Net
支持卸载模型以便重用对象来加载其他模型:
net.clear();
4. 使用模型
NCNN 用自己的数据结构 Mat
来存放输入和输出数据 输入图像的数据要转换为 Mat
,依需要减去均值和乘系数:
#include "mat.h"
unsigned char* rgbdata; // data pointer to RGB image pixels
int w; // image width
int h; // image height
ncnn::Mat in = ncnn::Mat::from_pixels(rgbdata, ncnn::Mat::PIXEL_RGB, w, h);
const float mean_vals[3] = { 104.f, 117.f, 123.f };
in.substract_mean_normalize(mean_vals, 0);
执行前向网络,获得计算结果:
#include "net.h"
ncnn::Mat in; // input blob as above
ncnn::Mat out;
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.input("data", in);
ex.extract("prob", out);
如果是二进制的 param.bin
方式,没有可见字符串,利用 alexnet.id.h
的枚举来代替 blob
的名字
#include "net.h"
#include "alexnet.id.h"
ncnn::Mat in; // input blob as above
ncnn::Mat out;
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.input(alexnet_param_id::BLOB_data, in);
ex.extract(alexnet_param_id::BLOB_prob, out);
获取 Mat
中的输出数据,Mat
内部的数据通常是三维的,c / h / w
,遍历所有获得全部分类的分数:
ncnn::Mat out_flatterned = out.reshape(out.w * out.h * out.c);
std::vector<float> scores;
scores.resize(out_flatterned.w);
for (int j=0; j<out_flatterned.w; j++) {
scores[j] = out_flatterned[j];
}
5. 使用技巧
5.1 转换输入数据
Mat
转换图像的时候可以顺便转换颜色和缩放大小,这些顺带的操作也是有优化的,支持 RGB2GRAY
、GRAY2RGB
、RGB2BGR
等常用转换,支持缩小和放大。
#include "mat.h"
unsigned char* rgbdata; // data pointer to RGB image pixels
int w; // image width
int h; // image height
int target_width = 227; // target resized width
int target_height = 227; // target resized height
ncnn::Mat in = ncnn::Mat::from_pixels_resize(rgbdata, ncnn::Mat::PIXEL_RGB2GRAY, w, h, target_width, target_height)
5.2 轻量模式
Extractor
有轻量模式,开启后每次计算一层将上一层的数据释放,以达到节省内存的目的,但是这样就无法获取中间态结果,适合生产模式下使用:
ex.set_light_mode(true);
5.3 多线程加速
Extractor
有个多线程加速的开关,设置线程数能加快计算:
ex.set_num_threads(4);