簡介
在本篇文章中,我們採用邏輯迴歸作為案例,探索神經網路的構建方式。文章詳細闡述了神經網路中層結構的實現過程,並提供了線性層、啟用函式以及損失函式的定義(實現方法)。
目錄
- 背景介紹
- 網路框架構建
- 層的定義
- 線性層
- 啟用函式
- 損失函式
背景介紹
在網路的實現過程中,往往設計大量層的計算,對於簡單的網路(演算法),其實現相對較容易,例如線性迴歸,但對於邏輯迴歸,從輸入到啟用值再到損失估計的過程整體已經較冗長,實現複雜,並且難以維護,因此,我們需要採用系統性的框架來實現網路(演算法),以達到更好的效能、可維護性等。
以邏輯迴歸為例初步探究
邏輯迴歸的決策過程可以分為三步
- 對取樣資料\(X\)進行評估
- 啟用函式變換
- 損失函式計算
其中\(\circ\)表述逐元素相乘或稱哈達瑪積
上述過程中反應出了這些層的一些共性:
- 計算函式值的輸入與計算梯度時的輸入相同。
- 計算函式值時,每一步的輸入都是上一步的輸出。
- 計算梯度時,由鏈式法則,每一步的梯度累乘即為損失關於引數的梯度。
函式值的計算過程從輸入開始直至輸出結果,故稱為前向傳播(forward)
導數值的計算過程從輸出開始反向直至第一層,故稱為反向傳播(backward)
由此,可以總結,並設計出層的基本程式碼如下:
class Layer {
private:
MatrixXd para; // 引數
MatrixXd cache; // 快取
public:
MatrixXd forward(MatrixXd input); // 前向傳播
MatrixXd backward(MatrixXd prevGrad); // 反向傳播
void update(double learning_rate); // 引數更新
}
部分層是沒有引數的,例如啟用函式、損失函式。這裡只有線性層是有引數的。
框架功能探索
基於上一小節提出的框架,我們可以構建三個層次,虛擬碼如下:
Layer linear;
Layer sigmoid;
Layer logit;
下面我們來詳細討論如何基於該框架進行計算。
前向傳播
MatrixXd Layer::forward(MatrixXd input) {
cache = input; // 儲存輸入
return input * para; // 對輸入做計算並返回;這裡給了一個示例
}
-
z=linear.forward(X)
:線性層對輸入\(\text{X}\)進行評估,並在其記憶體cache
中儲存\(\text{X}\) -
hat_y=sigmoid.forward(z)
:啟用函式對輸入\(\text{z}\)進行變換,得到類別機率(預測)\(\hat{\text{y}}\),同時在其記憶體cache
中儲存\(\text{z}\) -
loss = logit.forward(y, hat_y)
:損失函式依據真實值\(\text{y}\)和預測值\(\hat{\text{y}}\)估計模型誤差.
可以注意到,loss具有兩個輸入,一個是前向傳播過程中的計算值,一個是真實結果。
從多型性的角度,雖然行為上有大量的一致性,但是由於輸入引數數量不一致,其很難和普通層以及啟用函式一樣從基本層類派生,而是需要另外定義基類。
反向傳播
反向傳播的示例程式碼如下:
MatrixXd Layer::backward(MatrixXd prevGrad) {
MatrixXd grad = ...; // 採用cache中內容計算梯度
cache = grad * prevGrad; // 梯度累乘,同時儲存進記憶體
return cache; // 返回梯度,以供下一層進行計算
}
-
grad = logit.backward(y, hat_y)
:依據公式計算損失的梯度\(\partial \text{LOSS}/\partial \hat{\text{y}}\)。 -
grad = sigmoid.backward(grad)
:首先計算梯度\(\partial{\hat{\text{y}}/\partial \text{z}}\),其中,計算所需的\(\text{z}\)從快取中讀取。最後,將梯度與輸入的梯度相乘,得到梯度\(\partial \text{LOSS}/\partial \text{z}\)。 -
grad = linear.backward(grad)
:首先計算梯度\(\partial\text{z}/\partial \text{w}\),計算所需的\(X\)從快取中讀取。最後,將梯度與輸入梯度相乘,得到梯度\(\partial \text{LOSS}/\partial \text{w}\)。
引數更新
引數更新的示例程式碼如下:
MatrixXd Layer::update(double learning_rate) {
w -= learning_rate * cache; // 採用快取中儲存的梯度進行引數更新
}
在本例中,只有linear
是具有引數的層,因此在更新的時候只用呼叫linear
的update()
方法即可。
總結
在本小節中,我們以邏輯迴歸為例,初步探索了神經網路的構建方法,即:定義層型別,用以表示網路的每一層(或元件),在計算過程中,分為前向傳播(推理及評估模型誤差),反向傳播(計算梯度)以及引數更新三個步驟。
該實現方法很好的將複雜的計算拆分為了多個獨立的單元,便於較大體量的演算法的實現、並且提供了極好可維護性。同時,對記憶體cache
的合理使用,也極大的簡化了呼叫過程,並提升了演算法效率。
網路框架構建
神經網路是由多個元件一層層元件起來的,或者說組建神經網路的基本單元是層Layer
,在本文中,將詳細講述怎麼設計並實現層單元。
層的定義
層通常包含三種基本方法(行為):前向傳播(forward)、反向傳播(backward)和引數更新(update)。
對於每種行為,不同型別的層所對同一操作所執行的行為不同,例如,在前向傳播過程中,線性層對輸入進行線性對映,啟用函式則將每一元素對映至\([0,1]\)。
C++中,允許在基類中定義方法,在派生類中過載這些方法,並在執行時根據實際型別來調整呼叫的函式。這種方法稱之為多型性(Polymorphism)。
下面給出了層的定義(基類)。
class Layer {
public:
virtual MatrixXd forward(const MatrixXd& input) = 0;
virtual MatrixXd backward(const MatrixXd& prevGrad) = 0;
virtual void update(double learning_rate) = 0;
virtual ~Layer() {}
};
在層的宣告中,定義了層的三種基本方法。
線性層
對於線性層,其包含兩個引數:權重矩陣W
,和偏置b
。其包含三個記憶體,一個用於儲存輸入inputCache
,另外兩個用於儲存引數的變化量dW
和db
(一般是損失函式關於引數的梯度)。下面給出程式碼:
class Linear : public Layer {
private:
// para
MatrixXd W;
MatrixXd b;
// cache
MatrixXd inputCache;
MatrixXd dW;
MatrixXd db;
public:
Linear(size_t input_D, size_t output_D);
MatrixXd forward(const MatrixXd& input) override;
MatrixXd backward(const MatrixXd& prevGrad) override;
void update(double learning_rate) override;
};
建構函式
建構函式用於初始化線性層的引數和快取變數,程式碼如下:
Linear::Linear(size_t input_D, size_t output_D)
: W(MatrixE::Random(input_D, output_D)), b(MatrixE::Random(1, output_D))
{
inputCache = MatrixE::Constant(1, input_D, 0);
dW = MatrixE::Constant(input_D, output_D, 0);
db = MatrixE::Constant(1, output_D, 0);
};
-
建構函式的引數:
input_D
:輸入特徵的維度,即輸入資料的特徵數量。output_D
:輸出特徵的維度,即線性層輸出的特徵數量。
-
成員初始化列表:
將引數W
和b
初始化為指定尺寸的隨機矩陣。隨機矩陣中的元素通常從標準正態分佈中取樣。 -
快取變數的初始化:
將快取變數初始化為與其尺寸匹配的全零矩陣。對於輸入快取,由於輸入尺寸未知,僅知其特徵維度,故初始化時設定其尺寸為1。
前向傳播
下述為線性層前向傳播計算的程式碼實現。
MatrixXd ML::Linear::forward(const MatrixXd& input) {
// 快取輸入
this->inputCache = input;
// 返回計算結果
return (input * W) + b.replicate(input.rows(), 1);
}
在計算過程中,input * W
表示矩陣乘法,其中input
是m x n
矩陣,W
是n x o
矩陣,結果為m x o
矩陣,表示線性變換的結果。
b
是一個 o x 1
的偏置向量,透過b.replicate(input.rows(), 1)
將其沿行方向複製m
次,生成一個m x o
的矩陣,為每個樣本新增相同的偏置項。
最終,將線性變換結果與偏置矩陣相加,得到輸出矩陣。
反向傳播
下述為線性層方向傳播計算的程式碼實現。
MatrixXd ML::Linear::backward(const MatrixXd& prevGrad) {
// 計算關於權重的梯度
this->dW = inputCache.transpose() * prevGrad;
// 計算關於偏置的梯度
this->db = prevGrad.colwise().sum();
// 計算並返回關於輸入的梯度
return prevGrad * W.transpose();
}
該函式接受前一層的梯度prevGrad
,並根據快取的輸入矩陣inputCache
計算損失關於本層引數的偏導(分別計算dW
和db
)並儲存以用於引數更新。最後,返回關於輸入的梯度矩陣。
啟用函式
啟用函式是比較特殊的層函式,其不包含任何的引數資訊,因此,其無需進行引數更新,或者說引數更新函式不做任何操作;進一步的,啟用函式也不需要在快取中儲存梯度資訊,因為它無需更新引數。
下面給出較經典的三種啟用函式的定義,它們都是從Layer
中派生的:
ReLU啟用函式
class ReLU : public Layer {
private:
MatrixXd inputCache;
public:
MatrixXd forward(const MatrixXd& input) override;
MatrixXd backward(const MatrixXd& prevGrad) override;
void update(double learning_rate) override {}
};
MatrixXd ReLU::forward(const MatrixXd& input) {
inputCache = input;
return input.unaryExpr([](double x) { return x > 0 ? x : 0; });
}
MatrixXd ReLU::backward(const MatrixXd& prevGrad) {
MatrixXd derivative = inputCache.unaryExpr([](double x) { return x > 0 ? 1 : 0; }).cast<double>().matrix();
return prevGrad.cwiseProduct(derivative);
}
Sigmoid啟用函式
class Sigmoid : public Layer {
private:
MatrixXd inputCache;
public:
MatrixXd forward(const MatrixXd& input) override;
MatrixXd backward(const MatrixXd& prevGrad) override;
void update(double learning_rate) override {}
};
MatrixXd Sigmoid::forward(const MatrixXd& input) {
inputCache = input;
return input.unaryExpr([](double x) { return 1.0 / (1.0 + exp(-x)); });
}
MatrixXd Sigmoid::backward(const MatrixXd& prevGrad) {
MatrixXd sigmoidOutput = inputCache.unaryExpr([](double x) { return 1.0 / (1.0 + exp(-x)); });
return prevGrad.cwiseProduct(sigmoidOutput.cwiseProduct((1 - sigmoidOutput.array()).matrix()));
}
tanh啟用函式
class Tanh : public Layer {
private:
MatrixXd inputCache;
public:
MatrixXd forward(const MatrixXd& input) override;
MatrixXd backward(const MatrixXd& prevGrad) override;
void update(double learning_rate) override {}
};
MatrixXd Tanh::forward(const MatrixXd& input) {
inputCache = input;
return input.unaryExpr([](double x) { return tanh(x); });
}
MatrixXd Tanh::backward(const MatrixXd& prevGrad) {
MatrixXd tanhOutput = inputCache.unaryExpr([](double x) { return tanh(x); });
return prevGrad.cwiseProduct((1 - tanhOutput.cwiseProduct(tanhOutput).array()).matrix());
}
損失函式
損失函式是一種特殊的層,其行為與普通的層相似,但在前向傳播和反向傳播過程中與普通的層存在較大差異:
- 前向傳播:從使用來看,如果僅需使用網路,而無需評價,或者訓練網路,前向傳播過程中,無需呼叫損失函式。從輸入個數來看,普通層函式只需要傳入上一層的輸出即可,而損失函式需要上一層的輸出(也即:網路的預測結果)和真實結果兩個引數。
- 反向傳播:損失函式是反向傳播的起點,其接受預測結果和真實結果作為其輸入,返回梯度矩陣,而其它矩陣則是輸入上一層的梯度矩陣,返回本層的梯度矩陣。
由於損失函式與普通層的差異,一般單獨定義損失函式的呼叫介面(基類),程式碼如下:
class LossFunction {
public:
virtual double computeLoss(const MatrixXd& predicted, const MatrixXd& actual) = 0;
virtual MatrixXd computeGradient(const MatrixXd& predicted, const MatrixXd& actual) = 0;
virtual ~LossFunction() {}
};
下文分別以均方根誤差和對數損失為例,給出程式碼,供讀者參考學習
MSE均方根誤差
class MSELoss : public LossFunction {
public:
double computeLoss(const MatrixXd& predicted, const MatrixXd& actual) override;
MatrixXd computeGradient(const MatrixXd& predicted, const MatrixXd& actual) override;
};
double MSELoss::computeLoss(const MatrixXd& predicted, const MatrixXd& actual) {
MatrixXd diff = predicted - actual;
return diff.squaredNorm() / (2.0 * predicted.rows());
}
MatrixXd MSELoss::computeGradient(const MatrixXd& predicted, const MatrixXd& actual) {
MatrixXd diff = predicted - actual;
return diff / predicted.rows();
}
對數損失函式
class LogisticLoss : public LossFunction {
public:
double computeLoss(const MatrixXd& predicted, const MatrixXd& actual) override;
MatrixXd computeGradient(const MatrixXd& predicted, const MatrixXd& actual) override;
};
double LogisticLoss::computeLoss(const MatrixXd& predicted, const MatrixXd& actual) {
MatrixXd log_predicted = predicted.unaryExpr([](double p) { return log(p); });
MatrixXd log_1_minus_predicted = predicted.unaryExpr([](double p) { return log(1 - p); });
MatrixXd term1 = actual.cwiseProduct(log_predicted);
// MatrixXd term2 = (1 - actual).cwiseProduct(log_1_minus_predicted);
MatrixXd term2 = (1 - actual.array()).matrix().cwiseProduct(log_1_minus_predicted);
double loss = -(term1 + term2).mean();
return loss;
}
MatrixXd LogisticLoss::computeGradient(const MatrixXd& predicted, const MatrixXd& actual) {
MatrixXd temp1 = predicted - actual;
MatrixXd temp2 = predicted.cwiseProduct((1 - predicted.array()).matrix());
return (temp1).cwiseQuotient(temp2);
}