教程 | 如何在Python中快速進行語料庫搜索:近似最近鄰算法

2021-03-04 機器之心

最近,我一直在研究在 GloVe 詞嵌入中做加減法。例如,我們可以把「king」的詞嵌入向量減去「man」的詞嵌入向量,隨後加入「woman」的詞嵌入得到一個結果向量。隨後,如果我們有這些詞嵌入對應的語料庫,那麼我們可以通過搜索找到最相似的嵌入並檢索相應的詞。如果我們做了這樣的查詢,我們會得到:

King + (Woman - Man) = Queen

我們有很多方法來搜索語料庫中詞嵌入對作為最近鄰查詢方式。絕對可以確保找到最優向量的方式是遍歷你的語料庫,比較每個對與查詢需求的相似程度——這當然是耗費時間且不推薦的。一個更好的技術是使用向量化餘弦距離方式,如下所示:

vectors = np.array(embeddingmodel.embeddings)

ranks = np.dot(query,vectors.T)/np.sqrt(np.sum(vectors**2,1))

mostSimilar = []

[mostSimilar.append(idx) for idx in ranks.argsort()[::-1]]

想要了解餘弦距離,可以看看這篇文章:http://masongallo.github.io/machine/learning,/python/2016/07/29/cosine-similarity.html

矢量化的餘弦距離比迭代法快得多,但速度可能太慢。是近似最近鄰搜索算法該出現時候了:它可以快速返回近似結果。很多時候你並不需要準確的最佳結果,例如:「Queen」這個單詞的同義詞是什麼?在這種情況下,你只需要快速得到足夠好的結果,你需要使用近似最近鄰搜索算法。

在本文中,我們將會介紹一個簡單的 Python 腳本來快速找到近似最近鄰。我們會使用的 Python 庫是 Annoy 和 Imdb。對於我的語料庫,我會使用詞嵌入對,但該說明實際上適用於任何類型的嵌入:如音樂推薦引擎需要用到的歌曲嵌入,甚至以圖搜圖中的圖片嵌入。

製作一個索引

讓我們創建一個名為:「make_annoy_index」的 Python 腳本。首先我們需要加入用得到的依賴項:

'''

Usage: python2 make_annoy_index.py \

   --embeddings=<embedding path> \

   --num_trees=<int> \

   --verbose

Generate an Annoy index and lmdb map given an embedding file

Embedding file can be

 1. A .bin file that is compatible with word2vec binary formats.

    There are pre-trained vectors to download at https://code.google.com/p/word2vec/

 2. A .gz file with the GloVe format (item then a list of floats in plaintext)

 3. A plain text file with the same format as above

'''

import annoy

import lmdb

import os

import sys

import argparse

from vector_utils import get_vectors

最後一行裡非常重要的是「vector_utils」。稍後我們會寫「vector_utils」,所以不必擔心。

接下來,讓我們豐富這個腳本:加入「creat_index」函數。這裡我們將生成 lmdb 圖和 Annoy 索引。

1. 首先需要找到嵌入的長度,它會被用來做實例化 Annoy 的索引。

2. 接下來實例化一個 Imdb 圖,使用:「env = lmdb.open(fn_lmdb, map_size=int(1e9))」。

3. 確保我們在當前路徑中沒有 Annoy 索引或 lmdb 圖。

4. 將嵌入文件中的每一個 key 和向量添加至 lmdb 圖和 Annoy 索引。

5. 構建和保存 Annoy 索引。

'''

function create_index(fn, num_trees=30, verbose=False)

-

Creates an Annoy index and lmdb map given an embedding file fn

Input:

   fn              - filename of the embedding file

   num_trees       - number of trees to build Annoy index with

   verbose         - log status

Return:

   Void

'''

