機器學習之神經網路識別手寫數字(純python實現)

swensun發表於2019-03-03

最近在學習機器學習有關神經網路的部分,其中有兩份很好的參考資料,連結如下:
Neural Networks and Deep Learning
colah.github.io
前者是關於神經網路和深度學習的一份簡單的入門資料,後者是關於神經網路的一系列文章。
前者涉及到神經網路基本概念及其公式的介紹, 搭配手寫數字識別的演算法,對初學者非常友好。
不過我建議在閱讀之前,先看一下更適合入門的視訊,連結如下:
深度學習入門

這篇文章我打算在作者已有python2演算法的基礎上,對其進行python3的轉換,同時做出自己的理解,算是加深印象。

神經網路基本結構

class Network(object):
    def __init__(self, sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]
複製程式碼

如上,sizes舉例如下,[2, 3, 1]。表示該神經網路有3層,分別為輸入層,隱藏層和輸出層。
對於該資料集來說,每張圖片為28 * 28畫素,共計784個畫素,輸出為10個神經元,取其中最大值作為預測數字。
因此 net = Network([784, 30, 10])表示該神經網路有3層,輸入層有784個神經元,輸出層有10個神經元。

資料載入

def load_data():
    f = gzip.open(`./data/mnist.pkl.gz`, `rb`)
    training_data, validation_data, test_data = pickle.load(f, encoding=`latin1`)
    f.close()
    return (training_data, validation_data, test_data)
複製程式碼

從檔案中讀取該資料集進行處理。

def load_data_wrapper():
    tr_d, va_d, te_d = load_data()
    training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
    training_results = [vectorized_result(y) for y in tr_d[1]]
    training_data = zip(training_inputs, training_results)
    validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
    validation_data = zip(validation_inputs, va_d[1])
    test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
    test_data = zip(test_inputs, te_d[1])
    return (training_data, validation_data, test_data)
複製程式碼

經過以上兩步處理,training_data包含資料50000組資料,每組資料包含向量分別如下:
前者是784個畫素點的灰度值,後者包含10個值,對應該圖片的正確分類值。

//表示分類為5.
       [ 0.],
       [ 0.],
       [ 0.],
       [ 0.],
       [ 0.],
       [ 1.],
       [ 0.],
       [ 0.],
       [ 0.],
       [ 0.]
複製程式碼

利用如下方法可以將矩陣轉為圖片檢視:

    c = tr_d[0][0]
    s = np.reshape(c, (784, 1))
    img = s.reshape((28, 28))
    new_im = Image.fromarray(img)
    # print(s)
    new_im.show()
複製程式碼

以上,資料準備完畢。

基本的概念,比如sigmoid啟用函式,隨機梯度下降,前向傳播演算法和反向傳播演算法,程式碼中會有實現,有疑問可以參考上述資料。

隨機梯度下降

    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        """
        desc: 隨機梯度下降
        :param training_data: list of tuples (x,y)
        :param epochs: 訓練次數
        :param mini_batch_size: 隨機的最小集合
        :param eta: learning rate: 學習速率
        :param test_data: 測試資料,有的話會評估演算法,但會降低執行速度
        :return:
        """
        if test_data:
            test_data = list(test_data)
            n_test = len(test_data)
        training_data = list(training_data)
        n = len(training_data)
        for j in range(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k: k + mini_batch_size]
                for k in range(0, n, mini_batch_size)
            ]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print("Epoch {}: {} / {}".format(
                    j, self.evaluate(test_data), n_test))
            else:
                print("Epoch {} complete".format(j))
複製程式碼

引數的解釋如上。
對於每一次迭代,首先打亂資料集,根據隨機梯度下降給定的最小batch資料集,將訓練資料分開進行。對於每一個batch資料集,用學習速率進行更新。如果有測試集,則評估演算法的準確性。評估演算法如下:

    def evaluate(self, test_data):
        """
        評估測試集的準確性
        :param test_data:
        :return:
        """
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)
        
    def feedforward(self, a):
        """return the output of the network if "a" is input"""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a) + b)
        return a
複製程式碼

