SimHash算法原理与应用(Java版)
引言
項目中原使用的文本對比算法是使用MD5 Hash的方法。MD5 Hash算法簡單來說是指對于任何長度的文本都可生成一段128bit長度的字符串,相同文本生成的Hash字符串是相同的,因此可用來比較文本是否相同。
但這種傳統的Hash算法,對于文本的查找效率是很低的,另外文本間的相似度計算是很困難,因為即使改動文本的一個字符,得到的Hash結果也是完全不同的。因此在新項目中考慮用新的算法去做,對此作了一些技術調研,也收獲了一些更好的方法。接下來會在系列博客中總結一些成果。
簡要介紹
通過引言,我們已經知道傳統Hash的局限性,因此,接下來引入一個名詞“局部敏感哈希”。
與傳統的Hash不同,局部敏感哈希是一種解決在海量的高維數據集中查找與查詢數據點(query data point)近似最相鄰的某個或某些數據點的方法。常用的方法包括:歐式距離、余弦距離、海明距離、Jaccard相似度等。本篇博客將介紹的SimHash算法就屬于一種局部敏感哈希算法,利用海明距離比較內容之間的相似度。
SimHash是Google用來處理海量文本去重的算法。主要思想是降維,將高維的特征向量轉化為一個f位的指紋,通過算出兩個指紋的海明距離來確定文本的相似度,海明距離越小,相似度越低。
計算原理
核心代碼
1. 文本處理,過濾特殊標簽,符號統一為半角比較
/*** 全角轉半角** @param text* @return*/ public static String toDBC(String text) {char chars[] = text.toCharArray();for (int i = 0; i < chars.length; i++) {if (chars[i] == '\u3000') {chars[i] = ' ';} else if (chars[i] > '\uFF00' && chars[i] < '\uFF5F') {chars[i] = (char) (chars[i] - 65248);}}return new String(chars); }/*** 去除特殊符號* @param text 文本內容* @return*/ private String clearCharacters(String text) {// 將內容轉換為小寫text = StringUtils.lowerCase(text);// 過來HTML標簽text = Jsoup.clean(text, Whitelist.none());// 過濾特殊字符String[] strings = {" ", "\n", "\r", "\t", "\\r", "\\n", "\\t", " ", "&", "<", ">", """, "&qpos;"};for (String string : strings) {text = text.replaceAll(string, "");}//符號轉換text = toDBC(text);//去空格text = StringUtils.deleteWhitespace(text);return text; }2. 文本分詞,配置分詞權重,計算每個分詞的Hash值,合并分詞向量,得到Hash值
/*** 計算分詞Hash,合并分詞向量,得到文本Hash* @param word * @return*/ public BigInteger simHash() {// 對內容進行分詞處理List<Term> terms = StandardTokenizer.segment(this.text);// 配置詞性權重Map<String, Integer> weightMap = new HashMap<>(16, 0.75F);weightMap.put("n", 1);// 設置停用詞Map<String, String> stopMap = new HashMap<>(16, 0.75F);stopMap.put("w", "");// 設置超頻詞上線Integer overCount = 5;// 設置分詞統計量Map<String, Integer> wordMap = new HashMap<>(16, 0.75F);for (Term term : terms) {// 獲取分詞字符串String word = term.word;// 獲取分詞詞性String nature = term.nature.toString();// 過濾超頻詞if (wordMap.containsKey(word)) {Integer count = wordMap.get(word);if (count > overCount) {continue;} else {wordMap.put(word, count + 1);}} else {wordMap.put(word, 1);}// 過濾停用詞if (stopMap.containsKey(nature)) {continue;}// 計算單個分詞的Hash值BigInteger wordHash = this.countHash(word);for (int i = 0; i < this.hashCount; i++) {// 向量位移BigInteger bitMask = new BigInteger("1").shiftLeft(i);// 對每個分詞hash后的列進行判斷,例如:1000...1,則數組的第一位和末尾一位加1,中間的62位減一,也就是,逢1加1,逢0減1,一直到把所有的分詞hash列全部判斷完// 設置初始權重Integer weight = 1;if (weightMap.containsKey(nature)) {weight = weightMap.get(nature);}// 計算所有分詞的向量if (wordHash.and(bitMask).signum() != 0) {hashArray[i] += weight;} else {hashArray[i] -= weight;}}}// 生成指紋BigInteger fingerPrint = new BigInteger("0");for (int i = 0; i < this.hashCount; i++) {if (hashArray[i] >= 0) {fingerPrint = fingerPrint.add(new BigInteger("1").shiftLeft(i));}}return fingerPrint; }/*** 計算每個分詞的Hash* @param word * @return*/ private BigInteger countHash(String word) {if (StringUtils.isEmpty(word)) {// 如果分詞為null,則默認hash為0return new BigInteger("0");} else {// 分詞補位,如果過短會導致Hash算法失敗while (word.length() < SimHashUtil.WORD_MIN_LENGTH) {word = word + word.charAt(0);}// 分詞位運算char[] wordArray = word.toCharArray();BigInteger x = BigInteger.valueOf(wordArray[0] << 7);BigInteger m = new BigInteger("1000003");// 初始桶pow運算BigInteger mask = new BigInteger("2").pow(this.hashCount).subtract(new BigInteger("1"));for (char item : wordArray) {BigInteger temp = BigInteger.valueOf(item);x = x.multiply(m).xor(temp).and(mask);}x = x.xor(new BigInteger(String.valueOf(word.length())));if (x.equals(ILLEGAL_X)) {x = new BigInteger("-2");}return x;} }3. 獲取文本的海明距離
private int getHammingDistance(SimHashUtil simHashUtil) {// 求差集BigInteger subtract = new BigInteger("1").shiftLeft(this.hashCount).subtract(new BigInteger("1"));// 求異或BigInteger xor = this.bigSimHash.xor(simHashUtil.bigSimHash).and(subtract);int total = 0;while (xor.signum() != 0) {total += 1;xor = xor.and(xor.subtract(new BigInteger("1")));}return total; }4. 文本間海明距離的比較
public Double getSimilar(SimHashUtil simHashUtil) {// 獲取海明距離Double hammingDistance = (double) this.getHammingDistance(simHashUtil);// 求得海明距離百分比Double scale = (1 - hammingDistance / this.hashCount) * 100;Double formatScale = Double.parseDouble(String.format("%.2f", scale));return formatScale; }測試結果
對于任意一些文本,測試結果如下:
通過多次測試結果發現,該算法對于語意相同、文本較小差異的調整,比如文字順序的修改、個別字的增加刪減,得到的相似度結果都是百分之百。因此更適用于文本比較結果不要求每一個字符都精確完全相同的場景。
參考資料
局部敏感哈希介紹
使用SimHash進行海量文本去重
總結
以上是生活随笔為你收集整理的SimHash算法原理与应用(Java版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js获得相同css的第几个,vue,cs
- 下一篇: java美元兑换,(Java实现) 美元