Alink漫談(十四) :多層感知機 之 總體架構

羅西的思考發表於2020-07-26

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 梯度下降

使用迭代法,那麼隨之而來另外一個問題:這樣一個一個嘗試,雖然最終結果是一定會找到我們所需要的值,但有沒有什麼方法可以讓它離“極值”遠的時候,挪動的步子更大,離“極值”近的時候,挪動的步子變小(防止越過極值),實現更快更準確地“收斂”。假如是一個“二次函式”的影像,那麼如果取得點越接近“極小值”,在這個點的函式“偏導”越小(偏導即“在那個點的函式斜率”)。接下來引出下面這個方法:

\[x_{n+1} = x_n - η\frac{df(x)}{dx} \]

梯度下降核心思想:Xn代表的就是挪動的“步長”,後面的部分表示當前這個點在函式的“偏導”,這樣也就代表當點越接近極值點,那麼“偏導”越小,所以挪動的“步長”就短;反之如果離極值點很遠,則下一次挪動的“步長”越大。

把這個公式換到我們的演算法模型,就找到了“挪動步長”與Loss和(w,b)之間的關係,實現快速“收斂”。

通過“迭代法”和“梯度下降法”的配合,我們實現了一輪一輪地迭代,每次更新都會越來越接近極值點,直到更新的值非常小或已經滿足我們的誤差範圍內,訓練結束,此時得到的(w,b)就是我們尋找的模型。

1.5 相關公式

以下是相關各種公式,摘錄出來給大家在閱讀時查閱。

1.5.1 加權求和 h

\[h_j = \sum_{i=0}^M w_{ij}x_j \]

hj 表示當前節點的所有輸入加權之和。

1.5.2 神經元輸出值 a

\[a_j = g(h_j) = g(\sum_{i=0}^M w_{ij}x_j) \]
  • a_j 表示隱藏層神經元的輸出值。
  • g()代表啟用函式,w是權重,x是輸入。
  • a_j=x_jk 即當前層神經元的輸出值,等於下一層神經元的輸入值。

1.5.3 輸出層的輸出值 y

\[y = a_k = g(h_k) = g(\sum_{i=0}^M w_{jk}x_{jk}) \]
  • y 表示輸出層的值,也就是最終結果。
  • h_k 表示輸出層神經元k的輸入加權之和。

1.5.4 啟用函式g(h)

採用Sigmoid function:

\[g(h)=σ(h)= \frac{1}{1+e^{-h}} \]

sigmoid函式的導數:

\[σ ′(x)=σ(x)[1−σ(x)] \]

將 aj=g(hj) 代入可得

\[g ′ (h)=a_j (1−a_j ) \]

1.5.5 損失函式E

採用誤差平方和(sum-of-squares error function)

\[E = \frac{1}{2}\sum _{k=1}^N(y-t)^2 \]
  • 平方是為了避免超平面兩端的誤差點相互抵消(y−t 存在正負)。
  • 前面係數取1/2 是為了之後採用梯度下降時,求梯度(偏導數)時能抵消平方求導後的2。

1.5.6 誤差反向傳播——更新權重

採用梯度下降求最優解,也就是求損失函式E關於權重w的偏導數

\[\frac{∂E}{∂w_{ik}} = \frac{∂E}{∂h_k} \frac{∂h_k}{∂w_{ik}} \]

等式右邊可以解釋為:如果我們想知道當權重w改變時,輸出的誤差E是如何變化的,我們可以通過觀察誤差E是如何隨著啟用函式的輸入值h變化,以及啟用函式的輸入值h是如何隨著權重w變化。

h_k表示輸出層神經元k的所有輸入加權之和,也就是啟用函式g(h)的輸入值。

1.5.7 輸出層增量項 δo

右邊第一項比較重要,這裡稱為增量項δ(error or delta term),繼續通過鏈式法則推導,最終得到輸出層的增量項

\[δ_o(k) = \frac{∂E}{∂h_{k}} = \frac{∂E}{∂y} \frac{∂y}{∂h_k} = (y−t)g ′(h_ k ) \]

接下來可以對輸出層的權重w進行更新。

1.5.8 更新輸出層權重wjk

對損失函式使用梯度下降法,更新權重:

\[w_{jk} ←w _{jk} −η \frac{∂E}{∂w_{jk}} \]

於是得到

\[w_{jk} = w_{jk} - ηδ_o(k)a_i \]

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
  • 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

CalDirection 計算方向

  • 這裡沒有用到目標函式的拓撲模型。

CalcLosses 計算損失

  • 1)呼叫 AnnObjFunc.calcSearchValues; 其內部會呼叫 calcLoss 計算損失;
    • 1.1)呼叫 topologyModel.computeGradient 來計算損失
      • 1.1.1)計算各層的輸出;forward(data, true)
      • 1.1.2)計算輸出層損失;labelWithError.loss

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 函式,其定義如下:

\[σ_i(Z) = \frac{exp(Z_i)}{\sum_{j=1}^m exp(z_j)}, i = 1,...,m \]

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 = - \sum_{j=1}^T y_j logS_j \]
  • L是損失。
  • Sj是softmax的輸出向量S的第j個值,表示的是這個樣本屬於第j個類別的概率。
  • yj前面有個求和符號,j的範圍也是1到類別數T,因此 y 是一個1*T的向量,裡面的T個值只有1個值是1,其他T-1個值都是0。那麼哪個位置的值是1呢?答案是真實標籤對應的位置的那個值是1,其他都是0。

所以這個公式其實有一個更簡單的形式:

\[L = -logS_j \]

當然此時要限定 j 是指向當前樣本的真實標籤。

3.4.3.3 cross entropy

理清了softmax loss,就可以來看看cross entropy了。corss entropy是交叉熵的意思,它的公式如下:

\[E = - \sum_{j=1}^T y_j logP_j \]

大多數現代的神經網路使用最大似然來訓練。這意味著代價函式就是負的對數似然,它與訓練資料和模型分佈間的交叉熵等價。代價函式的具體形式隨著模型而改變

在資訊理論中,交叉熵是表示兩個概率分佈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

SoftmaxLayerWithCrossEntropyLossa 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 參考

深度學習中的深度前饋網路簡介

Deep Learning 中文翻譯

https://github.com/fengbingchun/NN_Test

深度學習入門——Affine層(仿射層-矩陣乘積)

機器學習——多層感知機MLP的相關公式

多層感知器速成

神經網路(多層感知器)信用卡欺詐檢測(一)

手擼ANN之——損失層

【機器學習】人工神經網路ANN

人工神經網路(ANN)的公式推導

[深度學習] [梯度下降]用程式碼一步步理解梯度下降和神經網路(ANN))

softmax和softmax loss詳細解析

Softmax損失函式及梯度的計算

Softmax vs. Softmax-Loss: Numerical Stability

【技術綜述】一文道盡softmax loss及其變種

前饋神經網路入門:為什麼它很重要?

深度學習基礎理解:以前饋神經網路為例

監督學習與迴歸模型

機器學習——前饋神經網路

AI產品:BP前饋神經網路與梯度問題

深度學習之前饋神經網路(前向傳播和誤差反向傳播)

一文搞懂反向傳播演算法

相關文章