【NCE】

零、写在前面

噪声对比估计(Noise Contrastive Estimation, 简称 NCE) 是由 Gutmann 和 Hyvärinen 在 2010 年提出的一种强大的参数估计算法。它主要用于解决**非归一化概率模型(Unnormalized Models)**中配分函数(Partition Function)难以计算的问题。

现如今,NCE 的思想深深影响了自然语言处理(如 Word2Vec)和计算机视觉(如 SimCLR、MoCo 等对比学习方法中的 InfoNCE)。

一、为什么要用 NCE?

在统计机器学习和深度学习中,我们经常需要拟合一个概率分布。对于离散数据(比如语言模型预测下一个单词),我们通常使用 Softmax 函数:

Pθ(x)=exp⁡(uθ(x))Z(θ) P_\theta(x) = \frac{\exp(u_\theta(x))}{Z(\theta)} Pθ(x)=Z(θ)exp(uθ(x))

其中:

  • uθ(x)u_\theta(x)uθ(x) 是模型(如神经网络)输出的未归一化的分数(Logits)。
  • Z(θ)=∑x∈Vexp⁡(uθ(x))Z(\theta) = \sum_{x \in V} \exp(u_\theta(x))Z(θ)=∑x∈Vexp(uθ(x)) 是配分函数(归一化常数)

痛点在于:

如果词表大小 VVV 非常大(比如几十万个词),或者 xxx 是连续的高维变量(积分代替求和),计算 Z(θ)Z(\theta)Z(θ) 的成本是极高的。如果我们用传统的最大似然估计(MLE)来优化,每次梯度下降都需要计算 Z(θ)Z(\theta)Z(θ) 及其梯度,这在工程上几乎不可行。

NCE 的目标:避开计算 Z(θ)Z(\theta)Z(θ),依然能够准确估计模型参数 θ\thetaθ。

二、核心思想

NCE的核心思想:将"无监督的生成问题"转化为"有监督的二分类问题"。

既然直接拟合数据分布太难,那我们就引入一个已知的、容易采样的噪声分布 pn(x)p_n(x)pn(x)

然后,我们训练一个二分类器,让它去区分:

  • 正样本(Data): 来自真实数据分布 pd(x)p_d(x)pd(x) 的样本,标签记为 D=1D=1D=1。
  • 负样本(Noise): 来自噪声分布 pn(x)p_n(x)pn(x) 的样本,标签记为 D=0D=0D=0。

只要我们的分类器足够强大,能够完美区分真实数据和噪声,那么它实际上也就"学会"了真实数据的分布 pd(x)p_d(x)pd(x)。

三、数学推导

1. 设定先验概率

假设我们有一批真实数据,同时我们根据噪声分布 pn(x)p_n(x)pn(x) 生成噪声数据。

设定真实数据与噪声数据的比例为 1:k1 : k1:k(即每 1 个真实样本,对应 kkk 个噪声样本)。

那么,对于任意一个进入分类器的样本,它的先验概率为:

  • 来自真实数据的概率:P(D=1)=11+kP(D=1) = \frac{1}{1+k}P(D=1)=1+k1
  • 来自噪声数据的概率:P(D=0)=k1+kP(D=0) = \frac{k}{1+k}P(D=0)=1+kk

2. 根据贝叶斯定理计算后验概率

给定一个样本 xxx,它属于真实数据(D=1D=1D=1)的真实后验概率是多少?根据贝叶斯定理:

P(D=1∣x)=P(x∣D=1)P(D=1)P(x∣D=1)P(D=1)+P(x∣D=0)P(D=0) P(D=1 | x) = \frac{P(x | D=1) P(D=1)}{P(x | D=1) P(D=1) + P(x | D=0) P(D=0)} P(D=1∣x)=P(x∣D=1)P(D=1)+P(x∣D=0)P(D=0)P(x∣D=1)P(D=1)