def create_index(fn, num_trees=30, verbose=False):

   fn_annoy = fn + '.annoy'

   fn_lmdb = fn + '.lmdb' # stores word <-> id mapping

   word, vec = get_vectors(fn).next()

   size = len(vec)

   if verbose:

       print("Vector size: {}".format(size))

   env = lmdb.open(fn_lmdb, map_size=int(1e9))

   if not os.path.exists(fn_annoy) or not os.path.exists(fn_lmdb):

       i = 0

       a = annoy.AnnoyIndex(size)

       with env.begin(write=True) as txn:

           for word, vec in get_vectors(fn):

               a.add_item(i, vec)

               id = 'i%d' % i

               word = 'w' + word

               txn.put(id, word)

               txn.put(word, id)

               i += 1

               if verbose:

                   if i % 1000 == 0:

                       print(i, '...')

       if verbose:

           print("Starting to build")

       a.build(num_trees)

       if verbose:

           print("Finished building")

       a.save(fn_annoy)

       if verbose:

           print("Annoy index saved to: {}".format(fn_annoy))

           print("lmdb map saved to: {}".format(fn_lmdb))

   else:

       print("Annoy index and lmdb map already in path")

我已經推斷出 argparse,因此,我們可以利用命令行啟用我們的腳本:

'''

private function _create_args()

-

Creates an argeparse object for CLI for create_index() function

Input:

   Void

Return:

   args object with required arguments for threshold_image() function

'''

def _create_args():

   parser = argparse.ArgumentParser()

   parser.add_argument("--embeddings", help="filename of the embeddings", type=str)

   parser.add_argument("--num_trees", help="number of trees to build index with", type=int)

   parser.add_argument("--verbose", help="print logging", action="store_true")

   args = parser.parse_args()

   return args

添加主函數以啟用腳本,得到 make_annoy_index.py:

if __name__ == '__main__':

   args = _create_args()

   create_index(args.embeddings, num_trees=args.num_trees, verbose=args.verbose)

現在我們可以僅利用命令行啟用新腳本,以生成 Annoy 索引和對應的 lmdb 圖!

python2 make_annoy_index.py \

   --embeddings=<embedding path> \

   --num_trees=<int> \

   --verbose

寫向 量Utils

我們在 make_annoy_index.py 中推導出 Python 腳本 vector_utils。現在要寫該腳本,Vector_utils 用於幫助讀取.txt, .bin 和 .pkl 文件中的向量。

寫該腳本與我們現在在做的不那麼相關,因此我已經推導出整個腳本,如下:

'''

Vector Utils

Utils to read in vectors from txt, .bin, or .pkl.

Taken from Erik Bernhardsson

Source: https://github.com/erikbern/ann-presentation/blob/master/util.py

'''

import gzip

import struct

import cPickle

def _get_vectors(fn):

   if fn.endswith('.gz'):

       f = gzip.open(fn)

       fn = fn[:-3]

   else:

       f = open(fn)

   if fn.endswith('.bin'): # word2vec format

       words, size = (int(x) for x in f.readline().strip().split())

       t = 'f' * size

       while True:

           pos = f.tell()

           buf = f.read(1024)

           if buf == '' or buf == '\n': return

           i = buf.index(' ')

           word = buf[:i]

           f.seek(pos + i + 1)

           vec = struct.unpack(t, f.read(4 * size))

           yield word.lower(), vec

   elif fn.endswith('.txt'): # Assume simple text format

       for line in f:

           items = line.strip().split()

           yield items[0], [float(x) for x in items[1:]]

   elif fn.endswith('.pkl'): # Assume pickle (MNIST)

       i = 0

       for pics, labels in cPickle.load(f):

           for pic in pics:

               yield i, pic

               i += 1

def get_vectors(fn, n=float('inf')):

   i = 0

   for line in _get_vectors(fn):

       yield line

       i += 1

       if i >= n:

           break


測試 Annoy 索引和 lmdb 圖

我們已經生成了 Annoy 索引和 lmdb 圖,現在我們來寫一個腳本使用它們進行推斷。

將我們的文件命名為 annoy_inference.py,得到下列依賴項:

'''

Usage: python2 annoy_inference.py \

   --token='hello' \

   --num_results=<int> \

   --verbose

Query an Annoy index to find approximate nearest neighbors

'''

import annoy

import lmdb

