ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

得分匹配(Score Matching)原理、推导与扩散模型应用解析

得分匹配(Score Matching)原理、推导与扩散模型应用解析 1. Score Matching 到底是什么如果你最近关注生成模型、扩散模型或者密度估计方向一定绕不开 Score Matching 这个词。我最早接触它是在做独立成分分析的时候当时被配分函数困扰得不行后来发现这一招能直接把配分函数绕过去瞬间觉得整个世界清爽了。简单来说Score Matching 是一种估计概率密度模型参数的方法。它不直接拟合概率密度本身而是拟合密度函数的对数梯度也就是所谓的“得分函数”。我们不去和那个归一化常数较劲而是换个赛道让模型在不同样本点上的得分尽量逼近真实数据分布在该点的得分。就这么一个思路转换很多原来算不动的问题突然变得可以解了。这篇文章我想把这个方法的原理、推导、代码实现和它在如今大火的方向尤其是扩散模型里的作用全部拆开来讲适合有一定概率统计和机器学习基础、但被“得分”这个概念绕晕的读者。学完你不仅能手动实现一个简化版 Score Matching还能理解为什么扩散模型的训练目标其实就是一个变种的得分匹配。先说清楚一点这里有个经常被搜索引擎带偏的坑有一部分人搜“得分匹配”会跑到“倾向得分匹配Propensity Score Matching”去那是因果推断领域的工具用来处理观察性研究中样本自选择偏差的。和本文要讲的 Score Matching 是两码事后面我会专门在常见误区里再展开一次。2. 配分函数难题为什么我们非要绕开它2.1 最大似然估计的死穴在统计建模里最常见的参数估计思路是最大似然估计。假设我们有数据 x模型给它的概率密度是 p(x;θ)那我们就最大化所有样本的 log p(x;θ) 之和。看起来很完美但问题是在实际建模时密度函数往往长这样p(x;θ) (1/Z(θ)) * exp(-E(x;θ))这里的 E 是能量函数Z(θ) 是配分函数等于对 exp(-E(x;θ)) 在整个定义域上做积分。麻烦就出在这个 Z(θ) 上。对数密度展开后Z(θ) 会进入梯度项而这个积分通常没有解析解。数据维度一高数值积分也救不了你算都算不动更别说求梯度了。我记得第一次跑受限玻尔兹曼机时就深刻体会过这种痛苦为了估算配分函数得跑蒙特卡洛采样跑了半天还不一定收敛。所谓“似然是理论上最优实操是计算上要命”就是这个感受。2.2 得分函数如何绕开 Z(θ)得分函数定义为对数密度的梯度s(x) ∇x log p(x;θ)注意这里是对 x 求梯度不是对 θ。现在我们把 p(x;θ) 的形式代进去log p(x;θ) -E(x;θ) - log Z(θ)对 x 求梯度后log Z(θ) 这一项压根不依赖 x梯度直接为零配分函数就这么消失了。左边剩下的就是对能量函数求梯度干净利落。这就是 Score Matching 的精髓我根本不碰密度值本身只管密度在空间上的变化方向。配分函数不管多难算反正它在得分函数眼里不存在。这个思路在物理里其实挺自然能量函数的梯度就是力我们直接用力来推断体系的性质没必要先去把配分函数算出来。2.3 Fisher 散度衡量两个得分函数有多像现在问题变成了模型得让它在样本点上的得分函数尽量接近真实数据分布在样本点上的得分。怎么衡量两个向量场有多接近最常用的是 Fisher 散度它本质上就是两个向量之间差的平方再按真实分布加权J(θ) (1/2) ∫ p_data(x) ∥ s_θ(x) - s_data(x) ∥² dx其中 s_data(x) ∇x log p_data(x)s_θ(x) ∇x log p_model(x;θ)。这个目标很简单直觉化如果模型在每个位置上的“密度变化方向”和数据真实的变化方向一致那这个散度就小模型就学得好。但是这个目标函数里还有个 s_data 没有解析表达式实际数据分布我们只知道样本导数怎么算还没有那么简单。下一节讲怎么绕。3. 核心原理拆解从 Fisher 散度到可计算的目标3.1 Hyvärinen 的核心技巧分部积分消掉真实得分2005 年 Aapo Hyvärinen 提出了一个漂亮的推导。把 Fisher 散度展开(1/2) ∫ p_data ∥s_θ∥² dx - ∫ p_data · s_data · s_θ dx 常数问题主要出在中间项它含 s_data。Hyvärinen 的技巧是用分部积分处理这一项。因为在自然边界条件下概率密度在无穷远处趋于零分部积分后这一项可以变成一个不含 s_data 的表达式。标准目标函数长这样J(θ) E_p_data [ (1/2) ∥s_θ(x)∥² tr(∇x s_θ(x)) ] 常数这里的 tr(∇x s_θ(x)) 是雅可比矩阵的迹也叫得分函数的散度。括号里两项都只依赖模型得分函数本身和它的导数跟真实数据密度的梯度完全无关。真实分布只通过样本期望进入梯度计算直接可用样本均值估计。中间我用纸笔推了几天最有感的是这地方用了分部积分和边界项消失这个假设。实际数据分布并不一定满足无穷远处密度为零的条件但实践中极少有人去较真这个因为效果是真的好。3.2 显式得分匹配和隐式得分匹配按照目标函数有没有显式写出模型密度可以把 Score Matching 分成两类显式得分匹配模型给出了带参数的密度形式能够直接写出得分函数和它的雅可比矩阵。比如高斯混合模型能量函数和梯度都能显式表示。前面那套推导直接可用。隐式得分匹配模型不是一个显式概率分布而是一个直接输出得分函数的神经网络。我们不知道它的归一化常数甚至不知道它对应什么能量函数但关联的就是它可以被当作某个分布的得分来训练。现代深度生成模型基本都是这条路。我刚开始学的时候一直对“模型直接输出得分”有点转不过弯既然得分是密度的梯度那这个密度是什么答案是不必知道。只要训练好后能通过它做朗之万采样得到样本密度本身并无必要显式写出来。这种“只学方向不学高度”的思路后来就是扩散模型的底层引擎。3.3 Denoising Score Matching把问题再转换一次原版 Score Matching 对神经网络来说其实不太友好因为要算 tr(∇x s_θ(x))相当于对网络输出做逐维微分。在网络参数量很大的时候这个“对输入的二阶信息”的获取过程非常消耗算力。于是 Vincent 在 2011 年提出了 Denoising Score Matching思路更加聪明如果我先把真实样本故意加上高斯噪声扰动一下然后训练模型去预测这个噪声。直觉上扰动后的数据分布得分函数正是从噪声中恢复干净数据的逆过程。用噪声预测作为训练目标几乎等价于在扰动分布上做得分匹配。很多刚接触扩散模型的人可能会觉得扩散模型的训练目标是“预测噪声完全是一种任务设计上的巧合”其实它的数学本质就是一个去噪得分匹配。DDPM 里的损失函数跟 Denoising Score Matching 在数学上是等价的只是多了一步重新参数化。4. 从零实现改进版简化得分匹配4.1 一维混合高斯模型最容易看清原理的实验台纸上谈兵到这里差不多了我直接带大家跑一个实验看看得分匹配到底怎么用。这里我选了一维高斯混合模型因为它所有解析量都能算出来方便校验代码算出的梯度对不对。设定目标数据分布是p_data(x) 0.3 * N(-2, 0.7) 0.7 * N(2, 1.1)模型的参数是两组高斯参数均值 μ1 和 μ2方差 σ1² 和 σ2²以及两个混合权重为了保证权重和为 1我会用 softmax 来参数化权重。模型得分函数可以解析求出来s_θ(x) ∇x log [ w1 * N(x;μ1,σ1²) w2 * N(x;μ2,σ2²) ]这里由于得分匹配目标函数里有模型得分对 x 的导数项雅可比矩阵的迹我对它做了自动微分其实写起来反而简单。代码如下import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt torch.manual_seed(42) # 真实数据分布 def true_sample(n): comp torch.rand(n) 0.3 x torch.where(comp, torch.randn(n) * 1.1 2.0, torch.randn(n) * 0.7 - 2.0) return x.view(-1, 1) class GMMScore(nn.Module): def __init__(self): super().__init__() self.logits nn.Parameter(torch.tensor([0.2, 0.0])) self.mu nn.Parameter(torch.tensor([[-1.0], [1.5]])) self.log_sigma nn.Parameter(torch.tensor([0.0, 0.0])) def component_log_probs(self, x): # x: (N,1) x x.unsqueeze(0) # (1,N,1) mu self.mu.view(2,1,1) # (2,1,1) sigma self.log_sigma.exp().view(2,1,1) log_p -0.5 * ((x - mu) / sigma) ** 2 - self.log_sigma.view(2,1,1) - 0.5 * np.log(2 * np.pi) return log_p # (2,N,1) def log_prob(self, x): w torch.softmax(self.logits, dim0).view(2,1,1) log_comp self.component_log_probs(x) # log-sum-exp max_log, _ log_comp.max(dim0, keepdimTrue) log_sum max_log torch.log((w * (log_comp - max_log).exp()).sum(dim0, keepdimTrue)) return log_sum.squeeze(-1) # (N,1) def score(self, x): x.requires_grad_(True) lp self.log_prob(x).sum() grad torch.autograd.grad(lp, x, create_graphTrue)[0] return grad def score_matching_loss(model, x): x x.detach().requires_grad_(True) s model.score(x) # (N,1) # 计算 tr(nabla s) div torch.zeros_like(x) for i in range(x.shape[1]): gi torch.autograd.grad(s[:, i].sum(), x, create_graphTrue)[0][:, i] div[:, i] gi loss (s ** 2 / 2.0 div).mean() return loss model GMMScore() opt torch.optim.Adam(model.parameters(), lr0.02) data true_sample(2000) for epoch in range(3000): x data.clone() loss score_matching_loss(model, x) opt.zero_grad() loss.backward() opt.step() if epoch % 500 0: print(fepoch {epoch:4d} loss {loss.item():.4f}) # 结果打印 w torch.softmax(model.logits, dim0).detach().numpy() mu model.mu.detach().numpy().flatten() sigma model.log_sigma.exp().detach().numpy().flatten() print(学到的权重:, w, 真实权重: [0.3, 0.7]) print(学到的均值:, mu, 真实均值: [-2.0, 2.0]) print(学到的标准差:, sigma, 真实标准差: [0.7, 1.1])跑出来的效果随着训练轮数增加参数会逐步逼近真实值。我自己跑这段代码大概到 1500 轮左右均值和标准差就比较接近真实值了权重也能基本对准说明靠得分匹配确实能恢复出真实分布的形状。这个实验虽然简单但它验证了整条逻辑链我只需要给模型一批样本不用算配分函数不用做马尔可夫链蒙特卡洛采样纯粹用一个可微分的源函数求梯度最后就能拿到这个分布的真实参数。4.2 多维度拓展时雅可比矩阵的迹怎么算刚才的代码是一维场景tr(∇x s) 其实就是一个数直接对唯一维度求梯度就行。但深度学习里数据维度非常高图像就是成千上万维的如果逐维手动展开计算内存和计算时间会非常夸张。实际工程中有两种常用替代方案Sliced Score Matching把 score 函数沿随机投影方向做投影再对投影方向求梯度近似原目标。这个技巧只需要一次反向传播计算成本大幅下降是现在扩散模型的常见训练方式。原论文是 Song 等人 2019 年提出叫做 Sliced Score Matching。Denoising Score Matching 重参数化直接把训练目标改成预测噪声的 L2 损失。这在 DDPM 里就被化简成一个极简的形式效果又稳又好。如果让我给建议做图像生成直接上 Denoising Score Matching做表格类数据的密度估计Sliced Score Matching 就够用做小规模时序密度建模原版目标也不是不能跑注意维度别太高就行。4.3 训练中的实操心得笔记实操得分匹配时有一些容易被忽略但很影响效果的细节。第一学习率不要设太大。得分匹配的损失尺度比普通回归损失小一个量级左右因为它有良性的平方项和导数项混合学习率一大容易训练震荡。Adam 一般用 1e-3 到 1e-4 之间。第二对数据进行标准化很重要。得分函数对数据的尺度极其敏感如果特征尺度差异很大雅可比矩阵的迹会出现数值病态。先把数据归一化到标准区间或者做 z-score 归一化训练会稳定很多。第三模拟训练的数据要干净。因为得分匹配涉及对输入的二阶导如果数据里有异常值或随机跳变点梯度变化会非常剧烈这会让优化过程非常不稳定。这也是为什么大部分扩散模型要先对图像做把像素归一化到 [-1,1] 区间跟这里是一个原因。第四千万要加 create_graphTrue否则雅可比矩阵的梯度没法继续回传。我一开始忘了加报错信息还不太直观花了一阵子才排查出来。5. Score Matching 为什么成了扩散模型的发动机5.1 从随机微分方程看生成过程现在生成模型的大热门扩散模型核心思路其实是前向过程把数据一点一点加噪声破坏掉直到变成纯高斯噪声逆向过程去学习如何从噪声恢复出数据。如果你把加噪声的过程看成是一个连续时间随机微分方程那么它的逆动力学方程里恰好有一个漂移项依赖数据分布的得分函数。换句话说只要你能估计出加噪后每一步分布的得分函数就能用一个反向随机微分方程把样本从噪声里完整地生成出来。这也解释了为什么扩散模型训练的核心不依赖对抗生成也不用变分下界本质上就是一个巨大的得分函数学习网络。你训练它是为了做生成但中途学到的其实就是数据分布的梯度场。5.2 扩散模型训练目标推导DDPM 的训练目标推导到最后等价于优化L E_{t, x0, ε} [ λ(t) ∥ ε_θ(x_t, t) - ε ∥² ]x_t 是加了 t 步噪声后的含噪样本ε 是当时加进去的噪声ε_θ 是网络预测的噪声。这个形式和 Denoising Score Matching 在数学上是等价的只是做了线性变换把得分网络变成了噪声预测网络。有人可能会问为什么要预测噪声不直接预测 X0实验和经验都表明预测噪声更像是回归任务里一个好的目标参数化方式因为噪声在不同时间步下的尺度和分布都是已知的目标比较稳定训练中不容易出现各时间步损失尺度失衡的问题。这也是现在主流扩散代码库默认的设计。5.3 不同时间步的损失权重为什么不能随便改训练扩散模型的时候每个时间步 t 的样本损失会乘上一个和时间相关的权重 λ(t)这个权重不是凭感觉定的它直接对应去噪得分匹配中不同噪声强度下的 Fisher 散度加权。在实践中权重选择会影响生成品质。过大的权重集中在高噪声阶段模型会花大量算力在还原低频轮廓上细节可能变差权重集中在低噪声阶段模型又会过度关注细节织造整体结构可能不稳定。所以工程上大家会用一些简单的调度策略比如线性衰减或者固定的加权方式而这些经验的根其实就是得分匹配理论中的噪声调度。我在做条件生成模型时试过自己定义权重调度发现一个规律保持 λ(t) 与噪声方差 σ(t) 成反比的调度效果比较稳健这和“各时间步训练难度相近”的直觉一致。6. 概念迷思大盘点得分匹配、倾向得分、评分卡别再搞混了6.1 和倾向得分匹配的界线在哪里搜索“得分匹配”的时候经常会跳出来“倾向得分匹配”。这两者除了名字里都有“得分”和“匹配”四个字技术路线完全不一样。倾向得分匹配是经济学、医学、社会科学里做因果推断的常用方法。它解决的场景是在一项观察性研究中有的人接受了处理有的人没有而接受处理的概率往往和个体特征有关这就导致处理组和对照组在特征分布上不平衡。倾向得分就是“在给定特征下接受处理的概率”有了这个概率就可以把处理组和对照组里倾向得分相近的个体配对模拟随机对照试验的效果。而 Score Matching 里的得分是概率密度对数据维度的梯度是描述密度几何形状的一个量。它不涉及因果推断也不涉及处理分配纯粹是统计机器学习里的密度估计技术。一句话区分一个在找概率密度的梯度一个在算样本进入处理组的概率。如果你搜到“pymatch”或者“python倾向匹配得分”相关代码那是在处理倾向得分匹配的问题跟本文的模型完全不同千万别看混了。6.2 和评分卡、评分模型也没关系还有一类搜索流量来自“评分卡”“信用评分”等领域。那些内容说的是用逻辑回归构建的信用评分模型给客户打分这里的“得分”是指分数。和 Score Matching 没有任何关系。我在很多公开问答平台上见过有人把这两个概念混在一起提问提问者其实是想要一个评分卡模型的实现但用的是“得分匹配”关键词底下的回复答非所问。如果大家以后遇到类似模糊的搜索词建议先用英文确认领域score matching 是机器学习里的密度估计技术propensity score matching 是因果推断技术credit scoring 是风控建模技术。三个方向完全独立。6.3 什么时候选择 Score Matching 而不是 MLE 或 GAN如果你正在设计一个概率模型先考虑一下有没有必要用 Score Matching。我的判断经验是这样模型能写出能量函数、归一化常数难算但能量的梯度好算用 Score Matching 就对了。能量模型、受限玻尔兹曼机等场景非常典型。你只需要生成新样本不需要显式算似然值那 Score Matching 配合朗之万采样是简单可靠的方案。你确实需要计算似然值用于模型比较那得用归一化流或者自回归模型Score Matching 只给你一个未归一化的能量函数似然需要再额外做归一化估计比较麻烦。你的数据量很小且维度很高Score Matching 可能不如基于核的方法稳定因为二阶导估计在小样本下噪声会比较大。6.4 判断自己是不是学岔路的五个自查问题如果你学完还是不确定自己理解的是不是 Score Matching可以拿下面这组问题自查得分函数是对谁求梯度答案是数据 x而不是参数 θ。目标函数里的核心项是哪两项第一项是模型得分平方的期望第二项是模型得分的散度雅可比矩阵的迹。这个方法和配分函数的关系是什么配分函数在得分函数里不出现所以不用计算。扩散模型里的训练目标本质是什么它就是在带噪声的数据分布上做了去噪得分匹配。Score Matching 和倾向得分匹配的区别是什么一个是密度梯度估计一个是条件概率建模。这五个问题都答得上来才算真正过了概念关。7. 工程实践避坑指南7.1 训练不收敛时先查哪些层如果得分匹配训练的损失一路狂飙或者震荡我一般会按照以下顺序排查确认数据预处理有没有把特征尺度压到合理范围。这个最常见的坑因为得分函数对尺度太敏感。确认模型输出有没有做合理的激活限制。有些模型配置里输出得分函数前不加限制导致梯度爆炸要处理。确认优化器和学习率的设置有没有问题。建议从 1e-4 起步用 Adam 优化器。确认损失函数里的雅可比迹有没有用 create_graph 留住计算图不然反向传播会传不齐。7.2 评估质量光看损失没有意义Score Matching 的训练损失下降只能说明向量场匹配得好了一些但它并不能直接给出对数似然的绝对值所以不同任务之间随意比较绝对值没有意义。我通常建议把评估分成两部分一是看生成样品的可视化或下游指标比如图像生成用 FID、IS表格数据看边际分布的拟合程度二是看朗之万采样出来的样本与真实样本之间的统计量差异。如果采样样本分布跟真实样本分布的重叠度很高就说明训练是成功的。7.3 采样时的步长与噪声添加策略用学习好的得分函数生成样本通常要做朗之万采样采样过程中步长的设置相当关键。步长太大粒子会在高概率区域附近乱跳步长太小采样效率太低可能一直困在低概率区。我常用的策略是退火式采样步长从大逐步变小。这个做法的逻辑也是得分匹配的正常范畴大噪声阶段探索全局小噪声阶段精修细节。和扩散模型的逆向过程异曲同工。7.4 数据量不足时的增强方案如果只有几百条数据想用高维得分匹配效果会非常惨。一个可行的小技巧是利用数据增强间接扩大样本量对数据进行加微小噪声扰动相当于人为扩大样本分布的支持范围这样反而能让得分函数估计更平滑。这是我在小样本表格数据上验证过的做法比直接硬训或者换模型都要省事。8. 一些个人经验与扩展方向这套思路我从前几年接触到今天前前后后在不同的项目里用了不少次中间踩过的坑也不少。回头来看Score Matching 给了我一个非常核心的心智模型很多计算的困难都来自归一化常数而很多问题本质上只需要梯度和变化方向方向对了归一化常数天然就会绕开。如果你继续深入可以关注 Score-Based Generative Models、SDE 框架和基于得分匹配的扩散模型这些方向。也可以把它和最优传输、条件生成模型结合用得分函数做约束在很多实际任务里会有不可思议的效果。最后再分享一个小经验如果你要在实际项目里用得分匹配先把数据可视化做出来。把学到的得分函数画成向量场和真实分布叠在一起看非常直观。这一步能帮你发现 80% 的建模问题比盯着损失曲线盲目调参靠谱得多。
返回列表