自然語言處理中句子相似度計算的幾種方法

崔慶才丨靜覓發表於2018-06-15

在做自然語言處理的過程中,我們經常會遇到需要找出相似語句的場景,或者找出句子的近似表達,這時候我們就需要把類似的句子歸到一起,這裡面就涉及到句子相似度計算的問題,那麼本節就來了解一下怎麼樣來用 Python 實現句子相似度的計算。

基本方法

句子相似度計算我們一共歸類了以下幾種方法:

  • 編輯距離計算

  • 傑卡德係數計算

  • TF 計算

  • TFIDF 計算

  • Word2Vec 計算

下面我們來一一瞭解一下這幾種演算法的原理和 Python 實現。

編輯距離計算

編輯距離,英文叫做 Edit Distance,又稱 Levenshtein 距離,是指兩個字串之間,由一個轉成另一個所需的最少編輯操作次數,如果它們的距離越大,說明它們越是不同。許可的編輯操作包括將一個字元替換成另一個字元,插入一個字元,刪除一個字元。

例如我們有兩個字串:string 和 setting,如果我們想要把 string 轉化為 setting,需要這麼兩步:

  • 第一步,在 s 和 t 之間加入字元 e。

  • 第二步,把 r 替換成 t。

所以它們的編輯距離差就是 2,這就對應著二者要進行轉化所要改變(新增、替換、刪除)的最小步數。

那麼用 Python 怎樣來實現呢,我們可以直接使用 distance 庫:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

s1 = 'string'
s2 = 'setting'
print(edit_distance(s1, s2))複製程式碼

這裡我們直接使用 distance 庫的 levenshtein() 方法,傳入兩個字串,即可獲取兩個字串的編輯距離了。

執行結果如下:

2複製程式碼

這裡的 distance 庫我們可以直接使用 pip3 來安裝:

pip3 install distance複製程式碼

這樣如果我們想要獲取相似的文字的話可以直接設定一個編輯距離的閾值來實現,如設定編輯距離為 2,下面是一個樣例:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

strings = [
    '你在幹什麼',
    '你在幹啥子',
    '你在做什麼',
    '你好啊',
    '我喜歡吃香蕉'
]

target = '你在幹啥'
results = list(filter(lambda x: edit_distance(x, target) <= 2, strings))
print(results)複製程式碼

這裡我們定義了一些字串,然後定義了一個目標字串,然後用編輯距離 2 的閾值進行設定,最後得到的結果就是編輯距離在 2 及以內的結果,執行結果如下:

['你在幹什麼', '你在幹啥子']複製程式碼

通過這種方式我們可以大致篩選出類似的句子,但是發現一些句子例如“你在做什麼” 就沒有被識別出來,但他們的意義確實是相差不大的,因此,編輯距離並不是一個好的方式,但是簡單易用。

傑卡德係數計算

傑卡德係數,英文叫做 Jaccard index, 又稱為 Jaccard 相似係數,用於比較有限樣本集之間的相似性與差異性。Jaccard 係數值越大,樣本相似度越高。

實際上它的計算方式非常簡單,就是兩個樣本的交集除以並集得到的數值,當兩個樣本完全一致時,結果為 1,當兩個樣本完全不同時,結果為 0。

演算法非常簡單,就是交集除以並集,下面我們用 Python 程式碼來實現一下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np

def jaccard_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 將字中間加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 轉化為TF矩陣
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 求交集
    numerator = np.sum(np.min(vectors, axis=0))
    # 求並集
    denominator = np.sum(np.max(vectors, axis=0))
    # 計算傑卡德係數
    return 1.0 * numerator / denominator

s1 = '你在幹嘛呢'
s2 = '你在幹什麼呢'
print(jaccard_similarity(s1, s2))複製程式碼

這裡我們使用了 Sklearn 庫中的 CountVectorizer 來計算句子的 TF 矩陣,然後利用 Numpy 來計算二者的交集和並集,隨後計算傑卡德係數。

這裡值得學習的有 CountVectorizer 的用法,通過它的 fit_transform() 方法我們可以將字串轉化為詞頻矩陣,例如這裡有兩句話“你在幹嘛呢”和“你在幹什麼呢”,首先 CountVectorizer 會計算出不重複的有哪些字,會得到一個字的列表,結果為:

['麼', '什', '你', '呢', '嘛', '在', '幹']複製程式碼

這個其實可以通過如下程式碼來獲取,就是獲取詞表內容:

cv.get_feature_names()複製程式碼

接下來通過轉化之後,vectors 變數就變成了:

[[0 0 1 1 1 1 1]
 [1 1 1 1 0 1 1]]複製程式碼

它對應的是兩個句子對應詞表的詞頻統計,這裡是兩個句子,所以結果是一個長度為 2 的二維陣列,比如第一句話“你在幹嘛呢”中不包含“麼”字,那麼第一個“麼”字對應的結果就是0,即數量為 0,依次類推。

後面我們使用了 np.min() 方法並傳入了 axis 為 0,實際上就是獲取了每一列的最小值,這樣實際上就是取了交集,np.max() 方法是獲取了每一列的最大值,實際上就是取了並集。

二者分別取和即是交集大小和並集大小,然後作商即可,結果如下:

0.5714285714285714複製程式碼

這個數值越大,代表兩個字串越接近,否則反之,因此我們也可以使用這個方法,並通過設定一個相似度閾值來進行篩選。

TF計算

第三種方案就是直接計算 TF 矩陣中兩個向量的相似度了,實際上就是求解兩個向量夾角的餘弦值,就是點乘積除以二者的模長,公式如下:

cosθ=a·b/|a|*|b|複製程式碼

上面我們已經獲得了 TF 矩陣,下面我們只需要求解兩個向量夾角的餘弦值就好了,程式碼如下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from scipy.linalg import norm

def tf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 將字中間加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 轉化為TF矩陣
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 計算TF係數
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))

s1 = '你在幹嘛呢'
s2 = '你在幹什麼呢'
print(tf_similarity(s1, s2))複製程式碼

在在這裡我們使用了 np.dot() 方法獲取了向量的點乘積,然後通過 norm() 方法獲取了向量的模長,經過計算得到二者的 TF 係數,結果如下:

0.7302967433402214複製程式碼

TFIDF計算

另外除了計算 TF 係數我們還可以計算 TFIDF 係數,TFIDF 實際上就是在詞頻 TF 的基礎上再加入 IDF 的資訊,IDF 稱為逆文件頻率,不瞭解的可以看下阮一峰老師的講解:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html,裡面對 TFIDF 的講解也是十分透徹的。

下面我們還是藉助於 Sklearn 中的模組 TfidfVectorizer 來實現,程式碼如下:

from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
from scipy.linalg import norm

def tfidf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 將字中間加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 轉化為TF矩陣
    cv = TfidfVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 計算TF係數
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))

s1 = '你在幹嘛呢'
s2 = '你在幹什麼呢'
print(tfidf_similarity(s1, s2))複製程式碼

這裡的 vectors 變數實際上就對應著 TFIDF 值,內容如下:

[[0.         0.         0.4090901  0.4090901  0.57496187 0.4090901 0.4090901 ]
 [0.49844628 0.49844628 0.35464863 0.35464863 0.  0.35464863 0.35464863]]複製程式碼

執行結果如下:

0.5803329846765686複製程式碼

所以通過 TFIDF 係數我們也可以進行相似度的計算。

Word2Vec計算

Word2Vec,顧名思義,其實就是將每一個詞轉換為向量的過程。如果不瞭解的話可以參考:https://blog.csdn.net/itplus/article/details/37969519。

這裡我們可以直接下載訓練好的 Word2Vec 模型,模型的連結地址為:https://pan.baidu.com/s/1TZ8GII0CEX32ydjsfMc0zw,是使用新聞、百度百科、小說資料來訓練的 64 維的 Word2Vec 模型,資料量很大,整體效果還不錯,我們可以直接下載下來使用,這裡我們使用的是 news_12g_baidubaike_20g_novel_90g_embedding_64.bin 資料,然後實現 Sentence2Vec,程式碼如下:

import gensim
import jieba
import numpy as np
from scipy.linalg import norm

model_file = './word2vec/news_12g_baidubaike_20g_novel_90g_embedding_64.bin'
model = gensim.models.KeyedVectors.load_word2vec_format(model_file, binary=True)

