基础神经网络学习笔记

我猜测可能会有很多人跟我一样,在学完cs231n后仍然对神经网络没有一个整体印象,所以这是一份为后人能更轻松地理解神经网络而编写的学习笔记,本文通过我自己的理解对一份神经网络代码进行解释,将所有的知识点串联起来,希望可以造福后人。

import numpy as np

def unpickle(file):
    import pickle
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
    return dict


def fetch_data():
    train_data = []
    train_label = []
    test_data = []
    testlabel = []
    for i in range(5):
        data = unpickle(
            # datasets\cifar-10-batches-py\data_batch_1
            f'E:/projects/datasets/cifar-10-batches-py/data_batch_{i+1}')
        train_data.append(data[b'data'])
        train_label.append(data[b'labels'])
    test = unpickle('E:/projects/datasets/cifar-10-batches-py/test_batch')
    test_data = test[b'data']
    testlabel = test[b'labels']
    train_data = np.concatenate(train_data)
    train_label = np.concatenate(train_label)
    train_data = train_data.reshape(
        (50000, 3, 32, 32)).transpose(0, 2, 3, 1).astype("float32")
    train_data = train_data.reshape(-1, 32*32*3)
    test_data = test_data.reshape((10000, 3, 32, 32)).transpose(
        0, 2, 3, 1).astype("float32")
    test_data = test_data.reshape(-1, 32*32*3)
    return train_data, train_label, test_data, testlabel


def preprocess_data(train_data, test_data):
    mean = np.mean(train_data, axis=0)
    train_data -= mean
    test_data -= mean
    return train_data, test_data


