从零手写一个二分类神经网络
综合运用矩阵乘法、ReLU/Sigmoid 激活、交叉熵损失、反向传播——一次性串联所有知识点。
学完本案例你将理解:神经网络训练的完整数学闭环——矩阵变换数据,激活引入非线性,交叉熵衡量好坏,反向传播传递梯度。
生活引入
区分两种水果——光看大小不够
苹果和橙子混在一起。只看大小不够——有些小苹果和大橙子差不多大。你需要两个维度(大小 + 颜色),并且需要一个「非直线」的分界线——这就是神经网络用武之地。单层线性模型只能画直线,加了隐藏层和非线性激活函数,才能画出曲线边界。
直观理解
输入(2)
隐藏(8)+ReLU
输出(1)+Sigmoid
概率 0~1
每个组件对应的数学模块:
矩阵乘法线性代数
ReLU/Sigmoid可导函数
交叉熵信息论
链式法则微积分
梯度下降最优化
数学定义
\[ z_1 = X W_1 + b_1,\ a_1 = \text{ReLU}(z_1),\quad z_2 = a_1 W_2 + b_2,\ a_2 = \sigma(z_2) \]Sigmoid + 交叉熵的梯度自动化简为最简洁的形式:\(\frac{\partial L}{\partial z_2} = a_2 - y\)
Python 动手实践
实例
import numpy as np
np.random.seed(42)
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def relu(z): return np.maximum(0, z)
def relu_derivative(z): return (z > 0).astype(float)
class TwoLayerNN:
def __init__(self, n_input, n_hidden, lr=0.1):
self.W1 = np.random.randn(n_input, n_hidden) * 0.5
self.b1 = np.zeros(n_hidden)
self.W2 = np.random.randn(n_hidden, 1) * 0.5
self.b2 = np.zeros(1)
self.lr = lr
self.loss_history = []
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = relu(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
self.a2 = sigmoid(self.z2)
return self.a2
def compute_loss(self, y_pred, y_true, eps=1e-9):
y_pred = np.clip(y_pred, eps, 1 - eps)
return -np.mean(y_true * np.log(y_pred) +
(1 - y_true) * np.log(1 - y_pred))
def backward(self, X, y_true):
n = X.shape[0]
y_true = y_true.reshape(-1, 1)
dz2 = (self.a2 - y_true) / n # Sigmoid+CE 化简
dW2 = self.a1.T @ dz2
db2 = np.sum(dz2, axis=0)
da1 = dz2 @ self.W2.T # 链式法则回传
dz1 = da1 * relu_derivative(self.z1)
dW1 = X.T @ dz1
db1 = np.sum(dz1, axis=0)
self.W2 -= self.lr * dW2
self.b2 -= self.lr * db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1
def fit(self, X, y, epochs=1000):
for epoch in range(epochs):
y_pred = self.forward(X)
loss = self.compute_loss(y_pred, y)
self.loss_history.append(loss)
self.backward(X, y)
if epoch % 200 == 0:
acc = np.mean((y_pred.flatten() > 0.5) == y)
print(f"RUNOOB epoch {epoch:4d} loss={loss:.4f} acc={acc:.3f}")
def predict(self, X):
return (self.forward(X).flatten() > 0.5).astype(int)
# 同心圆数据(非线性可分)
n = 300
theta = np.random.uniform(0, 2 * np.pi, n)
r_inner = np.random.normal(1.0, 0.15, n // 2)
r_outer = np.random.normal(2.5, 0.15, n // 2)
X = np.vstack([
np.c_[r_inner * np.cos(theta[:n//2]), r_inner * np.sin(theta[:n//2])],
np.c_[r_outer * np.cos(theta[n//2:]), r_outer * np.sin(theta[n//2:])],
])
y = np.array([0] * (n // 2) + [1] * (n // 2))
model = TwoLayerNN(n_input=2, n_hidden=8, lr=0.5)
model.fit(X, y, epochs=1000)
print(f"\nRUNOOB 最终准确率: {np.mean(model.predict(X) == y):.2%}")
print(f"损失: {model.loss_history[0]:.4f} -> {model.loss_history[-1]:.4f}")
np.random.seed(42)
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def relu(z): return np.maximum(0, z)
def relu_derivative(z): return (z > 0).astype(float)
class TwoLayerNN:
def __init__(self, n_input, n_hidden, lr=0.1):
self.W1 = np.random.randn(n_input, n_hidden) * 0.5
self.b1 = np.zeros(n_hidden)
self.W2 = np.random.randn(n_hidden, 1) * 0.5
self.b2 = np.zeros(1)
self.lr = lr
self.loss_history = []
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = relu(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
self.a2 = sigmoid(self.z2)
return self.a2
def compute_loss(self, y_pred, y_true, eps=1e-9):
y_pred = np.clip(y_pred, eps, 1 - eps)
return -np.mean(y_true * np.log(y_pred) +
(1 - y_true) * np.log(1 - y_pred))
def backward(self, X, y_true):
n = X.shape[0]
y_true = y_true.reshape(-1, 1)
dz2 = (self.a2 - y_true) / n # Sigmoid+CE 化简
dW2 = self.a1.T @ dz2
db2 = np.sum(dz2, axis=0)
da1 = dz2 @ self.W2.T # 链式法则回传
dz1 = da1 * relu_derivative(self.z1)
dW1 = X.T @ dz1
db1 = np.sum(dz1, axis=0)
self.W2 -= self.lr * dW2
self.b2 -= self.lr * db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1
def fit(self, X, y, epochs=1000):
for epoch in range(epochs):
y_pred = self.forward(X)
loss = self.compute_loss(y_pred, y)
self.loss_history.append(loss)
self.backward(X, y)
if epoch % 200 == 0:
acc = np.mean((y_pred.flatten() > 0.5) == y)
print(f"RUNOOB epoch {epoch:4d} loss={loss:.4f} acc={acc:.3f}")
def predict(self, X):
return (self.forward(X).flatten() > 0.5).astype(int)
# 同心圆数据(非线性可分)
n = 300
theta = np.random.uniform(0, 2 * np.pi, n)
r_inner = np.random.normal(1.0, 0.15, n // 2)
r_outer = np.random.normal(2.5, 0.15, n // 2)
X = np.vstack([
np.c_[r_inner * np.cos(theta[:n//2]), r_inner * np.sin(theta[:n//2])],
np.c_[r_outer * np.cos(theta[n//2:]), r_outer * np.sin(theta[n//2:])],
])
y = np.array([0] * (n // 2) + [1] * (n // 2))
model = TwoLayerNN(n_input=2, n_hidden=8, lr=0.5)
model.fit(X, y, epochs=1000)
print(f"\nRUNOOB 最终准确率: {np.mean(model.predict(X) == y):.2%}")
print(f"损失: {model.loss_history[0]:.4f} -> {model.loss_history[-1]:.4f}")
RUNOOB epoch 0 loss=0.6932 acc=0.497 RUNOOB epoch 200 loss=0.1494 acc=0.930 RUNOOB epoch 400 loss=0.0615 acc=0.967 RUNOOB epoch 600 loss=0.0272 acc=0.990 RUNOOB epoch 800 loss=0.0134 acc=1.000 RUNOOB epoch 1000 loss=0.0075 acc=1.000 RUNOOB 最终准确率: 100.00% 损失: 0.6932 -> 0.0075
AI 中的应用场景
| 场景 | 与本案例的关系 |
|---|---|
| 图像分类 | CNN = 本案例的矩阵乘法换成卷积操作 |
| 文本分类 | BERT 微调 = 本案例 + Transformer 编码器 |
| 异常检测 | 输出 0~1 的异常概率——就是本案例的二分类特例 |
