L2归一化能修复错误的平均池化吗

黎 浩然/ 23 10 月, 2026/ 教程/TUTORIAL, 计算机/COMPUTER/ 0 comments

L2归一化有时会掩盖错误的平均池化,却不能普遍修复它。若错误仅把一个非零句向量乘上正数,单位方向不变;若padding隐藏状态进入求和、改变了方向,归一化仍会留下错误。只比较最终余弦相似度,可能漏掉前一种问题。

文章目录
  1. 先分清注意力与池化的参与集合
  2. 分母错了,方向却可能恰好没错
  3. 分子被污染后,单位长度也救不回来
  4. 二十四项检查,把被掩盖的错误也测出来
  5. 如何把这个测试接到真实模型
  6. 全padding输入需要业务决定
  7. 来源与补发说明

这里固定token隐藏状态,分别构造零pad与非零pad,用相同代码验证两种结果。读完可以给句向量管线增加一个不依赖模型分数的池化回归测试。Read in English。

先分清注意力与池化的参与集合

注意力阶段的mask限制信息从哪些位置读取;平均池化阶段需要决定哪些输出行参加句向量的求和与计数。前一阶段传入过mask,不等于后一阶段可以对整个序列直接mean。尤其不能据此断言padding对应的输出隐藏状态一定全零。

Sentence Transformers v5.1.0的Pooling实现在普通mean分支中,把attention_mask扩展到隐藏维度,按mask求和,再除以有效位置计数,并为分母设下界。该版本还支持其他池化方式;这里讨论的公式限于没有额外token权重的普通平均。

设隐藏状态H的形状为B×L×D,mask为B×L。对一个样本,hᵢ是第i个位置的D维向量,mᵢ∈{0,1},n=Σmᵢ。n>0时,正确平均为u=Σ(mᵢhᵢ)/n。mask在最后一维广播,意味着同一个位置的所有特征一起参加或一起排除,不是给每个特征另算一种有效长度。

① 注意力mask
限制读取哪些位置
② 池化mask
选择求和行与有效计数
③ L2归一化
改变长度,不清除错误方向
原创职责图:前两个步骤确定参与集合,最后一步只处理所得向量的长度。

此前的padding_idx与注意力mask解释了零嵌入为何仍会影响注意力分母。本篇继续检查注意力之后的池化与归一化,重点是找出一个测试盲区,而不是再把“零向量”一概视为安全或危险。

分母错了,方向却可能恰好没错

把两个有效隐藏状态设为(2,0)和(0,2)。正确平均u=(1,1)。追加一行全零pad后,直接对三行求平均得到v=(2/3,2/3),显然v不等于u,但v=(2/3)u。

定义N(x)=x/‖x‖₂。对非零x和正数c,在精确数学下N(cx)=N(x),因为‖cx‖₂=c‖x‖₂。于是这两个向量归一化后都为(1/√2,1/√2)。如果单元测试只断言归一化输出接近,它会通过,即使平均分母已经用错。这里的归一化发生在池化之后;先把每个token分别单位化再平均,是另一种运算,不属于本例的抵消结论。

更一般地,若只保留有效位置的和S,却错误地除以补齐总长度L,而不是有效数量n,那么S/L=(n/L)u。只要u非零,这种错误具有相同的正比例关系。零pad只是最容易看见的特例,关键在于分子是否保持不变。

这不代表错误无害。管线若还使用未归一化向量做点积、范数阈值或其他计算,比例本身就有意义;若数学上接近零,具体归一化函数的eps下界也会影响结果。上面的抵消结论不能不加条件地推广到零向量或任意浮点实现。相关边界可接着看余弦相似度中的eps。

分子被污染后,单位长度也救不回来

仍保留原来的两个有效行,只把pad行改成(6,0)。正确mask排除它后,平均仍是(1,1)。未遮蔽平均则为((2+0+6)/3,(0+2+0)/3)=(8/3,2/3),方向与(4,1)相同,已经不是原向量的正倍数。

