2. 文本预处理基础:分词、去停用词、词干提取与词形还原、文本清洗

大家好,欢迎来到文本预处理这一章。

说实话,很多做 NLP 的新手,上来就急着调模型。结果数据一喂进去,全是乱码和噪音。我见过太多这样的案例了。文本预处理,说白了就是给模型「喂饭」之前,先把菜洗干净、切好块。这一步做不好,后面再牛的 Transformer 也白搭。

2.1 文本清洗:正则表达式是基本功

先聊聊文本清洗。你想想看,从网上爬下来的数据,什么乱七八糟的都有。HTML 标签、特殊符号、多余的空格、URL 链接……这些东西对模型来说就是噪音。

我个人习惯,拿到文本第一件事,就是用正则表达式做一次「大扫除」。

核心思路: 保留中文、英文、数字,其他统统干掉。
import re

def clean_text(text):
    # 去掉 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 去掉 URL
    text = re.sub(r'http\S+|www\S+', '', text)
    # 只保留中文、英文、数字和基本标点
    text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.\,\!\?]', '', text)
    # 合并多余空格
    text = re.sub(r'\s+', ' ', text).strip()
    return text

sample = "这是一段<b>测试</b>文本!访问 https://example.com 了解更多。"
print(clean_text(sample))
# 输出:这是一段测试文本!访问 了解更多。
我的小技巧: 正则表达式写完后,一定要用几个极端案例测试一下。我曾经因为没处理好全角半角符号,导致后面分词全乱了。

2.2 分词:Jieba 与 NLTK 的实战选择

分词是中文 NLP 的第一步,也是最关键的一步。英文天然有空格分隔,中文可没有。你想想看,「南京市长江大桥」这句话,分错了意思就完全变了。

2.2.1 Jieba:中文分词的首选

Jieba 是我用得最多的中文分词工具。它支持精确模式、全模式和搜索引擎模式。我个人习惯用精确模式,准确率最高。

import jieba

text = "我在项目中遇到过文本摘要的难题,最终通过优化分词解决了。"
# 精确模式
words = jieba.lcut(text)
print(words)
# 输出:['我', '在', '项目', '中', '遇到', '过', '文本', '摘要', '的', '难题', ',', '最终', '通过', '优化', '分词', '解决', '了', '。']

# 添加自定义词典
jieba.add_word("文本摘要")
words_custom = jieba.lcut(text)
print(words_custom)
# 输出:['我', '在', '项目', '中', '遇到', '过', '文本摘要', '的', '难题', ',', '最终', '通过', '优化', '分词', '解决', '了', '。']
注意: 专业领域一定要加自定义词典。我曾经做一个医疗文本摘要项目,不加词典的话,「心肌梗死」被分成了「心肌」和「梗死」,语义完全变了。

2.2.2 NLTK:英文分词的利器

做英文文本时,我一般用 NLTK。它内置了分词器,还能处理缩写、标点等特殊情况。

import nltk
from nltk.tokenize import word_tokenize

text = "I'm working on NLP, and it's fascinating!"
tokens = word_tokenize(text)
print(tokens)
# 输出:['I', "'m", 'working', 'on', 'NLP', ',', 'and', 'it', "'s", 'fascinating', '!']

嗯,这里要注意。NLTK 第一次用需要下载 punkt 模型。别问我怎么知道的,我第一次跑的时候报错报了半天。

2.3 去停用词:给文本做减法

分词之后,你会发现很多词其实没什么用。「的」、「了」、「在」、「是」……这些词出现频率极高,但对语义贡献几乎为零。去停用词,就是把这些「废话」删掉。

我一般会准备一个停用词表,网上有很多现成的。中文常用的有哈工大停用词表、百度停用词表。我个人习惯把几个表合并一下,去重后自己再补一些领域相关的词。

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

stopwords = load_stopwords('stopwords.txt')

def remove_stopwords(tokens, stopwords):
    return [word for word in tokens if word not in stopwords and len(word) > 1]

