NLP點滴——文本類似度

前言

在天然語言處理過程當中,常常會涉及到如何度量兩個文本之間的類似性,咱們都知道文本是一種高維的語義空間,如何對其進行抽象分解,從而可以站在數學角度去量化其類似性。而有了文本之間類似性的度量方式,咱們即可以利用劃分法的K-means、基於密度的DBSCAN或者是基於模型的機率方法進行文本之間的聚類分析;另外一方面,咱們也能夠利用文本之間的類似性對大規模語料進行去重預處理,或者找尋某一實體名稱的相關名稱(模糊匹配)。而衡量兩個字符串的類似性有不少種方法,如最直接的利用hashcode,以及經典的主題模型或者利用詞向量將文本抽象爲向量表示,再經過特徵向量之間的歐式距離或者皮爾森距離進行度量。本文圍繞文本類似性度量的主題,從最直接的字面距離的度量到語義主題層面的度量進行整理總結,並將平時項目中用到的文本類似性代碼進行了整理,若有任何紕漏還請指出,我會第一時間改正^v^。(ps.平時用的Java和scala較多,本文主要以Java爲例。)java

字面距離

提到如何比較兩個字符串,咱們從最初編程開始就知道:字符串有字符構成,只要比較比較兩個字符串中每個字符是否相等便知道兩個字符串是否相等,或者更簡單一點將每個字符串經過哈希函數映射爲一個哈希值,而後進行比較。可是這種方法有一個很明顯的缺點,就是過於「硬」,對於類似性的度量其只有兩種,0不類似,1類似,哪怕兩個字符串只有一個字符不相等也是不類似,這在NLP的不少狀況是沒法使用的,因此下文咱們就「軟」的類似性的度量進行整理,而這些方法僅僅考慮了兩個文本的字面距離,沒法考慮到文本內在的語義內容。python

common lang庫

文中在部分代碼應用中使用了Apache提供的common lang庫,該庫包含不少Java標準庫中沒有的但卻很實用的函數。其maven引用以下:git

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

相同字符數

在傳統的字符串比較過程當中,咱們考慮字符串中每一個字符是否相等,而且考慮了字符出現的順序,若是不考慮字符出現的順序,咱們能夠利用兩個文本之間相同的字符數量,很簡單再也不贅述,能夠利用common lang中的getFuzzyDistance:github

int dis = StringUtils.getFuzzyDistance(term, query, Locale.CHINA);

萊文斯坦距離(編輯距離)

定義

咱們在學習動態規劃的時候,一個很經典的算法即是計算兩個字符串的編輯距離,即:算法

萊文斯坦距離,又稱Levenshtein距離,是編輯距離(edit distance)的一種。指兩個字串之間,由一個轉成另外一個所需的最少編輯操做次數。許可的編輯操做包括將一個字符替換成另外一個字符,插入一個字符,刪除一個字符。apache

例如將kitten一字轉成sitting:編程

  1. sitten (k→s)
  2. sittin (e→i)
  3. sitting (→g)

那麼兩者的編輯距離爲3。
俄羅斯科學家弗拉基米爾·萊文斯坦在1965年提出這個概念。網絡

實現方式

咱們能夠利用common lang中StringUtils的函數來計算:app

int dis = StringUtils.getLevenshteinDistance(s1, s2);
//實現
public static int getLevenshteinDistance(CharSequence s, CharSequence t) {
    if (s == null || t == null) {
        throw new IllegalArgumentException("Strings must not be null");
    }
    int n = s.length(); // length of s
    int m = t.length(); // length of t
    if (n == 0) {
        return m;
    } else if (m == 0) {
        return n;
    }
    if (n > m) {
        // swap the input strings to consume less memory
        final CharSequence tmp = s;
        s = t;
        t = tmp;
        n = m;
        m = t.length();
    }
    int p[] = new int[n + 1]; //'previous' cost array, horizontally
    int d[] = new int[n + 1]; // cost array, horizontally
    int _d[]; //placeholder to assist in swapping p and d
    // indexes into strings s and t
    int i; // iterates through s
    int j; // iterates through t
    char t_j; // jth character of t
    int cost; // cost
    for (i = 0; i <= n; i++) {
        p[i] = i;
    }
    for (j = 1; j <= m; j++) {
        t_j = t.charAt(j - 1);
        d[0] = j;
        for (i = 1; i <= n; i++) {
            cost = s.charAt(i - 1) == t_j ? 0 : 1;
            // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
            d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
        }
        // copy current distance counts to 'previous row' distance counts
        _d = p;
        p = d;
        d = _d;
    }
    // our last action in the above loop was to switch d and p, so p now
    // actually has the most recent cost counts
    return p[n];
}

Jaro距離

定義

Jaro Distance也是字符串類似性的一種度量方式,也是一種編輯距離,Jaro 距離越高本文類似性越高;而Jaro–Winkler distance是Jaro Distance的一個變種。聽說是用來斷定健康記錄上兩個名字是否相同,也有說是是用於人口普查。從最初其應用咱們即可看出其用法和用途,其定義以下:

其中

  • 是匹配數目(保證順序相同)
  • 字符串長度
  • 是換位數目

其中t換位數目表示:兩個分別來自S1和S2的字符若是相距不超過

