基於同義詞詞林擴展版的詞語類似度計算

詞語類似度計算

詞義類似度計算在不少領域中都有普遍的應用,例如信息檢索、信息抽取、文本分類、詞義排歧、基於實例的機器翻譯等等。國內目前主要是使用知網和同義詞詞林來進行詞語的類似度計算。java

本文主要是根據《基於同義詞詞林的詞語類似度計算方法—田久樂》論文中所提出的分層算法實現類似度計算,程序採用Java語言編寫。git

同義詞詞林擴展版

《同義詞詞林》是梅家駒等人於1983年編纂而成,這本詞典中不只包括了一個詞語的同義詞,也包含了必定數量的同類詞,即廣義的相關詞。《同義詞詞林擴展版》是由哈爾濱工業大學信息檢索實驗室所從新修訂所得。該版收錄詞語近7萬條,所有按意義進行編排,是一部同義類詞典。github

同義詞詞林按照樹狀的層次結構把全部收錄的詞條組織到一塊兒,把詞彙分紅大、中、小三類,大類有12個,中類有97個,小類有1400個。每一個小類裏都有不少的詞,這些詞又根據詞義的遠近和相關性分紅了若干個詞羣(段落)。每一個段落中的詞語又進一步分紅了若干個行,同一行的詞語要麼詞義相同,要麼詞義有很強的相關性。web

《同義詞詞林》提供了5層編碼,第1級用大寫英文字母表示;第2級用小寫英文字母表示;第3級用二位十進制整數表示;第4級用大寫英文字母表示;第5級用二位十進制整數表示。例如:
Cb30A01= 這裏 這邊 此地 此間 此處 此
Cb30A02# 該地 該鎮 該鄉 該站 該區 該市 該村
Cb30A03@ 這方算法

分層及編碼表以下所示

apache

因爲第5級有的行是同義詞,有的行是相關詞,有的行只有一個詞,分類結果須要特別說明,能夠分出具體的3種狀況。使用特殊符號對這3種狀況進行區別對待,因此第8位的標記有3種,分別是「=」表明「相等」、「同義」;「#」表明「不等」、「同類」,屬於相關詞語;「@」表明「自我封閉」、「獨立」,它在詞典中既沒有同義詞,也沒有相關詞。app

在對文本內容進行類似度計算中,採用該論文中給出的計算公式,兩個義項的類似度用Sim表示
若兩個義項不在同一棵樹上,Sim(A,B)=f
若兩個義項在同一棵樹上:
若在第2層分支,係數爲a Sim(A,B)=1*a*cos*(n*π/180)((n-k+1)/n)
若在第3層分支,係數爲b Sim(A,B)=1*1*b*cos*(n*π/180)((n-k+1)/n)
若在第4層分支,係數爲c Sim(A,B)=1*1*1*c×cos*(n*π/180)((n-k+1)/n)
若在第5層分支,係數爲d Sim(A,B)=1*1*1*1*d*cos*(n*π/180)((n-k+1)/n)函數

當編碼相同,而只有末尾是「#」時,那麼認爲其類似度爲e。
例如Ad02B04# 非洲人 亞洲人 則其類似度爲e。測試

其中n是分支層的節點分支總數,k是兩個分支間的距離。
如:人 Aa01A01= 和 少兒 Ab04B01=
以A開頭的子分支只有14個,分別是Aa*——An*,而不是以A開頭的全部結點的個數;在第2層,人的編碼是a,少兒的編碼是b因此k=1。ui

該文獻中給出的參數值爲a=0.65b=0.8c=0.9d=0.96e=0.5f=0.1

如:人 Aa01A01= 和 少兒 Ab04B01=因爲A開頭的分支個數爲14個,因此n=14;在第2層,人的編碼是a,少兒的編碼是b因此k=1。

Java代碼實現

package edu.shu.similarity.cilin;

import com.google.common.base.Preconditions;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

import static java.lang.Math.PI;
import static java.lang.Math.cos;

/**
* <p>
* Created with IntelliJ IDEA. 2015/8/2 21:54
* </p>
* <p>
* ClassName:WordSimilarity
* </p>
* <p>
* Description:<br/>
* "=" 表明 相等 同義 <br/>
* "#" 表明 不等 同類 屬於相關詞語 <br/>
* "@" 表明 自我封閉 獨立 它在詞典中既沒有同義詞, 也沒有相關詞 <br/>
* </P>
*
* @author Wang Xu
* @version V1.0.0
* @since V1.0.0
*/

