原文:https://blog.csdn.net/CHIERYU/article/details/86647014

HNSW算法原理(二)之删除结点

本篇文章继之前的一篇文章 HNSW算法原理(一) ,这次讲讲HNSW算法中一个关键问题:HNSW中如何删除元素。

一、HNSW中如何删除元素

一个理想的索引结构应该支持增删改查操作,由于 HNSW算法原始论文 只给出了增与查的伪代码,并没有给出删的代码,而一些项目中往往需要对已经插入到HNSW索引中的结点进行删除,比如在某些人脸识别任务中,已经1个月没有出现的人脸特征可以删除掉。下面开始分析如何在HNSW中实现删除元素操作。

首先,明确下HNSW中插入元素的过程是怎么样的。当来了一个待插入新特征,记为 x,我们先随机生成 x 所在的层次level,然后分2个步骤,1)从max_level到level+1查询离 x 最近的一个结点;2)从level到0层查询 x 最近的 maxM 个结点,然后将 x 与这些M个点建立有向连接。上述2个步骤,当需要删除元素时,就需要删除与它相连的边,第1个步骤对删除操作毫无影响,第2个步骤对删除操作的影响主要包括:1)x 在每一层有许多有向边指向 x 的邻居结点,删除 x 那么也要删除 x 所指向的边;2)x 在每一层可能是其他结点的前M个邻居结点之一,设该结点为 b,当删除 x时,应该删除由 b 出现指向 x 的有向边。

当我们需要实现删除操作时,那么必须要检查每一层中所有结点的maxM个邻居是否因为删除 x 而发生改变,如果发生改变,那么就需要重建该结点的maxM邻居信息。这个工作非常耗时,要达到此目的,我这有2个办法。一是,采用lazy模式,实际上不删除 x,而是把x加入到黑名单中,下次搜索时判断是否在黑名单中而决定是否返回x,当黑名单数超过一定阈值时,重建HNSW索引;二是采用空间换时间办法,当把 x插入时就在x的邻居信息中记住 x 是哪些结点的邻居。

我们发现在hnswlib和faiss中都没有关于删除操作的实现,可以从下面代码中关于双向链表的内存申请中看出:

  1. //from hnswlib code

  2. //https://github.com/nmslib/hnswlib/blob/master/hnswlib/hnswalg.h

  3. HierarchicalNSW(SpaceInterface<dist_t> *s, size_t max_elements, size_t M = 16, size_t ef_construction = 200, size_t random_seed = 100) :

  4. link_list_locks_(max_elements), element_levels_(max_elements) {

  5. max_elements_ = max_elements;

  6. // other codes

  7. visited_list_pool_ = new VisitedListPool(1, max_elements);

  8. // other codes

  9. //此处给双向链表开辟空间,第i个元素的邻居结点信息在linkLists_[i];

  10. //第i个元素的第j>=1层邻居信息起始地址放在linkLists_[i]+(layer-1)*size_links_per_element_处

  11. linkLists_ = (char **) malloc(sizeof(void *) * max_elements_);

  12. //linkLists_是char**,计算每层每个结点的邻居信息字节长度

  13. size_links_per_element_ = maxM_ * sizeof(tableint) + sizeof(linklistsizeint);

  14. // other codes

  15. }

  1. //from faiss code

  2. //https://github.com/facebookresearch/faiss/blob/master/HNSW.h

  3. /// neighbors[offsets[i]:offsets[i+1]] is the list of neighbors of vector i

  4. /// for all levels. this is where all storage goes.

  5. std::vector<storage_idx_t> neighbors;

下面讲讲online-hnsw实现删除操作的原理及代码解读。

在基于图的索引结构中,邻居关系是非对称关系,A是B的topk邻居,但B不一定是A的topk邻居,因此表示邻居关系需要用一条有向边来表示,当A是B的topk邻居,则有一条从B指向A的有向边,为了在检索时按照距离排序,有向边带有权重,权重为A到B的距离。在hnsw中,为了保持邻居关系,设置每个结点有M个邻居结点,结点之间建立单向连接;为了支持删除操作,那么需要建立双向连接,即当A是B的topk邻居,则B的outgoing_links中有条边指向A,且A的ingoing_links中有一条边指向B。这样,当插入B时,建立B的与其M个邻居的双向连接,同时将B添加到每个邻居的ingoing_links中;当删除B时,需要删除B的所有outgoing_links,同时在B的邻居结点的ingoing_links中删除B,此外B也有ingoing_links,需要删除B的ingoing_links,之后还得在B的ingoing_links中对每个结点C,将B从C的outgoing_links中删除。

