本篇博客,主要是描述一種計算文本類似度的算法,基於TF-IDF算法和餘弦類似性。算法的描述請務必看阮一峯的博客,否則看不懂本篇博客,地址:html
http://www.ruanyifeng.com/blog/2013/03/tf-idf.htmljava
http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.htmlgit
在這裏,主要討論具體的代碼的實現。過程以下:算法
首先,請看此算法代碼的文件結構:網絡
接下來,是算法的實現步驟:url
/** * (1)使用TF-IDF算法,找出兩篇文章的關鍵詞; * * @param uri * 待比較的文本的路徑 * @return 文本被分詞後,詞的ELementSet集合 * @throws IOException */ private static ElementSet getKeyTerms(String uri) throws IOException { // 分詞後,得到ElementMap集合 ElementMap em = null; String text = Utils.readText(uri); em = Utils.tokenizer(text); ElementSet es = em.getElementSetOrderbyTf(); // 計算tf、idf和tf_idf的值 Double de = (double) ConnectKit.countTatolFromTerm("的")+1; for (Element e : es.getElementSet()) { // 計算tf Double tf = e.getTf() / es.getElementSet().size(); e.setTf(tf); // 計算idf Double num = (double) ConnectKit.countTatolFromTerm(e.getTerm()); Double idf = Math.log10(de / (num+1)); e.setIdf(idf); // 計算tf_idf Double tf_idf = tf * idf; e.setTf_idf(tf_idf); } // 將排序的依據更改成tf_idf es.orderbyTf_idf(); System.out.println("第一步:計算詞的tf、idf和tf_idf"); for (Element e : es.getElementSet()) { System.out.println(e.getTerm() + "---tf:" + e.getTf() + "---idf:" + e.getIdf() + "---tf_idf:" + e.getTf_idf()); } return es; }
其中的ElementSet和ElementMap是本身封裝的集合類(沒有繼承任何一個集合),分別使用Set和Map做爲其屬性,並提供二者相互轉換的方法。spa
/** * (2)每篇文章各取出若干個關鍵詞(好比20個),合併成一個集合, * 計算每篇文章對於這個集合中的詞的詞頻(爲了不文章長度的差別,可使用相對詞頻); (3)生成兩篇文章各自的詞頻向量; * * @param es01 * 詞的ElementSet的集合 * @param es02 * 詞的ElementSet的集合 * @param percent * 須要用於的計算的詞百分比(相對於全部的詞) * @return Vectors */ private static Vectors getVectors(ElementSet es01, ElementSet es02, int percent) { // (2)每篇文章各取出若干個關鍵詞(好比20個),合併成一個集合, // 計算每篇文章對於這個集合中的詞的詞頻(爲了不文章長度的差別,可使用相對詞頻); percent = Math.abs(percent); if (percent > 100) percent %= 100; int num01 = Math.round(es01.getSize() * ((float) percent / 100)); int num02 = Math.round(es02.getSize() * ((float) percent / 100)); if (num01 == 0) num01 = 1; if (num02 == 0) num02 = 1; HashSet<String> hs = new HashSet<String>(); Iterator<Element> it01 = es01.getElementSet().iterator(); for (int i = 0; i < num01; i++) { if (it01.hasNext()) { hs.add(it01.next().getTerm()); } } Iterator<Element> it02 = es02.getElementSet().iterator(); for (int i = 0; i < num02; i++) { if (it02.hasNext()) { hs.add(it02.next().getTerm()); } } // (3)生成兩篇文章各自的詞頻向量; ElementMap em01 = es01.getElementMap(); ElementMap em02 = es02.getElementMap(); List<Double> vector01 = new ArrayList<Double>(); List<Double> vector02 = new ArrayList<Double>(); for (String term : hs) { if (em01.getElementMap().containsKey(term)) { vector01.add(em01.getDataByTerm(term).getTf()); } else { vector01.add(0D); } if (em02.getElementMap().containsKey(term)) { vector02.add(em02.getDataByTerm(term).getTf()); } else { vector02.add(0D); } } System.out.println(); System.out.println("第二步:分別提取若干個關鍵字,並分別計算兩篇文章的詞頻向量"); for (Double d : vector01) { System.out.print(d + ","); } System.out.println(); for (Double d : vector02) { System.out.print(d + ","); } System.out.println(); System.out.println(); return new Vectors(vector01, vector02); }
其中,Vectors的屬性是兩個向量。code
/** * 計算餘弦類似度 * * @param vs * Vectors的實現 * @return 餘弦類似度的值 */ private static Double getCosSimilarty(Vectors vs) { List<Double> list1 = vs.getVector01(); List<Double> list2 = vs.getVector02(); Double countScores = 0D; Double element = 0D; Double denominator1 = 0D; Double denominator2 = 0D; int index = -1; for (Double it : list1) { index++; Double left = it.doubleValue(); Double right = list2.get(index).doubleValue(); element += left * right; denominator1 += left * left; denominator2 += right * right; } try { countScores = element / Math.sqrt(denominator1 * denominator2); } catch (ArithmeticException e) { e.printStackTrace(); } return countScores; }
到此,算法就結束了,可是還有兩個重要的點須要提一下,就是如何計算idf值和分詞:orm
/** * * @param url 訪問的地址 * @return 訪問的返回值 * @throws MalformedURLException * @throws IOException */ private static Long getTatolFromUrl(String url) throws MalformedURLException, IOException { InputStream instream = null; BufferedReader rd = null; Long tatol = -1L; try { instream = new URL(url).openStream(); rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8"))); //使用正則匹配,從網頁中獲取搜索數信息 Pattern pattern = Pattern.compile("百度爲您找到相關結果約[0-9-,]+個"); String s; while ((s = rd.readLine()) != null) { Matcher matcher = pattern.matcher(s); if (matcher.find()) { Pattern p = Pattern.compile("[0-9-,]+"); Matcher m = p.matcher(matcher.group()); if (m.find()) { String str = m.group().replace(",", ""); tatol = Long.valueOf(str); break; } } } } finally { instream.close(); rd.close(); } return tatol; } public static Long countTatolFromTerm(String term) throws IOException { int i = 0; while(true){ //由於有時訪問的內容是錯誤的,因此要屢次訪問,以獲得正確的結果,可是若是重複的次數過多的話,退出進程 if((i++)==100){ System.out.println("訪問百度失敗!!"); System.exit(0); }; //拼接訪問百度的URL Long answer = getTatolFromUrl("http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=" + term); if(answer != -1){ return answer; } } }
相似與爬蟲程序,可是隻是獲取搜索數。你也許會問,爲何idf要這麼計算,請看下圖,注意觀察,詞的搜索數和idf值之間的關係:htm
可見,但搜索量變少後,idf值會急劇增大,這樣能夠篩選出關鍵詞。固然,這樣的訪問網絡的操做,是很耗時的。
/** * 將傳入的文本進行分詞,而後詞(單個字要被過濾掉)放入ElementMap中 * @param text 須要分詞的文本 * @return 分好詞的ElementMap集合 * @throws IOException */ public static ElementMap tokenizer(String text) throws IOException { Map<String, Data> map = new TreeMap<String, Data>(); IKAnalyzer analyzer = new IKAnalyzer(); analyzer.setUseSmart(true); TokenStream stream = null; try { stream = analyzer.tokenStream("", new StringReader(text)); stream.reset(); while (stream.incrementToken()) { CharTermAttribute charTermAttribute = stream.getAttribute(CharTermAttribute.class); String term = charTermAttribute.toString(); if (term.length() > 1) { Data data = new Data(); data.setTf(1D); boolean isContainsKey = map.containsKey(term); if (isContainsKey) { Data temp = map.get(term); data.setTf(temp.getTf() + 1D); map.replace(term, temp, data); } map.put(term, data); } } } finally { stream.close(); analyzer.close(); } ElementMap em = new ElementMap(map); if(em.getElementMap().isEmpty()){ throw new NullPointerException(); } return em; }
分詞這部分可能不多接觸,這部分須要兩個jar包,在lib文件中,還可參考博客:
https://www.cnblogs.com/lyssym/p/4880896.html
本程序的部分代碼也是出自這篇博客。
到這裏,本篇博客結束,可是要強調一點,這個算法並不能用在實際的生產中,由於搜索數是從網絡獲取的,是一個很耗時的過程。本算法會進一步修改的,源碼地址(碼雲):
https://gitee.com/liutaigang/cosineSimilarty.git
經過git獲取。
end