tokens = ['我', '在', '项目', '中', '遇到', '过', '文本摘要', '的', '难题']
filtered = remove_stopwords(tokens, stopwords)
print(filtered)
# 输出:['项目', '遇到', '文本摘要', '难题']
避坑指南: 我曾经把「不」和「没」也加进了停用词表,结果「不喜欢」变成了「喜欢」,语义完全反了。否定词一定要保留!

2.4 词干提取与词形还原

这两个概念容易搞混。我简单解释一下:

  • 词干提取(Stemming): 粗暴地砍掉词缀。比如 "running" → "run","flies" → "fli"。速度快,但可能不准确。
  • 词形还原(Lemmatization): 根据词典还原单词原型。比如 "better" → "good","ran" → "run"。准确率高,但速度慢。

你想想看,做文本摘要时,我们通常希望把不同形态的同一个词统一起来。比如 "running"、"ran"、"runs" 都应该归一化为 "run"。

2.4.1 词干提取实战

from nltk.stem import PorterStemmer

stemmer = PorterStemmer()
words = ["running", "flies", "easily", "better"]
stems = [stemmer.stem(word) for word in words]
print(stems)
# 输出:['run', 'fli', 'easili', 'better']

看到了吗?"flies" 变成了 "fli","easily" 变成了 "easili"。这就是词干提取的「粗暴」之处。不过在很多场景下,够用了。

2.4.2 词形还原实战

from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()
words = ["running", "flies", "easily", "better"]
lemmas = [lemmatizer.lemmatize(word, pos='v') for word in words]
print(lemmas)
# 输出:['run', 'fly', 'easily', 'better']
关键区别: 词形还原需要指定词性(pos),否则默认当成名词处理。比如 "running" 不指定词性,还原结果还是 "running"。

我个人习惯,在做文本摘要时优先用词形还原。虽然慢一点,但准确率更高。尤其是做英文摘要时,词形还原能保证语义的完整性。

2.5 完整的预处理流程

好了,我们把上面所有步骤串起来,形成一个完整的预处理流水线。这是我个人项目中常用的模板:

def preprocess_pipeline(text, lang='zh'):
    # 1. 文本清洗
    text = clean_text(text)
    
    # 2. 分词
    if lang == 'zh':
        tokens = jieba.lcut(text)
    else:
        tokens = word_tokenize(text)
    
    # 3. 去停用词
    tokens = remove_stopwords(tokens, stopwords)
    
    # 4. 词形还原(仅英文)
    if lang == 'en':
        tokens = [lemmatizer.lemmatize(token, pos='v') for token in tokens]
    
    return ' '.join(tokens)

# 测试
zh_text = "我正在做一个关于文本摘要的项目,它非常有趣!"
en_text = "I am working on a text summarization project, and it is very interesting!"

print(preprocess_pipeline(zh_text, 'zh'))
# 输出:项目 文本摘要 有趣

print(preprocess_pipeline(en_text, 'en'))
# 输出:I work text summarization project interesting
我的经验: 预处理流程不是一成不变的。不同任务、不同领域,需要调整的参数都不一样。比如做情感分析,停用词表就要更保守一些,因为很多情感词本身就是短词。

2.6 本章小结

文本预处理,说白了就是「清洗 + 分词 + 去噪 + 归一化」。每一步看起来简单,但细节决定成败。

我记得刚入行时,总觉得预处理是「体力活」,想跳过直接上模型。结果模型训练出来,效果差得离谱。后来老老实实把预处理做好,效果立竿见影。

下一章我们会聊特征提取,也就是把文本变成模型能理解的数字。到时候你会发现,预处理做得越好,特征提取就越轻松。

最后提醒一句: 预处理代码一定要模块化。写成一个函数,方便复用。别问我为什么强调这个——我曾经在一个项目里写了五遍同样的正则表达式,改需求时改到崩溃。