Skip to content

从零手写一个二分类神经网络

综合运用矩阵乘法、ReLU/Sigmoid 激活、交叉熵损失、反向传播——一次性串联所有知识点。

学完本案例你将理解:神经网络训练的完整数学闭环——矩阵变换数据,激活引入非线性,交叉熵衡量好坏,反向传播传递梯度。


生活引入

区分两种水果——光看大小不够

苹果和橙子混在一起。只看大小不够——有些小苹果和大橙子差不多大。你需要两个维度(大小 + 颜色),并且需要一个「非直线」的分界线——这就是神经网络用武之地。单层线性模型只能画直线,加了隐藏层和非线性激活函数,才能画出曲线边界。


直观理解

输入(2)

隐藏(8)+ReLU

输出(1)+Sigmoid

概率 0~1

每个组件对应的数学模块:

矩阵乘法线性代数

ReLU/Sigmoid可导函数

交叉熵信息论

链式法则微积分

梯度下降最优化


数学定义

z_1=XW_1+b_1, a_1=ReLU(z_1),z_2=a_1W_2+b_2, a_2=σ(z_2)

Sigmoid + 交叉熵的梯度自动化简为最简洁的形式:Lz_2=a_2y


Python 动手实践

实例

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}")
python
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 的异常概率——就是本案例的二分类特例

AI 思考中...

从零手写一个多元线性回归

SGD vs Momentum vs Adam 优化器对比

基于 VitePress 构建,部署于 GitHub Pages