3、文本预处理基础:分词、去停用词、词干提取与词形还原、文本清洗实战
文本预处理,说白了就是给原始文本「洗个澡」。我做了这么多年 NLP 项目,可以负责任地告诉你:预处理做得好,模型成功一半。很多同学一上来就调模型参数,结果数据里全是噪声,那再好的模型也白搭。
今天咱们就把预处理这四板斧——分词、去停用词、词干提取与词形还原、文本清洗——挨个讲透。我会结合实战经验,把那些坑都给你指出来。
3.1 文本清洗:先把垃圾倒掉
文本清洗是第一步,也是最容易被忽视的一步。我见过太多人拿到数据直接分词,结果模型学了一堆乱码和 HTML 标签。
常见的清洗内容包括:
- 去除 HTML 标签:爬虫拿到的数据常有
<div>、<p>等标签 - 去除特殊符号:@、#、$、% 等,除非你的任务需要保留
- 统一大小写:英文文本全部转小写
- 去除多余空白:多个空格、换行符压缩成一个
- 处理编码问题:乱码字符、Unicode 控制字符
核心原则:清洗要「适度」。过度清洗会丢失语义信息。比如「iPhone 12」中的数字和大小写,在情感分析里可能很重要。
来看一段 Python 清洗代码:
import re
import unicodedata
def clean_text(text):
# 去除 HTML 标签
text = re.sub(r'<[^>]+>', '', text)
# 去除 URL
text = re.sub(r'http\S+|www\S+', '', text)
# 统一转小写
text = text.lower()
# 只保留字母、数字、空格和基本标点
text = re.sub(r'[^a-zA-Z0-9\s\.\,\!\?]', '', text)
# 压缩多余空白
text = re.sub(r'\s+', ' ', text).strip()
return text
我的经验:清洗规则最好做成可配置的。不同任务需要保留的东西不一样。我曾经做过一个产品评论分析,发现「#」号在美妆评论里代表色号,去掉后模型准确率掉了 5 个点。
3.2 分词:把句子切成词
分词就是把连续的文本切成一个个有意义的单元。中文和英文的切法完全不同。
3.2.1 英文分词
英文天然有空格分隔,看起来简单,但坑也不少:
- 缩写词:don't 要拆成 do 和 n't 吗?
- 连字符:state-of-the-art 是一个词还是三个?
- 专有名词:New York 不能拆成 New 和 York
我一般用 NLTK 或 spaCy 的分词器,它们内置了这些规则:
from nltk.tokenize import word_tokenize
text = "I don't like state-of-the-art methods."
tokens = word_tokenize(text)
print(tokens)
# 输出:['I', 'do', "n't", 'like', 'state-of-the-art', 'methods', '.']
3.2.2 中文分词
中文分词就麻烦多了。没有空格,全靠算法猜。常用的工具有 jieba、HanLP、pkuseg 等。
我个人习惯用 jieba,轻量又好用:
import jieba
text = "我爱自然语言处理"
# 精确模式
tokens = jieba.lcut(text)
print(tokens)
# 输出:['我', '爱', '自然语言处理']
# 添加自定义词典
jieba.add_word('自然语言处理')
tokens = jieba.lcut(text)
print(tokens)
# 输出:['我', '爱', '自然语言处理']
注意:中文分词没有「完美」的答案。同一个句子,不同分词器结果可能不同。我建议根据你的领域数据,准备一份自定义词典,能显著提升分词准确率。
3.3 去停用词:扔掉没用的词
停用词就是那些高频但没啥实际意义的词,比如「的」「了」「是」「a」「the」等。去掉它们可以减少噪声,降低特征维度。
但这里有个坑——停用词不是绝对的。比如在情感分析中,「不」这个字很重要:「我不喜欢」和「我喜欢」意思完全相反。如果你把「不」也去掉,那就完蛋了。
我一般会准备两份停用词表:
| 任务类型 | 停用词策略 | 示例 |
|---|---|---|
| 文本分类 | 去除高频虚词 | 的、了、是、在、有 |
| 情感分析 | 保留否定词和程度词 | 不、很、非常、太 |
| 关键词提取 | 严格去除 | 所有虚词、代词、介词 |
| 机器翻译 | 基本不去停用词 | 保留所有词 |
实战代码:
def remove_stopwords(tokens, stopwords_set):
return [token for token in tokens if token not in stopwords_set]
# 加载停用词表
with open('stopwords_zh.txt', 'r', encoding='utf-8') as f:
stopwords = set([line.strip() for line in f])
# 使用
tokens = ['我', '爱', '自然语言处理', '的', '研究']
filtered = remove_stopwords(tokens, stopwords)
print(filtered)
# 输出:['我', '爱', '自然语言处理', '研究']
避坑指南:我曾经做过一个电商评论情感分析,把「不」字加进了停用词表,结果模型把「不好」和「好」当成一类,准确率直接腰斩。从那以后,我每次都会先检查停用词表里有没有否定词。
3.4 词干提取与词形还原:把词变回原形
这两个操作都是为了把不同形态的词统一起来。比如「running」「ran」「runs」都指向「run」。
3.4.1 词干提取
词干提取比较粗暴,直接砍掉词缀。速度快,但结果可能不是合法单词。
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
words = ['running', 'ran', 'runs', 'better', 'studies']
for w in words:
print(f"{w} -> {stemmer.stem(w)}")
# 输出:
# running -> run
# ran -> ran (没变,因为ran不是规则变化)
# runs -> run
# better -> better (没变)
# studies -> studi (砍过头了)
3.4.2 词形还原
词形还原更智能,它会根据词性把词还原成词典里的原形。比如「better」还原成「good」,「ran」还原成「run」。
from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet
lemmatizer = WordNetLemmatizer()
# 需要指定词性,否则默认是名词
words = [('running', 'v'), ('ran', 'v'), ('better', 'a'), ('studies', 'n')]
for w, pos in words:
print(f"{w} -> {lemmatizer.lemmatize(w, pos)}")
# 输出:
# running -> run
# ran -> run
# better -> good
# studies -> study
怎么选?
- 词干提取:速度快,适合大规模数据、信息检索场景
- 词形还原:准确率高,适合情感分析、文本分类等需要语义理解的任务
我个人在文本分类中更倾向词形还原,虽然慢一点,但效果更好。
3.5 实战:完整的预处理流程
好了,理论讲完了,咱们来写一个完整的预处理流水线。这是我做项目时常用的模板:
import re
import jieba
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords
class TextPreprocessor:
def __init__(self, lang='zh'):
self.lang = lang
self.lemmatizer = WordNetLemmatizer()
# 加载停用词
if lang == 'zh':
with open('stopwords_zh.txt', 'r') as f:
self.stopwords = set([l.strip() for l in f])
else:
self.stopwords = set(stopwords.words('english'))
def clean(self, text):
# 清洗
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'http\S+', '', text)
text = text.lower()
text = re.sub(r'[^a-zA-Z0-9\u4e00-\u9fff\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def tokenize(self, text):
if self.lang == 'zh':
return jieba.lcut(text)
else:
return text.split()
def lemmatize(self, token):
# 简单处理,实际应用中需要词性标注
return self.lemmatizer.lemmatize(token)
def process(self, text):
text = self.clean(text)
tokens = self.tokenize(text)
# 去停用词 + 词形还原
result = []
for token in tokens:
if token not in self.stopwords and len(token) > 1:
if self.lang == 'en':
token = self.lemmatize(token)
result.append(token)
return result
# 使用示例
preprocessor = TextPreprocessor(lang='zh')
text = "我真的很喜欢这个产品,它太棒了!"
print(preprocessor.process(text))
# 输出:['真的', '喜欢', '这个', '产品', '太棒了']
重要提醒:预处理流程的顺序很重要。我建议:先清洗 → 再分词 → 去停用词 → 最后词形还原。如果先分词再清洗,可能会把词切碎,导致清洗不干净。
3.6 常见问题与避坑指南
做预处理这么多年,我踩过的坑不少,分享几个最常见的:
- 编码问题:中文文本经常遇到 GBK 和 UTF-8 混用。我建议统一用 UTF-8,遇到乱码直接丢弃或转码。
- 数字处理:电话号码、日期、价格要不要保留?看任务。如果是情感分析,价格数字可能有用;如果是新闻分类,数字意义不大。
- 表情符号:社交媒体文本里表情符号很有用。😊 代表正面,😡 代表负面。我一般会保留或者转成文字标签。
- 拼写错误:英文文本里常见拼写错误。可以用 PySpellChecker 纠正,但要注意速度。
我的习惯:每次做新项目,我都会先随机抽 100 条数据,手动检查预处理结果。看看有没有不该去掉的被去掉了,有没有该去掉的没去掉。这一步花不了 10 分钟,但能避免很多后期问题。
嗯,文本预处理这块就讲到这里。说白了,预处理没有银弹,关键是根据你的数据和任务灵活调整。下一章咱们会讲特征工程,到时候这些预处理的结果就要派上大用场了。
公众号:蓝海资料掘金营,微信deep3321