文章目录
TF-IDF
一、概述
- TF-IDF (term frequency-inverse- document frequency)是一种用于资讯检索与资讯探勘的常用加权技术。
- TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度.
- 字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降
》继续搜索:王者荣耀
》继续搜索:王者荣耀露娜
》继续搜索:王者荣耀露娜连招
- 每个字词都有对应出现的页面
- 通过字词数量缩小范围
- 最终通过字词对于页面的权重来进行排序
- term frequency-词频,指的是某一个给定的词语在一份给定的文件中出现的次数
- 一篇文章中,一个词语出现的次数越多说明越重要
- 但是还要考虑整篇文章的词的总数,单个词所占的比例综合来看比例越高越重要
- 计算公式
* 分子n是该词在文件中的出现次数
* 分母则是在文件中所有分词的出现次数之和(也就是分出了多少个词)
- inverse document frequency - 逆向文件频率
- 由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到
- 多篇文章中,一个词语出现的次数越多反而越不重要
- 计算公式
- 分子D语料库中的文件总数
- 分母表示包含指定词语文件的数目
- 一般计算时分母数目会加1,防止出现分母为0的错误
- 某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-DF
- 倾向于过滤掉常见的词语,保留重要的词语
- 计算公式
- IK Analyzer 是一个开源的,基于java 语言开发的轻量级的中文分词工具包。从2006年 12 月推出1.0 版开始,IKAnalyzer 已经推出了4 个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从 3.0版本开始,IK发展为面向 Java的公用分词组件,独立于 Lucene项目,同时提供了对 Lucene的默认优化实现。
- 需要在依赖中导入包
<!-- 分词器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
- IKSegmenter 的第一个构造参数为StringReader类型 。 StringReader是装饰Reader的类,其用法是读取一个String字符串
- IKSegmenter 的第二个构造参数userSmart 为切分粒度 true表示最大切分 false表示最细切分
- Lexeme: 词单位类
返回顶部
二、案例_统计猫眼电影数据中的TF-IDF
我们以一份猫眼电影数据为例来进行TF-IDF的统计
1.整体思路
- 整体分为三大部分:求TF、求IDF、求TF-IDF
- 每一部分需要使用MapReduce进行处理,主要依据就是公式
对于上面的数据集,我们只需要使用到 name(电影名称)、==common(评论)==两列数据即可。
返回顶部
2.代码实现
Step 1 — 计算 TF
part 1:自定义SQLBean获取需要数据
package TF_IDF;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.lib.db.DBWritable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SQLBean implements Writable, DBWritable {
// 封装数据库字段
private String name; // 电影名称
private String common; // 评论
// toString()
@Override
public String toString() {
return name + "\t" + common;
}
// 序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.name);
out.writeUTF(this.common);
}
// 反序列化
@Override
public void readFields(DataInput in) throws IOException {
this.name = in.readUTF();
this.common = in.readUTF();
}
// 写入数据库
@Override
public void write(PreparedStatement statement) throws SQLException {
statement.setString(1,this.name);
statement.setString(2,this.common);
}
// 读取数据库
@Override
public void readFields(ResultSet resultSet) throws SQLException {
this.name = resultSet.getString(1);
this.common = resultSet.getString(2);
}
// set\get方法集
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCommon() {
return common;
}
public void setCommon(String common) {
this.common = common;
}
}
part 2:编写Mapper1类
TF表示分词在文档中出现的频率,算法是:(该分词在该文档出现的次数)/(该文档分词的总数),这个值越大表示这个词越重要,即权重就越大,TF (例如:一篇文档分词后,总共有500个分词,而分词”Hello”出现的次数是20次,则TF值是: TF =20/500=0.04)。
在第一阶段,我们需要考虑到后续的Reducer中能够去获取到该分词在该文档出现的次数、该文档分词的总数。考虑到相同的key会进入同一个ReduceTask,也就是说,我们可以对每一条电影评论的分词单独计算。
所以Map端我们先要去进行分词,然后以 (电影名,分词_1)的形式输出;同时,我们还进行总评论数的统计,每处理一条数据,输出一个(count,1)
package TF_IDF;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;
import org.wltea.analyzer.core.IKSegmenter;
import org.wltea.analyzer.core.Lexeme;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
/**
* todo
* 输入 LongWritable,SQLBean
* 输出 Text, IntWritable
*/
public class Mapper1 extends Mapper<LongWritable,SQLBean, Text, Text> {
Text k = new Text();
Text v = new Text();
@Override
protected void map(LongWritable key, SQLBean value, Context context) throws IOException, InterruptedException {
// 获取一条数据 肖申克的救赎\t希望让人自由。
// 拆分
String[] fields = value.toString().trim().split("\t");
String filmName = fields[0].trim(); // 获取电影名称
String common = fields[1].trim(); // 获取评论
// 开始分词
StringReader reader = new StringReader(common); // 字符串输入流、其本质就是字符串
IKSegmenter ikSegmenter = new IKSegmenter(reader,true); // true:使用智能分词;false:使用最细粒度分词
Lexeme word = null;
while ((word = ikSegmenter.next()) != null){
// 完整的评论 :希望让人自由。
// 这里的 w 可能返回的就是 希望 这个词
String w = word.getLexemeText(); // 获取词元的文本内容
// 写出内容
k.set(filmName);
v.set(w+"_"+1);
context.write(k,v); // 肖申克的救赎,希望_1
System.out.println(k.toString()+" "+v.toString());
}
context.write(new Text("count"),new Text("1"));
}
}
part 3:编写Reducer1类
进入到了reduce中,每个reduce中都是相同的key所包含的分词内容,例如:
- (肖申克的救赎,希望_1)
- (肖申克的救赎,让_1)
- (肖申克的救赎,人_1)
- (肖申克的救赎,自由_1)
然后将其放入一个HashMap中以键值对的形式存储,遍历的同时判断,如果存在,值加1;否则新建。这样可以统计出每个分词在该条评论中出现的次数;至于总分词数,在遍历的同时计数就行了。
由于map阶段还输出了一个总数统计的count,所以我们可以在reduce阶段定义公共变量count,用于统计每个reduce的次数。
最后输出的时候,利用公式计算出TF值。
注意:这里一开始我在计算的时候使用的是hashMap.size(),这样会产生一个问题,就是假如一个记录分词中有重复,hashMap.size()实际上是去重后的分词个数,并不是真正的分词数。
package TF_IDF;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* todo
*
*/
public class Reducer1 extends Reducer<Text, Text,Text, DoubleWritable> {
int count = 0; // 记录总评论数
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
int sum = 0; // 记录每条评论的总分词数
// 肖申克的救赎,希望_1
// 用于存储每个词、及其总个数
Map<String, Integer> hashMap = new HashMap<>();
String[] fields = {} ;
for (Text v:values){
fields = v.toString().split("_");
// 判断集合中是否存在,存在累加;不存在,新建 --- 统计所有电影中该词出现的次数
if (hashMap.containsKey(fields[0])){
hashMap.put(fields[0],hashMap.get(fields[0])+1);
} else {
hashMap.put(fields[0],1);
}
sum++;
count++;
}
// 计数
if ("count".equals(key.toString())){
context.write(new Text("count"),new DoubleWritable(count));
} else {
for (Map.Entry<String,Integer> entry:hashMap.entrySet()){
// 写出: 电影名_词 , TF值(该词出现的次数/)
context.write(new Text(key+"_"+entry.getKey()),new DoubleWritable(entry.getValue()*1.0 /sum));
System.out.println(new Text(key+"_"+entry.getKey())+" "+new DoubleWritable(entry.getValue()*1.0 /sum));
}
}
System.out.println(count);
}
}
part 4:编写Partition分区类
前面输出的时候有两种,一种是输出count统计总评论数量,另一种是输出内容,在这里我们将其分开。
package TF_IDF;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* 自定义分区
*/
public class FirstPartition extends Partitioner<Text, Text> {
@Override
public int getPartition(Text text, Text intWritable, int numPartitions) {
if ("count".equals(text.toString())){
return 1;
}else {
return 0;
}
}
}
返回顶部
Step 2 — 计算 IDF
part 1:编写Mapper2类
第二阶段要计算IDF值的时候会用到总评论条数,所以我们使用MapJoin缓存出第一次生成的小文件,获取总评论条数,附着文本中进行传输
这一阶段要统计的是某个分词在出现过的评论数,也就是在那个评论中出现过,紧接上一个Reduce的输出结果,我们将分词单独作为key传递下去,在下一个reduceTask中,相同的key会进入一并处理。其余信息合并作为value传输
package TF_IDF;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
/**
* 获取的数据 2001太空漫游_最 0.2222222222222222
* 输出 最 2001太空漫游_0.2222222222222222_250.0
*/
public class Mapper2 extends Mapper<LongWritable, Text,Text, Text> {
Text k = new Text();
Text v = new Text();
HashMap<String,Double> hashMap = new HashMap<>();
@Override
protected void setup(Context context) throws IOException {
// 获取缓存小表的内容 --- 总电影评论数
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(path),"UTF-8"));
String line;
while (StringUtils.isNotEmpty(line=br.readLine())){
String[] fields = line.split("\t");
hashMap.put(fields[0],Double.parseDouble(fields[1]));
}
// 关闭流
IOUtils.closeStream(br);
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 获取一行数据进行拆分
String[] fields1 = value.toString().split("\t"); // 0:2001太空漫游_最 1:0.2222222222222222
String[] fislds2 = fields1[0].split("_"); // 0:2001太空漫游 1:最
// 封装
k.set(fislds2[1]);
v.set(fislds2[0]+"_"+fields1[1]+"_"+hashMap.get("count"));
// 写出
context.write(k,v);
System.out.println(k+" "+v);
}
}
part 2:编写Reducer2类
IDF是是一个词语普遍重要性的度量。一个文档库中,一个分词出现在的文档数越少越能和其它文档区别开来。算法是: log(总文档数/(出现该分词的文档数+1)) 。如果一个词越常见,那么分母就越大,逆文档频率就越小越接近0。分母之所以要加1,是为了避免分母为0(即所有文档都不包含该词),IDF (例如:一个文档库中总共有10000篇文档, 99篇文档中出现过“Hello”分词,则idf是: IDF = log(10000/(99+1)) =100)
在这里,相同的key会进入同一个ReduceTask。例如:
- 最 2001太空漫游_0.2222222222222222_250.0
- 最 海豚湾_0.125_250.0
- 最 素媛_0.0625_250.0
这样一来,同样的分词,会有不同的其余信息,也就是在不同的评论中出现,我们只需要遍历的同时计数即可,也就是出现该分词的评论数。
有了出现该分词的评论数,同时还有总评论数,只要按照公式计算就行,然后通过Reduce输出。
注意:
1.一开始我是定义了一个count遍历,然后获取出现该分词的评论数,结果发现了问题,那样是在统计的同时输出,也就是count统计小了,所以转换成先将所有内容传入一个haspmap集合,然后通过集合大小获取出现该分词的评论数。(也可以不用到Map集合)
2.不能够对参数迭代内容多次迭代。
package TF_IDF;
import com.google.common.collect.Iterators;
import org.apache.commons.lang.ObjectUtils;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 输入 最 2001太空漫游_0.2222222222222222_250.0 获取在所有评论中出现的次数
* 输出 2001太空漫游_最_0.2222222222222222 新计算的IDF
*/
public class Reducer2 extends Reducer<Text,Text,Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
HashMap<String,Integer> hashMap = new HashMap<>();
// 1.遍历求次数
// 相同的key也就是相同的词会进入同一个reduce,如下所示
// 最 2001太空漫游_0.2222222222222222_250.0
// 最 海豚湾_0.125_250.0
// 最 素媛_0.0625_250.0
// 所以我们只需遍历计数就可以统计出,多少评论中包含了当前分词
// 首先存储
for (Text text:values){
if (hashMap.containsKey(text.toString())){
break;
} else {
hashMap.put(text.toString(), 1);
}
}
// 获取数据拆分
String filmName = ""; // 电影名
String word = key.toString(); // 分词
String TF = ""; // TF
Double total = 0.0; // 总评论数
for (Map.Entry<String,Integer> entry:hashMap.entrySet()){
String[] fields = entry.getKey().split("_");
filmName = fields[0]; // 电影名
TF = fields[1]; // TF
total = Double.parseDouble(fields[2]); // 总评论数
// 计算IDF
Double IDF = Math.log10(total/(hashMap.size()+1.0));
System.out.println(new Text(filmName+"_"+word+"_"+ TF));
// 写出
context.write(new Text(filmName+"_"+word+"_"+ TF+"_"+IDF),NullWritable.get());
}
}
}
返回顶部
Step 3 — 计算 TF-IDF
part 1:自定义BeanSQL收集结果数据集
创建数据库收集表
CREATE TABLE `splitword` (
`name` varchar(255) DEFAULT NULL,
`word` varchar(255) DEFAULT NULL,
`TF` double(255,5) DEFAULT NULL,
`IDF` double(255,5) DEFAULT NULL,
`TF_IDF` double(255,11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
package TF_IDF;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.lib.db.DBWritable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SQLBean implements Writable, DBWritable {
// 封装数据库字段
private String name; // 电影名称
private String common; // 评论
// toString()
@Override
public String toString() {
return name + "\t" + common;
}
// 序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.name);
out.writeUTF(this.common);
}
// 反序列化
@Override
public void readFields(DataInput in) throws IOException {
this.name = in.readUTF();
this.common = in.readUTF();
}
// 写入数据库
@Override
public void write(PreparedStatement statement) throws SQLException {
statement.setString(1,this.name);
statement.setString(2,this.common);
}
// 读取数据库
@Override
public void readFields(ResultSet resultSet) throws SQLException {
this.name = resultSet.getString(1);
this.common = resultSet.getString(2);
}
// set\get方法集
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCommon() {
return common;
}
public void setCommon(String common) {
this.common = common;
}
}
part 2:编写Mapper3类
TF-IDF就是TF*IDF,TF-IDF与一个词在文档中的出现次数成正比,与整个语料库中包含该词的文档数成反比
通过前面两个阶段输出后,这里只需要计算最后的TF-IDF就可以了。
获取之前的内容,拆分,计算,封装,输出。
计算的时候对结果保留了五位小数
package TF_IDF;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
import java.text.DecimalFormat;
/**
* 输入 哈利·波特与死亡圣器(下)_10年_0.25_4.8283137373023015
* 输出 哈利·波特与死亡圣器(下)\t10年\t0.25\t4.8283137373023015\t新计算的TF-IDF
*/
public class Mapper3 extends Mapper<LongWritable, Text,BeanSQL, NullWritable> {
BeanSQL k = new BeanSQL();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 获取到一行数据,进行拆分
String[] fields = value.toString().split("_");
String name = fields[0];
String word = fields[1];
// 格式化浮点数
DecimalFormat df = new DecimalFormat("#.#####");
double TF = Double.parseDouble(df.format(Double.parseDouble(fields[2])));
double IDF =Double.parseDouble(df.format(Double.parseDouble(fields[3])));
double TF_IDF = TF*IDF;
// 封装
k.set(name,word,TF,IDF,TF_IDF);
// 写出
context.write(k,NullWritable.get());
System.out.println(k);
}
}
part 3:编写Reducer3类
package TF_IDF;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* 输入 哈利·波特与死亡圣器(下)\t10年\t0.25\t4.8283137373023015\tTF-IDF
* 直接输出
*/
public class Reducer3 extends Reducer<BeanSQL, NullWritable,BeanSQL,NullWritable> {
@Override
protected void reduce(BeanSQL key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
// 直接输出
for (NullWritable n:values){
context.write(key,NullWritable.get());
}
}
}
返回顶部