[學習筆記]神經網絡之一:簡單實現一個神經網絡

這幾天開始學習神經網絡,本帖爲我在讀完《Python神經網絡編程》後的一個總結,因爲我是神經網絡的初學者,當出現一些錯誤或者說法不當時,請多多指正。

本文的目的是使用二層神經網絡(輸入層、隱藏層和輸出層,輸入層一般只是負責輸入)來實現對手寫體數字的識別,這裏分別採用《Python神經網絡編程》中的代碼和Pytorch來實現。

訓練數據集:http://www.pjreddie.com/media/files/mnist_train.csv

測試數據集:http://www.pjreddie.com/media/files/mnist_test.csv

1.使用Python和numpy

第一段使用Python和numpy來實現一個二層的神經網絡。

1.1 神經網絡展示

最簡單的二層神經網絡的結構如下:

圖1-1 二層神經網絡(摘自《Python神經網絡編程》)

我所理解的神經網絡就類似於一個函數,給定一個輸入,然後神經網絡返回一個輸出,如此而已。

 

首先是輸入層,這一層不做其他事情,僅僅表示輸入信號,輸入節點不對輸入值應用激活函數

然後是隱藏層,第一層輸入值在經過組合後作爲隱藏層的輸入,然後經過激活函數後傳遞;

最後是輸出層,組合隱藏層的輸入,然後經過激活函數後輸出。

組合的意思就是按權重相加,可以理解爲表達式:

y = Ax + b

 A表示鏈接權重矩陣,b表示偏移值(常數,這裏爲0),這裏的組合方式就是pytorch中的nn.Linear()。

常用的激活函數有sigmoid、relu等,這裏使用的是sigmoid函數。


爲什麼要使用激活函數?

如果不用激活函數的話,多層神經網絡與單層神經網絡等同。


 其表達式如下:y = \frac{1}{1 + e^{-x}},值域爲(0, 1)

圖1-2 sigmoid函數

拿圖1-1舉例子,輸入層在經過表達式y=Ax後作爲隱藏層的輸入,隱藏層輸出的爲sigmoid(y)的值。

1.2 初始化函數

接下來就開始編寫python代碼了,本示例使用的是二層神經網絡,因此有兩個鏈接權重矩陣,而鏈接權重的初始化,則也有一些講究。大的初始權重會造成大的信號傳遞給激活函數,導致網絡飽和,因此應該避免過大的初始權重。數學家所得到的經驗規則是,我們可以在一個節點傳入鏈接數量平方根倒數的大致範圍內隨機採樣,初始化權重。

如果一個節點有三個輸入,那麼初始權重的範圍應該在-1\sqrt{3}+1\sqrt{3}之間,如此類推。這一經驗法則實際上講的是從均值爲0、標準方差等於節點傳入鏈接數量平方根倒數的正態分佈中進行採樣。另外,應該避免設置相同的權重或0,相同的權重會導致同等的權重更新,從而達不到更新權重矩陣的效果;權重爲0時同樣也使得網絡喪失了更新權重的能力

class NeuralNetwork(object):
    def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
        # 輸入層 隱藏層和輸出層的節點個數
        self.inodes = input_nodes
        self.hnodes = hidden_nodes
        self.onodes = output_nodes
        # learning rate
        self.lr = learning_rate
        # 創建輸入層和隱藏層的鏈接權重矩陣
        self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        # 隱藏層和輸出層的權重矩陣
        self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
        # 定義激活函數
        self.activation_func = lambda x: expit(x)

因爲有隱藏層和輸出層,所以有兩個鏈接權重矩陣。 

self.wih爲輸入層(input layer)和隱藏層(hidden layer)的鏈接權重矩陣;self.who爲隱藏層(hidden layer)和輸出層(output layer)的鏈接權重矩陣。

1.3 正向傳播

