1. 程式人生 > 其它 >從零開始開發自己的類keras深度學習框架2 :實現全連線層

從零開始開發自己的類keras深度學習框架2 :實現全連線層

技術標籤:深度學習神經網路神經網路深度學習

認真學習,佛系更博。

上一章簡單介紹瞭如何實現資料的讀取功能,本章將詳細介紹如何實現神經網路最基礎的層:全連線層。

全連線層的原理想必很多讀者都接觸過很多資料,比如鏈式法則,反向傳播,梯度下降法等等。說來慚愧,博主也早早地接觸過,確一直沒有仔細推敲其中的原理,以至於一直對該網路層困惑了很久,其實靜下心來仔細去研究一下,會發現內部原理也很簡單直觀,我們先來了解一下鏈式法則:

我們知道,神經網路的很多操作都可以當作一個個獨立的層,比如卷積層、全連線層、sigmoid啟用層等,其原因在於,這些操作都可以當作一系列對應的函式對映,比如全連線層可以表示為:

y=X*W+b

我們進行一下封裝,表示為y=f(x),其中f表示矩陣運算,如果在後面再接一個啟用層,比如sigmoid,可以表示為:

y=sigmoid(X*W+b)

可以封裝為y=g(f(x)),其中g表示sigmoid運算;

前向運算很好理解,直接帶入x就可以計算到y,那麼反向傳播該怎樣計算呢?

我們先來了解一個叫gradient_check的思想,對於每個網路層,其引數(W,b)的梯度定義為結果誤差變化相對於變數變化的程度,比如某一層的W,給予其一個極小的變化,誤差函式發生變化的值就是該W的梯度,基於該思想,我們可以對網路模型的每個引數依次施加微小的變化,從而計算每個引數的梯度,最後運用梯度下降法更新引數。

上訴方法被驗證為一種可行的方法,但也存在明顯的缺點,比如效率極低,因此,引出了鏈式法則的思想:

我們知道,反向傳播就是求y對x的導數,複合函式求導滿足下圖(截自百度知道),這是在高中或大學的知識:

神經網路層就相當於上面的f、p、g操作,若我們想對某一層的引數進行求導,則可以利用該求導公式,該計算也被稱為鏈式法則;

於是乎,我們可以想象,在反向傳播過程中,對於某一個全連線層,先接收來自上層的梯度,然後對本層引數進行求導,最後計算針對輸入資料的梯度,作為前一層的更新梯度,這便是全連線層(卷積層同理)的引數更新過程;

我們先定義一個父類網路層,實現一些基本操作,在enet下新建一個新的模組layers,並新建檔案bas_layer.py:

import numpy as np


class Layer(object):
    """
    基礎網路層
    """

    def __init__(self, layer_type="layer"):

        self.input_shape = None
        self.output_shape = None
        self.weight_shape = None
        self.layer_type = layer_type
        self.activation = None
        self.name = None

        self.cache = None

    @staticmethod
    def add_weight(shape=None,
                   dtype=np.float,
                   initializer="normal",
                   node_num=None):
        """
        初始化網路引數
        :param node_num: 上一層神經網路節點的數量
        :param shape: 引數的shape
        :param dtype: 引數的dtype
        :param initializer: 初始化方法
        :return: 初始化後的資料
        """

        if initializer == "zero":
            return np.zeros(shape=shape, dtype=dtype)
        if initializer == "normal":
            return np.random.normal(size=shape) * np.sqrt(1 / np.prod(node_num))
            # return np.random.normal(size=shape)

        raise TypeError("initializer must be normal or zero")

    def build(self, *args, **k_args):
        """
        編譯網路層
        :param k_args:
        :return:
        """
        pass

    def forward(self, *args, **k_args):
        """
        前向運算
        :param k_args:
        :return:
        """
        pass

    def backward(self, *args, **k_args):
        """
        反向傳播,只計算梯度而不更新引數
        :param k_args:
        :return:
        """
        pass

    def update(self, *args, **k_args):
        """
        更新引數
        :param k_args:
        :return:
        """
        pass

    def get_input_shape(self):
        """
        獲取網路輸入形狀
        :return:
        """
        return self.input_shape

    def get_output_shape(self):
        """
        獲取網路輸出形狀
        :return:
        """
        return self.output_shape

    def get_layer_type(self):
        """
        獲取網路型別
        :return:
        """
        return self.layer_type

    def get_activation_layer(self):
        """
        獲取啟用函式型別
        :return:
        """
        return self.activation

    def get_weight_shape(self):
        """
        獲取權重形狀
        :return:
        """
        return self.weight_shape

    def get_name(self):
        """
        獲取網路層名
        :return:
        """
        return self.name

    def set_name(self, name):
        """
        設定網路層名字
        :param name: 名字
        :return:
        """
        if not self.name:
            self.name = name

定義了一些公公變數,這些變數在以後會用到,另外添加了一個add_weight方法,該方法用於初始化網路的引數;

