Scala实现:KD-Tree(k-dimensional tree)

Scala实现:KD-Tree(k-dimensional tree)

kd-tree是一种分割k维数据空间的数据结构。主要应用于多维空间数据的搜索,经常使用在SIFT、KNN等多维数据搜索的场景中,以KNN(K近邻)为例,使用线性搜索的方式效率低下,k-d树本质是对多维空间的划分,其每个节点都为k维点的二叉树kd-tree,因此可以大大提高搜索效率。

KD-Tree的构建步骤:

kd树实现步骤.jpg

上述文字引自李航博士的《统计学习方法》

以{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}数据集为例构建KD-Tree。

KD-Tree空间划分示意图如下:

划分结果.png
kdtree树结构.jpg

关于三维数据的空间划分示意图如下所示

三维kdtree空间划分

更多维度的数据划分只能靠脑补了······

KD-Tree最邻近搜索:

  1. 从根节点开始,递归的往下访问kd树,比较目标点与切分点在当前切分维度的大小,小于则移动到左子结点,大于则移动到右子结点,知道子结点为叶结点为止。

  2. 一旦移动到叶结点,将该结点当作"当前最邻近点"。

  3. 递归回退,对每个经过的叶结点递归地执行下列操作:

    1. 如果当前所在点比"当前最邻近点"更靠近输入点,则将其变为当前最邻近点。
    1. 当前最近点一定存在于该节点一个子结点对应的区域,检查另一子结点对应的区域是否与目标点为球心,以目标点与“当前最邻近点”之间的距离为半径的超球体相交:
    • 1.如果相交,可能在另一结点对应之区域内存在距离目标点更近的点,移动到另一子结点,接着递归地进行最近邻搜索;
    • 2.如果不相交,向上回退。
  1. 当回退到根节点时,搜索结束。最后的“当前最邻近点"即为x的最近邻点。

Scala代码实现

定义树节点
/**
  *
  * @param value 节点数据
  * @param dim   当前切分维度
  * @param left  左子结点
  * @param right 右子结点
  */
case class TreeNode(value: Seq[Double],
                    dim: Int,
                    var left: TreeNode,
                    var right: TreeNode) {

    var parent: TreeNode = _ //父结点
    var brotherNode: TreeNode = _ //兄弟结点

    if (left != null) {
      left.parent = this
      left.brotherNode = right
    }

    if (right != null) {
      right.parent = this
      right.brotherNode = left
    }

}

创建KD-Tree
/**
    *
    * @param value 数据序列
    * @param dim   当前划分的维度
    * @param shape 数据维数
    * @return 
    */
  def creatKdTree(value: Seq[Seq[Double]], dim: Int, shape: Int): TreeNode = {

    // 数据按照当前划分的维度排序
    val sorted = value.sortBy(_ (dim))
    //中间位置的索引
    val midIndex: Int = value.length / 2

    sorted match {
      // 当节点为空时,返回null
      case Nil => null
      //节点不为空时,递归调用 
      case _ =>
        val left = sorted.slice(0, midIndex)
        val right = sorted.slice(midIndex + 1, value.length)

        val leftNode = creatKdTree(left, (dim + 1) % shape, shape) //左子节点递归创建树
        val rightNode = creatKdTree(right, (dim + 1) % shape, shape) //右子节点递归创建树

        TreeNode(sorted(midIndex), dim, leftNode, rightNode)

    }
  }

最近邻查找

// 欧式距离算法
 def euclidean(p1: Seq[Double], p2: Seq[Double]) = {
    require(p1.size == p2.size)
    val d = p1
      .zip(p2)
      .map(tp => math.pow(tp._1 - tp._2, 2))
      .sum
    math.sqrt(d)
  }

