嘿,朋友!我是Agnes,一个刚刚读完“整个互联网”的AI助手。既然你想从零开始手撸神经网络,那咱们就把那些花里胡哨的框架先放一边,回到本质,看看这玩意儿到底是怎么“思考”的。
很多人一听“深度学习”就头大,觉得这是数学家或者顶级科学家才懂的黑科技。其实,神经网络的本质超级朴素——它就是一个能自动调整参数的函数。
准备好了吗?咱们泡杯咖啡,我从最基础的数学开始,带你一步步拆解,最后写出一个能真正识别数字的神经网络。
第一章:别怕数学,我们只学三个动作
神经网络的核心运算只有三个:矩阵乘法、加法、和一个激活函数。就这三个!
1.1 什么是“神经元”?
想象一下,你的大脑里有几十亿个神经元。每个神经元都在做一件小事:
- 接收一堆信号(输入 \(x_1, x_2, x_3...\))
- 给每个信号打个分(权重 \(w_1, w_2, w_3...\))
- 把这些加权后的信号加起来,再加上一个偏置项 \(b\)
- 如果总和超过了某个阈值,就“兴奋”一下,发出信号
用数学公式表示,就是: $\(z = x_1w_1 + x_2w_2 + ... + x_nw_n + b\)\( 或者写成矩阵形式(更高效): \)\(z = X \cdot W + b\)$
其中:
- \(X\) 是输入向量
- \(W\) 是权重向量
- \(b\) 是偏置(.bias,让模型更灵活,不用非要从0开始)
1.2 为什么要加激活函数?
如果不加激活函数,多层神经网络叠加起来,终究还是一个线性函数(一层线性+另一层线性=还是线性)。线性函数解决不了复杂问题,比如分类、图像识别。
我们需要一个非线性的东西,让网络能学习复杂的曲线。最常用的有两个:
- Sigmoid:把结果压缩到 (0, 1) 之间。适合输出概率。 $\(\sigma(z) = \frac{1}{1 + e^{-z}}\)$
- ReLU(Rectified Linear Unit):大于0保留,小于0变成0。 $\(f(z) = \max(0, z)\)$ > 这是目前最常用的激活函数,因为计算简单且能缓解“梯度消失”问题。
1.3 反向传播:让网络“认错并改进”
神经网络一开始是瞎猜的。它怎么知道猜错了?
- 计算损失(Loss):预测值和真实值之间的差距。
- 计算梯度:损失函数对每个权重参数的导数,告诉我们要往哪个方向调整权重,能让损失变小。
- 更新权重:沿着梯度的反方向,小步走(学习率 \(\eta\))。
\[w_{new} = w_{old} - \eta \cdot \frac{\partial Loss}{\partial w}\]
这个过程叫反向传播(Backpropagation)。听起来很吓人?其实它就是链式法则(Chain Rule)的应用。
第二章:从零实现一个最简单的神经网络
现在,我们不导入 tensorflow 或 pytorch,只用 Python 和 numpy,手写一个三层神经网络(输入层 -> 隐藏层 -> 输出层)。
2.1 任务:用 Python 实现逻辑回归(二分类)
我们要解决一个问题:给定一个人的身高和体重,预测他是男生还是女生。
第一步:定义 Sigmoid 和 ReLU 函数
import numpy as np
# 定义激活函数
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def relu(z):
return np.maximum(0, z)
# 定义导数,用于反向传播
def sigmoid_derivative(z):
s = sigmoid(z)
return s * (1 - s)
def relu_derivative(z):
return (z > 0).astype(float)
第二步:初始化权重
权重必须是随机初始化的,不能全为0,否则所有神经元都会学到同样的东西。
np.random.seed(42) # 固定随机种子,保证结果可复现
# 网络结构:2个输入 -> 4个隐藏神经元 -> 1个输出
input_size = 2
hidden_size = 4
output_size = 1
# 随机初始化权重和偏置
W1 = np.random.randn(input_size, hidden_size) * 0.5 # 输入层到隐藏层权重
b1 = np.zeros((1, hidden_size)) # 隐藏层偏置
W2 = np.random.randn(hidden_size, output_size) * 0.5 # 隐藏层到输出层权重
b2 = np.zeros((1, output_size)) # 输出层偏置
第三步:前向传播(Forward Pass)
def forward_propagation(X, W1, b1, W2, b2):
# 第一层(隐藏层)
Z1 = np.dot(X, W1) + b1 # 线性部分:X·W + b
A1 = relu(Z1) # 激活部分:ReLU
# 第二层(输出层)
Z2 = np.dot(A1, W2) + b2 # 线性部分
A2 = sigmoid(Z2) # 激活部分:Sigmoid(输出概率)
return Z1, A1, Z2, A2
第四步:计算损失(Cost Function)
我们用二元交叉熵损失(Binary Cross Entropy Loss),这是分类任务的标准做法。
def compute_loss(A2, Y):
m = Y.shape[0] # 样本数量
# 防止取对数出现无穷大,加一个极小值 epsilon
epsilon = 1e-8
loss = -np.sum(Y * np.log(A2 + epsilon) + (1 - Y) * np.log(1 - A2 + epsilon)) / m
return loss
第五步:反向传播(Backward Pass)
这是最关键的一步。我们要计算损失对每个参数的梯度。
def backward_propagation(X, Y, Z1, A1, Z2, A2, W1, W2):
m = Y.shape[0]
# 输出层的误差
dZ2 = A2 - Y # 导数推导结果:(A2 - Y) * sigmoid'(Z2) 简化后
dW2 = np.dot(A1.T, dZ2) / m
db2 = np.sum(dZ2, axis=0, keepdims=True) / m
# 隐藏层的误差
dA1 = np.dot(dZ2, W2.T)
dZ1 = dA1 * relu_derivative(Z1)
dW1 = np.dot(X.T, dZ1) / m
db1 = np.sum(dZ1, axis=0, keepdims=True) / m
return dW1, db1, dW2, db2
第六步:更新权重(梯度下降)
def update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate=0.1):
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
return W1, b1, W2, b2
第七步:训练循环
def train_neural_network(X, Y, epochs=1000, learning_rate=0.1):
# 初始化权重(省略,见上文)
for i in range(epochs):
# 前向传播
Z1, A1, Z2, A2 = forward_propagation(X, W1, b1, W2, b2)
# 计算损失
loss = compute_loss(A2, Y)
# 反向传播
dW1, db1, dW2, db2 = backward_propagation(X, Y, Z1, A1, Z2, A2, W1, b1, W2, b2)
# 更新权重
W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
# 每100次打印一次损失,看看是否在下降
if i % 100 == 0:
print(f"Epoch {i}, Loss: {loss:.4f}")
return W1, b1, W2, b2
2.2 完整可运行的代码示例
下面是一个完整的、可以直接运行的 Python 脚本。你可以复制粘贴到 Jupyter Notebook 或 Python 文件中运行。
import numpy as np
import matplotlib.pyplot as plt
# 1. 生成模拟数据:身高(cm)和体重(kg),标签:0=女,1=男
np.random.seed(42)
n_samples = 100
# 女生:身高160cm左右,体重55kg左右
female_height = np.random.normal(160, 5, n_samples//2)
female_weight = np.random.normal(55, 3, n_samples//2)
female_label = np.zeros(n_samples//2)
# 男生:身高175cm左右,体重75kg左右
male_height = np.random.normal(175, 5, n_samples//2)
male_weight = np.random.normal(75, 3, n_samples//2)
male_label = np.ones(n_samples//2)
# 合并数据
X = np.vstack((female_height, female_weight)).T # 输入特征:2维
Y = np.hstack((female_label, male_label)).reshape(-1, 1) # 标签:1维
# 归一化(重要!让输入在0-1之间,加速训练)
X = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
# 2. 定义神经网络组件
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def relu(z):
return np.maximum(0, z)
def sigmoid_derivative(z):
s = sigmoid(z)
return s * (1 - s)
def relu_derivative(z):
return (z > 0).astype(float)
# 3. 初始化参数
input_size = 2
hidden_size = 4
output_size = 1
W1 = np.random.randn(input_size, hidden_size) * 0.5
b1 = np.zeros((1, hidden_size))
W2 = np.random.randn(hidden_size, output_size) * 0.5
b2 = np.zeros((1, output_size))
# 4. 训练函数
def train(X, Y, epochs=1000, lr=0.5):
for i in range(epochs):
# 前向传播
Z1 = np.dot(X, W1) + b1
A1 = relu(Z1)
Z2 = np.dot(A1, W2) + b2
A2 = sigmoid(Z2)
# 计算损失
epsilon = 1e-8
loss = -np.sum(Y * np.log(A2 + epsilon) + (1 - Y) * np.log(1 - A2 + epsilon)) / len(Y)
# 反向传播
dZ2 = A2 - Y
dW2 = np.dot(A1.T, dZ2) / len(Y)
db2 = np.sum(dZ2, axis=0, keepdims=True) / len(Y)
dA1 = np.dot(dZ2, W2.T)
dZ1 = dA1 * relu_derivative(Z1)
dW1 = np.dot(X.T, dZ1) / len(Y)
db1 = np.sum(dZ1, axis=0, keepdims=True) / len(Y)
# 更新参数
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
if i % 100 == 0:
print(f"Epoch {i}, Loss: {loss:.4f}")
return W1, b1, W2, b2
# 5. 开始训练
W1, b1, W2, b2 = train(X, Y, epochs=500, lr=0.5)
# 6. 预测
Z1 = np.dot(X, W1) + b1
A1 = relu(Z1)
Z2 = np.dot(A1, W2) + b2
predictions = sigmoid(Z2)
# 判断准确率
predicted_labels = (predictions > 0.5).astype(int)
accuracy = np.mean(predicted_labels == Y)
print(f"\n准确率:{accuracy * 100:.2f}%")
# 7. 可视化决策边界
plt.figure(figsize=(10, 5))
# 绘制数据点
plt.subplot(1, 2, 1)
plt.scatter(X[Y==0, 0], X[Y==0, 1], c='blue', label='Female')
plt.scatter(X[Y==1, 0], X[Y==1, 1], c='red', label='Male')
plt.title('原始数据分布(归一化后)')
plt.legend()
# 绘制决策边界
plt.subplot(1, 2, 2)
h = 0.01
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = np.dot(np.hstack((xx.ravel(), yy.ravel())).reshape(-1, 1), W1) + b1
Z = relu(Z)
Z = np.dot(Z, W2) + b2
Z = sigmoid(Z)
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, levels=1, cmap=plt.cm.RdBu, alpha=0.8)
plt.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=2)
plt.scatter(X[Y==0, 0], X[Y==0, 1], c='blue', marker='o', s=50, edgecolors='black')
plt.scatter(X[Y==1, 0], X[Y==1, 1], c='red', marker='x', s=50, edgecolors='black')
plt.title('神经网络学到的决策边界')
plt.xlabel('身高(归一化)')
plt.ylabel('体重(归一化)')
plt.tight_layout()
plt.show()
第三章:从线性到非线性,为什么深层网络更强?
你可能会问:“我只有一层隐藏层,为啥还需要‘深度’学习呢?”
好问题!我们来做一个思想实验。
3.1 线性不可分问题:XOR 门
想象一下,你要设计一个网络来判断两个输入是否不同(XOR 逻辑):
- 0, 0 -> 0
- 0, 1 -> 1
- 1, 0 -> 1
- 1, 1 -> 0
如果你只用一层线性神经元,你画一条直线永远无法把 (0,1) 和 (1,0) 这两类与 (0,0) 和 (1,1) 分开。这被称为线性不可分。
但如果你加一个隐藏层,网络可以学到:
- 第一层神经元:判断“是否至少有一个为1”
- 第二层神经元:判断“是否两个都为1”(AND)
- 最后输出:判断“是否恰好有一个为1”(XOR)
这就是多层网络的魔力:每一层都在学习新的抽象特征。
3.2 特征的层级抽象
- 浅层网络:学习边缘、颜色、纹理。
- 中层网络:学习形状、部件(如眼睛、车轮)。
- 深层网络:学习整体对象(如人脸、汽车)。
这就好比人眼看东西:
- 先看到光的明暗变化(边缘)
- 再组合成线条和轮廓
- 最后识别出这是“一只猫”
神经网络也是类似的,越深层,特征越抽象,语义信息越强。
第四章:实战进阶——用 PyTorch 实现真正的深度学习
手写的神经网络虽然有助于理解原理,但在实际工程中,我们使用 PyTorch 或 TensorFlow,因为它们:
- 自动微分:自动计算梯度,不需要手动推导反向传播公式。
- GPU加速:利用显卡并行计算,训练速度提升几十倍。
- 丰富生态:预训练模型、数据增强工具等。
下面我们用 PyTorch 实现一个更真实的例子:识别 MNIST 手写数字。
4.1 安装 PyTorch
pip install torch torchvision
4.2 完整代码:MNIST 手写数字识别
”`python import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader
1. 数据预处理
transform = transforms.Compose([
transforms.Resize((28, 28)), # 调整大小
