寻找一个包含 n 个不同向量的子集,使得它们之间的最小距离为 d。

4
我有一组向量 V(例如一个64维的向量,如[1,2,...64])。从向量集合 V 中,我想找到一个由 n 个不同向量组成的子集 V1,其中:

  1. 子集 V1 中的任意两个向量之间的最小距离为 d;并且
  2. 对于向量集合 V 中的任何元素 w,都存在一个元素 w' 在子集 V1 中,使得它与 w 的距离小于 d

什么算法能够有效地确定一个包含 n 个不同向量的子集,其中 n 的值是最小的?

距离可以是欧几里得距离或余弦相似度(当然不能混用)。


最小可能的n是什么意思? - joostblack
@joostblack 最小值的 n 值,对吧? - user202729
1
你应该询问最大的n吗?因为任何一个只有1个元素的集合都满足条件。 - user202729
@tobias_k 是的,那就是要点。 - william007
1
通常情况下(k-means,k-medians),我们感兴趣的是v1 成为一组新向量(不一定是v 的成员)。我所知道的一个例外是k-medoids。但这并不完全符合您的问题,只是探索的一个领域。其他感兴趣的领域包括分层聚类。 - Pierre D
显示剩余7条评论
3个回答

2
让我们这样考虑你的问题:你在64维空间中有一组球,每个球的半径为d,表示每个输入向量周围的空间。(注意,我假设你所说的“距离”是欧几里得距离。)
现在,你想找到覆盖每个原始点的最小子集,这意味着你需要每个点至少在子集中的一个球内部,并且覆盖中的球必须具有距离d的中心。
如果没有这个额外的限制,你将面临一个难以解决但经过深入研究的问题,称为几何集覆盖,它又是更著名的集合覆盖问题的一个特例。直观上看,额外的限制使问题更加困难,但我没有证明。
坏消息是,(几何)集合覆盖问题是NP难问题,这意味着如果原始集合中可能有许多点,则您将无法快速找到确切的最小值。
好消息是,有很好的算法可以找到近似解,这将给您一个不一定尽可能小的集合,但是接近最小可能。
以下是一个简单贪心算法的Python代码,它不总能找到最小大小的覆盖,但总是返回一个有效的覆盖:
def greedy(V, d):
    cover = set()
    uncovered = set(V)
    # invariant: all(distance(x,y) > d for x in cover for y in uncovered)
    while uncovered:
        x = uncovered.pop()
        cover.add(x)
        uncovered = {y for y in uncovered if distance(x,y) > d}

请注意,您可以通过用更聪明的选择替换 uncovered.pop() 调用来使这个贪心算法更好,例如选择一个能够“覆盖”最多剩余点的向量。

1
谢谢,我确实是指欧几里得距离,但如果我将距离度量改为余弦相似度,会有什么区别吗? - william007
@william007 对于我所知道的解决方案(大多基于集合覆盖),距离度量不会有影响,但可能有一些技巧依赖于具有距离度量(余弦相似度不是)。 - Dan R
1
但是集合覆盖或顶点覆盖方案是必要但不充分的:我们不仅希望v中的每个点都可以被至少一个距离 <rv1 中的点到达,而且我们还希望v1中的所有点相互之间的距离都大于 r - Pierre D
@PierreD 你是对的,我忽略了那个限制的重要性。我会相应地编辑我的答案。 - Dan R
1
@wildplasser 是的,但是OP也提到了余弦相似度,它不满足三角不等式,因此不是一个度量。 - Dan R
显示剩余2条评论

0

首先,我们可以推导出一个无向图,其中顶点是向量v的元素,边缘是不超过r的点对。这不需要距离成为一个适当的度量:它不必满足三角不等式,只需对称(dist(a,b) == dist(b,a))并且为0,如果两个点相等。

在完成这一步之后,我们可以完全忽略距离,并专注于对图进行分区。正如其他人所指出的那样,该分区是一个Vertex Cover问题。但是,它有一个扭曲的地方:我们要求覆盖中的所有顶点都是不相交的(即:v1中的向量不能彼此在距离r以内)。

为了获得图形,我们可以使用高效的KDTree仅计算一次最近邻结构。特别地,我们将使用kd.sparse_distance_matrix(kd, r)来获取所有距离小于r的点对:

from scipy.spatial import KDTree

def get_graph(v, r):
    kd = KDTree(v)
    sm = kd.sparse_distance_matrix(kd, r)
    graph = {i: {i} for i, _ in sm.keys()}
    for i, j in sm.keys():
        graph[i] |= {j}
    return graph

(注意:如果我们想要使用其他距离,.sparse_distance_matrix()有一个参数p)。

图的分区可以通过多种方式完成。@DanR展示了一种贪婪的方法,非常快速,但通常在找到v1的大小时不够优秀。

下面展示了一种暴力方法,如果存在最小解,则保证找到最小解。对于相对较少的向量来说是足够的,并且可以为其他启发式算法的最优解提供基准。

为了加速组合搜索,我们首先观察到通常一些点与任何其他点的距离不超过r(即上面找到的图并未表示v的所有点)。我们可以将这些点放在一边,因为它们必然是v1的一部分,但不会干扰搜索的其余部分(下面称之为“单例”)。其次,如果它们不完全不相交,我们会削减无望的“线索”(组合扩展中的前缀)。这通过切断整个搜索空间的大片区域来显著加速完整搜索。如果没有削减前缀,则完整搜索的时间呈指数增长。

这个的速度是多少?它取决于v向量的分布,以及与距离r(强烈)相关。实际上,它取决于找到的聚类数。为了给出一个想法,在2D中均匀选择v 20个向量,我观察到大约30毫秒。对于40个向量,通常在100毫秒左右,但对于某些r值,它可能会跳到超过2秒。

