PyTorch 实战:计算 Wasserstein 距离

2019-09-23 18:42:56

This blog is copied from: https://mp.weixin.qq.com/s/nTUKYNxdiPK3xdOoSXvTJQ

最优传输理论及 Wasserstein 距离是很多读者都希望了解的基础,本文主要通过简单案例展示了它们的基本思想,并通过 PyTorch 介绍如何实战 W 距离。

机器学习中的许多问题都涉及到令两个分布尽可能接近的思想,例如在 GAN 中令生成器分布接近判别器分布就能伪造出逼真的图像。但是 KL 散度等分布的度量方法有很多局限性,本文则介绍了 Wasserstein 距离及 Sinkhorn 迭代方法,它们 GAN 及众多任务上都展示了杰出的性能。

在简单的情况下,我们假设从未知数据分布 p(x) 中观测到一些随机变量 x(例如,猫的图片),我们想要找到一个模型 q(x|θ)(例如一个神经网络)能作为 p(x) 的一个很好的近似。如果 p 和 q 的分布很相近,那么就表明我们的模型已经学习到如何识别猫。

因为 KL 散度可以度量两个分布的距离,所以只需要最小化 KL(q‖p) 就可以了。可以证明,最小化 KL(q‖p) 等价于最小化一个负对数似然,这样的做法在我们训练一个分类器时很常见。例如,对于变分自编码器来说,我们希望后验分布能够接近于某种先验分布,这也是我们通过最小化它们之间的 KL 散度来实现的。

尽管 KL 散度有很广泛的应用,在某些情况下,KL 散度则会失效。不妨考虑一下如下图所示的离散分布:

KL 散度假设这两个分布共享相同的支撑集(也就是说,它们被定义在同一个点集上)。因此,我们不能为上面的例子计算 KL 散度。由于这一个限制和其他计算方面的因素促使研究人员寻找一种更适合于计算两个分布之间差异的方法。

在本文中,作者将:

  • 简单介绍最优传输问题

  • 将 Sinkhorn 迭代描述为对解求近似

  • 使用 PyTorch 计算 Sinkhorn 距离

  • 描述用于计算 mini-batch 之间的距离的对该实现的扩展

移动概率质量函数

我们不妨把离散的概率分布想象成空间中分散的点的质量。我们可以观测这些带质量的点从一个分布移动到另一个分布需要做多少功,如下图所示:

接着,我们可以定义另一个度量标准,用以衡量移动做所有点所需要做的功。要想将这个直观的概念形式化定义下来,首先,我们可以通过引入一个耦合矩阵 P(coupling matrix),它表示要从 p(x) 支撑集中的一个点上到 q(x) 支撑集中的一个点需要分配多少概率质量。对于均匀分布,我们规定每个点都具有 1/4 的概率质量。如果我们将本例支撑集中的点从左到右排列,我们可以将上述的耦合矩阵写作:

也就是说,p(x) 支撑集中点 1 的质量被分配给了 q(x) 支撑集中的点 4,p(x) 支撑集中点 2 的质量被分配给了 q(x) 支撑集中的点 3,以此类推,如上图中的箭头所示。

为了算出质量分配的过程需要做多少功,我们将引入第二个矩阵:距离矩阵。该矩阵中的每个元素 C_ij 表示将 p(x) 支撑集中的点移动到 q(x) 支撑集中的点上的成本。点与点之间的欧几里得距离是定义这种成本的一种方式,它也被称为「ground distance」。如果我们假设 p(x) 的支撑集和 q(x) 的支撑集分别为 {1,2,3,4} 和 {5,6,7,8},成本矩阵即为:

根据上述定义,总的成本可以通过 P 和 C 之间的 Frobenius 内积来计算:

你可能已经注意到了,实际上有很多种方法可以把点从一个支撑集移动到另一个支撑集中,每一种方式都会得到不同的成本。上面给出的只是一个示例,但是我们感兴趣的是最终能够让成本较小的分配方式。这就是两个离散分布之间的「最优传输」问题,该问题的解是所有耦合矩阵上的最低成本 L_C。

由于不是所有矩阵都是有效的耦合矩阵,最后一个条件会引入了一个约束。对于一个耦合矩阵来说,其所有列都必须要加到带有 q(x) 概率质量的向量中。在本例中,该向量包含 4 个值为 1/4 的元素。更一般地,我们可以将两个向量分别记为 a 和 b,因此最有运输问题可以被写作:

当距离矩阵基于一个有效的距离函数构建时,最小成本即为我们所说的「Wasserstein 距离」。

