开始前,会这些就够了
- 理解平均值与独立抽样
- 能读双重循环和指数函数
- 先读 输入变了,模型就一定变差了吗?:按该文准备相关概念。
你比较两组向量,程序返回 −0.393469。变量明明叫 MMD 平方,负数似乎证明代码错了。真正需要先查的是:它返回总体差异本身,还是用有限样本计算的一种估计?
本例用两个只取 0 或 1 的小样本,把每一种可能抽样都列出来。这样无须依赖“多跑几次应该差不多”,就能精确验证平均意义上的无偏。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- mmd_estimators.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B mmd_estimators.py解压到独立练习文件夹,在含 mmd_estimators.py 的目录打开终端运行。已运行 16 种等概率独立样本的精确枚举、核宽度配套缩放及非法输入检查;未执行统计假设检验、采集真实嵌入或评测模型质量。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
先把两点的接近程度写成一个数
使用 k(a,b)=exp(−(a−b)²/(2σ²))。a、b 是一维观测,σ>0 控制多远的点仍被看作接近;同一点 k(a,a)=1,距离越大,结果越接近零。本文取 σ=1,因此 k(0,1)=exp(−0.5)≈0.606531。
总体 MMD²=E[k(X,X′)]+E[k(Y,Y′)]−2E[k(X,Y)]。X、X′ 是从第一分布独立抽取的点,Y、Y′ 来自第二分布,四者彼此独立。它比较组内接近程度与组间接近程度,使用合适的正定核时总体值非负。
关键是同组能不能与自己配对
令第一组有 m 个点,第二组有 n 个点。无偏版本 U 的第一项是 Σᵢ≠ⱼk(xᵢ,xⱼ)/[m(m−1)],第二项对 y 同样计算;跨组项为 −2Σᵢⱼk(xᵢ,yⱼ)/(mn)。同组排除自己,跨组保留所有配对,因为两组样本独立。
含对角线的版本 V 把第一项改为 Σᵢⱼk(xᵢ,xⱼ)/m²,第二项类似。它是两组经验核均值差的平方,适当核下非负,但有限样本一般有偏。两个版本估计目标相关,不能仅靠名字 MMD 判断返回哪一个。
取 x=y=[0,1]。U 的两项组内均值都是 k=0.606531,跨组均值是 (1+k)/2,因此 U=2k−(1+k)=k−1≈−0.393469。V 则为零。这里相同列表只是一次数值输入,不是在证明同一批随机变量可被当成两份独立样本。
用全部 16 种情况验证无偏
让两组都独立来自公平的 0/1 分布,每组抽两个点。四个位置各有两种取值,共 16 种等概率结果。真实分布相同,总体 MMD²=0。程序逐一计算再平均,E[U] 为零,而 E[V]≈0.196735。
这解释了为什么 U 可以偶尔为负:无偏约束的是所有可能样本的期望,不是要求每次结果都非负。把所有负结果截成零后,正结果保留、负结果消失,平均数就变成正值;代码直接打印这个变化。
如果下游需要可开平方的展示值,可以另定义显示规则,但原始 U、规则和检验依据都应保留。不能把 max(0,U) 继续称为同一个无偏估计。
差异数值还不是显著性结论
统计检验还需要明确零假设、抽样假设、核参数以及检验阈值。作者项目列出了重采样等阈值求法;本文不执行检验,因此不会由某个正值宣布“已检测到漂移”,也不会由负值宣布“两分布相同”。
σ 也带有输入单位。把数据整体乘以 10,同时把 σ 乘以 10,核值与统计量不变;只改变单位而忘记调整 σ,则可能改变结果。程序检查这种配套缩放及组内置换不变性。
运行完整实验并检查报告
保存为 mmd_estimators.py,运行 python -B mmd_estimators.py。预期输出来自本次实际运行;完整结果写入 report.json。
已运行 16 种等概率独立样本的精确枚举、核宽度配套缩放及非法输入检查;未执行统计假设检验、采集真实嵌入或评测模型质量。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Exact finite enumeration for biased and unbiased MMD squared."""
import itertools
import json
import math
from pathlib import Path
def mmd2(x,y,sigma=1.0,unbiased=True):
if not math.isfinite(sigma) or sigma <= 0:
raise ValueError('positive finite bandwidth required')
if len(x)<(2 if unbiased else 1) or len(y)<(2 if unbiased else 1):
raise ValueError('not enough observations')
if not all(math.isfinite(v) for v in list(x)+list(y)):
raise ValueError('finite observations required')
def k(a,b): return math.exp(-0.5*((a-b)/sigma)**2)
def within(values):
n=len(values)
return sum(k(a,b) for i,a in enumerate(values) for j,b in enumerate(values)
if not unbiased or i!=j)/(n*(n-1) if unbiased else n*n)
return within(x)+within(y)-2*sum(k(a,b) for a in x for b in y)/(len(x)*len(y))
def main():
x=y=[0.0,1.0]
u=mmd2(x,y); v=mmd2(x,y,unbiased=False)
k=math.exp(-0.5)
assert abs(u-(k-1))<1e-12 and abs(v)<1e-12
# Each four-bit sequence has probability 1/16: independent iid Bernoulli samples.
outcomes=[]
for bits in itertools.product([0.0,1.0],repeat=4):
a,b=bits[:2],bits[2:]
outcomes.append(dict(x=a,y=b,u=mmd2(a,b),v=mmd2(a,b,unbiased=False)))
mean_u=sum(r['u'] for r in outcomes)/16
mean_v=sum(r['v'] for r in outcomes)/16
clipped_mean=sum(max(0,r['u']) for r in outcomes)/16
assert abs(mean_u)<1e-12 and abs(mean_v-(1-k)/2)<1e-12
assert clipped_mean > 0
assert all(r['v'] >= -1e-12 for r in outcomes)
assert abs(mmd2([0,2],[1,3],2)-mmd2([0,20],[10,30],20))<1e-12
assert abs(mmd2([0,2],[1,3])-mmd2([2,0],[3,1]))<1e-12
rejected=0
for a,b,s in [([0],[1],1),([0,1],[0,1],0),([0,1],[0,1],float('nan'))]:
try: mmd2(a,b,s)
except ValueError: rejected+=1
assert rejected==3
report=dict(example_u=u,example_v=v,population_mmd2=0,mean_u=mean_u,mean_v=mean_v,
clipped_mean=clipped_mean,exact_outcomes=outcomes,invalid_rejections=rejected,checks_passed=True)
Path('report.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
print(f'same observed lists: U={u:.6f}; V={v:.6f}')
print(f'16 equally likely iid outcomes: E[U]={mean_u:.6f}; E[V]={mean_v:.6f}')
print(f'E[max(0,U)]={clipped_mean:.6f}')
print('checks=PASS; report.json written')
if __name__=='__main__': main()
怎样运行:在独立练习文件夹中执行 python -B mmd_estimators.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。已运行 16 种等概率独立样本的精确枚举、核宽度配套缩放及非法输入检查;未执行统计假设检验、采集真实嵌入或评测模型质量。
运行后对照结果
same observed lists: U=-0.393469; V=0.000000
16 equally likely iid outcomes: E[U]=0.000000; E[V]=0.196735
E[max(0,U)]=0.098367
checks=PASS; report.json written记住这些要点
- 非负的是总体 MMD²;某些无偏有限样本估计可以为负。
- 同组去对角线、跨组保留配对,是本文 U 版本的合同。
- 把负数截零会改变估计的期望。
- MMD 数值不自动给出检验结论或模型质量结论。
别把例子用过头
- 对 U 直接开平方,然后把报错归咎于浮点精度。
- 把同组去对角线也照搬到独立跨组项。
- 忽略核宽度与输入单位的关系。
自己试一小步
每组抽三个点时,截零后的偏差会怎样变化?
- 枚举六个 0
/1位置的全部结果,分别平均 U、V 与截零后的 U。 - 把 σ 改成 0.5 和 2,观察结果。
- 单独保留一个负值样本,手工列出它的同组和跨组求和。
做到这些,就算完成
- 说明为何每组至少要有两个点。
- 报告原始估计与显示规则。
- 不把这次枚举写成实际分布漂移检测。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- A Kernel Two-Sample Test (新标签页)↗
MMD² 的 U
/V统计估计与负值边界 - MaximumMeanDiscrepancy (新标签页)↗
RBF 核、排除同组对角项与批次约定
- A Kernel Two-Sample Test 作者项目 (新标签页)↗
统计量与检验阈值不能混为一谈