import argparse

現在我們需要在 Annoy 索引和 lmdb 圖中加載依賴項,我們將進行全局加載,以方便訪問。注意,這裡設置的 VEC_LENGTH 為 50。確保你的 VEC_LENGTH 與嵌入長度匹配,否則 Annoy 會不開心的哦~

VEC_LENGTH = 50

FN_ANNOY = 'glove.6B.50d.txt.annoy'

FN_LMDB = 'glove.6B.50d.txt.lmdb'

a = annoy.AnnoyIndex(VEC_LENGTH)

a.load(FN_ANNOY)

env = lmdb.open(FN_LMDB, map_size=int(1e9))

有趣的部分在於「calculate」函數。

1. 從 lmdb 圖中獲取查詢索引;

2. 用 get_item_vector(id) 獲取 Annoy 對應的向量;

3. 用 a.get_nns_by_vector(v, num_results) 獲取 Annoy 的最近鄰。

'''

private function calculate(query, num_results)

-

Queries a given Annoy index and lmdb map for num_results nearest neighbors

Input:

   query           - query to be searched

   num_results     - the number of results

Return:

   ret_keys        - list of num_results nearest neighbors keys

'''

def calculate(query, num_results, verbose=False):

   ret_keys = []

   with env.begin() as txn:

       id = int(txn.get('w' + query)[1:])

       if verbose:

           print("Query: {}, with id: {}".format(query, id))

       v = a.get_item_vector(id)

       for id in a.get_nns_by_vector(v, num_results):

           key = txn.get('i%d' % id)[1:]

           ret_keys.append(key)

   if verbose:

       print("Found: {} results".format(len(ret_keys)))

   return ret_keys

再次,這裡使用 argparse 來使讀取命令行參數更加簡單。

'''

private function _create_args()

-

Creates an argeparse object for CLI for calculate() function

Input:

   Void

Return:

   args object with required arguments for threshold_image() function

'''

def _create_args():

   parser = argparse.ArgumentParser()

   parser.add_argument("--token", help="query word", type=str)

   parser.add_argument("--num_results", help="number of results to return", type=int)

   parser.add_argument("--verbose", help="print logging", action="store_true")

   args = parser.parse_args()

   return args

主函數從命令行中啟用 annoy_inference.py。

if __name__ == '__main__':

   args = _create_args()

   print(calculate(args.token, args.num_results, args.verbose))

現在我們可以使用 Annoy 索引和 lmdb 圖,獲取查詢的最近鄰!

python2 annoy_inference.py --token="test" --num_results=30

['test', 'tests', 'determine', 'for', 'crucial', 'only', 'preparation', 'needed', 'positive', 'guided', 'time', 'performance', 'one', 'fitness', 'replacement', 'stages', 'made', 'both', 'accuracy', 'deliver', 'put', 'standardized', 'best', 'discovery', '.', 'a', 'diagnostic', 'delayed', 'while', 'side']

代碼

本教程所有代碼的 GitHub 地址:https://github.com/kyang6/annoy_tutorial

原文地址:https://medium.com/@kevin_yang/simple-approximate-nearest-neighbors-in-python-with-annoy-and-lmdb-e8a701baf905

本文為機器之心編譯,轉載請聯繫本公眾號獲得授權

✄---

加入機器之心(全職記者/實習生):hr@jiqizhixin.com

投稿或尋求報導:editor@jiqizhixin.com

廣告&商務合作:bd@jiqizhixin.com

