找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2380

积分

0

好友

313

主题
发表于 3 天前 | 查看: 12| 回复: 0

你是否觉得神经网络很神秘,想抛开框架从零理解它的运作?其实,它的核心不过是一系列矩阵运算和非线性变换的叠加。本文将用大约100行 Python 代码,带你手动搭建一个能实际运行的两层神经网络,用于解决一个简单的非线性二分类问题。

目标与数据准备

我们的目标是构建一个逻辑清晰、代码量小的可运行网络。为此,我们设计一个简单的Demo:

  • 使用 numpy 生成二维特征数据。
  • 人为设定一个非线性的分类规则(让线性模型难以处理)。
  • 构建一个两层的全连接网络进行分类。

首先,我们生成模拟数据,这比下载大型数据集更直观、运行更快。

import numpy as np

# 设置随机种子,保证结果可复现
np.random.seed(42)

# 生成200个二维数据点
num_samples = 200
X = np.random.randn(num_samples, 2)  # 形状: (200, 2)

# 人为创建一个非线性分类规则:x和y同号时为1,异号时为0
y = ((X[:, 0] * X[:, 1]) > 0).astype(int)  # 形状: (200,)
y = y.reshape(-1, 1)  # 变为列向量 (200, 1)

简单来说:

  • X:输入特征,200行2列,每一行是一个样本点 (x1, x2)
  • y:对应标签,取值为0或1,告诉模型每个样本属于哪一类。

网络结构拆解

在写代码前,先用通俗语言描述网络的工作流程:

  1. 第一层(全连接):将2维输入映射到一个16维的隐藏层。
  2. 激活函数(ReLU):对隐藏层输出进行非线性变换,将负数置零。
  3. 第二层(全连接):将16维隐藏层输出映射到1维。
  4. 激活函数(Sigmoid):将最终输出压缩到(0, 1)区间,作为预测概率。
  5. 损失计算:使用交叉熵衡量预测与真实标签的差距。
  6. 反向传播:通过链式法则计算梯度,并更新网络权重。

对应的数学公式为:

  • 第一层:z1 = X @ W1 + b1a1 = relu(z1)
  • 第二层:z2 = a1 @ W2 + b2y_pred = sigmoid(z2)

初始化参数与定义激活函数

接下来,我们初始化网络参数并定义两个关键的激活函数。

# 定义网络结构维度
input_dim = 2
hidden_dim = 16
output_dim = 1  # 二分类,输出一个概率值

# 初始化权重和偏置(使用小随机数)
W1 = 0.01 * np.random.randn(input_dim, hidden_dim)
b1 = np.zeros((1, hidden_dim))
W2 = 0.01 * np.random.randn(hidden_dim, output_dim)
b2 = np.zeros((1, output_dim))

# 定义ReLU激活函数
def relu(x):
    return np.maximum(0, x)

# 定义Sigmoid激活函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

这里只需建立初步印象:

  • W1:一个2x16的矩阵,负责将二维输入投射到16维空间。
  • b1:偏置项,维度为1x16。
  • relu:将所有负输入值变为0,正数保持不变。
  • sigmoid:将任意实数映射到(0, 1)之间,完美适配概率解释。

实现前向传播与损失函数

前向传播指的是数据从输入到输出通过网络的全过程。

def forward(X):
    """前向传播,返回各层中间结果,便于后续反向传播。"""
    z1 = X @ W1 + b1          # (N, 2) @ (2, 16) -> (N, 16)
    a1 = relu(z1)             # (N, 16)
    z2 = a1 @ W2 + b2         # (N, 16) @ (16, 1) -> (N, 1)
    y_pred = sigmoid(z2)      # (N, 1)
    return z1, a1, z2, y_pred

