4、NLP基础:分词技术(Jieba分词)、停用词过滤、词性标注、命名实体识别

好,咱们进入NLP实战的第一站。说实话,很多初学者一上来就搞深度学习模型,结果数据预处理那一步就翻车了。我当年也是这样,觉得分词嘛,不就是按空格切一切?后来被中文的「南京市长江大桥」狠狠教育了一顿。

这一章,咱们把NLP最基础的四个工具练扎实:分词、停用词过滤、词性标注、命名实体识别。你想想看,这些就是情感分析的「地基」,地基不稳,后面全白搭。

4.1 分词技术:Jieba分词实战

中文分词和英文不一样。英文单词自带空格,中文词与词之间没有天然分隔符。我习惯用 Jieba,它轻量、速度快,而且支持自定义词典,非常适合做情感分析的预处理。

4.1.1 安装与基本用法

pip install jieba

安装完直接上手。Jieba 提供三种模式:精确模式、全模式、搜索引擎模式。我个人最常用精确模式,它最精准,不会把词切得稀碎。

import jieba

text = "我今天心情不太好,感觉特别糟糕"
words = jieba.lcut(text)  # 精确模式
print(words)
# 输出:['我', '今天', '心情', '不太', '好', ',', '感觉', '特别', '糟糕']

你看,「心情不太好」被切成了「心情」「不太」「好」,这个粒度对情感分析来说刚刚好。全模式会把所有可能的词都列出来,比如「北京大学」会切成「北京」「大学」「北京大学」,太冗余了。

我的小技巧: 做情感分析时,建议用精确模式 + 自定义词典。比如「蓝瘦香菇」这种网络新词,Jieba 默认不认识,你得手动加进去。

4.1.2 自定义词典

我在项目中遇到过一个问题:用户评论里有很多产品名,比如「小爱同学」「天猫精灵」,Jieba 默认会切成「小爱」「同学」。这显然不对。解决办法就是加自定义词典。

# 创建一个 user_dict.txt 文件,每行一个词
# 小爱同学 5 n
# 天猫精灵 5 n

jieba.load_userdict("user_dict.txt")
text = "小爱同学今天播放了一首悲伤的歌"
words = jieba.lcut(text)
print(words)
# 输出:['小爱同学', '今天', '播放', '了', '一首', '悲伤', '的', '歌']

嗯,这里要注意:词典里可以指定词频和词性,但如果你不确定,只写词也行,Jieba 会自动调整。

4.2 停用词过滤

分词之后,你会发现一堆「的」「了」「在」「是」这种词。它们对情感分析几乎没有贡献,反而会引入噪声。我习惯在预处理阶段就把它们干掉。

4.2.1 什么是停用词?

说白了,就是那些高频但无实际意义的词。比如:

  • 助词:的、地、得
  • 介词:在、从、到
  • 连词:和、与、但是
  • 语气词:啊、呢、吗

你想想看,如果一条评论是「这个产品真的太好了」,去掉停用词后变成「产品 真的 好」,情感倾向依然保留,但特征维度降低了。

4.2.2 实战:加载停用词表

网上有很多现成的停用词表,比如哈工大停用词表、百度停用词表。我一般用哈工大的,比较全。