相關焦點

  • 如何在Python中進行二進位搜索?搜索算法,中級python技術點
    它指的是將元素的集合為兩半,並在算法的每一步扔掉它們中的一個。這樣可以大大減少查找元素所需的比較次數。但是有一個陷阱,必須首先對集合中的元素進行排序。其背後的想法類似於在書中查找頁面的步驟。首先,您通常將書打開到一個完全隨機的頁面,或者至少打開一個與您認為所需頁面可能接近的頁面。
  • 算法基礎:五大排序算法Python實戰教程
    -43ea9aa02889算法基礎:五大排序算法Python實戰教程排序算法的複雜度排序是每個軟體工程師和開發人員都需要掌握的技能。不同的排序算法很好地展示了算法設計上如何強烈的影響程序的複雜度、運行速度和效率。讓我們看一下前6種排序算法,看看如何在Python中實現它們!
  • 常見面試算法:k-近鄰算法原理與python案例實現
    ,我們這裡只討論分類問題中的 k-近鄰算法。k 近鄰算法的輸入為實例的特徵向量,對應於特徵空間的點;輸出為實例的類別,可以取多類。k 近鄰算法假設給定一個訓練數據集,其中的實例類別已定。分類時,對新的實例,根據其 k 個最近鄰的訓練實例的類別,通過多數表決等方式進行預測。因此,k近鄰算法不具有顯式的學習過程。k 近鄰算法實際上利用訓練數據集對特徵向量空間進行劃分,並作為其分類的「模型」。
  • 實例最簡,帶你輕鬆進階機器學習K最近鄰算法
    前言K最近鄰(k-NearestNeighbor,K-NN)算法是一個有監督的機器學習算法,也被稱為K-NN算法,由Cover和Hart於1968年提出。可以用於解決分類問題和回歸問題。此外,作為一個理論上比較成熟的機器學習算法,關於K近鄰算法的介紹有很多,比如算法執行的步驟、應用領域等。
  • 如何選擇最佳的最近鄰算法
    為了測試更多的算法,我們整理了幾種ANN算法,例如作為數據科學家,我我們這裡將制定一個數據驅動型決策來決定那種算法適合我們的數據。在此數據集上,scann算法在任何給定的Recall中具有最高的每秒查詢數,因此在該數據集上具有最佳的算法。
  • 什麼是算法?快速學會使用python編寫算法
    比如,我們要求計算機幫我們將輸入的100個整數從小到大進行排序,那麼排序的具體方法,就是算法。舉個例子,比如我們現在有這麼一列數據 [ 5,7,8,3,1],現在需要程序幫我們進行從小到大進行排序。應該怎麼辦呢?
  • 貪心算法與近似算法
    在獲得精確解需要的時間太長時,可使用近似算法。判斷近似算法優劣的標準如下:速度有多快;得到的近似解與最優解的接近程度。貪婪算法是不錯的選擇,它們不僅簡單,而且通常運行速度很快。在這個例子中,貪婪算法的運行時間為O(n2),其中n為廣播臺數量。下面來看看解決這個問題的代碼。
  • 極簡python教程:快速入門好方法
    其實很久之前,就有身邊的同事或者網友讓我分享一些關於python程式語言的快速教程,他們的痛點同大多數自學程式語言的人一樣,遇到了這些問題:網絡上的信息太多,良莠不全,不知道如何分辨;初學時「冗餘」知識太多,不知道該學些什麼
  • 從零開始:用Python實現KNN算法
    KNN算法是如何工作的?KNN算法屬於基於實例的、競爭學習與懶惰學習算法。基於實例的算法是算法模型使用數據實例(或者說數據行),並依據這些實例做出預測的算法。KNN算法是基於實例算法中的一個極端例子,因為所有的訓練數據都成了算法模型的一部分。
  • Python中的Timsort算法,Timsort算法的優缺點,中級python技術點
    這允許Timsort算法對數組的一部分進行排序。修改函數而不是創建一個新的函數意味著它可以被插入排序和Timsort重用。現在看看Timsort的實現:儘管實現比以前的算法要複雜一些,但是我們可以通過以下方式快速總結一下:第6行和第7行創建或運行數組的小片,並使用插入排序對它們進行排序。您以前了解到插入排序在小列表中是快速的,Timsort利用了這一點。
  • 快速掌握 Spacy 在 Python 中進行自然語言處理
    業界的數據科學團隊時常處理大量文本數據,這也是機器學習中使用的四大數據類別之一,通常是人為生成的文本,但也不全是這樣。想想看:商業世界的「作業系統」是如何運行的? 通常,有合同(銷售合同、工作協議、合作關係),發票,保險單,規章制度和其他法律條文等等。所有這些都被表示為文本。
  • python的中文文本挖掘庫snownlp進行購物評論文本情感分析實例
    現在研一,機器學習算法學完以後,又想起來要繼續學習文本挖掘了。所以前半個月開始了用Python進行文本挖掘的學習,很多人都推薦我從《python自然語言處理》這本書入門,學習了半個月以後,可能本科畢業設計的時候有些基礎了,再看這個感覺沒太多進步,並且這裡通篇將nltk庫進行英文文本挖掘的,英文文本挖掘跟中文是有很大差別的,或者說學完英文文本挖掘,再做中文的,也是完全懵逼的。
  • Python中的快速排序算法,快速排序的優缺點,中級python技術點
    Python中的快速排序算法就像合併排序一樣,快速排序算法採用分治法的原理將輸入數組分為兩個列表,第一個包含小項目,第二個包含大項目。然後,該算法將對兩個列表進行遞歸排序,直到對結果列表進行完全排序為止。劃分輸入列表稱為對列表進行分區。
  • Python中排序算法的重要性,希爾排序 ShellSort,中級python技術
    Python中排序算法的重要性排序是計算機科學中最深入研究的算法之一。有許多不同的排序實現和應用程式,您可以使用它們來提高代碼的效率。你可以使用排序來解決各種各樣的問題:搜索:如果列表是排序的,那麼在列表中搜索條目的速度會快得多。選擇:使用已排序的數據,從列表中根據項與其他項的關係選擇項更容易。
  • Python中的插入排序算法,插入排序的優缺點,中級python技術點
    屆時,您需要將卡插入正確的位置,然後重新開始製作新卡,重複進行直到您手上的所有卡都被排序為止。在Python中實現插入排序插入排序算法的工作原理與紙牌示例完全相同。這是Python中的實現:與冒泡排序不同,此插入排序的實現通過將較小的項目向左推來構造排序列表。
  • Python中的冒泡排序算法,冒泡排序的優缺點,中級python技術點
    Python中的冒泡排序算法冒泡排序是最直接的排序算法之一。它的名字來自於算法的工作方式:每經過一個新遍歷,列表中最大的元素就會向正確的位置「冒泡」。冒泡排序包括對列表進行多次遍歷、逐個比較元素以及交換順序混亂的相鄰項。
  • python教程
    開發知識體系序號教程名稱400301-Python快速入門連結: 400402-Python初級課程連結:400503- Python 定向爬蟲入門400604-Linux從入門到精通400705-Linux從入門到精通400806-Python資料庫操作400907-Apache
  • 「近水樓臺先得月」——理解KNN算法
    在人工智慧領域,有一種算法,非常貼近上述的形象比喻,這就是KNN算法,即K最近鄰算法(K-NearestNeighbors,簡稱KNN),它是一個比較簡單的機器學習算法,也是一個理論上比較成熟的、運用基於樣本估計的最大後驗概率規則的判別方法。本文對KNN算法做一個通俗易懂的介紹,並通過python進行編碼示範,讓讀者朋友對該算法有較好的理解。
  • KDD2020 | 揭秘Facebook搜索中的語義檢索技術
    文中共介紹了三方面的經驗:提出了一套統一的 embedding 框架用於建模個性化搜索中的語義提出了基於經典的倒排索引進行在線 embedding 檢索的系統討論了整個個性化搜索系統中很多端對端的優化技巧,例如最近鄰搜索調參經驗、全鏈路優化等最後,在Facebook 垂直搜索場景下驗證了本文方法的有效性
  • Python基礎 | 大學小白如何入門Python程序設計
    本文首發於微信公眾號:"算法與編程之美",歡迎關注,及時了解更多此系列文章。二、如何進行自主學習其實python非常適合初學者入門。相比較其他不少主流程式語言,有更好的可讀性,因此上手相對容易。自帶的各種模塊加上豐富的第三方模塊,免去了很多「重複造輪子」的工作,可以更快地寫出東西。