简介
在第一部分,我们介绍了面向深度神经网络的英特尔® 数学核心函数库(英特尔® MKL-DNN),MKL-DNN 是一款面向深度学习应用的开源性能库。提供了在配有英特尔处理器的电脑上安装库组件的具体步骤,要求处理器支持英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)并运行 Ubuntu* 操作系统。第一部分还包括从命令行中创建 C 和 C++ 代码示例的详细信息。
第二部分将介绍如何配置集成开发环境 (IDE),以创建 C++ 代码示例,并提供基于 AlexNet* 深度学习拓扑的代码详解。本教程使用的是安装了 C/C++ 开发工具 (CDT)的 Eclipse Neon* IDE。(如果您的系统没有安装 Eclipse*,可以按照 Ubuntu 手册网站上的说明指定 Oracle Java* 8 和 Eclipse IDE,以便 C/C++ 开发人员选择。)
在 Eclipse IDE 中创建 C++ 示例
本章节介绍了如何在 Eclipse 中创建一个新项目,以及如何导入英特尔 MKL-DNN C++ 示例代码。
在 Eclipse 中创建一个新项目:
- 打开 Eclipse。
- 单击位于屏幕左上角的 New。
- 在 Select a wizard屏幕中选择 C++ Project 并单击 Next(图 1)。
图 1.在 Eclipse 中创建一个新的 C++ 项目。
- 输入项目名称 simple_net。项目类别请选择 Executable, Empty Project。工具链请选择 Linux GCC。单击 Next。
- 在 Select Configurations屏幕中单击 Advanced Settings。
面向项目启用 C++11:
- 在 Properties屏幕中展开菜单树中的 C/C++ Build选项并选择 Settings。
- 在 Tool Settings选项卡中选择 GCC C++ Compiler,然后选择 Miscellaneous。
- 在 Other flags框中已有字符串的后面添加 -std=c++11,用空格隔开(图 2)。
图 2.为项目启用 C++11(二中取一)。
- 在 Properties屏幕上展开 C/C++ General并选择 Preprocessor Include Paths, Macros etc。
- 选择 Providers选项卡,然后选择您正在使用的编译器(如 CDT GCC 内置编译器设置)。
- 找到 Command to get compiler specs:字段并添加
-std=c++11
。完成后,命令与以下部分相似:
“${COMMAND} ${FLAGS} -E -P -v -dD “${INPUTS}” -std=c++11”. - 单击Apply后单击 OK(图 3)。
图 3.为项目启用 C++11(二中取二)。
将库添加至链接器设置:
- 在 Properties屏幕中展开菜单树中的 C/C++ Build选项,然后选择 Settings。
- 在 Tool Settings选项卡中选择 GCC C++ Linker,然后选择 Libraries。
- 在 Libraries (l)板块下单击 Add。
- 输入 mkldnn并单击 OK(图 4)。
图 4.将库添加至链接器设置。
完成项目创建:
- 在 Properties屏幕底部单击 OK。
- 在 C++ Project屏幕底部单击 Finish。
添加 C++ 源文件(注:此时 simple_net项目应该出现在项目资源管理器中):
- 在项目资源管理器中右键单击项目名称,并选择 New, Source Folder。输入文件夹名称 src并单击 Finish。
- 在项目资源管理器中右键单击 src文件夹并选择 Import…
- 在 Import屏幕中展开 General文件夹并突出显示 File System。单击 Next。
- 在 File System屏幕中单击 From directory字段旁的 Browse按钮。转至包含英特尔 MKL-DNN 示例文件的地址,在本例中,地址为 /mkl-dnn/examples。单击屏幕底部的 OK。
- 返回 File System屏幕,查看 simple_net.cpp框并单击 Finish。
创建 Simple_Net 项目:
- 在项目资源管理器中右键单击 simple_net项目名称。
- 单击 Build Project并验证未出现错误。
Simple_Net 代码示例
虽然 Simple_Net 不是功能齐全的深度学习框架,但是它为神经网络拓扑模块的创建提供了基础,将模块所包含的卷积、修正线性单元 (ReLU)、本地响应标准化 (LRN) 和池化集成至单个可执行项目中。虽然此文档逐步概述了英特尔 MKL-DNN C++ API,但是 Simple_Net 代码示例提供了基于 AlexNet 拓扑的更完整的详解。因此,首先向您简要介绍 AlexNet 架构。
AlexNet 架构
“利用深度卷积神经网络进行 ImageNet 分类”文章中这样描述,AlexNet 架构包含一张输入图像 (L0) 和 8 个经过学习的层(L1 到 L8)—5 个卷积层和 3 个完全连接层。图 5 以图形方式描述了该拓扑。
图 5.AlexNet 拓扑(来源:麻省理工学院*)。
表 1 提供了关于 AlexNet 架构的其他详细信息:
层 | 类型 | 描述 |
---|---|---|
L0 | 输入图像 | 尺寸:227 x 227 x 3(在图中显示为 227 x 227 x 3) |
L1 | 卷积 | 尺寸:55* x 55 x 96
*尺寸= (N - F)/S + 1 = (227 - 11)/4 + 1 = 55 |
- | 最大池化 | 尺寸:27* x 27 x 96
*尺寸= (N - F)/S + 1 = (55 – 3)/2 + 1 = 27 |
L2 | 卷积 | 尺寸:27 x 27 x 256
|
- | 最大池化 | 尺寸:13* x 13 x 256
*尺寸= (N - F)/S + 1 = (27 - 3)/2 + 1 = 13 |
L3 | 卷积 | 尺寸:13 x 13 x 384
|
L4 | 卷积 | 尺寸:13 x 13 x 384
|
L5 | 卷积 | 尺寸:13 x 13 x 256
|
- | 最大池化 | 尺寸:6* x 6 x 256
*尺寸 = (N - F)/S + 1 = (13 - 3)/2 + 1 = 6 |
L6 | 完全连接 | 4096 个神经元 |
L7 | 完全连接 | 4096 个神经元 |
L8 | 完全连接 | 1000 个神经元 |
表 1.AlexNet 层描述。
本教程不会对卷积神经网络和 AlexNet 拓扑进行详细描述,但是如果读者想要获取更多的实用信息,可以访问以下链接。
Simple_Net 代码详解
如下所示的源代码在本质上和存储库中的 Simple_Net 示例相同,但是前者进行了重构,能够使用完全合格的英特尔 MKL-DNN 类别来增强可读性。本代码实施了拓扑的第一层 (L1)。
- 将 include 指令添加至库标头文件:
#include "mkldnn.hpp"
- 将 CPU 引擎初始化为索引 0:
auto cpu_engine = mkldnn::engine(mkldnn::engine::cpu, 0);
- 分配数据并创建张量结构:
const uint32_t batch = 256; std::vector<float> net_src(batch * 3 * 227 * 227); std::vector<float> net_dst(batch * 96 * 27 * 27); /* AlexNet: conv * {batch, 3, 227, 227} (x) {96, 3, 11, 11} -> {batch, 96, 55, 55} * strides:{4, 4} */ mkldnn::memory::dims conv_src_tz = {batch, 3, 227, 227}; mkldnn::memory::dims conv_weights_tz = {96, 3, 11, 11}; mkldnn::memory::dims conv_bias_tz = {96}; mkldnn::memory::dims conv_dst_tz = {batch, 96, 55, 55}; mkldnn::memory::dims conv_strides = {4, 4}; auto conv_padding = {0, 0}; std::vector<float> conv_weights(std::accumulate(conv_weights_tz.begin(), conv_weights_tz.end(), 1, std::multiplies<uint32_t>())); std::vector<float> conv_bias(std::accumulate(conv_bias_tz.begin(), conv_bias_tz.end(), 1, std::multiplies<uint32_t>()));
- 面向用户数据创建内存:
auto conv_user_src_memory = mkldnn::memory({{{conv_src_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::nchw}, cpu_engine}, net_src.data()); auto conv_user_weights_memory = mkldnn::memory({{{conv_weights_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::oihw}, cpu_engine}, conv_weights.data()); auto conv_user_bias_memory = mkldnn::memory({{{conv_bias_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::x}, cpu_engine}, conv_bias.data());
- 利用面向卷积数据格式的通配符 any创建面向卷积数据的内存描述符(支持卷积数据基元选择最适合输入参数的数据格式—内核尺寸、步长、填充等):
auto conv_src_md = mkldnn::memory::desc({conv_src_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::any); auto conv_bias_md = mkldnn::memory::desc({conv_bias_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::any); auto conv_weights_md = mkldnn::memory::desc({conv_weights_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::any); auto conv_dst_md = mkldnn::memory::desc({conv_dst_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::any);
- 通过指定算法、传播类型、输入形状、权重、偏差、输出和卷积步长、填充以及填充类型,创建一个卷积描述符:
auto conv_desc = mkldnn::convolution_forward::desc(mkldnn::prop_kind::forward, mkldnn::convolution_direct, conv_src_md, conv_weights_md, conv_bias_md, conv_dst_md, conv_strides, conv_padding, conv_padding, mkldnn::padding_kind::zero);
- 创建一个卷积基元描述符。创建完成后,这个描述符有特定的格式,而非在卷积描述符中-规定的任意通配符格式:
auto conv_prim_desc = mkldnn::convolution_forward::primitive_desc(conv_desc, cpu_engine);
- 创建代表网络的基元矢量:
std::vector<mkldnn::primitive> net;
- 如需要,在用户和数据之间创建重新排序,并在卷积之前添加至网络:
auto conv_src_memory = conv_user_src_memory; if (mkldnn::memory::primitive_desc(conv_prim_desc.src_primitive_desc()) != conv_user_src_memory.get_primitive_desc()) { conv_src_memory = mkldnn::memory(conv_prim_desc.src_primitive_desc()); net.push_back(mkldnn::reorder(conv_user_src_memory, conv_src_memory)); } auto conv_weights_memory = conv_user_weights_memory; if (mkldnn::memory::primitive_desc(conv_prim_desc.weights_primitive_desc()) != conv_user_weights_memory.get_primitive_desc()) { conv_weights_memory = mkldnn::memory(conv_prim_desc.weights_primitive_desc()); net.push_back(mkldnn::reorder(conv_user_weights_memory, conv_weights_memory)); } auto conv_dst_memory = mkldnn::memory(conv_prim_desc.dst_primitive_desc());
- 创建卷积基元并添加至网络:
net.push_back(mkldnn::convolution_forward(conv_prim_desc, conv_src_memory, conv_weights_memory, conv_user_bias_memory, conv_dst_memory));
- 创建 ReLU 基元并添加至网络:
/* AlexNet: relu * {batch, 96, 55, 55} -> {batch, 96, 55, 55} */ const double negative_slope = 1.0; auto relu_dst_memory = mkldnn::memory(conv_prim_desc.dst_primitive_desc()); auto relu_desc = mkldnn::relu_forward::desc(mkldnn::prop_kind::forward, conv_prim_desc.dst_primitive_desc().desc(), negative_slope); auto relu_prim_desc = mkldnn::relu_forward::primitive_desc(relu_desc, cpu_engine); net.push_back(mkldnn::relu_forward(relu_prim_desc, conv_dst_memory, relu_dst_memory));
- 创建一个 AlexNet LRN 基元:
/* AlexNet: lrn * {batch, 96, 55, 55} -> {batch, 96, 55, 55} * local size:5 * alpha:0.0001 * beta:0.75 */ const uint32_t local_size = 5; const double alpha = 0.0001; const double beta = 0.75; auto lrn_dst_memory = mkldnn::memory(relu_dst_memory.get_primitive_desc()); /* create lrn scratch memory from lrn src */ auto lrn_scratch_memory = mkldnn::memory(lrn_dst_memory.get_primitive_desc()); /* create lrn primitive and add it to net */ auto lrn_desc = mkldnn::lrn_forward::desc(mkldnn::prop_kind::forward, mkldnn::lrn_across_channels, conv_prim_desc.dst_primitive_desc().desc(), local_size, alpha, beta); auto lrn_prim_desc = mkldnn::lrn_forward::primitive_desc(lrn_desc, cpu_engine); net.push_back(mkldnn::lrn_forward(lrn_prim_desc, relu_dst_memory, lrn_scratch_memory, lrn_dst_memory));
- 创建一个 AlexNet 池化基元:
/* AlexNet: pool * {batch, 96, 55, 55} -> {batch, 96, 27, 27} * kernel:{3, 3} * strides:{2, 2} */ mkldnn::memory::dims pool_dst_tz = {batch, 96, 27, 27}; mkldnn::memory::dims pool_kernel = {3, 3}; mkldnn::memory::dims pool_strides = {2, 2}; auto pool_padding = {0, 0}; auto pool_user_dst_memory = mkldnn::memory({{{pool_dst_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::nchw}, cpu_engine}, net_dst.data()); auto pool_dst_md = mkldnn::memory::desc({pool_dst_tz}, mkldnn::memory::data_type::f32, mkldnn::memory::format::any); auto pool_desc = mkldnn::pooling_forward::desc(mkldnn::prop_kind::forward, mkldnn::pooling_max, lrn_dst_memory.get_primitive_desc().desc(), pool_dst_md, pool_strides, pool_kernel, pool_padding, pool_padding,mkldnn::padding_kind::zero); auto pool_pd = mkldnn::pooling_forward::primitive_desc(pool_desc, cpu_engine); auto pool_dst_memory = pool_user_dst_memory; if (mkldnn::memory::primitive_desc(pool_pd.dst_primitive_desc()) != pool_user_dst_memory.get_primitive_desc()) { pool_dst_memory = mkldnn::memory(pool_pd.dst_primitive_desc()); }
- 从池化 dst 中创建池化指数内存:
auto pool_indices_memory = mkldnn::memory(pool_dst_memory.get_primitive_desc());
- 创建池化基元并添加至网络:
net.push_back(mkldnn::pooling_forward(pool_pd, lrn_dst_memory, pool_indices_memory, pool_dst_memory));
- 如需要,在内部和用户数据之间创建重新排序,在池化后添加至网络:
if (pool_dst_memory != pool_user_dst_memory) { net.push_back(mkldnn::reorder(pool_dst_memory, pool_user_dst_memory)); }
- 创建流,提交全部基元并等待完成:
mkldnn::stream(mkldnn::stream::kind::eager).submit(net).wait();
- simple_net()函数包含上述代码,在 main中利用异常处理调用该函数:
int main(int argc, char **argv) { try { simple_net(); } catch(mkldnn::error& e) { std::cerr << "status:"<< e.status << std::endl; std::cerr << "message:"<< e.message << std::endl; } return 0; }
结论
本教程系列的第一部分提供了几个资源,您可以利用这些资源了解英特尔 MKL-DNN 的技术预览版。还详细说明了如何安装和构建库组件。本文(教程系列的第二部分)介绍了如何配置 Eclipse 集成开发环境,以创建 C++ 代码示例,还提供了基于 AlexNet 深度学习拓扑的代码详解。敬请关注即将上市的英特尔 MKL-DNN 生产发行版。