/**
    *
    * @param treeNode kdtree
    * @param data     查询点
    *                 最近邻搜索
    */
  def nearestSearch(treeNode: TreeNode, data: Seq[Double]): TreeNode = {

    var nearestNode: TreeNode = null //当前最近节点
    var minDist: Double = Double.MaxValue //当前最小距离

    def finder(treeNode: TreeNode): TreeNode = {

      treeNode match {
        case null => nearestNode
        case _ =>
          val dimr = data(treeNode.dim) - treeNode.value(treeNode.dim)
          if (dimr > 0) finder(treeNode.right) else finder(treeNode.left)

          val distc = euclidean(treeNode.value, data)
          if (distc <= minDist) {
            minDist = distc
            nearestNode = treeNode
          }

          // 目标点与当前节点相交
          if (math.abs(dimr) < minDist)
            if (dimr > 0) finder(treeNode.left) else finder(treeNode.right)

          nearestNode
      }
    }

    finder(treeNode)

  }

结果查看
   val nodes: Seq[Seq[Double]] =
      Seq(Seq(2, 3), Seq(5, 4), Seq(9, 6), Seq(4, 7), Seq(8, 1), Seq(7, 2))

    val treeNode: TreeNode = KdTree.creatKdTree(nodes, 0, 2)

    println(treeNode)

    println(KdTree.nearestSearch(treeNode, Seq(2.1, 4.5)).value)

    println("==============")
    nodes.map(x => {
        val d = KdTree.euclidean(x, Seq(2.1, 4.5))
        (d, x)
      })
      .sortBy(_._1)
      .foreach(println)
TreeNode(List(7.0, 2.0),0,TreeNode(List(5.0, 4.0),1,TreeNode(List(2.0, 3.0),0,null,null),TreeNode(List(4.0, 7.0),0,null,null)),TreeNode(List(9.0, 6.0),1,TreeNode(List(8.0, 1.0),0,null,null),null))
List(2.0, 3.0)
==============
(1.503329637837291,List(2.0, 3.0))
(2.9427877939124323,List(5.0, 4.0))
(3.1400636936215163,List(4.0, 7.0))
(5.500909015790027,List(7.0, 2.0))
(6.860029154456998,List(8.0, 1.0))
(7.061161377563892,List(9.0, 6.0))
TODO K近邻查找(KNN)
/**
    * 从root节点开始,DFS搜索直到叶子节点,同时在stack中顺序存储已经访问的节点。
    * 如果搜索到叶子节点,当前的叶子节点被设为最近邻节点。
    * 然后通过stack回溯:
    * 如果当前点的距离比最近邻点距离近,更新最近邻节点.
    * 然后检查以最近距离为半径的圆是否和父节点的超平面相交.
    * 如果相交,则必须到父节点的另外一侧,用同样的DFS搜索法,开始检查最近邻节点。
    * 如果不相交,则继续往上回溯,而父节点的另一侧子节点都被淘汰,不再考虑的范围中.
    * 当搜索回到root节点时,搜索完成,得到最近邻节点。
    *
    * @param treeNode
    * @param data
    * @param k
    * @return
    */
  def knn(treeNode: TreeNode, data: Seq[Double], k: Int) = {

    var resArr = new Array[(Double, TreeNode)](k)
      .map(_ => (Double.MaxValue, null))
      .asInstanceOf[Array[(Double, TreeNode)]]

    def finder(treeNode: TreeNode): TreeNode = {

      if (treeNode != null) {
        val dimr = data(treeNode.dim) - treeNode.value(treeNode.dim)
        if (dimr > 0) finder(treeNode.right) else finder(treeNode.left)

        val distc: Double = distanceUtils.euclidean(treeNode.value, data)

        if (distc < resArr.last._1  ) {
          resArr.update(k - 1, (distc, treeNode))
          resArr = resArr.sortBy(_._1)
        }

        if (math.abs(dimr) < resArr.last._1)
          if (dimr > 0) finder(treeNode.left) else finder(treeNode.right)

      }
      resArr.last._2
    }

    finder(treeNode)
    resArr

  }

KNN结果查看
 KdTree
      .knn(treeNode, Seq(2.1, 4.5), 3)
      .map(x => (x._1, x._2.value))
      .foreach(println)
(1.503329637837291,List(2.0, 3.0))
(2.9427877939124323,List(5.0, 4.0))
(3.1400636936215163,List(4.0, 7.0))
参考资料

https://baike.baidu.com/item/kd-tree/2302515?fr=aladdin#7_1
《统计学习方法》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335