我们知道条件概率就是分布本身:

  • P(x∣D=1)=pd(x)P(x | D=1) = p_d(x)P(x∣D=1)=pd(x) (真实数据分布)
  • P(x∣D=0)=pn(x)P(x | D=0) = p_n(x)P(x∣D=0)=pn(x) (噪声分布)

代入先验概率,得到:

P(D=1∣x)=pd(x)⋅11+kpd(x)⋅11+k+pn(x)⋅k1+k=pd(x)pd(x)+k⋅pn(x) P(D=1 | x) = \frac{p_d(x) \cdot \frac{1}{1+k}}{p_d(x) \cdot \frac{1}{1+k} + p_n(x) \cdot \frac{k}{1+k}} = \frac{p_d(x)}{p_d(x) + k \cdot p_n(x)} P(D=1∣x)=pd(x)⋅1+k1+pn(x)⋅1+kkpd(x)⋅1+k1=pd(x)+k⋅pn(x)pd(x)

同理,它是噪声的后验概率为:

P(D=0∣x)=k⋅pn(x)pd(x)+k⋅pn(x) P(D=0 | x) = \frac{k \cdot p_n(x)}{p_d(x) + k \cdot p_n(x)} P(D=0∣x)=pd(x)+k⋅pn(x)k⋅pn(x)

3. 用 pθ(x)p_\theta(x)pθ(x)替代pd(x)p_d(x)pd(x)

在现实中,我们不知道真实的 pd(x)p_d(x)pd(x),我们的目的正是用模型 pθ(x)p_\theta(x)pθ(x) 去逼近 pd(x)p_d(x)pd(x)。因此,我们将上述公式中的 pd(x)p_d(x)pd(x) 替换为我们的模型 pθ(x)p_\theta(x)pθ(x),得到分类模型预测的概率:

Pθ(D=1∣x)=pθ(x)pθ(x)+k⋅pn(x) P_\theta(D=1 | x) = \frac{p_\theta(x)}{p_\theta(x) + k \cdot p_n(x)} Pθ(D=1∣x)=pθ(x)+k⋅pn(x)pθ(x)

Pθ(D=0∣x)=k⋅pn(x)pθ(x)+k⋅pn(x) P_\theta(D=0 | x) = \frac{k \cdot p_n(x)}{p_\theta(x) + k \cdot p_n(x)} Pθ(D=0∣x)=pθ(x)+k⋅pn(x)k⋅pn(x)

但是pθ(x)p_\theta(x)pθ(x)的计算不还是要算那个很大的配分函数吗?

NCE 作者提出一个极其大胆的假设:将归一化常数 Z(θ)Z(\theta)Z(θ) 看作是一个普通的可学习参数 ccc ,甚至在神经网络能力足够强时,直接强制假设 Z=1Z=1Z=1(即网络输出自动趋于归一化)。

因此,在 NCE 中,我们直接让 pθ(x)=exp⁡(uθ(x))p_\theta(x) = \exp(u_\theta(x))pθ(x)=exp(uθ(x)),彻底抛弃了对配分函数的全局计算。

4. 构建目标函数(交叉熵/对数似然)

既然这是一个二分类问题,我们就用标准的二元交叉熵(或者说最大化对数似然)来作为目标函数 J(θ)J(\theta)J(θ)。

对于来自真实分布 pdp_dpd 的样本 xxx,我们希望 Pθ(D=1∣x)P_\theta(D=1|x)Pθ(D=1∣x) 越大越好;

对于来自噪声分布 pnp_npn 的样本 yyy,我们希望 Pθ(D=0∣y)P_\theta(D=0|y)Pθ(D=0∣y) 越大越好。

目标函数(期望形式 )为:

J(θ)=Ex∼pdln⁡Pθ(D=1∣x)+k⋅Ey∼pnln⁡Pθ(D=0∣y) J(\theta) = \mathbb{E}{x \sim p_d} \\ln P_\\theta(D=1 \| x) + k \cdot \mathbb{E}{y \sim p_n} \\ln P_\\theta(D=0 \| y) J(θ)=Ex∼pdlnPθ(D=1∣x)+k⋅Ey∼pnlnPθ(D=0∣y)