我们实现了combinations的变体,它具有一个check函数来削减不太有希望的前缀:

from itertools import combinations

def _okall(tup, *args, **kwargs):
    return True

def combinations_all(iterable, n0=1, n1=None, check=None, *args, **kwargs):
    pool = tuple(iterable)
    n = len(pool)
    if n0 > n:
        return
    n1 = n if n1 is None else n1
    check = _okall if check is None else check
    
    if n0 < 1:
        yield ()
        n0 = 1
    seed = list(combinations(range(n), n0-1))
    for r in range(n0, n1+1):
        prev_seed = seed
        seed = []
        for prefix in prev_seed:
            for j in range(max(prefix, default=-1)+1, n):
                indices = prefix + (j,)
                tup = tuple(pool[i] for i in indices)
                if check(tup, *args, **kwargs):
                    seed.append(indices)
                    yield tup

例子:

# list all combinations of [0,1,2,3] (of all sizes), excluding those starting
# by (0,1)

>>> list(combinations_all(range(4), check=lambda tup: tup[:2] != (0,1)))
[(0,),
 (1,),
 (2,),
 (3,),
 (0, 2),
 (0, 3),
 (1, 2),
 (1, 3),
 (2, 3),
 (0, 2, 3),
 (1, 2, 3)]

现在,我们使用它来查找最小不相交覆盖并返回v1的索引:
def check(tup, graph):
    # refuses tup if any one is within reach of another
    # optimization: tup[:-1] has already passed this test, so just check the last one
    if len(tup) < 2:
        return True
    reach_of_last = graph[tup[-1]]
    prefix = set(tup[:-1])
    return prefix.isdisjoint(reach_of_last)

def brute_vq(v, r):
    n = v.shape[0]
    graph = get_graph(v, r)
    to_part = set(graph)
    singletons = set(range(n)).difference(to_part)
    if not to_part:
        return sorted(singletons)
    for tup in combinations_all(to_part, check=check, graph=graph):
        # here tup are indices that are far apart enough (guaranteed disjoint in reach)
        # check if they fully cover to_part
        cover = {j for i in tup for j in graph[i]}
        if cover == to_part:
            v1_idx = sorted(singletons.union(tup))
            return v1_idx

示例:

v = np.random.uniform(size=(20, 2))
r_s = [[0.2, 0.3], [0.4, 0.5]]
fig, axes = plt.subplots(nrows=len(r_s), ncols=len(r_s[0]),
                         figsize=(12,12), sharex=True, sharey=True)
for r, ax in zip(np.ravel(r_s), np.ravel(axes)):
    idx = brute_vq(v, r)
    ax.set_aspect('equal')
    ax.scatter(v[:, 0], v[:, 1])
    for i, p in enumerate(v):
        ax.text(*p, str(i))
    for i in idx:
        circ = plt.Circle(v[i], radius=r, color='g', fill=False)
        ax.add_patch(circ)
plt.show();

enter image description here


你读了问题吗?OP想要找到一个单一的子集。这不是关于聚类的问题。 - wildplasser
在上述代码中,idxv 向量列表的索引,用于生成 v1。在向量量化(VQ)和聚类术语中,这样的解决方案 v1 被称为码本或原型。每张图片显示了给定 r 值的解决方案,它绘制了每个原型的绿色圆圈(以其为中心,半径为 r),以及 v 向量作为蓝点,以便通过视觉验证解决方案。 - Pierre D

-2

我的看法:


创建一个距离矩阵(这是一个 O*O 操作,显然):
  A B C D | min,max
A 0 3 2 1 | 1,3
B 3 0 5 4 | 3,5
C 2 5 0 6 | 2,6
D 1 4 6 0 | 1,6 # note: D-C violates the triangle inequality

距离矩阵,重新标记:

  A D B C | min,max
A 0 1 3 2 | 1,3
D 1 0 4 6 | 1,6
B 3 4 0 5 | 3,5
C 2 6 5 0 | 2,6

现在,对于每一行(或列),取最小值的最大值(或最大值的最小值...) 距离为3时,西北部分完全连接,其余部分仍然部分连接 (对于d=4,B也将进入子集)

  A D | B C | min,max
A 0 1 | 3 2 | 1,3
D 1 0 | 4 6 | 1,6
  ----+ ----------
B 3 4 | 0 5 | 3,5
C 2 6 | 5 0 | 2,6

我不明白你暗示的算法。v1的向量是什么? - Pierre D
此外,数字距离本身并不重要。一对点要么比“r”更接近,要么比“r”更远。使用kd = KDTree(v),然后kd.sparse_distance_matrix(kd,r)是一种非常有效的方法,用于查找彼此之间距离更近的所有点对。然后这只是一个带有扭曲的图形覆盖问题:覆盖中相互排斥。 - Pierre D
在这种情况下,您可以看到MAX(min())的结果为三,将隔离最小连接子集。我将其移动到左上角以便阅读。 - wildplasser
不,问题只是定义不清楚。再提一个反问:你能发明一个方法不起作用的问题吗? - wildplasser
尊敬的,我认为所提出的方法根本不解决OP的问题。 v1中的所有向量必须相互间隔至少d。v中的所有向量必须在d之内与v1中的某个向量保持一致。找到满足这些条件的最小向量集v1是NP难题。 如果您在您的示例中建议{A,D}是v1的解决方案,那么这并不起作用,因为它们彼此比v中的任何其他点更接近。 这与OP想要的相反。 - Pierre D
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接