下面这个代码是修改一个结点的邻居关系操作:

  1. //from online-hnsw code

  2. //https://github.com/andrusha97/online-hnsw/blob/master/include/hnsw/index.hpp

  3. //重新设置结点node的邻居关系为new_links_set

  4. void set_links(const key_t &node,

  5. size_t layer,

  6. const std::vector<std::pair<key_t, scalar_t>> &new_links_set)

  7. {

  8. size_t need_links = max_links(layer);

  9. std::vector<std::pair<key_t, scalar_t>> new_links;

  10. new_links.reserve(need_links);

  11. if (options.insert_method == index_options_t::insert_method_t::link_nearest) {

  12. new_links.assign(

  13. new_links_set.begin(),

  14. new_links_set.begin() + std::min(new_links_set.size(), need_links)

  15. );

  16. } else {

  17. select_diverse_links(max_links(layer), new_links_set, new_links);

  18. }

  19. auto &outgoing_links = nodes.at(node).layers.at(layer).outgoing;

  20. //将node从原来的连接中清除

  21. for (const auto &link: outgoing_links) {

  22. nodes.at(link.first).layers.at(layer).incoming.erase(node);

  23. }

  24. //对node的所有邻居按key升序排列

  25. std::sort(new_links.begin(), new_links.end(), [](const auto &l, const auto &r) { return l.first < r.first; });

  26. //重新设置node的outgoing_links

  27. outgoing_links.assign_ordered_unique(new_links.begin(), new_links.end());

  28. //更新node邻居点的ingoing_links

  29. for (const auto &key: new_links) {

  30. nodes.at(key.first).layers.at(layer).incoming.insert(node);

  31. }

  32. }

下面的代码是删除一个结点node的操作:

  1. //把key对应的结点移除

  2. void remove(const key_t &key) {

  3. auto node_it = nodes.find(key);

  4. if (node_it == nodes.end()) {

  5. return;

  6. }

  7. const auto &layers = node_it->second.layers;

  8. for (size_t layer = 0; layer < layers.size(); ++layer) {

  9. for (const auto &link: layers[layer].outgoing) {

  10. nodes.at(link.first).layers.at(layer).incoming.erase(key);

  11. }

  12. for (const auto &link: layers[layer].incoming) {

  13. nodes.at(link).layers.at(layer).outgoing.erase(key);

  14. }

  15. }

  16. if (options.remove_method != index_options_t::remove_method_t::no_link) {

  17. //other code

  18. }

  19. auto level_it = levels.find(layers.size());

  20. if (level_it == levels.end()) {

  21. throw std::runtime_error("hnsw_index::remove: the node is not present in the levels index");

  22. }

  23. level_it->second.erase(key);

  24. // Shrink the hash table when it becomes too sparse

  25. // (to reduce memory usage and ensure linear complexity for iteration).

  26. if (4 * level_it->second.load_factor() < level_it->second.max_load_factor()) {

  27. level_it->second.rehash(size_t(2 * level_it->second.size() / level_it->second.max_load_factor()));

  28. }

  29. if (level_it->second.empty()) {

  30. levels.erase(level_it);

  31. }

  32. nodes.erase(node_it);

  33. if (4 * nodes.load_factor() < nodes.max_load_factor()) {

  34. nodes.rehash(size_t(2 * nodes.size() / nodes.max_load_factor()));

  35. }

  36. }

hnsw中删除一个结点后会出现一个问题,因为hnsw需要保持每个结点有固定数目的邻居点和它连接,如果删除了一个结点,将可能使得其他结点的连接数减少。为了使得结点删除后依然满足固定连接数的要求,需要对连接数减少的结点重新进行搜索,在重新搜索时只需要比较不在已有邻居点集中的结点即可。代码入下:

  1. if (options.remove_method != index_options_t::remove_method_t::no_link) {

  2. for (size_t layer = 0; layer < layers.size(); ++layer) {

  3. for (const auto &inverted_link: layers[layer].incoming) {

  4. auto &peer_links = nodes.at(inverted_link).layers.at(layer).outgoing;

  5. const key_t *new_link_ptr = nullptr;

  6. if (options.insert_method == index_options_t::insert_method_t::link_nearest) {

  7. new_link_ptr = select_nearest_link(inverted_link, peer_links, layers.at(layer).outgoing);

  8. } else if (options.insert_method == index_options_t::insert_method_t::link_diverse) {

  9. new_link_ptr = select_most_diverse_link(inverted_link, peer_links, layers.at(layer).outgoing);

  10. } else {

  11. assert(false);

  12. }

  13. if (new_link_ptr) {

  14. auto new_link = *new_link_ptr;

  15. auto &new_link_node = nodes.at(new_link);

  16. auto d = distance(nodes.at(inverted_link).vector, new_link_node.vector);

  17. peer_links.emplace(new_link, d);

  18. new_link_node.layers.at(layer).incoming.insert(inverted_link);

  19. try_add_link(new_link, layer, inverted_link, d);

  20. }

  21. }

  22. }

  23. }

