闪电般的 R 机器学习算法
使用 Intel® Data Analytics Acceleration Library 和最新的 Intel® Xeon Phi™ 处理器获得成果
点击此处注册并下载免费的 Intel® Parallel Studio XE 30天试用版.
现在,数据科学家可以在最新的 Intel® Xeon Phi™ 处理器 x200 系列(代号为 Knights Landing)上运行 R* 应用程序时获得显著的加速。Intel® 软件工具,如 Intel® Data Analytics Acceleration Library (Intel® DAAL),正在帮助实现这一点——开发者的工作量极少。
最新的 Intel Xeon Phi 处理器是专为苛刻计算应用程序设计的专用平台。它带来了几项新技术突破,包括:
- 插槽式外形(自引导 CPU),以及协处理器版本
- 除了 DDR4 内存之外,还有一个高带宽、封装内置内存,称为多通道 DRAM (MCDRAM)
- 最新、最先进的 向量化 技术:Intel® Advanced Vector Extensions (Intel® AVX-512) 指令集
- 大规模并行处理,每个芯片最多 72 个核心,每个核心 4 个线程
- 3+ TFLOPS 的双精度浮点计算能力
- 6+ TFLOPS 的单精度浮点计算能力
凭借这些技术,Intel 在 Intel® Parallel Studio XE 套件中提供了一套软件工具,帮助开发人员充分利用 Intel Xeon Phi 处理器。其中一个工具是 Intel DAAL,它提供了预优化的机器学习和数据分析算法。现实世界的机器学习问题通常在 CPU 和内存需求方面排名最高。Intel Xeon Phi 是解决这类问题的理想平台。Intel DAAL 提供了一种快速构建针对 Intel Xeon Phi 处理器优化的机器学习应用程序的方法。
R 是一个开源项目和软件环境,它提供了最全面的统计分析、数据挖掘和机器学习算法集1。R 在数据科学从业者中非常受欢迎。然而,没有经过特殊调优,R 及其所有包都无法利用 Intel Xeon Phi 处理器提供的高性能特性。事实上,R 在 Intel Xeon Phi 处理器上的开箱即用性能预计会很差。这是因为在许多情况下,标准的 R 只会在单个 Intel Xeon Phi 处理器核心上使用单个线程。这意味着它只能利用 Intel Xeon Phi 处理器上可用计算资源的 1/288(假设有 72 个核心,每个核心 4 个线程)。单个 Intel Xeon Phi 处理器大致相当于一个 Intel® Atom™ 处理器,其功能不如单个 Intel® Xeon® 处理器。标准的 R 也无法利用其他性能特性,例如基于 Intel AVX-512 指令集的 高级向量化 和高带宽内存 MCDRAM。
但是,R 程序员有一个简单的方法可以利用 Intel Xeon Phi 处理器强大的数据处理能力:将预优化的库集成到 R 环境中。在这里,我们选择了一个经典的机器学习算法,朴素贝叶斯分类器。我们将逐步展示如何使用 Intel DAAL 在 Intel Xeon Phi 处理器自引导系统上构建和运行 R 中的朴素贝叶斯分类器。然后,我们将我们的解决方案与来自经典的 R e1071 包的本地实现进行比较2。
朴素贝叶斯分类器
朴素贝叶斯算法是一种基于贝叶斯定理的分类方法3。它假设所有特征都是相互独立的。尽管其简单,但它通常可以胜过更复杂的分类方法。它已广泛用于文档分类、电子邮件垃圾邮件检测等。
给定一个特征向量 \(X_{i}=(x_{i1},...,x_{ip}),i=1,...,n,\) 其中 \(x_{ik}\) 是第 \(k\) 个特征在第 \(i\) 个观测值中观察到的缩放频率,\(p\) 是特征的数量。此外,给定 \(C=(C_{1},...,C_{d})\) 一组可能的标签,朴素贝叶斯算法基于
即,对于标签 \(C_{k1}\)
后验概率 与(先验概率 × 似然度)成正比
在训练阶段,使用具有已知标签的训练数据集来学习一个模型,该模型包含参数,例如每个类(标签)的先验概率以及所有特征的似然度。然后在预测阶段,对于每个新观测值,算法找到该观测值的最大后验概率,并为其分配相应的标签。
R 中的朴素贝叶斯
R 中一个著名的朴素贝叶斯分类器由 e1071 包提供:统计系、概率论组的杂项函数2。
它具有一个简单的界面,包含两个函数:一个用于训练模型,另一个用于应用模型。
1 library(e1071) 2 3 model <- naiveBayes(training_data, ground_truths) 4 result <- predict(model, new_data)
当我们在 Intel Xeon Phi 处理器上运行此程序时,对于中等大小的数据集(100K 个观测值、200 个特征和 100 个类别),模型训练步骤需要超过 200 秒。而相同大小数据集的预测步骤需要 6,272 秒。这几乎是 1 小时 45 分钟。
这种性能阻止了 e1071 朴素贝叶斯在 Intel Xeon Phi 处理器上的任何实际应用。现在我们将展示如何使用 Intel DAAL 的朴素贝叶斯快速解决此问题。
Intel® DAAL 的朴素贝叶斯
多项式朴素贝叶斯分类器是 Intel DAAL 提供的分类算法之一4。除了卓越的性能外,Intel DAAL 中的实现还提供了一些 e1071 包不具备的功能和灵活性。特别是,Intel DAAL 中的朴素贝叶斯在模型训练中支持三种处理模式:批量处理、在线处理和分布式处理。批量模式与 e1071 包支持的模式相同:整个数据集一次性处理。在线模式支持一种使用模型,即内存无法一次性容纳的大型数据集可以分块处理,并在所有块处理完成后学习模型。分布式模式支持在集群上进行分布式模型训练。为简单起见,此处我们仅使用批量处理模式。但是,此方法也可以应用于将其他两种处理模式集成到 R 中。
在完成下面描述的集成步骤后,我们期望像这样使用我们新的、启用了 Intel DAAL 的朴素贝叶斯分类器:
1 # DAAL Naive Bayes 2 test <- loadData("traindata.csv", nfeatures) 3 model.daal <- nbTrain(test$data, test$labels, nclasses) 4 labels.daal <- nbPredict(model.daal, test$data, nclasses)
将 Intel DAAL 的朴素贝叶斯分类器集成到 R 中
Intel DAAL 具有 C++ 编程接口(除了 Java* 和 Python* API)。要将 Intel DAAL 的朴素贝叶斯算法与 R 一起使用,我们必须将训练和预测步骤封装到 C++ 函数中,然后将这些函数导出到 R。Rcpp 包可用于此目的5,6。
工具设置:Rcpp 和相关包
Rcpp 包是使用 C++ 代码扩展 R 的事实上的标准。数百个其他 R 包使用此包通过 C++ 实现加速计算并连接到其他 C++ 项目。Rcpp 提供无缝的 R 和 C++ 集成。它允许 R 对象在 R 和 C++ 之间直接交换。
Rcpp 可以从 CRAN 安装。我们还需要安装 inline 包7。此包允许直接从 R 代码编译、链接和加载 C++ 代码。在 Rcpp 支持的多种编译、链接和加载 C++ 函数以供 R 使用的方法中,我们发现将 inline 包与 Rcpp 结合使用是在易用性和灵活性之间取得了良好的平衡。最后,我们需要一个 C++ 编译器。通常,Intel® C++ 编译器是构建和优化 Intel Xeon Phi 处理器代码的首选编译器。但在此案例中,我们没有构建大量的 C++ 源代码。我们正在将预编译的二进制文件(来自 Intel DAAL)链接到一个小的动态库中,供 R 加载。因此,任何编译器都可以。如果您系统上没有默认的 C++ 编译器,您可以执行以下操作之一:
- 在 Windows* 上,安装 Rtools
- 在 Mac OS* 上,从 App Store 安装 Xcode
- 在 Linux* 上,运行 sudo apt-get install r-base-dev 或类似命令
连接 R 和 C++ 函数的粘合代码
inline 包提供了一个简单的 cxxfunction 函数,它接受 C++ 函数的签名、C++ 函数的定义和一个插件对象。插件对象用于指定我们项目依赖的其他 C++ 头文件和其他链接行。这通过一个示例(图 1)会更清楚。
图 1 中的第 5-9 行使用 Rcpp 插件生成器工具创建并向 R 注册了一个 Rcpp 插件。该插件允许我们指定对外部库的依赖。在这种情况下,依赖是 Intel DAAL。代码段的其余部分创建了三个 R 函数 `loadData`、`nbTrain` 和 `nbPredict`,分别用于从 CSV 文件读取数据、训练朴素贝叶斯模型和预测新数据。
接下来,我们使用 Intel DAAL 数据结构和算法来实现这些 C++ 函数。
1 library(Rcpp) 2 library(inline) 3 4 # Create and register a Rcpp plugin 5 plug <- Rcpp:::Rcpp.plugin.maker( 6 include.before = "#include <daal.h> ", 7 libs = paste("-L$DAALROOT/lib/ -ldaal_core -ldaal_thread ", 8 "-ltbb -lpthread -lm", sep="")) 9 registerPlugin("daalNB", plug) 10 11 # R function for loading data and labels 12 loadData <- cxxfunction(signature(file="character", ncols="integer"), 13 readCSV, plugin="daalNB") 14 15 # R function for training a model 16 nbTrain <- cxxfunction(signature(X="raw", y="raw", nclasses="integer"), 17 train, plugin="daalNB") 18 19 # R function for scoring 20 nbPredict <- cxxfunction(signature(model="raw", X="raw", nclasses="integer"), 21 predict, plugin="daalNB") 22
从 CSV 文件中读取数据
我们可以使用 `read.csv()`、`read.table()` 或 `scan()` 等 R 函数轻松地从 CSV 文件中读取数据。但是,然后我们必须将数据转换为 Intel DAAL 可识别的表示形式。
Intel DAAL 使用 NumericTables(C++ 类的层次结构)进行内存数据表示。相反,我们希望使用 Intel DAAL 的数据源设施直接从它加载数据并构建 NumericTables。图 2 中的第 13-16 行定义了一个数据源,该数据源连接到包含训练数据和训练数据真实值的 CSV 文件。我们假设 CSV 表的最后一列显示了真实值(标签)。第 19-24 行创建 Intel DAAL NumericTables 来存储要读取的数据和标签。第 27 行将整个数据集加载到 NumericTables 中。在返回到 R 空间之前,我们将数据 NumericTable 和标签 NumericTable 序列化为两块原始字节,然后将它们在一个列表中返回。稍后,C++ 模型训练函数将获取原始字节并将其恢复为 NumericTables。
1 # load data 2 readCSV <- ' 3 using namespace daal; 4 using namespace daal::data_management; 5 6 // Inputs: 7 // file - file name 8 // ncols - number of columns in file 9 std::string fname = Rcpp::as<std::string>(file); 10 int k = Rcpp::as<int>(ncols); 11 12 // Data source 13 FileDataSource<CSVFeatureManager> dataSource( 14 fname, 15 DataSource::notAllocateNumericTable, 16 DataSource::doDictionaryFromContext); 17 18 // DAAL NumericTables for data and labels 19 services::SharedPtr<NumericTable> data( 20 new HomogenNumericTable<double>(k-1, 0, NumericTable::notAllocate)); 21 services::SharedPtr<NumericTable> labels( 22 new HomogenNumericTable<int>(1, 0, NumericTable::notAllocate)); 23 services::SharedPtr<NumericTable> merged( 24 new MergedNumericTable(data, labels)); 25 26 // Load data 27 dataSource.loadDataBlock(merged.get()); 28 29 // Serialize NumericTables 30 InputDataArchive dataArch, labelsArch; 31 data->serialize(dataArch); 32 labels->serialize(labelsArch); 33 Rcpp::RawVector dataBytes(dataArch.getSizeOfArchive()); 34 dataArch.copyArchiveToArray(&dataBytes[0], dataArch.getSizeOfArchive()); 35 Rcpp::RawVector labelsBytes(labelsArch.getSizeOfArchive()); 36 labelsArch.copyArchiveToArray(&labelsBytes[0], labelsArch.getSizeOfArchive()); 37 38 // Return a list of RawVectors 39 return Rcpp::List::create( 40 _["data"] = dataBytes, 41 _["labels"] = labelsBytes); 42 '
训练朴素贝叶斯模型
在使用代码中的任何 Intel DAAL 算法时,编程模型都有一个易于遵循的顺序来组合各种组件:
- 为选定的算法和选定的处理模式创建一个算法对象。
- 使用算法对象的 `input.set` 方法设置输入数据。
- 在算法对象上调用 `compute` 方法。
- 使用算法对象的 `getResult` 方法检索结果。
图 3 中的第 25-28 行展示了在实现 C++ 训练函数时执行此序列。请注意,在此序列之前,我们首先反序列化输入(训练数据和相应的标签),然后将其恢复为 NumericTables。在此序列之后,我们序列化结果——模型对象——为原始字节。这样,模型就可以传递给预测函数,用于对新数据进行分类。
1 # Naive Bayes: train a model 2 train <- ' 3 using namespace daal; 4 using namespace daal::algorithms; 5 using namespace daal::algorithms::multinomial_naive_bayes; 6 using namespace daal::data_management; 7 8 // Inputs: 9 // X - training dataset 10 // y - training data groundtruth 11 // nclasses - number of classes 12 Rcpp::RawVector Xr(X); 13 Rcpp::RawVector yr(y); 14 int nClasses = Rcpp::as<int>(nclasses); 15 16 // Deserialize data and labels 17 OutputDataArchive dataArch(&Xr[0], Xr.length()); 18 services::SharedPtr<NumericTable> ntData(new HomogenNumericTable<double>()); 19 ntData->deserialize(dataArch); 20 OutputDataArchive labelsArch(&yr[0], yr.length()); 21 services::SharedPtr<NumericTable> ntLabels(new HomogenNumericTable<int>()); 22 ntLabels->deserialize(labelsArch); 23 24 // Train a model 25 training::Batch<> algorithm(nClasses); 26 algorithm.input.set(classifier::training::data, ntData); 27 algorithm.input.set(classifier::training::labels, ntLabels); 28 algorithm.compute(); 29 30 // Get result 31 services::SharedPtr<training::Result> result = algorithm.getResult(); 32 InputDataArchive archive; 33 result->get(classifier::training::model)->serialize(archive); 34 35 Rcpp::RawVector out(archive.getSizeOfArchive()); 36 archive.copyArchiveToArray(&out[0], archive.getSizeOfArchive()); 37 return out; 38 '
预测新数据的标签
图 4 展示了 C++ 预测函数。同样,代码的相同序列出现在第 28-31 行。预测结果是一个 n×1 的 NumericTable,其中包含预测的标签,n 是我们预测的观测值数量。代码的最后一部分从结果 NumericTable 中读取标签,并将它们组合成一个 `Rcpp::IntegerVector` 对象。然后,该对象无缝地转换为 R 空间,并成为一个整数数组。
1 # Naive Bayes: predict 2 predict <- ' 3 using namespace daal; 4 using namespace daal::algorithms; 5 using namespace daal::algorithms::multinomial_naive_bayes; 6 using namespace daal::data_management; 7 8 // Inputs: 9 // model - a trained model 10 // X - input data 11 // nclasses - number of classes 12 Rcpp::RawVector modelBytes(model); 13 Rcpp::RawVector dataBytes(X); 14 int nClasses = Rcpp::as<int>(nclasses); 15 16 // Retrieve model 17 OutputDataArchive modelArch(&modelBytes[0], modelBytes.length()); 18 services::SharedPtr<multinomial_naive_bayes::Model> nb( 19 new multinomial_naive_bayes::Model()); 20 nb->deserialize(modelArch); 21 22 // Deserialize data 23 OutputDataArchive dataArch(&dataBytes[0], dataBytes.length()); 24 services::SharedPtr<NumericTable> ntData(new HomogenNumericTable<double>()); 25 ntData->deserialize(dataArch); 26 27 // Predict for new data 28 prediction::Batch<> algorithm(nClasses); 29 algorithm.input.set(classifier::prediction::data, ntData); 30 algorithm.input.set(classifier::prediction::model, nb); 31 algorithm.compute(); 32 33 // Return newlabels 34 services::SharedPtr<NumericTable> predictionResult = 35 algorithm.getResult()->get(classifier::prediction::prediction); 36 BlockDescriptor<int> block; 37 int n = predictionResult->getNumberOfRows(); 38 predictionResult->getBlockOfRows(0, n, readOnly, block); 39 int* newlabels = block.getBlockPtr(); 40 IntegerVector predictedLabels(n); 41 std::copy(newlabels, newlabels+n, predictedLabels.begin()); 42 return predictedLabels; 43 '
整合所有部分
我们可以将图 1-4 中所示的所有代码放入一个脚本中——例如,NaiveBayesClassifierDaal.R。可以使用 R 的 `source()` 函数将其导入 R 环境。然后,新的函数可用作 `loadData()`、`nbTrain()` 和 `nbPredict()`。图 5 展示了如何在 R 中使用这些函数。还显示了如何使用 `microbenchmark()` 来对训练和预测步骤的性能进行基准测试。
每次对脚本进行源化到 R 时,都会自动启动编译和链接过程,以将 C++ 代码构建为 R 扩展。此过程确实需要时间。希望避免此开销的用户应考虑使用 Rcpp 编写专门的 R 包。
专门的 R 包包含预编译的动态库,因此在将其加载到 R 时不需要编译和链接。用于使用 Intel DAAL 的完全相同的 C++ 代码可用于构建 R 包。编写 R 包超出了本次讨论的范围。如果您有兴趣,请参阅官方《编写 R 扩展》手册以获取详细信息8。
1 source("NaiveBayesClassifierDaal.R") 2 3 # DAAL Naive Bayes 4 test <- loadData("traindata.csv", nfeatures) 5 trainperf.daal <- microbenchmark( 6 model.daal <- nbTrain(test$data, test$labels, nclasses)) 7 scoreperf.daal <- microbenchmark( 8 labels.daal <- nbPredict(model.daal, test$data, nclasses))
性能提升
我们的解决方案显著加快了模型训练和预测的速度。如上所述,e1071 朴素贝叶斯需要 200 多秒才能训练完成,1 小时 45 分钟才能预测完成。现在,这两个步骤中的每一个都可以在 0.25 秒内完成。
我们对使用 Intel DAAL 的朴素贝叶斯实现的 R 扩展在不同大小的数据集上进行了基准测试。我们在相同的数据集上运行了 e1071 朴素贝叶斯。不出所料,我们的实现对训练步骤实现了超过 1000 倍的加速,对预测步骤实现了高达 30,000 倍的加速。下面的图表 1 和 **图表 2** 显示了这些数据集的加速数字。
结论
Intel Xeon Phi 处理器是一个高性能、大规模并行平台,非常适合机器学习和数据分析工作负载。然而,数据科学界流行的 R 等主流软件工具可能尚未针对在 Intel Xeon Phi 处理器上有效运行进行优化。在此,我们为希望立即利用 Intel Xeon Phi 处理器优势的 R 程序员介绍了一个解决方案。
该解决方案涉及通过编写使用 Rcpp 的 R 扩展,将 Intel DAAL 函数集成到 R 环境中。Intel DAAL 是一个提供现成算法的数据分析、机器学习和深度学习的库。这些算法已针对 Intel Xeon Phi 处理器以及各种 Intel® Xeon、Core™ 和 Atom™ 处理器进行了优化。我们的基准测试结果表明,此解决方案与 e1071 包提供的原生 R 解决方案相比,具有巨大的性能优势。我们的方法立即为许多 R 应用程序带来了利用 Intel Xeon Phi 处理器优势的可能性。
我们以多项式朴素贝叶斯算法为例来说明这些观点。但同样的也可以应用于将其他 Intel DAAL 算法集成到 R 中。Intel DAAL 提供了一套丰富的算法,包括:线性回归、SVM、朴素贝叶斯、带提升的分类、推荐系统、聚类、深度神经网络等。
查看 Github* 上的 Intel DAAL 开源项目 >
使用的配置和工具
用于基准测试的系统配置
- Intel Xeon Phi 自引导系统
- CPU:Intel Xeon Phi-D B0,68 核 @ 1.40 GHz,34 MB L2 缓存
- 内存:16 GB MCDRAM,96 GB DDR4
- 操作系统版本:Red Hat Enterprise Linux* 7.2 (kernel 3.10.0-327.0.1.el7.x86_64, glibc 2.17-105.el7.x86_64)
此示例中使用的软件工具
- Intel DAAL 2017 Beta 更新 1
- R 版本 3.3.1 (Bug in Your Hair)
- Rcpp 包版本 0.12.5
- Inline 包版本 0.3.14
- e1071 包版本 1.6-7
- g++ 版本 4.8.5
用于性能测试的软件和工作负载可能已针对仅在 Intel 微处理器上运行进行了优化。性能测试,如 SYSmark 和 MobileMark,使用特定的计算机系统、组件、软件、操作和功能进行测量。更改任何这些因素都可能导致结果有所不同。您应参考其他信息和性能测试,以帮助您全面评估您打算购买的产品,包括该产品与其他产品结合时的性能。Intel 技术的功能和优势取决于系统配置,并可能需要启用硬件、软件或服务激活。性能因系统配置而异。请咨询您的系统制造商或零售商,或访问 intel.com 了解更多信息。本文档不授予任何知识产权的明示或暗示(包括禁止反悔或以其他方式)许可。Intel 否认所有明示和暗示的保证,包括但不限于适销性、特定用途的适用性和非侵权性的暗示保证,以及任何由履约过程、交易过程或商业惯例产生的保证。本文档包含有关正在开发的产品、服务和/或流程的信息。此处提供的所有信息如有更改,恕不另行通知。请联系您的 Intel 代表以获取最新的预测、计划、规格和路线图。描述的产品和服务可能存在缺陷或错误,称为勘误,这可能导致与已发布规格的偏差。当前已勘误的勘误可应要求提供。可通过致电 1-800-548-4725 或访问 www.intel.com/design/literature.htm 获取本文档中引用的具有订购号的文档副本。
此示例源代码根据 Intel 示例源代码许可协议 发布。
参考文献
- R* 项目统计计算,r-project.org/。
- e1071:统计系、概率论组的杂项函数,David Meyer,cran.rproject.org/web/packages/e1071/index.html。
- 电子统计教科书,StatSoft, Inc.,塔尔萨,OK,2013。
- Intel® Data Analytics Acceleration Library 的开发者指南和参考,2016。 http://software.intel.com/sites/products/documentation/doclib/daal/daal-user-and-reference-guides/index.htm
- Rcpp:无缝的 R* 和 C++ 集成,Dirk Eddelbuettel,cran.r-project.org/web/packages/Rcpp/index.html。
- 使用 Rcpp 进行无缝的 R* 和 C++ 集成,D. Eddelbuettel,Springer,2013。
- inline:从 R* 中内联 C、C++、Fortran 函数调用的函数,Oleg Sklyar,cran.r-project.org/web/packages/inline/index.html。
- 编写 R* 扩展,cran.r-project.org/doc/manuals/r-release/R-exts.html。