用查询q=(1,0)比较余弦分数:正确方向的分数为1/√2,约0.7071;污染后的分数为4/√17,约0.9701。保持查询与另一篇文档的表示不变,假设后者的单位向量为(0.8,0.6),其查询分数为0.8,那么两篇文档的顺序便会反转。这里的向量和分数都是教学构造,不是模型检索成绩,也不证明某个模型会出现同样幅度的误差。

有效行:(2,0)、(0,2)
正确平均:(1,1)
查询余弦≈0.7071
零pad:(0,0)
错误平均:(2/3,2/3)
单位方向相同
非零pad:(6,0)
错误平均:(8/3,2/3)
查询余弦≈0.9701
原创反例:保持有效行不变,只改变被错误计入的pad行。分数针对构造查询(1,0),不是模型实测。

若pad向量恰好与有效和共线,方向也可能仍然不变。因此,“有非零pad”本身不是方向必然变化的充分条件。能推出的结论是:一旦未参与位置进入求和,就不能再靠统一的比例抵消保证结果正确;需要实际检查分子及其方向。

二十四项检查,把被掩盖的错误也测出来

下面是可独立运行的Python标准库程序。池化用Fraction保留例子里的精确有理数,归一化转为浮点数并用容差比较。它只验证固定隐藏状态的归约,没有加载Transformer、运行PyTorch或测试GPU内核。

from fractions import Fraction as F
from math import sqrt, isfinite, isclose


def mean_pool(rows, mask):
    if not rows or len(rows) != len(mask):
        raise ValueError('empty input or mask length mismatch')
    d = len(rows[0])
    if not d or any(len(row) != d for row in rows):
        raise ValueError('invalid hidden dimension')
    if any(m not in (0, 1) for m in mask):
        raise ValueError('mask must contain 0 or 1')
    if any(not isfinite(x) for row in rows for x in row):
        raise ValueError('hidden states must be finite')
    count = sum(mask)
    if not count:
        raise ValueError('no participating token')
    return tuple(sum((F(row[j]) for row, m in zip(rows, mask) if m), F(0)) / count
                 for j in range(d))


def unit(v):
    length = sqrt(sum(float(x) ** 2 for x in v))
    if length == 0:
        raise ValueError('zero vector has no direction')
    return tuple(float(x) / length for x in v)


def close(a, b):
    return all(isclose(x, y, rel_tol=1e-12, abs_tol=1e-12) for x, y in zip(a, b))


checks = 0

def check(condition):
    global checks
    assert condition
    checks += 1


def reject(fn):
    try:
        fn()
    except ValueError:
        check(True)
    else:
        raise AssertionError('expected ValueError')


valid = [(2, 0), (0, 2)]
correct = mean_pool(valid, [1, 1])
check(correct == (1, 1))
zero_pad = valid + [(0, 0)]
wrong_zero = mean_pool(zero_pad, [1, 1, 1])
check(wrong_zero == (F(2, 3), F(2, 3)))
check(wrong_zero != correct)
check(close(unit(wrong_zero), unit(correct)))
nonzero_pad = valid + [(6, 0)]
wrong_nonzero = mean_pool(nonzero_pad, [1, 1, 1])
check(wrong_nonzero == (F(8, 3), F(2, 3)))
check(not close(unit(wrong_nonzero), unit(correct)))
check(mean_pool(nonzero_pad, [1, 1, 0]) == correct)
# A correct numerator with the wrong length is still only a rescaling.
wrong_denominator = tuple(x * F(2, 3) for x in correct)
check(wrong_denominator != correct)
check(close(unit(wrong_denominator), unit(correct)))
# These are synthetic cosine scores against query (1, 0).
check(unit(correct)[0] < 0.8 < unit(wrong_nonzero)[0])
for n in (1, 3, 7):
    rows = valid + [(6, 0)] * n
    check(mean_pool(rows, [1, 1] + [0] * n) == correct)
    check(mean_pool(rows, [1] * len(rows)) != correct)