class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size, std=1e-4):
        self.params = {}
        self.params['W1'] = std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def relu(self, x):
        return np.maximum(0, x)

    def relu_backward(self, dout, cache):
        dx, x = dout, cache
        dx[x <= 0] = 0
        return dx

    def loss(self, X, y=None, reg=0.0):
        W1, b1 = self.params['W1'], self.params['b1']
        W2, b2 = self.params['W2'], self.params['b2']

        Hidden_layer = self.relu(np.dot(X, W1) + b1)
        scores = np.dot(Hidden_layer, W2) + b2

        if y is None:
            return scores

        loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) /
                              np.sum(np.exp(scores), axis=1)))
        loss /= len(X)
        loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2))

        grads = {}

        dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True)
        dscores[range(len(scores)), y] -= 1
        dscores /= len(X)
        grads['W2'] = np.dot(Hidden_layer.T, dscores) + reg*W2
        grads['b2'] = np.sum(dscores, axis=0)
        dhidden = np.dot(dscores, W2.T)
        dhidden_relu = self.relu_backward(dhidden, Hidden_layer)
        grads['W1'] = np.dot(X.T, dhidden_relu) + reg*W1
        grads['b1'] = np.sum(dhidden, axis=0)

        return loss, grads

    def train(self, X, y,
              learning_rate=1e-3, learning_rate_decay=0.95,
              reg=5e-6, num_iters=100,
              batch_size=200, verbose=False):
        num_train = X.shape[0]
        iterations_per_epoch = max(num_train // batch_size, 1)

        for it in range(num_iters):
            idx = np.random.choice(num_train, batch_size, replace=True)
            X_batch = X[idx]
            y_batch = y[idx]
            loss, grads = self.loss(X_batch, y=y_batch, reg=reg)

            self.params['W1'] -= learning_rate * grads['W1']
            self.params['b1'] -= learning_rate * grads['b1']
            self.params['W2'] -= learning_rate * grads['W2']
            self.params['b2'] -= learning_rate * grads['b2']

            # if verbose and it % 100 == 0:
            #     print('iteration %d / %d: loss %f' % (it, num_iters, loss))
            if it % iterations_per_epoch == 0 and it != 0:
                learning_rate *= learning_rate_decay

    def predict(self, X):
        Hidden_layer = self.relu(
            np.dot(X, self.params['W1']) + self.params['b1'])
        scores = np.dot(Hidden_layer, self.params['W2']) + self.params['b2']
        return np.argmax(scores, axis=1)


def main():
    train_data, train_label, test_data, testlabel = fetch_data()
    train_data, test_data = preprocess_data(train_data, test_data)
    net = NeuralNetwork(32*32*3, 100, 10)
    net.train(train_data, train_label, num_iters=10000,
              batch_size=200, verbose=True)
    print(f'train accuracy: {np.mean(net.predict(train_data) == train_label)}')
    return


if __name__ == '__main__':
    main()

这里是本文所使用的代码。(训练内容为CIFAR-10数据集)

  1. 首先,fetch_dataunpickle函数作为数据读入函数,在这里不做讲解,我们只需要知道 train_data,train_label,test_data,testlabel这四个都分别是什么即可,train_data是一个n*3072的矩阵,它存储了n个训练需要的图像数据,train_label是一个列表,有n项,存储了 train_data对应的答案,test_data,testlabel同理。

  2. 之后我们需要对数据进行预处理,我们来看 preprocess_data函数,这份代码中只采用了均值减法初始化,这是最为常见的初始化方式,且在这份代码中效果显著,当然我们仍然可以在这个函数中添加更多的初始化方式,比如归一化等等,但是我们需要注意数据的类别,防止产生负优化,比如在CIFAR-10数据集上,归一化会显著降低它的准确率。

  3. 现在我们马上就要进入最紧张刺激的神经网络环节了,但是在训练之前,我们仍然需要明确我们的神经网络格式,比如在这份代码中,我们使用的是一个两层全连接神经网络,所以我们有这个预处理函数

     def __init__(self, input_size, hidden_size, output_size, std=1e-4):
             self.params = {}
             self.params['W1'] = std * np.random.randn(input_size, hidden_size)
             self.params['b1'] = np.zeros(hidden_size)
             self.params['W2'] = std * np.random.randn(hidden_size, output_size)
             self.params['b2'] = np.zeros(output_size)
    

    这里,我们对每一层分别定义了它的权重W和偏量b,这四个参数是我们需要训练的主要参数,并且我们注意到,他们都是根据神经元数量来定义的矩阵,那么我现在来解释一下为什么是矩阵。我们来看这个初始化函数的传入参数 input_size, hidden_size, output_size, std=1e-4,std先不用管,前面三个分别是输入的格式,第一层格式,输出格式,我们完全可以简单理解为我们需要将一个有若干个数字的集合,通过一系列的数学运算,先压缩为 hidden_size的大小,最后再压缩为 output_size的大小,而这里的 output_size就是我们需要分类的10个标签。而这里对参数的初始化使用了随机数,因为全部为0是不正确的,因为我们后续还要对其进行求偏导以及计算梯度。

  4. 初始化之后,我们就可以进行训练了,而训练的目的是迭代计算出最好的参数,也就是神经网络初始化所定义的四个参数。

        def train(self, X, y,
                  learning_rate=1e-3, learning_rate_decay=0.95,
                  reg=5e-6, num_iters=100,
                  batch_size=200, verbose=False):
            num_train = X.shape[0]
            iterations_per_epoch = max(num_train // batch_size, 1)
    
            for it in range(num_iters):
                idx = np.random.choice(num_train, batch_size, replace=True)
                X_batch = X[idx]
                y_batch = y[idx]
                loss, grads = self.loss(X_batch, y=y_batch, reg=reg)
    
                self.params['W1'] -= learning_rate * grads['W1']
                self.params['b1'] -= learning_rate * grads['b1']
                self.params['W2'] -= learning_rate * grads['W2']
                self.params['b2'] -= learning_rate * grads['b2']
    
                # if verbose and it % 100 == 0:
                #     print('iteration %d / %d: loss %f' % (it, num_iters, loss))
                if it % iterations_per_epoch == 0 and it != 0:
                    learning_rate *= learning_rate_decay
    

    我们来看这个训练主函数,reg是我们定义的L2正则化超参数,我们直接来看迭代训练循环,我们每一次迭代随机抽取数据进行训练,用这份数据计算出四个参数的梯度,以此来更新我们的参数,使我们的参数不断优化。在这个函数中我们还使用了两个参数,learning_ratelearning_rate_decay,这两个参数用来不断降低模型的学习率,使参数的迭代越来越精确。

  5. 我们发现在训练迭代中使用了一个很重要的 loss函数来计算我们的参数梯度,这也是神经网络最重要的部分。

        def relu(self, x):
            return np.maximum(0, x)
    
        def relu_backward(self, dout, cache):
            dx, x = dout, cache
            dx[x <= 0] = 0
            return dx
    
        def loss(self, X, y=None, reg=0.0):
            W1, b1 = self.params['W1'], self.params['b1']
            W2, b2 = self.params['W2'], self.params['b2']
    
            Hidden_layer = self.relu(np.dot(X, W1) + b1)
            scores = np.dot(Hidden_layer, W2) + b2
    
            if y is None:
                return scores
    
            loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) /
                                  np.sum(np.exp(scores), axis=1)))
            loss /= len(X)
            loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2))
    
            grads = {}
    
            dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True)
            dscores[range(len(scores)), y] -= 1
            dscores /= len(X)
            grads['W2'] = np.dot(Hidden_layer.T, dscores) + reg*W2
            grads['b2'] = np.sum(dscores, axis=0)
            dhidden = np.dot(dscores, W2.T)
            dhidden_relu = self.relu_backward(dhidden, Hidden_layer)
            grads['W1'] = np.dot(X.T, dhidden_relu) + reg*W1
            grads['b1'] = np.sum(dhidden, axis=0)
    
            return loss, grads
    

    这里便是我们计算所需要的函数,我们来对 loss函数逐步分析。首先传入现有参数没有什么好说的,然后便是我们的正向传播环节,我们先用现有参数计算出10个类别所对应的得分,然后我们将这10个分数放进线性分类器里进行计算,最后得到的就是我们的损失 loss。之后便是我们喜闻乐见的反向传播环节,所谓反向传播,其实就是按照线性求导法则对两层神经网络的所有参数求偏导,以此找到loss下降最快的点,才能以此更新参数。26-28行都是在对softmax线性分类器求梯度,到了29行我们发现我们在运算时对矩阵进行了转置,通俗地讲,我们明白矩阵是不服从乘法交换律的,所以如果我们想让它正确地乘,便需要给他转个方向,这样才可以进行矩阵乘法。还有一点需要注意,在32行我们对激活函数进行了求导,因为我们在前向传播的时候在第一层使用了激活函数,所以这一步也必不可少。至此,我们便可以完整地计算出参数梯度,并以此迭代训练我们的四个主要参数。