MMDMaximum Mean Discrepancy最大均值差异这个名字初次接触的人容易先被最大和均值绕晕。其实它就是用来回答一个非常朴素的问题两个分布的差异到底有多大做机器学习的人经常会遇到这类场景训练集和测试集分布不一致生成模型生成的图片和真实图片到底像不像或者两个特征空间是否真的来自同一分布。过去常用的做法是拼均值、拼方差但分布这种东西均值方差相同也可能长得完全不一样。MMD的做法更彻底——把样本映射到高维再生核希尔伯特空间再比较两个分布在这个空间里的均值差异。这篇文章我直接把自己实践过的思路和代码拿出来讲明白。核心内容包括三部分MMD的数学直觉、PyTorch和TensorFlow两套可运行实现、以及我在实际调参和调试中反复踩过的坑。适合正在做迁移学习、域适应、生成模型评估或者只是单纯想给特征加分布约束的读者。1. 先搞清楚MMD到底在算什么一个不用密度估计的分布距离1.1 从均值方差到分布嵌入的跳跃假设你有两组样本一组来自分布 p一组来自分布 q。想量化它们是否相同最笨的办法是估计两个分布的概率密度函数再算KL散度或JS散度。但密度估计本身是高维空间里的老大难问题维数稍微高一点密度估计就变得极不稳定。MMD绕开了密度估计这条路径。它的思路是把每个样本点通过一个映射函数送入某个特征空间然后比较两个分布在这个特征空间中的均值。如果两个分布相同无论怎么做特征映射它们的第一阶统计量均值都应该相等。但这里有个关键问题随便找的映射函数太弱了搞了半天特征均值都一样根本区分不了分布。所以MMD定义了一个最大——在一个足够丰富的函数集合里去找那个能让两个分布均值差达到最大的函数这个最大差值就是两个分布的距离度量。形式化的定义是MMD(F, p, q) sup_{f ∈ F} ( E_p[f(x)] - E_q[f(y)] )其中 F 是函数集合。如果 F 只是有界函数当且仅当 p q 时这个值为0鉴别能力很强但实际无法计算因为在任意函数上求上界没有闭合解。1.2 换成再生核希尔伯特空间的单位球事情就通了真正的突破口在于当 F 选择为再生核希尔伯特空间RKHS中的单位球时这个上界问题存在闭合解。此时MMD可以写成核函数的形式MMD² E[k(x, x)] - 2E[k(x, y)] E[k(y, y)]其中 x, x 都来自分布 py, y 都来自分布 qk 是再生核希尔伯特空间对应的核函数。这组公式非常实用。它把需要上界的抽象问题转化成了三个期望值的线性组合。期望值在有限样本下直接用经验均值估计即可这是MMD能够落地的根本原因。为什么这个公式有意义以最常用的高斯核为例k(x, y) exp(-||x - y||² / (2σ²))高斯核把两个向量的距离映射成一个0到1之间的相似度。距离近核值接近1距离远核值趋于0。再仔细看高斯核的泰勒展开它实际上隐含了无限维的特征映射。所以比较两个分布在这个特征空间中的均值相当于同时比较了两个分布的无穷多个矩这些矩的组合由核的带宽参数决定。这也是为什么MMD比单纯比较均值、方差强得多——它天然地把更高阶的结构信息纳入了考量。1.3 有限样本下的经验估计形式给到实际代码时更常用的是MMD²的经验估计式。设源域样本 X {x_1,...,x_n}目标域样本 Y {y_1,...,y_m}则MMD²的经验估计可以写为MMD² (1/(n²)) Σ_{i,j} k(x_i, x_j) (1/(m²)) Σ_{i,j} k(y_i, y_j) - (2/(nm)) Σ_{i,j} k(x_i, y_j)训练过程中我们希望最小化源域和目标域之间的MMD让特征分布逼近一致。对应地就需要对MMD²做梯度下降。还有一种无偏估计版本把对角线项去掉MMD² (1/(n(n-1))) Σ_{i≠j} k(x_i, x_j) (1/(m(m-1))) Σ_{i≠j} k(y_i, y_j) - (2/(nm)) Σ_{i,j} k(x_i, y_j)交叉项不需要去掉配对因为这里的匹配关系正是要比较两个分布之间的相互作用。这两个公式在代码里只差一个对角掩码的处理但效果上无偏版本在小batch场景下方差更小。2. PyTorch版的MMD实现从核矩阵到完整可训练模块2.1 最基础的高斯核矩阵计算PyTorch的实现核心是计算核矩阵。假设输入的源域特征 X 和目标域特征 Y 形状都是 (batch_size, feature_dim) 我们需要计算出三组矩阵X与X之间的、Y与Y之间的、X与Y之间的。以平方距离矩阵的广播计算为例import torch def compute_squared_dist(x, y): # x: (n, d), y: (m, d) # 返回: (n, m) 平方欧氏距离矩阵 x_norm (x ** 2).sum(dim1, keepdimTrue) # (n, 1) y_norm (y ** 2).sum(dim1, keepdimTrue) # (m, 1) dist x_norm y_norm.T - 2.0 * torch.mm(x, y.t()) dist torch.clamp(dist, min0.0) # 数值误差保护 return dist def gaussian_kernel_matrix(x, y, sigma): dist compute_squared_dist(x, y) return torch.exp(-dist / (2.0 * sigma ** 2))这里的细节是torch.clamp。由于浮点数运算误差平方距离矩阵可能出现极小的负值直接送入exp函数倒没有致命问题但如果在后续处理中要开根号负数会引发NaN。加一行clamp是低成本的稳定性保障。2.2 单核MMD的完整PyTorch实现把高斯核和MMD经验公式组合起来一段完整可运行的PyTorch代码如下import torch def mmd_linear_rbf(x, y, sigma1.0): 计算两个特征批次之间的MMD²用于最小化。 x: (n, d) y: (m, d) kxx gaussian_kernel_matrix(x, x, sigma) # (n, n) kyy gaussian_kernel_matrix(y, y, sigma) # (m, m) kxy gaussian_kernel_matrix(x, y, sigma) # (n, m) n x.size(0) m y.size(0) mmd kxx.sum() / (n * n) kyy.sum() / (m * m) - 2.0 * kxy.sum() / (n * m) return mmd这段代码在训练里怎么用比如在某个域适应任务中你有一个源域特征f_source和一个目标域特征f_target把MMD作为额外正则项加到总损失里loss_task criterion(model(f_source), label_source) loss_mmd mmd_linear_rbf(f_source, f_target, sigma1.0) loss loss_task lamda * loss_mmdlamda的取值我自己的经验是先从0.1试起。如果loss_task的数量级是1左右而loss_mmd初始只有0.01那lamda偏小MMD约束根本起不到作用。相反如果lamda太大模型可能会提前把特征压成一坨任务性能反而下降。2.3 无偏估计和多核RBF版本在迁移学习场景中源域和目标域的batch_size可能不一致此时更稳妥的做法是使用无偏估计版本。PyTorch实现如下def mmd_rbf_unbiased(x, y, sigma1.0): kxx gaussian_kernel_matrix(x, x, sigma) kyy gaussian_kernel_matrix(y, y, sigma) kxy gaussian_kernel_matrix(x, y, sigma) n x.size(0) m y.size(0) # 去掉对角线项 mask_xx 1.0 - torch.eye(n, devicex.device) mask_yy 1.0 - torch.eye(m, devicey.device) xx (kxx * mask_xx).sum() / (n * (n - 1)) yy (kyy * mask_yy).sum() / (m * (m - 1)) xy kxy.sum() / (n * m) return xx yy - 2.0 * xy单一高斯核的带宽 σ 需要人为指定这对实际效果影响很大。σ太小核矩阵迅速衰减几乎每个样本之间都接近0MMD梯度消失σ太大核矩阵退化成近似全1矩阵MMD又变得迟钝。一个被验证很有效的处理方式是多核MMDMulti-kernel MMD。做法很简单固定一组带宽候选值把各个核函数下的MMD取平均。这样做的直觉是不同带宽对应着不同的特征关注尺度小带宽放大局部差异大带宽捕捉整体分布。多个核组合后相当于同时从多个尺度观察分布差异既鲁棒又实用。def mmd_multi_rbf(x, y, sigma_list[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]): mmd_total None n x.size(0) m y.size(0) kxx_all 0.0 kyy_all 0.0 kxy_all 0.0 for sigma in sigma_list: kxx_all gaussian_kernel_matrix(x, x, sigma).sum() kyy_all gaussian_kernel_matrix(y, y, sigma).sum() kxy_all gaussian_kernel_matrix(x, y, sigma).sum() mmd kxx_all / (n * n) kyy_all / (m * m) - 2.0 * kxy_all / (n * m) return mmd / len(sigma_list)其实还有更讲究的多核加权策略比如按每个核的检验力分配权重的核学习方案。但那套东西在工程上实现复杂收益有限。固定带宽列表求平均是性价比最高的选择实测下来稳定度已经很好了。2.4 一个快速验证MMD实现正确性的小实验写完代码后强烈建议先做一个单元验证。生成两组服从已知分布的数据手动计算一次MMD确保数值符合预期torch.manual_seed(42) x torch.randn(64, 128) # N(0, 1) y torch.randn(64, 128) 2.0 # N(2, 1)平移后的分布 mmd_value mmd_multi_rbf(x.detach(), y.detach()) print(mmd_value.item()) # 预期明显大于0 z torch.randn(64, 128) mmd_same mmd_multi_rbf(x.detach(), z.detach()) print(mmd_same.item()) # 预期接近0同一分布的两批数据MMD应该接近0但不是绝对0因为有限样本估计本身有方差。不同分布的数据MMD则应该显著大于0。如果这个基本性质都不满足说明核函数或者公式写错了后面再调模型都是白费。3. TensorFlow 2的实现API风格差异与训练集成3.1 TensorFlow版核心函数TensorFlow 2的写法在思想上完全一致区别主要在矩阵运算API和梯度处理机制。用tf.matmul、tf.reduce_sum配合广播机制一样能写出简洁的实现import tensorflow as tf def compute_squared_dist(x, y): # x: (n, d), y: (m, d) x_norm tf.reduce_sum(tf.square(x), axis1, keepdimsTrue) # (n, 1) y_norm tf.reduce_sum(tf.square(y), axis1, keepdimsTrue) # (m, 1) dist x_norm tf.transpose(y_norm) - 2.0 * tf.matmul(x, tf.transpose(y)) dist tf.maximum(dist, 0.0) return dist def gaussian_kernel_matrix(x, y, sigma): dist compute_squared_dist(x, y) return tf.exp(-dist / (2.0 * sigma ** 2)) def mmd_multi_rbf_tf(x, y, sigma_list[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]): n tf.shape(x)[0] m tf.shape(y)[0] kxx_total 0.0 kyy_total 0.0 kxy_total 0.0 for sigma in sigma_list: kxx_total tf.reduce_sum(gaussian_kernel_matrix(x, x, sigma)) kyy_total tf.reduce_sum(gaussian_kernel_matrix(y, y, sigma)) kxy_total tf.reduce_sum(gaussian_kernel_matrix(x, y, sigma)) n_f tf.cast(n, dtypetf.float32) m_f tf.cast(m, dtypetf.float32) mmd kxx_total / (n_f * n_f) kyy_total / (m_f * m_f) - 2.0 * kxy_total / (n_f * m_f) return mmd / tf.cast(len(sigma_list), dtypetf.float32)3.2 在tf.function和自定义训练循环中的注意事项TensorFlow中如果使用了tf.function实际上自定义模型和fit流程背后都会编译计算图要注意MMD函数里的Python循环。上面代码中的for sigma in sigma_list在tf.function下会被展开为常量图操作没有问题因为sigma_list是确定的列表。但如果某个参数是Tensor张量那么Python的流程控制就可能导致AutoGraph重新介入底层行为不好预测。我在实际训练时更推荐把MMD函数设计成只接收Tensor输入不接收任何Python可变对象的函数。上面的实现做到了这一点。一个更隐蔽的问题混合精度训练。开启tf.keras.mixed_precision.set_global_policy(mixed_float16)后张量会自动变成 float16。但MMD中的核矩阵计算涉及exp(-距离)float16下数值范围有限距离稍大一点指数部分下溢到0梯度也随之消失。实测中会出现MMD loss突然变成0.0且持续为0的情况。建议把MMD计算强制在float32下进行def mmd_multi_rbf_tf_stable(x, y): x tf.cast(x, tf.float32) y tf.cast(y, tf.float32) # ... 其余逻辑同上或者在核函数内局部转float32计算完后再转回原精度。3.3 与Keras模型集成的方式如果使用函数式API或子类API构建模型MMD可以像普通正则项一样被附加到自定义train_step中。以下是一个典型实现class MMDModel(tf.keras.Model): def __init__(self, base_model, lamda0.1): super().__init__() self.base_model base_model self.lamda lamda def train_step(self, data): x_source, y_source, x_target data with tf.GradientTape() as tape: f_source self.base_model(x_source, trainingTrue) f_target self.base_model(x_target, trainingTrue) logits tf.keras.layers.Dense(10)(f_source) # 示例分类头 loss_task tf.reduce_mean( tf.keras.losses.sparse_categorical_crossentropy(y_source, logits) ) loss_mmd mmd_multi_rbf_tf_stable(f_source, f_target) total_loss loss_task self.lamda * loss_mmd grads tape.gradient(total_loss, self.trainable_variables) self.optimizer.apply_gradients(zip(grads, self.trainable_variables)) return {loss: total_loss, loss_task: loss_task, loss_mmd: loss_mmd}关键一点MMD的梯度必须能顺利反传到特征提取层。如果f_source或f_target在传入MMD前被加了tf.stop_gradient那么MMD对特征层的约束会完全失效。我就犯过这个错——想防止目标域标签泄漏把目标域特征的梯度关掉了结果MMD loss一直不下降查了半天才发现问题出在stop_gradient上。4. 两个框架实现的实际差异对比4.1 性能与GPU利用率对比从计算公式上看PyTorch和TensorFlow两个版本的复杂度完全一致都是三个核矩阵的求和运算理论复杂度 O((nm)²d)。实际运行下来差异主要体现在两块**数据布局和通道调度。**PyTorch在小batch下内存开销略低TensorFlow在连续训练循环中通过图优化有一定吞吐优势但这种差距不大基本在同一数量级。真正拉开差距的反而是数据预处理管线的效率。MMD通常在特征层计算特征提取是主要耗时MMD本身的占比很低不需要在此优化。**矩阵运算的精度。**PyTorch使用torch.mmTensorFlow使用tf.matmul两者默认精度都是float32。但如果维度不够对齐广播机制可能悄悄引入额外拷贝。比如x_norm y_norm.T这个操作在PyTorch中内存连续性能更好在TensorFlow中则要注意tf.transpose会返回非连续的张量后续的广播计算仍能跑但访存效率略低。考虑到MMD本身计算量不大这个差异基本感知不到。4.2 计算图模式下的梯度行为差异这是两个框架版本最值得注意的差异。PyTorch的默认执行方式是动态图当gaussian_kernel_matrix每次被调用时计算图是实时构建的梯度自动跟踪。这在调试时很方便——可以随时打印核矩阵的值直接mmd_value.backward()检查梯度是否正常。TensorFlow在tf.function下是静态图编译MMD函数里的Python列表推导和循环虽然能被AutoGraph转换但调试能力会下降。我的习惯是**开发初期不使用tf.function先把数值调对确认MMD在上升或下降再开启编译优化。**一旦进入静态图内部无法打印中间结果排查问题非常痛苦。还有一个容易踩的坑TensorFlow中如果漏掉了tf.castbatch_size是Tensor类型参与n * n这种运算时会保持Tensor类型最后除以它得到的是Tensor。这个没问题。但如果在计算loss的均值时不小心对Tensor和Python float做了混合运算在AutoGraph下可能触发创建额外的隐式节点导致输出的train_step日志类型异常。用上tf.cast(n, tf.float32)这类显式转换问题会少很多。4.3 内部函数命名和结构设计的共性建议为了让代码既能在PyTorch跑也能在TensorFlow跑我的做法是把MMD计算拆成三个原子函数compute_squared_dist(x, y)输入两个特征矩阵输出平方距离矩阵gaussian_kernel_matrix(x, y, sigma)基于平方距离矩阵生成核矩阵mmd_loss(x, y, sigma_list)循环不同带宽组合核矩阵计算最终MMD损失这套结构的最大好处是后续想换核函数比如换成拉普拉斯核、多项式核只需要改第二个函数。同时单元测试可以只针对第一个函数写——平方距离矩阵的正确性直接影响最终结果而这个函数逻辑简单、极易验证。5. 我在实际调MMD时反复踩过的坑这一段不讲理论了全是实操经验。5.1 batch size不一致时的MMD估计陷阱在域适应场景中我经常遇到源域数据量大、目标域数据量小的情况。为了训练方便通常会让源域和目标域各自独立地随机取样导致两个batch的尺寸经常不同。问题来了当 n ≠ m 时MMD²的估计会系统性偏置。细想一下如果源域batch有100个样本目标域batch只有10个样本那目标域样本对之间的核值项在整体MMD中占比很小而交叉项占比变得很大。交叉项的大致量级受两个batch的共同影响结果就是MMD的绝对值整体被拉高甚至出现明显不同的假象。我的应对办法是**在采样阶段就强制让两个域每个step都取相同大小的batch。**目标域即使总数不够也通过重复采样或者带替换采样凑够数目。如果实在做不到至少不要用MMD的绝对数值去做跨实验对比只看它在训练过程中的相对变化趋势。另一个相关问题是**当 batch_size 太小时MMD估计的方差非常大。**我试过batch_size4的情况MMD值在0.05到2.0之间剧烈波动损失曲线看起来像心电图根本没法用它指导训练。后来把batch_size提到64曲线的稳定性肉眼可见地变好了。MMD这种基于核值的估计天生对样本量敏感但求量级要保证在64以上比较稳妥。5.2 高斯核饱和问题σ 的取值范围和特征尺度高斯核的输入是-||x-y||² / (2σ²)如果特征向量的尺度很大比如每一维的数量级都在1e3附近那么任意两个样本间的平方距离动辄达到1e6此时就算把σ调到100指数的输入依然是个极大的负数核矩阵元素全部趋近于0。这种情况我遇到过很多次。症状非常典型MMD loss从头到尾几乎不下降打印核矩阵发现全是小于1e-8的小数。排查到最后根因就是特征没有做标准化。处理方法建议按顺序做先对特征做标准化。最简单的做法是对特征向量做z-score归一化使得每一维的均值接近0、方差接近1。之后大多数样本间的欧氏距离落在0到10的区间内σ 取 1 到 10 就能正常工作。特征标准化后再用 median heuristic 确定 σ。具体做法是随机抽一部分样本对计算它们之间的平方距离取这些距离的中位数作为 σ²。这是从核学习中借鉴的经典经验省去了手工搜索的麻烦。如果标准的z-score归一化不好插入训练流程至少也得在测试阶段做一个简单的全局缩放让特征范数的平均值保持在合理范围内。另外要特别提醒如果用的是深层网络提取的特征最后一层的输出特征可能维度极高本身发散就比较严重。这种情况下我会把MMD约束放在一个专门的嵌入层之后而不是直接作用在全连接层上。嵌入层的输出维度保持中等比如256到512方便核计算同时保留足够的信息。5.3 MMD权重 λ 的调整如何避免约束变成了主导在一个典型工作流里任务损失比如分类的cross-entropy和MMD约束同时作用于网络。如果MMD的系数设置太大网络会走向另一个极端——刻意把源域和目标域的特征压到同一个点附近特征失去了判别能力任务损失同样会上升。我的经验套路是这样的初始先把 λ 设为0跑几个step记录任务损失的数值范围 L0。然后启用MMD把 λ 设为一个很小的值比如0.01跑训练看MMD loss的变化范围。如果MMD loss的量级远远小于任务损失按比例调高 λ如果MMD loss已经能和任务损失持平甚至超过说明 λ 过大要调小。一般建议让 MMD loss 在训练初期占整体 loss 的 10%~30% 左右。这个比例下模型通常既能完成主任务又能够得到有效的分布对齐。还有一个细节不是所有训练阶段都适合施加完整的MMD约束。在训练早期特征还处于剧烈变化阶段MMD约束可能干扰特征的初始化收敛。一种常见的做法是预热——前几个epoch让特征自由训练然后逐渐提高 λ 到目标值。这种由松到紧的调度比全程恒定权重稳定得多。5.4 用训练日志判断MMD是否真正参与优化很多时候你以为MMD在起作用其实它根本没有梯度。这里分享一个非常廉价的检查手段在每次迭代后手动计算一次MMD的梯度范数。PyTorch中的实现方式mmd_val mmd_multi_rbf(f_source, f_target) mmd_val.backward(retain_graphTrue) grad_norm f_source.grad.norm().item() print(grad_norm)如果这个梯度范数是0.0或者极其微小小于1e-8那大概率是特征输入带detach()或者Banana了梯度。如果梯度范数正常但MMD loss始终不降那就去检查特征内部的结构是否真的存在分布差异。另外如果想要观察MMD曲线是否平滑建议用滑动平均观察。我一般保留最近50个step的MMD值做EMA这样能排除偶发的batch波动更能反映真正的训练趋势。6. 除了域适应MMD还能用在哪几个地方MMD最有名的应用是迁移学习和域适应但它并不局限于此。几个我实际试过且效果不错的方向6.1 MMD作为生成模型的学习目标或评估指标在GAN训练中判别器的作用是区分真假样本但GAN的训练稳定性问题一直存在。MMD-GAN一类方法直接把判别器替换为MMD计算用核距离代替对抗损失。这样避开了JS散度饱和的问题也省去了判别器与生成器之间的平衡博弈。作为一个轻量级的生成质量评估MMD也能用。给定真实图像的特征和生成图像的特征MMD越小说明分布越接近。相比FID需要假设特征空间是高斯的MMD的优势在于没有强假设。但要注意MMD作为评估指标对特征提取器的选择非常敏感不同层的特征会得到差别很大的结论。建议在同一组特征层上横向比较不同模型不要跨层比较数值。6.2 数据增强后的分布漂移监控训练过程中如果使用了强数据增强我们通常会担心增强后的数据是否偏离了原始分布太远。这时可以周期性地计算增强后样本与原始样本之间的MMD如果MMD在可接受范围内说明增强策略没有破坏分布特性如果MMD持续增大说明增强导致训练分布发生了明显偏移可能需要降低增强强度这个监控手段的实现成本极低只要特征提取器固定MMD本身计算开销很小。我在一组图像分类实验里用这个方案判断AutoAugment的强度上限效果不错。6.3 从深度特征到条件MMD更精细的分布对齐标准MMD只对齐边际分布这在实际应用中往往不够。分类任务里两个域如果类别比例不同边际分布对齐可能会把不同类别的特征混在一起。更精细的做法是条件MMD——在标签条件下做对齐。实现上可以先按类别把特征分组然后对每一类分别计算MMD求和。代价是batch里必须包含足够的每类样本实际使用中可以对每类采样固定数量组成一个batch。条件MMD还有个变体叫Joint MMD把特征和标签拼接在一起做一个联合分布的MMD。从我自己实验来看如果标签信息可靠Joint MMD对齐效果往往比仅特征对齐更好训练收敛也更快。但如果标签本身带有噪音拼接标签反而引入干扰这时候默认的MMD反而更稳。6.4 对特征维度的降维诊断MMD的核函数可以作用于任意特征空间所以它也可以用来做特征表征质量的诊断。例如训练一个自编码器后计算编码特征的MMD与原始特征的MMD观测二者是否接近。如果相差很大说明编码过程可能丢失了大量分布级信息而不仅仅是重构损失能够捕捉的误差。这个用法不要求额外的label对自监督任务尤其适用。还有一个技巧是把MMD用在网络的多个层上一起计算多个层的对称分布距离。比如第2、第4、第6层的特征都计算MMD并求和。这种方式叫做多层MMD比只计算最后一层更能抓到语义级别的分布差异训练时也能更充分地传递梯度。不过要小心多层MMD给整体loss带来的收益会随层数增加而递减一般选三层就足够了。关于MMD的最后一组建议回过头来看MMD的原理并不复杂但真正在用的时候细节中的坑一个接一个。工具的选型上PyTorch和TensorFlow两个版本我都保留了——PyTorch版适合快速实验和调试TensorFlow版适合已经用Keras搭建好整套训练管线、想要直接塞入MMD约束的情况。代码的核心部分完全一致迁移成本低。我的实际体会是**MMD的价值不全在数学公式上而在如何把它正确接入到你的训练系统中。**标准化、核带宽、batch size、权重系数、梯度流检查——这些东西听着琐碎但一个处理不好MMD就会变成一个看起来在算、实际上毫无作用的死代码。如果你正在调试MMD却怎么都看不到效果先从上面第5章的检查清单逐项过一遍大部分问题都能解决。最后分享一个小技巧当你能相对稳定地使用MMD后试着把核带宽列表作为一个可超参搜索对象配合验证集的表现扫一遍。核带宽的选择对MMD效果的影响往往比我最初想象的大得多。一组合适的带宽组合可以让同样的特征对齐问题收敛速度快上一倍。多试几组找到适合你数据的那一组比在模型结构上反复折腾更省时间。