咱們就認爲這兩個字符串是匹配的;而這些相互匹配的字符則決定了換位的數目t,簡單來講就是不一樣順序的匹配字符的數目的一半即爲換位的數目t,舉例來講,MARTHA與MARHTA的字符都是匹配的,可是這些匹配的字符中,T和H要換位才能把MARTHA變爲MARHTA,那麼T和H就是不一樣的順序的匹配字符,t=2/2=1。
而Jaro-Winkler則給予了起始部分就相同的字符串更高的分數,他定義了一個前綴p,給予兩個字符串,若是前綴部分有長度爲 的部分相同,則Jaro-Winkler Distance爲:

  • 是兩個字符串的Jaro Distance
  • 是前綴的相同的長度,可是規定最大爲4
  • 則是調整分數的常數,規定不能超過0.25,否則可能出現dw大於1的狀況,Winkler將這個常數定義爲0.1

舉個簡單的例子:
計算s_1=DIXON,s_2=DICKSONX的距離

咱們利用\lfloor \frac{max(|s_1|,|s_2|)}{2}-1 \rfloor能夠獲得一個匹配窗口距離爲3,圖中黃色部分即是匹配窗口,其中1表示一個匹配,咱們發現兩個X並無匹配,由於其超出了匹配窗口的距離3。咱們能夠獲得:

其Jaro score爲:

d_j=\frac{1}{3}(\frac{4}{5}+\frac{4}{8}+\frac{4-0}{4})=0.767

而計算Jaro–Winkler score,咱們使用標準權重p=0.1,\ell=2,其結果以下:

實現方式

一樣咱們能夠利用common lang中的getJaroWinklerDistance函數來實現,注意這裏實現的是Jaro–Winkler distance

double dis = StringUtils.getJaroWinklerDistance(reviewName.toLowerCase(), newsName.toLowerCase());

//實現
public static double getJaroWinklerDistance(final CharSequence first, final CharSequence second) {
    final double DEFAULT_SCALING_FACTOR = 0.1; //標準權重
    if (first == null || second == null) {
        throw new IllegalArgumentException("Strings must not be null");
    }
    final double jaro = score(first,second); // 計算Jaro score
    final int cl = commonPrefixLength(first, second); // 計算公共前綴長度
    final double matchScore = Math.round((jaro + (DEFAULT_SCALING_FACTOR * cl * (1.0 - jaro))) *100.0)/100.0;   // 計算 Jaro-Winkler score

    return  matchScore;
}

應用

在Wetest輿情監控中,咱們在找尋遊戲名簡稱和全稱的對應關係時便使用到了Jaro-Winkler score進行衡量,其中咱們將Jaro分數大於0.6的認爲是類似文本,以後在總的類似文本中提取最類似的做爲匹配項,實現效果還不錯:

其中冒號左邊是待匹配項,右邊是匹配項<遊戲名 詞頻,Jaro-Winkler score>,Jaro-Winkler score較高的通常都是正確的匹配項。

SimHash

定義

SimHash是一種局部敏感hash,它也是Google公司進行海量網頁去重使用的主要算法。
傳統的Hash算法只負責將原始內容儘可能均勻隨機地映射爲一個簽名值,原理上僅至關於僞隨機數產生算法。傳統的hash算法產生的兩個簽名,若是原始內容在必定機率下是相等的;若是不相等,除了說明原始內容不相等外,再也不提供任何信息,由於即便原始內容只相差一個字節,所產生的簽名也極可能差異很大。因此傳統的Hash是沒法在簽名的維度上來衡量原內容的類似度,而SimHash自己屬於一種局部敏感哈希算法,它產生的hash簽名在必定程度上能夠表徵原內容的類似度。
咱們主要解決的是文本類似度計算,要比較的是兩個文章是否類似,固然咱們降維生成了hash簽名也是用於這個目的。看到這裏估計你們就明白了,咱們使用的simhash就算把文章中的字符串變成 01 串也仍是能夠用於計算類似度的,而傳統的hash卻不行。

咱們能夠來作個測試,兩個相差只有一個字符的文本串,「你媽媽喊你回家吃飯哦,回家羅回家羅」 和 「你媽媽叫你回家吃飯啦,回家羅回家羅」。
經過simhash計算結果爲:
1000010010101101111111100000101011010001001111100001001011001011
1000010010101101011111100000101011010001001111100001101010001011
經過傳統hash計算爲:
0001000001100110100111011011110
1010010001111111110010110011101

經過上面的例子咱們能夠很清晰的發現simhash的局部敏感性,類似文本只有部分01變化,而hash值很明顯,即便變化很小一部分,也會相差很大。

基本流程

