- 示例工程
【免费下载链接】52-technologies-in-2016
Let's learn a new technology every week. A new technology blog every Sunday in 2016.
导读
本文基于本仓库03-stanford-corenlp目录下的完整示例项目,系统讲解如何使用 Scala 调用 Stanford CoreNLP 开源库完成非结构化文本的情感分析(Sentiment Analysis / Opinion Mining)。你将掌握 CoreNLP 管线的 annotator 配置、Sentiment枚举与整数情感分值的映射规则、以句子长度加权选取"主情感"的算法,以及基于 ScalaTest 的正/负/中性三类测试写法,最终可以独立搭建一个可运行、可测试的 Scala 情感分析器。
为什么选择 Stanford CoreNLP 做情感分析
情感分析(Sentiment Analysis)也称观点挖掘(Opinion Mining),是自然语言处理中用于分析一段给定文本所蕴含情感倾向的技术,其应用场景覆盖从市场营销到客户服务的众多领域。原文作者在多年前曾用 Naive Bayes 分类器写过 Java 程序,通过分析某部电影相关的推文来判断观众是否喜欢该电影——这正是情感分析的典型用例。
与之相比,Stanford CoreNLP 提供了一整套自然语言分析工具,按官方说明其能力包括:给出单词的词形(base forms)、词性标注(parts of speech)、识别文本中的公司名/人名等命名实体、规范化日期时间与数值、标记句子的短语结构与词语依赖关系、判断哪些名词短语指向同一实体、指示情感倾向、抽取提及之间的开放类关系等。本示例只取其情感分析能力,但整个管线能力是共用的。
本文讲解的完整代码位于仓库的 sentiment-analyzer 目录,核心实现见 SentimentAnalyzer.scala,测试见 SentimentAnalyzerSpec.scala。
第一步:搭建 sbt 项目骨架
创建目录结构
先在文件系统的合适位置创建项目目录sentiment-analyzer:
$ mkdir sentiment-analyzer随后创建 Scala 源码与测试的标准布局(与仓库中src/main/scala/com/shekhargulati/sentiment_analyzer/、src/test/scala/com/shekhargulati/sentiment_analyzer/的包结构保持一致):
$ mkdir -p src/main/{scala,resources} $ mkdir -p src/test/scala如果你对 sbt 构建工具还不熟悉,可以先阅读本仓库 02-sbt 系列文章,那里有 sbt 构建脚本的入门讲解。
编写 build.sbt
在sentiment-analyzer目录下新建build.sbt(sbt 构建脚本),填入以下内容:
name := "sentiment-analyzer" description := "A demo application to showcase sentiment analysis using Stanford CoreNLP and Scala" version := "0.1.0" scalaVersion := "2.11.7" libraryDependencies += "edu.stanford.nlp" % "stanford-corenlp" % "3.5.2" artifacts (Artifact("stanford-corenlp", "models"), Artifact("stanford-corenlp")) libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.6" % "test"理解artifacts关键配置
上面构建脚本中最值得关注的是artifacts的用法。当某个依赖在仓库中发布了多个 artifact时,就需要用artifacts显式声明需要拉取哪些构件。这里我们通过artifacts告诉 sbt:classpath 中需要同时包含stanford-corenlp和stanford-models两个依赖:
stanford-corenlp:定义了我们将在代码中使用的核心 API(如StanfordCoreNLP、Annotation、CoreAnnotations等类);stanford-models:包含stanford-corenlp在底层运行时所需的所有数据模型文件(语法解析模型、情感模型等)。
需要特别提醒的是,stanford-models库体积约为378.1 MB,因此 sbt 首次下载依赖时会耗时较长,属正常现象。
另外,仓库中的 plugins.sbt 还引入了sbt-dependency-graph插件(net.virtual-void % sbt-dependency-graph % 0.8.1),运行sbt dependencyTree即可直观查看stanford-corenlp与stanford-models等依赖的传递关系。
第二步:定义 Sentiment 枚举与分值映射
分析器输出的情感结果是一个枚举值。原文档在应用内定义了一个 Scala 枚举Sentiment,仓库源码 SentimentAnalyzer.scala 中它的完整实现如下:
object Sentiment extends Enumeration { type Sentiment = Value val POSITIVE, NEGATIVE, NEUTRAL = Value def toSentiment(sentiment: Int): Sentiment = sentiment match { case x if x == 0 || x == 1 => Sentiment.NEGATIVE case 2 => Sentiment.NEUTRAL case x if x == 3 || x == 4 => Sentiment.POSITIVE } }这里的toSentiment方法用于把stanford-corenlpAPI 返回的整数情感分值转换为枚举常量,映射规则是:
| CoreNLP 整数分值 | 情感含义 | 映射后的枚举 |
|---|---|---|
| 0 或 1 | 负面情感 | Sentiment.NEGATIVE |
| 2 | 中性 | Sentiment.NEUTRAL |
| 3 或 4 | 正面情感 | Sentiment.POSITIVE |
(从源码结构看,该映射是使用模式匹配实现的:case x if x == 0 || x == 1与case x if x == 3 || x == 4两个守卫条件分别捕获负向与正向分值区间,case 2精确匹配中性分值;这种写法比逐一列举 0~4 五个 case 更紧凑。)
第三步:编写 SentimentAnalyzer 核心类
完整源码
SentimentAnalyzer是整个应用的主体。仓库中 SentimentAnalyzer.scala 的完整实现如下:
import java.util.Properties import com.shekhargulati.sentiment_analyzer.Sentiment.Sentiment import edu.stanford.nlp.ling.CoreAnnotations import edu.stanford.nlp.neural.rnn.RNNCoreAnnotations import edu.stanford.nlp.pipeline.{Annotation, StanfordCoreNLP} import edu.stanford.nlp.sentiment.SentimentCoreAnnotations import scala.collection.convert.wrapAll._ object SentimentAnalyzer { val props = new Properties() props.setProperty("annotators", "tokenize, ssplit, parse, sentiment") val pipeline: StanfordCoreNLP = new StanfordCoreNLP(props) /** * Extracts the main sentiment for a given input */ def mainSentiment(input: String): Sentiment = Option(input) match { case Some(text) if text.nonEmpty => extractSentiment(text) case _ => throw new IllegalArgumentException("input can't be null or empty") } /** * Extracts a list of sentiments for a given input */ def sentiment(input: String): List[(String, Sentiment)] = Option(input) match { case Some(text) if text.nonEmpty => extractSentiments(text) case _ => throw new IllegalArgumentException("input can't be null or empty") } private def extractSentiment(text: String): Sentiment = { val (_, sentiment) = extractSentiments(text) .maxBy { case (sentence, _) => sentence.length } sentiment } private def extractSentiments(text: String): List[(String, Sentiment)] = { val annotation: Annotation = pipeline.process(text) val sentences = annotation.get(classOf[CoreAnnotations.SentencesAnnotation]) sentences .map(sentence => (sentence, sentence.get(classOf[SentimentCoreAnnotations.SentimentAnnotatedTree]))) .map { case (sentence, tree) => (sentence.toString, Sentiment.toSentiment(RNNCoreAnnotations.getPredictedClass(tree))) } .toList } }逐步拆解实现原理
原文档对上述代码给出了四条核心说明,结合仓库源码可以进一步细化如下:
1. 构建 StanfordCoreNLP 管线
props.setProperty("annotators", "tokenize, ssplit, parse, sentiment")指定了本次使用的 4 个 annotator。StanfordCoreNLP内部会依据该属性构造一条管线:输入文本依次经过tokenize(分词)、ssplit(句子切分)、parse(句法解析)、sentiment(情感分析),最终产出各种被分析过的语言学形态。注意:情感 annotator 依赖前面的解析结果,因此parse必须先于sentiment执行,annotator 顺序不可随意调换。
2.mainSentiment的输入校验
mainSentiment通过Option(input)配合守卫条件text.nonEmpty做空值防御:只有当输入非 null 且非空时才调用extractSentiment,否则抛出IllegalArgumentException("input can't be null or empty")。
3.extractSentiment的"主情感"策略
extractSentiment对extractSentiments返回的「(句子文本, 情感)」二元组列表执行maxBy操作,比较键是sentence.length,即以句子长度为权重,把最长的句子视为整段文本的主情感。因此对输入Scala is a great general purpose language. I don't use it often.,较长的句子Scala is a great general purpose language带有正面情绪,mainSentiment就会返回Sentiment.POSITIVE,尽管第二句含否定词。
4.extractSentiments的管线调用细节
pipeline.process(text)对输入文本运行整条管线,得到Annotation对象;- 通过
annotation.get(classOf[CoreAnnotations.SentencesAnnotation])取出全部句子注解(SentencesAnnotation); - 由于已导入
scala.collection.convert.wrapAll._,可以在 Java 集合sentences上直接使用 Scala 集合方法(map、toList等); - 对每个句子注解,取出
SentimentCoreAnnotations.SentimentAnnotatedTree(情感注解树),再通过RNNCoreAnnotations.getPredictedClass(tree)读取递归神经网络(RNN)预测出的整数情感类别,最后交给Sentiment.toSentiment转换为枚举。
提供逐句情感列表的公开方法
除了主情感,应用还暴露了sentiment方法,返回所有句子及其各自情感的列表:
def sentiment(input: String): List[(String, Sentiment)] = Option(input) match { case Some(text) if !text.isEmpty => extractSentiments(text) case _ => throw new IllegalArgumentException("input can't be null or empty") }该方法与mainSentiment共用相同的空值校验逻辑,区别在于直接返回完整列表而非只取最长句子的情感,适合需要细粒度逐句分析的场景。
第四步:用 ScalaTest 编写三类情感测试
项目使用 ScalaTest 编写测试,测试文件位于仓库的 SentimentAnalyzerSpec.scala,采用FunSpec+Matchers风格,完整内容如下:
package com.shekhargulati.sentiment_analyzer import org.scalatest.{FunSpec, Matchers} class SentimentAnalyzerSpec extends FunSpec with Matchers { describe("sentiment analyzer") { it("should return POSITIVE when input has positive emotion") { val input = "Scala is a great general purpose language." val sentiment = SentimentAnalyzer.mainSentiment(input) sentiment should be(Sentiment.POSITIVE) } it("should return NEGATIVE when input has negative emotion") { val input = "Dhoni laments bowling, fielding errors in series loss" val sentiment = SentimentAnalyzer.mainSentiment(input) sentiment should be(Sentiment.NEGATIVE) } it("should return NEUTRAL when input has no emotion") { val input = "I am reading a book" val sentiment = SentimentAnalyzer.mainSentiment(input) sentiment should be(Sentiment.NEUTRAL) } } }三条用例覆盖了三种情感类别:
- 正面用例:
"Scala is a great general purpose language."期望返回Sentiment.POSITIVE; - 负面用例:
"Dhoni laments bowling, fielding errors in series loss"(板球新闻标题,含抱怨/失误等负面语义)期望返回Sentiment.NEGATIVE; - 中性用例:
"I am reading a book"(陈述事实、无情绪倾向)期望返回Sentiment.NEUTRAL。
用例均调用SentimentAnalyzer.mainSentiment,并与Sentiment枚举常量做断言比较。在项目根目录运行sbt test即可执行全部测试;由于首跑需要下载约 378 MB 的模型文件,请预留足够时间与磁盘空间。
小结与扩展方向
通过这个示例,我们完成了一个麻雀虽小五脏俱全的 Scala 情感分析器:用 sbt 的artifacts正确引入了 CoreNLP 核心库与模型库,用 4 个 annotator 配置了最小可用的情感分析管线,用枚举 + 模式匹配封装了 CoreNLP 整数分值的语义映射,并以「最长句子作为整段文本主情感」的策略对外提供mainSentiment与逐句sentiment两个 API,最后用 ScalaTest 验证了正、负、中三类场景。
如果你想继续深入,可以沿着以下方向在本仓库之外自行扩展实验(仓库本身为只读参考):
- 在
build.sbt的 annotator 列表中加入ner、lemma、parse之外的能力,体验 CoreNLP 更完整的语言学分析; - 将
extractSentiment的主情感策略从「按句长加权」替换为「按情感分值聚合投票」等更精细的算法; - 用 sbt-dependency-graph 插件(
sbt dependencyTree)核对stanford-corenlp与stanford-models的传递依赖,加深对 sbt artifact 机制的理解。
本示例同时属于 2016 年「每周一门新技术」系列的一部分,该系列还包含 01-finatra 与 02-sbt 等 Scala 生态主题,可对照阅读以构建更完整的 Scala 技术栈认知。
- 示例工程
【免费下载链接】52-technologies-in-2016
Let's learn a new technology every week. A new technology blog every Sunday in 2016.
相关推荐
从0到1掌握Java语音识别:CMU Sphinx与Kaldi全攻略
从0到1掌握Java语音识别:CMU Sphinx与Kaldi全攻略 引言:探索Java语音识别的技术痛点 你是否曾在开发语音交互应用时遇到以下困境:开源库配置
文档知识库Flair 情感分析实战指南:使用预训练 sentiment 模型进行文本情感分类
Flair 情感分析实战指南:使用预训练 sentiment 模型进行文本情感分类 导读 本文介绍如何在 Flair 框架中使用官方预训练的情感分析(senti
NLP深度学习机器学习Feedback Sentiment Analyzer(FSA)实战指南:基于 AWS 多语言 SDK 构建跨服务评论情感分析应用
Feedback Sentiment Analyzer(FSA)实战指南:基于 AWS 多语言 SDK 构建跨服务评论情感分析应用 Feedback Senti
示例工程教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考