所謂的正向傳播,指的是給神經網絡輸入,然後得到它的輸出,此即爲正向傳播,在本示例中爲NeuralNetwork類的query函數:

    def query(self, inputs_list):
        hidden_inputs = np.dot(self.wih, inputs_list)
        hidden_outputs = self.activation_func(hidden_inputs)

        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_func(final_inputs)

        return final_outputs

 query函數中輸出層也調用了一次激活函數,因爲使用的激活函數爲sigmoid,所以輸出值的值域爲(0, 1),兩端取不到。

1.4 反向傳播

反向傳播的目的是爲了訓練,訓練神經網絡的目的就是根據訓練樣本來獲得一個“正確”的權重矩陣。通過把訓練樣本輸入到神經網絡得到輸出值,然後神經網絡輸出值和訓練樣本輸出值的差值來更新鏈接權重矩陣的值,進而達到學習的目的,此即爲反向傳播。

反向傳播的流程如下:

  1. 得到輸出層的誤差;
  2. 根據輸出層和相應的權重得到隱藏層的誤差;
  3. 根據學習效率(learning rate)和斜率對矩陣進行更新;
  4. 重複1~3。
    def train(self, inputs_list, targets_list):
        # 轉換到二維矩陣
        inputs = np.array(inputs_list, ndmin=2).T
        targets = np.array(targets_list, ndmin=2).T
        # 計算
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_func(hidden_inputs)

        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_func(final_inputs)
        # 計算誤差
        output_errors = targets - final_outputs
        hidden_errors = np.dot(self.who.T, output_errors)
        # 更新權重
        self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)),
                                     np.transpose(hidden_outputs))
        self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1 - hidden_outputs)),
                                     np.transpose(inputs))

 由於這些節點都不是簡單的線性分類器。這些稍微複雜的節點,對加權後的信號進行求和,並運用了sigmoid函數,將所得到的的輸出給下一層的節點。由於這裏使用到的激活函數爲sigmoid,因此更新權重的公式略微複雜。

數學家們發現了梯度下降(gradient descent),採用步進的方式接近答案。

圖1-3 梯度下降

步進時要避免超調,這也就是learning rate的作用。 

誤差E的值是目標值減去實際值,即E=t-o,而由於誤差不能相互抵消,因此應該去掉E的符號值;爲了避免超調,因此使用平方的形式:E = (t-a)^{2}。接着就需要對上面的值求偏導即可。

詳細參見《Python神經網絡編程》P96-P102

1.5 樣本訓練

if __name__ == '__main__':
    input_nodes = 784
    hidden_nodes = 100
    output_nodes = 10
    learning_rate = 0.1

    net = NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
    # 加載訓練數據 並 訓練
    train_data = pd.read_csv('mnist_train.csv', header=None)
    for i, series in train_data.iterrows():
        # 輸入轉爲[0.01, 1.0]
        scale_inputs = (np.array(series[1:]) / 255 * 0.99) + 0.01
        # 把第一行數據轉爲輸出
        targets = np.zeros(output_nodes) + 0.01
        targets[series[0]] = 0.99
        # 訓練
        net.train(scale_inputs, targets)
    print('訓練完成,正在測試...')
    # 使用測試數據
    test_data = pd.read_csv('mnist_test.csv', header=None)

    correct_count = 0
    for index, series in test_data.iterrows():
        correct_label = series[0]
        inputs = (np.array(series[1:]) / 255 * 0.99) + 0.01
        # 預測
        outputs = net.query(inputs)
        label = np.argmax(outputs)
        if label == correct_label:
            correct_count += 1
    print('正確率', correct_count / len(test_data))

 這裏要先說明幾點:

1.5.1 輸入

首先是輸入,數據集爲784維度,即28*28,每個值的大小是[0, 255],這裏限定輸入值的值域在[0.01, 1.0]之間。刻意選擇0.01作爲範圍的最低點,是爲了避免先前觀察到的0值輸入造成的權重更新失敗(0 * 鏈接權重=0);選擇1.0作爲輸入的上限,是因爲不需要避免輸入1.0造成這個問題,只需要避免輸出值爲1.0即可。