把步骤3中的式子代入,得到 NCE 的最终目标函数公式

J(θ)=Ex∼pdln⁡pθ(x)pθ(x)+k⋅pn(x)+k⋅Ey∼pnln⁡k⋅pn(y)pθ(y)+k⋅pn(y) J(\theta) = \mathbb{E}{x \sim p_d} \left \\ln \\frac{p_\\theta(x)}{p_\\theta(x) + k \\cdot p_n(x)} \\right + k \cdot \mathbb{E}{y \sim p_n} \left \\ln \\frac{k \\cdot p_n(y)}{p_\\theta(y) + k \\cdot p_n(y)} \\right J(θ)=Ex∼pdlnpθ(x)+k⋅pn(x)pθ(x)+k⋅Ey∼pnlnpθ(y)+k⋅pn(y)k⋅pn(y)

在实际训练中,期望 E\mathbb{E}E 会被替换为经验平均(即在一个 Batch 内的平均值)。

这个其实还是可以理解的,基于大数定律,我们用样本均值替代期望。

四、为什么 NCE 有效?

我们对目标函数跑梯度下降,真能使得pθ(x)p_\theta(x)pθ(x) 收敛到真实分布 pd(x)p_d(x)pd(x) 吗?

我们求导来证明:

第一项的导数:

\\begin{align} \\ln \\frac{p_\\theta}{p_\\theta + k p_n} \&= \\ln p_\\theta - \\ln(p_\\theta + k p_n) \\ \\nabla_\\theta \\ln \\frac{p_\\theta}{p_\\theta + k p_n}\&=\\nabla_\\theta \\left( \\ln p_\\theta - \\ln(p_\\theta + k p_n) \\right)\\ \&= \\nabla_\\theta \\ln p_\\theta - \\frac{1}{p_\\theta + k p_n} \\nabla\\theta p_\\theta\\ \&= \\nabla_\\theta \\ln p_\\theta - \\frac{p_\\theta}{p_\\theta + k p_n} \\nabla\\theta \\ln p_\\theta\\ \&= \\frac{kp_n}{p_\\theta + k p_n} \\nabla\\theta \\ln p_\\theta\\ \\end{align}

第二项的导数:

ln⁡kpnpθ+kpn=ln⁡(kpn)−ln⁡(pθ+kpn)∇θln⁡kpnpθ+kpn=−pθpθ+kpn∇θln⁡pθ \begin{align} \ln \frac{k p_n}{p_\theta + k p_n} &= \ln(k p_n) - \ln(p_\theta + k p_n)\\ \nabla_\theta \ln \frac{k p_n}{p_\theta + k p_n} &= -\frac{p_\theta}{p_\theta + k p_n} \nabla_\theta \ln p_\theta \end{align} lnpθ+kpnkpn∇θlnpθ+kpnkpn=ln(kpn)−ln(pθ+kpn)=−pθ+kpnpθ∇θlnpθ

我们把期望写成积分形式:

∇θJ(θ)=Ex∼pdk⋅pn(x)pθ(x)+k⋅pn(x)∇θln⁡pθ(x)−k⋅Ey∼pnpθ(y)pθ(y)+k⋅pn(y)∇θln⁡pθ(y)=∫pd(x)k⋅pn(x)pθ(x)+k⋅pn(x)∇θln⁡pθ(x)dx−∫pn(x)⋅k⋅pθ(x)pθ(x)+k⋅pn(x)∇θln⁡pθ(x)dx=∫pd(x)−pθ(x)k⋅pn(x)pθ(x)+k⋅pn(x)∇θln⁡pθ(x)dx \begin{align} \nabla_\theta J(\theta) &= \mathbb{E}{x \sim p_d} \left \\frac{k \\cdot p_n(x)}{p_\\theta(x) + k \\cdot p_n(x)} \\nabla_\\theta \\ln p_\\theta(x) \\right - k \cdot \mathbb{E}{y \sim p_n} \left \\frac{p_\\theta(y)}{p_\\theta(y) + k \\cdot p_n(y)} \\nabla_\\theta \\ln p_\\theta(y) \\right \\ &= \int p_d(x) \left \\frac{k \\cdot p_n(x)}{p_\\theta(x) + k \\cdot p_n(x)} \\nabla_\\theta \\ln p_\\theta(x) \\right dx - \int p_n(x) \cdot k \cdot \left \\frac{p_\\theta(x)}{p_\\theta(x) + k \\cdot p_n(x)} \\nabla_\\theta \\ln p_\\theta(x) \\right dx \\ &= \int \left \\color{blue}{p_d(x)} - \\color{blue}{p_\\theta(x)} \\right \color{red}{\left \\frac{k \\cdot p_n(x)}{p_\\theta(x) + k \\cdot p_n(x)} \\nabla_\\theta \\ln p_\\theta(x) \\right} dx \end{align} ∇θJ(θ)=Ex∼pdpθ(x)+k⋅pn(x)k⋅pn(x)∇θlnpθ(x)−k⋅Ey∼pnpθ(y)+k⋅pn(y)pθ(y)∇θlnpθ(y)=∫pd(x)pθ(x)+k⋅pn(x)k⋅pn(x)∇θlnpθ(x)dx−∫pn(x)⋅k⋅pθ(x)+k⋅pn(x)pθ(x)∇θlnpθ(x)dx=∫pd(x)−pθ(x)pθ(x)+k⋅pn(x)k⋅pn(x)∇θlnpθ(x)dx

我们发现最大化 NCE 的目标函数,其全局最优解正是使得模型分布完全等于真实数据分布。

五、实验验证

简单写一个浅一点的神经网络,然后以一个双峰高斯分布为真实数据,选一个简单的、很宽的单峰高斯分布为噪声数据。以NCE作为loss,看看训练出来的结果如何。

5.1 如何把公式写成代码?

Pθ(D=1∣x)=exp(uθ(x))exp(uθ(x)+k⋅pn(x))=11+exp(log(k⋅pn(x))−uθ(x))=Sigmoid(uθ(x)−log(k⋅pn(x))) \begin{align} P_\theta(D=1|x) &= \frac{exp(u_\theta(x))}{exp(u_\theta(x) + k\cdot p_n(x))} \\ &= \frac{1}{1 + exp(log(k\cdot p_n(x)) - u_\theta(x))} \\ &= Sigmoid(u_\theta(x) - log(k \cdot p_n(x))) \end{align} Pθ(D=1∣x)=exp(uθ(x)+k⋅pn(x))exp(uθ(x))=1+exp(log(k⋅pn(x))−uθ(x))1=Sigmoid(uθ(x)−log(k⋅pn(x)))

5.2 代码验证

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
python 复制代码
def sample_real_data(batch_size):
    msk = torch.rand(batch_size) > 0.5
    sample1 = torch.randn(batch_size) * 0.5 - 2.0
    sample2 = torch.randn(batch_size) * 0.5 + 2.0
    return torch.where(msk, sample1, sample2).unsqueeze(1)
python 复制代码
noise_dist = torch.distributions.Normal(0.0, 4.0)
python 复制代码
def sample_noise_data(batch_size):
    return noise_dist.sample((batch_size, 1))
python 复制代码
class EnergyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(1, 32),
            nn.ReLU(),
            nn.Linear(32, 32),
            nn.ReLU(),
            nn.Linear(32, 1)
        )
    
    def forward(self, x):
        return self.net(x)
python 复制代码
model = EnergyModel()
optimizer = optim.Adam(model.parameters(), lr=1e-2)
criterion = nn.BCEWithLogitsLoss() # binary classificaion cross entropy

