Intel® MKL-DNN:第二部分 – 示例代码构建和实践
在第 2 部分中,我们将探讨如何配置集成开发环境 (IDE) 来构建 C++ 代码示例,并基于 AlexNet 深度学习拓扑提供代码演练。
引言
在 第 1 部分 中,我们介绍了 Intel® Math Kernel Library for Deep Neural Networks (Intel® MKL-DNN),这是一个用于深度学习应用程序的开源性能库。我们提供了在支持 Intel® Advanced Vector Extensions 2 (Intel® AVX2) 的 Intel® 处理器上运行 Ubuntu* 操作系统的计算机上安装库组件的详细步骤。第 1 部分还涵盖了如何从命令行构建 C 和 C++ 代码示例的详细信息。
在第 2 部分中,我们将探讨如何配置集成开发环境 (IDE) 来构建 C++ 代码示例,并基于 AlexNet* 深度学习拓扑提供代码演练。在本教程中,我们将使用 Eclipse Neon* IDE 和 C/C++ Development Tools (CDT)。(如果您的系统尚未安装 Eclipse*,可以按照 Ubuntu Handbook 网站上的说明进行操作,并选择 Oracle Java* 8 和 Eclipse IDE for C/C++ Developers 选项。)
在 Eclipse IDE 中构建 C++ 示例
本节介绍如何在 Eclipse 中创建新项目并导入 Intel MKL-DNN C++ 示例代码。
在 Eclipse 中创建新项目
- 启动 Eclipse。
- 单击屏幕左上角的 **New**。
- 在“Select a wizard”屏幕中,选择 **C++ Project**,然后单击 **Next**(图 1)。
- 输入 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)。
- 在“Properties”屏幕中,展开 **C/C++ General**,然后选择 **Preprocessor Include Paths, Macros etc.**。
- 选择 **Providers** 选项卡,然后选择您正在使用的编译器(例如,CDT GCC Built-in Compiler Settings)。
- 找到名为“Command to get compiler specs:”的字段,并添加 **
-std=c++11
**。完成后,该命令应与以下内容类似:
"${COMMAND} ${FLAGS} -E -P -v -dD "${INPUTS}" -std=c++11"。 - 单击 **Apply**,然后单击 **OK**(图 3)。
向链接器设置添加库
- 在“Properties”屏幕中,展开菜单树中的 **C/C++ Build** 选项,然后选择 **Settings**。
- 在“Tool Settings”选项卡中,选择 **GCC C++ Linker**,然后选择 **Libraries**。
- 在“Libraries (l)”部分下单击 **Add**。
- 输入 **mkldnn**,然后单击 **OK**(图 4)。
完成项目创建
- 在“Properties”屏幕底部单击 **OK**。
- 在“C++ Project”屏幕底部单击 **Finish**。
添加 C++ 源文件(注意:此时“simple_net”项目应出现在 Project Explorer 中)
- 在 Project Explorer 中右键单击项目名称,选择 **New**、**Source Folder**。输入 src 作为文件夹名称,然后单击 **Finish**。
- 在 Project Explorer 中右键单击“src”文件夹,选择 **Import…**
- 在“Import”屏幕中,展开 **General** 文件夹,然后高亮显示 **File System**。单击 **Next**。
- 在“File System”屏幕中,单击“From directory”字段旁边的 **Browse** 按钮。导航到包含 Intel MKL-DNN 示例文件的位置,在本例中为 /mkl-dnn/examples。单击屏幕底部的 **OK**。
- 回到“File System”屏幕,选中 **simple_net.cpp** 复选框,然后单击 **Finish**。
构建 Simple_Net 项目
- 在 Project Explorer 中右键单击项目名称 **simple_net**。
- 单击 **Build Project**,并验证是否出现任何错误。
Simple_Net 代码示例
虽然 Simple_Net 不是一个功能齐全的深度学习框架,但它提供了构建包含卷积、ReLU(整流线性单元)、LRN(局部响应归一化)和池化操作的神经网络拓扑块的基本功能,所有这些都在一个可执行项目中。Intel MKL-DNN C++ API 的简要分步说明在 文档 中提供;然而,Simple_Net 代码示例基于 AlexNet 拓扑提供了更完整的演练。因此,我们将首先简要概述 AlexNet 架构。
AlexNet 架构
正如论文 ImageNet Classification with Deep Convolutional Neural Networks 中所述,AlexNet 架构包含一个输入图像 (L0) 和八个学习层(L1 至 L8)—五个卷积层和三个全连接层。此拓扑在图 5 中以图形方式显示。
表 1 提供了 AlexNet 架构的更多详细信息
Layer |
类型 |
描述 |
---|---|---|
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 个神经元 |
卷积神经网络和 AlexNet 拓扑的详细描述超出了本教程的范围,但如果需要更多信息,读者可以参考以下链接。
Simple_Net 代码演练
下面提供的源代码本质上与存储库中包含的 Simple_Net 示例相同,只是进行了重构以使用完全限定的 Intel MKL-DNN 类型来增强可读性。此代码实现了拓扑的第一层(L1)。
- 添加库头文件的包含指令
#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()); }
- 从池化目标创建池化索引内存
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; }
结论
本教程系列的第 1 部分 确定了学习 Intel MKL-DNN 技术预览版的几个资源。还提供了安装和构建库组件的详细说明。在本篇(本教程系列的第 2 部分)中,提供了关于如何配置 Eclipse 集成开发环境以构建 C++ 代码示例的信息,以及基于 AlexNet 深度学习拓扑的代码演练。敬请关注 Intel MKL-DNN 即将发布的生产版本。