前言
在這個科技發展日新月異的時代,行業的寵兒與棄兒就如同手掌的兩面,只需輕輕一翻,從業者的境遇便會有天翻地覆的改變。
人工智慧作為近兩年來業界公認的熱門領域,不同於之前火熱的移動端開發或前端開發,其距離傳統軟體開發行業之遠,入門門檻之高,都是以往不曾出現過的,這也讓許多希望能夠終身學習,持續關注行業發展的軟體工程師們望而卻步。
在我們進一步討論阻止傳統軟體工程師向人工智慧領域轉型的障礙之前,讓我們先來明確幾個名詞的定義:
- 人工智慧:以機器為載體展現出的人類智慧,如影象識別等傳統計算機無法完成的工作
- 機器學習:一種實現人工智慧的方式
- 深度學習:一種實現機器學習的技術
那麼到底是哪些障礙阻止了傳統軟體工程師進入人工智慧領域呢?
- 數學:不同於後端,前端,移動端等不同領域之間的區別,人工智慧,或者說我們接下來將要重點討論的深度學習,是一門以數學為基礎的科學。學習它的前置條件,不再是搭建某一個開發環境,瞭解某一門框架,而是需要去理解一些諸如矩陣,反向傳播,梯度下降等數學概念。
- 生態:很久以來,學術界都以 Python 作為其研究的預設語言,創造瞭如 NumPy,Matplotlib 等一系列優秀的科學計算工具。而對於終日與使用者介面打交道的 Web 開發者來說,一系列基礎工具的缺乏直接導致了哪怕是建立起來一個最基礎的深度學習模型都異常困難。
為了解決上面提到的這兩個障礙,筆者使用 TypeScript 以零依賴的方式初步完成了一個基於 JavaScript 的深度學習框架:deeplearning-js,希望可以以 Web 開發者熟悉的語言與生態為各位提供一種門檻更低的深度學習的入門方式,並將以寫給 Web 開發者的深度學習教程這一系列文章,幫助各位理解深度學習的基本思路以及其中涉及到的數學概念。
整體架構
src/
├── activationFunction // 啟用函式
│ ├── index.ts
│ ├── linear.spec.ts
│ ├── linear.ts
│ ├── linearBackward.spec.ts
│ ├── linearBackward.ts
│ ├── relu.spec.ts
│ ├── relu.ts
│ ├── reluBackward.spec.ts
│ ├── reluBackward.ts
│ ├── sigmoid.spec.ts
│ ├── sigmoid.ts
│ ├── sigmoidBackward.spec.ts
│ ├── sigmoidBackward.ts
│ ├── softmax.spec.ts
│ ├── softmax.ts
│ ├── softmaxBackward.spec.ts
│ └── softmaxBackward.ts
├── costFunction // 損失函式
│ ├── crossEntropyCost.spec.ts
│ ├── crossEntropyCost.ts
│ ├── crossEntropyCostBackward.spec.ts
│ ├── crossEntropyCostBackward.ts
│ ├── index.ts
│ ├── quadraticCost.spec.ts
│ ├── quadraticCost.ts
│ ├── quadraticCostBackward.spec.ts
│ └── quadraticCostBackward.ts
├── data // 資料結構:矩陣 & 標量
│ ├── Array2D.spec.ts
│ ├── Array2D.ts
│ ├── Scalar.spec.ts
│ ├── Scalar.ts
│ └── index.ts
├── index.ts
├── math // 計算:矩陣計算函式 & 生成隨機數矩陣 & 生成零矩陣
│ ├── add.spec.ts
│ ├── add.ts
│ ├── divide.spec.ts
│ ├── divide.ts
│ ├── dot.spec.ts
│ ├── dot.ts
│ ├── index.ts
│ ├── multiply.spec.ts
│ ├── multiply.ts
│ ├── randn.spec.ts
│ ├── randn.ts
│ ├── subtract.spec.ts
│ ├── subtract.ts
│ ├── transpose.spec.ts
│ ├── transpose.ts
│ ├── zeros.spec.ts
│ └── zeros.ts
├── model // 模型:初始化引數 & 正向傳播 & 反向傳播 & 更新引數
│ ├── Cache.ts
│ ├── backPropagation.ts
│ ├── forwardPropagation.ts
│ ├── index.ts
│ ├── initializeParameters.spec.ts
│ ├── initializeParameters.ts
│ ├── train.ts
│ └── updateParameters.ts
├── preprocess // 資料預處理:資料標準化
│ ├── index.ts
│ └── normalization
│ ├── index.ts
│ ├── meanNormalization.spec.ts
│ ├── meanNormalization.ts
│ ├── rescaling.spec.ts
│ └── rescaling.ts
└── utils // 幫助函式:資料結構轉換 & 矩陣廣播
├── broadcasting.spec.ts
├── broadcasting.ts
├── convertArray1DToArray2D.ts
├── convertArray2DToArray1D.ts
└── index.ts
複製程式碼
作為一個專注於深度學習本身的框架,deeplearning-js 只負責構建及訓練深度學習模型,使用者可以使用提供的 API 在任意資料集的基礎上搭建深度學習模型並獲得訓練後的結果,具體的例子各位可以參考 Logistic regression。
我們將學習率,迭代次數,隱藏層神經元個數等這些超引數暴露給終端使用者,deeplearning-js 會自動調整模型,給出不同的輸出。基於這些輸出,我們就可以自由地使用任意圖表或視覺化庫來展現模型訓練後的結果。
另外,大家在閱讀本系列文章的同時,建議配合著 deeplearning-js 的原始碼一起閱讀,相信這樣的話,你將會對深度學習到底在做一件什麼樣的事情有一個更感性的認識。
向量化
不同於其他的機器學習教程,我們並不希望在一開始就將大量拗口的數學名詞及概念灌輸給大家,相反,我們將從訓練深度學習模型的第一步資料處理講起。
讓我們以學術界非常著名的 Iris 資料集為例。
現在我們擁有了 150 個分別屬於 3 個品種的鳶尾屬植物的花萼長度,寬度及花瓣長度,寬度的樣本資料,目的是訓練一個輸入任意一個鳶尾屬植物的花萼長度,寬度及花瓣長度,寬度,判斷它是否是這 3 個品種中的某一個品種,即邏輯迴歸。
雖然我們的最終模型是輸入任意一個樣本資料得到結果,但我們在訓練時,並不希望每次只能夠輸入一個樣本資料,而是希望一次性地輸入所有樣本資料,得到訓練結果與實際結果的差值,然後使用反向傳播來修正這些差異。
於是我們就需要將多個樣本資料組合成一個矩陣,如下圖所示:
在將資料向量化後,我們才有了處理大資料集的能力,即在整個資料集上而不是在某個資料樣本上訓練模型。這也是為什麼在深度學習領域,GPU 比 CPU 要快得多的原因。在訓練深度學習模型時,所有的計算都是基於矩陣的,於是平行計算架構(處理多工時計算時間等於最複雜任務的完成時間)的 GPU 就要比序列計算架構(處理多工時計算時間等於所有任務執行時間的總和)的 CPU 快得多。
細心的讀者可能會觀察到上圖中的一個資料樣本中的不同維度的資料是豎排列的,這與傳統陣列中資料的橫排列方式恰好相反,即我們需要將
[5.1, 3.5, 1.4, 0.2]複製程式碼
轉換為
[
[5.1],
[3.5],
[1.4],
[0.2],
]複製程式碼
細心的讀者可能又會問了,如 Iris 資料集,為什麼一定要將初始資料轉換為 4 行 150 列的矩陣,用方便處理的 150 行 4 列的矩陣不可以嗎?
對於這個問題有以下兩方面的考慮。在接下來輸入資料與隱藏層做矩陣點乘時
隱藏層矩陣(W)的列數需要等於輸入層(A)的行數,所以為了減少不必要的計算量,我們希望輸入層的行數儘可能得小,於是我們將資料樣本的維度數與樣本數量進行對比,不難得出在絕大多數情況下,資料樣本的維度數都遠遠小於樣本數量這個結論。另一方面,在點乘之後,結果矩陣的列數將等於輸入層的列數,也就是說如果我們希望我們的輸出是一個 [X, 150] 的矩陣,輸入層就需要是一個 [4, 150] 的矩陣。
那麼如何快速地在原始資料集與使用資料集之間進行這樣的轉換呢?這就涉及到矩陣的一個常用運算,矩陣轉置了。
矩陣
說起矩陣,它的許多奇怪的特性,如轉置,點乘等,想必是許多朋友大學時代的噩夢。在這裡我們不談具體的數學概念,先嚐試用幾句話來描述一下矩陣及它的基礎運算。
從最直觀的角度來講,確定一個矩陣需要哪些資訊?一是矩陣的形狀,即座標系(空間),二是矩陣在這個座標系下各個維度上的值(位置)。
- 矩陣(Array2D):N 維空間中的一個物體,在每一維度上都有其確定的位置
- 矩陣相加(add):在某一維度或多個維度上對原物體進行拉伸
- 矩陣相減(subtract):在某一維度或多個維度上對原物體進行裁剪
- 矩陣相乘(multiply):基於原物體的某一個原點對原物體進行等比放大
- 矩陣相除(divide):基於原物體的某一個原點對原物體進行等比縮放
- 矩陣轉置(transpose):基於原物體的原點對原物體進行翻轉
- 矩陣點乘(dot):對原物體進行左邊矩陣所描述的位置轉換,即移動
在 deeplearning-js 中我們使用二維陣列的資料結構來表示矩陣,對於上述運算的具體程式碼實現各位可以參考 Array2D。
一個簡單的資料轉換的例子如下:
function formatDataSet(dataset: Array<any>) {
const datasetSize = dataset.length;
let inputValues: Array<number> = [];
map(dataset, (example: {
"sepalLength": number,
"sepalWidth": number,
"petalLength": number,
"petalWidth": number,
"species": string,
}) => {
const input: any = omit(example, 'species');
inputValues = inputValues.concat(values(input));
});
const input = new Array2D(
[datasetSize, inputValues.length / datasetSize],
inputValues,
).transpose();
return input;
}複製程式碼
小結
在理解了資料向量化及矩陣的概念後,相信大家已經可以將大樣本量,以陣列形式儲存的資料轉換為適合進行深度學習模型訓練的大型矩陣了,接下來讓我們從如何初始化引數開始,一步步搭建我們的第一個深度學習模型。