本篇博客,主要是描述一种计算文本类似度的算法,基于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