神经网络做预测,并不神秘:输入数据先与权重进行点积,再经过激活函数得到输出;训练阶段则通过损失函数衡量误差,利用反向传播计算梯度,最后用梯度下降调整权重。把这条链路拆开,很多看似复杂的 AI 模型都会变得容易理解。
一次预测是怎样完成的
对于输入向量 x 和权重向量 w,最基本的神经元会计算:
z = x · w + b
这里的 · 是点积,b 是偏置。之后,神经元通常会把 z 交给激活函数。例如 Sigmoid 函数可以把任意实数压缩到 0 到 1 之间:
sigmoid(z) = 1 / (1 + e^(-z))
这使它适合表达二分类概率。一个隐藏层网络会重复这个过程:上一层的输出成为下一层的输入,直到得到最终预测值。
激活函数也带来了非线性。如果每一层都只做线性计算,多层网络仍然可以被合并成一个线性模型,无法表达 XOR 这类非线性关系。
训练阶段的三件事
预测只是前向传播,模型真正“学会”任务还需要训练循环:
- 前向传播:使用当前权重计算预测值。
- 计算损失:比较预测值和真实标签,例如使用均方误差。
- 反向传播与更新:根据链式法则计算每个参数对损失的影响,再用梯度下降更新参数。
梯度下降的基本形式是:
parameter = parameter - learning_rate * gradient
学习率太大,参数可能在最低点附近来回跳动;太小,训练会变得缓慢。因此,学习率不是越大越好,也不是越小越稳定,而是需要结合数据和模型进行调整。
可运行示例:训练一个 XOR 网络
下面的示例只使用 Python 标准库,实现一个 2-2-1 的全连接神经网络。它的目标是学习 XOR:两个输入不同则输出 1,相同则输出 0。代码中的矩阵和梯度计算写得比较直接,适合观察每一步发生了什么;生产环境通常会使用 NumPy、PyTorch 或其他成熟框架。
将代码保存为 xor_network.py,然后运行 python xor_network.py:
import math
import random
random.seed(7)
def sigmoid(value):
return 1.0 / (1.0 + math.exp(-value))
def sigmoid_derivative(output):
return output * (1.0 - output)
# 两个输入、两个隐藏单元、一个输出单元
w_hidden = [[random.uniform(-1, 1) for _ in range(2)] for _ in range(2)]
b_hidden = [0.0, 0.0]
w_output = [random.uniform(-1, 1) for _ in range(2)]
b_output = 0.0
samples = [
([0.0, 0.0], 0.0),
([0.0, 1.0], 1.0),
([1.0, 0.0], 1.0),
([1.0, 1.0], 0.0),
]
learning_rate = 0.8
for epoch in range(10000):
total_loss = 0.0
random.shuffle(samples)
for inputs, target in samples:
# 前向传播:输入 -> 隐藏层 -> 输出层
hidden = []
for unit in range(2):
value = sum(inputs[i] * w_hidden[unit][i] for i in range(2))
hidden.append(sigmoid(value + b_hidden[unit]))
output = sigmoid(sum(hidden[i] * w_output[i] for i in range(2)) + b_output)
error = output - target
total_loss += 0.5 * error * error
# 输出层梯度
output_delta = error * sigmoid_derivative(output)
# 隐藏层梯度:把输出层的误差传回来
hidden_delta = [
output_delta * w_output[i] * sigmoid_derivative(hidden[i])
for i in range(2)
]
# 参数更新。使用更新前的输出权重计算隐藏层梯度后再更新。
old_w_output = w_output[:]
for i in range(2):
w_output[i] -= learning_rate * output_delta * hidden[i]
b_output -= learning_rate * output_delta
for unit in range(2):
for i in range(2):
w_hidden[unit][i] -= learning_rate * hidden_delta[unit] * inputs[i]
b_hidden[unit] -= learning_rate * hidden_delta[unit]
if epoch % 2000 == 0:
print(f"epoch={epoch:5d}, loss={total_loss:.6f}")
print("\\nPredictions:")
for inputs, target in samples:
hidden = [
sigmoid(sum(inputs[i] * w_hidden[unit][i] for i in range(2)) + b_hidden[unit])
for unit in range(2)
]
prediction = sigmoid(sum(hidden[i] * w_output[i] for i in range(2)) + b_output)
print(f"{inputs} -> {prediction:.4f} (target={target:.0f})")
这个例子里有几个值得留意的细节。hidden 保存隐藏层输出,output 是最终预测;output_delta 表示输出误差经过 Sigmoid 导数后的结果;hidden_delta 则继续使用链式法则把误差传播到隐藏层。每个权重的更新量都由“误差影响程度 × 输入值 × 学习率”决定。
由于随机初始化和浮点运算的影响,输出不一定每次都完全相同。只要训练后四组结果接近 0、1、1、0,就说明网络学到了 XOR 的模式。如果损失长期不下降,可以尝试调整随机种子、训练轮数或学习率,但不要只看单次预测,应同时观察损失变化。
从手写实现迁移到真实项目
手写网络适合学习点积、激活函数、梯度和参数更新之间的关系,但它不适合承担真实项目中的张量计算、自动求导和模型部署。迁移到框架时,可以把同样的流程映射为:
- 线性层负责点积和偏置。
- 激活层提供非线性变换。
- 损失函数衡量预测和标签之间的差异。
- 优化器执行梯度下降及其改进算法。
实践中应保留训练集、验证集和测试集的划分,避免用训练数据评估泛化能力。还要确认输入特征的尺度,否则某些维度可能在点积中占据过大影响。对于分类任务,损失函数、标签格式和输出层激活函数也必须匹配,不能只凭预测结果的表面数值判断模型是否正确。
一份排查清单
当网络无法学到有效结果时,可以按下面的顺序检查:
- 先用几个样本手算或打印点积,确认输入、权重和偏置的维度一致。
- 检查激活函数是否实现正确,尤其是指数运算和导数。
- 打印每轮损失,确认损失是否整体下降,而不是只看最后一次输出。
- 检查梯度更新的符号,梯度下降应当沿着减少损失的方向移动。
- 尝试更小的学习率,排除参数更新过大的问题。
- 用一个很小的数据集过拟合,验证前向传播和反向传播本身是否连通。
理解这条从点积、激活函数到反向传播和梯度下降的完整路径,才真正掌握了神经网络预测的基本机制。框架可以替你完成大量计算,但不会替你决定数据、损失函数和评估方式是否合理。