Word2Vec 细节 负采样导致溢出的防范

问题

公式:

\[L= \log\sigma(v'_o{}^Tv_i) + \sum_{j=1}^{k} \log\sigma(-v'_{n_j}{}^Tv_i) \]

如果负样本碰巧是和中心词c极度相关的,向量相乘就会得到一个极大的数,sigmoid函数作用下就会变成0,而最终log0是一个无穷小,最终会不会导致数据溢出或崩溃?

解答

\[\mathbf{u}_{n_k}^\top \mathbf{v}_c \to +\infty \;\Rightarrow\; \sigma(-\mathbf{u}_{n_k}^\top \mathbf{v}_c) \to 0 \;\Rightarrow\; \log(0) \to -\infty \]

结论先行:理论上确实存在 \(-\infty\) 的风险,但在实际工程实现中,这个问题通过“数值稳定技巧”被彻底解决了,不会导致崩溃。 而且从优化角度看,这种情况恰恰是模型在“正确地惩罚错误”。

下面从三个层面详细解答:


1. 数学层面:为什么 \(-\infty\) 反而是"正确的"?

假设一个负样本 \(n_k\) 碰巧与中心词 \(c\) 语义极度相关(比如同义词),那么 \(\mathbf{u}_{n_k}^\top \mathbf{v}_c\) 是一个很大的正数。

此时目标函数中的这一项:

\[\log \sigma(-\mathbf{u}_{n_k}^\top \mathbf{v}_c) \to -\infty \]

这意味着损失变得极大(因为我们是在最大化 \(J\),\(-\infty\) 是最差的情况)。这恰恰是正确的行为:

💡 负采样的设计意图就是"把不相关的词推开"。如果一个本该是负样本的词实际上和中心词很相似,模型就应该受到强烈惩罚,梯度会非常大,迫使 \(\mathbf{u}_{n_k}\) 远离 \(\mathbf{v}_c\)。

回顾第5节推导的梯度:

\[\frac{\partial J}{\partial \mathbf{u}_{n_k}} = -\sigma(\mathbf{u}_{n_k}^\top \mathbf{v}_c) \, \mathbf{v}_c \]

当点积很大时,\(\sigma(\cdot) \to 1\),梯度 \(\to -\mathbf{v}_c\),这是一个有限值!也就是说:

量 当负样本意外相似时的行为 是否危险
目标函数值 \(J\) \(\to -\infty\) ⚠️ 理论上有问题
梯度 \(\nabla J\) \(\to -\mathbf{v}_c\)(有限) ✅ 安全,且方向正确

梯度是有限的,所以 SGD 更新不会爆炸。模型会在下一步把这个负样本推开,下一轮迭代时该项就不再是 \(-\infty\) 了。


2. 工程层面:如何防止 \(\log(0)\) 导致的 NaN/Inf?

尽管梯度安全,但目标函数值变成 -inf 仍可能导致日志记录、早停判断等环节出错。所有成熟的实现都采用了以下数值稳定技巧:

技巧一:Clamp / Clip(最常用)

在计算 \(\log \sigma(x)\) 之前,对 \(\sigma(x)\) 的输出做截断:

# Gensim / PyTorch 等框架的标准做法
EPS = 1e-7
score = sigmoid(dot_product)
score = np.clip(score, EPS, 1.0 - EPS)  # 永远不为 0 或 1
loss = np.log(score)                      # 安全!最小值为 log(1e-7) ≈ -16.1

这样 \(\log \sigma(\cdot)\) 的下界被锁定在约 \(-16.1\),永远不会出现 -inf。

技巧二:Log-Sigmoid 的稳定实现

直接计算 \(\log \sigma(x)\) 而不经过 \(\sigma\) 的中间步骤:

\[\log \sigma(x) = \log \frac{1}{1+e^{-x}} = -\log(1+e^{-x}) \]

当 \(x\) 很大时,\(e^{-x} \to 0\),上式 \(\to 0\),数值安全。
当 \(x\) 很小时(即负样本意外相似,\(-x\) 是很大的负数),改写为:

\[\log \sigma(x) = x - \log(1+e^{x}) \]

PyTorch 的 F.logsigmoid 和 NumPy 的 scipy.special.logsumexp 内部就是用这种分段策略实现的:

# PyTorch 内部等价实现
def stable_log_sigmoid(x):
    return -F.softplus(-x)  # softplus(z) = log(1+exp(z)),数值稳定

技巧三:梯度裁剪(Gradient Clipping)

作为最后一道防线:

grad = np.clip(grad, -5.0, 5.0)  # 即使梯度异常也限制在合理范围

3. 概率层面:这种情况发生的频率有多低?

负样本是从噪声分布 \(P_n(w) \propto U(w)^{3/4}\) 中随机采样的。对于一个给定的中心词 \(c\):

  • 词表大小 \(V\) 通常为 \(10^5 \sim 10^6\)
  • 与 \(c\) 真正语义相似的词最多几十到几百个
  • 随机采样 \(K=5 \sim 20\) 个负样本,恰好采到语义相似词的概率约为:

\[P(\text{collision}) \approx 1 - \left(1 - \frac{100}{10^6}\right)^{10} \approx 0.1\% \]

即使发生了,如前所述,大的梯度会在一步之内把它推开,不会持续累积。


📌 总结

你的担忧 实际情况
\(\log(0) = -\infty\) 会导致崩溃? 理论上是的,但工程中用 clip / log-sigmoid 彻底规避
梯度会不会爆炸? 不会。梯度公式中是 \(\sigma(\cdot)\) 而非 \(\log \sigma(\cdot)\),始终有界
负样本意外相似是 bug 吗? 不是。这正是模型学习的机会,大梯度会快速纠正
需要自己处理吗? 如果用 Gensim/PyTorch,框架已内置保护;如果纯 NumPy 手写,记得加 np.clip

如果你正在手写 NumPy 实现,建议在 log(sigmoid(x)) 处加上 clamp,这是唯一一个需要手动防护的地方。其余部分的梯度计算天然数值稳定。

posted @ 2026-08-29 16:09  立体风  阅读(6)  评论(0)    收藏  举报