ElasticSearch学习篇11_ANNS之基于图的NSW、HNSW算法

  1. 输出结果:最终的聚类中心即为聚类结果,每个数据点被分配到一个簇中。

需要注意的是,k-means算法对初始聚类中心的选择敏感,可能会得到不同的结果。因此,可以多次运行算法,选择最优的结果。另外,k-means算法对于非凸形状的数据集效果可能不佳,此时可以考虑使用其他聚类算法。

ps:凸形数据集:如果一个数据集D是凸的,简单来说,数据集D中任意两点的连线上的点,也会在数据集D内,那么数据集D就是一个凸集。

简单的k-means代码demo

package org.example.kmeans;

import java.util.ArrayList;
import java.util.List;

/\*\*
 \* @author sichaolong
 \* @createdate 2024/3/29 10:20
 \*/

public class SimpleKMeansDemo {
    private int k; // 聚类的个数
    private List<Point> points; // 数据集
    private List<Cluster> clusters; // 聚类结果

    public SimpleKMeansDemo(int k, List<Point> points) {
        this.k = k;
        this.points = points;
        this.clusters = new ArrayList<>();
    }

    public void run() {
        // 初始化聚类中心, 随机选取k个数据点作为初始聚类中心
        for (int i = 0; i < k; i++) {
            Cluster cluster = new Cluster();
            cluster.setCentroid(points.get(i));
            clusters.add(cluster);
        }

        // 循环直到收敛
        boolean converged = false;
        while (!converged) {
            // 清空聚类结果
            for (Cluster cluster : clusters) {
                cluster.clearPoints();
            }

            // 将每个数据点分配到最近的聚类中心
            for (Point point : points) {
                Cluster nearestCluster = null;
                double minDistance = Double.MAX\_VALUE;
                // 遍历每个聚类中心,找到最近的
                for (Cluster cluster : clusters) {
                    double distance = point.distanceTo(cluster.getCentroid());
                    if (distance < minDistance) {
                        minDistance = distance;
                        nearestCluster = cluster;
                    }
                }
                nearestCluster.addPoint(point);
            }

            // 更新聚类中心
            converged = true;
            // 遍历每个聚类中心,如果聚类中心没有发生变化,则收敛
            for (Cluster cluster : clusters) {
                Point oldCentroid = cluster.getCentroid();
                Point newCentroid = cluster.calculateCentroid();
                if (!oldCentroid.equals(newCentroid)) {
                    cluster.setCentroid(newCentroid);
                    converged = false;
                }
            }
        }
    }

    public List<Cluster> getClusters() {
        return clusters;
    }

    public static void main(String[] args) {
        // 创建数据集
        List<Point> points = new ArrayList<>();
        points.add(new Point(1, 1));
        points.add(new Point(1, 2));
        points.add(new Point(2, 2));
        points.add(new Point(5, 5));
        points.add(new Point(6, 6));
        points.add(new Point(7, 7));

        // 创建K-means对象并运行算法
        SimpleKMeansDemo kMeans = new SimpleKMeansDemo(2, points);
        kMeans.run();

        // 输出聚类结果
        List<Cluster> clusters = kMeans.getClusters();
        for (int i = 0; i < clusters.size(); i++) {
            System.out.println("Cluster " + (i + 1) + ":");
            for (Point point : clusters.get(i).getPoints()) {
                System.out.println("(" + point.getX() + ", " + point.getY() + ")");
            }
            System.out.println();
        }
    }
}

class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    /\*\*
 \* 计算两个点之间的距离,使用欧几里徳距离
 \* @param other
 \* @return
 \*/
    public double distanceTo(Point other) {
        double dx = x - other.x;
        double dy = y - other.y;
        return Math.sqrt(dx \* dx + dy \* dy);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Point other = (Point) obj;
        return Double.compare(other.x, x) == 0 && Double.compare(other.y, y) == 0;
    }

    @Override
    public int hashCode() {
        return Double.hashCode(x) + Double.hashCode(y);
    }
}

class Cluster {
    private Point centroid;
    private List<Point> points;

    public Cluster() {
        this.points = new ArrayList<>();
    }

    public Point getCentroid() {
        return centroid;
    }

    public void setCentroid(Point centroid) {
        this.centroid = centroid;
    }

    public List<Point> getPoints() {
        return points;
    }

    public void addPoint(Point point) {
        points.add(point);
    }

    public void clearPoints() {
        points.clear();
    }


    /\*\*
 \* 计算聚类中心点
 \* @return
 \*/
    public Point calculateCentroid() {
        double sumX = 0;
        double sumY = 0;
        for (Point point : points) {
            sumX += point.getX();
            sumY += point.getY();
        }
        double avgX = sumX / points.size();
        double avgY = sumY / points.size();
        return new Point(avgX, avgY);
    }
}





