1. 程式人生 > >深入理解空間搜索算法 ——數百萬數據中的瞬時搜索

深入理解空間搜索算法 ——數百萬數據中的瞬時搜索

高效 建立 ext 交流 span 地球 範圍 感謝 第一次

轉自 幹貨|深入理解空間搜索算法 ——數百萬數據中的瞬時搜索

2017-05-01 10:50

全球人工智能:專註為AI開發者提供全球最新AI技術動態和社群交流。用戶來源包括:北大、清華、中科院、復旦、麻省理工、卡內基梅隆、斯坦福、哈佛、牛津、劍橋等世界名校的AI技術碩士、博士和教授;以及谷歌、騰訊、百度、臉譜、微軟、華為、阿裏、海康威視、滴滴、英偉達等全球名企的AI開發者和AI科學家。

文章來源:medium 編譯:孫菁

技術分享

上圖為全球138,000個熱門地點的R-tree的可視化圖示

我這個人沈迷於軟件性能的提升,我在Mapbox(https://www.mapbox.com/)的職責之一就是找到能使我們的映射平臺更加快速的方法。當面對大規模的空間數據時,一個最有效也是最重要的方法就是空間索引(https://en.wikipedia.org/wiki/Spatial_database#Spatial_index)。

空間索引是一系列可以通過排列幾何數據來進行高效索引的算法。例如,查詢“本區域所有的建築”、“距此點最近的1000個加油站”等問題,查詢結果往往能夠在幾毫秒內返回,即使所要查詢的目標有幾百萬個。

空間索引是數據庫如PostGIS的基礎,同時也是我們平臺的核心。在很多其他任務尤其是性能至關重要的任務中,空間索引也非常重要。特別的,在處理遙測數據時,我們需要對數百萬個GPS樣本與道路網進行匹配,以產生導航所用的實時交通數據。在客戶端,我們則需要實時在地圖中展示地標,以及在鼠標滯留時查找鼠標所指的目標。

在過去的四年裏,我建立了一些快速的用於空間搜索的Java 庫,包括:

  • rbush(https://github.com/mourner/rbush),

  • rbush-knn(https://github.com/mourner/rbush-knn/),

  • kdbush(https://github.com/mourner/kdbush),

  • geokdbush(https://github.com/mourner/geokdbush)。

本文中,我會努力將這幾個庫背後的原理講解清楚。

空間搜索問題

空間數據有兩種基本查詢類型:最相鄰查詢和範圍查詢。這兩種查詢都是很多幾何問題和GIS問題的基本模塊。

K相鄰

技術分享

如果給出幾千個數據點,如城市的坐標,我們如何檢索出與某特定查詢點最相鄰的點呢?

  • 我們很自然想到的方法可能是這樣:

  • 計算每個點與查詢點之間的距離。

  • 按距離大小對所有的點進行排序。

  • 返回前k個點。

當有幾百個數據點時我們可以用這種方法,但是當我們面臨數百萬的數據點時,這種方法就顯得太慢且無法應用到實際情景。

範圍查詢和半徑查詢

如何在一個給定的範圍內檢索出所有的數據點呢?這裏又分為兩種情況,一種是所給範圍是矩形的情況(範圍查詢),另一種是所給範圍是圓的情況(半徑查詢)。

一種樸素的方法是遍歷所有數據點並判斷每一個點是否在給定範圍內,但當數據集很大時,這種方法也因低效而失去了實用性。

空間樹是如何工作的

大規模地解決這兩種問題時就需要將數據點轉換到空間索引中。由於數據轉變的頻率會遠遠少於查詢的頻率,因此將數據轉變到空間索引的花銷對於之後的快速搜索是非常值得的。

幾乎所有的空間數據結構都具有相同的原理,以實現有效的搜索:分支和綁定(https://en.wikipedia.org/wiki/Branch_and_bound)。數據被排列在一個樹狀結構中,因此當在某一節點的某一分支不符合查詢條件時,該分支之下的所有的節點都可被略過。

R-tree

現在讓我們來看一個例子,下圖的示例將所有的輸入點分在9個矩形框中,並且每個矩形框中的點的數目相同:

技術分享

現在,我們將每個矩形框再分為9個更小的矩形框:

技術分享

我們會將這個過程重復幾次,找到最後每個矩形框包含的點不超過9個:

技術分享

最終我們得到了R-tree(https://en.wikipedia.org/wiki/R-tree),這可以說是最常見的空間數據結構,被廣泛用於現代空間數據集和遊戲引擎,我的rbush JS庫中也實現了R-tree。

除了點之外,R-tree也可以包含矩形,用於表示任何幾何對象。同時,R-tree也可擴展到3維或高維的情況。為了易於說明,本文中以二維的情況舉例講解。

K-d tree

K-d tree (https://en.wikipedia.org/wiki/K-d_tree)是另外一種流行的空間數據結構。我的kdbush JS庫(https://github.com/mourner/kdbush),用於靜態的二維的索引,就是基於K-d tree實現的。K-d tree與R-tree類似,但與在每一層次上將數據點均分到幾個矩形框中不同的是,K-d tree會將數據點從中間分為兩部分——或左或右,或上或下,每個層次上都會在x坐標和y坐標進行劃分,如下圖所示:

技術分享

與R-tree相比,K-d tree只能包含數據點而不能包含矩形,並且不能添加或刪除點。但是K-d tree在高效的同時更易實現。R-tree和K-d tree 都采用了相同的原則,即將數據組織為樹的結構。因此,下文所討論的搜索算法與樹的搜索算法相同。

在樹中的範圍查詢

下圖是一個典型的空間樹:

技術分享

每一個節點的孩子數都固定,本文中R-tree的例子中為9。那麽樹的深度是多少呢?對於有1,000,000 個節點的樹,高度為(log(1000000) / log(9)) = 7。

當我們在樹中執行範圍搜索算法時,我們可以從樹根開始向下,忽略所有不滿足查詢框的矩形框。對於一個小的查詢框,這意味著在樹的每一層上只需要搜索幾個矩形框即可。因此,得到最終查詢結果的時間不會多於60次矩形框的比較(7 * 9 = 63),而不是1,000,000次比較,這使其比原始的循環搜索快了16000倍。

使用R-tree 的範圍搜索所用的平均時間為O(K log(N))(這裏K是結果的數目),而線性搜索所用時間為O(N)。因此,R-tree的搜索是非常高效的算法。

這裏我們選用9作為每一節點的孩子數,9是一個很不錯的默認值,但是理論上,越高的數值意味著更快的索引和更慢的查詢,反之亦然。

K相鄰查詢

相鄰查詢相較於範圍查詢稍難一些。對於一個特定的查詢點,我們怎麽知道哪棵子樹上的節點與該節點最相鄰呢?我們可以做半徑查詢,但我們不知道如何選擇半徑的大小——最相鄰的點可能在樹中離查詢點很遠的位置。如果靠增加半徑來找到一些點又會極大地降低搜索效率。

為了在空間樹中找到一些最相鄰點,我們會利用另一個簡潔的數據結構——優先隊列(https://en.wikipedia.org/wiki/Priority_queue)。優先隊列會維護一個有序列表,該列表可以將“最小的”元素以很快的速度提取出來。為了更好地理解知識,我通常喜歡從頭開始寫一個數據結構,因此我寫的優先隊列的JS庫 tinyqueue(https://github.com/mourner/tinyqueue)可能是有史以來最好的優先隊列哦。

讓我們回顧一下R-tree的例子:

技術分享

我們可以很直觀地想到,與查詢點更臨近的矩形框可能有我們想要搜索的點。為了有效地利用這一點,我們將按從最近到最遠的順序將最大的矩形框排在隊列中,從頂層開始進行搜索:

技術分享

之後,我們“打開”相鄰的矩形框,從隊列中移除,並將它的孩子(較小的矩形框)放到隊列中與其相鄰的位置上。

技術分享

重復上述步驟,當從隊列中移除的相鄰項是真正的點而不是矩形框時,這就是我們要查找的點了,隊列頂部第二個點就是第二個最相鄰的點,以此類推。

技術分享

由於我們還未“打開”的矩形框中包含的點比當前矩形框中的點的距離更遠,因此我們從隊列中取出的任何點都會比剩余的矩形框中的點的距離更近。

技術分享

如果我們的空間樹很平衡的話,即所有節點的分支數基本相同,那麽我們只需處理幾個矩形框即可,而忽略其余的矩形框。這種策略使得該算法在搜索過程中非常快速。

在rbush庫中,該算法在rbush-knn模塊中實現。對於地理信息中的點,我最近發布了另外一個kNN庫——geokdbush(https://github.com/mourner/geokdbush)。Geokdbush可以很好地處理地球的曲率和時間線的包裝。我真應該專門以geokdbush寫一篇文章,因為它是我第一次將微積分應用到實際工作中的項目。

自定義的kNN距離衡量

這種“打開”矩形框的方法是非常靈活的,除了點對點距離之外還適用於其他的距離類型。該算法依賴於查詢一個已定義的與矩形框內所有對象之間的距離的下限。如果我們自定義這個下限標準,我們依然可以使用相同的算法。這就意味著,我們可以改變算法使其搜索最接近一條線段的K個點(而不是與一個點最相近的點):

技術分享

在算法中我們唯一需要改變的就是將點與點之間的距離和點與矩形框之間的距離計算換成線段與點之間的距離和線段與矩形框之間的距離計算。

當我建立Concaveman(https://github.com/mapbox/concaveman)庫時——一個JS中快速的2D凹面庫,這樣做就顯得很方便。 它需要很多點,並生成一個如下所示的圖:

技術分享

該算法以凸包(https://github.com/mikolalysenko/monotone-convex-hull-2d)開始,然後通過將它們連接到最接近的點的之一來向內彎曲它的每一段:

技術分享

深入閱讀:A New Concave Hull Algorithm and Concaveness Measure for n-dimensional Datasets, 2012(http://www.iis.sinica.edu.tw/page/jise/2012/201205_10.pdf)

下面是一段引自論文的話:

在我們提出的凹面算法中,從邊界邊緣開始找到最近的內部點是一個耗時的過程,這些點是進一步挖掘目標點的候選點。開發更有效的方法是我們未來的研究課題。

我將數據點進行索引並執行“最近點到一個段”的查詢,這使得該算法變得更快。

未來工作

在未來在這一些列的文章中,我會將kNN算法拓展到地理信息對象,並詳細地講解三個打包算法,即如何將數據點最好地排列到矩形框中。

最後感謝您耐心的閱讀,如果您有任何意見或問題歡迎留言。歡迎試用我們的SDKs(https://www.mapbox.com/products/),如果您能夠解決硬工程的挑戰和地圖的問題,歡迎查看我們的job openings(https://www.mapbox.com/jobs/)。

深入理解空間搜索算法 ——數百萬數據中的瞬時搜索