Skip to content

s05 前向传播与计算图 — exercise.py 练习指南

Download exercise.py

练习目标

通过实现三个核心模块——单层全连接前向传播、GELU 激活函数、计算图追踪,从代码层面深入理解神经网络前向传播的完整流程和计算图的概念。

预备知识

在开始练习前,确保你已经理解了以下概念(参见 demo.py 代码详解 中的详细解释):

  • 线性变换 z=Waprev+b 的矩阵维度规则
  • ReLU、Sigmoid、Tanh 三种激活函数的定义和实现
  • 中间值缓存(cache)的概念:存储 zaprevWb 供反向传播使用
  • GELU 的数学定义:GELU(z)=zΦ(z),其中 Φ(z) 是标准正态 CDF
  • 计算图的概念:将复杂表达式分解为基本运算节点的有向无环图

任务清单

任务1:实现单层全连接层的前向传播 dense_layer_forward(A_prev, W, b, activation)

  • 用到的公式
    • 线性变换:Z=WAprev+b
    • ReLU:ReLU(z)=max(0,z)
    • Sigmoid:σ(z)=11+ez
    • Tanh:tanh(z)
  • 实现步骤
    1. 计算 Z=W@Aprev+b(矩阵乘法 + 广播加法)
    2. 根据 activation 参数选择激活函数:
      • "relu"np.maximum(0, Z)
      • "sigmoid"1.0 / (1.0 + np.exp(-Z))(注意数值稳定:可先用 np.clip(Z, -500, 500)
      • "tanh"np.tanh(Z)(NumPy 内置)
      • "none"A=Z(恒等映射,无激活)
    3. 创建 cache 字典,包含 {Z: 线性输出, A_prev: 上一层激活, W: 权重矩阵, b: 偏置向量}
  • 需要调用的函数@ 运算符(矩阵乘法)、np.maximum()np.exp()np.tanh()np.clip()
  • 返回(Z, A, cache) 三元组
  • 期望输出Z.shape(n_curr, m)A.shape 也为 (n_curr, m),cache 包含四个键

任务2:实现 GELU 激活函数 gelu_exact(z)gelu_derivative(z)

  • GELU 的精确数学定义

    GELU(z)=zΦ(z)

    其中 Φ(z) 是标准正态分布的累积分布函数(CDF):

    Φ(z)=12[1+erf(z2)]

    erf(x) 是误差函数(error function)。

  • GELU 的近似实现(如果需要避免引入 scipy):

    GELU(z)0.5z[1+tanh(2π(z+0.044715z3))]
  • GELU 导数的精确形式

    GELU(z)=Φ(z)+zϕ(z)

    其中 ϕ(z) 是标准正态分布的概率密度函数(PDF):

    ϕ(z)=12πez2/2
  • 实现提示

    • 如果使用 scipy:from scipy.special import erfΦ(z)=0.5(1+erf(z/2))
    • 如果不想引入 scipy:使用 tanh 近似公式
    • 导数实现:需要同时用到 Φ(z)ϕ(z)
  • 验证GELU(0)=0GELU(2)1.95z 大时行为接近 ReLU),GELU(2)0.05

任务3:手动追踪计算图 trace_computational_graph(X)

给定表达式:

f(x1,x2,x3)=σ((x1w1+x2w2+b)w3+x3)

其中 w1=0.5, w2=0.3, w3=2.0, b=0.1σ 是 Sigmoid。

  • 任务:把这个复合函数分解为 7 个基本运算节点,每个节点执行一个简单操作(加减乘除、Sigmoid)

  • 计算图的节点链

    节点计算操作类型输入
    u1x1w1multiplyx1, w1
    u2x2w2multiplyx2, w2
    u3u1+u2addu1, u2
    u4u3+baddu3, b
    u5u4w3multiplyu4, w3
    u6u5+x3addu5, x3
    u7σ(u6)sigmoidu6
  • 实现:对每个节点,计算并记录到 graph_nodes 字典中

  • 每个节点的记录格式

    python
    graph_nodes["u1"] = {
        "value": 计算结果,
        "inputs": ["x1", "w1"],
        "op": "multiply"
    }
  • Sigmoid 实现提示σ(z)=1/(1+ez),可使用 np.exp()。为了防止数值溢出,建议先 clip 输入

  • 核心理解:这个练习让你手动体验 PyTorch/TensorFlow 底层在做什么——自动将复杂的数学表达式分解为计算图上的基本操作节点,每个节点只需要知道自己的局部操作和链式法则导数规则。

验证标准

