科技公司網(wǎng)站主頁設(shè)計(jì)網(wǎng)絡(luò)營銷網(wǎng)站平臺(tái)有哪些
一、說明
????????提及機(jī)器學(xué)習(xí)框架與研究和工業(yè)的相關(guān)性。現(xiàn)在很少有項(xiàng)目不使用Google TensorFlow或Meta PyTorch,在于它們的可擴(kuò)展性和靈活性。也就是說,花時(shí)間從頭開始編碼機(jī)器學(xué)習(xí)算法似乎違反直覺,即沒有任何基本框架。然而,事實(shí)并非如此。自己對(duì)算法進(jìn)行編碼可以清晰而扎實(shí)地理解算法的工作原理以及模型真正在做什么。
? ? ? 在本系列中,我們將學(xué)習(xí)如何僅使用普通和現(xiàn)代C++編寫必須知道的深度學(xué)習(xí)算法,例如卷積、反向傳播、激活函數(shù)、優(yōu)化器、深度神經(jīng)網(wǎng)絡(luò)等。
????????我們將通過學(xué)習(xí)一些現(xiàn)代 C++ 語言功能和相關(guān)編程細(xì)節(jié)來編碼深度學(xué)習(xí)和機(jī)器學(xué)習(xí)模型,開始我們的故事之旅。
????????查看其他故事:
1 — Coding 2D convolutions in C++
2 — Cost Functions using Lambdas
3 — Implementing Gradient Descent
4 — Activation Functions
...更多內(nèi)容即將推出。
我無法創(chuàng)造的,我不明白?!?理查德·費(fèi)曼
二、新式C++、?和?標(biāo)頭<algorithm>
<numeric>
????????C++曾經(jīng)是一種古老的語言,在過去十年中發(fā)生了翻天覆地的變化。主要變化之一是對(duì)函數(shù)式編程的支持。但是,還引入了其他幾項(xiàng)改進(jìn),幫助我們開發(fā)更好、更快、更安全的機(jī)器學(xué)習(xí)代碼。
????????為了我們?cè)谶@里的任務(wù),C++ 和 標(biāo)頭中包含一組方便的通用例程。作為一個(gè)說明性的例子,我們可以通過以下方式獲得兩個(gè)向量的內(nèi)積:<numeric>
<algorithm>
#include <numeric>
#include <iostream>int main()
{std::vector<double> X {1., 2., 3., 4., 5., 6.};std::vector<double> Y {1., 1., 0., 1., 0., 1.};auto result = std::inner_product(X.begin(), X.end(), Y.begin(), 0.0);std::cout << "Inner product of X and Y is " << result << '\n';return 0;
}
并使用如下函數(shù):accumulate
reduce
std::vector<double> V {1., 2., 3., 4., 5.};double sum = std::accumulate(V.begin(), V.end(), 0.0);std::cout << "Summation of V is " << sum << '\n';double product = std::accumulate(V.begin(), V.end(), 1.0, std::multiplies<double>());std::cout << "Productory of V is " << product << '\n';double reduction = std::reduce(V.begin(), V.end(), 1.0, std::multiplies<double>());std::cout << "Reduction of V is " << reduction << '\n';
標(biāo)頭是大量有用的例程,例如,, , , ,等。讓我們看一個(gè)說明性的例子:algorithm
std::transform
std::for_each
std::count
std::unique
std::sort
#include <algorithm>
#include <iostream>double square(double x) {return x * x;}int main()
{std::vector<double> X {1., 2., 3., 4., 5., 6.};std::vector<double> Y(X.size(), 0);std::transform(X.begin(), X.end(), Y.begin(), square);std::for_each(Y.begin(), Y.end(), [](double y){std::cout << y << " ";});std::cout << "\n";return 0;
}
事實(shí)證明,在現(xiàn)代C++中,我們可以使用 、、 等函數(shù),將函子、lambda 甚至香草函數(shù)作為參數(shù)傳遞,而不是顯式使用 or 循環(huán)。for
while
std::transform
std::for_each
std::generate_n
上面的示例可以在?GitHub 上的此存儲(chǔ)庫中找到。
順便說一下,是一個(gè)lambda?,F(xiàn)在讓我們談?wù)労瘮?shù)式編程和lambda。[](double v){...}
三、函數(shù)式編程
????????C++是一種多范式編程語言,這意味著我們可以使用它來創(chuàng)建使用不同“樣式”的程序,例如OOP,過程式和最近的功能。
????????對(duì)函數(shù)式編程的C++支持始于標(biāo)頭:<functional>
#include <algorithm> // std::for_each
#include <functional> // std::less, std::less_equal, std::greater, std::greater_equal
#include <iostream> // std::coutint main()
{std::vector<std::function<bool(double, double)>> comparators {std::less<double>(), std::less_equal<double>(), std::greater<double>(), std::greater_equal<double>()};double x = 10.;double y = 10.;auto compare = [&x, &y](const std::function<bool(double, double)> &comparator){bool b = comparator(x, y);std::cout << (b?"TRUE": "FALSE") << "\n";};std::for_each(comparators.begin(), comparators.end(), compare);return 0;
}
在這里,我們使用、、、和作為多態(tài)調(diào)用的示例,而不使用指針。std::function
std::less
std::less_equal
std::greater
std::greater_equal
正如我們已經(jīng)討論過的,C++ 11 包括語言核心的更改以支持函數(shù)式編程。到目前為止,我們已經(jīng)看到了其中之一:
auto compare = [&x, &y](const std::function<bool(double, double)> &comparator)
{bool b = comparator(x, y);std::cout << (b?"TRUE": "FALSE") << "\n";
};
此代碼定義一個(gè) lambda,一個(gè) lambda 定義一個(gè)函數(shù)對(duì)象,即可調(diào)用對(duì)象。
請(qǐng)注意?,這不是 lambda 名稱,而是 lambda 分配到的變量的名稱。事實(shí)上,lambda 是匿名對(duì)象。
compare
此 lambda 由 3 個(gè)子句組成:捕獲列表 ( )、參數(shù)列表 () 和正文(大括號(hào)之間的代碼)。[&x, &y]
const std::function<boll(double, double)> &comparator
{...}
參數(shù)列表和 body 子句的工作方式與任何常規(guī)函數(shù)類似。捕獲子句指定可在 lambda 主體中尋址的外部變量集。
Lambda 非常有用。我們可以像舊式函子一樣聲明和傳遞它們。例如,我們可以定義一個(gè)?L2 正則化?lambda:
auto L2 = [](const std::vector<double> &V)
{double p = 0.01;return std::inner_product(V.begin(), V.end(), V.begin(), 0.0) * p;
};
并將其作為參數(shù)傳遞回我們的層:
auto layer = new Layer::Dense();
layer.set_regularization(L2)
默認(rèn)情況下,lambda 不會(huì)引起副作用,即它們不能更改外部?jī)?nèi)存空間中對(duì)象的狀態(tài)。但是,如果需要,我們可以定義一個(gè) lambda??紤]以下動(dòng)量實(shí)現(xiàn):mutable
#include <algorithm>
#include <iostream>using vector = std::vector<double>;int main()
{auto momentum_optimizer = [V = vector()](const vector &gradient) mutable {if (V.empty()) V.resize(gradient.size(), 0.);std::transform(V.begin(), V.end(), gradient.begin(), V.begin(), [](double v, double dx) {double beta = 0.7;return v = beta * v + dx; });return V;};auto print = [](double d) { std::cout << d << " "; };const vector current_grads {1., 0., 1., 1., 0., 1.};for (int i = 0; i < 3; ++i) {vector weight_update = momentum_optimizer(current_grads);std::for_each(weight_update.begin(), weight_update.end(), print);std::cout << "\n";}return 0;
}
????????每次調(diào)用都會(huì)產(chǎn)生不同的值,即使我們傳遞的值與參數(shù)相同。發(fā)生這種情況是因?yàn)槲覀兪褂藐P(guān)鍵字 .momentum_optimizer(current_grads)
mutable
????????對(duì)于我們現(xiàn)在的目的,函數(shù)式編程范式特別有價(jià)值。通過使用功能特性,我們將編寫更少但更健壯的代碼,更快地執(zhí)行更復(fù)雜的任務(wù)。
四、矩陣和線性代數(shù)庫
????????好吧,當(dāng)我說“純C++”時(shí),這并不完全正確。我們將使用可靠的線性代數(shù)庫來實(shí)現(xiàn)我們的算法。
????????矩陣和張量是機(jī)器學(xué)習(xí)算法的構(gòu)建塊。C++中沒有內(nèi)置矩陣實(shí)現(xiàn)(也不應(yīng)該有)。幸運(yùn)的是,有幾個(gè)成熟且優(yōu)秀的線性代數(shù)庫可用,例如?Eigen?和?Armadillo。
????????多年來,我一直在使用Eigen。Eigen(在Mozilla公共許可證2.0下)是僅標(biāo)頭的,不依賴于任何第三方庫。因此,我將使用本征作為這個(gè)故事及以后的線性代數(shù)后端。
五、常見矩陣運(yùn)算
最重要的矩陣運(yùn)算是逐矩陣乘法:
#include <iostream>
#include <Eigen/Dense>int main(int, char **)
{Eigen::MatrixXd A(2, 2);A(0, 0) = 2.;A(1, 0) = -2.;A(0, 1) = 3.;A(1, 1) = 1.;Eigen::MatrixXd B(2, 3);B(0, 0) = 1.;B(1, 0) = 1.;B(0, 1) = 2.;B(1, 1) = 2.;B(0, 2) = -1.;B(1, 2) = 1.;auto C = A * B;std::cout << "A:\n" << A << std::endl;std::cout << "B:\n" << B << std::endl;std::cout << "C:\n" << C << std::endl;return 0;
}
????????通常稱為 ,此操作的計(jì)算復(fù)雜度為?O(N3)。由于廣泛用于機(jī)器學(xué)習(xí),我們的算法受到矩陣大小的強(qiáng)烈影響。mulmat
mulmat
讓我們談?wù)劻硪环N類型的逐矩陣乘法。有時(shí),我們只需要系數(shù)矩陣乘法:
auto D = B.cwiseProduct(C);
std::cout << "coefficient-wise multiplication is:\n" << D << std::endl;
當(dāng)然,在系數(shù)乘法中,參數(shù)的維度必須匹配。以同樣的方式,我們可以添加或減去矩陣:
auto E = B + C;
std::cout << "The sum of B & C is:\n" << E << std::endl;
最后,讓我們討論三個(gè)非常重要的矩陣運(yùn)算:、 和 :transpose
inverse
determinant
std::cout << "The transpose of B is:\n" << B.transpose() << std::endl;
std::cout << "The A inverse is:\n" << A.inverse() << std::endl;
std::cout << "The determinant of A is:\n" << A.determinant() << std::endl;
逆向、轉(zhuǎn)置和行列式是實(shí)現(xiàn)我們的模型的基礎(chǔ)。另一個(gè)關(guān)鍵點(diǎn)是將函數(shù)應(yīng)用于矩陣的每個(gè)元素:
auto my_func = [](double x){return x * x;};
std::cout << A.unaryExpr(my_func) << std::endl;
上面的例子可以在這里找到。
六、關(guān)于矢量化的一句話
????????現(xiàn)代編譯器和計(jì)算機(jī)體系結(jié)構(gòu)提供了稱為矢量化的增強(qiáng)功能。簡(jiǎn)而言之,矢量化允許使用多個(gè)寄存器并行執(zhí)行獨(dú)立的算術(shù)運(yùn)算。例如,以下 for 循環(huán):
for (int i = 0; i < 1024; i++)
{A[i] = A[i] + B[i];
}
????????以靜默方式替換為矢量化版本:
for(i=0; i < 512; i += 2)
{ A[i] =A[i] + B[i];
A[i + 1] = A[i + 1] + B[i + 1 ];
}
????????由編譯器。訣竅是指令與指令同時(shí)運(yùn)行。這是可能的,因?yàn)閮蓷l指令彼此獨(dú)立,并且底層硬件具有重復(fù)的資源,即兩個(gè)執(zhí)行單元。A[i + 1] = A[i + 1] + B[i + 1]
A[i] = A[i] + B[i]
????????如果硬件有四個(gè)執(zhí)行單元,編譯器將按以下方式展開循環(huán):
for(i=0; i < 256; i += 4)
{ A[i] =A[i] + B[i] ;
A[i + 1] = A[i + 1] + B[i + 1]; A[i + 2] = A[i + 2] + B[i + 2]; A[i + 3] = A[i + 3] + B[i + 3];
}
????????與原始版本相比,此矢量化版本使程序運(yùn)行速度提高了 4 倍。值得注意的是,這種性能提升不會(huì)影響原始程序的行為。
????????盡管矢量化是由編譯器、操作系統(tǒng)和硬件在木頭下執(zhí)行的,但我們?cè)诰幋a時(shí)必須注意允許矢量化:
- 啟用編譯程序所需的矢量化標(biāo)志
- 在循環(huán)開始之前,必須知道循環(huán)邊界,動(dòng)態(tài)或靜態(tài)
- 循環(huán)體指令不應(yīng)引用以前的狀態(tài)。例如,諸如此類的事情可能會(huì)阻止矢量化,因?yàn)樵谀承┣闆r下,編譯器無法安全地確定在當(dāng)前指令調(diào)用期間是否有效。
A[i] = A[i — 1] + B[i]
A[i-1]
- 循環(huán)體應(yīng)由簡(jiǎn)單和直線代碼組成。 還允許函數(shù)調(diào)用和先前矢量化的函數(shù)。但復(fù)雜的邏輯、子例程、嵌套循環(huán)和函數(shù)調(diào)用通常會(huì)阻止矢量化工作。
inline
在某些情況下,遵循這些規(guī)則并不容易??紤]到復(fù)雜性和代碼大小,有時(shí)很難說編譯器何時(shí)對(duì)代碼的特定部分進(jìn)行了矢量化處理。
根據(jù)經(jīng)驗(yàn),代碼越精簡(jiǎn)和直接,就越容易被矢量化。因此,使用 、、 和 STL 容器的標(biāo)準(zhǔn)功能表示更有可能被矢量化的代碼。<numeric>
algorithm
functional
七、機(jī)器學(xué)習(xí)中的矢量化
????????矢量化在機(jī)器學(xué)習(xí)中起著重要作用。例如,批次通常以矢量化方式處理,使具有大批次的火車比使用小批次(或不批處理)的火車運(yùn)行得更快。
????????由于我們的矩陣代數(shù)庫詳盡地使用了矢量化,因此我們通常將行數(shù)據(jù)聚合成批次,以便更快地執(zhí)行操作。請(qǐng)考慮以下示例:

