多层人工神经网路从零开始的实作与MNIST资料集训练

简介

在本篇文章中,我们将实作一个简单的多层感知机(MLP)模型,并使用MNIST资料集来进行训练。MNIST资料集包含手写数字的影像,我们将其作为多类别分类问题来解决。此实作主要运用numpy进行数值运算,而避免使用像TensorFlow或PyTorch这类的深度学习框架。

程式码概述

我们将实作一个带有一层隐藏层的MLP模型,并透过反向传播来更新模型参数。具体过程分为以下几个步骤:

  • 初始化权重我们从随机初始化输入层、隐藏层、输出层的权重矩阵开始。隐藏层的大小由参数hidden_size决定。

  • 激活函数

    • 我们使用sigmoid函数作为隐藏层的激活函数,定义为:$$\\sigma(z) = \\frac{1}{1 + e^{-z}}$$

    • 输出层使用softmax函数来确保输出的数值符合概率的范畴:$$\\text{softmax}(z) = \\frac{e^{z_i}}{\\sum_j e^{z_j}}$$

  • 前向传播我们首先计算隐藏层的输入与激活值,接着计算输出层的激活值(即预测结果)。

  • 损失函数损失函数使用交叉熵损失,它计算了预测值和实际标籤之间的差异。具体公式如下:$$\\text{Loss} = - \\frac{1}{n} \\sum_{i=1}^{n} y_i \\log(\\hat{y_i})$$

  • 反向传播在反向传播中,我们透过链式法则计算损失相对于各层权重和偏差的导数,并更新权重矩阵。

  • 训练模型训练过程通过设定一个迭代次数(epoch)来进行,每次更新模型参数后,我们会计算当前的损失值。


  • 延伸说明

    在第11章中,我们详细探讨了多层人工神经网路(MLP)的概念,并逐步实作了反向传播演算法。以下是几个重点:

    • 前向传播与反向传播:我们在网路中依次将数据传递给每一层,并计算出输出。在每个节点中,使用激活函数来产生非线性的映射【20:11†source】。反向传播则通过计算损失函数的梯度,将误差分配给每个神经元,从而调整权重【20:19†source】。

    • MNIST资料集:MNIST资料集包含60,000张训练图像与10,000张测试图像,图像大小为28x28像素。我们将数据转换成784维向量进行训练【20:17†source】。


    程式码

    import numpy as np
    from sklearn.datasets import fetch_openml
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import OneHotEncoder

    def initialize_weights(input_size, hidden_size, output_size):
    W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1. / input_size)
    b1 = np.zeros((1, hidden_size))
    W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1. / hidden_size)
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

    def sigmoid(z):
    return 1 / (1 + np.exp(-z))

    def sigmoid_derivative(z):
    return sigmoid(z) * (1 - sigmoid(z))

    def softmax(z):
    exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) # For numerical stability
    return exp_z / np.sum(exp_z, axis=1, keepdims=True)

    def forward_pass(X, W1, b1, W2, b2):
    z1 = np.dot(X, W1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, W2) + b2
    a2 = softmax(z2)
    return z1, a1, z2, a2

    def compute_loss(y_true, y_pred):
    n_samples = y_true.shape[0]
    logp = - np.log(y_pred[range(n_samples), np.argmax(y_true, axis=1)])
    loss = np.sum(logp) / n_samples
    return loss

    def backward_pass(X, y_true, z1, a1, z2, a2, W1, W2):
    n_samples = X.shape[0]
    dz2 = a2 - y_true
    dW2 = np.dot(a1.T, dz2) / n_samples
    db2 = np.sum(dz2, axis=0, keepdims=True) / n_samples
    dz1 = np.dot(dz2, W2.T) * sigmoid_derivative(z1)
    dW1 = np.dot(X.T, dz1) / n_samples
    db1 = np.sum(dz1, axis=0, keepdims=True) / n_samples
    return dW1, db1, dW2, db2

    def update_weights(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    return W1, b1, W2, b2

    def train(X_train, y_train, hidden_size=64, epochs=10, learning_rate=0.1):
    input_size = X_train.shape[1]
    output_size = y_train.shape[1]
    W1, b1, W2, b2 = initialize_weights(input_size, hidden_size, output_size)
    for epoch in range(epochs):
    z1, a1, z2, a2 = forward_pass(X_train, W1, b1, W2, b2)
    loss = compute_loss(y_train, a2)
    dW1, db1, dW2, db2 = backward_pass(X_train, y_train, z1, a1, z2, a2, W1, W2)
    W1, b1, W2, b2 = update_weights(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
    if epoch % 100 == 0:
    print(f\'Epoch {epoch}, Loss: {loss}\')
    return W1, b1, W2, b2

    分段说明

    1. 汇入必要的函式库

    import numpy as np
    from sklearn.datasets import fetch_openml
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import OneHotEncoder

    2. 初始化权重

    def initialize_weights(input_size, hidden_size, output_size):
    W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1. / input_size)
    b1 = np.zeros((1, hidden_size))
    W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1. / hidden_size)
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

    • 功能:初始化神经网路的权重和偏差。
    • W1、W2:分别是从输入层到隐藏层、隐藏层到输出层的权重矩阵。
    • b1、b2:偏差向量,用于调整神经元的输出。

    3. 激活函数,Sigmoid 和 Softmax

    def sigmoid(z):
    return 1 / (1 + np.exp(-z))

    def sigmoid_derivative(z):
    return sigmoid(z) * (1 - sigmoid(z))

    def softmax(z):
    exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
    return exp_z / np.sum(exp_z, axis=1, keepdims=True)

    • sigmoid:隐藏层使用的激活函数,用来引入非线性。范围为 (0,1)。
    • sigmoid_derivative:sigmoid 函数的导数,用于反向传播。
    • softmax:输出层使用的激活函数,将输出转换为概率分布,范围为 (0,1),且总和为1。

    4. 前向传播

    def forward_pass(X, W1, b1, W2, b2):
    z1 = np.dot(X, W1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, W2) + b2
    a2 = softmax(z2)
    return z1, a1, z2, a2

    • 功能:计算资料通过网路的结果,输入到隐藏层,再到输出层。
    • z1:隐藏层的线性输入,等于输入资料与隐藏层权重相乘加上偏差。
    • a1:隐藏层的输出,通过sigmoid函数激活。
    • z2:输出层的线性输入,等于隐藏层输出与输出层权重相乘加上偏差。
    • a2:输出层的最终输出,通过softmax函数得到。 """

    5.损失函数:交叉熵

    def compute_loss(y_true, y_pred):
    n_samples = y_true.shape[0]
    logp = - np.log(y_pred[range(n_samples), np.argmax(y_true, axis=1)])
    loss = np.sum(logp) / n_samples
    return loss

    • 功能:计算模型的损失,这里使用的是交叉熵损失。
    • y_true:实际的标籤(One-Hot 编码)。
    • y_pred:预测的输出(通过 softmax 得到的概率分布)。
    • loss:衡量预测值与实际值之间的差异。

    6. 反向传播

    def backward_pass(X, y_true, z1, a1, z2, a2, W1, W2):
    n_samples = X.shape[0]
    dz2 = a2 - y_true
    dW2 = np.dot(a1.T, dz2) / n_samples
    db2 = np.sum(dz2, axis=0, keepdims=True) / n_samples
    dz1 = np.dot(dz2, W2.T) * sigmoid_derivative(z1)
    dW1 = np.dot(X.T, dz1) / n_samples
    db1 = np.sum(dz1, axis=0, keepdims=True) / n_samples
    return dW1, db1, dW2, db2

    • 功能:根据损失计算权重和偏差的梯度。
    • dz2:输出层的误差,等于预测值减去实际标籤。
    • dW2:输出层权重的梯度,通过隐藏层输出的转置与输出层误差相乘计算得出。
    • dz1:隐藏层的误差,通过输出层误差与权重反向传播,并乘上sigmoid的导数。
    • dW1:隐藏层权重的梯度,通过输入的转置与隐藏层误差相乘得出