@Log4j2
//注意使用Log4j2的註解,那麼在pom中必須引入2.x版本的log4j,若是使用Log4j註解,pom中引入1.x版本的log4j
//相應的配置文件也要一致,2.x版本配置文件爲log4j2.xml,1.x版本配置文件爲log4j.xml
public class WordSimilarity {
   /**
    * when we use Lombok's Annotation, such as @Log4j
    *
    * @Log4j <br/>
    * public class LogExample {
    * }
    * <p>
    * will generate:
    * public class LogExample {
    * private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.Logger.getLogger(LogExample.class);
    * }
    * </p>
    */


   //定義一些常數先
   private final double a = 0.65;
   private final double b = 0.8;
   private final double c = 0.9;
   private final double d = 0.96;
   private final double e = 0.5;
   private final double f = 0.1;

   private final double degrees = 180;


   //存放的是以詞爲key,以該詞的編碼爲values的List集合,其中一個詞可能會有多個編碼
   private static Map<String, ArrayList<String>> wordsEncode = new HashMap<String, ArrayList<String>>();
   //存放的是以編碼爲key,以該編碼多對應的詞爲values的List集合,其中一個編碼可能會有多個詞
   private static Map<String, ArrayList<String>> encodeWords = new HashMap<String, ArrayList<String>>();

   /**
    * 讀取同義詞詞林並將其注入wordsEncode和encodeWords
    */

   private static void readCiLin() {
       InputStream input = WordSimilarity.class.getClassLoader().getResourceAsStream("cilin.txt");
       List<String> contents = null;
       try {
           contents = IOUtils.readLines(input);
       } catch (IOException e) {
           e.printStackTrace();
           log.error(e.getMessage());
       }
       for (String content : contents) {
           content = Preconditions.checkNotNull(content);
           String[] strsArr = content.split(" ");
           String[] strs = Preconditions.checkNotNull(strsArr);
           String encode = null;
           int length = strs.length;
           if (length > 1) {
               encode = strs[0];//獲取編碼
           }
           ArrayList<String> encodeWords_values = new ArrayList<String>();
           for (int i = 1; i < length; i++) {
               encodeWords_values.add(strs[i]);
           }
           encodeWords.put(encode, encodeWords_values);//以編碼爲key,其後全部值爲value
           for (int i = 1; i < length; i++) {
               String key = strs[i];
               if (wordsEncode.containsKey(strs[i])) {
                   ArrayList<String> values = wordsEncode.get(key);
                   values.add(encode);
                   //從新放置回去
                   wordsEncode.put(key, values);//以某個value爲key,其可能的全部編碼爲value
               } else {
                   ArrayList<String> temp = new ArrayList<String>();
                   temp.add(encode);
                   wordsEncode.put(key, temp);
               }
           }
       }
   }

   /**
    * 對外暴露的接口,返回兩個詞的類似度的計算結果
    *
    * @param word1
    * @param word2
    * @return 類似度值
    */