关于该问题的解以及将其扩展到连续概率分布中还有大量问题需要解决。如果想要获取更正式、更容易理解的解释,读者可以参阅 Gabriel Peyré 和 Marco Cuturi 编写的「Computational Optimal Transport」一书,此书也是本文写作的主要参考来源之一。

这里的基本设定是,我们已经把求两个分布之间距离的问题定义为求最优耦合矩阵的问题。事实证明,我们可以通过一个小的修改让我们以迭代和可微分的方式解决这个问题,这将让我们可以很好地使用深度学习自动微分机制完成该工作。

熵正则化和 Sinkhorn 迭代

首先,我们将一个矩阵的熵定义如下:

正如信息论中概率分布的熵一样,一个熵较低的矩阵将会更稀疏,它的大部分非零值集中在几个点周围。相反,一个具有高熵的矩阵将会更平滑,其最大熵是在均匀分布的情况下获得的。我们可以将正则化系数 ε 引入最优传输问题,从而得到更平滑的耦合矩阵:

通过增大 ε,最终得到的耦合矩阵将会变得更加平滑;而当 ε 趋近于零时,耦合矩阵会更加稀疏,同时最终的解会更加趋近于原始最优运输问题。

通过引入这种熵正则化,该问题变成了一个凸优化问题,并且可 以通过使用「Sinkhorn iteration」求解。解可以被写作 P=diag(u)Kdiag(v),在迭代过程中交替更新 u 和 v:

其中 K 是一个用 C 计算的核矩阵(kernel matrix)。由于这些迭代过程是在对原始问题的正则化版本求解,因此对应产生的 Wasserstein 距离有时被称为 Sinkhorn 距离。该迭代过程会形成一个线性操作的序列,因此对于深度学习模型,通过这些迭代进行反向传播是非常简单的。

通过 PyTorch 实现 Sinkhorn 迭代

为了提升 Sinkhorn 迭代的收敛性和稳定性,还可以加入其它的步骤。我们可以在 GitHub 上找到 Gabriel Peyre 完成的详细实现。

项目链接:https://github.com/gpeyre/SinkhornAutoDiff。

让我们先用一个简单的例子来测试一下,现在我们将研究二维空间(而不是上面的一维空间)中的离散均匀分布。在这种情况下,我们将在平面上移动概率质量。让我们首先定义两个简单的分布:

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42) n_points = 5
a = np.array([[i, 0] for i in range(n_points)])
b = np.array([[i, 1] for i in range(n_points)]) plt.figure(figsize=(6, 3))
plt.scatter(a[:, 0], a[:, 1], label='supp($p(x)$)')
plt.scatter(b[:, 0], b[:, 1], label='supp($q(x)$)')
plt.legend();

我们很容易看出,最优传输对应于将 p(x) 支撑集中的每个点分配到 q(x) 支撑集上的点。对于所有的点来说,距离都是 1,同时由于分布是均匀的,每点移动的概率质量是 1/5。因此,Wasserstein 距离是 5×1/5= 1。现在我们用 Sinkhorn 迭代来计算这个距离:

import torch
from layers import SinkhornDistance x = torch.tensor(a, dtype=torch.float)
y = torch.tensor(b, dtype=torch.float) sinkhorn = SinkhornDistance(eps=0.1, max_iter=100, reduction=None)
dist, P, C = sinkhorn(x, y)
print("Sinkhorn distance: {:.3f}".format(dist.item())) ————————————————————————————————————————————————
Sinkhorn distance: 1.000

结果正如我们所计算的那样,距离为 1。现在,让我们查看一下「Sinkhorn( )」方法返回的矩阵,其中 P 是计算出的耦合矩阵,C 是距离矩阵。距离矩阵如下图所示:

plt.imshow(C)
plt.title('Distance matrix')
plt.colorbar();
plt.imshow(C)plt.title('Distance matrix')plt.colorbar();

元素「C[0, 0]」说明了将(0,0)点的质量移动到(0,1)所需要的成本 1 是如何产生的。在该行的另一端,元素「C[0, 4]」包含了将点(0,0)的质量移动到点(4,1)所需要的成本,这个成本是整个矩阵中最大的:

由于我们为距离矩阵使用的是平方后的 ℓ2 范数,计算结果如上所示。现在,让我们看看计算出的耦合矩阵吧:

plt.imshow(P)
plt.title('Coupling matrix');
plt.imshow(P)plt.title('Coupling matrix');

该图很好地向我们展示了算法是如何有效地发现最优耦合,它与我们前面确定的耦合矩阵是相同的。到目前为止,我们使用了 0.1 的正则化系数。如果将该值增加到 1 会怎样?

