1. 程式人生 > >句子相似度計算的幾種方法

句子相似度計算的幾種方法

在做自然語言處理的過程中,我們經常會遇到需要找出相似語句的場景,或者找出句子的近似表達,這時候我們就需要把類似的句子歸到一起,這裡面就涉及到句子相似度計算的問題,那麼本節就來了解一下怎麼樣來用 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://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 計算的方式是非常好的。