   public double getSimilarity(String word1, String word2) {
       //若是比較詞沒有出如今同義詞詞林中,則類似度爲0
       if (!wordsEncode.containsKey(word1) || !wordsEncode.containsKey(word2)) {
           return 0;
       }
       //獲取第一個詞的編碼
       ArrayList<String> encode1 = getEncode(word1);
       //獲取第二個詞的編碼
       ArrayList<String> encode2 = getEncode(word2);

       double maxValue = 0;//最終的計算結果值,取全部類似度裏面結果最大的那個
       for (String e1 : encode1) {
           for (String e2 : encode2) {
               log.info(e1);
               log.info(e2);
               String commonStr = getCommonStr(e1, e2);
               int length = StringUtils.length(commonStr);
               double k = getK(e1, e2);
               double n = getN(commonStr);
               log.info("k--" + k);
               log.info("n--" + n);
               log.info("length--" + length);
               double res = 0;
               //若是有一個以「@」那麼表示自我封閉,確定不在一棵樹上,直接返回f
               if (e1.endsWith("@") || e2.endsWith("@") || 0 == length) {
                   if (f > maxValue) {
                       maxValue = f;
                   }
                   continue;
               }
               if (1 == length) {
                   //說明在第二層上計算
                   res = a * cos(n * PI / degrees) * ((n - k + 1) / n);
               } else if (2 == length) {
                   //說明在第三層上計算
                   res = b * cos(n * PI / degrees) * ((n - k + 1) / n);
               } else if (4 == length) {
                   //說明在第四層上計算
                   res = c * cos(n * PI / degrees) * ((n - k + 1) / n);
               } else if (5 == length) {
                   //說明在第五層上計算
                   res = d * cos(n * PI / degrees) * ((n - k + 1) / n);
               } else {
                   //注意不存在前面七個字符相同,而結尾不一樣的狀況,因此這個分支必定是8個字符都相同,那麼只需比較結尾便可
                   if (e1.endsWith("=") && e2.endsWith("=")) {
                       //說明兩個徹底相同
                       res = 1;
                   } else if (e1.endsWith("#") && e2.endsWith("#")) {
                       //只有結尾不一樣,說明結尾是「#」
                       res = e;
                   }
               }
               log.info("res :" + res);
               if (res > maxValue) {
                   maxValue = res;
               }
           }
       }
       return maxValue;
   }

   /**
    * 判斷一個詞在同義詞詞林中是不是自我封閉的,是不是獨立的
    *
    * @param source
    * @return
    */

   private boolean isIndependent(String source) {
       Iterator<String> iter = wordsEncode.keySet().iterator();
       while (iter.hasNext()) {
           String key = iter.next();
           if (StringUtils.equalsIgnoreCase(key, source)) {
               ArrayList<String> values = wordsEncode.get(key);
               for (String value : values) {
                   if (value.endsWith("@")) {
                       return true;
                   }
               }
           }

       }
       return false;
   }

   /**
    * 根據word的內容,返回其對應的編碼
    *
    * @param word
    * @return
    */

   public ArrayList<String> getEncode(String word) {
       return wordsEncode.get(word);
   }

   /**
    * 計算N的值,N表示所在分支層分支數,如:人 Aa01A01= 和 少兒 Ab04B01=,以A開頭的子分支只有14個
    * 這一點在論文中說的很是不清晰,因此以國人的文章進行編碼真是痛苦
    *
    * @param encodeHead 輸入兩個字符串的公共開頭
    * @return 通過計算以後獲得N的值
    */

   public int getN(String encodeHead) {
       int length = StringUtils.length(encodeHead);
       switch (length) {
           case 1:
               return getCount(encodeHead, 2);
           case 2:
               return getCount(encodeHead, 4);
           case 4:
               return getCount(encodeHead, 5);
           case 5:
               return getCount(encodeHead, 7);
           default:
               return 0;
       }
   }

   public int getCount(String encodeHead, int end) {
       Set<String> res = new HashSet<String>();
       Iterator<String> iter = encodeWords.keySet().iterator();
       while (iter.hasNext()) {
           String curr = iter.next();
           if (curr.startsWith(encodeHead)) {
               String temp = curr.substring(0, end);
               if (res.contains(temp)) {
                   continue;
               } else {
                   res.add(temp);
               }
           }
       }
       return res.size();
   }

   /**
    * @param encode1 第一個編碼
    * @param encode2 第二個編碼
    * @return 這兩個編碼對應的分支間的距離,用k表示
    */

   public int getK(String encode1, String encode2) {
       String temp1 = encode1.substring(0, 1);
       String temp2 = encode2.substring(0, 1);
       if (StringUtils.equalsIgnoreCase(temp1, temp2)) {
           temp1 = encode1.substring(1, 2);
           temp2 = encode2.substring(1, 2);
       } else {
           return Math.abs(temp1.charAt(0) - temp2.charAt(0));
       }
       if (StringUtils.equalsIgnoreCase(temp1, temp2)) {
           temp1 = encode1.substring(2, 4);
           temp2 = encode2.substring(2, 4);
       } else {
           return Math.abs(temp1.charAt(0) - temp2.charAt(0));
       }
       if (StringUtils.equalsIgnoreCase(temp1, temp2)) {
           temp1 = encode1.substring(4, 5);
           temp2 = encode2.substring(4, 5);
       } else {
           return Math.abs(Integer.valueOf(temp1) - Integer.valueOf(temp2));
       }
       if (StringUtils.equalsIgnoreCase(temp1, temp2)) {
           temp1 = encode1.substring(5, 7);
           temp2 = encode2.substring(5, 7);
       } else {
           return Math.abs(temp1.charAt(0) - temp2.charAt(0));
       }
       return Math.abs(Integer.valueOf(temp1) - Integer.valueOf(temp2));
   }

