類似度計算方面java
Jaccard類似度:集合之間的Jaccard類似度等於交集大小與並集大小的比例。適合的應用包括文檔文本類似度以及顧客購物習慣的類似度計算等。異步
Shingling:k-shingle是指文檔中連續出現的任意k個字符。若是將文檔表示成其k-shingle集合,那麼就能夠基於集合之間的 Jaccard類似度來計算文檔之間的文本類似度。有時,將shingle哈希成更短的位串很是有用,能夠基於這些哈希值的集合來表示文檔。函數
最小哈希:集合上的最小哈希函數基於全集上的排序轉換來定義。給定任意一個排列轉換,集合的最小哈希值爲在排列轉換次序下出現的第一個集合元素。優化
最小哈希簽名:能夠選出多個排列轉換,而後在每一個排列轉換下計算集合的最小哈希值,這些最小哈希值序列構成集合的最小哈希簽名。給定兩個集合,產生相同哈希值的排列轉換所佔的指望比率正好等於集合之間的Jaccard類似度。編碼
高效最小哈希:因爲實際不可能產生隨機的排列轉換,所以一般會經過下列方法模擬一個排列轉換:選擇一個隨機哈希函數,利用該函數對集合中全部的元素進行哈希操做,其中獲得的最小值當作是集合的最小哈希值。spa
簽名的局部敏感哈希:該技術能夠容許咱們避免計算全部集合對或其最小哈希簽名對之間的類似度。給定集合的簽名,咱們能夠將它們劃分紅行條,而後僅僅計算至少有一個行條相等的集合對之間的類似度。經過合理選擇行條大小,能夠消除不知足類似度閾值的大部分集合對之間的比較。code
向量空間距離方面排序
歐式距離:n維空間下的歐式距離,是兩個點在各維上差值的平方和的算數平方根。適合歐式空間的另外一個距離是曼哈頓距離,指兩個點各維度的差的絕對值之和。索引
Jaccard距離:1減去Jaccard類似度也是一個距離測度。ci
餘弦距離:向量空間下兩個向量的夾角大小。
編輯距離:該距離測度應用於字符串,指的是經過須要的插入、刪除操做將一個字符串處理成另外一個字符串的操做次數。編輯距離還能夠經過兩個字符串長度之和減去二者最長公共子序列長度的兩倍來計算。
海明距離:應用於向量空間。兩個向量之間的海明距離計算的是它們之間不相同的位置個數。
索引輔助方面
字符索引:若是將集合表示成字符串,且須要達到的類似度閾值接近1。那麼就能夠將每一個字符串按照其頭部的一小部分字母創建索引。須要索引的前綴的長度大概等於整個字符串的長度乘以給定的最大的Jaccard距離。
位置索引:咱們不只能夠給出索引字符串前綴中的字符,也能夠索引其在前綴中的位置。若是兩個字符串共有的一個字符並不出如今雙方的第一個位置,那麼咱們就知道要麼存在某些前面的字符出如今並集但不出如今交集中,那麼在兩個字符串中存在一個更前面的公共字符。這樣的話,咱們就能夠減小須要比較的字符串對數目。
後綴索引:咱們不只能夠索引字符串前綴中的字符及其位置,還能夠索引當前字符後綴的長度,即字符串中該字符以後的位置數量。因爲相同字符可是後綴長度不一樣意味着有額外的字符必須出如今並集但不出如今交集中,所以上述結構可以進一步減小須要比較的字符串數目。
總結
以上的一些概念和方法能夠配合使用,能夠基本知足許多場景下的類似度計算。類似度計算又能夠爲相關推薦作基礎。怎麼作好詞的粒度切分,怎麼劃定閾值,選擇何種距離測算,如何優化實現方法仍是要下不少功夫的。
兩個例子
Levenshtein實際上是編輯距離,下面計算編輯距離的方法是把兩個String串裏的字/詞當成一個矩陣來比較和計算。
public class LevenshteinDis { public static void main(String[] args) { // 要比較的兩個字符串 String str1 = "類似度計算方法"; String str2 = "文本類似項發現"; levenshtein(str1, str2); } public static void levenshtein(String str1, String str2) { int len1 = str1.length(); int len2 = str2.length(); int[][] dif = new int[len1 + 1][len2 + 1]; for (int a = 0; a <= len1; a++) { dif[a][0] = a; } for (int a = 0; a <= len2; a++) { dif[0][a] = a; } int temp; for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { if (str1.charAt(i - 1) == str2.charAt(j - 1)) { temp = 0; } else { temp = 1; } // 取三個值中最小的 dif[i][j] = min(dif[i - 1][j - 1] + temp, dif[i][j - 1] + 1, dif[i - 1][j] + 1); } } System.out.println("字符串\"" + str1 + "\"與\"" + str2 + "\"的比較"); System.out.println("差別步驟:" + dif[len1][len2]); // 計算類似度 float similarity = 1 - (float) dif[len1][len2] / Math.max(str1.length(), str2.length()); System.out.println("類似度:" + similarity); } private static int min(int... is) { int min = Integer.MAX_VALUE; for (int i : is) { if (min > i) { min = i; } } return min; } }
下面是餘弦距離計算的例子:
public class CosineSimilarAlgorithm { public static double getSimilarity(String doc1, String doc2) { if (doc1 != null && doc1.trim().length() > 0 && doc2 != null && doc2.trim().length() > 0) { Map<Integer, int[]> AlgorithmMap = new HashMap<Integer, int[]>(); //將兩個字符串中的中文字符以及出現的總數封裝到,AlgorithmMap中 for (int i = 0; i < doc1.length(); i++) { char d1 = doc1.charAt(i); if(isHanZi(d1)){ int charIndex = getGB2312Id(d1); if(charIndex != -1){ int[] fq = AlgorithmMap.get(charIndex); if(fq != null && fq.length == 2){ fq[0]++; }else { fq = new int[2]; fq[0] = 1; fq[1] = 0; AlgorithmMap.put(charIndex, fq); } } } } for (int i = 0; i < doc2.length(); i++) { char d2 = doc2.charAt(i); if(isHanZi(d2)){ int charIndex = getGB2312Id(d2); if(charIndex != -1){ int[] fq = AlgorithmMap.get(charIndex); if(fq != null && fq.length == 2){ fq[1]++; }else { fq = new int[2]; fq[0] = 0; fq[1] = 1; AlgorithmMap.put(charIndex, fq); } } } } Iterator<Integer> iterator = AlgorithmMap.keySet().iterator(); double sqdoc1 = 0; double sqdoc2 = 0; double denominator = 0; while(iterator.hasNext()){ int[] c = AlgorithmMap.get(iterator.next()); denominator += c[0]*c[1]; sqdoc1 += c[0]*c[0]; sqdoc2 += c[1]*c[1]; } return denominator / Math.sqrt(sqdoc1*sqdoc2); } else { throw new NullPointerException( " the Document is null or have not cahrs!!"); } } public static boolean isHanZi(char ch) { // 判斷是否漢字 return (ch >= 0x4E00 && ch <= 0x9FA5); } /** * 根據輸入的Unicode字符,獲取它的GB2312編碼或者ascii編碼, * * @param ch * 輸入的GB2312中文字符或者ASCII字符(128個) * @return ch在GB2312中的位置,-1表示該字符不認識 */ public static short getGB2312Id(char ch) { try { byte[] buffer = Character.toString(ch).getBytes("GB2312"); if (buffer.length != 2) { // 正常狀況下buffer應該是兩個字節,不然說明ch不屬於GB2312編碼,故返回'?',此時說明不認識該字符 return -1; } int b0 = (int) (buffer[0] & 0x0FF) - 161; // 編碼從A1開始,所以減去0xA1=161 int b1 = (int) (buffer[1] & 0x0FF) - 161; // 第一個字符和最後一個字符沒有漢字,所以每一個區只收16*6-2=94個漢字 return (short) (b0 * 94 + b1); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return -1; } }