elasticsearch演算法之詞項相似度演算法(二)

無風聽海 發表於 2022-01-24
演算法 ElasticSearch

六、萊文斯坦編輯距離

前邊的幾種距離計算方法都是針對相同長度的詞項,萊文斯坦編輯距離可以計算兩個長度不同的單詞之間的距離;萊文斯坦編輯距離是通過新增、刪除、或者將一個字元替換為另外一個字元所需的最小編輯次數;

我們假設兩個單詞u、v的長度分別為i、j,則其可以分以下幾種情況進行計算

當有一個單詞的長度為0的時候,則編輯距離為不為零的單詞的長度;

\[ld_{u,v}(i,j)=max(i,j)\; \; \; \; \; \; \; \; min(i,j) = 0 \]

從編輯距離的定義上來看,在單詞的變化過程中,每個字元的變化都可以看做是在其字首子字串的編輯距離基礎上,進行當前字元的刪除、新增、替換操作;

name當u和v的長度都不為0的時候,存在三種轉化的可能

u的不包含最後一個字元的字首轉化為v的前提下,這是刪除字元的情形;
image

此時的數學公式為

\[ld_{u,v}(i,j) = ld_{u,v}(i-1,j)+1 \]

u已經整個轉化為v不包含最後一個字元的字首的前提下,這是新增字元的情形;
image

此時的數學公式為

\[ld_{u,v}(i,j) = ld_{u,v}(i,j-1)+1 \]

u字首已經轉化為v的字首的前提下,這個時候需要根據兩個詞的最後一個字元是否相等,如果不等的話就需要替換;
image

此時的數學公式為