對於測試資料集,feedforward函式根據隨機梯度下降中更新得到的biases, weights計算10個值的預測輸出。np.argmax()函式得到10個值中的最大值,即使預測的輸出值。
判斷是否相等並統計,看共計多少個預測準確。
如上,讓我們忽略update_mini_batch()函式

if __name__ == `__main__`:
    training_data, validation_data, test_data = 
        mnist_loader.load_data_wrapper()
    net = Network([784, 30, 10])
    net.SGD(training_data, 30, 10, 3.0, test_data)
複製程式碼

初始化神經網路,建立3層,輸入層784個神經元,隱藏層30個神經元,輸出層10個神經元。
利用隨機梯度下降,迭代30次,隨機下降的資料集為10個,學習速率為3.0。針對測試集的結果如下:

Epoch 0: 9080 / 10000
Epoch 1: 9185 / 10000
Epoch 2: 9327 / 10000
Epoch 3: 9348 / 10000
Epoch 4: 9386 / 10000
Epoch 5: 9399 / 10000
Epoch 6: 9391 / 10000
Epoch 7: 9446 / 10000
Epoch 8: 9427 / 10000
Epoch 9: 9478 / 10000
Epoch 10: 9467 / 10000
Epoch 11: 9457 / 10000
Epoch 12: 9453 / 10000
Epoch 13: 9440 / 10000
Epoch 14: 9452 / 10000
Epoch 15: 9482 / 10000
Epoch 16: 9470 / 10000
Epoch 17: 9483 / 10000
Epoch 18: 9488 / 10000
Epoch 19: 9484 / 10000
Epoch 20: 9476 / 10000
Epoch 21: 9496 / 10000
Epoch 22: 9469 / 10000
Epoch 23: 9503 / 10000
Epoch 24: 9495 / 10000
Epoch 25: 9499 / 10000
Epoch 26: 9510 / 10000
Epoch 27: 9495 / 10000
Epoch 28: 9487 / 10000
Epoch 29: 9478 / 10000
複製程式碼

可以看到隨著訓練次數的增加,模型的準確率在不斷提高。

隨機梯度下降更新biases和weights

    def update_mini_batch(self, mini_batch, eta):
        """
        梯度下降更新weights和biases, 用到backpropagation反向傳播。
        :param mini_batch:
        :param eta:
        :return:
        """
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]

        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]

        self.weights = [w - (eta / len(mini_batch)) * nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b - (eta / len(mini_batch)) * nb
                       for b, nb in zip(self.biases, nabla_b)]
複製程式碼

首先初始化與biases和weights相同大小的矩陣。
對於每一個nimi_batch的x(畫素矩陣, 784), y(預測矩陣, 10),利用反向傳播演算法計算
delta_nabla_b, delta_nabla_w = self.backprop(x, y)得到每一次的梯度,相加得到mini_batch的梯度,進行權重和偏置項更新。

反向傳播演算法

    def backprop(self, x, y):
        """
        :param x:
        :param y:
        :return: (nabla_b, nabla_w): gradient for 損失函式,類似於biaes, weight。
        """
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x]  # 儲存所有啟用值
        zs = []  # 儲存所有的z向量
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)

        # backward pass
        delta = self.cost_derivative(activations[-1], y) * 
                sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())

        for l in range(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
        return (nabla_b, nabla_w)

    def cost_derivative(self, output_activations, y):
        """
        :param output_activations:
        :param y:
        :return: 給定輸出激發。
        """
        return (output_activations - y)
複製程式碼

上面就是最重要也就是最複雜的反向傳播演算法。該演算法mini_bitch中的向量為引數,
首先也是初始化與biases和weights相同大小的矩陣,並儲存所有的啟用值。

 for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
複製程式碼

上述函式對biases和weights進行運算,通過sigmoid函式得到啟用值。
接著求出最後一層的引數。

        for l in range(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
複製程式碼

該函式從倒數第二層開始,迭代分別求出每一層梯度值,返回更新梯度。

至此演算法的全部程式碼完成。
完整程式碼請檢視:
github: code

總結:

  • python3
  • 神經網路入門
  • 隨機梯度下降
  • 反向傳播演算法

todo:
利用這個思想去看kaggle上的手寫數字識別的題目。

相關文章