注:具體的事例摘自Lanceyan[10]的博客《海量數據類似度計算之simhash和海明距離》

  1. 分詞,把須要判斷文本分詞造成這個文章的特徵單詞。最後造成去掉噪音詞的單詞序列併爲每一個詞加上權重,咱們假設權重分爲5個級別(1~5)。好比:「 美國「51區」僱員稱內部有9架飛碟,曾看見灰色外星人 」 ==> 分詞後爲 「 美國(4) 51區(5) 僱員(3) 稱(1) 內部(2) 有(1) 9架(3) 飛碟(5) 曾(1) 看見(3) 灰色(4) 外星人(5)」,括號裏是表明單詞在整個句子裏重要程度,數字越大越重要。
  2. hash,經過hash算法把每一個詞變成hash值,好比「美國」經過hash算法計算爲 100101,「51區」經過hash算法計算爲 101011。這樣咱們的字符串就變成了一串串數字,還記得文章開頭說過的嗎,要把文章變爲數字計算才能提升類似度計算性能,如今是降維過程進行時。
  3. 加權,經過 2步驟的hash生成結果,須要按照單詞的權重造成加權數字串,好比「美國」的hash值爲「100101」,經過加權計算爲「4 -4 -4 4 -4 4」;「51區」的hash值爲「101011」,經過加權計算爲 「 5 -5 5 -5 5 5」。
  4. 合併,把上面各個單詞算出來的序列值累加,變成只有一個序列串。好比 「美國」的 「4 -4 -4 4 -4 4」,「51區」的 「 5 -5 5 -5 5 5」, 把每一位進行累加, 「4+5 -4+-5 -4+5 4+-5 -4+5 4+5」 ==》 「9 -9 1 -1 1 9」。這裏做爲示例只算了兩個單詞的,真實計算須要把全部單詞的序列串累加。
  5. 降維,把4步算出來的 「9 -9 1 -1 1 9」 變成 0 1 串,造成咱們最終的simhash簽名。 若是每一位大於0 記爲 1,小於0 記爲 0。最後算出結果爲:「1 0 1 0 1 1」。
    整個過程的流程圖爲:

類似性度量

有了simhash值,咱們須要來度量兩個文本間的類似性,就像上面的例子同樣,咱們能夠比較兩個simhash間0和1不一樣的數量。這即是漢明距離(Hamming distance)

在信息論中,兩個等長字符串之間的漢明距離(英語:Hamming distance)是兩個字符串對應位置的不一樣字符的個數。換句話說,它就是將一個字符串變換成另一個字符串所須要替換的字符個數。
漢明重量是字符串相對於一樣長度的零字符串的漢明距離,也就是說,它是字符串中非零的元素個數:對於二進制字符串來講,就是1的個數,因此11101的漢明重量是4。
例如:
1011101與1001001之間的漢明距離是2

通常在利用simhash進行文本類似度比較時,咱們認爲漢明距離小於3的文本是類似的。

存儲索引


存儲:

  1. 將一個64位的simhash簽名拆分紅4個16位的二進制碼。(圖上紅色的16位)
  2. 分別拿着4個16位二進制碼查找當前對應位置上是否有元素。(放大後的16位)
  3. 對應位置沒有元素,直接追加到鏈表上;對應位置有則直接追加到鏈表尾端。(圖上的 S1 — SN)

查找:

  1. 將須要比較的simhash簽名拆分紅4個16位的二進制碼。
  2. 分別拿着4個16位二進制碼每個去查找simhash集合對應位置上是否有元素。
  3. 若是有元素,則把鏈表拿出來順序查找比較,直到simhash小於必定大小的值,整個過程完成。
  4. 在去重時,由於漢明距離小於3則爲重複文本,那麼若是存在simhash類似的文本,對於四段simhash則至少有一段simhash是相同的,因此在去重時對於待判斷文本D,若是D中每一段的simhash都沒有相同的,那麼D爲無重複文本。

原理:
借鑑hashmap算法找出能夠hash的key值,由於咱們使用的simhash是局部敏感哈希,這個算法的特色是隻要類似的字符串只有個別的位數是有差異變化。那這樣咱們能夠推斷兩個類似的文本,至少有16位的simhash是同樣的。具體選擇16位、8位、4位,你們根據本身的數據測試選擇,雖然比較的位數越小越精準,可是空間會變大。分爲4個16位段的存儲空間是單獨simhash存儲空間的4倍。

實現

在實際NLP的使用中,我利用Murmur3做爲字符串的64位哈希值,用Java和spark分別實現了一個simhash的版本
我將源碼放在了github上,以下連接:

github: xlturing/simhashJava

其中利用告終巴做爲文本的分詞工具,Murmur3用來產生64位的hashcode。另外根據上述存儲方式,進行了simhash分段存儲,提升搜索速度,從而進行高效查重。

應用

simhash從最一開始用的最多的場景即是大規模文本的去重,對於爬蟲從網上爬取的大規模語料數據,咱們須要進行預處理,刪除重複的文檔才能進行後續的文本處理和挖掘,那麼利用simhash是一種不錯的選擇,其計算複雜度和效果都有一個很好的折中。
可是在實際應用過程當中,也發現一些badcase,徹底無關的文本正好對應成了相同的simhash,精確度並非很高,並且simhash更適用於較長的文本,可是在大規模語料進行去重時,simhash的計算速度優點仍是很不錯的。

語義類似性

在NLP中有時候咱們度量兩個短文本或者說更直接的兩個詞語的類似性時,直接經過字面距離是沒法實現的,如:中國-北京,意大利-羅馬,這兩個短語之間的類似距離應該是相似的,由於都是首都與國家的關係;再好比(男人、男孩),(女人、女孩)應該是相同的關係,可是咱們看其字面距離都是0。
想要作到語義層面的度量,咱們須要用到機器學習建模,而天然語言的問題轉化爲機器學習的首要問題即是找到一種方法把天然語言的符號數學化。