PQ量化以及搜索

对一个向量数据库进行PQ量化主要分为聚类、量化两个部分,简单理解就是横向拆分,然后k-means聚类,分别计算每个字向量距离最近的簇ID作为本子向量的代表,库中的向量(包含若干子向量)会得到一个量化矩阵,后续搜索按照上述步骤直接从量化矩阵匹配就能找出相似向量。

  • 乘积量化有个重要的参数m_split ,这个参数控制着向量被切分的段数,如图所示,假设每个向量的维度为128,每个向量被切分为4段,这样就得到了4个小的向量,对大量每段小向量分别进行聚类,聚类个数为256个,这就完成了Cluster。
  • 然后做Assign操作,先每个聚类进行编码,然后分别对每个向量来说,先切分成四段小的向量,对每一段小向量,分别计算其对应的最近的簇心,然后使用这个簇心的ID当做该向量的第一个量化编码,依次类推,每个向量都可以由4个ID进行编码。每个ID可以由一个字节保存,每个向量只需要用4个字节就可以编码,这样就完成的向量的压缩,节省了大量的内存,压缩比率2000+。这一步其实就是Faiss训练的部分,目的是为了获取索引Index。

如何得到量化矩阵:假设向量库存在N个大向量,一个大向量是128维,切分4 * 32 个子向量,通过k-means形成了256个聚类,
(1)得到子向量量化编码:那么每个子向量会找出最近的 1/256 个簇ID,这个簇ID假如是a就作为每个字向量的量化编码。一个大向量切分了4个子向量,所以大向量量化向量为(a1,a2,a3,a4).
(2)如果是N个大向量,那就是N * (a1,a2,a3,a4),此就是向量库N个大向量的量化矩阵。
image.png

搜索过程:大的向量是128维,被切分为4 * 32维的小向量,每个小向量从256个簇ID找到一个距离最近(根据k-means选取的距离公式计算32维向量的最相似向量)的值,然后组成(a,b,c,d)去距离量化矩阵匹配,匹配上去了,那么就找到了簇ID邻近的向量数据(至于如何找,有很多方法,比如_IVFPQ_就是为了加快检索速率创建簇中心ID ——> 本簇中心的向量的倒排索引),接着找这个簇ID上面的全部相似子向量,组合大向量 or 最近距离返回。

image.png

二、基于图的ANNS算法

摘自论文-基于图的近似最近邻搜索的综合综述和实验比较
现有的基于图的ANNS算法的索引一般是四种经典基图从不同角度的衍生,即Delaunay图(DG)[35]、相对邻域图(RNG)[92]、K-近邻图(KNNG) [75]和最小生成树(MST)[58]。一些代表性的 ANNS 算法,例如 KGraph [31]、HNSW [67]、DPG [61]、SPTAG [27],可以分为基于 KNNG(KGraph 和 SPTAG)和基于 RNG(DPG 和 HNSW)的组

基于图的ANNS算法可以分为,针对基础图当前很多算法,当前也有很多优化算法,比如HNSW,DPG,NGT,NSG,SPTAG。

  • 德劳内图(DG)
  • 相对领域图(RNG)
  • K近邻图(KNNG)
  • 最小生成树(MST)

image.png

2.1、NSW算法

NSW(Navigable Small World )翻译为可导航小世界,是一种基于图的紧邻搜索算法。

// TODO

2.2、HNSW算法

Hierarchical Navigable Small Worlds (HNSW)可以被翻译为 分层可导航小世界,是16年提出来的一种图结构紧邻搜索算法。

摘要论文 — 我们提出了一种基于具有可控层次结构的可导航小世界图(新南威尔士州分层,HNSW)的近似K最近邻搜索的新方法。所提出的解决方案 是完全基于图的,不需要任何额外的搜索结构,这些结构通常用于最接近图技术的粗略搜索阶段。分层新南威尔士州以增量方式构建多层结构,该结 构由存储元素的嵌套子集的分层邻近图(层)集组成。存在元素的最大层是随机选择的,概率分布呈指数衰减。这允许生成类似于先前研究的可导航 小世界 (NSW) 结构的图形,同时还具有由其特征距离尺度分隔的链接。与新南威尔士州相比,从上层开始搜索并利用刻度分离可以提高性能,并 允许对数复杂性缩放。额外使用启发式方法来选择邻近图邻居,可显著提高高召回率和高度聚类数据下的性能。性能评估表明,所提出的通用度量空 间搜索索引能够大大优于以前的开源最先进的纯向量方法。该算法与跳过列表结构的相似性允许直接平衡的分布式实现。

参考

img
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

数据检索工业级解决方案,阿里巴巴浙大博士论文]( ),知乎

[外链图片转存中…(img-1LQTA7qB-4702073951707)]
[外链图片转存中…(img-7PDi2mT4-4702073951708)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值