batch_size = 512
k = 5
epochs = 300

for epoch in range(epochs):
    # 1:k sample
    x_real = sample_real_data(batch_size)
    x_noise = sample_noise_data(batch_size * k)
    
    x_all = torch.cat([x_real, x_noise], dim=0)
    u_theta = model(x_all)

    log_p_n = noise_dist.log_prob(x_all)

    logits = u_theta - np.log(k) - log_p_n

    labels_real = torch.ones(batch_size, 1)
    labels_noise = torch.zeros(batch_size * k, 1)
    labels = torch.cat([labels_real, labels_noise], dim=0)

    loss = criterion(logits, labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 50 == 0:
        print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.4f}")
复制代码
Epoch 50/300, Loss: 0.4031
Epoch 100/300, Loss: 0.3607
Epoch 150/300, Loss: 0.3159
Epoch 200/300, Loss: 0.3107
Epoch 250/300, Loss: 0.3240
Epoch 300/300, Loss: 0.3147
python 复制代码
print("Training finished! Plotting results...")
model.eval()
复制代码
Training finished! Plotting results...
EnergyModel(
  (net): Sequential(
    (0): Linear(in_features=1, out_features=32, bias=True)
    (1): ReLU()
    (2): Linear(in_features=32, out_features=32, bias=True)
    (3): ReLU()
    (4): Linear(in_features=32, out_features=1, bias=True)
  )
)
python 复制代码
x_test = torch.linspace(-8, 8, 400).unsqueeze(1)
python 复制代码
with torch.no_grad():
    u_theta_test = model(x_test)
    p_theta_unnormalized = torch.exp(u_theta_test).numpy()
python 复制代码
x_test_np = x_test.numpy()
python 复制代码
plt.figure(figsize=(10, 6))
real_samples = sample_real_data(10000).numpy()
plt.hist(real_samples, bins=60, density=True, alpha=0.3, color='blue', label='True Data Histogram (Bimodal)')

noise_pdf = torch.exp(noise_dist.log_prob(x_test)).numpy()
plt.plot(x_test_np, noise_pdf, 'g--', label='Noise Distribution p_n(x)')

scale_factor = np.max(p_theta_unnormalized) / 0.4  # 0.4 约是直方图最高点
plt.plot(x_test_np, p_theta_unnormalized / scale_factor, 'r-', linewidth=2, label='Learned unnormalized p_theta(x)')

plt.legend()
plt.title("NCE: Learning Data Distribution via Binary Classification")
plt.show()

效果还不错。

相关推荐
Tisfy2 天前
LeetCode 3014.输入单词需要的最少按键次数 I:遍历 / if-else计算(比纯数学公式写起来麻烦但好想)
数学·算法·leetcode·字符串·题解·贪心
闻缺陷则喜何志丹5 天前
【计算几何第五章】正交区域查找:数据库查询
数学·计算数学·区域树·kd树·树套树·正交区域查询·二维线段树
jz_ddk6 天前
[信号处理] 数学与工程之美:相邻差分共轭乘积
数学·算法·信号处理·差分·共轭
anew___7 天前
历史性突破!两位中国数学家同摘“数学界诺奖”,改写百年难题
数学·菲尔兹奖
Tisfy7 天前
LeetCode 3536.两个数字的最大乘积:O(1)空间维护max2
数学·算法·leetcode·题解
闻缺陷则喜何志丹9 天前
【计算几何 第四章】线性规划:铸模制造
数学·计算几何·线性规划·铸造
大卫小东(Sheldon)10 天前
斯梅尔问题(Smale's Problems)清单
数学·ai
Tisfy14 天前
LeetCode 1979.找出数组的最大公约数:模拟(附手动gcd)
java·数学·算法·leetcode·题解·最大公约数
Luhui Dev16 天前
用 AI 生成一道几何题配图:三角形四心作图案例
人工智能·数学·agent·luhuidev