你是否曾好奇过,计算机如何识别手写数字、预测一封邮件是否为垃圾邮件、推荐视频,或理解一句话?
许多现代 AI 系统依赖于一种称为 神经网络 的东西。
现在,这个名称可能会让它们听起来比实际要复杂得多。你可能会认为,要构建一个需要高级微积分、一台巨型计算机以及数千行代码。
你不需要。
在最基本的层面上,神经网络是一个数学模型,它接收一些数字作为输入,对这些数字进行计算,做出预测,检查预测与正确答案之间的偏差,然后调整自身,以便下次能做得稍微好一点。
在本教程中,我们将使用 Python 和 NumPy 亲手构建一个。
我们将从一个单一的人工神经元开始,逐步构建完整的神经网络。到最后,你将了解权重和偏置是什么,激活函数的作用,网络如何从错误中学习,反向传播和梯度下降到底意味着什么,以及这些部分如何组合在一起。
你不需要了解高级机器学习就能跟上。一些基本的 Python 和代数知识会有所帮助,但我会在讲解过程中解释重要的数学概念。
让我们从一个简单的例子开始。
想象我们希望电脑能够预测一个学生是否会通过考试。
我们可以给电脑提供以下信息:
学生学习了多少小时
他们完成了多少道练习题
他们的之前考试得分
例如:
Study Hours = 5 Practice Questions = 80 Previous Score = 82
我们还知道学生实际是否通过:
Passed = 1
看到许多这样的例子后,我们希望计算机能学习到一种模式。
学习时间更长的学生可能表现更好。之前的考试成绩可能很有用。做练习题也可能有帮助。
与其自己编写所有这些规则,不如把例子交给神经网络,让它自行学习其中的关系。
基本思想如下:
预测结果可能类似于:0.92
如果我们在预测及格的概率,可以将其理解为大约有92%的及格机会。
重要的是,我们并没有告诉网络……
"学习时间很重要,之前的成绩稍微更重要一些。"
相反,网络会学习到称为权重的数字,这些数字决定了不同输入对其预测的影响程度。
名称来源于生物大脑。
你的大脑包含神经元,它们接收信号、处理信息,并将信号传递给其他神经元。
人工神经网络并不是人工大脑。它们的工作方式并不完全像生物神经元。但将许多简单处理单元连接起来的总体思想启发了这个名称。
一个非常简化的人工神经元看起来像这样:
神经元接收数字,执行一些数学运算,并输出另一个数字。
神经网络是通过将许多人工神经元连接在一起构成的。
一个简单的神经网络可以分为三种类型的层:
输入层
隐藏层(们)
输出层
让我们逐一查看。
输入层包含我们提供给网络的信息。
以我们的学生例子为例,我们可能有三个输入:
Input 1 = Study Hours
Input 2 = Practice Questions
Input 3 = Previous Score
因此,一个学生的输入可能如下所示:
[5, 80, 82]
网络并不一定明白这些数字代表“学习时长”或“考试得分”。在网络的数学部分,它们仅仅是数字。
这是一个需要牢记的重要概念:
神经网络处理的是数字。
图像、文本、音频以及其他信息在被神经网络处理之前,最终都必须转换为数字形式。
输入层之后是隐藏层。
网络可能如下所示:
隐藏层包含对输入进行计算的神经元。
网络可以具有一个隐藏层或多个隐藏层。
当网络拥有许多层时,我们通常称其为深度神经网络。
输出层产生最终结果。
对于简单的是/否问题,我们可能将答案表示为:
0 = No
1 = Yes
例如:
0.12 → probably No
0.91 → probably Yes
对于包含多个类别的问题,输出可能包含多个数字:
Cat = 0.05
Dog = 0.90
Bird = 0.05
最大的值与 "Dog" 关联,因此模型会预测为 Dog。
现在让我们放大来看一个神经元。
假设我们的神经元接收到三个输入:
x₁
x₂
x₃
每个输入都有对应的权重:
w₁
w₂
w₃
神经元将每个输入乘以其权重,然后将结果相加。
它还会加上一个称为 偏置 的项。
方程如下:
z = x₁w₁ + x₂w₂ + x₃w₃ + b
别担心,这个等式看起来吓人。
其实它基本上就是:
input × weight
+
input × weight
+
input × weight
+
bias
让我们使用实际数字。
假设:
x₁ = 2
x₂ = 3
x₃ = 4
w₁ = 0.5
w₂ = 0.2
w₃ = 0.8
b = 1
然后:
z = (2 × 0.5) + (3 × 0.2) + (4 × 0.8) + 1
计算每个部分:
2 × 0.5 = 1.0
3 × 0.2 = 0.6
4 × 0.8 = 3.2
现在将它们添加:
z = 1.0 + 0.6 + 3.2 + 1
z = 5.8
神经元已经产生了 5.8。
但我们还没有完成。
权重控制输入对神经元的影响强度。
假设我们有:
x = 5
如果重量是:
w = 2
然后:
x × w = 5 × 2
= 10
但是如果重量是:
w = 0.1
然后:
x × w = 5 × 0.1
= 0.5
相同的输入由于权重的变化而产生了截然不同的结果。
可以把权重想象成音量旋钮。
较大的正权重会使输入产生更强的正向影响;接近零的权重则使输入影响甚微;负权重则会把结果推向相反的方向。
网络在训练过程中会学习这些权重。
偏置是加到神经元计算中的另一个数值。
如果没有偏置,我们将得到:
z = x₁w₁ + x₂w₂ + x₃w₃
带有偏置:
z = x₁w₁ + x₂w₂ + x₃w₃ + b
为什么要再加一个数字?因为这能让神经元的计算更灵活。
可以把它想象成调整神经元计算的起点。
在训练过程中,网络学习偏置的方式与学习权重相同。
所以当你看到:
weights + bias
您正在查看神经网络在学习过程中可以调节的一些参数。
此时,我们的神经元可以计算加权和:
z = x₁w₁ + x₂w₂ + ... + b
但神经网络需要学习的关系比简单的加权和更复杂。
这就是 激活函数 发挥作用的地方。激活函数会取神经元计算得到的值并进行变换。
一种常见的激活函数是 ReLU。ReLU 代表 修正线性单元。
它的公式是:
ReLU(x) = max(0, x)
简单来说:
如果数字为正,则保持不变。
如果数字为负,则将其设为零。
例如:
ReLU(-5) = 0
ReLU(-2) = 0
ReLU(0) = 0
ReLU(3) = 3
ReLU(10) = 10
在Python中:
def relu(x):
return max(0, x)
使用NumPy数组时,我们可以使用:
def relu(x):
return np.maximum(0, x)
激活函数很重要,因为它们使得多层神经网络能够学习更复杂的模式。
让我们把数学转化为 Python 代码。
首先,导入 NumPy:
import numpy as np
NumPy 为我们提供了处理数字、数组、向量和矩阵的工具。
现在让我们创建输入:
x = np.array([2, 3, 4])
这会创建一个包含三个值的数组:
[2, 3, 4]
现在创建权重:
weights = np.array([0.5, 0.2, 0.8])
每个输入都有一个权重:
x₁ = 2 w₁ = 0.5
x₂ = 3 w₂ = 0.2
x₃ = 4 w₃ = 0.8
接下来,创建偏置:
bias = 1
接下来,我们计算加权和:
z = np.dot(x, weights) + bias
np.dot() 执行我们之前描述的乘法-加法运算。
在这种情况下:
np.dot(x, weights)
等同于:
(2 × 0.5) + (3 × 0.2) + (4 × 0.8)
即:
4.8
然后我们添加偏置:
4.8 + 1 = 5.8
现在应用 ReLU:
output = np.maximum(0, z)
由于 z 为 5.8,ReLU 将其保持不变:
output = 5.8
最后:
print(output)
打印:
5.8
所以我们的整个神经元是:
import numpy as np
x = np.array([2, 3, 4])
weights = np.array([0.5, 0.2, 0.8])
bias = 1
z = np.dot(x, weights) + bias
output = np.maximum(0, z)
print(output)
我们刚刚创建了一个微小的人工神经元。
单个神经元不足以解决大多数有趣的问题。
相反,我们可以将多个神经元连接在一起。
例如:
这些神经元共同形成一个 层。
一个小型神经网络可能看起来像:
Input Layer
↓
Hidden Layer
↓
Output Layer
一层中的每个神经元都能够将其输出传递给下一层的神经元。
这就是神经网络开始变得更强大的地方。
到目前为止,我们一直手动选择权重:
0.5
0.2
0.8
但真实的神经网络一开始并不知道正确的权重。
相反,它的初始权重通常是一些小的随机值。
然后它会经历一个循环:
Make a prediction
↓
Compare prediction with correct answer
↓
Measure the error
↓
Figure out how to change the weights
↓
Update the weights
↓
Try again
此过程会反复进行,网络会逐步调整其参数,以在训练数据上做出更好的预测。
让我们逐部分分解。
假设正确答案是:
1
但我们的网络预测:
0.3
预测结果与目标相差甚远。
我们需要一种方法来衡量它有多错误。这就是 损失函数 的作用。
损失函数接收预测值和正确答案,输出一个表示模型误差的数值。
举个简单的例子,我们可以使用平方误差:
Loss = (prediction - actual)²
使用我们的数据:
Loss = (0.3 - 1)²
首先:
0.3 - 1 = -0.7
然后将其平方:
(-0.7)² = 0.49
所以:
Loss = 0.49
通常,损失越小,说明预测越接近目标。
在实际的神经网络中,不同的问题会采用不同的损失函数。对于二分类问题,常用的是二元交叉熵。
现在我们遇到了一个问题。
我们知道预测是错误的,但应该如何调整权重呢?
这就是梯度发挥作用的地方。梯度告诉我们改变一个参数会如何影响损失。
你可以把它想象成站在山坡上。假设你的目标是到达最低点。如果你知道哪个方向是上坡,你就可以朝相反的方向走,从而下坡。
训练神经网络的思路与此类似。我们希望降低损失。梯度提供了参数应朝哪个方向移动的信息。
梯度下降 是利用梯度调整网络参数的过程。
一个简化的更新规则是:
new weight = old weight - learning rate × gradient
在Python中:
weight = weight - learning_rate * gradient
学习率控制更新幅度的大小。
例如:
learning_rate = 0.01
如果学习率太大,网络可能会产生巨大的变化,甚至在寻找良好解之前就跳来跳去。
如果太小,学习过程可能会非常漫长。
因此,训练的目标是寻找能够使模型朝着损失减小的方向更新参数,同时保持训练过程的稳定。
仍然有一个重要的问题:
如果一个神经网络有成千上万甚至数百万个权重,它如何确定哪些权重导致了误差?
这就是 反向传播 发挥作用的地方。反向传播通过从后往前遍历网络来计算参数的梯度。
想象一个类似这样的网络:
Input
↓
Hidden Layer
↓
Output
↓
Loss
在前向传播过程中,信息按以下方向流动:
Input → Hidden Layer → Output
在反向传播过程中,梯度信息向后传播:
Loss → Output → Hidden Layer → Input
网络利用这些梯度来确定其权重和偏置应如何变化。
在构建真实神经网络时,通常不会手动计算所有这些导数。PyTorch 等库可以自动完成计算。
但理解基本思想很重要:
反向传播计算参数对误差的贡献,梯度下降则利用此信息更新参数。
现在我们可以把所有内容综合起来。
更具体地说:
Give the network data
↓
Calculate a prediction
↓
Compare it with the correct answer
↓
Calculate the loss
↓
Calculate gradients
↓
Update weights and biases
↓
Repeat
一次完整遍历训练数据通常被称为 epoch。
例如:
Epoch 1 → Loss: 0.82
Epoch 2 → Loss: 0.61
Epoch 3 → Loss: 0.43
Epoch 4 → Loss: 0.29
Epoch 5 → Loss: 0.18
这些数字仅作示例,但理想情况下,随着训练的进行,损失应会下降。
恭喜!你现在已经掌握了神经网络的基础知识。接下来是把这些想法付诸实践的时候了。
我们将仅使用以下内容构建一个小型神经网络:
Python + NumPy
我们的网络将学习一个经典的机器学习问题,称为 XOR。
XOR 是一个有两个输入的逻辑运算。
其规则如下:
0 XOR 0 → 0
0 XOR 1 → 1
1 XOR 0 → 1
1 XOR 1 → 0
换句话说,当输入中恰好有一个为 1 时,输出为 1。
因此,我们的训练数据将是:
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
而正确的答案是:
y = np.array([
[0],
[1],
[1],
[0]
])
我们希望神经网络能够学习这个模式。
我们的网络将包含:
2 input neurons
↓
4 hidden neurons
↓
1 output neuron
两个输入分别代表每个 XOR 示例中的两个数字。
四个隐藏神经元使网络具有足够的灵活性来学习 XOR 关系。
输出神经元产生一个介于 0 和 1 之间的数字。
让我们开始编写 Python 程序。
import numpy as np
这会导入 NumPy。我们将使用 NumPy 进行数组、矩阵乘法以及数学运算。
接下来:
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
X 包含我们的四个训练样本。
每行是一个样本:
[0, 0]
[0, 1]
[1, 0]
[1, 1]
现在创建正确的答案:
y = np.array([
[0],
[1],
[1],
[0]
])
X 的第一行与 y 的第一行相对应。
因此:
[0, 0] → 0
[0, 1] → 1
[1, 0] → 1
[1, 1] → 0
现在我们需要网络的参数。
首先:
np.random.seed(42)
这使得我们的随机数可重复。
如果没有这一行,网络在每次运行程序时会得到不同的随机初始权重。
现在创建第一层的权重:
W1 = np.random.randn(2, 4)
为什么 (2, 4)?
因为:
我们有 2 个输入值。
隐藏层中有 4 个神经元。
所以 W1 需要一个权重,将每个输入与每个隐藏神经元相连。
有:
2 × 4 = 8
权重。
下一步:
b1 = np.zeros((1, 4))
这会创建四个偏置,每个隐藏神经元对应一个。
现在是第二层:
W2 = np.random.randn(4, 1)
这里包含四个隐藏神经元和一个输出神经元,因此我们需要:
4 × 1 = 4
权重。
最后:
b2 = np.zeros((1, 1))
这样输出神经元就有一个偏置。
因此,我们的网络参数为:
W1 → input-to-hidden weights
b1 → hidden-layer biases
W2 → hidden-to-output weights
b2 → output-layer bias
我们的输出代表一个概率,所以我们希望它介于 0 和 1 之间。
我们可以使用 sigmoid 函数。
它的方程是:
sigmoid(x) = 1 / (1 + e⁻ˣ)
在Python中:
def sigmoid(x):
return 1 / (1 + np.exp(-x))
我们来看看它的作用:
sigmoid(-5) ≈ 0.007
sigmoid(0) = 0.5
sigmoid(5) ≈ 0.993
无论输入多大或多小,结果始终在 0 和 1 之间。
当我们的输出表示概率时,这一点非常有用。
现在我们可以将数据通过网络传输。这被称为 前向传播。
首先,计算隐藏层:
z1 = X @ W1 + b1
这里有一个新符号:
@
在 Python 中,@ 执行矩阵乘法。
可以把这个操作看作一次性完成许多加权求和。
而不是手动计算每个神经元:
input × weight + input × weight + bias
NumPy 可以一次性计算所有这些值。
结果存储在 z1 中。
接下来:
a1 = np.tanh(z1)
在这里,我们使用 tanh 激活函数 来处理隐藏层。
Tanh 将其输入转换为 -1 到 1 之间的值。
为什么在这里使用 tanh?
因为 XOR 不是单个简单线性计算能够解决的问题。非线性激活函数为隐藏层提供了学习该模式所需的灵活性。
现在计算输出层:
z2 = a1 @ W2 + b2
这一步使用第二组权重将隐藏层的输出组合起来。
最后:
a2 = sigmoid(z2)
现在 a2 中存放着我们的预测结果。
例如,在训练之前,网络可能会产生类似下面的输出:
0.52
0.61
0.48
0.55
这些预测目前还没有用处,但这是可以预期的。网络尚未学到任何东西。
现在我们需要衡量这些预测的好坏。
对于二分类任务,我们将使用 二元交叉熵,这是机器学习中用于二分类的损失函数。它衡量的是模型输出为 0 到 1 之间概率值的性能。
公式是:
Loss = -mean(
y × log(prediction)
+
(1 - y) × log(1 - prediction)
)
这看起来比之前的平方误差示例复杂得多,但我们不需要记住公式。
在 Python 中:
loss = -np.mean(
y * np.log(a2 + 1e-8) +
(1 - y) * np.log(1 - a2 + 1e-8)
)
1e-8 是一个非常小的数。
如果 a2 极其接近 0 或 1,就会导致问题,因为对恰好为零的数取对数是无效的。
在训练开始时,损失可能会相对较高。但随着网络的学习,我们希望它能够降低。
现在是我们程序中最数学的部分。
我们需要计算梯度。
从以下开始:
dz2 = a2 - y
这给出了在 sigmoid + 二元交叉熵 组合下,损失相对于输出层前激活值的梯度。
接下来:
dW2 = (a1.T @ dz2) / len(X)
这计算了 W2 的梯度。
.T 表示转置。
我们的隐藏层输出有四个神经元,而 dz2 表示输出层的误差。通过矩阵乘法将它们结合,可以计算出每个隐藏层到输出层的权重对损失的贡献。
我们将其除以:
len(X)
因为我们有四个训练样本,并且我们正在计算平均梯度。
现在计算输出偏置梯度:
db2 = np.mean(dz2, axis=0, keepdims=True)
这将计算输出偏置的平均梯度。
接下来:
da1 = dz2 @ W2.T
这会把梯度信息从输出层向后传播到隐藏层。
现在我们需要考虑 tanh 激活函数的导数。
tanh 的导数可以表示为:
1 - tanh(x)²
既然我们已经在 a1 中得到了隐藏层的激活值,我们可以写出:
dz1 = da1 * (1 - a1**2)
这告诉我们隐藏层预激活值如何影响损失。
现在计算第一层权重的梯度:
dW1 = (X.T @ dz1) / len(X)
以及隐藏层的偏置:
db1 = np.mean(dz1, axis=0, keepdims=True)
此时,我们已经获得了所有可训练参数的梯度。
现在我们使用梯度下降。
首先:
W2 -= learning_rate * dW2
这会更新第二层的权重。
这里的-=表示:
W2 = W2 - learning_rate * dW2
然后:
b2 -= learning_rate * db2
更新输出偏置。
以及:
W1 -= learning_rate * dW1
更新第一层的权重。
最后:
b1 -= learning_rate * db1
更新隐藏层偏置。
这些更新才是使网络能够学习的关键。
现在让我们把所有内容整合在一起。
import numpy as np
# 1. Training data
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
y = np.array([
[0],
[1],
[1],
[0]
])
# 2. Initialize parameters
np.random.seed(42)
W1 = np.random.randn(2, 4)
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1)
b2 = np.zeros((1, 1))
learning_rate = 0.1
# 3. Activation functions
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 4. Training
for epoch in range(10000):
# Forward propagation
z1 = X @ W1 + b1
a1 = np.tanh(z1)
z2 = a1 @ W2 + b2
a2 = sigmoid(z2)
# Calculate loss
loss = -np.mean(
y * np.log(a2 + 1e-8) +
(1 - y) * np.log(1 - a2 + 1e-8)
)
# Backpropagation
dz2 = a2 - y
dW2 = (a1.T @ dz2) / len(X)
db2 = np.mean(dz2, axis=0, keepdims=True)
da1 = dz2 @ W2.T
dz1 = da1 * (1 - a1**2)
dW1 = (X.T @ dz1) / len(X)
db1 = np.mean(dz1, axis=0, keepdims=True)
# Update parameters
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
# Display progress
if epoch % 1000 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
让我们从上到下逐步浏览程序。
import numpy as np
我们导入 NumPy,因为我们的网络需要进行数组和矩阵运算。
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
每行代表一个 XOR 示例。
共有四个示例,每个示例包含两个输入值。
因此,X 的形状是:
4 × 2
y = np.array([
[0],
[1],
[1],
[0]
])
每行 X 对应一个正确答案,共有四个正确答案。
np.random.seed(42)
这会让 NumPy 每次生成相同的起始随机值。
数字 42 并没有特别之处。你可以使用其他数字。
W1 = np.random.randn(2, 4)
这会生成一个包含随机数的矩阵。
其形状为 2*4
有两个输入和四个隐藏神经元。
b1 = np.zeros((1, 4))
这会创建四个零:
[0, 0, 0, 0]
每个隐藏神经元对应一个偏置。
W2 = np.random.randn(4, 1)
这里有四个隐藏神经元和一个输出神经元。
因此:
4 × 1
需要权重。
b2 = np.zeros((1, 1))
输出层只有一个神经元,因此需要一个偏置。
learning_rate = 0.1
这控制梯度对每次更新的影响程度。
def sigmoid(x):
return 1 / (1 + np.exp(-x))
这将输出转换为介于 0 和 1 之间的值。
for epoch in range(10000):
这会让 Python 重复训练过程 10,000 次。
每次重复称为一个 epoch,即完整地将整个训练数据集通过神经网络一次。
z1 = X @ W1 + b1
这会对所有四个隐藏神经元和所有四个训练样本进行加权求和计算。
a1 = np.tanh(z1)
这将非线性激活函数应用于隐藏层。
z2 = a1 @ W2 + b2
它会取用隐藏层的数值,并计算输出神经元的加权和。
a2 = sigmoid(z2)
这将输出转换为介于 0 和 1 之间的概率。
loss = -np.mean(
y * np.log(a2 + 1e-8) +
(1 - y) * np.log(1 - a2 + 1e-8)
)
这用来衡量预测结果与正确答案的差异。
数值越低通常表示预测效果越好。
dz2 = a2 - y
这会计算出更新输出层所需的梯度。
dW2 = (a1.T @ dz2) / len(X)
这决定了每个连接隐藏层与输出层的权重对损失的贡献程度。
db2 = np.mean(dz2, axis=0, keepdims=True)
这计算了输出偏置的平均梯度。
da1 = dz2 @ W2.T
这将梯度信息通过输出层向后传播。
dz1 = da1 * (1 - a1**2)
这解释了 tanh 激活函数的作用。
dW1 = (X.T @ dz1) / len(X)
这决定了输入到隐藏层的权重是如何对损失产生影响的。
然后:
db1 = np.mean(dz1, axis=0, keepdims=True)
计算隐藏层偏置的梯度。
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
这四行代码是网络修改其已学内容的地方。
梯度告诉我们应该朝哪个方向移动,而学习率则决定移动的幅度。
if epoch % 1000 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
% 运算符返回除法运算后的余数。
因此:
epoch % 1000 == 0
每 1000 个周期成立一次。
这意味着我们不会打印 10,000 次。相反,我们会偶尔得到如下更新:
Epoch 0, Loss: ...
Epoch 1000, Loss: ...
Epoch 2000, Loss: ...
...
如果训练进行顺利,损失应总体下降。
训练完成后,我们可以使用网络进行预测。
z1 = X @ W1 + b1
a1 = np.tanh(z1)
z2 = a1 @ W2 + b2
predictions = sigmoid(z2)
print(predictions)
网络应该输出接近以下的值:
[[0],
[1],
[1],
[0]]
实际值可能不会恰好是 0 和 1。
你可能会得到类似这样的结果:
[[0.01],
[0.98],
[0.99],
[0.02]]
没关系。
网络正在输出概率。
我们可以用阈值把这些概率转换为类别:
classes = (predictions >= 0.5).astype(int)
print(classes)
结果应该是:
[[0],
[1],
[1],
[0]]
我们的网络已经学会了 XOR 模式。
你可能会好奇为什么我们不能直接把两个输入连接到输出。
原因在于单个线性层无法表示 XOR。
隐藏层为网络提供了额外的变换,使其能够学习更复杂的关系。
这是神经网络背后最重要的思想之一:网络不一定会学习一个巨大的规则。相反,不同的层可以逐步转换信息。
对于图像识别系统,你可以想象一个简化的过程如下:
真实的神经网络并不会真的创建称为“边缘”、“形状”和“物体”的整齐层。这只是一种直觉,说明通过多层可以产生越来越复杂的表示。
我们构建的网络很小。现代神经网络可以拥有数百万、数十亿甚至更多的参数。
一个简化的网络可能看起来像这样:
每个连接可以有自己的权重。
网络拥有的神经元和连接越多,它可能需要学习的参数就越多。
因此,大型模型需要大量的计算能力和内存。
但请记住基本过程:
Input
↓
Calculations
↓
Prediction
↓
Loss
↓
Gradients
↓
Parameter Updates
网络的规模可能会发生巨大变化,但基本的训练思想保持不变。
不必。从零开始构建神经网络有助于学习,因为它迫使你去理解库背后发生的事情。
但在构建实际的机器学习应用时,你通常不会手动计算每个梯度。
这时机器学习框架就派上用场了。常用的 Python 库包括:
NumPy
PyTorch
TensorFlow
Keras
scikit-learn
在深度学习中,PyTorch 是最常用的框架之一。它能够自动计算梯度,并处理训练过程中涉及的许多数学运算。
让我们看看使用 PyTorch 能让网络变得多么简洁。
首先,安装它:
pip install torch
然后导入它:
import torch
import torch.nn as nn
现在创建模型:
model = nn.Sequential(
nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
nn.Sigmoid()
)
我们来分解一下吧。
nn.Linear(2, 4)
创建一个接受两个输入、产生四个输出的层。
这就是我们的隐藏层。
接下来:
nn.Tanh()
应用tanh激活函数。
然后:
nn.Linear(4, 1)
将四个隐藏神经元连接到一个输出神经元上。
最后:
nn.Sigmoid()
将输出转换为介于 0 和 1 之间的值。
因此架构是:
2 inputs
↓
4 hidden neurons
↓
Tanh
↓
1 output neuron
↓
Sigmoid
注意,这段代码比我们的 NumPy 实现要短得多。
这是因为 PyTorch 帮我们完成了许多计算。
首先,创建训练数据:
X = torch.tensor([
[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.]
])
y = torch.tensor([
[0.],
[1.],
[1.],
[0.]
])
小数点至关重要,因为神经网络通常处理的是浮点数。
现在创建模型:
model = nn.Sequential(
nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
nn.Sigmoid()
)
接下来,选择损失函数:
loss_function = nn.BCELoss()
BCELoss 计算二元交叉熵损失。
现在创建一个优化器:
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.01
)
Adam 是一种在训练过程中更新模型参数的优化算法。
model.parameters() 告诉优化器需要更新哪些参数。
lr=0.01 设置学习率。
现在我们可以开始训练:
for epoch in range(5000):
predictions = model(X)
loss = loss_function(predictions, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 500 == 0:
print(
f"Epoch {epoch}, Loss: {loss.item():.4f}"
)
我们来看看重要的部分。
首先:
predictions = model(X)
这会将训练数据通过网络传递。
然后:
loss = loss_function(predictions, y)
将预测结果与正确答案进行比较。
下一步:
optimizer.zero_grad()
清除上一次训练步骤中的梯度。
然后:
loss.backward()
使用反向传播自动计算梯度。
最后:
optimizer.step()
使用这些梯度更新模型参数。
这就是我们用 NumPy 手动实现的基本学习过程;不同之处在于 PyTorch 负责了许多计算。
那么为什么我们要构建两次网络?因为这两个版本教授的内容不同。
使用 NumPy 时,我们手动处理了权重、偏置、前向传播、
损失、梯度、反向传播和参数更新。这样机制就更易于观察。
使用 PyTorch,我们可以用更少的代码表达相同的思想,因为框架处理了许多这些计算。
你可以这样理解:
了解 NumPy 版本的工作原理会让 PyTorch 版本变得不那么神秘。
你可能已经听说过 深度学习 这个术语。深度学习是机器学习的一个分支,它使用多层神经网络。
例如:
Input
↓
Layer 1
↓
Layer 2
↓
Layer 3
↓
Layer 4
↓
Output
"深度"一词指的是网络的深度,即所涉及的层数。
没有神奇的临界点,神经网络会突然变得智能。仅仅增加层数只是为模型提供了更多将输入转化为有用表示的机会。
神经网络在许多不同领域都有应用。以下是一些例子……
神经网络可以处理图像。
例如:
Image
↓
Neural Network
↓
Prediction
它们可以用于图像分类和目标检测等任务。
神经网络也可以处理文本。
例如:
Text
↓
Neural Network
↓
Prediction
现代语言模型使用神经网络来处理和生成文本。
神经网络能够处理音频,并帮助将口语转化为文本。
Audio
↓
Neural Network
↓
Words
神经网络可以从用户行为中学习模式,并帮助预测哪些内容或产品可能对某人有用。
大型神经网络也可以用于生成文本、图像、音频、代码、视频等更多内容。
这些系统比我们构建的小型 XOR 网络复杂得多,但它们仍然依赖于从数据中学习参数的相同基本思想。
到目前为止,我们已经介绍了很多。
以下是完整的训练过程:
Data
↓
Neural Network
↓
Prediction
↓
Loss
↓
Backpropagation
↓
Update Parameters
↓
Repeat
训练完成后,我们使用学习到的参数对新数据进行预测:
New Data
↓
Trained Neural Network
↓
Prediction
这就是神经网络训练的基本思想。
如果你记不住本教程中的每个方程,也没有关系。
从这些概念开始。
我们给网络的数字。
x₁, x₂, x₃...
决定输入对神经元影响程度的数值。
w₁, w₂, w₃...
为神经元提供更大灵活性的额外值。
b
激活函数用于变换神经元输出,使网络能够学习非线性模式。
常见例子包括:
ReLU
Tanh
Sigmoid
将数据从输入端向输出端传送。
Input → Hidden Layers → Output
衡量预测结果与正确答案之间差异的指标。
通过在网络中逆向传播来计算梯度。
利用这些梯度来更新网络的参数。
整个学习过程可以总结为:
Predict
↓
Measure Error
↓
Calculate Gradients
↓
Update Parameters
↓
Repeat
如果你想继续用 Python 学习神经网络,则无需直接跳入复杂的研究论文。
一个有用的学习路径是:
Python
↓
NumPy
↓
Basic Linear Algebra
↓
Probability & Statistics
↓
Machine Learning Basics
↓
Neural Networks
↓
PyTorch
↓
Deep Learning
↓
Computer Vision / NLP / Generative AI
你也可以通过构建小项目来学习。
例如:
异或分类器
房价预测器
手写数字分类器
简单图像分类器
垃圾信息分类器
能学习数学函数的神经网络
项目不必规模庞大。你完全理解的小项目往往比你不懂就照搬代码的大项目更有用。
现代 AI 系统中的神经网络往往包含巨量参数,因而看起来令人望而生畏。
但其基本思想其实非常简单。
神经网络以数字作为输入,利用权重和偏置将它们组合,应用数学函数,得到预测,评估预测的误差,然后根据误差调整参数。
这个循环如下所示:
Input
↓
Weighted Calculations
↓
Activation Functions
↓
Prediction
↓
Loss
↓
Gradients
↓
Parameter Updates
↓
Repeat
这就是基础。
本教程中我们搭建的 XOR 网络相较于现代 AI 中使用的神经网络来说非常小。但你刚才学到的这些思想(参数、层、激活函数、前向传播、损失、反向传播、梯度和优化)是深度学习中反复出现的基本概念。
下次当你听说一个 AI 模型拥有数百万甚至数十亿个参数时,可能仍会感到吃惊。
但在如此庞大的规模之下,基本的学习循环仍然熟悉:
进行预测。
测量误差。
找出如何改进。
更新参数。
再次尝试。
这就是神经网络背后的核心思想。
编码愉快,持续学习!
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。