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)
# 输出:['今天', '去', '图书馆', '看', '一本', '关于', '自然语言处理', '书']
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 中文怎么办?
中文没有词干提取的概念。因为中文没有时态、单复数变化。但中文有「同义词归一化」的需求。比如「电脑」和「计算机」应该算同一个意思。
我一般用两种方法:
- 同义词词典:手动维护一个映射表,比如「电脑→计算机」「手机→移动电话」。
- 词向量聚类:用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)
# 输出:今天 去 图书馆 看 一本 关于 自然语言处理 书
最后说一句:预处理不是越复杂越好。我见过有人把预处理搞了十几步,结果模型效果反而下降了。你想想看,每一步都可能引入误差。所以,够用就好,别过度处理。
下一节,咱们聊聊文本的向量化表示。到时候你会发现,预处理做得好,后面的工作会轻松很多。