Java实现TF-IDF算法
TF-IDF是一种用于信息检索与数据挖掘的常用加权技术。TF是词频,IDF是逆文本频率指数,说白了就是提取文章关键字。
原理:
简单来说就是一个词语在一篇文章中出现的次数TF,和该词语在语料库中所出现的频率IDF。
TF = 文章中该词出现次数/文章总词数
IDF = log(语料库中所有文章 / (语料库中出现该词汇的文章+1))
TF-IDF == TF*IDF,这个值越大,就说明这个词汇越可能是该文章的关键字,统计这类关键字以便于文章提出主旨。
Java代码实现:
首先我在写自己毕设的时候需要提出目标创新创业项目计划书的主旨,我就上网找了几篇计划书的模板范文填充语料库:(当然这个越多越好,做成txt是由于txt比较方便扫描)
接着我借到了学弟的创新创业项目的计划书(由于提交计划书肯定不能是txt形式,所以这里我做成了PDF:因为我写了识别PDF的代码) 记录了一下存放路径
然后创建Maven项目:
记得在pom.xml中导入这两个所需要的Maven包
<dependency> <!-- 用来识别文章中词性并分词的 -->
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
<dependency> <!-- 用来处理PDF文件的导入问题 -->
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.12</version>
</dependency>
<!-- 当然网上Maven仓库里也有不同的版本 -->
具体ansj的使用可以去康康链接: http://nlpchina.github.io/ansj_seg/.里面还有分词以及词性的相关说明。
这是我的Maven项目:
Word类
用来存找出来的词汇和存储最后的TF-IDF
package com.tfidf1;
public class Word implements Comparable<Word> {
String name;
Double tf;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getTf() {
return tf;
}
public void setTf(Double tf) {
this.tf = tf;
}
public Word(String name, Double tf) {
super();
this.name = name;
this.tf = tf;
}
public Word() {
super();
}
@Override
public String toString() {
return "Word [name=" + name + ", tf=" + tf + "]";
}
public int compareTo(Word o) { //重写排序方法,后面方便降序排序
if (this.tf > o.tf)
return -1;
else if (this.tf < o.tf)
return 2;
return 0;
}
}
TF类:(目标文件读取,并统计词频)
这块用来PDFBox这个包里的方法
参考:https://iowiki.com/pdfbox/pdfbox_quick_guide.html.
package com.tfidf1;
import org.ansj.domain.Result;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import java.io.File;
import java.util.*;
// 词频
public class TF {
// 分词
public static List<Word> ansj(String str) {
Set<String> Nature = new HashSet<String>();
Nature.add("n");
Nature.add("vn"); //只取n和vn两种词性 即名词和一部分动词
Result result = ToAnalysis.parse(str); // 分词结果的一个封装,主要是一个List<Term>的terms
List<Term> terms = result.getTerms(); // 拿到terms
// System.out.println(terms.size());
Map<String, Double> map = new HashMap<String, Double>();
// 定义map来存储词汇名称和词的数量
for (int i = 0; i < terms.size(); i++) {
String word = terms.get(i).getName(); // 拿到词
String natureStr = terms.get(i).getNatureStr(); // 拿到词性
if (Nature.contains(natureStr)) // 判断set里有没有该词性,只取自己需要的
map.put(word, map.getOrDefault(word, 0.0) + 1.0);
}
List<Word> list = new ArrayList<Word>();
//word类的list存词汇名称以及暂时存一下词频
for (String key : map.keySet()) { // 便利map 计算词频
Double ans = map.get(key);
double tf = ans / terms.size();
Word word = new Word(key, tf);
list.add(word);
}
// Collections.sort(list); 这个地方实际没必要排序
return list;
}
public static List<Word> FileRead(String path) { //这里是对pdfbox的运用
// 对PDF文件的读取
String str = "";
try {
File file = new File(path);
PDDocument document = PDDocument.load(file);
PDFTextStripper pdfStripper = new PDFTextStripper();
String text = pdfStripper.getText(document);
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) >= 0x4e00 && text.charAt(i) <= 0x9fbb)
str += text.charAt(i); //这块把所有不是汉字的过滤掉了
}
document.close();
} catch (Exception e) {
e.printStackTrace();
}
return ansj(str);
}
}
IDF类:
统计语料库里的逆文本频率指数,语料库不变的话,这个也是不变的,写到项目里时只需要统计一次把统计结果放到数据库里就行了
package com.tfidf1;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.ansj.domain.Result;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
//逆向文件频率
public class IDF {
static Map<String, Double>idf_map = new HashMap<String, Double>();
// 存储词汇和该词汇的逆向文件频率
static Double ant = 1.0; // 总文档数量
private static void calculate() { // 计算逆向文件频率
for (String key : idf_map.keySet()) {
idf_map.put(key, Math.log(ant/idf_map.get(key)));
}
}
public static void idfFileRead() { // Java 的基础文件操作
try {
File file = new File("C:\\Users\\37388\\Desktop\\corpus\\1.txt");
int i = 2;
while(file.isFile()) {
String str = "";
InputStreamReader reader = null;
reader = new InputStreamReader(new FileInputStream(file));
int tempchar;
while ((tempchar = reader.read()) != -1) {
if ((char) tempchar >= 0x4e00 && (char) tempchar <= 0x9fbb)
str += (char) tempchar;
}
ansj(str); // 取出一个文件里所有汉字就进行一次分词
reader.close();
String s = Integer.toString(i);
file = new File("C:\\Users\\37388\\Desktop\\corpus\\"+s+".txt");
i++;
//System.out.println(file);
ant++;
}
// 参考我上面的语料库里的存储,可以这么循环所有文件
// 写到项目里应该存一个自增的id和路径到数据库里进行遍历比较方便
} catch (Exception e) {
e.printStackTrace();
}
calculate();
}
public static void ansj(String str) { //分词操作同上TF类
Set<String> Nature = new HashSet<String>();
Nature.add("n");
Nature.add("vn");
Result result = ToAnalysis.parse(str); // 分词结果的一个封装,主要是一个List<Term>的terms
List<Term> terms = result.getTerms(); // 拿到terms
//System.out.println(terms.size());
Map<String, Double> map = new HashMap<String, Double>();
for (int i = 0; i < terms.size(); i++) {
String word = terms.get(i).getName(); // 拿到词
String natureStr = terms.get(i).getNatureStr(); //拿到词性
if(Nature.contains(natureStr))
map.put(word, map.getOrDefault(word, 0.0) + 1.0);
}
for (String key : map.keySet()) {
idf_map.put(key, idf_map.getOrDefault(key, 0.0) + 1.0);
}
}
public static Map<String, Double> getIdf_map() { // 将统计结果返回
idfFileRead();
return idf_map;
}
}
TF-IDF类
调用整合计算:
package com.tfidf1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.*;
public class TF_IDF {
public static List<Word> calcuate(String path) {
List<Word> tf = TF.FileRead(path);
Map<String, Double> idf = IDF.getIdf_map();
List<Word> list = new ArrayList<Word>();
System.out.println(tf.size());
for(int i=0; i<tf.size(); i++) {
Word e = new Word();
String s = tf.get(i).getName();
double tmp = tf.get(i).getTf();
e.setName(s);
if(idf.containsKey(s)){
e.setTf(tmp * idf.get(s));
}
list.add(e);
}
Collections.sort(list); // 降序排序list
return list;
}
public static void main(String []args) {
List<Word> list = calcuate("C:\\Users\\37388\\Desktop\\资料文献\\创新创业计划书\\创新创业计划书.pdf");
//这块可以自定义字符串
for(int i=0; i<list.size(); i++) {
System.out.println(list.get(i).name + " " + list.get(i).tf);
}
}
}
最后附上运行结果:(越靠前越关键)