\[ld_{u,v}(i,j) = ld_{u,v}(i,j)+C_{u_{i}\ne{v_{i}}},\;\; C_{u_{i}\ne{v_{i}}} = \left\{\begin{matrix} 1, u_{i}\ne v_{i} \\ 0, u_{i}=v_{i} \\ \end{matrix}\right. \]

然後取三種情況裡最小的值作為編輯距離即可;

以上都是從遞迴的角度進行的抽象描述,可能直接理解起來有點困難;我們還是通過vlna轉變為vlan為例,使用矩陣了直觀的瞭解一下這個演算法;

矩陣的第一行可以理解為vlna的字首子串轉化為v的編輯距離;

v => v,編輯距離為0;

vl => v,編輯距離為1;

同樣的道理,矩陣的第一列是vlna的子串v分別轉化為vlan的所有字首子串的編輯距離;

接下來我們就可以看看第二行第二列的編輯距離是怎麼計算的;

第二行第一列表示v=>vl的編輯距離為1,即

\[ld_{vlna,vlan}(0,1) = ld_{v,vl} = 1 \]

此時只需要在這個基礎上刪除l即可;這就是對應刪除字元的情況,此時編輯距離為2;

\[ld_{vlna,vlan}(1,1)= ld_{vl,vl} = ld_{v,vl} + 1 = 2 \]

第一行第二列表示vl=>v的編輯距離為1,即

\[ld_{vlna,vlan}(1,0) = ld_{vl,v} = 1 \]

此時只需要在這個基礎上增加字元l即可;這就是對應新增字元的情況,此時編輯距離為2;

\[ld_{vlna,vlan}(1,1)= ld_{vl,vl} = ld_{vl,v} + 1 = 2 \]

第一行第一列表示v=>v的編輯距離為0,即

\[ld_{vlna,vlan}(0,0) = ld_{v,v} = 0 \]

由於此時由於兩個單詞裡的字元都是l,所以字元不需要替換,這就是對應字元替換的情況,此時編輯距離為0;

\[ld_{vlna,vlan}(1,1)= ld_{vl,vl} = ld_{v,v} + 0 = 0 \]

通過以上分析我們可以知道編輯距離為0;同時我們也可以發現矩陣中每個位置的編輯距離是跟其左邊、左上、上邊的編輯距離有關的,只需要計算三者中的最小者作為編輯距離即可;

image

通過以上的編輯距離矩陣,我們最終只關注最後一個單元格的值,而其計算只需要關注其上一行和當前行的編輯距離;

為了計算方便,我們可以在u和v前邊分別加一個空白佔位符,這樣對每個字元都存在三個方向位置的編輯距離了;

我們使用如下的方法計算編輯距離和編輯距離矩陣;

import copy
import pandas as pd

def levenshtein_edit_distance(u, v):
    u = u.lower()
    v = v.lower()
    distance = 0

    if len(u) == 0:
        distance = len(v)
    elif len(v) == 0:
        distance = len(u)
    else:
        edit_matrix = []
        pre_row = [0] * (len(v) + 1)
        current_row = [0] * (len(v) + 1)

        # 初始化補白行的編輯距離
        for i in range(len(u) +1):
            pre_row[i] = i

        for i in range(len(u)):
            current_row[0] = i + 1
            for j in range(len(v)):
                cost = 0 if u[i] == v[j] else 1
                current_row[j+1] = min(current_row[j] + 1, pre_row[j+1] + 1, pre_row[j] + cost)

            for j in range(len(pre_row)):
                pre_row[j] = current_row[j]

            edit_matrix.append(copy.copy(current_row))

        distance = current_row[len(v)]
        edit_matrix = np.array(edit_matrix)
        edit_matrix = edit_matrix.T
        edit_matrix = edit_matrix[1:,]
        edit_matrix = pd.DataFrame(data = edit_matrix, index=list(v), columns=list(u))

    return distance,edit_matrix

我們使用相同的關鍵字,使用如下程式碼進行測試

vlan = 'vlan'
vlna = 'vlna'
http='http'
words = [vlan, vlna, http]


input_word = 'vlna'
for word in words:
    distance, martrix = levenshtein_edit_distance(input_word, word)
    print(f"{input_word} and {word} levenshtein edit distance is  {distance}")
    print('the complate edit distance matrix')
    print(martrix)
    

vlna and vlan levenshtein edit distance is  2
the complate edit distance matrix
   v  l  n  a
v  0  1  2  3
l  1  0  1  2
a  2  1  1  1
n  3  2  1  2
vlna and vlna levenshtein edit distance is  0
the complate edit distance matrix
   v  l  n  a
v  0  1  2  3
l  1  0  1  2
n  2  1  0  1
a  3  2  1  0
vlna and http levenshtein edit distance is  4
the complate edit distance matrix
   v  l  n  a
h  1  2  3  4
t  2  2  3  4
t  3  3  3  4
p  4  4  4  4

七、餘弦距離

餘弦距離是一個跟餘弦相似度關聯的的概念;我們可以使用向量來表示不同的單詞,而兩個不同單詞向量之間的餘弦值便是餘弦相似度;兩者之間的夾角越小則餘弦值越小,則兩者約相似;
image

根據向量的內積公式可以得到如下的餘弦相似度公式;

\[cs(u,v) = \cos\theta = \frac{u\cdot v }{\|u\| \|v\|} = \frac{\sum_{i=1}^{n} u_{i}v_{i} }{\sqrt{\sum_{i=1}^{n} u_{i}^{2} } \sqrt{\sum_{i=1}^{n} v_{i}^{2} }} \]

餘弦相似度越大,則兩個單詞越相似,而距離則正好相反,則可得餘弦距離為

\[cs(u,v)= 1- cs(u,v) =1- \cos\theta =1- \frac{u\cdot v }{\|u\| \|v\|} =1- \frac{\sum_{i=1}^{n} u_{i}v_{i} }{\sqrt{\sum_{i=1}^{n} u_{i}^{2} } \sqrt{\sum_{i=1}^{n} v_{i}^{2} }} \]

要計算餘弦距離就需要首先將單詞轉化為向量,我們可以通過scipy.stats.itemfreq來將單詞進行字元袋向量化;我們通過以下方法計算每個詞中的每個字元出現的次數;

from scipy.stats import itemfreq

def boc_term_vectors(words):
    words = [word.lower() for word in words]
    unique_chars = np.unique(np.hstack([list(word) for word in words]))
    word_term_counts = [{char:count for char,count in itemfreq(list(word))} for word in words]


    boc_vectors = [np.array([
        int(word_term_count.get(char, 0)) for char in unique_chars])
        for   word_term_count in  word_term_counts
    ]

    return list(unique_chars), boc_vectors

使用以下程式碼測試一下

vlan = 'vlan'
vlna = 'vlna'
http='http'
words = [vlan, vlna, http]

chars, (boc_vlan,boc_vlna,boc_http) = boc_term_vectors(words)
print(f'all chars {chars}')
print(f"vlan {boc_vlan}")
print(f"vlna {boc_vlna}")
print(f"http {boc_http}")


all chars ['a', 'h', 'l', 'n', 'p', 't', 'v']
vlan [1 0 1 1 0 0 1]
vlna [1 0 1 1 0 0 1]
http [0 1 0 0 1 2 0]

我們可以根據公式使用以下方法計算餘弦距離;

def cosin_distance(u, v):
    distance = 1.0 - np.dot(u,v)/(np.sqrt(sum(np.square(u))) * np.sqrt(sum(np.square(v))))
    return distance

使用相同的關鍵字,使用以下程式碼測試餘弦距離;

vlan = 'vlan'
vlna = 'vlna'
http='http'
words = [vlan, vlna, http]

chars, boc_words = boc_term_vectors(words)
input_word =vlna
boc_input = boc_words[1]
for word, boc_word in zip(words, boc_words):
    print(f'{input_word} and {word} cosine distance id {cosin_distance(boc_input, boc_word)}')
    

vlna and vlan cosine distance id 0.0
vlna and vlna cosine distance id 0.0
vlna and http cosine distance id 1.0

相關文章