check(mean_pool(list(reversed(nonzero_pad)), [0, 1, 1]) == correct)
for rows, mask in [([], []), (valid, [1]), (valid, [0, 0]),
                   ([(2, 0), (1,)], [1, 1]), (valid, [1, 2]),
                   ([(float('nan'), 0)], [0])]:
    reject(lambda rows=rows, mask=mask: mean_pool(rows, mask))
reject(lambda: unit((0, 0)))
print(f'{checks} pooling checks passed')
print('correct:', tuple(map(float, correct)))
print('unmasked nonzero pad:', tuple(map(float, wrong_nonzero)))
print('synthetic query cosine:', unit(correct)[0], unit(wrong_nonzero)[0])

本地输出为“24 pooling checks passed”。程序分别检查直接平均全部行,以及只保留有效分子却除以总长度这两种错误。先检查未归一化平均,再检查方向与构造的分数顺序;随后追加1、3、7个被排除的非零行,验证正确mask下结果不变。还把行与mask一起反转,确认参与关系跟随位置数据移动。

空输入、长度不匹配、全零mask、隐藏维度不一致、非二值mask及非有限隐藏状态均被拒绝;零句向量也不做单位化。这是本示例选择的输入契约。函数通过筛选有效行求和,不能拿它对NaN的处理去替代张量乘法语义;程序主动拒绝全部非有限输入,因此没有宣称乘以零就能消除NaN。

如何把这个测试接到真实模型

先保存一次真实前向的隐藏状态与mask,只测试池化函数。保持有效行不变,追加几行非零合成pad,并把新增mask设为0。正确的池化输出应在合理浮点误差内一致。非零pad比全零pad更有诊断价值,因为它能揭露漏掉分子mask的问题,而全零pad可能只暴露一个随后被归一化抵消的缩放。

比较应分成两个断言:原始池化值符合参考公式,最终归一化值符合参考方向。只测后者不足以证明前者正确;只测向量长度接近1更弱,因为错误方向也可以被变成单位长度。排查时同时记录有效数量、分子与归一化前范数,通常比只保存最终相似度更容易定位偏差。

随后再做端到端的补齐测试。此时要固定分词、截断、有效token的位置语义和随机因素,只改变padding数量或批内配对。若有效隐藏状态本身已经变化,就应先检查上游计算,不能把差异全归咎于mean pooling。池化层的不变性证明只针对输入有效行固定的条件,不是对整个模型的无条件保证。

最后核对模型的池化契约。CLS、末token、普通mean和带权mean不是可以随意互换的实现细节;也不要因为某些token看起来是“特殊符号”,就自行把它们从mask中去掉。是否包含它们、是否包含提示前缀,应遵循所用模型与配置,而不是用本例的二值mask替代完整模型设计。

全padding输入需要业务决定

当n=0时,平均没有数学定义。示例选择抛异常,让无参与token的问题直接暴露。库实现给分母设下界可以避免某种除零运算,但不能使一个没有有效内容的样本自动获得有意义的语义表示。业务可以拒绝、跳过或显式标记这类输入;选哪种都应在进入相似度和索引之前定义清楚。

这里的“全padding”是mask全零,不等于用户输入空字符串。分词器仍可能产生特殊token,是否参加池化需要另外核实。不要根据原始字符串长度猜测n,也不要用“没有抛错”代替检查句向量是否可用。

修复时优先保证参与集合正确,再看归一化是否符合所用相似度。能够把比例错误和方向错误分开,就能解释为什么有些测试一直通过,而换一个批次或一条pad隐藏状态后,检索顺序突然发生变化。

来源与补发说明

实现依据为Sentence Transformers v5.1.0 Pooling.py的普通mean分支,该版本于2025年8月6日发布。本文没有把它称为当前最新版本。两幅图、向量反例、排名反转与二十四项检查为原创教学内容;没有运行真实句向量模型,也没有测量检索质量。

文章日期保留2026年10月23日14:00(北京时间),实际于2026年10月25日补发。

支持

如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*