sinkhorn = SinkhornDistance(eps=1, max_iter=100, reduction=None)
dist, P, C = sinkhorn(x, y)
print("Sinkhorn distance: {:.3f}".format(dist.item()))
plt.imshow(P); ————————————————————————————————————————————————
Sinkhorn distance: 1.408

正如我们前面讨论过的,加大 ε 有增大耦合矩阵熵的作用。接下来,我们看看 P 是如何变得更加平滑的。但是,这样做也会为计算出的距离带来一个不好的影响,导致对 Wasserstein 距离的近似效果变差。

可视化支撑集的空间分配也很有意思:

def show_assignments(a, b, P):    
    norm_P = P/P.max()
    for i in range(a.shape[0]):
        for j in range(b.shape[0]):
            plt.arrow(a[i, 0], a[i, 1], b[j, 0]-a[i, 0], b[j, 1]-a[i, 1],
                     alpha=norm_P[i,j].item())
    plt.title('Assignments')
    plt.scatter(a[:, 0], a[:, 1])
    plt.scatter(b[:, 0], b[:, 1])
    plt.axis('off') show_assignments(a, b, P)

让我们在一个更有趣的分布(Moons 数据集)上完成这项工作。

from sklearn.datasets import make_moons

X, Y = make_moons(n_samples = 30)
a = X[Y==0]
b = X[Y==1] x = torch.tensor(a, dtype=torch.float)
y = torch.tensor(b, dtype=torch.float) sinkhorn = SinkhornDistance(eps=0.1, max_iter=100, reduction=None)
dist, P, C = sinkhorn(x, y)
print("Sinkhorn distance: {:.3f}".format(dist.item()))
show_assignments(a, b, P) ——————————————————————————————————————————
Sinkhorn distance: 1.714

Mini-batch 上的 Sinkhorn 距离

在深度学习中,我们通常对使用 mini-batch 来加速计算十分感兴趣。我们也可以通过使用额外的批处理维度修改 Sinkhorn 迭代来满足该设定。将此更改添加到具体实现中后,我们可以在一个 mini-batch 中计算多个分布的 Sinkhorn 距离。下面我们将通过另一个容易被验证的例子说明这一点。

代码:https://github.com/dfdazac/wassdistance/blob/master/layers.py

我们将计算包含 5 个支撑点的 4 对均匀分布的 Sinkhorn 距离,它们垂直地被 1(如上所示)、2、3 和 4 个单元分隔开。这样,它们之间的 Wasserstein 距离将分别为 1、4、9 和 16。

n = 5
batch_size = 4
a = np.array([[[i, 0] for i in range(n)] for b in range(batch_size)])
b = np.array([[[i, b + 1] for i in range(n)] for b in range(batch_size)]) # Wrap with torch tensors
x = torch.tensor(a, dtype=torch.float)
y = torch.tensor(b, dtype=torch.float) sinkhorn = SinkhornDistance(eps=0.1, max_iter=100, reduction=None)
dist, P, C = sinkhorn(x, y)
print("Sinkhorn distances: ", dist) ——————————————————————————————————————————
Sinkhorn distances:  tensor([ 1.0001,  4.0001,  9.0000, 16.0000])

这样做确实有效!同时,也请注意,现在 P 和 C 为 3 维张量,它包含 mini-batch 中每对分布的耦合矩阵和距离矩阵:

print('P.shape = {}'.format(P.shape))
print('C.shape = {}'.format(C.shape)) ——————————————————————————————————————————
P.shape = torch.Size([4, 5, 5])
C.shape = torch.Size([4, 5, 5])

结语

分布之间的 Wasserstein 距离及其通过 Sinkhorn 迭代实现的计算方法为我们带来了许多可能性。该框架不仅提供了对 KL 散度等距离的替代方法,而且在建模过程中提供了更大的灵活性,我们不再被迫要选择特定的参数分布。这些迭代过程可以在 GPU 上高效地执行,并且是完全可微分的,这使得它对于深度学习来说是一个很好的选择。这些优点在机器学习领域的最新研究中得到了充分的利用(如自编码器和距离嵌入),使其在该领域的应用前景更加广阔。

原文链接:https://dfdazac.github.io/sinkhorn.html

