文本類似度——基於TF-IDF與餘弦類似性

    本篇博客,主要是描述一種計算文本類似度的算法,基於TF-IDF算法和餘弦類似性。算法的描述請務必看阮一峯的博客,否則看不懂本篇博客,地址:html

http://www.ruanyifeng.com/blog/2013/03/tf-idf.htmljava

http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.htmlgit

在這裏,主要討論具體的代碼的實現。過程以下:算法

  1. 使用TF-IDF算法,找出兩篇文章的關鍵詞;
  2. 每篇文章各取出若干個關鍵詞(好比20個),合併成一個集合,計算每篇文章對於這個集合中的詞的詞頻(爲了不文章長度的差別,可使用相對詞頻);
  3. 生成兩篇文章各自的詞頻向量;
  4. 計算兩個向量的餘弦類似度,值越大就表示越類似。

    首先,請看此算法代碼的文件結構:網絡

    文件結構圖

接下來,是算法的實現步驟:url

第一步:使用TF-IDF算法,找出兩篇文章的關鍵詞

/**
	 * (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

第二步:每篇文章各取出若干個關鍵詞(好比20個),合併成一個集合,計算每篇文章對於這個集合中的詞的詞頻(爲了不文章長度的差別,可使用相對詞頻) 

&   

第三步:生成兩篇文章各自的詞頻向量

/**
	 * (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

如何獲取idf值:

/**
	 * 
	 * @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

相關文章
相關標籤/搜索