☰
使用 Scala 与 Stanford CoreNLP 构建文本情感分析器:sentiment-analyzer 实战解析
2026/10/15 17:44:06 网站建设 项目流程
  • 示例工程

【免费下载链接】52-technologies-in-2016

Let's learn a new technology every week. A new technology blog every Sunday in 2016.

项目地址:https://gitcode.com/gh_mirrors/52/52-technologies-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.

项目地址:https://gitcode.com/gh_mirrors/52/52-technologies-in-2016
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询