def vector_similarity(s1, s2):
    def sentence_vector(s):
        words = jieba.lcut(s)
        v = np.zeros(64)
        for word in words:
            v += model[word]
        v /= len(words)
        return v

    v1, v2 = sentence_vector(s1), sentence_vector(s2)
    return np.dot(v1, v2) / (norm(v1) * norm(v2))複製程式碼

在獲取 Sentence Vector 的時候,我們首先對句子進行分詞,然後對分好的每一個詞獲取其對應的 Vector,然後將所有 Vector 相加並求平均,這樣就可得到 Sentence Vector 了,然後再計算其夾角餘弦值即可。

呼叫示例如下:

s1 = '你在幹嘛'
s2 = '你正做什麼'
vector_similarity(s1, s2)複製程式碼

結果如下:

0.6701133967824016複製程式碼

這時如果我們再回到最初的例子看下效果:

strings = [
    '你在幹什麼',
    '你在幹啥子',
    '你在做什麼',
    '你好啊',
    '我喜歡吃香蕉'
]

target = '你在幹啥'

for string in strings:
    print(string, vector_similarity(string, target))複製程式碼

依然是前面的例子,我們看下它們的匹配度結果是多少,執行結果如下:

你在幹什麼 0.8785495016487204
你在幹啥子 0.9789649689827049
你在做什麼 0.8781992402695274
你好啊 0.5174225914249863
我喜歡吃香蕉 0.582990841450621複製程式碼

可以看到相近的語句相似度都能到 0.8 以上,而不同的句子相似度都不足 0.6,這個區分度就非常大了,可以說有了 Word2Vec 我們可以結合一些語義資訊來進行一些判斷,效果明顯也好很多。

所以總體來說,Word2Vec 計算的方式是非常好的。

另外學術界還有一些可能更好的研究成果,這個可以參考知乎上的一些回答:https://www.zhihu.com/question/29978268/answer/54399062。

以上便是進行句子相似度計算的基本方法和 Python 實現,本節程式碼地址:https://github.com/AIDeepLearning/SentenceDistance。

嗨~ 給大家重磅推薦一本書!上市兩月就已經重印 4 次的 Python 爬蟲書!它就是由靜覓部落格博主崔慶才所作的《Python3網路爬蟲開發實戰》!!!同時文末還有抽獎贈書活動,不容錯過!!!

自然語言處理中句子相似度計算的幾種方法

書籍介紹

本書《Python3網路爬蟲開發實戰》全面介紹了利用 Python3 開發網路爬蟲的知識,書中首先詳細介紹了各種型別的環境配置過程和爬蟲基礎知識,還討論了 urllib、requests 等請求庫和 Beautiful Soup、XPath、pyquery 等解析庫以及文字和各類資料庫的儲存方法,另外本書通過多個真實新鮮案例介紹了分析 Ajax 進行資料爬取,Selenium 和 Splash 進行動態網站爬取的過程,接著又分享了一些切實可行的爬蟲技巧,比如使用代理爬取和維護動態代理池的方法、ADSL 撥號代理的使用、各類驗證碼(圖形、極驗、點觸、宮格等)的破解方法、模擬登入網站爬取的方法及 Cookies 池的維護等等。

此外,本書的內容還遠遠不止這些,作者還結合移動網際網路的特點探討了使用 Charles、mitmdump、Appium 等多種工具實現 App 抓包分析、加密引數介面爬取、微信朋友圈爬取的方法。此外本書還詳細介紹了 pyspider 框架、Scrapy 框架的使用和分散式爬蟲的知識,另外對於優化及部署工作,本書還包括 Bloom Filter 效率優化、Docker 和 Scrapyd 爬蟲部署、分散式爬蟲管理框架Gerapy 的分享。

全書共 604 頁,足足兩斤重呢~ 定價為 99 元!

作者介紹

看書就先看看誰寫的嘛,我們來了解一下~

崔慶才,靜覓部落格博主(https://cuiqingcai.com),部落格 Python 爬蟲博文閱讀量已過百萬,北京航空航天大學碩士,天善智慧、網易雲課堂講師,微軟小冰大資料工程師,有多個大型分散式爬蟲專案經驗,樂於技術分享,文章通俗易懂 ^_^

附皁片一張 ~(@^_^@)~

自然語言處理中句子相似度計算的幾種方法

更多詳請點選➡️juejin.im/post/5b1eb3…

相關文章