背景知識

在天然語言處理領域中,有兩大理論方向,一種是基於統計的經驗主義方法,另外一種是基於規則的理性主義方法[15]。而隨着計算機性能的提高,以及互聯網發展而獲得的海量語料庫,目前NLP的研究更可能是基於統計的經驗主義方法。因此在本文討論的語義類似性中,也是從統計學的角度出發進行總結。

統計語言模型

對於統計語言模型而言,最基礎的理論即是貝葉斯理論(Bayes' theorem PS.關於貝葉斯理論強烈推薦:數學之美番外篇:平凡而又神奇的貝葉斯方法,一篇深刻淺出的好文。另外推薦下本身師兄參與翻譯的做品《貝葉斯方法——機率編程與貝葉斯推斷》很全面的貝葉斯理論+實踐書籍)。對於大規模語料庫,咱們能夠經過詞頻的方式來獲取機率,例如100個句子中,出現了1次"Okay",那麼

而一樣的對於句子"An apple ate the chicken"咱們能夠認爲其機率爲0,由於這不符合咱們說話的邏輯。
統計語言模型是用來計算一個句子的機率,其一般基於一個語料庫D來構建。如何表示一個句子的機率呢?咱們用來表示一個基元(一般就是指詞語,也能夠是字或短語),那麼對於一個由N個詞組成的句子W能夠表示爲

那麼其聯合機率

就能夠認爲是該句子的機率,根據貝葉斯公式的鏈式法則能夠獲得:

其中條件機率p(\omega_1)p(\omega_2|\omega_1)p(\omega_3|\omega_1,\omega_2)...p(\omega_n|\omega_1...\omega_{n-1})即是語言模型的參數,若是咱們把這些所有算出來,那麼一個句子的機率咱們就能很輕易的得出。可是很明顯,這個參數的量是巨大的是沒法計算的。這時咱們能夠將\omega_i|\omega_1...\omega_{i-1}映射到某個等價類E(\omega_i|\omega_1...\omega_{i-1}),從而下降參數數目。
ps.語料庫咱們用C表示,而詞典D通常爲語料中出現的全部不重複詞

n-gram模型

既然每一個單詞依賴的單詞過多,從而形成了參數過多的問題,那麼咱們就簡單點,假設每一個單詞只與其前n-1個單詞有關,這即是n-1階Markov假設,也就是n-gram模型的基本思想。
那麼對於句子W的機率咱們能夠簡化以下:

那麼對於最簡單的一階狀況也稱unigram或uni-gram或monogram(二階bigram 三階trigram)就簡單表示爲

爲了在句首和句尾可以統一,咱們通常會在句首加一個BOS標記,句尾加一個EOS標記,那麼對於句子"Mark wrote a book",其機率能夠表示以下:

爲了預估p(\omega_i|\omega_{i-1})條件機率,根據大數定理,簡單統計語料庫中\omega_{i-1},\omega_i出現的頻率,並進行歸一化。咱們用c來表示頻率,那麼可表示以下:

p(\omega_i|\omega_{i-1})=\frac{c(\omega_{i-1}\omega_i)}{\sum_{\omega_i}c(\omega_{i-1}\omega_i)}

其中分母在unigram中就能夠簡單認爲是詞語\omega_{i-1}出現的次數。
在n-gram模型中還有一個很重要的問題就是平滑化,由於再大的語料庫都不可能涵蓋全部狀況,考慮兩個問題:

  1. c(\omega_{i-1}\omega_i)=0那麼p(\omega_i|\omega_{i-1})=0就是0嗎?
  2. c(\omega_{i-1}\omega_i)=\sum_{\omega_i}c(\omega_{i-1}\omega_i)那麼p(\omega_i|\omega_{i-1})=0就是1嗎?

這顯然是不合理的,這就須要進行平滑,這裏不展開討論。
根據最大似然,咱們能夠獲得:

其中C表示語料庫,Context(omega)表示詞語的上下文,而這裏對於n-gram模型Context(\omega)=\oemga^{i-1}_{i-n+1},取對數後的對數似然函數爲:

從上式咱們能夠看出p(\omega|Context(\omega))能夠看作是\omega關於Context(omega)的函數,即:

其中theta爲待定參數集,經過語料庫訓練獲得參數集後,F便肯定了,咱們不須要再存儲機率p(\omega|Context(\omega)),能夠直接計算獲得,而語言模型中很關鍵的就在於F的構造

詞向量

爲了從使得計算機從語義層面理解人類語言,首先要作的就是將語言數學化,如何進行表示呢?人們便提出了詞向量的概念,即用一個向量來表示一個詞。

One-hot Representation

一種最簡單詞向量就是利用詞頻向量將高維的語義空間抽象成數學符號表示,向量長度爲詞典的大小,這種表示方式很是直觀,可是容易形成維度災難,而且仍是不能刻畫語義的信息。

詞語表示

對於詞語而言,用一個向量來表示一個詞,最直觀簡單的方式就是將每一個詞變爲一個很長的向量,向量長度即是詞典的長度,其中絕大部分爲0,只有一個維度爲1表明了當前詞。
假設語料庫:「衝突容易引起戰爭」,那麼詞典爲

  • D=[衝突,容易,引起,戰爭]
  • 衝突=[1,0,0,0]
  • 戰爭=[0,0,0,1]

每一個詞都是含有一個1的n維向量(),這種方式咱們壓縮存儲下,就是給每一個詞語分配一個ID,一般實際變成咱們最簡單的就是用hash值表示一個詞語。這種方式能夠用在SVM、最大熵和CRF等等算法中,完成NLP的大多數場景。例如,咱們能夠直接將
可是缺點很明顯,就是咱們用這種方式依舊沒法度量兩個詞的語義類似性,任意兩個詞之間都是孤立的,好比上面的衝突和戰爭是近義詞,可是卻沒有任何關聯性。

文檔表示

一樣文檔也能夠用詞頻向量的形式來表示,通常咱們會利用tf-idf做爲每個詞的特徵值,以後會挑選每篇文檔比較重要的部分詞來表示一篇文檔,拿遊戲來講,以下:
[王者榮耀, 陰陽師, 夢幻西遊]

  • doc1:[tf-idf(王者榮耀), tf-idf(陰陽師), tf-idf(夢幻西遊)]
  • doc2:[tf-idf(王者榮耀), tf-idf(陰陽師), tf-idf(夢幻西遊)]

而後咱們就能夠利用K-means等聚類算法進行聚類分析,固然對於每篇文檔,通常咱們只會選取部分詞彙,由於若是詞彙過多可能形成NLP中常見的維度「災難」。這種方式在大多數NLP場景中都是適用的,可是因爲這種表示每每是創建在高維空間,爲了不維度災難就要損失必定的語義信息,這也是這種方法的弊端。

Distributed representation

另一種詞向量的表示Distributed representation最先由 Hinton在 1986年提出。它是一種低維實數向量,這種向量通常長成這個樣子:
[0.792, −0.177, −0.107, 0.109, −0.542, …]
維度以 50 維和 100 維比較常見,固然了,這種向量的表示不是惟一的。
Distributed representation的關鍵點在於,將高維空間中的詞彙映射到一個低維的向量空間中,而且讓相關或者類似的詞,在距離上更接近(看到這裏你們有沒有想到普通hash以及simhash的區別呢?),這裏引用一張圖片(來自[13]):

圖中是英語和西班牙語經過訓練分別獲得他們的詞向量空間,以後利用PCA主成分分析進行降維表示在二維座標圖中的。咱們能夠清晰的看出,對於兩種語系的一二三四五,在空間距離上竟是如此的類似,這就是Distributed representation詞向量表示的意義所在。
這種採用低維空間表示法,不但解決了維數災難問題,而且挖掘了word之間的關聯屬性,從而提升了向量語義上的準確度,下面咱們討論的語言模型都是基於這種詞向量表示方式。
PS. 有時候也會出現Word Represention或 Word Embedding(所謂詞嵌入)的說法。另外咱們這裏說的詞向量是在詞粒度進行分析,固然咱們也能夠在字粒度的字向量、句子粒度的句向量以及文檔粒度的文檔向量進行表示分析。

主題模型

在長文本的篇章處理中,主題模型是一種經典的模型,常常會用在天然語言處理、推薦算法等應用場景中。本節從LDA的演變過程對LDA進行闡述,而後就LDA在長文本類似性的判斷聚類上作簡要說明。

LSA

首先對於一篇文檔Document,詞語空間的一個詞頻向量以下:

其中每一個維度表示某一詞語term在該文檔中出現的次數,最終對於大量的訓練樣本,咱們能夠獲得訓練樣本的矩陣X,以下圖:

LSA的基本思想,即是利用最基本的SVD奇異值分解,將高維語義空間映射到低維空間,其流程以下:

這樣對於訓練樣本中詞表的每個term咱們便獲得了一個低維空間的向量表示。但LSA的顯著問題即是值考慮詞頻,並不區分同一詞語的不一樣含義

PLSA

LSA基於最基本的SVD分解,但缺少嚴謹的數理統計邏輯,因而Hofmann提出了PLSA,其中P即是Probabilistic,其基本的假設是每一個文檔所表示的詞頻空間向量w服從多項式分佈(Multinomial distribution


簡單扯兩句多項式分佈:

  • 伯努利分佈Bernoulli distribution)咱們從接觸機率論開始便知道,即所謂的投硬幣,其離散分佈以下:

    可是吊吊的數學家們總喜歡作一些優雅的讓人看不懂的事情,因此也能夠寫做以下公式:

    其中k爲0或者1

  • 二項分佈Binomial distribution):


    若是進行次投硬幣實驗,計算出現m次正面朝上的機率
    伯努利分佈是二項分佈中n=1時的特殊狀況

  • Categorical分佈Categorical distribution),若是咱們將投硬幣改爲擲骰子,那麼原來一維向量x就會變成一個六維向量,其中每一維度爲1表示出現該面,0表示沒出現,用數學表示即對於隨機變量X有k中狀況,其中第種狀況出現的機率爲

    那麼咱們能夠獲得其離散機率分佈以下:

    其中若是那麼爲1,不然爲0

  • 多項式分佈Multinomial distribution):與二項分佈相似,Categorical分佈進行N次試驗,便獲得多項式分佈:

    一樣咱們能夠寫成吊吊的形式:

其中gamma函數:當n>0,則(ps.該形式與狄利克雷分佈(Dirichlet distribution)的形式很是類似,由於多項式分佈是狄利克雷分佈的共軛先驗)


OK簡單梳理了下過去的知識,PLSA假設每篇文檔的詞頻向量服從Categorical分佈,那麼對於整個訓練樣本的詞頻矩陣W則服從多項式分佈。PLSA利用了aspect model,引入了潛在變量z(即所謂主題),使其變成一個混合模型(mixture model)。其圖模型以下:

其中表示文檔集,Z即是PLSA中引入的隱含變量(主題/類別),表示詞表。表示單詞出如今文檔的機率,表示文檔中出現主題下的單詞的機率,給定主題出現單詞的機率。其中每一個主題在全部詞項上服從Multinomial分佈,每一個文檔在全部主題上服從Multinmial分佈。按照生成模型,整個文檔的生成過程以下:
(1)以的機率生成文檔
(2)以的機率選中主題
(3)以的機率產生一個單詞
那麼對於單詞出如今文檔的聯合機率分佈,而是隱含變量。

其中分別對應了兩組Multinomial分佈,PLSA須要訓練兩組分佈的參數

LDA

有了PLSA,那麼LDA就相對簡單了,其至關於貝葉斯(Bayes' theorem PS.關於貝葉斯理論強烈推薦:數學之美番外篇:平凡而又神奇的貝葉斯方法,一篇深刻淺出的好文)PLSA即:
LDA=Bayesian pLSA
爲何這麼說呢?咱們站在貝葉斯理論的角度看上文提到的PLSA,基於上文的闡述,咱們知道PLSA的假設是文檔-詞語的詞頻矩陣服從多項式分佈(multinomial distribution),那麼在貝葉斯理論中,至關於咱們找到了似然函數,那麼想要計算後驗機率時,咱們須要找到先驗機率。


簡單扯兩句共軛先驗:
根據貝葉斯理論咱們有以下形式:

OK其中咱們能夠成爲似然函數即一件事情發生的似然性(最大似然估計),那麼至關於先驗機率分佈,通常爲一個常數,因此忽略。那麼對於計算後驗機率,咱們須要找到似然函數和先驗分佈。
通常當咱們已知似然函數的形式的時候,咱們須要找到先驗分佈,那麼對於全部知足[0,1]區間內的分佈都符合這個條件,爲了計算簡單,咱們採用與似然函數形式儘可能一致的分佈做爲先驗分佈,這就是所謂的共軛先驗。
在上文中介紹多項式分佈時提到了Dirichlet分佈,咱們看多項式分佈的形式以下:

那麼咱們須要找尋形式類似以下的分佈:

而Dirichlet分佈的形式以下:

看出來了吧,去掉左邊的Beta分佈不說,在右邊的形式上Dirichlet分佈和Multinomial分佈是及其類似的,因此Dirichlet分佈是Multinomial分佈的共軛先驗。


再回到LDA,根據以前分析的PLSA可知,每一個文檔中詞的Topic分佈服從Multinomial分佈,其先驗選取共軛先驗即Dirichlet分佈;每一個Topic下詞的分佈服從Multinomial分佈,其先驗也一樣選取共軛先驗即Dirichlet分佈。其圖模型以下:

咱們能夠看出LDA中每篇文章的生成過程以下:

  1. 選擇單詞數N服從泊松分佈,,
  2. 選擇服從狄利克雷分佈,,
  3. 對於N個單詞中的每一個單詞 a. 選擇一個主題,服從多項分佈, b. 以機率生成單詞,其中表示在主題上的條件多項式機率。

在LDA中咱們能夠利用來表示一篇文檔。

應用

從以前LDA的闡述中,咱們能夠利用theta來表示一篇文檔,那麼咱們天然能夠利用這個向量對文檔進行語義層面的詞語和文檔的類似性分析從而達到聚類、推薦的效果。固然了LDA自己對於文檔分析出的主題,以及每一個主題下的詞彙,就是對於文檔詞彙的一層低維聚類。
以前用過Git上Java版的LDA實現,可是語料不是很大,對其性能並不能作出很好的評估。其地址以下:
github: A Java implemention of LDA(Latent Dirichlet Allocation)

public static void main(String[] args)
{
    // 1. Load corpus from disk
    Corpus corpus = Corpus.load("data/mini");
    // 2. Create a LDA sampler
    LdaGibbsSampler ldaGibbsSampler = new LdaGibbsSampler(corpus.getDocument(), corpus.getVocabularySize());
    // 3. Train it
    ldaGibbsSampler.gibbs(10);
    // 4. The phi matrix is a LDA model, you can use LdaUtil to explain it.
    double[][] phi = ldaGibbsSampler.getPhi();
    Map<String, Double>[] topicMap = LdaUtil.translate(phi, corpus.getVocabulary(), 10);
    LdaUtil.explain(topicMap);
}

其採用吉布斯採樣的方法對LDA進行求解。以後本身也準備嘗試用spark進行實現,看是否可以對性能進行優化。

Word2Vec

谷歌的Tomas Mikolov團隊開發了一種詞典和術語表的自動生成技術,可以把一種語言轉變成另外一種語言。該技術利用數據挖掘來構建兩種語言的結構模型,而後加以對比。每種語言詞語之間的關係集合即「語言空間」,能夠被表徵爲數學意義上的向量集合。在向量空間內,不一樣的語言享有許多共性,只要實現一個向量空間向另外一個的映射和轉換,語言翻譯便可實現。該技術效果很是不錯,對英語和西語間的翻譯準確率高達90%。

什麼是word2vec?你能夠理解爲word2vec就是將詞表徵爲實數值向量的一種高效的算法模型,其利用神經網絡(關於神經網絡以前有簡單進行整理:馬里奧AI實現方式探索 ——神經網絡+加強學習),能夠經過訓練,把對文本內容的處理簡化爲K維向量空間中的向量運算,而向量空間上的類似度能夠用來表示文本語義上的類似。(PS. 這裏每每人們會將word2vec和深度學習掛鉤,但其實word2vec僅僅只是用了一個很是淺層的神經網絡,跟深度學習的關係並不大。)
Word2vec輸出的詞向量能夠被用來作不少NLP相關的工做,好比聚類、找同義詞、詞性分析等等。若是換個思路, 把詞當作特徵,那麼Word2vec就能夠把特徵映射到K維向量空間,能夠爲文本數據尋求更加深層次的特徵表示 。

神經網絡語言模型

word2vec的思想最先起源於2003年Yoshua Bengio等人的論文A Neural Probabilistic Language Model

Traditional but very successful approaches based on n-grams obtain generalization by concatenating very short overlapping sequences seen in the training set. We propose to fight the curse of dimensionality by learning a distributed representation for words which allows each training sentence to inform the model about an exponential number of semantically neighboring
sentences. [16]

從文中摘要中的這段話咱們能夠看出,神經網絡語言模型提出的初衷即是爲了解決傳統的n-gram模型中維度災難的問題,用distributed representation詞向量的形式來表示每個詞語。
文中提出的模型利用了一個三層神經網絡以下圖(通常投影層算在輸入層中,這裏分開闡述):

其中,對於語料庫C,詞典D的長度爲(|D|=N)爲語料庫C的詞彙量大小。對於任意一個詞omegaContext(omega)表示其前n-1個詞語,相似於n-gram模型,二元對(Context(omega),omega)爲一個訓練樣本。咱們v(omega)爲詞向量,詞向量的維度爲m。圖中W,U分別爲投影層和隱藏層以及隱藏層和輸出層之間的權值矩陣,p,q分別爲隱藏層和輸出層上的偏置向量。
論文中給出的神經網絡模型以下圖:

其中C(i)表示第i個詞的特徵向量(詞向量),咱們看到圖中第一層爲詞omega的上下文Context(omega)的每一個詞向量,在第二層咱們將輸入層的n-1個詞向量按順序首尾拼接在一塊兒,造成一個長向量,其長度爲(n-1)m,輸入到激活函數tanh雙曲正切函數中,計算方式以下:

通過上述兩步計算獲得的只是一個長度爲N的向量,咱們看到圖中第三層還作了一次softmax(Softmax function)歸一化,歸一化後
就能夠表示爲:

i_omega爲詞omega在詞典D中的索引。
在以前的背景知識n-gram模型

咱們知道語言模型中很關鍵的即是F的肯定,其中參數theta以下:

  • 詞向量:,以及填充向量(上下文詞彙不夠n時)
  • 神經網絡參數:

論文的主要貢獻有一下兩點:

  1. 詞語之間的類似性能夠經過詞向量來表示
    不一樣於以前咱們討論的One-hot Representation表示方式,論文中指出在進行訓練時,向量空間表達的詞語維度通常爲30、60或100,遠遠小於詞典長度17000,避免了維度災難。同時語義類似句子的機率是類似的。好比:某個語料庫中的兩個句子S1="A dog is running in the room", S2="A cat is running in the room",兩個句子從語義上看僅僅是在dog和cat處有一點區別,假設在語料庫中S1=1000即出現1000次而S2=1即僅出現一次,按照以前咱們講述的n-gram模型,p(S1)>>p(S2),可是咱們從語義上來看dog和cat在句子中不管從句法仍是語義上都扮演了類似的角色,因此二者機率應該類似纔對。
    而神經網絡語言模型能夠作到這一點,緣由是:1)在神經網絡語言模型中假設了類似的詞在詞向量上也是類似的,即向量空間中的距離相近,2)模型中的機率函數關於詞向量是光滑的,那麼詞向量的一個小變化對機率的影響也是一個小變化,這樣下面的句子:

    A dog is ruuning in the room
    A cat is running in the room
    The cat is running in the room
    A dog is walking in the bedroom
    The dog was walking in the bedroom