运行 python exercise.py

  1. TODO 1:对 4 种激活函数(relu, sigmoid, tanh, none)都应输出正确的 Z.shapeA 的范围
  2. TODO 2GELU(0)=0GELU(z)z for z0(如 z=2 时约等于 1.95
  3. TODO 3:打印出 7 个节点的计算图,最终输出 u7 是一个在 (0,1) 之间的值(因为经过了 Sigmoid)

完整代码

py
# -*- coding: utf-8 -*-
"""
s05 计算图与前向传播 — 练习代码
================================
请完成以下 TODO 任务,巩固对前向传播和计算图的理解。

每个 TODO 都有详细的指示和预期输出描述。
建议先阅读 README.md,再尝试独立补全代码。
"""

import numpy as np


# ============================================================================
# TODO 1: 实现单层全连接层的前向传播
# ============================================================================
def dense_layer_forward(A_prev: np.ndarray, W: np.ndarray, b: np.ndarray,
                        activation: str = "relu") -> tuple:
    """
    实现单个全连接层的前向传播。

    数学公式:
        Z = W @ A_prev + b
        A = activation(Z)

    参数:
        A_prev: 上一层的激活值,shape (n_prev, m),其中 n_prev 是输入维度,m 是样本数
        W: 权重矩阵,shape (n_curr, n_prev)
        b: 偏置向量,shape (n_curr, 1)
        activation: 激活函数名称,可选 "relu", "sigmoid", "tanh", "none"

    返回:
        Z: 线性输出,shape (n_curr, m)
        A: 激活输出,shape (n_curr, m)
        cache: 字典,包含 Z, A_prev, W, b,供反向传播使用
    """
    # TODO: 补全以下代码

    # 步骤 1: 计算线性变换 Z = W @ A_prev + b
    # 提示: 使用 np.dot 或 @ 运算符进行矩阵乘法
    Z = None  # ← TODO: 实现 Z = W @ A_prev + b

    # 步骤 2: 应用激活函数
    # 提示: 根据 activation 参数选择对应的激活函数
    if activation == "relu":
        A = None  # ← TODO: 对 Z 应用 ReLU (np.maximum)
    elif activation == "sigmoid":
        A = None  # ← TODO: 对 Z 应用 sigmoid (1/(1+exp(-Z)))
    elif activation == "tanh":
        A = None  # ← TODO: 对 Z 应用 tanh (np.tanh)
    elif activation == "none":
        A = None  # ← TODO: 线性激活,A = Z
    else:
        raise ValueError(f"不支持的激活函数: {activation}")

    # 步骤 3: 创建 cache 字典,存储反向传播所需的所有中间值
    cache = None  # ← TODO: 创建包含 Z, A_prev, W, b 的字典

    return Z, A, cache


# ---- 测试 TODO 1 ----
def test_dense_layer():
    """测试单层全连接前向传播的实现。"""
    print("=" * 60)
    print("TODO 1 测试: 单层全连接层的前向传播")
    print("=" * 60)

    # 测试数据:3 个输入特征,2 个神经元,5 个样本
    np.random.seed(42)
    A_prev = np.random.randn(3, 5)   # (3, 5)
    W = np.random.randn(2, 3) * 0.1  # (2, 3)
    b = np.zeros((2, 1))             # (2, 1)

    for act in ["relu", "sigmoid", "tanh", "none"]:
        Z, A, cache = dense_layer_forward(A_prev, W, b, activation=act)
        if Z is None:
            print(f"  [{act}] TODO 未完成,请补全 dense_layer_forward 函数")
        else:
            print(f"  [{act}] Z.shape={Z.shape}, A.shape={A.shape}, "
                  f"A range=[{A.min():.4f}, {A.max():.4f}]")

    print()


# ============================================================================
# TODO 2: 实现 GELU 激活函数
# ============================================================================
def gelu_exact(z: np.ndarray) -> np.ndarray:
    """
    实现 GELU (Gaussian Error Linear Unit) 激活函数的精确版本。

    GELU 的精确定义:
        GELU(z) = z · Φ(z)

    其中 Φ(z) 是标准正态分布的累积分布函数 (CDF):
        Φ(z) = 0.5 * (1 + erf(z / √2))

    参数:
        z: 输入数组,任意形状

    返回:
        逐元素应用的 GELU 结果

    参考资料:
        Hendrycks & Gimpel (2016): "Gaussian Error Linear Units (GELUs)"
        https://arxiv.org/abs/1606.08415
    """
    # TODO: 实现 GELU 的精确版本
    # 提示 1: 使用 scipy.special.erf 或手动实现 erf 近似
    # 提示 2: Φ(z) = 0.5 * (1 + erf(z / sqrt(2)))
    # 提示 3: GELU(z) = z * Φ(z)
    # 如果不想引入 scipy,可以使用下面的近似公式:
    #   GELU(z) ≈ 0.5 * z * (1 + tanh(sqrt(2/π) * (z + 0.044715 * z^3)))

    result = None  # ← TODO: 实现 GELU 函数

    return result


def gelu_derivative(z: np.ndarray) -> np.ndarray:
    """
    实现 GELU 激活函数的导数。

    GELU 的导数(精确形式):
        GELU'(z) = Φ(z) + z · φ(z)

    其中 φ(z) 是标准正态分布的概率密度函数 (PDF):
        φ(z) = exp(-z²/2) / √(2π)

    参数:
        z: 输入数组

    返回:
        GELU 在 z 处的导数
    """
    # TODO: 实现 GELU 的导数
    # 提示: 需要同时用到 Φ(z) 和 φ(z)

    result = None  # ← TODO: 实现 GELU 导数

    return result


# ---- 测试 TODO 2 ----
def test_gelu():
    """测试 GELU 激活函数的实现。"""
    print("=" * 60)
    print("TODO 2 测试: GELU 激活函数")
    print("=" * 60)

    z = np.array([-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0])
    result = gelu_exact(z)

    if result is None:
        print("  TODO 未完成,请补全 gelu_exact 函数")
    else:
        print(f"  输入 z:        {z}")
        print(f"  GELU(z):       {np.round(result, 4)}")
        # 预期输出(近似值):
        # GELU(-2) ≈ -0.0454, GELU(-1) ≈ -0.1588, GELU(0) = 0,
        # GELU(0.5) ≈ 0.3457, GELU(1) ≈ 0.8413, GELU(2) ≈ 1.9546

        # 验证几个关键性质
        print(f"\n  性质验证:")
        print(f"    GELU(0) = {result[3]:.6f} (预期: 0.0)")
        print(f"    GELU(z) ≈ z for z >> 0: GELU(2)={result[6]:.4f} vs z=2.0")

    print()


# ============================================================================
# TODO 3: 追踪计算图
# ============================================================================
def trace_computational_graph(X: np.ndarray) -> dict:
    """
    对于给定的表达式,追踪并打印计算图的所有中间节点。

    表达式: f(x1, x2, x3) = sigmoid( (x1 * w1 + x2 * w2 + b) * w3 + x3 )

    任务:把这个表达式分解为计算图中的基本操作节点,
          并记录每个节点的输入、输出和操作类型。

    参数:
        X: 包含 [x1, x2, x3] 的数组,shape (3,)

    返回:
        graph_nodes: 字典,key 为节点名,value 为包含 value、inputs、op 的字典
    """
    # 给定的参数
    w1, w2, w3, b = 0.5, -0.3, 2.0, 0.1  # 权重和偏置
    x1, x2, x3 = X[0], X[1], X[2]         # 输入

    # TODO: 补全以下计算图的追踪
    # 提示: 将表达式分解为以下步骤,每一步录为一个节点:
    #   node1: u1 = x1 * w1       (乘法)
    #   node2: u2 = x2 * w2       (乘法)
    #   node3: u3 = u1 + u2       (加法)
    #   node4: u4 = u3 + b        (加法)
    #   node5: u5 = u4 * w3       (乘法)
    #   node6: u6 = u5 + x3       (加法)
    #   node7: u7 = sigmoid(u6)   (sigmoid)

    graph_nodes = {}  # 初始化计算图节点字典

    # TODO: 实现每一步计算,并记录到 graph_nodes 中
    # 示例格式:
    # graph_nodes["u1"] = {"value": 计算结果, "inputs": ["x1", "w1"], "op": "multiply"}

    # 打印计算图
    print("\n计算图结构:")
    print("-" * 40)
    for name, info in graph_nodes.items():
        inputs_str = ", ".join(info.get("inputs", []))
        print(f"  {name} = {info.get('op', '?')}({inputs_str}) = {info.get('value', '?')}")
    print("-" * 40)

    return graph_nodes


# ---- 测试 TODO 3 ----
def test_computational_graph():
    """测试计算图追踪功能。"""
    print("=" * 60)
    print("TODO 3 测试: 计算图追踪")
    print("=" * 60)

    X = np.array([1.0, 2.0, 0.5])  # x1=1.0, x2=2.0, x3=0.5
    graph = trace_computational_graph(X)

    if not graph:
        print("  TODO 未完成,请补全 trace_computational_graph 函数")
    else:
        print(f"\n  共追踪了 {len(graph)} 个计算节点")
        # 检查最终输出
        if "u7" in graph:
            print(f"  最终输出 (sigmoid): {graph['u7']['value']:.4f}")

    print()


# ============================================================================
# 主程序
# ============================================================================
if __name__ == "__main__":
    print("\n╔══════════════════════════════════════════════════════════════╗")
    print("║   s05 计算图与前向传播 — 动手练习                           ║")
    print("║   请依次完成 TODO 1, 2, 3                                    ║")
    print("╚══════════════════════════════════════════════════════════════╝\n")

    test_dense_layer()
    test_gelu()
    test_computational_graph()

    print("=" * 60)
    print("所有测试完成!请检查输出结果。")
    print("如有未通过的测试,请回到对应的 TODO 部分补全代码。")
    print("=" * 60)