数据挖掘:基于Spark+HanLP实现影视评论关键词抽取(1)
1. 背景
近日项目要求基于爬取的影视评论信息,抽取影视的关键字信息。考虑到影视评论数据量较大,因此采用Spark处理框架。关键词提取的处理主要包含分词+算法抽取两部分。目前分词工具包较为主流的,包括哈工大的LTP以及HanLP,而关键词的抽取算法较多,包括TF-IDF、TextRank、互信息等。本次任务主要基于LTP、HanLP、Ac双数组进行分词,采用TextRank、互信息以及TF-IDF结合的方式进行关键词抽取。
说明:本项目刚开始接触,因此效果层面需迭代调优。
2. 技术选型
(1) 词典
1) 基于HanLP项目提供的词典数据,具体可参见HanLP的github。
2) 考虑到影视的垂直领域特性,引入腾讯的嵌入的汉语词,参考该地址。
(2) 分词
1) LTP分词服务:基于Docker Swarm部署多副本集服务,通过HTTP协议请求,获取分词结果(部署方法可百度); 也可以直接在本地加载,放在内存中调用,效率更高(未尝试)
2) AC双数组:基于AC双数组,采用最长匹配串,采用HanLP中的AC双数组分词器
(3) 抽取
1) 经典的TF-IDF:基于词频统计实现
2) TextRank:借鉴于PageRank算法,基于HanLP提供的接口
3) 互信息:基于HanLP提供的接口
3. 实现代码
(1) 代码结构
1) 代码将分词服务进行函数封装,基于不同的名称,执行名称指定的分词
2) TextRank、互信息、LTP、AC双数组等提取出分词或短语,最后均通过TF-IDF进行统计计算
(2) 整体代码
1) 主体代码:细节层面与下载的原始评论数据结构有关,因此无需过多关注,只需关注下主体流程即可
def extractFilmKeyWords(algorithm: String): Unit ={
// 测试
println(HanLPSpliter.getInstance.seg("如何看待《战狼2》中的爱国情怀?"))
val sc = new SparkContext(new SparkConf().setAppName("extractFileKeyWords").set("spark.driver.maxResultSize", "3g"))
val baseDir = "/work/ws/video/parse/key_word"
import scala.collection.JavaConversions._
def extractComments(sc: SparkContext, inputInfo: (String, String)): RDD[(String, List[String])] = {
sc.textFile(s"$baseDir/data/${inputInfo._2}")
.map(data => {
val json = JSONObjectEx.fromObject(data.trim)
if(null == json) ("", List())
else{
val id = json.getStringByKeys("_id")
val comments: List[String] = json.getArrayInfo("comments", "review").toList
val reviews: List[String] = json.getArrayInfo("reviews", "review").toList
val titles: List[String] = json.getArrayInfo("reviews", "title").toList
val texts = (comments ::: reviews ::: titles).filter(f => !CleanUtils.isEmpty(f))
(IdBuilder.getSourceKey(inputInfo._1, id), texts)
}
})
}
// 广播停用词
val filterWordRdd = sc.broadcast(sc.textFile(s"$baseDir/data/stopwords.txt").map(_.trim).distinct().collect().toList)
def formatOutput(infos: List[(Int, String)]): String ={
infos.map(info => {
val json = new JSONObject()
json.put("status", info._1)
try{
json.put("res", info._2)
} catch {
case _ => json.put("res", "[]")
}
json.toString.replaceAll("[\\s]+", "")
}).mkString(" | ")
}
def genContArray(words: List[String]): JSONArray ={
val arr = new JSONArray()
words.map(f => {
val json = new JSONObject()
json.put("cont", f)
arr.put(json)
})
arr
}
// 基于LTP分词服务
def splitWordByLTP(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val url = "http://dev.content_ltp.research.com/ltp"
val params = new util.HashMap[String, String]()
params.put("s", f)
params.put("f", "json")
params.put("t", "ner")
// 调用LTP分词服务
val result = HttpPostUtil.httpPostRetry(url, params).replaceAll("[\\s]+", "")
if (CleanUtils.isEmpty(result)) (0, f) else {
val resultArr = new JSONArray()
val jsonArr = try { JSONArray.fromString(result) } catch { case _ => null}
if (null != jsonArr && 0 < jsonArr.length()) {
for (i <- 0 until jsonArr.getJSONArray(0).length()) {
val subJsonArr = jsonArr.getJSONArray(0).getJSONArray(i)
for (j <- 0 until subJsonArr.length()) {
val subJson = subJsonArr.getJSONObject(j)
if(!filterWordRdd.value.contains(subJson.getString("cont"))){
resultArr.put(subJson)
}
}
}
}
if(resultArr.length() > 0) (1, resultArr.toString) else (0, f)
}
})
}
// 基于AC双数组搭建的分词服务
def splitWordByAcDoubleTreeServer(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = SplitQueryHelper.splitQueryText(f)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
})
}
// 内存加载AC双数组
def splitWordByAcDoubleTree(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLPSpliter.getInstance().seg(f)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else (1, genContArray(splitResults).toString)
})
}
// TextRank
def splitWordByTextRank(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLP.extractKeyword(f, 100)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else {
val arr = genContArray(splitResults)
if(0 == arr.length()) (0, f) else (1, arr.toString)
}
})
}
// 互信息
def splitWordByMutualInfo(texts: List[String]): List[(Int, String)] ={
texts.map(f => {
val splitResults = HanLP.extractPhrase(f, 50)
.filter(f => !CleanUtils.isEmpty(f) && !filterWordRdd.value.contains(f.toLowerCase)).toList
if (0 == splitResults.size) (0, f) else {
val arr = genContArray(splitResults)
if(0 == arr.length()) (0, f) else (1, arr.toString)
}
})
}
// 提取分词信息
val unionInputRdd = sc.union(
extractComments(sc, SourceType.DB -> "db_review.json"),
extractComments(sc, SourceType.MY -> "my_review.json"),
extractComments(sc, SourceType.MT -> "mt_review.json"))
.filter(_._2.nonEmpty)
unionInputRdd.cache()
unionInputRdd.map(data => {
val splitResults = algorithm match {
case "ltp" => splitWordByLTP(data._2)
case "acServer" => splitWordByAcDoubleTreeServer(data._2)
case "ac" => splitWordByAcDoubleTree(data._2)
case "textRank" => splitWordByTextRank(data._2)
case "mutualInfo" => splitWordByMutualInfo(data._2)
}
val output = formatOutput(splitResults)
s"${data._1}\t$output"
}).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/wordSplit/$algorithm"))
val splitRDD = sc.textFile(s"$baseDir/result/wordSplit/$algorithm/part*", 30)
.flatMap(data => {
if(data.split("\\t").length < 2) None
else{
val sourceKey = data.split("\\t")(0)
val words = data.split("\\t")(1).split(" \\| ").flatMap(f => {
val json = JSONObjectEx.fromObject(f.trim)
if (null != json && "".equals(json.getStringByKeys("status"))) {
val jsonArr = try { JSONArray.fromString(json.getStringByKeys("res")) } catch { case _ => null }
var result: List[(String, String)] = List()
if (jsonArr != null) {
for (j <- 0 until jsonArr.length()) {
val json = jsonArr.getJSONObject(j)
val cont = json.getString("cont")
result ::= (cont, cont)
}
}
result.reverse
} else None
}).toList
Some((sourceKey, words))
}
}).filter(_._2.nonEmpty)
splitRDD.cache()
val totalFilms = splitRDD.count()
val idfRdd = splitRDD.flatMap(result => {
result._2.map(_._1).distinct.map((_, 1))
}).groupByKey().filter(f => f._2.size > 1).map(f => (f._1, Math.log(totalFilms * 1.0 / (f._2.sum + 1))))
idfRdd.cache()
idfRdd.map(f => s"${f._1}\t${f._2}").saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/idf/$algorithm"))
val idfMap = sc.broadcast(idfRdd.collectAsMap())
// 计算TF
val tfRdd = splitRDD.map(result => {
val totalWords = result._2.size
val keyWords = result._2.groupBy(_._1)
.map(f => {
val word = f._1
val tf = f._2.size * 1.0 / totalWords
(tf * idfMap.value.getOrElse(word, 0D), word)
}).toList.sortBy(_._1).reverse.filter(_._2.trim.length > 1).take(50)
(result._1, keyWords)
})
tfRdd.cache()
tfRdd.map(f => {
val json = new JSONObject()
json.put("_id", f._1)
val arr = new JSONArray()
for (keyWord <- f._2) {
val subJson = new JSONObject()
subJson.put("score", keyWord._1)
subJson.put("word", keyWord._2)
arr.put(subJson)
}
json.put("keyWords", arr)
json.toString
}).saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/withScore"))
tfRdd.map(f => s"${f._1}\t${f._2.map(_._2).toList.mkString(",")}")
.saveAsTextFile(HDFSFileUtil.clean(s"$baseDir/result/keyword/$algorithm/noScore"))
tfRdd.unpersist()
splitRDD.unpersist()
idfMap.unpersist()
idfRdd.unpersist()
unionInputRdd.unpersist()
filterWordRdd.unpersist()
sc.stop()
}
2) 基于HanLP提供的AC双数组封装
import com.google.common.collect.Lists;
import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.Segment;
import com.hankcs.hanlp.seg.common.Term;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.Serializable;
import java.util.List; public class HanLPSpliter implements Serializable{
private static Logger logger = LoggerFactory.getLogger(Act.class); private static HanLPSpliter instance = null; private static Segment segment = null; private static final String PATH = "conf/tencent_word_act.txt"; public static HanLPSpliter getInstance() {
if(null == instance){
instance = new HanLPSpliter();
}
return instance;
} public HanLPSpliter(){
this.init();
} public void init(){
initSegment();
} public void initSegment(){
if(null == segment){
addDict();
HanLP.Config.IOAdapter = new HadoopFileIOAdapter();
segment = HanLP.newSegment("dat");
segment.enablePartOfSpeechTagging(true);
segment.enableCustomDictionaryForcing(true);
}
} public List<String> seg(String text){
if(null == segment){
initSegment();
} List<Term> terms = segment.seg(text);
List<String> results = Lists.newArrayList();
for(Term term : terms){
results.add(term.word);
}
return results;
}
}
3) HanLP加载HDFS中的自定义词典
import com.hankcs.hanlp.corpus.io.IIOAdapter;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI; public class HadoopFileIOAdapter implements IIOAdapter{
@Override
public InputStream open(String path) throws IOException {
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(path), conf);
return fs.open(new Path(path));
} @Override
public OutputStream create(String path) throws IOException {
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(path), conf);
OutputStream out = fs.create(new Path(path));
return out;
}
}
4. 采坑总结
(1) Spark中实现HanLP自定义词典的加载
由于引入腾讯的嵌入词,因此使用HanLP的自定义词典功能,参考的方法如下:
a. 《基于hanLP的中文分词详解-MapReduce实现&自定义词典文件》,该方法适用于自定义词典的数量较少的情况,如果词典量较大,如腾讯嵌入词820W+,理论上jar包较为臃肿
b. 《Spark中使用HanLP分词》,该方法的好处在于无需手工构件词典的bin文件,操作简单
切记:如果想让自定义词典生效,需先将data/dictionary/custom中的bin文件删除。通过HanLP源码得知,如果存在bin文件,则直接加载该bin文件,否则会将custom中用户自定义的词典重新加载,在指定的环境中(如本地或HDFS)中自动生成bin文件。
腾讯820W词典,基于HanLP生成bin文件的时间大概为30分钟。
(2) Spark异常
Spark执行过程中的异常信息:
1) 异常1
a. 异常信息:
Job aborted due to stage failure: Total size of serialized results of 3979 tasks (1024.2 MB) is bigger than spark.driver.maxResultSize (1024.0 MB)
b. 解决:通过设置spark.driver.maxResultSize=4G,参考:《Spark排错与优化》
2) 异常2
a. 异常信息:java.lang.OutOfMemoryError: Java heap space
b. 解决:参考https://blog.csdn.net/guohecang/article/details/52088117
如有问题,请留言回复!
数据挖掘:基于Spark+HanLP实现影视评论关键词抽取(1)的更多相关文章
- 31页PPT:基于Spark的移动大数据挖掘
31页PPT:基于Spark的移动大数据挖掘 数盟11.16 Data Science Meetup(DSM北京)分享:基于Spark的移动大数据挖掘分享嘉宾:张夏天(TalkingData首席数据科 ...
- 大数据实时处理-基于Spark的大数据实时处理及应用技术培训
随着互联网.移动互联网和物联网的发展,我们已经切实地迎来了一个大数据 的时代.大数据是指无法在一定时间内用常规软件工具对其内容进行抓取.管理和处理的数据集合,对大数据的分析已经成为一个非常重要且紧迫的 ...
- 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用
https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...
- 基于 Spark 的文本情感分析
转载自:https://www.ibm.com/developerworks/cn/cognitive/library/cc-1606-spark-seniment-analysis/index.ht ...
- 基于Spark的电影推荐系统(实战简介)
写在前面 一直不知道这个专栏该如何开始写,思来想去,还是暂时把自己对这个项目的一些想法 和大家分享 的形式来展现.有什么问题,欢迎大家一起留言讨论. 这个项目的源代码是在https://github. ...
- 基于Spark的电影推荐系统(推荐系统~1)
第四部分-推荐系统-项目介绍 行业背景: 快速:Apache Spark以内存计算为核心 通用 :一站式解决各个问题,ADHOC SQL查询,流计算,数据挖掘,图计算 完整的生态圈 只要掌握Spark ...
- 京东基于Spark的风控系统架构实践和技术细节
京东基于Spark的风控系统架构实践和技术细节 时间 2016-06-02 09:36:32 炼数成金 原文 http://www.dataguru.cn/article-9419-1.html ...
- 基于Spark ALS构建商品推荐引擎
基于Spark ALS构建商品推荐引擎 一般来讲,推荐引擎试图对用户与某类物品之间的联系建模,其想法是预测人们可能喜好的物品并通过探索物品之间的联系来辅助这个过程,让用户能更快速.更准确的获得所需 ...
- 【基于spark IM 的二次开发笔记】第一天 各种配置
[基于spark IM 的二次开发笔记]第一天 各种配置 http://juforg.iteye.com/blog/1870487 http://www.igniterealtime.org/down ...
随机推荐
- .netcore2.0 Startup 全局配置文件小技巧
- WPF 控件库——可拖动选项卡的TabControl
WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...
- p2p_server
以太坊系列之十九 对p2p模块server的理解 type transport interface { // The two handshakes. doEncHandshake(prv *ecdsa ...
- 三、SpringBoot-application.properties配置文件和application.yml配置文件
其实SpringBoot的配置文件有.properties和.yml两种形式,两种配置文件的效果类似,只不过是格式不同而已,孩儿们可以根据下面这几种张截图,通过对比端口号的配置,以及连接SQLServ ...
- 实现求解线性方程(矩阵、高斯消去法)------c++程序设计原理与实践(进阶篇)
步骤: 其中A是一个n*n的系数方阵 向量x和b分别是未知数和常量向量: 这个系统可能有0个.1个或者无穷多个解,这取决于系数矩阵A和向量b.求解线性系统的方法有很多,这里使用一种经典的方法——高斯消 ...
- 通用唯一识别码——UUID(Python)
一.概述: UUID(Universally Unique Identity)的缩写,是一种软件建构的标准,通常由32字节16进制数表示(128位),它可以保证时间和空间的唯一性.目前应用最广泛的UU ...
- C#下Excel的普通处理和报表设计
一.准备:想要操作Excel,先要在添加2个COM引用: 1.Microsoft Excel 14.0 Object Library (版本不同14.0也可能是12.0等) 2.Microsoft O ...
- BZOJ4650/UOJ219 [Noi2016]优秀的拆分
本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000 作者博客:http://www.cnblogs.com/ljh2000-jump/ ...
- centos7用docker安装mysql5.7.24后配置主从
1)使用docker安装完成mysql5.7.24,我规划的是3台: 192.168.0.170(Master) 192.168.0.169(Slave) 192.168.0.168(Slave) 2 ...
- java 常用模块代码
1.文件的读写操作 (1)进行读操作 import java.io.BufferedReader; import java.io.FileNotFoundException; import java. ...