def load_stopwords(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        stopwords = set([line.strip() for line in f])
    return stopwords

stopwords = load_stopwords('stopwords.txt')

def filter_stopwords(words, stopwords):
    return [w for w in words if w not in stopwords and w.strip()]

text = "我今天心情不太好"
words = jieba.lcut(text)
filtered = filter_stopwords(words, stopwords)
print(filtered)
# 输出:['今天', '心情', '不太好']
我曾经踩过的坑: 停用词表里不要包含否定词!比如「不」「没」「别」。一旦你把「不」过滤掉,「不太好」就变成了「太好」,情感极性直接反转。所以,停用词表要针对情感分析场景做定制。

4.3 词性标注

词性标注就是给每个词打上标签:名词、动词、形容词、副词……为什么需要这个?因为情感词大多是形容词和副词。比如「开心」「难过」是形容词,「非常」「极其」是副词。通过词性标注,我们可以精准定位情感词。

4.3.1 Jieba 词性标注

Jieba 自带词性标注功能,用 jieba.posseg 模块就行。

import jieba.posseg as pseg

text = "这部电影真的非常感人"
words = pseg.lcut(text)
for word, flag in words:
    print(f"{word}: {flag}")
# 输出:
# 这: r
# 部: q
# 电影: n
# 真的: d
# 非常: d
# 感人: a

你看,「感人」是形容词(a),「非常」是副词(d)。我习惯把形容词和副词单独提取出来,作为情感特征。

4.3.2 常见词性标签

标签 含义 示例
a 形容词 好、坏、开心
d 副词 非常、很、极其
n 名词 电影、产品、服务
v 动词 喜欢、讨厌、推荐
r 代词 我、你、这
我的经验: 在情感分析中,形容词(a)和副词(d)是最重要的。名词和动词可以作为上下文特征。比如「服务很差」,「差」是形容词,直接决定情感极性。

4.4 命名实体识别(NER)

命名实体识别,听起来高大上,其实就是从文本中找出人名、地名、机构名、时间等专有名词。在情感分析里,NER 可以帮助我们定位情感对象。比如「我对华为的售后服务很不满意」,NER 能识别出「华为」是机构名,「售后服务」是产品相关实体。

4.4.1 使用 Jieba 做简单 NER

Jieba 的 NER 能力比较基础,主要靠词性标注和自定义词典。比如人名、地名可以通过词性标签识别。

import jieba.posseg as pseg

text = "张三昨天去了北京,对小米的客服非常失望"
words = pseg.lcut(text)
for word, flag in words:
    if flag in ['nr', 'ns', 'nt']:  # nr:人名, ns:地名, nt:机构名
        print(f"实体: {word}, 类型: {flag}")
# 输出:
# 实体: 张三, 类型: nr
# 实体: 北京, 类型: ns
# 实体: 小米, 类型: nt

嗯,这里要注意:Jieba 的 NER 准确率一般,尤其是对长尾实体。如果你需要高精度的 NER,建议用 HanLP 或 LAC。但在情感分析入门阶段,Jieba 完全够用。

4.4.2 实战:提取情感对象

我做过一个项目,分析用户对手机品牌的评论。通过 NER 提取出品牌名(如「华为」「小米」「苹果」),然后结合情感词,就能知道用户对哪个品牌满意、对哪个品牌不满。

def extract_entities_and_sentiment(text):
    words = pseg.lcut(text)
    entities = []
    sentiments = []
    for word, flag in words:
        if flag in ['nt']:  # 机构名
            entities.append(word)
        if flag in ['a']:   # 形容词
            sentiments.append(word)
    return entities, sentiments

text = "华为的拍照效果很好,但小米的电池续航太差了"
entities, sentiments = extract_entities_and_sentiment(text)
print(f"实体: {entities}")   # ['华为', '小米']
print(f"情感词: {sentiments}") # ['好', '差']
核心思路: 情感分析 = 情感对象(实体) + 情感倾向(情感词)。NER 帮你找到「谁」,分词和词性标注帮你找到「怎么样」。

4.5 整合:一个完整的预处理流水线

好了,咱们把上面四个步骤串起来,形成一个可复用的预处理函数。我习惯把它封装成一个类,方便后续调用。

import jieba
import jieba.posseg as pseg

class TextPreprocessor:
    def __init__(self, stopwords_path, user_dict_path=None):
        self.stopwords = self._load_stopwords(stopwords_path)
        if user_dict_path:
            jieba.load_userdict(user_dict_path)
    
    def _load_stopwords(self, path):
        with open(path, 'r', encoding='utf-8') as f:
            return set([line.strip() for line in f])
    
    def process(self, text):
        # 1. 分词
        words = jieba.lcut(text)
        # 2. 停用词过滤
        words = [w for w in words if w not in self.stopwords and w.strip()]
        # 3. 词性标注
        pos_tags = pseg.lcut(text)
        pos_dict = {word: flag for word, flag in pos_tags}
        # 4. 命名实体识别
        entities = [(word, flag) for word, flag in pos_tags if flag in ['nr', 'ns', 'nt']]
        return {
            'words': words,
            'pos_tags': pos_dict,
            'entities': entities
        }

# 使用示例
preprocessor = TextPreprocessor('stopwords.txt', 'user_dict.txt')
result = preprocessor.process("我今天对小米的售后服务非常不满意")
print(result['words'])     # ['今天', '小米', '售后服务', '非常', '不满意']
print(result['entities'])  # [('小米', 'nt')]
我曾经犯过的错: 一开始我把所有预处理步骤都写在同一个循环里,结果代码又臭又长,调试起来想哭。后来我学乖了,每个步骤单独封装,出了问题直接定位。记住:模块化是工程化的第一步

这一章的内容就到这里。你想想看,分词、停用词过滤、词性标注、NER,这四个工具就像厨师手里的刀、砧板、锅、铲。单独看都不难,但组合起来就能做出一桌好菜。下一章咱们就用这些工具,开始搭建情感分析的第一个模型。