PyTorch 实战:计算 Wasserstein 距离的更多相关文章

  1. KL散度、JS散度、Wasserstein距离

    1. KL散度 KL散度又称为相对熵,信息散度,信息增益.KL散度是是两个概率分布 $P$ 和 $Q$  之间差别的非对称性的度量. KL散度是用来 度量使用基于 $Q$ 的编码来编码来自 $P$ 的 ...

  2. 深度学习之PyTorch实战(1)——基础学习及搭建环境

    最近在学习PyTorch框架,买了一本<深度学习之PyTorch实战计算机视觉>,从学习开始,小编会整理学习笔记,并博客记录,希望自己好好学完这本书,最后能熟练应用此框架. PyTorch ...

  3. 【深度学习】K-L 散度,JS散度,Wasserstein距离

    度量两个分布之间的差异 (一)K-L 散度 K-L 散度在信息系统中称为相对熵,可以用来量化两种概率分布 P 和 Q 之间的差异,它是非对称性的度量.在概率学和统计学上,我们经常会使用一种更简单的.近 ...

  4. 计算两点距离 ios

    //计算两点距离 -(float)distanceBetweenTwoPoint:(CGPoint)point1 point2:(CGPoint)point2 { ) + powf(point1.y ...

  5. 【百度地图API】如何根据摩卡托坐标进行POI查询,和计算两点距离

    原文:[百度地图API]如何根据摩卡托坐标进行POI查询,和计算两点距离 摘要: 百度地图API有两种坐标系,一种是百度经纬度,一种是摩卡托坐标系.在本章你将学会: 1.如何相互转换这两种坐标: 2. ...

  6. js计算元素距离顶部的高度及元素是否在可视区判断

    前言: 在业务当中,我们经常要计算元素的大小和元素在页面的位置信息.比如说,在一个滚动区域内,我要知道元素A是在可视区内,还是在隐藏内容区(滚动到外边看不到了).有时还要进一步知道,元素是全部都显示在 ...

  7. Spark Java API 计算 Levenshtein 距离

    Spark Java API 计算 Levenshtein 距离 在上一篇文章中,完成了Spark开发环境的搭建,最终的目标是对用户昵称信息做聚类分析,找出违规的昵称.聚类分析需要一个距离,用来衡量两 ...

  8. 微信小程序计算经纬距离

    微信小程序计算经纬距离 微信小程序计算两点间的距离 getDistance: function (lat1, lng1, lat2, lng2) { lat1 = lat1 || 0; lng1 = ...

  9. C#实现根据地图上的两点坐标,计算直线距离

    根据地图上的两点坐标,计算直线距离,在网上找到javascript的写法,用C#实现一下 /// <summary> /// 根据地图上的两点坐标,计算直线距离 /// </summ ...

随机推荐

  1. 【故障处理】ORA-12162 错误的处理

    [故障处理]ORA-12162: TNS:net service name is incorrectly specified 一.1  场景 今天拿到一个新的环境,可是执行sqlplus / as s ...

  2. 复盘一篇浅谈KNN的文章

    认识-什么是KNN KNN 即 K-nearest neighbors, 是一个hello world级别, 但被广泛使用的机器学习算法, 中文叫K近邻算法, 是一种基本的分类和回归方法. KNN既可 ...

  3. IT黑马-面向对象

    先说面向过程 面向过程主要考虑的是怎么做 把完成摸个需求的 所有步骤 从头到尾 逐步实现 根据开发需求,将某些功能独立的代码封装成一个又一个的函数 最后完成的代码就是顺序的调用不同的函数. 特点是: ...

  4. mysql数据库查询缓存总结

    概述 查询缓存(Query Cache,简称QC),存储SELECT语句及其产生的数据结果.闲来无事,做一下这块的总结,也做个备忘! 工作原理 查询缓存工作原理如下: 缓存SELECT操作的结果集和S ...

  5. Linux之RHEL7root密码破解(一)

    很多时候我们都会有这样的经历,各种密码,各种复杂,忘记了怎么办???Windows的有关密码忘记了是可以通过相关的邮箱啊手机号等等是可以 找回的,那么Linux的root密码忘记了,该怎么办呢?那么接 ...

  6. solr的倒序索引

    倒序索引: 在每次进行检索时,搜索引擎必须遍历每个网页,查找网页中是否包含你指定的关键词,这个工作量是十分巨大的,主要原因有: 1.互联网的网页基数非常大; 2.在每个网页中检索是否含有指定的关键词并 ...

  7. Python开发应用-正则表达进行排序搜索

    re模块提供了3个方法对输入的字符串进行确切的查询,match和search最多只会返回一个匹配条件的子串,可以理解为非贪婪模式,而findall会返回N个匹配条件的子串,可以理解为贪婪模式 re.m ...

  8. 7-html列表

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...

  9. 4、NameNode启动过程详解

    NameNode 内存 本地磁盘 fsimage edits 第一次启动HDFS 格式化HDFS,目的就是生成fsimage start NameNode,读取fsimage文件 start Data ...

  10. spingboot jar 包启动遇到得坑

    先摘抄一篇文章 pringboot打成jar包后,可直接用java -jar app.jar 启动,或者使用 nohup java -jar app.jar & 后台启动,也可以将 jar包链 ...