Skip to content

第 2 章:NCNN 部署 AlexNet

1. 什么是 AlexNet

AlexNet 是一个非常经典的卷积神经网络,应用于分类任务。Alex 等人在 2012 年提出了 AlexNet 网络结构模型,并在 ILSVRC-2012 上以巨大的优势获得第一名,引爆了神经网络的应用热潮,使得卷积神经网络 CNN 成为在图像分类上的核心算法模型。

因此,AlexNet 的被认为是计算机视觉领域最有影响力的研究之一,它刺激了更多使用卷积神经网络和 GPU 来加速深度学习的论文的出现。

AlexNet 包含八层。前五层是卷积层,之后一些层是最大池化层,最后三层是全连接层。它使用了非饱和的 ReLU 激活函数,显示出比 tanhsigmoid 更好的训练性能。

2. 转换 Caffe 模型

AlexNet 的官方版本是 Caffe 模型,所以我们需要将 Caffe 格式的网络模型转换为 NCNN 格式的模型。

使用 NCNN 需要下面这两个模型文件:

Caffe 官方版本也包含了将旧版本的 Caffe 模型转换成新版本的工具,如果我们下载的模型是旧版本的,需要进行转换:

bash
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

properties
layer {
  name: "data"
  type: "Input"
  top: "data"
  input_param { shape: { dim: 1 dim: 3 dim: 227 dim: 227 } }
}

最后使用 NCNN 自带的工具 caffe2ncnn 将 Caffe 模型转换为 NCNN:

bash
caffe2ncnn deploy.prototxt bvlc_alexnet.caffemodel alexnet.param alexnet.bin

如果你需要将模型本身转换为代码,可以使用 ncnn2mem 工具将模型转换为代码,这样可以降低模型的可见性,防止明文保存 .param 文件:

bash
ncnn2mem alexnet.param alexnet.bin alexnet.id.h alexnet.mem.h

以上命令将生成 alexnet.param.bin 文件和两个头文件(.h 文件),这样我们可以在生产环境中使用。

3. 加载网络模型

结合上一节的代码框架,我们进行快速验证。

3.1 快速验证

直接加载 .param.bin,适合快速验证效果使用:

cpp
ncnn::Net net;
net.load_param("alexnet.param");
net.load_model("alexnet.bin");

3.2 构建生产版本

加载二进制的 .param.bin.bin,没有可见字符串,适合 APP 分发模型资源:

cpp
ncnn::Net net;
net.load_param_bin("alexnet.param.bin");
net.load_model("alexnet.bin");

3.3 从内存加载

从内存引用加载网络和模型,没有可见字符串,模型和数据全在代码中,没有任何外部文件:

cpp
#include "alexnet.mem.h"

ncnn::Net net;
net.load_param(alexnet_param_bin);
net.load_model(alexnet_bin);

3.4 从单个文件中加载模型

Net 类有从 FILE* 文件描述加载的接口,可以利用这点把多个网络和模型文件合并为一个:

合并网络模型文件:

bash
cat alexnet.param.bin alexnet.bin > alexnet-all.bin

使用网络模型:

cpp
#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 支持卸载模型以便重用对象来加载其他模型:

cpp
net.clear();

4. 使用模型

NCNN 用自己的数据结构 Mat 来存放输入和输出数据 输入图像的数据要转换为 Mat,依需要减去均值和乘系数:

cpp
#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);

执行前向网络,获得计算结果:

cpp
#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 的名字

cpp
#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,遍历所有获得全部分类的分数:

cpp
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 转换图像的时候可以顺便转换颜色和缩放大小,这些顺带的操作也是有优化的,支持 RGB2GRAYGRAY2RGBRGB2BGR 等常用转换,支持缩小和放大。

cpp
#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 有轻量模式,开启后每次计算一层将上一层的数据释放,以达到节省内存的目的,但是这样就无法获取中间态结果,适合生产模式下使用:

cpp
ex.set_light_mode(true);

5.3 多线程加速

Extractor 有个多线程加速的开关,设置线程数能加快计算:

cpp
ex.set_num_threads(4);