def binary_cross_entropy(y_true, y_pred, eps=1e-8):
    """计算二分类交叉熵损失。"""
    y_pred = np.clip(y_pred, eps, 1 - eps)  # 防止log(0)计算错误
    loss = - (y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
    return np.mean(loss)

forward 函数返回每一层的中间结果 (z1a1z2),这在反向传播计算梯度时会用到,避免了重复计算。

核心:反向传播与参数更新

神经网络的大部分“神秘感”源于反向传播。但对于我们这个小网络,它本质上是应用链式法则进行的一系列矩阵求导运算。

def train_step(X, y, lr=0.1):
    """
    单步训练:执行一次前向传播、反向传播和参数更新。
    此处使用批量梯度下降。
    """
    global W1, b1, W2, b2

    num_samples = X.shape[0]

    # 1. 前向传播
    z1, a1, z2, y_pred = forward(X)
    loss = binary_cross_entropy(y, y_pred)

    # 2. 反向传播
    # 输出层梯度:对于二分类交叉熵+sigmoid,有 dz2 = y_pred - y
    dz2 = y_pred - y                        # (N, 1)
    dW2 = a1.T @ dz2 / num_samples         # (16, N) @ (N, 1) -> (16, 1)
    db2 = np.sum(dz2, axis=0, keepdims=True) / num_samples  # (1, 1)

    # 将梯度传播回隐藏层
    da1 = dz2 @ W2.T                        # (N, 1) @ (1, 16) -> (N, 16)
    dz1 = da1.copy()
    dz1[z1 <= 0] = 0  # ReLU的梯度:输入<=0的位置,梯度为0

    dW1 = X.T @ dz1 / num_samples           # (2, N) @ (N, 16) -> (2, 16)
    db1 = np.sum(dz1, axis=0, keepdims=True) / num_samples  # (1, 16)

    # 3. 参数更新(梯度下降)
    W1 -= lr * dW1
    b1 -= lr * db1
    W2 -= lr * dW2
    b2 -= lr * db2

    return loss

你可以把这个过程当作一个模板来记忆:

  • 输出层梯度dz2 = y_pred - y 这个公式在二分类(交叉熵损失 + Sigmoid激活)中非常经典。
  • 权重梯度:通常是 (上一层输出).T @ (当前层梯度)
  • 偏置梯度:对当前层梯度沿批次(batch)维度求和即可。
  • ReLU梯度:输入为正时梯度为1,输入非正时梯度为0。代码通过 z1 <= 0 来判断。

执行训练循环

有了 train_step 函数,训练循环就变得非常简洁明了。

# 开始训练
epochs = 1000
for epoch in range(1, epochs + 1):
    loss = train_step(X, y, lr=0.5)  # 使用稍大的学习率加速收敛

    if epoch % 100 == 0:
        # 计算并打印当前准确率
        _, _, _, y_pred = forward(X)
        y_label = (y_pred > 0.5).astype(int)
        acc = (y_label == y).mean()
        print(f"epoch {epoch}, loss = {loss:.4f}, acc = {acc:.3f}")

在这个循环中,每次调用 train_step 就完成了一次全量数据的梯度下降。每隔100轮打印一次损失和准确率,你可以直观地看到模型性能的提升过程。

使用模型进行预测

训练完成后,我们提供一个简单的预测函数。

def predict(X_new):
    """对新数据进行预测。"""
    _, _, _, y_pred = forward(X_new)
    return (y_pred > 0.5).astype(int)

现在,你可以随意创建一些点来测试模型的分类效果:

test_points = np.array([
    [1.0, 1.0],
    [1.0, -1.0],
    [-0.5, -0.8],
    [-0.5, 0.8]
])
print(predict(test_points))

总结与扩展思考

将上面的代码片段组合起来,一个完整的、可运行的简易神经网络脚本就在100行左右。通过这个实践,希望你脑中能建立以下几个关键认知:

  • 神经网络的基础是矩阵乘法和非线性激活函数的堆叠。
  • 反向传播是误差从输出层向输入层反向传递,并应用链式法则计算梯度的过程。
  • 对于二分类问题,“Sigmoid + 交叉熵”是输出层的标准配置之一。
  • 一旦你能用 numpy 手动实现,再看 PyTorch、Keras 等深度学习框架的API,就会觉得豁然开朗。

为了加深理解,你可以尝试以下小实验,这将是未来调参的直觉来源:

  • hidden_dim(隐藏层维度)改大或改小,观察模型拟合能力的变化。
  • relu 激活函数换成 np.tanh,感受不同非线性对训练效果的影响。
  • 调整学习率 lr(例如改为0.05或1.0),观察收敛速度是变快、变慢还是训练不稳定。

理解深度学习的基础原理,是从工具使用者迈向真正理解者的关键一步。欢迎在云栈社区分享你的实验心得或提出更深层次的技术问题。




上一篇:Datadog Bits AI SRE Agent深度解析:如何实现故障根因自动调查与定位
下一篇:戴尔S2718D显示器捡漏:600元体验Type-C一线连的悬浮美学
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 00:28 , Processed in 1.227637 second(s), 45 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表