一年的打磨,MNN正式版发布!
▐ 模型构建
Express
(表达式)接口来构建模型,如下例所示,接口还是比较简洁明了的。模型的构建、训练和保存具体可以参考说明文档【7】。VARP x = inputs[0];
x = conv1->forward(x);
x = _MaxPool(x, {2, 2}, {2, 2});
x = conv2->forward(x);
x = _MaxPool(x, {2, 2}, {2, 2});
x = _Convert(x, NCHW);
x = _Reshape(x, {0, -1});
x = ip1->forward(x);
x = _Relu(x);
x = dropout->forward(x);
x = ip2->forward(x);
x = _Softmax(x, 1);
return {x};
▐ 量化训练
MobileNet V2
为例说明:▐ 迁移学习示例
class MobilenetV2TransferModule : public Module {
public:
MobilenetV2TransferModule(const char* fileName) {
// 读取原始MobilenetV2模型
auto varMap = Variable::loadMap(fileName);
// MobilenetV2的输入节点
auto input = Variable::getInputAndOutput(varMap).first.begin()->second;
// MobilenetV2分类层之前的节点,AveragePooling的输出
auto lastVar = varMap["MobilenetV2/Logits/AvgPool"];
// 初始化一个4分类的全连接层,MNN中可以用卷积来表示全连接层
NN::ConvOption option;
option.channel = {1280, 4};
mLastConv = std::shared_ptr<Module>(NN::Conv(option));
// 初始化内部特征提取器, 内部提取器设成不需要训练
mFix.reset(PipelineModule::extract({input}, {lastVar}, false));
// 注意这里只注册了我们新初始化的4分类全连接层,那么训练时将只更新此4分类全连接层
registerModel({mLastConv});
}
virtual std::vector<VARP> onForward(const std::vector<VARP>& inputs) override {
// 输入一张图片,获得MobilenetV2特征提取器的输出
auto pool = mFix->forward(inputs[0]);
// 将上面提取的特征输入到新初始化的4分类层进行分类
auto result = _Softmax(_Reshape(_Convert(mLastConv->forward(pool), NCHW), {0, -1}));
return {result};
}
// MobilenetV2特征提取器,从输入一直到最后一个AveragePooling
std::shared_ptr<Module> mFix;
// 重新初始化的4分类全连接层
std::shared_ptr<Module> mLastConv;
};
int main(int argc, const char* argv[]) {
std::string trainImagesFolder = argv[2];
std::string trainImagesTxt = argv[3];
std::string testImagesFolder = argv[4];
std::string testImagesTxt = argv[5];
// 读取模型,并替换最后一层分类层
std::shared_ptr<Module> model(new MobilenetV2TransferModule(argv[1])); // arg1: /path/to/mobilenetV2Model
// 进入训练环节
MobilenetV2Utils::train(model, 4, 0, trainImagesFolder, trainImagesTxt, testImagesFolder, testImagesTxt);
return 0;
}
▐ x86
▐ ARM64
▐ ARMv8.2
FP16 extensions
和Dot Product
可以分别应用于浮点计算加速和量化计算加速。FP16extensions
asimdhp
(Advanced SIMD Half Precision),是 ARMv8.2 架构的可选扩展。asimdhp
可用时,可以使用相关 SIMD 指令实现float16
的读写计算。float16
将float32
所需的位数降低了一半,因此在 SIMD 下,可以实现两倍的并发吞吐,从而优化性能。为此,我们在卷积中,采用[N,C/8,H,W,8]
的数据布局,新增了部分卷积实现,效果如下:--fp16
输出选项,模型大小还能减小一半。一箭双雕。asimddp
(Advanced SIMD Dot Product),是 ARMv8.2 架构的可选扩展。asimddp
可用时,可以使用 SDOT/UDOT 指令实现 int8
/uint8
的点积计算。SDOT/UDOT 指令如上图所示,一次可以处理两个 4x4 int8
/uint8
数据乘,并累加到 4x1 的 int32
/uint32
的寄存器上。这样强大的硬件加速指令,还是双发射的。Express
(表达式)、模型训练的封装,累计新增超过 150 个接口。具体可以参考说明文档【11】。Express
构图,使用 Python 改写的版本如下:class Net(nn.Module):
"""construct a lenet 5 model"""
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.conv(1, 20, [5, 5])
self.conv2 = nn.conv(20, 50, [5, 5])
self.fc1 = nn.linear(800, 500)
self.fc2 = nn.linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool(x, [2, 2], [2, 2])
x = F.relu(self.conv2(x))
x = F.max_pool(x, [2, 2], [2, 2])
x = F.convert(x, F.NCHW)
x = F.reshape(x, [0, -1])
x = F.relu(self.fc1(x))
x = self.fc2(x)
x = F.softmax(x, 1)
return x
CPU 上,移动设备方面,ARMv8.2 将是新手机的主流,上文所展示 2 倍加速比非常诱人,我们会进一步挖掘 ARMv8.2 的优化空间;其他平台方面, x86 的性能在单机训练、服务端推理的场景中举足轻重,会是性能优化的另一个目标。
GPU 上,我们会聚焦 Vulkan—— Android 下一代 GPGPU API 的事实标准。
Express
(表达式)接口支持常用模型的训练、量化、蒸馏,我们会进一步完善训练能力,添加更多算子和求导的支持,以支持更多的模型。链接:
1、https://github.com/alibaba/MNN
2、https://proceedings.mlsys.org/static/paper_files/mlsys/2020/7-Paper.pdf
3、https://github.com/alibaba/MNNKit
4、http://mudu.tv/watch/4308076
5、http://mudu.tv/watch/4397479
6、http://mudu.tv/watch/4844443
7、https://www.yuque.com/mnn/cn/kgd9hd
8、https://www.yuque.com/mnn/cn/bhz5eu
9、https://www.yuque.com/mnn/cn/vniw5p
10、https://community.arm.com/developer/tools-software/oss-platforms/b/android-blog/posts/bringing-armv8-2-instructions-to-android-runtime
11、https://www.yuque.com/mnn/cn/usage_in_python
12、https://mp.weixin.qq.com/s/ZmKC4fHTNPUj5iGqag19HA