以下内容将从 基本概念 到 实用代码 分步骤、分场景地详细介绍 NLP 常见文本预处理 方法及其背后的思想。如果无法从外部导入数据,我们会模拟一份简易文本数据(如字符串列表),并在此基础上演示预处理代码及详细解释,确保在常规 Python 环境下可以运行。
一、文本预处理的常见需求和作用
在自然语言处理(NLP)任务(如机器学习、深度学习、大模型开发)中,原始文本数据通常会包含各种噪声,例如:
- 多余的空格、换行符、特殊符号
- HTML 标签、URL、邮箱等
- 语言层面的问题:大小写不统一、拼写错误
- 词形变化(went/walked 等过去式与原型 go/walk;dogs/buses 等名词复数形式)
- 不必要的词(常见停用词,如 the, is, at, on 等)
常见预处理目标
- 降噪 (Noise Reduction)
去除与任务无关或影响模型训练的噪声特征。 - 统一化 (Normalization)
包括大小写统一、词形还原(Lemma)或词干提取(Stem)、去除标点符号等。 - 结构化 (Tokenization 等)
将文本拆分成词元(tokens),或更细粒度的子词元(subwords)。 - 过滤 (Filtering)
去除某些非必要信息,如停用词(stop words)等。 - 增强 (Augmentation)
并非必需,但常见于深度学习中,如同义词替换、随机删除等,以增加鲁棒性。
二、常见的文本预处理方法概览
以下方法往往会按需组合使用:
- 大小写转换(Lowercasing / Uppercasing)
- 便于词频统计、一致性处理。多数英文 NLP 任务倾向统一转换为小写(除非对大小写敏感)。
- 去除 HTML 标签、URL、邮箱、货币符号等
- 使用正则表达式或简单字符串操作。
- 去除标点符号 (Punctuation Removal)
- 常见标点,如
. , ! ? : ; " ' -
等。
- 常见标点,如
- 分词 (Tokenization)
- 将句子拆分为词或子词。英文可使用空格拆分或更复杂的工具,中文需使用专门的分词器。
- 停用词去除 (Stop Words Removal)
- 语言中出现频率高但对上下文意义贡献不大的词,如
the, a, an, of, is
等。
- 语言中出现频率高但对上下文意义贡献不大的词,如
- 词干提取 (Stemming)
- 如 Porter Stemmer 或 Snowball Stemmer,把单词简化为词干(如 “studies”→“studi”)。
- 词形还原 (Lemmatization)
- 基于词典/词法规则,将不同时态、复数形式统一到词的原型(如 “studies”→“study”)。
- 子词切分 (Subword Tokenization)
- 用于大模型(如 BERT、GPT)或深度学习中的细粒度切分。典型算法有 Byte-Pair Encoding (BPE)、WordPiece 等。
- 数字化、特殊符号处理
- 将数字统一替换为
<num>
,或保持数字原状,视任务需要。
- 将数字统一替换为
以下我们将使用 Python 及相关库演示以上方法的大部分实现。
三、示例数据准备
我们先模拟一段文本数据(用一个字符串列表表示)。请注意,此示例仅用于演示,真实场景可来自数据库、爬虫、csv 等形式。
# 模拟一段文本数据
text_corpus = [
"I love Machine Learning! It's awesome.",
"Data science is an interdisciplinary field. The quick brown fox jumps over the lazy dog, doesn't it?",
"Visit us at https://example.com or send an email to contact@example.com!",
"<html>This is a sample HTML snippet.</html> NLP can be fun :)"
]
四、分步骤预处理示例
下面演示从最基础的清洗开始,逐步进行各种预处理操作。为了演示效果,我们使用一些常见 Python 库(re
, nltk
),并保证无需外部数据文件即可运行。若你的环境没有安装 nltk
,请先执行 pip install nltk
。
说明:如果环境中无法使用 NLTK,也可根据需求自行编写简单函数或使用其他库(如
spaCy
,但需pip install spacy
并下载语言模型)。
1. 基础清洗与正则替换
目标:
- 将文本统一为小写
- 移除 HTML 标签
- 移除 URL
- 移除邮箱
- 去除多余的标点或特殊字符(可视情况而定)
import re
def basic_cleaning(text: str) -> str:
# 1) 转小写
text = text.lower()
# 2) 移除 HTML 标签,如 <html>, <p> 等
text = re.sub(r"<.*?>", " ", text)
# 3) 移除 URL (http://xxx 或 https://xxx)
text = re.sub(r"http\S+|www\S+", " ", text)
# 4) 移除邮箱 (xxx@xxx.com)
text = re.sub(r"\S+@\S+\.\S+", " ", text)
# 5) 去除多余的标点符号,仅保留字母、数字、常见的符号
# 视任务而定,这里演示删除大部分标点,只保留句子间空格
text = re.sub(r"[^a-z0-9\s.,!?]", " ", text)
# 6) 去除多余空格
text = re.sub(r"\s+", " ", text).strip()
return text
# 测试:
cleaned_corpus = [basic_cleaning(sentence) for sentence in text_corpus]
for i, (orig, c) in enumerate(zip(text_corpus, cleaned_corpus)):
print(f"原文 {i}: {orig}")
print(f"清洗后: {c}")
print("-"*50)
解释
re.sub(r"<.*?>", " ", text)
:用空格替换任何形如<...>
的 HTML 标签。re.sub(r"http\S+|www\S+", " ", text)
:匹配并移除所有 URL。re.sub(r"\S+@\S+\.\S+", " ", text)
:匹配并移除邮箱格式(极简匹配)。re.sub(r"[^a-z0-9\s.,!?]", " ", text)
:只保留字母、数字、空格和部分标点(.,!?
),其余替换为空格。text.lower()
:统一转为小写。\s+
匹配连续空格,将其替换成单个空格;然后.strip()
去掉首尾空格。
此时得到的是一个基本“干净”、大小写统一的语句列表。
2. 分词 (Tokenization)
常见做法:
- 基于空格:适合简单场景,但对带标点的情况处理不佳。
- NLTK 的 word_tokenize(对英文较友好)。
- 自定义正则:如
re.split(r"\W+", text)
等。
此处演示 nltk.tokenize.word_tokenize:
import nltk
# 第一次使用需要下载 punkt 包:
# nltk.download('punkt')
from nltk.tokenize import word_tokenize
def tokenize_text(text: str) -> list:
tokens = word_tokenize(text)
return tokens
tokenized_corpus = [tokenize_text(sentence) for sentence in cleaned_corpus]
for i, (clean_sen, toks) in enumerate(zip(cleaned_corpus, tokenized_corpus)):
print(f"清洗后文本 {i}: {clean_sen}")
print(f"分词结果: {toks}")
print("-"*50)
解释
word_tokenize
会把标点符号、撇号等也当成单独词元拆分,比如"doesn't"
会被拆成["does", "n't"]
,这在英文 NLP 里比较常见。
3. 停用词去除 (Stop Words Removal)
停用词指在文本中出现频率极高,但对主语义贡献不大或会干扰主题建模的词汇,如英语中的 the, a, an, of, is, are, was, were, ...
等。在 NLTK 中,可以使用内置的停用词表(需要 stopwords
数据包)。
# nltk.download('stopwords')
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english')) # 这里是英文停用词表
def remove_stopwords(tokens: list) -> list:
filtered_tokens = [tok for tok in tokens if tok not in stop_words]
return filtered_tokens
no_stopwords_corpus = [remove_stopwords(toks) for toks in tokenized_corpus]
for i, (toks, fs) in enumerate(zip(tokenized_corpus, no_stopwords_corpus)):
print(f"分词结果 {i}: {toks}")
print(f"去停用词后: {fs}")
print("-"*50)
解释
- 使用
set(stopwords.words('english'))
来加速 “是否在停用词表中” 的判断。 - 如果用户有自定义停用词表,可以自行扩充或删减。
4. 词干化(Stemming)和词形还原(Lemmatization)
Stemming:用算法直接截断或替换单词末尾,得到词干(例如 using PorterStemmer
、SnowballStemmer
)。
Lemmatization:基于词典规则,还原到词的原型(例如 studies
→study
,went
→go
)。英文中常用 WordNetLemmatizer
。
下面以 NLTK 为例。
# nltk.download('wordnet')
from nltk.stem import PorterStemmer, WordNetLemmatizer
stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()
def stem_and_lemmatize(tokens: list):
stemmed = [stemmer.stem(tok) for tok in tokens]
# WordNetLemmatizer 需要指定词性(pos),默认为名词,此处示例只做简单还原
lemmed = [lemmatizer.lemmatize(tok) for tok in tokens]
return stemmed, lemmed
for i, tokens in enumerate(no_stopwords_corpus):
s, l = stem_and_lemmatize(tokens)
print(f"去停用词后:{tokens}")
print(f"Stem 结果:{s}")
print(f"Lemma结果:{l}")
print("-"*50)
解释
- PorterStemmer 的规则较简单,可能产生非真实单词,如 “studies”→“studi”。
- WordNetLemmatizer 依赖词典,根据单词本身的原形进行还原,但是需确定词性会更准确(如
lemmatizer.lemmatize(word, pos='v')
来处理动词)。
具体业务中,可二选一,或在不同场景尝试哪种更有效。
5. 子词切分 (Subword Tokenization)
在大模型(如 BERT、GPT)中,子词切分是非常重要的一步。常见算法:
- Byte-Pair Encoding (BPE)
- WordPiece
- SentencePiece 等
这里示例一个简单的 BPE 思想演示(并不完整,只作概念说明)。现实中常用库如 tokenizers
(HuggingFace) 或 sentencepiece
(Google)来训练和使用子词模型。
BPE 核心思路:
- 统计语料中出现频率最高的字符对(bigram)。
- 将该字符对合并为一个新的“子词符号”。
- 反复迭代,直到达到子词表大小或满足其他停止条件。
下面的示例仅演示核心过程的简化版本(不涉及多轮迭代训练),让你了解大致机制。
# 一个极简BPE演示:基于当前句子的字符频率合并
from collections import Counter, defaultdict
def bpe_tokenize(sentence, merges=2):
"""
sentence: 输入字符串(假设已清洗、转小写)
merges: 合并次数(越大,子词越长)
此函数仅用于演示BPE核心合并机制,不是完整实现
"""
# 初始:把句子视作字符列表,用空格隔开
# 例如 "data" -> ["d", "a", "t", "a"]
tokens = list(sentence.replace(" ", "▁")) # 用下划线表示空格
#▁可以视为空格符号,用来和字符区分
for _ in range(merges):
# 1. 统计所有相邻字符对频率
pairs = Counter()
for i in range(len(tokens)-1):
pair = (tokens[i], tokens[i+1])
pairs[pair] += 1
if not pairs:
break
# 2. 找到出现次数最多的字符对
best_pair = max(pairs, key=pairs.get)
# 3. 合并该字符对
# 在 tokens 中出现 (a,b) 的地方替换成 ab
merged = []
skip = False
for i in range(len(tokens)):
if skip:
skip = False
continue
if i < len(tokens)-1 and (tokens[i], tokens[i+1]) == best_pair:
merged.append(tokens[i] + tokens[i+1])
skip = True
else:
merged.append(tokens[i])
tokens = merged
return tokens
# 示例句子
example_sentence = "i love machine"
print("原句:", example_sentence)
bpe_result = bpe_tokenize(example_sentence, merges=3)
print("BPE切分结果:", bpe_result)
解释
- 在真实应用中,BPE 需要在整个语料库上迭代多次统计频率最高的字符对,并更新“子词”表,直至达到预期词表大小。
- 以上代码只是演示如何把相邻字符合并,不具有生产环境可用性;但可以帮助理解 BPE 的基本流程。
五、综合示例:完整预处理流程
结合前面的步骤,可以组合成为一个 pipeline。示例:
def preprocess_pipeline(text_list):
"""
综合使用:
1) 基础清洗 (含大小写转化、去HTML/URL/邮箱、去标点等)
2) 分词
3) 去停用词
4) Lemmatization
"""
# 1) 基础清洗
cleaned = [basic_cleaning(t) for t in text_list]
# 2) 分词
tokenized = [word_tokenize(t) for t in cleaned]
# 3) 去停用词
filtered = []
for tokens in tokenized:
filtered_tokens = [tok for tok in tokens if tok not in stop_words]
filtered.append(filtered_tokens)
# 4) 词形还原 (Lemmatization) 简化示例
final_output = []
for tokens in filtered:
lemmed = [lemmatizer.lemmatize(tok) for tok in tokens]
final_output.append(lemmed)
return final_output
processed_corpus = preprocess_pipeline(text_corpus)
for i, (orig, pro) in enumerate(zip(text_corpus, processed_corpus)):
print(f"原文 {i}: {orig}")
print(f"最终预处理结果: {pro}")
print("="*70)
解释
- 这里使用了前面定义的
basic_cleaning
,word_tokenize
,stop_words
和WordNetLemmatizer
。 - 在实际项目中,你可以进一步细化或调整步骤,例如是否需要大小写转换、是否需要词干化、是否保留数字符号等。
六、常见的注意事项
- 根据任务需求选择步骤:并非所有预处理步骤都需要。例如,在情感分析中,有时保留标点符号(如
!
、?
)可能帮助模型理解情绪表达;去除或保留停用词会影响模型效果,应当视情况调参。 - 语言差异:中文、阿拉伯语等语言不依赖空格分词,需要额外的分词工具(如
jieba
,pkuseg
,spaCy
中文模型等)。 - 大小写敏感性:命名实体识别、关键词搜索等任务,可能需要保留原有的大小写信息。
- 保留原始文本:有些复杂任务(如纠错、摘要)需要在模型预测后映射回原文本,应保留映射关系。
- 性能影响:预处理操作可能在大型数据集上耗费大量时间,需要考虑并行化(多进程、多线程)或分批预处理等策略。
七、总结
在 NLP 中,无论是传统机器学习、深度学习还是大模型(如 BERT/GPT)开发,都需要对文本数据做一定程度的预处理。这些预处理操作往往包括:
- 噪声清理(HTML、URL、邮箱、特殊字符等)
- 正则化(大小写统一、去除标点、数字处理等)
- 分词与停用词过滤(减少无关词)
- 词形还原或词干化(减少形态变体对模型的干扰)
- 子词切分(BPE/WordPiece 等)(对大模型极为重要)
具体使用哪种预处理步骤,需根据 任务目标、数据特点 来决定,并结合实验结果进行调整和优化。
以上所有代码都可以本地直接运行,所需外部数据仅用 Python 列表 text_corpus
模拟。同时,为了更好地运用预处理结果,建议在实际场景中对完整语料执行这些步骤,并做相应的性能评估。
至此,我们详尽介绍了 NLP 常见的文本预处理方法及其思路,并提供可运行的示例代码。希望能帮助你在文本数据清理、模型训练中快速上手并灵活改进。
【哈佛博后带小白玩转机器学习】 哔哩哔哩_bilibili
总课时超400+,时长75+小时