原文链接:https://blog.csdn.net/CHIERYU/article/details/86647014

HNSW算法原理(二)之删除结点相关推荐

  1. HNSW算法原理(一)

    原文链接:https://blog.csdn.net/CHIERYU/article/details/81989920 HNSW算法可类比于skip lists数据结构,对于增和查操作,其与skip ...

  2. 【每日一算法】二叉搜索树结点最小距离

    微信改版,加星标不迷路! 每日一算法-二叉搜索树节点最小距离 作者:阿广 阅读目录 1 题目 2 解析 1 题目 给定一个二叉搜索树的根结点 root, 返回树中任意两节点的差的最小值. 示例: 输入 ...

  3. 二十七、二叉树--删除结点

    一.删除规则 如果删除的节点是叶子节点,则删除该节点 如果删除的节点是非叶子节点,则删除该子树. 注意到时候学习二叉排序树的时候删除非叶子结点就不是这样了 二.删除结点思路分析 三.代码实现 pack ...

  4. 图像处理:TDLMS算法原理介绍及MATLAB实现

    一.TDLMS介绍 1.1 算法原理 二维最小均方(two-dimensional least mean square, TDLMS)滤波算法由最小均方误差(least mean square err ...

  5. HNSW算法----Hierarchcal Navigable Small World graphs,第一贡献者:Y.Malkov(俄)

    原文地址:https://blog.csdn.net/u011233351/article/details/85116719 一.背景介绍 在浩渺的数据长河中做高效率相似性查找一直以来都是让人头疼的问 ...

  6. k近邻算法原理c语言,实验二 K-近邻算法及应用

    作业信息 一.[实验目的] 理解K-近邻算法原理,能实现算法K近邻算法: 掌握常见的距离度量方法: 掌握K近邻树实现算法: 针对特定应用场景及数据,能应用K近邻解决实际问题. 二.[实验内容] 实现曼 ...

  7. 数据结构专题二:2.6链表删除结点

    ///删除,删除指定位置的值,或者删除给定的值 //情形一:删除指定结点的后继结点 //情形二:删除第i个结点,假定头结点的i=0 //删除返回这个目标结点的地址,并不涉及到动态空间的回收 //在动态 ...

  8. 十三种基于直方图的图像全局二值化算法原理、实现、代码及效果(转)

    源:十三种基于直方图的图像全局二值化算法原理.实现.代码及效果.

  9. 十三种基于直方图的图像全局二值化算法原理、实现、代码及效果。

    图像二值化的目的是最大限度的将图象中感兴趣的部分保留下来,在很多情况下,也是进行图像分析.特征提取与模式识别之前的必要的图像预处理过程.这个看似简单的问题,在过去的四十年里受到国内外学者的广泛关注,产 ...

最新文章

  1. Linux下多文件链接执行及调试技术
  2. Selenium--调用js,对话框处理 (python)
  3. .NET Core中的认证管理解析
  4. java session失效之后跳转,session失效后如何实现页面不跳转到主页而是跳转到session失效时的页面...
  5. Linux学习总结(54)——Red Hat Enterprise Linux与CentOS的区别
  6. Spark Streaming实例
  7. html5 下拉树,HTML5拖拽API实现vue树形拖拽组件
  8. xp获取计算机管理员权限,xp管理员权限怎么获取?管理员权限不足的解决方法...
  9. 深思新推出高性价比智能卡加密锁--魔锐1
  10. 香港云服务器选阿里云好还是腾讯云好?
  11. JS中alert的三种使用方式
  12. 带权图 Weighted Graph
  13. Python用python-docx抓取公众号文章写入word
  14. Small Talk Matters【闲谈很重要】
  15. Git版本控制管理——基本Git概念
  16. 大型分布式数据库集群的研究
  17. 金山软件刘鑫:有限使用UML
  18. 技术大佬:我去,你竟然还在用 try–catch-finally
  19. 电商商品列表应以SPU还是SKU展示商品?
  20. 程序员怎样兼职接私活?必看经验之谈

热门文章

  1. 网卡驱动和队列层中的数据包接收
  2. simple css 汉化,Simple CSS(CSS文档生成器)
  3. 微博登录界面的PHP代码,关于接入微博登录的代码实现
  4. mongo java client_mongodb java客户端的使用,即MongoClient
  5. 可延迟函数、内核微线程以及工作队列
  6. Ubuntu下基于 Cilium CNI 的 Kubernetus集群环境搭建
  7. Contiki OS 开发快速入门
  8. 广东海洋大学微型计算机考试,广东海洋大学2007-2008微型计算机原理及应用
  9. linux获取目标主机shell,expect案例-批量获取主机并分发密钥
  10. java 范式 问号_巴科斯范式和扩展巴科斯范式