只要在語料庫中出現一個,其餘句子的機率也會相應增大。

  1. 基於詞向量的模型在機率計算上已是平滑的,不須要像n-gram模型同樣作額外的平滑處理,由於在softmax階段咱們已經作了歸一化,有了平滑性。

咱們最終訓練獲得的詞向量,在整個神經網絡模型中,彷佛只是一個參數,可是這個反作用也正是word2vec中的核心產物。

CBOW和Skip-gram模型

word2vec中用到了兩個重要模型:CBOW(Continuous Bag-of-Words Model)和Skip-gram(Continuous Skip-gram Model)模型,文中做者Tomas Mikolov[17]給出了模型圖以下:

由圖中咱們看出word2vec是一個三層結構的神經網絡:輸入層、投影層和輸出層(這裏咱們發現word2vec與上面咱們闡述的神經網絡模型的顯著區別是去掉了隱藏層)。對於圖中左邊的CBOW模型,是已知當前詞omega_t的上下文的前提下預測當前詞omega_t;而正好相反,Skip-gram模型是已知當前詞omega_t的前提下來預測其上下文
CBOW模型的目標函數,即其對數似然函數形式以下:

而Skip-gram模型的優化目標函數則形如:

Mikolov在word2vec中提出了兩套框架,Hieraichical Softmax和Negative Sampling,這裏因爲博文篇幅太長了,就不錯過多闡述,只對基於Hieraichical Softmax的CBOW模型進行簡單總結。
CBOW模型中,與以前神經網絡語言模型相似(Context(omega),omega)表示一個樣本,其中Context(omega)表示詞omega的先後各c個詞語(共2c個),其三層結構咱們能夠細化以下:

  1. 輸入層:包含(Context(omega),omega)中2c個詞的詞向量,每一個詞向量的維度都是m
  2. 投影層:將輸入層的2c個詞向量作求和累加,即
  3. 輸出層:輸出層對應一顆二叉樹,它是以語料中出現過的詞做爲葉子節點,以各詞在語料中出現的次數做爲權重構造出來的一顆Huffman樹(Huffman coding),其葉子節點共N(=|D|)個對應語料庫D中的各個詞,非葉子節點爲N-1個。