1.5.2 輸出

接着是輸出值,因爲目前的輸出值同樣也調用了sigmoid激活函數,而sigmoid是取不到1的,從圖1-2中可以看出,如果目標值爲1, 則會導致大的權重和飽和網絡,因此這裏使用了0.99代替了1.0。

1.5.3 神經網絡

最後則是神經網絡,輸入層是784個,是因爲訓練樣本有784個維度,輸出層爲10個,是因爲數字有0~9共10種情況:

圖1-4 輸出

 輸出層得到的長度爲10的數組,然後從中選出一個最大的值作爲標籤。

注:類似於這種分類,其實更應該使用softmax。

2.使用Pytorch

首先是導入包

"""
使用pytorch進行分類
"""
import os
import torch
import pandas as pd
from torch import nn
from torch.utils.data import DataLoader, Dataset
import torch.optim as optim

2.1 網絡結構

其實我個人覺得一層神經網絡就夠了,不過爲了統一,這裏仍然使用兩層神經網絡。使用類來組織:

class MnistNet(nn.Module):
    def __init__(self, input_nodes, hidden_nodes, output_nodes):
        super(MnistNet, self).__init__()
        self.hidden = nn.Linear(input_nodes, hidden_nodes, bias=False)
        self.output = nn.Linear(hidden_nodes, output_nodes, bias=False)

    def forward(self, x):
        y = torch.sigmoid(self.hidden(x))
        y = self.output(y)
        return y

 和第一部分有所不同的是,由於這裏主要用於分類,所以這裏的輸出並沒有使用激活函數(主要使用nn.CrossEntropyLoss類)。

2.2 獲取數據

使用pytorch的DataLoader和自定義的Dataset類來完成數據的獲取:

class MnistDataset(Dataset):
    def __init__(self, filename):
        self.df = pd.read_csv(filename, header=None)

    def __getitem__(self, idx):
        series = self.df.iloc[idx]
        features = (series[1:] / 255 * 0.99) + 0.01

        X = torch.tensor(features.to_numpy(), dtype=torch.float32)
        y = torch.tensor(series.iloc[0])

        return X, y

    def __len__(self):
        return len(self.df)

自定義的Dataset類必須要實現__getitem__和__len__方法。因爲在隱藏層使用了sigmoid函數,因此仍然需要輸入值在[0.01, 1]之間。

2.3 主函數

對於分類問題,一般可以使用softmax,在《動手學神經網絡》3.7.3節中,“分開定義softmax運算和交叉熵損失函數可能會造成數值 不不穩定。因此,PyTorch提供了了⼀一個包括softmax運算和交叉熵損失計算的函數”。綜上,這裏使用的交叉熵損失函數。優化同樣使用隨機梯度下降作爲優化算法:

if __name__ == '__main__':
    input_nodes = 784
    hidden_nodes = 100
    output_nodes = 10
    learning_rate = 0.1

    net = MnistNet(input_nodes, hidden_nodes, output_nodes)
    loss_func = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=learning_rate)

首先定義了網絡、損失函數和優化器。

    train_data = MnistDataset('mnist_train.csv')
    test_data = MnistDataset('mnist_test.csv')
    train_iter = DataLoader(train_data, batch_size=100, shuffle=True)
    test_iter = DataLoader(test_data, batch_size=100, shuffle=True)

 接着加載數據,把數據作爲迭代器,100爲一個批次,並隨機打亂數據。

    for epoch in range(10):
        train_loss_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            loss = loss_func(y_hat, y)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            train_loss_sum += loss.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' %
              (epoch + 1, train_loss_sum / n, train_acc_sum / n, test_acc))

 最後我們進行訓練,這裏訓練了10代,並在每一代結束的時候都測試準確度,測試結果如下:

可以看到,在訓練了第7代的時候,測試準確度已經達到了92%。 

3.參考文獻

  • 《Python神經網絡編程》
  • 《動手學深度學習》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章