用Python从零开始构建一个神经网络
2023年西安电子科技大学认知计算作业
潘笃驿 21009201106
下面的内容旨在帮助你完成从零构建一个神经网络,内容基础来自于victorzhou的一篇blog:Machine Learning for Beginners: An Introduction to Neural Networks
在上文基础上对内容做了优化
1. 构建基本的神经元
神经元往往是一个神经网络最基础的组成部分,一个神经元的任务是,它应该接受一个输入,然后根据内部的权重来做一个输出,本质上是一个数学模型,下面的内容会展示一个非常简单的神经元,它接受一个二维输入,然后输出一维结果
该神经元内部发生的计算过程如下:
首先每一维的特征需要与对应的权重值相乘,这里的权重就指 $w_1$
\(x_1 \rightarrow x_1 * w_1\)
\(x_2 \rightarrow x_2 * w_2\)
接下来当权重计算完成后,我们需要加入一个偏置值 $bias$ 在这里写为 $b$ : \((x_1 * w_1) + (x_2 * w_2) + b\)
最后得到的结果将经过一个激活函数 $f$ ,在后面的演示中,激活函数我们将采用$sigmod$: \(y = f(x_1 * w_1 + x_2 * w_2 + b)\)
sigmod 函数的图像如下,它的输出范围为 $(0, 1)$,可以将 $(-\infty, +\infty)$ 的输出范围 缩小到 $(0, 1)$ 非常适合完成二分类任务
一个简单示例
假设按照我们的设计存在这样一个神经元,权重为 [0, 1] ,偏置值为 4
\(w = [0, 1]\) \(b = 4\)
当输入为 $x = [2, 3]$. 它会进行如下的计算: \(\begin{aligned} (w \cdot x) + b &= ((w_1 * x_1) + (w_2 * x_2)) + b \\ &= 0 * 2 + 1 * 3 + 4 \\ &= 7 \\ \end{aligned}\)
\[y = f(w \cdot x + b) = f(7) = \boxed{0.999}\]最后我们得到的结果是 $0.999$ ,这样的一个完整的过程,我们称它为前向计算。
### Python代码实现一个简单的神经元
import numpy as np
def sigmoid(x):
# 激活函数的表达式: f(x) = 1 / (1 + e^(-x))
return 1 / (1 + np.exp(-x))
class Neuron:
def __init__(self, weights, bias):
self.weights = weights
self.bias = bias
def feedforward(self, inputs):
# 与权重相乘,再加上偏置值,最后经过激活函数
total = np.dot(self.weights, inputs) + self.bias
return sigmoid(total)
weights = np.array([0, 1]) # w1 = 0, w2 = 1
bias = 4 # b = 4
n = Neuron(weights, bias)
x = np.array([2, 3]) # x1 = 2, x2 = 3
print(n.feedforward(x)) # 0.9990889488055994
0.9990889488055994
2. 将神经元组装为一个神经网络
一个复杂的神经网络往往由许多个神经元连接组成,而这些神经元往往可以看成一层一层的结构,一个简单的神经网络结构如下:
该网络有 2 个输入,一个带有 2 个神经元的隐藏层 ($h_1$ and $h_2$), 以及一个具有 1 个神经元的输出层 ($o_1$).
隐藏层是输入(第一)层和输出(最后)层之间的任何层。可以有多个隐藏层
神经网络中的前向计算
让我们使用上图所示的网络并假设所有神经元具有相同的权重 $w = [0, 1]$ , 偏置值 $b = 0$ , 并且同样使用sigmod来作为激活函数.
来看看输入 $x = [2, 3]$ 会发生什么
\[\begin{aligned} h_1 = h_2 &= f(w \cdot x + b) \\ &= f((0 * 2) + (1 * 3) + 0) \\ &= f(3) \\ &= 0.9526 \\ \end{aligned}\] \[\begin{aligned} o_1 &= f(w \cdot [h_1, h_2] + b) \\ &= f((0 * h_1) + (1 * h_2) + 0) \\ &= f(0.9526) \\ &= \boxed{0.7216} \\ \end{aligned}\]我们输入了 $x = [2, 3]$ 得到了神经网络的计算值为 $0.7216$.
神经网络可以有任意数量的层,这些层中可以有任意数量的神经元。基本思想保持不变:通过网络中的神经元向前馈送输入,以最终获得输出。为简单起见,我们将在本文的其余部分继续使用上图所示的网络。
Python代码实现实现简单的神经网络
结构参考如下:
class OurNeuralNetwork:
'''
A neural network with:
- 2 inputs
- a hidden layer with 2 neurons (h1, h2)
- an output layer with 1 neuron (o1)
Each neuron has the same weights and bias:
- w = [0, 1]
- b = 0
'''
def __init__(self):
weights = np.array([0, 1])
bias = 0
# 这里的神经元我们使用上一小节定义的神经元
self.h1 = Neuron(weights, bias)
self.h2 = Neuron(weights, bias)
self.o1 = Neuron(weights, bias)
def feedforward(self, x):
out_h1 = self.h1.feedforward(x)
out_h2 = self.h2.feedforward(x)
out_o1 = self.o1.feedforward(np.array([out_h1, out_h2]))
return out_o1
network = OurNeuralNetwork()
x = np.array([2, 3])
print(network.feedforward(x)) # 0.7216325609518421
0.7216325609518421
3. 训练我们设计的神经网络, Part 1 Loss
现在我们有如下的数据信息:
Name | Weight (lb) | Height (in) | Gender |
---|---|---|---|
Alice | 133 | 65 | F |
Bob | 160 | 72 | M |
Charlie | 152 | 70 | M |
Diana | 120 | 60 | F |
我们现在希望能够训练一个模型,根据用户输入的特征,比如 180身高80公斤的体重 来得出一个人的性别
结构大概如下图:
为了方便训练,我们将女性性别表示为 $1$ ,将男性性别表示为 $0$,
Name | Weight (minus 135) | Height (minus 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
Bob | 25 | 6 | 0 |
Charlie | 17 | 4 | 0 |
Diana | -15 | -6 | 1 |
这里不会过多的介绍为什么这样处理训练数据,而且这里的数据处理非常简单,如果您有更多的兴趣,可以在网络上获取更多关于数据预处理操作的信息
损失
在训练网络之前,我们首先需要一种方法来量化它的好程度,以便它可以尝试做得更好。这就是损失函数。
我们将使用均方误差(MSE)损失::
\[\text{MSE} = \frac{1}{n} \sum_{i=1}^n (y_{true} - y_{pred})^2\]Let’s break this down:
- $n$ 是样本数, w为 $4$ (Alice, Bob, Charlie, Diana)。
- $y$ 代表被预测的变量, 也就是性别(0,1)。
- $y_{true}$ 是变量的真实值(“正确答案”)。也就是说 $y_{true}$ 当输入 Alice 的特征时,结果应该是 $1$ (Female)。
- $y_{pred}$ 是变量的预测值。这就是我们的网络输出结果。
$(y_{true} - y_{pred})^2$ 称为均方误差。我们的损失函数只是取所有平方误差的平均值(因此称为均方误差)。我们的预测越好,我们的损失就越低
通常越小的loss值就代表模型现在的能力越强(不考虑过拟合)
训练模型的过程就是希望最小化它的损失
Loss计算的示例
假设我们的网络总是输出0 换句话说,它确信所有人类都是男性🤔。我们的损失是什么?
Name | $y_{true}$ | $y_{pred}$ | $(y_{true} - y_{pred})^2$ |
---|---|---|---|
Alice | 1 | 0 | 1 |
Bob | 0 | 0 | 0 |
Charlie | 0 | 0 | 0 |
Diana | 1 | 0 | 1 |
Python代码实现 MSE Loss
import numpy as np
def mse_loss(y_true, y_pred):
# y_true and y_pred are numpy arrays of the same length.
return ((y_true - y_pred) ** 2).mean()
y_true = np.array([1, 0, 0, 1])
y_pred = np.array([0, 0, 0, 0])
print(mse_loss(y_true, y_pred)) # 0.5
4. 训练我们设计的神经网络, Part 2 反向传播, 优化神经网络
我们现在有一个明确的目标:最小化神经网络的损失。我们知道我们可以改变网络的权重和偏差来影响其预测,但网络权重与偏差的改变不可能由我们随心所欲地来进行,我们希望整个过程损失是不断减少的,那如何做到这一点呢?
为了简单起见,我们假设数据集中只有 Alice:
Name | Weight (minus 135) | Height (minus 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
那么均方误差损失就是 Alice 的平方误差:
\[\begin{aligned} \text{MSE} &= \frac{1}{1} \sum_{i=1}^1 (y_{true} - y_{pred})^2 \\ &= (y_{true} - y_{pred})^2 \\ &= (1 - y_{pred})^2 \\ \end{aligned}\]考虑损失的另一种方法是作为权重和偏差的函数。让我们标记网络中的每个权重和偏差:
然后,我们可以将损失写成多变量函数:
\[L(w_1, w_2, w_3, w_4, w_5, w_6, b_1, b_2, b_3)\]想象一下我们想要调整 $w_1$. 那当我们调整 $w_1$时,$L$ 会如何变化呢? 我们可以用偏导数来观察$L$的变化 $\frac{\partial L}{\partial w_1}$
利用链式法则,我们可以将 $\frac{\partial y_{pred}}{\partial w_1}$ 重写为:
\[\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial y_{pred}} * \frac{\partial y_{pred}}{\partial w_1}\]我们可以计算 $\frac{\partial L}{\partial y_{pred}}$ 因为我们已经计算过 $L = (1 - y_{pred})^2$ :
\[\frac{\partial L}{\partial y_{pred}} = \frac{\partial (1 - y_{pred})^2}{\partial y_{pred}} = \boxed{-2(1 - y_{pred})}\]接下来我们研究如何计算 $\frac{\partial y_{pred}}{\partial w_1}$. 我们用 $h_1, h_2, o_1$ 来代表神经网络每层的输出
\(y_{pred} = o_1 = f(w_5h_1 + w_6h_2 + b_3)\)
参数 $w_1$ 只影响 $h_1$ (not $h_2$), 所以我们可以将计算式写成
\[\frac{\partial y_{pred}}{\partial w_1} = \frac{\partial y_{pred}}{\partial h_1} * \frac{\partial h_1}{\partial w_1}\] \[\frac{\partial y_{pred}}{\partial h_1} = \boxed{w_5 * f'(w_5h_1 + w_6h_2 + b_3)}\]同样的,我们也可以对 $\frac{\partial h_1}{\partial w_1}$ 使用链式法则:
\[h_1 = f(w_1x_1 + w_2x_2 + b_1)\] \[\frac{\partial h_1}{\partial w_1} = \boxed{x_1 * f'(w_1x_1 + w_2x_2 + b_1)}\]$x_1$ 代表体重, and $x_2$ 代表身高. 接下来我们来观察sigmod函数的导数
\[f(x) = \frac{1}{1 + e^{-x}}\] \[f'(x) = \frac{e^{-x}}{(1 + e^{-x})^2} = f(x) * (1 - f(x))\]现在我们已经将最开始的 $\frac{\partial L}{\partial w_1}$ 拆解为下面这个我们可以计算的式子:
\[\boxed{\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial y_{pred}} * \frac{\partial y_{pred}}{\partial h_1} * \frac{\partial h_1}{\partial w_1}}\]这种通过向后计算偏导数的系统称为反向传播
如何计算偏导数
我们将继续假设只有 Alice 在我们的数据集中
Name | Weight (minus 135) | Height (minus 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
让我们将所有权重初始化为 $1$ 所有的偏置设置为 $0$. 当我们进行前向计算,我们会得到下面的结果:
\[\begin{aligned} h_1 &= f(w_1x_1 + w_2x_2 + b_1) \\ &= f(-2 + -1 + 0) \\ &= 0.0474 \\ \end{aligned}\] \[h_2 = f(w_3x_1 + w_4x_2 + b_2) = 0.0474\] \[\begin{aligned} o_1 &= f(w_5h_1 + w_6h_2 + b_3) \\ &= f(0.0474 + 0.0474 + 0) \\ &= 0.524 \\ \end{aligned}\]输出的结果为 $y_{pred} = 0.524$, 这不能够帮助我们准确的分辨出输入的是男性特征($0$)还是女性特征($1$) 。接下来让我们计算偏导 $\frac{\partial L}{\partial w_1}$:
\[\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial y_{pred}} * \frac{\partial y_{pred}}{\partial h_1} * \frac{\partial h_1}{\partial w_1}\] \[\begin{aligned} \frac{\partial L}{\partial y_{pred}} &= -2(1 - y_{pred}) \\ &= -2(1 - 0.524) \\ &= -0.952 \\ \end{aligned}\] \[\begin{aligned} \frac{\partial y_{pred}}{\partial h_1} &= w_5 * f'(w_5h_1 + w_6h_2 + b_3) \\ &= 1 * f'(0.0474 + 0.0474 + 0) \\ &= f(0.0948) * (1 - f(0.0948)) \\ &= 0.249 \\ \end{aligned}\] \[\begin{aligned} \frac{\partial h_1}{\partial w_1} &= x_1 * f'(w_1x_1 + w_2x_2 + b_1) \\ &= -2 * f'(-2 + -1 + 0) \\ &= -2 * f(-3) * (1 - f(-3)) \\ &= -0.0904 \\ \end{aligned}\] \[\begin{aligned} \frac{\partial L}{\partial w_1} &= -0.952 * 0.249 * -0.0904 \\ &= \boxed{0.0214} \\ \end{aligned}\]计算的偏导大于0,这告诉我们 增大$w_1$, $L$ 也会增大,所以我们需要减小 $w_1$
随机梯度下降
我们现在基本已经完成了训练的所有准备! 我们将使用一种称为随机梯度下降(SGD)的优化算法,它告诉我们如何改变权重和偏差以最小化损失。基本上就是这个更新方程:
\[w_1 \leftarrow w_1 - \eta \frac{\partial L}{\partial w_1}\]$\eta$ 是一个称为学习率的常数,它控制我们训练的速度。我们所做的只是让 $w_1$ 减去 $\eta \frac{\partial L}{\partial w_1}$ :
- 如果 $\frac{\partial L}{\partial w_1}$ 大于0, $w_1$ 将会减小, 使得 $L$ 下降.
- 如果 $\frac{\partial L}{\partial w_1}$ 小于0, $w_1$ 将会增大, 使得 $L$ 下降.
如果我们对网络中的每个权重和偏置都这样做,损失就会慢慢减少,我们的网络也会得到改善。
我们的训练过程将如下所示:
- 从数据集中随机采样,这里我们只随机采取一个样本。
- 计算损失相对于权重或偏差的所有偏导数 (e.g. $\frac{\partial L}{\partial w_1}$, $\frac{\partial L}{\partial w_2}$, etc).
- 使用SGD来更新每个权重和偏差
- 重复步骤1
实现完整的BP神经网络
训练数据如下:
Name | Weight (minus 135) | Height (minus 66) | Gender |
---|---|---|---|
Alice | -2 | -1 | 1 |
Bob | 25 | 6 | 0 |
Charlie | 17 | 4 | 0 |
Diana | -15 | -6 | 1 |
import numpy as np
def sigmoid(x):
# Sigmoid activation function: f(x) = 1 / (1 + e^(-x))
return 1 / (1 + np.exp(-x))
def deriv_sigmoid(x):
# Derivative of sigmoid: f'(x) = f(x) * (1 - f(x))
fx = sigmoid(x)
return fx * (1 - fx)
def mse_loss(y_true, y_pred):
# y_true and y_pred are numpy arrays of the same length.
return ((y_true - y_pred) ** 2).mean()
class OurNeuralNetwork:
'''
A neural network with:
- 2 inputs
- a hidden layer with 2 neurons (h1, h2)
- an output layer with 1 neuron (o1)
*** DISCLAIMER ***:
The code below is intended to be simple and educational, NOT optimal.
Real neural net code looks nothing like this. DO NOT use this code.
Instead, read/run it to understand how this specific network works.
'''
def __init__(self):
# Weights
self.w1 = np.random.normal()
self.w2 = np.random.normal()
self.w3 = np.random.normal()
self.w4 = np.random.normal()
self.w5 = np.random.normal()
self.w6 = np.random.normal()
# Biases
self.b1 = np.random.normal()
self.b2 = np.random.normal()
self.b3 = np.random.normal()
def feedforward(self, x):
# x is a numpy array with 2 elements.
h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1)
h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2)
o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3)
return o1
def train(self, data, all_y_trues):
'''
- data is a (n x 2) numpy array, n = # of samples in the dataset.
- all_y_trues is a numpy array with n elements.
Elements in all_y_trues correspond to those in data.
'''
learn_rate = 0.1
epochs = 1000 # number of times to loop through the entire dataset
for epoch in range(epochs):
for x, y_true in zip(data, all_y_trues):
# --- Do a feedforward (we'll need these values later)
sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1
h1 = sigmoid(sum_h1)
sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2
h2 = sigmoid(sum_h2)
sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3
o1 = sigmoid(sum_o1)
y_pred = o1
# --- Calculate partial derivatives.
# --- Naming: d_L_d_w1 represents "partial L / partial w1"
d_L_d_ypred = -2 * (y_true - y_pred)
# Neuron o1
d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1)
d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1)
d_ypred_d_b3 = deriv_sigmoid(sum_o1)
d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1)
d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1)
# Neuron h1
d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1)
d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1)
d_h1_d_b1 = deriv_sigmoid(sum_h1)
# Neuron h2
d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2)
d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2)
d_h2_d_b2 = deriv_sigmoid(sum_h2)
# --- Update weights and biases
# Neuron h1
self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1
self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2
self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1
# Neuron h2
self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3
self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4
self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2
# Neuron o1
self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5
self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6
self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3
# --- Calculate total loss at the end of each epoch
if epoch % 10 == 0:
y_preds = np.apply_along_axis(self.feedforward, 1, data)
loss = mse_loss(all_y_trues, y_preds)
print("Epoch %d loss: %.3f" % (epoch, loss))
# Define dataset
data = np.array([
[-2, -1], # Alice
[25, 6], # Bob
[17, 4], # Charlie
[-15, -6], # Diana
])
all_y_trues = np.array([
1, # Alice
0, # Bob
0, # Charlie
1, # Diana
])
# Train our neural network!
network = OurNeuralNetwork()
network.train(data, all_y_trues)
随着网络的不断学习,损失不断下降:
下面我们就能看到预测的结果:
emily = np.array([-7, -3]) # 128 pounds, 63 inches
frank = np.array([20, 2]) # 155 pounds, 68 inches
print("Emily: %.3f" % network.feedforward(emily)) # 0.951 - F
print("Frank: %.3f" % network.feedforward(frank)) # 0.039 - M