然後新建dense.py檔案,dense的實現稍微複雜,因為我們要考慮神經單元的個數,是否使用啟用函式等;另外,我們這裡做一下說明,一般的神經網路框架都單獨把優化器提出來作為統一的控制,但是神經網路的每層引數都可以使用不同的優化方法,adam、momentum,因此,我們為每個層建立一個優化控制器;

下面為dense的程式碼,將在下面做詳細說明:

from enet.layers.base_layer import Layer

import numpy as np

from enet.optimizer import optimizer_dict


class Dense(Layer):
    """
    全連線神經網路類
    """

    def __init__(self, kernel_size=None, activation=None, input_shape=None, optimizer="sgd", name=None, **k_args):
        """
        :param kernel_size: 神經元個數
        :param activation: 啟用函式
        :param input_shape: 輸入shape,只在輸入層有效;
        :param optimizer: 優化器;
        :param name: 網路層名字;
        """
        super(Dense, self).__init__(layer_type="dense")

        assert activation in {None, "sigmoid", "relu", "softmax"}
        assert optimizer in {"sgd", "momentum", "adagrad", "adam", "rmsprop"}

        self.output_shape = kernel_size
        self.activation = activation

        self.name = name

        # 該處的input_shape只在輸入層有效,input_shape樣式為(784,)
        if input_shape:
            self.input_shape = input_shape[0]

        self.weight = None
        self.bias = None

        # self.use_bias = use_bias
        self.optimizer = optimizer_dict[optimizer](**k_args)

    def build(self, input_shape):
        """
        根據input_shape來構建網路模型引數
        :param input_shape: 輸入形狀
        :return: 無返回值
        """

        last_dim = input_shape
        self.input_shape = input_shape

        shape = (last_dim, self.output_shape)
        self.weight_shape = shape

        self.weight = self.add_weight(shape=shape, initializer="normal", node_num=input_shape)
        self.bias = self.add_weight(shape=(self.output_shape,), initializer="zero")

    def forward(self, input_signal, *args, **k_args):
        """
        前向傳播
        :param input_signal: 輸入資訊
        :return: 輸出訊號
        """
        self.cache = input_signal

        return np.dot(input_signal, self.weight) + self.bias

    def backward(self, delta):
        """
        反向傳播
        :param delta: 輸入梯度
        :return: 誤差回傳
        """

        # if self.use_bias:
        #     delta_b = np.mean(delta, axis=0)
        # else:
        #     delta_b = 0
        delta_b = np.sum(delta, axis=0)
        delta_w = np.dot(self.cache.transpose(), delta)

        self.optimizer.grand(delta_w=delta_w, delta_b=delta_b)

        # 回傳給前一層的梯度
        return np.dot(delta, self.weight.transpose())

    def update(self, lr):
        """
        更新引數
        :param lr: 學習率
        :return:
        """
        delta_w, delta_b = self.optimizer.get_delta_and_reset(lr, "delta_w", "delta_b")

        self.weight += delta_w
        self.bias += delta_b



首先在初始化階段,我們定義了關鍵引數,kernel_size,該引數表示該層神經元的個數,另外定義了優化器,啟用函式等,這部分程式碼看不懂的不要著急,在後面實現優化器部分將詳細介紹;

build函式完成了函式的初始化,將在網路模型的compile函式進行呼叫,我們需要關注的是forward和backward函式,實現了網路層的前向傳播和反向傳播;

forward容易理解,直接用y=W*X+b即可計算結果,難點在於backward,我們先來弄清楚矩陣的反向傳播公式;

假設矩陣運算為:

y=W*X+b

上層關於y的求導為\frac{\partial L}{\partial y},則關於X,W, b的求導為:

\frac{\partial y}{\partial b} = \frac{\partial L}{\partial y}\\ \frac{\partial y}{\partial W} = X^{T}*\frac{\partial L}{\partial y}\\ \frac{\partial y}{\partial X} = \frac{\partial L}{\partial y}*W^{T}

關於公式的推導,這裡不做過多證明,可以直接拿來用,感興趣的可以自己展開矩陣推導下,過程並不難,或者用幾個小矩陣模擬一下,很容易得出答案;

於是乎,我們對於權重和偏執的求導便可以寫成如下關鍵部分:

        delta_b = np.sum(delta, axis=0)
        delta_w = np.dot(self.cache.transpose(), delta)

注意,這裡使用sum而不是mean,原因是我們計算誤差梯度時已經做了平均的操作,下一章將會介紹;

回傳給上一層的梯度為:

        # 回傳給前一層的梯度
        return np.dot(delta, self.weight.transpose())

然後更新引數,回傳梯度即可;

好了,本篇文章先寫到這裡,下一篇將介紹如何實現優化器類,以及啟用函式的實現;

整個程式碼的github網址為:https://github.com/darkwhale/neural_network,不斷更新中;