多层人工神经网路从零开始的实作与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:隐藏层权重的梯度,通过输入的转置与隐藏层误差相乘得出