對比咱們以前討論的最先的神經網絡語言模型,CBOW模型的區別主要爲如下三點:

  1. 從輸入層到投影層的操做,前者經過拼接,然後者經過累加求和
  2. 前者有隱藏層,後者無隱藏層
  3. 輸出層前者是線性結構(softmax),後者是樹形結構(Hierarchical softmax)

word2vec對於詞典D中的任意詞omega,Huffman樹必存在一條從根結點到詞omega的路徑p_omega(且惟一)。路徑p_omega上存在l_omega-1個分支(每條路徑上的總結點數爲l_omega),將每一個分支看作一次二次分類,每一次分類產生一個機率,將這些機率乘起來,即是所需的。在二分類的過程當中,能夠利用Huffman編碼值,即左樹爲1右樹爲0進行邏輯迴歸分類。
word2vec在求解的過程當中主要利用了梯度降低的方法,調整學習率eta,這裏咱們再也不長篇大論的闡述,具體能夠參考文獻[14],對word2vec中的數學原理闡述的很是清晰。

應用

word2vec從被髮布起就是各類大紅大紫,在谷歌的翻譯系統中,獲得了很好的驗證。圍繞本篇博文的主題,即文本類似度的度量,word2vec產生的詞向量能夠很是方便的讓咱們作這件事情,利用歐氏距離或者cos均可以。
在以前Wetest輿情項目,作句法分析時,須要找尋某一個詞的同類詞語,咱們用用戶的遊戲評論訓練word2vec,效果仍是不錯的以下圖:

對於遊戲的人工想到的維度詞進行同類擴展,獲得擴展維度詞。
以前在應用時是本身師兄使用的python版word2vec,而Java對於word2vec有一個較好的東東DL4J,但其性能我並無通過大規模預料測試,這個你們用的時候需謹慎。

OK,長舒一口氣~,好長的一篇整理,整個文章雖然涵蓋了好多個模型、算法,可是圍繞的一個主題即是如何度量兩個文本之間的類似性,從字面和語義兩個角度對本身平時用過接觸過的模型算法進行整理概括,若有任何紕漏還請留言指出,我會第一時間改正。(感謝身邊的同事和大神給予的指導幫助)

參考文獻

  1. 萊文斯坦距離
  2. Commons Lang
  3. Jaro–Winkler distance
  4. 字符串類似算法-(1) Jaro-Winkler Distance
  5. Probabilistic Latent Semantic Indexing Thomas Hofmann
  6. [Algorithm & NLP] 文本深度表示模型——word2vec&doc2vec詞向量模型
  7. 數學之美番外篇:平凡而又神奇的貝葉斯方法
  8. 機率語言模型及其變形系列(1)-PLSA及EM算法
  9. 機率語言模型及其變形系列(2)-LDA及Gibbs Sampling
  10. [Algorithm] 使用SimHash進行海量文本去重
  11. 海量數據類似度計算之simhash短文本查找
  12. word2vec 中的數學原理詳解
  13. DL4J
  14. 機器翻譯領域的新突破
  15. word2vec 中的數學原理詳解
  16. 《統計天然語言處理第2版》 宗成慶
  17. A Neural Probabilistic Language Model
  18. Exploiting Similarities among Languages for Machine Translation
相關文章
相關標籤/搜索