4、前向传播测试:SwiGLU前向计算实现、与数学公式的逐元素验证、批量测试与随机种子控制

好,咱们进入第四步。前向传播测试,说白了就是验证你写的代码算出来的结果,跟数学公式是不是一回事。我见过不少同学,代码跑通了就以为万事大吉,结果一比对数值,差了好几个数量级——嗯,这种坑我踩过不止一次。

4.1 SwiGLU前向计算实现

先回顾一下SwiGLU的数学公式:

SwiGLU(x) = Swish(W₁x + b₁) ⊗ (W₂x + b₂)

其中Swish激活函数是:

Swish(x) = x · σ(βx)

这里的σ是sigmoid函数,β是个可学习参数,默认取1.0。我个人习惯把β叫做「门控温度」,调大调小会影响激活曲线的平滑度。

下面是我写的一个简洁实现,直接用PyTorch的底层操作,不依赖任何高级封装——这样方便咱们逐元素验证:

import torch
import torch.nn.functional as F

def swish(x, beta=1.0):
    """Swish激活函数:x * sigmoid(beta * x)"""
    return x * torch.sigmoid(beta * x)

def swiglu_forward(x, w1, b1, w2, b2, beta=1.0):
    """
    SwiGLU前向传播
    x: 输入张量, shape [batch_size, d_model]
    w1, b1: 第一个线性层的权重和偏置
    w2, b2: 第二个线性层的权重和偏置
    """
    # 两条分支
    branch1 = F.linear(x, w1, b1)  # 经过Swish
    branch2 = F.linear(x, w2, b2)  # 直接线性
    
    # Swish激活
    activated = swish(branch1, beta)
    
    # 逐元素相乘(门控机制)
    output = activated * branch2
    
    return output

你看,代码其实就这几行。但要注意,F.linear默认的权重形状是[out_features, in_features],我第一次写的时候把维度搞反了,结果算出来的值全是NaN——排查了半天才发现是权重转置的问题。

⚠️ 维度陷阱: PyTorch的F.linear接受的权重形状是[out_features, in_features],千万别跟torch.mm[in_features, out_features]搞混了。

4.2 与数学公式的逐元素验证

代码写完了,怎么证明它是对的?我的做法是:手动算一个极简例子,然后跟代码结果逐元素比对。

咱们构造一个batch_size=2, d_model=4的小例子:

# 设置随机种子,保证可复现
torch.manual_seed(42)

# 构造极小规模的输入
x = torch.randn(2, 4)
w1 = torch.randn(3, 4)  # 映射到3维隐藏层
b1 = torch.randn(3)
w2 = torch.randn(3, 4)
b2 = torch.randn(3)

# 调用我们的函数
output = swiglu_forward(x, w1, b1, w2, b2)

# 手动计算第一个样本的第一个元素,验证
x0 = x[0]
# 分支1
branch1_0 = x0 @ w1.T + b1  # 注意转置
swish_0 = branch1_0 * torch.sigmoid(branch1_0)  # beta=1
# 分支2
branch2_0 = x0 @ w2.T + b2
# 逐元素乘
manual_output_0 = swish_0 * branch2_0

# 比对
print("代码输出:", output[0])
print("手动计算:", manual_output_0)
print("是否一致:", torch.allclose(output[0], manual_output_0, atol=1e-6))

为什么会用allclose而不是直接判等?因为浮点数运算有精度误差,直接==比较大概率会失败。我建议容忍度设到1e-6就够用了,除非你训练的是半精度模型。

✅ 验证结果: 如果输出True,说明你的前向实现跟数学公式完全一致。如果输出False,别慌——检查一下权重转置、偏置广播、sigmoid计算这三个地方,90%的问题出在这里。

4.3 批量测试与随机种子控制

单样本验证通过后,咱们得做批量测试。我习惯写一个测试函数,覆盖不同形状、不同batch size、不同beta值的情况:

def test_swiglu_forward():
    """批量测试SwiGLU前向传播"""
    test_cases = [
        {"batch": 1, "d_model": 4, "d_ff": 8, "beta": 1.0},
        {"batch": 4, "d_model": 8, "d_ff": 16, "beta": 0.5},
        {"batch": 16, "d_model": 32, "d_ff": 64, "beta": 2.0},
        {"batch": 2, "d_model": 128, "d_ff": 256, "beta": 1.0},
    ]
    
    for i, case in enumerate(test_cases):
        # 固定种子,确保每次测试可复现
        torch.manual_seed(42 + i)
        
        x = torch.randn(case["batch"], case["d_model"])
        w1 = torch.randn(case["d_ff"], case["d_model"])
        b1 = torch.randn(case["d_ff"])
        w2 = torch.randn(case["d_ff"], case["d_model"])
        b2 = torch.randn(case["d_ff"])
        
        output = swiglu_forward(x, w1, b1, w2, b2, beta=case["beta"])
        
        # 验证形状
        assert output.shape == (case["batch"], case["d_ff"]), \
            f"形状错误: 期望 {(case['batch'], case['d_ff'])}, 得到 {output.shape}"
        
        # 验证数值范围(SwiGLU输出不会爆炸)
        assert not torch.isnan(output).any(), f"测试用例{i}出现NaN"
        assert not torch.isinf(output).any(), f"测试用例{i}出现Inf"
        
        print(f"测试用例{i+1}通过: batch={case['batch']}, "
              f"d_model={case['d_model']}, d_ff={case['d_ff']}, "
              f"beta={case['beta']}")
    
    print("所有批量测试通过!")

# 运行测试
test_swiglu_forward()

关于随机种子控制,我有几点经验:

  • 每个测试用例用不同的种子偏移torch.manual_seed(42 + i),避免不同用例之间产生相关性
  • 在测试函数内部设置种子:不要依赖外部环境,否则换个机器就跑不通了
  • 记录种子值:我曾经有个bug,复现了三天才发现是种子没固定——从那以后,我每个实验的配置文件里都记着种子号
💡 小技巧: 如果你用GPU跑测试,记得加上torch.cuda.manual_seed_all(seed),否则GPU上的随机操作(比如dropout)每次结果都不一样。

4.4 知识体系总览

下面这张图梳理了本章的核心逻辑,从代码实现到验证闭环:

SwiGLU前向传播测试流程 输入 x, w1, b1, w2, b2 分支1: F.linear(x, w1, b1) 分支2: F.linear(x, w2, b2) Swish激活: x * sigmoid(βx) 保持原值(门控信号) 逐元素相乘: activated * branch2 输出 SwiGLU(x) 逐元素验证 allclose 比对 随机种子控制

整个流程其实就三步:线性变换 → Swish激活 + 门控信号 → 逐元素相乘。验证环节我习惯用allclose做数值比对,种子控制则保证每次跑的结果一模一样——这样出bug了也能精准复现。

好了,前向传播测试就到这里。代码跑通了,验证也过了,下一步就可以放心地把它集成到你的模型里了。


公众号:蓝海资料掘金营,微信deep3321