????????與其在六個(gè)向量和一個(gè)向量中的每一個(gè)之間執(zhí)行 6 個(gè)內(nèi)積以獲得 6 個(gè)輸出 , 等等,我們可以堆疊輸入向量以掛載一個(gè)包含六行的矩陣并使用單個(gè)乘法運(yùn)行一次。Xi
V
Y0
Y1
M
mulmat
Y = M*V
????????輸出是一個(gè)向量。我們最終可以解綁它的元素以獲得所需的 6 個(gè)輸出值。Y
八、結(jié)論和下一步
????????這是一個(gè)關(guān)于如何使用現(xiàn)代C++編寫深度學(xué)習(xí)算法的介紹性演講。我們涵蓋了高性能機(jī)器學(xué)習(xí)程序開發(fā)中非常重要的方面,例如函數(shù)式編程、代數(shù)演算和矢量化。
????????這里沒有涉及現(xiàn)實(shí)世界 ML 項(xiàng)目的一些相關(guān)編程主題,例如 GPU 編程或分布式訓(xùn)練。我們將在以后的故事中討論這些主題。
在下一個(gè)故事中,我們將學(xué)習(xí)如何編寫2D卷積代碼,這是深度學(xué)習(xí)中最基本的操作。
九、引用
C++參考資料
特征線性代數(shù)庫
C++中的 Lambda 表達(dá)式
英特爾矢量化要點(diǎn)