Alink漫談(十四) :多層感知機 之 總體架構
0x00 摘要
Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習算法平台,是業界首個同時支持批式算法、流式算法的機器學習平台。本文和下文將帶領大家來分析Alink中多層感知機的實現。
因為Alink的公開資料太少,所以以下均為自行揣測,肯定會有疏漏錯誤,希望大家指出,我會隨時更新。
0x01 背景概念
幾乎所有的深度學習算法都可以被描述為一個相當簡單的配方:特定的數據集、代價函數、優化過程和模型。
1.1 前饋神經網絡
前饋神經網絡(Feedforward Neural Network, FNN )中,把每個神經元按接收信息的先后分為不同的組,每一組可以看做是一個神經層。每一層中的神經元接收前一層神經元的輸出,並輸出到下一層神經元。整個網絡中的信息是朝着一個方向傳播的,沒有反向的信息傳播(和誤差反向傳播不是一回事),即整個網絡中無反饋,信號從輸入層向輸出層單向傳播,可以用一個有向無環圖來表示。在前饋神經網絡中,第0層叫做輸入層,最后一層叫做輸出層,其他中間層叫做隱藏層。
反饋神經網絡中神經元不但可以接收其他神經元的信號,而且可以接收自己的反饋信號。和前饋神經網絡相比,反饋神經網絡中的神經元具有記憶功能,在不同時刻具有不同的狀態。反饋神經網絡中的信息傳播可以是單向也可以是雙向傳播,因此可以用一個有向循環圖或者無向圖來表示。
前饋網絡的主要目標是近似一些函數f*。例如,回歸函數y = f *(x)將輸入x映射到值y。前饋網絡定義了y = f (x; θ)映射,並學習參數θ的值,使結果更加接近最佳函數。
例如,我們有三個函數f(1),f(2)和f(3)連接在一個鏈上以形成f(x)=f(3)(f(2)(f(1)(x)))。這些鏈式結構是神經網絡中最常用的結構。在這種情況下,f(1)被稱為網絡的第一層(first layer),f(2)被稱為第二層(second layer),依此類推。鏈的全長稱為模型的深度(depth)。正是因為這個術語才出現了”深度學習”這個名字。
現在問題來了,為什么當我們有線性機器學習模型時,還需要前饋網絡?這是因為線性模型僅限於線性函數,而神經網絡不是。當我們的數據不是線性可分離的線性模型時,面臨着近似的問題,而神經網絡則相當容易應對。隱藏層用於增加非線性並改變數據的表示,以便更好地泛化函數。
1.2 反向傳播
怎么理解這個“反向傳播”呢,其實DL的核心理念就在於找到全局性誤差函數Loss符合要求的,對應的權值 “w” 與 “b”。那么問題就來了,當得到的誤差Loss不符合要求(即誤差過大),就可以通過“反向傳播”的方式,把輸出層得到的誤差反過來傳到隱含層,並分配給不同的神經元,以此調整每個神經元的“權值”,最終調整至Loss符合要求為止,這就是“誤差反響傳播”的核心理念。
在此我們首先要澄清一個容易混淆的概念,即有的地方經常會用反向傳播來代指深度模型的整個學習算法,其實這是不准確的,整體的學習算法可以分為兩方面:
- 代價信息如何傳遞到深度模型的每一層?
- 基於傳遞到本層的信息,本層的參數應該如何更新?
在特定結構中,信息沿着組織結構向前流動,我們稱之為前向傳播,相應的,反向傳播則指信息沿着結構從后向前流動。
在前饋神經網絡中,前向傳播的是輸入,並且在過程中逐漸抽象為特征,反向傳播的則是當前輸出值與期望輸出的代價信息,或者說誤差,傳遞到每一層的信息則是該層的輸出值與該層的 “期望輸出” 的代價信息。
在如今的主流框架中,反向傳播與代價信息和梯度結合起來借助計算圖來實現。因此,反向傳播既不是只有神經網絡或者深度模型才有,也不能全部代表深度模型的整個學習算法,它所代表的只是第一個問題,即基於代價信息如何更新參數如何進行更高效的優化則是優化算法的問題。現代最有效的優化算法主要是基於梯度下降的,並以其為基礎做出了很多創新工作。
總結深度模型的訓練過程如下:針對既定的網絡結構和性能指標,細致地定義代價/誤差/目標函數,輸入通過前向傳播到達輸出層,並且針對每一個或一批輸入產生的輸出,在定義好的代價函數下計算代價信息,通過反向傳播傳遞到深度模型的每一層,在每一層上基於代價信息對參數的梯度更新參數,直到滿足停止條件,完成訓練。
1.3 代價函數
代價函數的作用是顯示了我們的模型得出的近似值與我們試圖達到的實際目標值之間的差異。
通常代價函數至少含有一項使學習過程進行統計估計的成分。最常見的代價函數是負對數似然、最小化代價函數導致的最大似然估計。代價函數也可能含有附加項,如正則化項。
在某些情況下,由於計算原因,我們不能實際計算代價函數。在這種情況下,只要我們有近似其梯度的方法,那么我們仍然可以使用迭代數值優化近似最小化目標。
與機器學習算法一樣,前饋網絡也使用基於梯度的學習方法進行訓練,在這種學習方法中,使用隨機梯度下降等算法來使代價函數達到最小化。整個訓練過程在很大程度上取決於我們的代價函數的選擇,其選擇或多或少與其他參數模型相同。
對於反向傳播算法的代價函數,它必須滿足兩個屬性:
-
代價函數必須能夠表達為平均值。
-
代價函數不能依賴於輸出層旁邊網絡的任何激活值。
代價函數的形式主要是C(W, B, Sr, Er),其中W是神經網絡的權重,B是網絡的偏置,Sr是單個訓練樣本的輸入,Er是該訓練樣本的期望輸出。
1.4 優化過程
1.4.1 迭代法
在一個算法模型訓練最開始,權值w和偏置b都是隨機賦予的,理論上它可能是出現在整個函數圖像中的任何位置,那如何讓他去找到我們所要求的那個值呢。
這里就要引入“迭代”的思想:我們可以通過代入左右不同的點去嘗試,假設代入當前 x 左面的一個點比右面的更小,那么就可以讓 x 變為左面的點,然后繼續嘗試,直到找到“極小值”么。這也是為什么算法模型需要時間去不斷迭代很訓練的原因。
1.4.2 梯度下降
使用迭代法,那么隨之而來另外一個問題:這樣一個一個嘗試,雖然最終結果是一定會找到我們所需要的值,但有沒有什么方法可以讓它離“極值”遠的時候,挪動的步子更大,離“極值”近的時候,挪動的步子變小(防止越過極值),實現更快更准確地“收斂”。假如是一個“二次函數”的圖像,那么如果取得點越接近“極小值”,在這個點的函數“偏導”越小(偏導即“在那個點的函數斜率”)。接下來引出下面這個方法:
梯度下降核心思想:Xn代表的就是挪動的“步長”,后面的部分表示當前這個點在函數的“偏導”,這樣也就代表當點越接近極值點,那么“偏導”越小,所以挪動的“步長”就短;反之如果離極值點很遠,則下一次挪動的“步長”越大。
把這個公式換到我們的算法模型,就找到了“挪動步長”與Loss和(w,b)之間的關系,實現快速“收斂”。
通過“迭代法”和“梯度下降法”的配合,我們實現了一輪一輪地迭代,每次更新都會越來越接近極值點,直到更新的值非常小或已經滿足我們的誤差范圍內,訓練結束,此時得到的(w,b)就是我們尋找的模型。
1.5 相關公式
以下是相關各種公式,摘錄出來給大家在閱讀時查閱。
1.5.1 加權求和 h
hj 表示當前節點的所有輸入加權之和。
1.5.2 神經元輸出值 a
- a_j 表示隱藏層神經元的輸出值。
- g()代表激活函數,w是權重,x是輸入。
- a_j=x_jk 即當前層神經元的輸出值,等於下一層神經元的輸入值。
1.5.3 輸出層的輸出值 y
- y 表示輸出層的值,也就是最終結果。
- h_k 表示輸出層神經元k的輸入加權之和。
1.5.4 激活函數g(h)
采用Sigmoid function:
sigmoid函數的導數:
將 aj=g(hj) 代入可得
1.5.5 損失函數E
采用誤差平方和(sum-of-squares error function)
- 平方是為了避免超平面兩端的誤差點相互抵消(y−t 存在正負)。
- 前面系數取1/2 是為了之后采用梯度下降時,求梯度(偏導數)時能抵消平方求導后的2。
1.5.6 誤差反向傳播——更新權重
采用梯度下降求最優解,也就是求損失函數E關於權重w的偏導數
等式右邊可以解釋為:如果我們想知道當權重w改變時,輸出的誤差E是如何變化的,我們可以通過觀察誤差E是如何隨着激活函數的輸入值h變化,以及激活函數的輸入值h是如何隨着權重w變化。
h_k表示輸出層神經元k的所有輸入加權之和,也就是激活函數g(h)的輸入值。
1.5.7 輸出層增量項 δo
右邊第一項比較重要,這里稱為增量項δ(error or delta term),繼續通過鏈式法則推導,最終得到輸出層的增量項
接下來可以對輸出層的權重w進行更新。
1.5.8 更新輸出層權重wjk
對損失函數使用梯度下降法,更新權重:
於是得到
ai是上一層的輸出值,也即是輸出層的輸入值xi。
0x02 示例代碼
本文示例代碼如下:
public class MultilayerPerceptronClassifierExample {
public static void main(String[] args) throws Exception {
BatchOperator data = Iris.getBatchData();
MultilayerPerceptronClassifier classifier = new MultilayerPerceptronClassifier()
.setFeatureCols(Iris.getFeatureColNames())
.setLabelCol(Iris.getLabelColName())
.setLayers(new int[]{4, 5, 3})
.setMaxIter(100)
.setPredictionCol("pred_label")
.setPredictionDetailCol("pred_detail");
BatchOperator res = classifier.fit(data).transform(data);
res.print();
}
}
Iris定義如下
public class Iris {
final static String URL = "https://alink-release.oss-cn-beijing.aliyuncs.com/data-files/iris.csv";
final static String SCHEMA_STR
= "sepal_length double, sepal_width double, petal_length double, petal_width double, category string";
public static BatchOperator getBatchData() {
return new CsvSourceBatchOp(URL, SCHEMA_STR);
}
public static StreamOperator getStreamData() {
return new CsvSourceStreamOp(URL, SCHEMA_STR);
}
public static String getLabelColName() {
return "category";
}
public static String[] getFeatureColNames() {
return new String[] {"sepal_length", "sepal_width", "petal_length", "petal_width"};
}
}
0x03 訓練總體邏輯
MultilayerPerceptronTrainBatchOp 類是批處理訓練的實現。
protected BatchOperator train(BatchOperator in) {
return new MultilayerPerceptronTrainBatchOp(this.getParams()).linkFrom(in);
}
所以還是老套路,直接看 MultilayerPerceptronTrainBatchOp 的 linkFrom 函數。
其大致思路如下:
- 1)獲取一些元信息,比如label名稱,特征列名,特征類型等;
- 2)獲取測試數據
trainData = getTrainingSamples
; - 3)訓練
- 3.1)獲取初始權重
initialWeights = getInitialWeights();
- 3.2)構建拓撲
topology = FeedForwardTopology.multiLayerPerceptron
- 3.3)構建訓練器
FeedForwardTrainer
。- 3.3.1)初始化模型
- 3.3.2)構建目標函數
- 3.3.3)訓練器會基於目標函數構建優化器,這里的優化器是
L-BFGS
。
- 3.4)訓練獲取最終權重
weights = trainer.train
- 3.1)獲取初始權重
- 4)輸出模型
DataSet<Row>;
- 5)把
DataSet<Row>
轉成Table;
@Override
public MultilayerPerceptronTrainBatchOp linkFrom(BatchOperator<?>... inputs) {
BatchOperator<?> in = checkAndGetFirst(inputs);
// 1)獲取一些元信息,比如label名稱,特征列名,特征類型等。
final String labelColName = getLabelCol();
final String vectorColName = getVectorCol();
final boolean isVectorInput = !StringUtils.isNullOrWhitespaceOnly(vectorColName);
final String[] featureColNames = isVectorInput ? null :
(getParams().contains(FEATURE_COLS) ? getFeatureCols() :
TableUtil.getNumericCols(in.getSchema(), new String[]{labelColName}));
final TypeInformation<?> labelType = in.getColTypes()[TableUtil.findColIndex(in.getColNames(),
labelColName)];
DataSet<Tuple2<Long, Object>> labels = getDistinctLabels(in, labelColName);
// 此處程序變量如下:
labelColName = "category"
vectorColName = null
isVectorInput = false
featureColNames = {String[4]@6412}
0 = "sepal_length"
1 = "sepal_width"
2 = "petal_length"
3 = "petal_width"
labelType = {BasicTypeInfo@6414} "String"
labels = {MapOperator@6415}
// 2)獲取測試數據
// get train data
DataSet<Tuple2<Double, DenseVector>> trainData =
getTrainingSamples(in, labels, featureColNames, vectorColName, labelColName);
// train 3)訓練
final int[] layerSize = getLayers();
final int blockSize = getBlockSize();
// 3.1)獲取初始權重
final DenseVector initialWeights = getInitialWeights();
// 3.2)獲取拓撲
Topology topology = FeedForwardTopology.multiLayerPerceptron(layerSize, true);
// 3.3)構建訓練器
FeedForwardTrainer trainer = new FeedForwardTrainer(topology,
layerSize[0], layerSize[layerSize.length - 1], true, blockSize, initialWeights);
// 3.4)訓練獲取最終權重
DataSet<DenseVector> weights = trainer.train(trainData, getParams());
// output model 4)輸出模型
DataSet<Row> modelRows = weights
.flatMap(new RichFlatMapFunction<DenseVector, Row>() {
@Override
public void flatMap(DenseVector value, Collector<Row> out) throws Exception {
List<Tuple2<Long, Object>> bcLabels = getRuntimeContext().getBroadcastVariable("labels");
Object[] labels = new Object[bcLabels.size()];
bcLabels.forEach(t2 -> {
labels[t2.f0.intValue()] = t2.f1;
});
MlpcModelData model = new MlpcModelData(labelType);
model.labels = Arrays.asList(labels);
model.meta.set(ModelParamName.IS_VECTOR_INPUT, isVectorInput);
model.meta.set(MultilayerPerceptronTrainParams.LAYERS, layerSize);
model.meta.set(MultilayerPerceptronTrainParams.VECTOR_COL, vectorColName);
model.meta.set(MultilayerPerceptronTrainParams.FEATURE_COLS, featureColNames);
model.weights = value;
new MlpcModelDataConverter(labelType).save(model, out);
}
})
.withBroadcastSet(labels, "labels");
// 5)把DataSet<Row>轉成Table
setOutput(modelRows, new MlpcModelDataConverter(labelType).getModelSchema());
}
3.1 總體邏輯示例圖
總體邏輯示例圖如下,這里為了更好說明,把初始化步驟順序做了微調。
----------------------------------------------------------------------------------------
│ │
│ │
┌──────────────────────┐ ┌────────────────────┐
│ multiLayerPerceptron │ 構建拓撲 │ getTrainingSamples │ 獲取訓練數據
└──────────────────────┘ └────────────────────┘
│ │ <label index, vector>
│ │
│ │
┌──────────────────────┐ │
│ FeedForwardTopology │ 拓撲,里面包含 layers │
└──────────────────────┘ layers是拓撲的各個層,比如AffineLayer │
│ │
│ │
│ │
┌────────────┐ ┌────────────────────┐
│ initModel │ 初始化模型 │trainData = stack() │
└────────────┘ └────────────────────┘
│ │ 把訓練數據壓縮成向量
│ │
│ │
┌─────────────────────────────┐ │
│ FeedForwardTrainer(topology)│ 生成訓練器 │
└─────────────────────────────┘ │
│ │
│ │
│ │
┌──────────────────────────┐ │
│ AnnObjFunc 目標函數 │ 基於FeedForwardTopology生成優化目標函數 │
│ [topology,topologyModel] │ 成員變量 topology 是神經網絡的拓撲 │
└──────────────────────────┘ 成員變量 topologyModel 是計算模型 │
│ │
│ │
│ │
┌──────────────────────────┐ │
│ AnnObjFunc.topologyModel │ 生成目標函數中的拓撲模型 │
└──────────────────────────┘ │
│ │
│ │
│ │
┌───────────────────────────────────────┐ │
│ optimizer = new Lbfgs(..annObjFunc..) │ 生成優化器(訓練過程中) │
└───────────────────────────────────────┘ 基於目標函數生成 │
│ │
│ │
│ │
┌──────────────────────────────────┐ │
│ optimizer.initCoefWith(initCoef) │ 初始化優化器 │
└──────────────────────────────────┘ │
│ │
│ │
│ <--------------------------------------------------------│
│
┌──────────────────────────────────────────────┐
│ optimizer.optimize() │ 優化器L-BFGS迭代訓練
│ │ │
│ │ │
│ ┌──────────────────────────┐ │
│ │ 計算梯度(利用拓撲模型) │ │
│ │ 1. 計算各層的輸出 │ │
│ │ 2. 計算輸出層損失 │ │
│ │ 3. 計算各層的Delta │ │
│ │ 4. 計算各層梯度 │ │
│ └──────────────────────────┘ │
│ │ │
│ │ │
│ ┌──────────────────────────┐ │
│ │ 計算方向 │ │
│ │這里沒有用到目標函數的拓撲模型 │ │
│ └──────────────────────────┘ │
│ │ │
│ │ │
│ ┌──────────────────────────┐ │
│ │ 計算損失(利用拓撲模型) │ │
│ │ 1. 計算各層的輸出 │ │
│ │ 2. 計算輸出層損失 │ │
│ └──────────────────────────┘ │
│ │ │
│ │ │
│ ┌──────────────────────────┐ │
│ │ 更新模型 │ │
│ │這里沒有用到目標函數的拓撲模型 │ │
│ └──────────────────────────┘ │
│ │ │
│ │ │
└──────────────────────────────────────────────┘
│
│
----------------------------------------------------------------------------------------
上面圖可能在手機上變形,所以也可以參見下面圖片:
3.2 L-BFGS訓練調用邏輯概述
針對上圖需要說明,L-BFGS是我們的優化器,其中幾個關鍵步驟如下:
CalcGradient()
計算梯度CalDirection(...)
計算方向CalcLosses(...)
計算損失UpdateModel(...)
更新模型
算法框架都是基本不變的,所差別的就是具體目標函數和損失函數的不同。比如線性回歸采用的是UnaryLossObjFunc,損失函數是 SquareLossFunc。而多層感知機這里,用的目標函數是:AnnObjFunc。
具體針對多層感知機,L-BFGS中 與目標函數 的相關步驟如下:
CalcGradient 計算梯度
- 1)調用
AnnObjFunc.updateGradient;
- 1.1)調用 目標函數中拓撲模型
topologyModel.computeGradient
來計算- 1.1.1)計算各層的輸出;
forward(data, true)
- 1.1.2)計算輸出層損失;
labelWithError.loss
- 1.1.3)計算各層的Delta;
layerModels.get(i).computePrevDelta
- 1.1.4)計算各層梯度;
layerModels.get(i).grad
- 1.1.1)計算各層的輸出;
- 1.1)調用 目標函數中拓撲模型
CalDirection 計算方向
- 這里沒有用到目標函數的拓撲模型。
CalcLosses 計算損失
- 1)調用
AnnObjFunc.calcSearchValues;
其內部會調用calcLoss
計算損失;- 1.1)調用
topologyModel.computeGradient
來計算損失- 1.1.1)計算各層的輸出;
forward(data, true)
- 1.1.2)計算輸出層損失;
labelWithError.loss
- 1.1.1)計算各層的輸出;
- 1.1)調用
UpdateModel 更新模型
- 這里沒有用到目標函數的拓撲模型。
3.3 獲取訓練數據
getTrainingSamples函數將從原始輸入獲取訓練數據。
原始數據舉例
5.1 3.5 1.4 0.2 Iris-setosa
5 2 3.5 1 Iris-versicolor
5.1 3.7 1.5 0.4 Iris-setosa
6.4 2.8 5.6 2.2 Iris-virginica
6 2.9 4.5 1.5 Iris-versicolor
主要做了如下:
- 1)獲取元數據,比如特征列的index,label列的index;
- 2)把labels廣播,后續會在open函數中使用;
- 3)open函數中得倒一個
label : index
的映射 - 4)map 函數中有兩種執行序列,都會轉換為
<label index, vector>
這樣的二元組- 4.1)原始輸入中有vector,比如類似 5.1 3.5 1.4 0.2 Iris-setosa 5.1 3.5 1.4 0.2,這些加粗的就是vector。
- 4.2)原始輸入中沒有vector,比如類似 5.1 3.5 1.4 0.2 Iris-setosa ;
具體代碼如下:
private static DataSet<Tuple2<Double, DenseVector>> getTrainingSamples(
BatchOperator data, DataSet<Tuple2<Long, Object>> labels,
final String[] featureColNames, final String vectorColName, final String labelColName) {
// 1)獲取元數據,比如特征列的index,label列的index;
final boolean isVectorInput = !StringUtils.isNullOrWhitespaceOnly(vectorColName);
final int vectorColIdx = isVectorInput ? TableUtil.findColIndex(data.getColNames(), vectorColName) : -1;
final int[] featureColIdx = isVectorInput ? null : TableUtil.findColIndices(data.getSchema(),
featureColNames);
final int labelColIdx = TableUtil.findColIndex(data.getColNames(), labelColName);
// 程序變量如下
isVectorInput = false
vectorColIdx = -1
featureColIdx = {int[4]@6443}
0 = 0
1 = 1
2 = 2
3 = 3
labelColIdx = 4
DataSet<Row> dataRows = data.getDataSet();
return dataRows
.map(new RichMapFunction<Row, Tuple2<Double, DenseVector>>() {
transient Map<Comparable, Long> label2index;
@Override
public void open(Configuration parameters) throws Exception {
List<Tuple2<Long, Object>> bcLabels = getRuntimeContext().getBroadcastVariable("labels");
this.label2index = new HashMap<>();
// 得倒一個label : index 的映射
bcLabels.forEach(t2 -> {
Long index = t2.f0;
Comparable label = (Comparable) t2.f1;
this.label2index.put(label, index);
});
// 變量是
this = {MultilayerPerceptronTrainBatchOp$2@11578}
label2index = {HashMap@11580} size = 3
"Iris-versicolor" -> {Long@11590} 2
"Iris-virginica" -> {Long@11592} 1
"Iris-setosa" -> {Long@11594} 0
}
@Override
public Tuple2<Double, DenseVector> map(Row value) throws Exception {
Comparable label = (Comparable) value.getField(labelColIdx);
Long labelIdx = this.label2index.get(label);
if (isVectorInput) { // 4.1)如果原始輸入中有vector
Vector vec = VectorUtil.getVector(value.getField(vectorColIdx));
// 轉換為 <label index, vector> 這樣的二元組
if (null == vec) {
return new Tuple2<>(labelIdx.doubleValue(), null);
} else {
return new Tuple2<>(labelIdx.doubleValue(),
(vec instanceof DenseVector) ? (DenseVector) vec
: ((SparseVector) vec).toDenseVector());
}
} else { // 4.2)如果原始輸入中沒有vector
int n = featureColIdx.length;
DenseVector features = new DenseVector(n);
for (int i = 0; i < n; i++) {
double v = ((Number) value.getField(featureColIdx[i])).doubleValue();
features.set(i, v);
}
// 轉換為 <label index, vector> 這樣的二元組
return Tuple2.of(labelIdx.doubleValue(), features);
}
}
})
.withBroadcastSet(labels, "labels"); // 2)把labels廣播,在open函數中使用;
}
3.4 構建拓撲
FeedForwardTopology.multiLayerPerceptron
完成了構建前饋神經網絡拓撲的工作。
public static FeedForwardTopology multiLayerPerceptron(int[] layerSize, boolean softmaxOnTop) {
List<Layer> layers = new ArrayList<>((layerSize.length - 1) * 2);
for (int i = 0; i < layerSize.length - 1; i++) {
layers.add(new AffineLayer(layerSize[i], layerSize[i + 1]));
if (i == layerSize.length - 2) {
if (softmaxOnTop) {
layers.add(new SoftmaxLayerWithCrossEntropyLoss());
} else {
layers.add(new SigmoidLayerWithSquaredError());
}
} else {
layers.add(new FuntionalLayer(new SigmoidFunction()));
}
}
return new FeedForwardTopology(layers);
}
回顧下概念:前饋神經網絡被稱作網絡 (network) 是因為它們通常用許多不同函數復合在一起來表示。該模型與一個有向無環圖相關聯,圖描述了函數是如何復合在一起的。
各神經元從輸入層開始,接收前一級輸入,並輸出到下一級,直至輸出層。整個網絡中無反饋。其中每一層包含若干個神經元,同一層的神經元之間沒有互相連接,層間信息的傳送只沿一個方向進行。其中第一層稱為輸入層。最后一層為輸出層.中間為隱含層,簡稱隱層。隱層可以是一層。也可以是多層。
FeedForwardTopology 是前饋神經網絡的拓撲結構,即上述網絡層的邏輯展示。這個拓撲里面包含了從隱藏層到輸出層的若干層。
/**
* The topology of a feed forward neural network.
*/
public class FeedForwardTopology extends Topology {
/**
* All layers of the topology.
*/
private List<Layer> layers;
}
構建出的拓撲變量大致如下,分為四個層:
- 仿射層
AffineLayer
。仿射變換 = 線性變換 + 平移,即h = WX + b
; - 功能層
FuntionalLayer
,其函數為SigmoidFunction
,其為前一個仿射層對應的激活層; - 仿射層
AffineLayer
; - 輸出層
SoftmaxLayerWithCrossEntropyLoss
;
這里仿射層和功能層一起構成了隱藏單元。大多數的隱藏單元可以描述為接受輸入向量x,計算仿射變換 z = wTx+b
,然后使用一個逐元素的非線性函數g(z)。大多數隱藏單元的區別僅僅在於激活函數 g(z) 的形式。
現在把程序運行時具體變量打印出來讓大家更有清晰認識。可以看出來,根據示例代碼設定的神經網絡參數 .setLayers(new int[]{4, 5, 3})
,這里的各個層也做了相應設置 : 4,5,3。
this = {FeedForwardTopology@4951}
layers = {ArrayList@4944} size = 4
0 = {AffineLayer@4947} // 仿射層
numIn = 4
numOut = 5
1 = {FuntionalLayer@4948}
activationFunction = {SigmoidFunction@4953} // 激活函數
2 = {AffineLayer@4949} // 仿射層
numIn = 5
numOut = 3
3 = {SoftmaxLayerWithCrossEntropyLoss@4950} // 激活函數
3.4.1 AffineLayer
是 y=A*x+b
的表示,即仿射層的各種配置信息,Layer properties of affine transformations。
public class AffineLayer extends Layer {
public int numIn;
public int numOut;
public AffineLayer(int numIn, int numOut) {
this.numIn = numIn;
this.numOut = numOut;
}
@Override
public LayerModel createModel() {
return new AffineLayerModel(this);
}
...
}
3.4.2 FuntionalLayer
是 y = f(x)
的表示。這里的 activationFunction
就是 f(x)
public class FuntionalLayer extends Layer {
public ActivationFunction activationFunction;
@Override
public LayerModel createModel() {
return new FuntionalLayerModel(this);
}
}
3.4.3 SoftmaxLayerWithCrossEntropyLoss
3.4.3.1 Softmax
輸出函數基本都使用Softmax 函數,其定義如下:
softmax的輸出向量就是概率,是該樣本屬於各個類的概率!它在 Logistic Regression 里其到的作用是講線性預測值轉化為類別概率。
假設 z_i = W_i + b_i
是第 i 個類別的線性預測結果,帶入 Softmax 的結果其實就是先對每一個z_i 取 exponential 變成非負,然后除以所有項之和進行歸一化,現在每個 σ_i = σ_i(z) 就可以解釋成觀察到的數據 x 屬於類別 i 的概率,或者稱作似然 (Likelihood)。
因此我們訓練全連接層的W的目標就是使得其輸出的 W.X 在經過 softmax 層計算后其對應於真實標簽的預測概率要最高。
3.4.3.2 softmax loss
弄懂了softmax,就要來說說softmax loss了。那softmax loss是什么意思呢??具體如下:
- L是損失。
- Sj是softmax的輸出向量S的第j個值,表示的是這個樣本屬於第j個類別的概率。
- yj前面有個求和符號,j的范圍也是1到類別數T,因此 y 是一個1*T的向量,里面的T個值只有1個值是1,其他T-1個值都是0。那么哪個位置的值是1呢?答案是真實標簽對應的位置的那個值是1,其他都是0。
所以這個公式其實有一個更簡單的形式:
當然此時要限定 j 是指向當前樣本的真實標簽。
3.4.3.3 cross entropy
理清了softmax loss,就可以來看看cross entropy了。corss entropy是交叉熵的意思,它的公式如下:
大多數現代的神經網絡使用最大似然來訓練。這意味着代價函數就是負的對數似然,它與訓練數據和模型分布間的交叉熵等價。代價函數的具體形式隨着模型而改變。
在信息論中,交叉熵是表示兩個概率分布p,q,其中p表示真實分布,q表示非真實分布,在相同的一組事件中,其中用非真實分布q來表示某個事件發生所需要的平均比特數。交叉熵可在神經網絡(機器學習)中作為損失函數,p表示真實標記的分布,q則為訓練后的模型的預測標記分布,交叉熵損失函數可以衡量p與q的相似性。
是不是覺得和softmax loss的公式很像。當cross entropy的輸入P是softmax的輸出時,cross entropy等於softmax loss。Pj是輸入的概率向量P的第j個值,所以如果你的概率是通過softmax公式得到的,那么cross entropy就是softmax loss
使用最大似然來導出代價函數的方法的一個優勢是,它減輕了為每個模型設計代價函數的負擔。明確一個模型p(y|x)則自動地確定了一個代價函數logp(y|x)。代價函數的梯度必須足夠的大和具有足夠的預測性,來為學習算法提供一個好的指引。
3.4.3.4 SoftmaxLayerWithCrossEntropyLoss
SoftmaxLayerWithCrossEntropyLoss
是 a softmax layer with cross entropy loss,即帶交叉熵損失的softmax層。
public class SoftmaxLayerWithCrossEntropyLoss extends Layer {
@Override
public LayerModel createModel() {
return new SoftmaxLayerModelWithCrossEntropyLoss();
}
}
3.5 構建訓練器
回憶示例代碼
.setLayers(new int[]{4, 5, 3})
這里指定了神經網絡的結構。輸入層是 4個,隱藏層是 5,輸出層是 3。
生成訓練器的代碼如下:
FeedForwardTrainer trainer = new FeedForwardTrainer(topology,
layerSize[0], layerSize[layerSize.length - 1], true, blockSize,
initialWeights);
FeedForwardTrainer 是前饋神經網絡的訓練器。
public class FeedForwardTrainer implements Serializable {
private Topology topology;
private int inputSize;
private int outputSize;
private int blockSize; // 數據分塊大小,默認值64,在壓縮時候被stack函數調用到
private boolean onehotLabel;
private DenseVector initialWeights;
}
變量打印如下
trainer = {FeedForwardTrainer@6456}
topology = {FeedForwardTopology@6455}
layers = {ArrayList@4963} size = 4
0 = {AffineLayer@6461}
1 = {FuntionalLayer@6462}
2 = {AffineLayer@6463}
3 = {SoftmaxLayerWithCrossEntropyLoss@6464}
inputSize = 4
outputSize = 3
blockSize = 64
onehotLabel = true
initialWeights = null
我們可以看到,訓練的核心變量是 FeedForwardTrainer,其包含了拓撲模型topology,而topology包含了四層layers。
我們提前把訓練器使用的優化器和目標函數也一起展示出來。訓練器使用優化器來優化目標函數。
這里優化器是Lbfgs,其包含的目標函數是 AnnObjFunc,包含拓撲和拓撲模型。
public class AnnObjFunc extends OptimObjFunc {
private Topology topology;
private transient TopologyModel topologyModel = null;
}
拓撲模型是依據拓撲生成的,這里是 FeedForwardModel,其中各層對應的模型是AffineLayerModel,FuntionalLayerModel等。
各層模型的作用就是計算損失,梯度等,比如 AffineLayerModel.eval 就是簡單的仿射變換 WX + b。
至此,多層感知機第一部分完成。敬請期待后文。
0xFF 參考
https://github.com/fengbingchun/NN_Test
[深度學習] [梯度下降]用代碼一步步理解梯度下降和神經網絡(ANN))