2. 文本预处理基础:分词技术(Jieba分词)、去停用词、词干提取与词形还原

文本预处理,说白了就是给原始文本「洗个澡」。我做了这么多年NLP,见过太多人把脏数据直接扔进模型,结果跑出来的结果惨不忍睹。你想想看,如果输入的都是垃圾,输出能是黄金吗?

这一节,咱们就聊聊文本预处理的四个核心步骤。我个人习惯把它们叫做「四步洗手法」——分词、去停用词、词干提取、词形还原。每一步都有它的门道。

2.1 分词技术:Jieba分词实战

分词是中文NLP的第一步,也是最关键的一步。英文单词之间有空格,天然分好了。中文呢?「我喜欢自然语言处理」——机器根本不知道哪里该切。

我最早做分词时用过各种工具,最后发现Jieba最顺手。它支持三种模式,咱们一个个看。

2.1.1 精确模式

这是最常用的模式。它会把句子最精确地切开,适合文本分析。

import jieba

text = "我在北京清华大学学习自然语言处理"
words = jieba.lcut(text)
print(words)
# 输出:['我', '在', '北京', '清华大学', '学习', '自然语言处理']

你看,「清华大学」被完整保留,没有拆成「清华」和「大学」。这就是Jieba基于词典的匹配能力。我在项目中遇到过一个问题:如果词典里没有某个词,它就会拆得乱七八糟。比如「奥利给」这种网络新词,默认词典里没有。

我的小技巧:遇到新词,可以手动添加自定义词典。用 jieba.load_userdict('my_dict.txt') 就行。我一般把行业术语、人名、产品名都放进去。

2.1.2 全模式

全模式会把所有可能的词都扫描出来。速度快,但会有冗余。

words = jieba.lcut(text, cut_all=True)
print(words)
# 输出:['我', '在', '北京', '清华', '清华大学', '华大', '大学', '学习', '自然', '自然语言', '语言', '处理']

看到了吗?「清华大学」被拆成了「清华」「清华大学」「华大」「大学」。嗯,这里要注意:全模式不适合做精确分析,但适合做关键词提取。

2.1.3 搜索引擎模式

这个模式在精确模式的基础上,对长词再切分。适合搜索引擎场景。

words = jieba.lcut_for_search(text)
print(words)
# 输出:['我', '在', '北京', '清华', '清华大学', '华大', '大学', '学习', '自然语言', '语言', '处理']
核心结论:文本相似度计算,我建议用精确模式。全模式和搜索引擎模式会产生太多噪声,影响相似度计算的准确性。

2.2 去停用词

分词之后,你会发现一堆「的」「了」「在」「是」这类词。它们出现频率极高,但对语义贡献几乎为零。说白了,就是语言里的「噪音」。

我曾经做过一个项目,计算两篇新闻的相似度。结果因为「的」「了」这些词太多,导致完全不相关的两篇文章被判定为高度相似。嗯,从那以后我再也不敢忽略停用词了。

2.2.1 停用词表的选择

我一般用哈工大停用词表,再加一些自定义的。比如在金融领域,「股票」「基金」不能算停用词,但「哈」「呀」这种语气词可以去掉。

def remove_stopwords(words, stopwords_path='stopwords.txt'):
    with open(stopwords_path, 'r', encoding='utf-8') as f:
        stopwords = set([line.strip() for line in f])
    return [w for w in words if w not in stopwords]

text = "我今天去了图书馆,看了一本关于自然语言处理的书。"
words = jieba.lcut(text)
clean_words = remove_stopwords(words)
print(clean_words)
# 输出:['今天', '去', '图书馆', '看', '一本', '关于', '自然语言处理', '书']
注意:停用词表不是万能的。有些词在A领域是停用词,在B领域可能是关键词。比如「分析」在通用场景下可以去掉,但在学术论文里它很重要。我建议根据业务场景动态调整。

2.3 词干提取与词形还原

这两个概念经常被混淆。我简单解释一下:

  • 词干提取(Stemming):粗暴地砍掉词缀,比如「running」→「run」,「flies」→「fli」。结果可能不是真实单词。
  • 词形还原(Lemmatization):根据词典把词还原成原型,比如「better」→「good」,「ran」→「run」。结果一定是真实单词。

你想想看,在文本相似度计算中,「running」和「ran」应该算同一个意思吧?如果不做归一化,它们会被当成两个不同的词,相似度就低了。

2.3.1 英文的词干提取

英文里最常用的是Porter Stemmer。我最早用NLTK做这个,后来发现它太慢了,就换成了SnowballStemmer。

from nltk.stem import PorterStemmer

stemmer = PorterStemmer()
words = ['running', 'runner', 'ran', 'easily', 'fairly']
stems = [stemmer.stem(w) for w in words]
print(stems)
# 输出:['run', 'runner', 'ran', 'easili', 'fairli']

看到了吗?「easily」变成了「easili」,这不是一个真实单词。这就是词干提取的缺点——它只追求规则,不追求语义。

2.3.2 英文的词形还原

词形还原更智能。它需要词性标注,但结果更准确。

from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()
print(lemmatizer.lemmatize('running', 'v'))  # 输出:run
print(lemmatizer.lemmatize('better', 'a'))   # 输出:good
print(lemmatizer.lemmatize('flies', 'n'))    # 输出:fly
我的建议:做文本相似度计算时,优先用词形还原。虽然慢一点,但语义更准确。词干提取适合对速度要求高的场景,比如搜索引擎的实时索引。

2.3.3 中文怎么办?

中文没有词干提取的概念。因为中文没有时态、单复数变化。但中文有「同义词归一化」的需求。比如「电脑」和「计算机」应该算同一个意思。

我一般用两种方法:

  1. 同义词词典:手动维护一个映射表,比如「电脑→计算机」「手机→移动电话」。
  2. 词向量聚类:用Word2Vec或BERT把词转成向量,然后聚类。语义相近的词会聚在一起。

说实话,第一种方法更实用。我在做电商评论分析时,就建了一个同义词表,把「快递」「物流」「配送」都映射到「物流」上。效果立竿见影。

2.4 完整的预处理流程

好了,咱们把四个步骤串起来。这是我个人习惯的流水线:

def preprocess(text):
    # 1. 分词
    words = jieba.lcut(text)
    
    # 2. 去停用词
    words = remove_stopwords(words)
    
    # 3. 词形还原(英文场景)
    # words = [lemmatizer.lemmatize(w) for w in words]
    
    # 4. 同义词归一化(中文场景)
    # words = [synonym_map.get(w, w) for w in words]
    
    return ' '.join(words)

text = "我今天去了图书馆,看了一本关于自然语言处理的书。"
clean_text = preprocess(text)
print(clean_text)
# 输出:今天 去 图书馆 看 一本 关于 自然语言处理 书
避坑指南:我曾经犯过一个错误——先做词形还原再做分词。结果「running」被还原成「run」后,分词器把「run」当成了动词,导致后续分析出错。正确的顺序是:先分词,再做归一化。

最后说一句:预处理不是越复杂越好。我见过有人把预处理搞了十几步,结果模型效果反而下降了。你想想看,每一步都可能引入误差。所以,够用就好,别过度处理。

下一节,咱们聊聊文本的向量化表示。到时候你会发现,预处理做得好,后面的工作会轻松很多。