   /**
    * 獲取編碼的公共部分字符串
    *
    * @param encode1
    * @param encode2
    * @return
    */

   public String getCommonStr(String encode1, String encode2) {
       int length = StringUtils.length(encode1);
       StringBuilder sb = new StringBuilder();

       for (int i = 0; i < length; i++) {
           if (encode1.charAt(i) == encode2.charAt(i)) {
               sb.append(encode1.charAt(i));
           } else {
               break;
           }
       }
       int sbLen = StringUtils.length(sb);
       //注意第三層和第五層均有兩個字符,因此長度不可能出現3和6的狀況
       if (sbLen == 3 || sbLen == 6) {
           sb.deleteCharAt(sbLen - 1);
       }

       return String.valueOf(sb);
   }

   @Test
   public void testGetN() {
       readCiLin();
       int a = getN("A");
       System.out.println(a);
   }

   @Test
   public void testGetK() {
       int k = getK("Aa01A01=", "Aa01A01=");
       System.out.println(k);
   }

   @Test
   public void testGetCommonStr() {
       String commonStr = getCommonStr("Aa01A01=", "Aa01A03=");
       System.out.println(commonStr);
   }

   @Test
   public void testGetSimilarity() {
       readCiLin();
       double similarity = getSimilarity("人民", "國民");
       System.out.println("人民--" + "國民:" + similarity);
       similarity = getSimilarity("人民", "羣衆");
       System.out.println("人民--" + "羣衆:" + similarity);
       similarity = getSimilarity("人民", "黨羣");
       System.out.println("人民--" + "黨羣:" + similarity);
       similarity = getSimilarity("人民", "良民");
       System.out.println("人民--" + "良民:" + similarity);
       similarity = getSimilarity("人民", "同志");
       System.out.println("人民--" + "同志:" + similarity);
       similarity = getSimilarity("人民", "成年人");
       System.out.println("人民--" + "成年人:" + similarity);
       similarity = getSimilarity("人民", "市民");
       System.out.println("人民--" + "市民:" + similarity);
       similarity = getSimilarity("人民", "親屬");
       System.out.println("人民--" + "親屬:" + similarity);
       similarity = getSimilarity("人民", "志願者");
       System.out.println("人民--" + "志願者:" + similarity);
       similarity = getSimilarity("人民", "先鋒");
       System.out.println("人民--" + "先鋒:" + similarity);
   }

   @Test
   public void testGetSimilarity2() {
       readCiLin();
       double similarity = getSimilarity("非洲人", "亞洲人");
       System.out.println(similarity);
       double similarity1 = getSimilarity("驕傲", "仔細");
       System.out.println(similarity1);
   }

}

說明,詞語類似度是個數值,通常取值範圍在[0,1]之間,在原論文中,使用cos函數計算主要是將值歸一化到[0,1]之間,能夠將cos函數看做是一個調節因子。

testGetSimilarity的測試結果以下所示:

人民--國民:1.0
人民--羣衆:0.9576614882494312
人民--黨羣:0.8978076452338418
人民--良民:0.7182461161870735
人民--同志:0.6630145969121822
人民--成年人:0.6306922220793977
人民--市民:0.5405933332109123
人民--親屬:0.36039555547394153
人民--志願者:0.22524722217121346
人民--先鋒:0.18019777773697077

本文使用的是同義詞詞林的擴展版,而原論文使用的是同義詞詞林,因爲二者存在微小差距,因此本文計算結果與論文中的計算結果存在稍許偏差,若是算法沒錯,這是能夠理解的!

以上僅爲我的理解,如若發現錯誤,歡迎你們積極留言指正!

目前整個項目已經推送到GitHub上了,地址點我。注意在文章末尾所注的參考資料中的連接裏面的計算方法在求n的時候存在錯誤,代碼我不肯定是不是其本身實現,望莫要受其誤導!

相關文章
相關標籤/搜索