自然语言处理(一)—— NLP 入门与文本预处理
Chen Kai BOSS

人类每天产生的文本数据量惊人:社交媒体上的帖子、搜索引擎的查询、客服系统的对话、新闻报道、学术论文……这些文字背后蕴藏着巨大的价值,但计算机天生不理解人类语言。自然语言处理( Natural Language Processing, NLP)就是教会机器"读懂"文字的技术,让它们能从海量文本中提取信息、理解意图、生成回复,甚至进行创作。

本文从零开始介绍 NLP 的核心概念和第一步:文本预处理。你将了解 NLP 如何从符号规则走向深度学习,为什么需要对文本进行分词、去噪、标准化,以及如何用 Python 工具实现中英文预处理流程。最后通过一个完整的文本分类实战案例,把理论和代码串联起来。

NLP 是什么?从符号主义到大模型的演进之路

自然语言处理的本质

人类用语言交流时,一个简单的句子"我在银行取钱"包含多重信息:

  • 词汇语义:"银行"指金融机构还是河岸?
  • 句法结构:"我"是主语,"取钱"是动作
  • 语用意图:陈述事实还是请求帮助?

NLP 的核心任务是让计算机理解这些层次的信息,并在此基础上完成具体任务:

  • 文本分类:判断邮件是否是垃圾邮件
  • 信息抽取:从新闻中提取人名、地点、时间
  • 机器翻译:把英文文档翻译成中文
  • 问答系统:回答"特斯拉的创始人是谁?"
  • 文本生成:写诗、摘要、对话回复

NLP 发展的四个阶段

第一阶段:符号主义( 1950s-1980s)

早期研究者认为语言可以用规则描述。比如:

  • 句子 = 主语 + 谓语 + 宾语
  • 如果句子包含"不"、"没有",则情感为负面

这种方法依赖专家手工编写语法规则和词典。典型代表是 ELIZA( 1966 年的聊天机器人),它用模式匹配把用户的话改写后抛回去:

1
2
用户:"我很难过"
ELIZA:"为什么你很难过?"(直接套用模板)

局限性:语言现象太复杂,规则无法穷尽。"银行"的歧义、"真香"的反讽、"雨我无瓜"的网络用语……人工规则很快就力不从心。

第二阶段:统计方法( 1990s-2010s)

统计 NLP 不再依赖专家,而是让机器从大量文本中学习规律。核心思想:

  • 数据驱动:给模型看 10 万篇新闻,让它统计哪些词经常一起出现
  • 概率建模:用 表示句子的合理性

标志性成果:

  • N-gram 语言模型:根据前 个词预测下一个词 $$

P(w_i | w_1, , w_{i-1}) P(w_i | w_{i-n+1}, , w_{i-1})$$

  • 隐马尔可夫模型( HMM):用于词性标注
  • 朴素贝叶斯分类器:用于垃圾邮件过滤

这一时期的模型需要特征工程:人工设计输入特征(如词频、是否包含特定词、句子长度等),然后训练分类器。

第三阶段:深度学习( 2013-2020)

神经网络的兴起改变了 NLP 格局。关键技术:

  • Word2Vec( 2013):把词映射为稠密向量,语义相似的词向量接近 - 循环神经网络( RNN/LSTM):处理变长序列,捕捉上下文关系
  • 注意力机制( Attention):让模型关注输入的关键部分
  • Transformer( 2017):并行化训练,成为现代 NLP 的基石

深度学习模型可以端到端训练:直接输入原始文本,输出任务结果,不需要手工设计特征。

第四阶段:预训练大模型( 2018 至今)

2018 年 BERT 横空出世,开启了"预训练-微调"范式:

  1. 预训练:在海量无标注文本上训练通用语言模型
  2. 微调:在少量标注数据上针对具体任务调整

随后 GPT-3( 2020, 1750 亿参数)、 ChatGPT( 2022)、 GPT-4( 2023)不断刷新能力上限。现在的大模型具备:

  • 少样本学习:给几个例子就能理解新任务
  • 指令跟随:理解"用正式语气写一封道歉信"
  • 多模态能力:同时处理文本、图像、语音

NLP 的现实应用场景

搜索引擎

当你在 Google 搜索"苹果新品发布会"时, NLP 帮助:

  • 查询理解:识别"苹果"指公司而非水果
  • 相关性排序:把最相关的新闻排在前面
  • 摘要生成:在搜索结果下方显示关键信息

机器翻译

Google 翻译、 DeepL 用神经机器翻译( NMT)实现:

  • Encoder-Decoder 架构:把源语言编码为语义表示,再解码为目标语言
  • 注意力机制:翻译长句时关注对应的源词

智能客服

电商平台的客服机器人能:

  • 意图识别:判断用户是要退货、查物流还是咨询尺码
  • 槽位填充:提取订单号、商品名等关键信息
  • 对话管理:多轮交互引导用户解决问题

推荐系统

新闻 App 推荐你可能感兴趣的文章:

  • 文本表示:用词向量或 BERT 把文章编码为向量
  • 相似度计算:推荐与你已读文章相似的内容
  • 用户画像:根据历史阅读构建兴趣模型

情感分析

分析商品评论的情感倾向:

  • 正面:"质量很好,物流快!"
  • 负面:"颜色和描述不符,申请退货"
  • 中性:"包装完好无损"

企业可以据此监控品牌口碑、发现产品问题。

为什么需要文本预处理?

假设你要训练一个模型判断电影评论的情感。原始数据可能是这样:

1
2
3
"This movie is GREAT!!! 😊"
"i loved it, best film ever."
"Terrible... worst experience :("

直接把这些文本喂给模型会遇到问题:

  1. 大小写不统一:"GREAT" 和 "great" 被当作不同的词
  2. 标点和表情:"!!!" 和 "😊" 干扰了核心词汇
  3. 拼写和形态:"loved" 和 "love" 语义相同但形式不同
  4. 噪音词:"this"、"is"、"it" 等高频词对情感判断贡献不大

文本预处理就是清洗和标准化原始文本,提取有效信息,让模型更容易学习。

文本预处理的核心流程

标准化( Normalization)

把文本转为统一格式:

  • 小写化"GREAT""great"
  • 去除标点"loved it!!!""loved it"
  • 统一编码:处理全角半角、繁简体转换
1
2
3
4
5
6
7
8
import re

text = "This movie is GREAT!!! 😊"
# 小写化
text = text.lower()
# 去除标点和表情
text = re.sub(r'[^\w\s]', '', text)
print(text) # "this movie is great"

分词( Tokenization)

把句子切分为词或子词单元。英文用空格分词较简单,中文则需要专门算法。

英文分词

1
2
3
text = "I love machine learning"
tokens = text.split()
print(tokens) # ['I', 'love', 'machine', 'learning']

中文分词(稍后详述):

1
2
3
4
import jieba
text = "我喜欢自然语言处理"
tokens = jieba.lcut(text)
print(tokens) # ['我', '喜欢', '自然语言处理']

去除停用词( Stop Words Removal)

停用词是高频但语义贡献小的词,如"的"、"了"、"is"、"the"。去掉它们可以:

  • 减少特征维度
  • 突出关键词
1
2
3
4
5
6
from nltk.corpus import stopwords

stop_words = set(stopwords.words('english'))
tokens = ['i', 'love', 'machine', 'learning']
filtered = [w for w in tokens if w not in stop_words]
print(filtered) # ['love', 'machine', 'learning']

词干化与词形还原

把词的不同形态归为统一形式:

  • 词干化( Stemming):粗暴地截取词根
    • runningrun
    • happilyhappi(不是真实单词)
  • 词形还原( Lemmatization):基于词典还原为基础形式
    • runningrun
    • bettergood
1
2
3
4
5
6
7
8
from nltk.stem import PorterStemmer, WordNetLemmatizer

stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()

print(stemmer.stem("running")) # "run"
print(lemmatizer.lemmatize("running", pos='v')) # "run"
print(lemmatizer.lemmatize("better", pos='a')) # "good"

词形还原更准确但速度慢,词干化快但有时产生不存在的词。实际中根据任务选择。

中文分词:挑战与工具

中文分词的特殊性

英文单词间有空格,中文则是连续的字符流。"我爱自然语言处理"可以有多种切分方式:

  • ["我", "爱", "自然", "语言", "处理"](字级别)
  • ["我", "爱", "自然语言", "处理"]
  • ["我", "爱", "自然语言处理"](词级别)

正确的分词需要理解语义。考虑歧义:"结婚的和尚未结婚的"

  • 错误分词:["结婚", "的", "和", "尚未", "结婚", "的"]
  • 正确分词:["结婚", "的", "和", "尚", "未", "结婚", "的"]

主流中文分词工具

jieba(结巴分词)

最流行的 Python 中文分词库,基于前缀词典和动态规划。

安装

1
pip install jieba

基本用法

jieba 分词提供了三种切分模式,每种模式适用于不同的场景。理解这些模式的区别对于选择合适的分词策略至关重要。

模式对比:精确模式追求准确性,每个词只出现一次,适合文本分析和分类任务;全模式输出所有可能的词组合,适合关键词提取和词云生成;搜索引擎模式在精确模式基础上对长词进行细粒度切分,提高召回率,适合搜索场景。

设计原理: jieba 基于前缀词典和动态规划算法,通过计算最大概率路径来确定最佳分词结果。精确模式选择概率最大的路径,全模式保留所有可能的路径,搜索引擎模式则对长词进行二次切分以增加匹配机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import jieba

# 示例文本:包含复合词"清华大学"(这是一个专有名词)
text = "我来到北京清华大学"

# ========== 模式 1:精确模式(默认,推荐) ==========
# cut_all=False: 精确模式,只输出概率最大的分词结果
# 特点:
# - 每个词只出现一次,无冗余
# - 分词结果最准确,适合大多数 NLP 任务
# - 速度较快
seg_list = jieba.cut(text, cut_all=False)
# 返回:生成器对象(惰性计算,节省内存)
# 注意: jieba.cut()返回生成器, jieba.lcut()返回列表

print("精确模式:", "/".join(seg_list))
# 输出:"我/来到/北京/清华大学"
# 解释:
# - "清华大学"被识别为一个整体(专有名词)
# - 没有冗余切分(如"清华"+"大学")

# ========== 模式 2:全模式 ==========
# cut_all=True: 全模式,输出所有可能的词组合
# 特点:
# - 包含所有可能的切分方式,有冗余
# - 适合关键词提取、词云生成等需要更多候选词的场景
# - 速度较慢,结果可能包含不存在的词
seg_list = jieba.cut(text, cut_all=True)
print("全模式:", "/".join(seg_list))
# 输出:"我/来到/北京/清华/清华大学/华大/大学"
# 解释:
# - "清华大学"被切分为多种组合:"清华"+"大学"+"清华大学"
# - "华大"是"清华"+"大学"的重叠部分,可能不是真实词
# - 适合需要更多候选词的场景(如关键词提取)

# ========== 模式 3:搜索引擎模式 ==========
# cut_for_search(): 搜索引擎模式,对长词进行细粒度切分
# 特点:
# - 在精确模式基础上,对长词( 3 字及以上)进行二次切分
# - 提高召回率:用户搜索"清华"也能匹配到"清华大学"
# - 适合搜索引擎、信息检索等场景
seg_list = jieba.cut_for_search(text)
print("搜索引擎模式:", "/".join(seg_list))
# 输出:"我/来到/北京/清华/华大/大学/清华大学"
# 解释:
# - "清华大学"被切分为:"清华"+"大学"+"清华大学"
# - 这样用户搜索"清华"或"大学"都能匹配到包含"清华大学"的文档
# - 比全模式更精确(不会切分短词)

# ========== 性能对比 ==========
# 精确模式:速度最快,结果最准确,推荐用于大多数任务
# 全模式:速度较慢,结果有冗余,适合关键词提取
# 搜索引擎模式:速度中等,结果平衡,适合搜索场景

# ========== 使用建议 ==========
# 1. 文本分类、情感分析:使用精确模式( cut_all=False)
# 2. 关键词提取、词云:使用全模式( cut_all=True)或搜索引擎模式
# 3. 搜索引擎、信息检索:使用搜索引擎模式( cut_for_search)
# 4. 不确定时:使用精确模式,这是最安全和通用的选择

深入解读: jieba 分词的原理与优化

jieba 分词看似简单,但背后有复杂的算法和设计考虑:

1. 分词算法原理

jieba 使用基于前缀词典的动态规划算法

  • 前缀词典:存储所有可能的词及其频率,用于计算分词路径的概率
  • 动态规划:计算从句子开头到当前位置的最大概率路径
  • HMM 模型:处理未登录词( OOV),识别新词

算法流程: 1. 构建有向无环图( DAG),每个节点表示可能的词 2. 使用动态规划计算最大概率路径 3. 对未登录词使用 HMM 模型识别

2. 三种模式的技术细节

模式 算法差异 时间复杂度 适用场景
精确模式 选择概率最大的单一路径 文本分类、 NER
全模式 保留所有可能的路径 关键词提取
搜索引擎模式 精确模式 + 长词二次切分 信息检索

其中 是句子长度, 是可能的词数, 是长词数量。

3. 自定义词典的使用

jieba 允许添加自定义词典,这对领域特定文本很重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 方法 1:添加单个词
jieba.add_word("自然语言处理", freq=1000, tag="n")
# freq: 词频(越高越可能被切分出来)
# tag: 词性标签

# 方法 2:加载词典文件
jieba.load_userdict("userdict.txt")
# 文件格式:每行一个词,格式为:词 词频 词性(可选)
# 示例:自然语言处理 1000 n

# 方法 3:动态调整词频
jieba.suggest_freq("自然语言处理", True)
# 强制将"自然语言处理"作为一个词

4. 分词准确率的提升

jieba 的默认准确率约 95%,可以通过以下方式提升:

  • 添加领域词典:针对特定领域(如医疗、法律)添加专业术语
  • 调整词频:对于歧义切分,提高正确切分的词频
  • 使用 pkuseg:对于特定领域, pkuseg 的准确率可能更高
  • 后处理规则:添加规则处理特定模式(如日期、数字)

5. 性能优化

对于大规模文本处理,可以优化:

1
2
3
4
5
6
7
8
9
10
# 方法 1:并行分词( jieba 支持多进程)
import jieba
jieba.enable_parallel(4) # 开启 4 进程并行

# 方法 2:批量处理
texts = ["文本 1", "文本 2", ...]
results = [list(jieba.cut(text)) for text in texts]

# 方法 3:使用 lcut()而非 cut()(如果不需要生成器)
# lcut()返回列表,适合小文本; cut()返回生成器,适合大文本

6. 常见问题与解决方案

问题 原因 解决方案
专有名词被拆分 词典中未包含该词 使用 add_word()添加
分词结果不一致 词典加载顺序或词频变化 固定词典和参数
新词无法识别 HMM 模型未训练 使用更大的语料训练或手动添加
处理速度慢 文本过长或未并行 开启并行处理或分段处理
歧义切分错误 词频设置不当 调整词频或使用规则

7. 与其他分词工具的对比

工具 速度 准确率 领域适应性 推荐场景
jieba 中等(~95%) 一般 通用文本、快速原型
pkuseg 中等 高(~97%) 好(有领域模型) 专业领域文本
THULAC 一般 学术研究
LAC 中等 一般 需要词性和 NER

8. 实际应用建议

  1. 默认使用精确模式:除非有特殊需求,否则使用cut_all=False
  2. 添加领域词典:针对特定领域添加专业术语词典
  3. 处理歧义:对于已知的歧义切分,使用add_word()suggest_freq()调整
  4. 性能监控:记录分词时间和准确率,根据需求选择工具
  5. 版本控制: jieba 的版本更新可能改变分词结果,固定版本号

理解 jieba 分词的原理和使用技巧,是中文 NLP 的基础。在实际项目中,根据任务需求选择合适的模式和参数,能够显著提升系统性能。

添加自定义词典

1
2
3
4
jieba.add_word("自然语言处理")
text = "我在学习自然语言处理"
print("/".join(jieba.cut(text)))
# "我/在/学习/自然语言处理"

pkuseg

北京大学开源的分词工具,针对不同领域训练了专用模型(新闻、医疗、旅游等),准确率更高但速度慢。

1
2
3
4
5
6
7
8
9
10
import pkuseg

seg = pkuseg.pkuseg() # 使用默认模型
text = "我爱自然语言处理"
print(seg.cut(text)) # ['我', '爱', '自然语言处理']

# 使用领域模型
seg_med = pkuseg.pkuseg(model_name='medicine')
text_med = "患者出现高血压症状"
print(seg_med.cut(text_med)) # ['患者', '出现', '高血压', '症状']

LAC( Lexical Analysis of Chinese)

百度开源的词法分析工具,不仅分词还能进行词性标注和命名实体识别。

1
2
3
4
5
6
7
8
9
10
11
from LAC import LAC

lac = LAC(mode='seg') # 只分词
text = "百度是一家高科技公司"
print(lac.run(text)) # ['百度', '是', '一家', '高科技', '公司']

# 分词 + 词性标注
lac = LAC(mode='lac')
seg_result, pos_result = lac.run(text)
print(list(zip(seg_result, pos_result)))
# [('百度', 'ORG'), ('是', 'v'), ('一家', 'm'), ('高科技', 'n'), ('公司', 'n')]

中文预处理完整流程

中文文本预处理面临独特的挑战:没有天然的词边界,需要先分词才能进行后续处理。与英文不同,中文的标点、数字、英文单词可能混在一起,需要统一处理。此外,中文停用词(如"的"、"了")虽然高频但语义贡献小,去除它们可以突出关键词,减少特征维度。

本代码实现了一个完整的中文预处理流程,包含四个核心步骤:文本清洗、分词、停用词过滤和单字过滤。每个步骤都有其设计考虑:正则表达式保留中英数字和空格, jieba 分词处理中文特有的歧义问题,停用词表过滤高频低义词,单字过滤则进一步精简输出。这个流程平衡了信息保留和噪声去除,适用于大多数中文 NLP 任务。

设计思路:采用管道式设计,每个步骤的输出作为下一步的输入,便于调试和扩展。停用词使用集合( set)而非列表,查找时间复杂度从 降到 ,提升处理速度。单字过滤是可选的,因为单字词(如"我"、"你")在某些任务中可能有用,但在关键词提取、主题建模等任务中通常噪声较大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import jieba
import re

def preprocess_chinese(text):
"""
中文文本预处理函数

功能:将原始中文文本转换为清洗后的词列表,去除标点、停用词和单字词

参数:
text (str): 原始中文文本,可能包含标点、数字、英文等混合内容

返回:
list[str]: 预处理后的词列表,每个元素是一个词(长度>=2)

处理流程:
1. 文本清洗:去除标点和特殊字符,保留中文、英文、数字和空格
2. 分词:使用 jieba 将文本切分为词序列
3. 停用词过滤:去除高频低义词和空白词
4. 单字过滤:去除长度小于 2 的词(可选步骤)

示例:
>>> text = "我在北京的清华大学学习自然语言处理,这是一门很有趣的课程!"
>>> preprocess_chinese(text)
['北京', '清华大学', '学习', '自然语言处理', '一门', '有趣', '课程']
"""
# 步骤 1:去除标点和特殊字符
# 正则表达式 [^\u4e00-\u9fa5a-zA-Z0-9\s] 的含义:
# - ^ 表示取反(不匹配后面的字符集)
# - \u4e00-\u9fa5 是中文字符的 Unicode 范围( CJK 统一汉字)
# - a-zA-Z 匹配所有英文字母(大小写)
# - 0-9 匹配数字
# - \s 匹配空白字符(空格、制表符等)
# 整体含义:保留中英数字和空格,删除其他所有字符(包括标点、 emoji 等)
text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s]', '', text)
# 结果示例:"我在北京的清华大学学习自然语言处理这是一门很有趣的课程"

# 步骤 2:中文分词
# jieba.lcut() 返回词列表,使用精确模式(默认)
# 分词算法:基于前缀词典和动态规划,处理歧义切分
# 例如:"自然语言处理"会被识别为一个整体,而不是"自然"+"语言"+"处理"
tokens = jieba.lcut(text)
# 结果示例:['我', '在', '北京', '的', '清华大学', '学习', '自然语言处理', '这', '是', '一门', '很', '有趣', '的', '课程']

# 步骤 3:去除停用词和空白词
# 停用词( stopwords):高频但语义贡献小的词,如"的"、"了"、"在"等
# 使用集合( set)而非列表,查找时间复杂度从 O(n)降到 O(1)
# w.strip() 检查词是否为空字符串(去除首尾空白后)
stopwords = set(['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都'])
tokens = [w for w in tokens if w not in stopwords and w.strip()]
# 过滤后:['北京', '清华大学', '学习', '自然语言处理', '这', '一门', '很', '有趣', '课程']

# 步骤 4:过滤单字词(可选步骤)
# 单字词(如"这"、"很")通常对语义贡献较小,过滤后可以:
# - 减少特征维度
# - 突出多字词(通常语义更明确)
# 注意:某些任务(如情感分析)可能需要保留单字词,此时应注释掉这一步
tokens = [w for w in tokens if len(w) > 1]
# 最终结果:['北京', '清华大学', '学习', '自然语言处理', '一门', '有趣', '课程']

return tokens

# 使用示例
text = "我在北京的清华大学学习自然语言处理,这是一门很有趣的课程!"
print(preprocess_chinese(text))
# 输出:['北京', '清华大学', '学习', '自然语言处理', '一门', '有趣', '课程']

深入解读:设计权衡与常见问题

这个预处理流程看似简单,但每个步骤都涉及重要的设计决策。让我们深入分析:

1. 正则表达式的选择

正则 [^\u4e00-\u9fa5a-zA-Z0-9\s] 看似全面,但实际应用中可能遇到问题:

  • 问题 1:保留英文和数字是否必要? 如果任务是纯中文文本分析,可以改为 [^\u4e00-\u9fa5\s],但现代中文文本常混有英文术语(如"NLP"、"AI"),保留它们可能更有价值。
  • 问题 2:如何处理 URL 和邮箱? 当前正则会保留 URL 中的字符,导致"https://example.com"被切分为多个词。更好的做法是先识别并替换为特殊标记(如<URL>),再清洗。
  • 问题 3: emoji 和特殊符号 当前正则会删除 emoji,但情感分析任务中 emoji 可能包含重要信息。需要根据任务调整。

2. 分词工具的选择

jieba 是最流行的中文分词工具,但并非唯一选择:

工具 优点 缺点 适用场景
jieba 速度快、易用、社区活跃 准确率中等,领域适应性一般 通用文本、快速原型
pkuseg 准确率高,有领域模型 速度较慢,需要下载模型 专业领域(医疗、新闻)
LAC 支持词性标注和 NER 依赖百度生态 需要词性信息的任务

3. 停用词表的构建

代码中使用了硬编码的停用词表,但实际应用中应该:

  • 加载外部停用词表:使用标准停用词库(如哈工大停用词表、百度停用词表),包含数百到数千个词。
  • 任务相关调整:情感分析任务中,"不"、"没有"等否定词不应被过滤;问答任务中,"谁"、"什么"等疑问词应保留。
  • 动态构建:根据语料统计词频,自动识别高频低义词加入停用词表。

4. 单字过滤的权衡

过滤单字词是一把双刃剑:

  • 优点:减少噪声,突出多字词(通常语义更明确)
  • 缺点:可能丢失重要信息,如"好"、"坏"在情感分析中很重要

建议:对于关键词提取、主题建模等任务,过滤单字词;对于情感分析、文本分类等任务,保留单字词或使用更精细的过滤规则。

5. 性能优化

当前实现每次调用都创建停用词集合,可以优化为模块级变量:

1
2
3
4
5
6
7
# 模块级停用词集合(只创建一次)
_STOPWORDS = set(['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都'])

def preprocess_chinese(text):
# ... 其他代码 ...
tokens = [w for w in tokens if w not in _STOPWORDS and w.strip()]
# ...

6. 常见问题排查

问题 原因 解决方案
分词结果不理想 jieba 词典未包含领域术语 使用jieba.add_word()添加自定义词
停用词过滤过度 停用词表包含任务相关词 根据任务调整停用词表
处理速度慢 文本过长或停用词表过大 使用生成器、批量处理、优化数据结构
单字词丢失 单字过滤过于激进 根据任务决定是否过滤单字词

7. 扩展建议

实际项目中,可以考虑以下扩展:

  • 文本规范化:繁体转简体(使用opencc库)、全角转半角
  • 数字处理:统一数字表示(如"100"、"一百"都转为"100")
  • 英文处理:英文词转小写、词形还原
  • 新词识别:使用更先进的分词工具(如基于 BERT 的分词)识别新词
  • 错误处理:添加异常处理,处理空输入、 None 值等边界情况

这个预处理流程是中文 NLP 的基础,理解每个步骤的设计考虑和权衡,有助于在实际项目中做出正确的选择。

英文文本预处理: NLTK 与 spaCy

NLTK( Natural Language Toolkit)

经典的 NLP 工具包,适合学习和原型开发。

安装与下载资源

1
2
pip install nltk
python -c "import nltk; nltk.download('punkt'); nltk.download('stopwords'); nltk.download('wordnet')"

完整预处理流程

英文文本预处理相比中文更直接,因为单词之间有天然的空格分隔。但英文也有其复杂性:大小写、标点、缩写(如"I'm")、词形变化(如"running"→"run")等都需要处理。本代码实现了一个标准的英文预处理流程,使用 NLTK 库完成分词、停用词过滤和词形还原。

核心步骤解析:小写化统一格式, word_tokenize 处理缩写和标点, isalnum()过滤非字母数字字符,停用词过滤去除高频低义词,词形还原将词的不同形态统一为基础形式。这个流程适用于大多数英文 NLP 任务,但需要根据具体任务调整:情感分析可能需要保留标点(如"!!!"表示强调),命名实体识别不能小写化("Apple"公司和"apple"水果不同)。

设计考虑:使用词形还原而非词干化,因为词形还原返回真实单词,更适合下游任务。词性标注( pos='v')确保动词正确还原,但实际应用中可能需要更复杂的词性标注流程。停用词使用集合而非列表,提升查找效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer

def preprocess_english(text):
"""
英文文本预处理函数

功能:将原始英文文本转换为清洗后的词列表,统一大小写、去除标点、停用词和词形还原

参数:
text (str): 原始英文文本,可能包含大小写混合、标点、缩写等

返回:
list[str]: 预处理后的词列表,每个元素是词形还原后的词

处理流程:
1. 小写化:统一转换为小写,消除大小写差异
2. 分词:使用 NLTK 的 word_tokenize 处理缩写和标点
3. 去除标点:只保留字母数字字符
4. 停用词过滤:去除高频低义词(如"the"、"is"、"a")
5. 词形还原:将词的不同形态还原为基础形式(如"running"→"run")

示例:
>>> text = "I'm learning Natural Language Processing! It's amazing."
>>> preprocess_english(text)
['learning', 'natural', 'language', 'processing', 'amazing']

注意:
- 词形还原需要词性信息,这里简化为动词( pos='v')
- 实际应用中应使用词性标注器(如 nltk.pos_tag)获取准确词性
- 某些任务(如情感分析)可能需要保留标点和大小写
"""
# 步骤 1:小写化
# 将所有字符转换为小写,统一格式
# 优点:消除大小写差异("Apple"和"apple"被视为同一词)
# 缺点:丢失专有名词信息("Apple"公司和"apple"水果无法区分)
# 适用场景:文本分类、信息检索等不需要区分专有名词的任务
text = text.lower()
# 结果示例:"i'm learning natural language processing! it's amazing."

# 步骤 2:分词( Tokenization)
# word_tokenize() 是 NLTK 的智能分词器,能够:
# - 处理缩写:"I'm" → ["I", "'m"] 或 ["I'm"](取决于版本)
# - 分离标点:"amazing." → ["amazing", "."]
# - 处理数字和特殊字符
# 相比简单的 split(), word_tokenize 更准确但速度较慢
tokens = word_tokenize(text)
# 结果示例:["i", "'m", "learning", "natural", "language", "processing", "!", "it", "'s", "amazing", "."]

# 步骤 3:去除标点和特殊字符
# isalnum() 检查字符串是否只包含字母和数字
# 过滤掉标点符号(如"!", ".", "'")和纯标点 token
# 注意:这也会过滤掉纯数字(如"2024"),某些任务可能需要保留
tokens = [w for w in tokens if w.isalnum()]
# 过滤后:["i", "m", "learning", "natural", "language", "processing", "it", "s", "amazing"]
# 注意:"'m"被拆分为"m","'s"被拆分为"s",这些会在停用词过滤时去除

# 步骤 4:去除停用词( Stop Words Removal)
# stopwords.words('english') 返回 NLTK 内置的英文停用词列表(约 179 个词)
# 停用词包括:冠词("a", "the")、代词("I", "you")、助动词("is", "are")等
# 使用集合( set)而非列表,查找时间复杂度从 O(n)降到 O(1)
# 注意:某些任务(如情感分析)中,"not"、"no"等否定词不应被过滤
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if w not in stop_words]
# 过滤后:["learning", "natural", "language", "processing", "amazing"]

# 步骤 5:词形还原( Lemmatization)
# WordNetLemmatizer 将词的不同形态还原为词典形式( lemma)
# 例如:"running" → "run", "better" → "good"(需要指定 pos='a')
# pos='v' 指定词性为动词,但这里简化处理,实际应用中应使用词性标注
# 词形还原 vs 词干化:
# - 词形还原:返回真实单词,更准确但需要词性信息
# - 词干化:快速但可能返回不存在的词(如"studies" → "studi")
lemmatizer = WordNetLemmatizer()
# 注意:这里假设所有词都是动词,实际应用中应使用 nltk.pos_tag()获取词性
tokens = [lemmatizer.lemmatize(w, pos='v') for w in tokens]
# 结果:["learn", "natural", "language", "process", "amaze"]
# 注意:"natural"和"language"是名词, pos='v'不会改变它们

return tokens

# 使用示例
text = "I'm learning Natural Language Processing! It's amazing."
print(preprocess_english(text))
# 输出:['learn', 'natural', 'language', 'process', 'amaze']
# 注意:实际输出可能因 NLTK 版本和词性标注不同而略有差异

深入解读:英文预处理的细节与优化

英文预处理看似简单,但每个步骤都有其复杂性和权衡:

1. 小写化的权衡

小写化是最常见的预处理步骤,但并非总是合适:

  • 应该小写化:文本分类、信息检索、主题建模等任务,其中"Apple"和"apple"应被视为相关
  • 不应小写化:命名实体识别( NER)、情感分析("LOVE"和"love"语气不同)、某些专有名词相关的任务

改进方案:根据任务选择性地小写化,或使用更智能的方法(如保留首字母大写的词作为候选专有名词)。

2. 分词器的选择

word_tokenize() 是 NLTK 的标准分词器,但还有其他选择:

分词器 优点 缺点 适用场景
word_tokenize 处理缩写和标点准确 速度较慢 通用文本
WhitespaceTokenizer 速度快 不处理缩写 简单文本
TweetTokenizer 处理社交媒体文本 需要单独导入 Twitter 、微博等
spaCy tokenizer 速度快、准确 需要安装 spaCy 生产环境

3. 标点处理的复杂性

代码中使用 isalnum() 过滤标点,但这可能丢失重要信息:

  • 问题 1:情感标点 "amazing!!!" 中的多个感叹号表达强烈情感,删除后丢失信息
  • 问题 2: URL 和邮箱 "visit https://example.com" 中的 URL 会被拆分
  • 问题 3:数字和单位 "" 和 "100"

改进方案

1
2
3
4
5
6
7
8
9
# 更精细的标点处理
import re

# 保留 URL 和邮箱
text = re.sub(r'http\S+|www\.\S+', '<URL>', text)
text = re.sub(r'\S+@\S+', '<EMAIL>', text)

# 保留货币符号和数字
tokens = [w for w in tokens if w.isalnum() or re.match(r'^[\$€£¥]\d+', w)]

4. 停用词表的定制

NLTK 的默认停用词表包含 179 个词,但可能需要调整:

  • 情感分析:保留否定词("not", "no", "never")和程度词("very", "extremely")
  • 问答系统:保留疑问词("what", "who", "where", "when", "why", "how")
  • 领域特定:某些领域的高频词可能不是通用停用词

改进方案

1
2
3
4
5
6
# 自定义停用词表
base_stopwords = set(stopwords.words('english'))
# 移除不应过滤的词
custom_stopwords = base_stopwords - {'not', 'no', 'never', 'very'}
# 添加领域特定停用词
custom_stopwords.update({'said', 'according', 'reported'})

5. 词形还原的准确性

代码中简化了词性标注,但词形还原的准确性很大程度上依赖于词性:

  • 问题lemmatize(w, pos='v') 假设所有词都是动词,导致名词和形容词无法正确还原
  • 示例:"better" 作为形容词应还原为 "good",但 pos='v' 不会改变它

改进方案:使用词性标注器获取准确词性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from nltk import pos_tag
from nltk.corpus import wordnet

def get_wordnet_pos(treebank_tag):
"""将 NLTK 词性标签转换为 WordNet 词性标签"""
if treebank_tag.startswith('J'):
return wordnet.ADJ
elif treebank_tag.startswith('V'):
return wordnet.VERB
elif treebank_tag.startswith('N'):
return wordnet.NOUN
elif treebank_tag.startswith('R'):
return wordnet.ADV
else:
return wordnet.NOUN # 默认

# 改进的词形还原
pos_tags = pos_tag(tokens)
tokens = [lemmatizer.lemmatize(w, pos=get_wordnet_pos(pos))
for w, pos in pos_tags]

6. 性能优化

当前实现在每次调用时都创建停用词集合和词形还原器,可以优化:

1
2
3
4
5
6
7
8
9
# 模块级初始化(只执行一次)
_STOPWORDS = set(stopwords.words('english'))
_LEMMATIZER = WordNetLemmatizer()

def preprocess_english(text):
# ... 其他代码 ...
tokens = [w for w in tokens if w not in _STOPWORDS]
tokens = [_LEMMATIZER.lemmatize(w, pos='v') for w in tokens]
# ...

7. 常见问题与解决方案

问题 原因 解决方案
缩写处理不当 word_tokenize 可能将"I'm"拆分为["I", "'m"] 使用 TweetTokenizer 或自定义规则
词形还原不准确 未使用正确的词性 使用 pos_tag()获取词性后再还原
专有名词丢失 小写化导致专有名词信息丢失 根据任务决定是否小写化,或使用 NER 识别专有名词
数字被过滤 isalnum()可能过滤某些数字格式 使用更精细的正则表达式
处理速度慢 词形还原和词性标注耗时 考虑使用词干化(更快)或批量处理

8. 与 spaCy 的对比

spaCy 是另一个流行的 NLP 库,预处理流程更简洁:

1
2
3
4
5
6
7
8
import spacy
nlp = spacy.load("en_core_web_sm")

def preprocess_with_spacy(text):
doc = nlp(text.lower())
tokens = [token.lemma_ for token in doc
if not token.is_stop and not token.is_punct and token.is_alpha]
return tokens

对比: - NLTK:更灵活,适合学习和研究,但需要手动组合多个步骤 - spaCy:更快速,一体化处理,但定制性稍弱

选择哪个取决于项目需求:快速原型用 spaCy,需要精细控制用 NLTK 。

9. 实际应用建议

在实际项目中,预处理流程应该:

  1. 根据任务调整:没有"一刀切"的预处理流程,必须根据具体任务定制
  2. 保留中间结果:保存原始文本和每个步骤的结果,便于调试和回滚
  3. 版本控制:记录预处理参数和版本,确保实验可复现
  4. 性能监控:记录处理时间和内存使用,优化瓶颈步骤
  5. 错误处理:处理空输入、 None 值、编码错误等边界情况

这个预处理流程是英文 NLP 的基础,理解每个步骤的原理和权衡,能够帮助你在实际项目中做出正确的选择。

spaCy

工业级 NLP 库,速度快、功能强、支持多语言。

安装与下载模型

1
2
pip install spacy
python -m spacy download en_core_web_sm

使用 spaCy 预处理

spaCy 是工业级的 NLP 库,相比 NLTK,它提供了更一体化、更快速的预处理流程。 spaCy 的核心优势在于将分词、词性标注、词形还原、命名实体识别等功能集成在一个管道中,通过一次处理完成所有任务。

设计理念: spaCy 采用管道( pipeline)架构,每个组件( tokenizer 、 tagger 、 parser 、 ner 等)按顺序处理文档,中间结果自动传递。这种设计避免了 NLTK 中需要手动组合多个步骤的复杂性,同时通过 Cython 优化实现了更高的处理速度。

核心优势: 1)速度优势: spaCy 用 Cython 编写,比纯 Python 的 NLTK 快 10-100 倍; 2)一体化处理:一次调用完成分词、词性标注、词形还原等; 3)丰富的语言模型:支持 60+种语言,包括中文; 4)生产就绪: API 设计简洁,适合生产环境部署。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import spacy

# 加载英文模型
# en_core_web_sm: 小型英文模型(约 12MB,速度快但准确率稍低)
# 其他选择:
# - en_core_web_md: 中型模型(约 40MB,包含词向量)
# - en_core_web_lg: 大型模型(约 560MB,最准确但速度慢)
nlp = spacy.load("en_core_web_sm")
# 注意:首次使用需要先下载模型: python -m spacy download en_core_web_sm

def preprocess_with_spacy(text):
"""
使用 spaCy 进行文本预处理

功能:一体化完成分词、词性标注、词形还原、停用词过滤

参数:
text (str): 原始文本

返回:
list[str]: 预处理后的词列表(词形还原后的词)

处理流程:
1. nlp()处理文本,返回 Doc 对象(包含所有 NLP 信息)
2. 遍历每个 token(词),提取词形还原形式
3. 过滤停用词、标点和非字母字符

spaCy 的 token 属性:
- token.text: 原始文本
- token.lemma_: 词形还原形式
- token.pos_: 词性标签(如 NOUN, VERB)
- token.is_stop: 是否为停用词
- token.is_punct: 是否为标点
- token.is_alpha: 是否为字母字符
- token.ent_type_: 命名实体类型(如果有)
"""
# 小写化文本( spaCy 保留原始大小写,需要手动处理)
text_lower = text.lower()

# nlp()处理文本,返回 Doc 对象
# Doc 对象包含: tokens(词列表)、 sentences(句子列表)、 ents(命名实体)等
# 处理过程包括:分词、词性标注、依存句法分析、命名实体识别等
doc = nlp(text_lower)
# doc 是 Doc 对象,可以迭代获取每个 token

# 列表推导式:提取词形还原形式,过滤停用词、标点和非字母字符
tokens = [
token.lemma_ # 词形还原形式(如"learning" → "learn")
for token in doc # 遍历文档中的每个 token
if not token.is_stop # 不是停用词
and not token.is_punct # 不是标点符号
and token.is_alpha # 是字母字符(过滤数字等)
]
# 结果:['learn', 'natural', 'language', 'process', 'amaze']
# 注意:"processing"被还原为"process","amazing"被还原为"amaze"

return tokens

# 使用示例
text = "I'm learning Natural Language Processing! It's amazing."
result = preprocess_with_spacy(text)
print(result)
# 输出:['learn', 'natural', 'language', 'process', 'amaze']

# ========== spaCy 的高级功能 ==========

# 1. 词性标注( Part-of-Speech Tagging)
doc = nlp("I'm learning Natural Language Processing!")
for token in doc:
print(f"{token.text:15} {token.pos_:10} {token.tag_:10} {token.lemma_:15}")
# 输出:
# I PRON PRP I
# 'm AUX VBP be
# learning VERB VBG learn
# Natural ADJ JJ natural
# Language NOUN NN language
# Processing NOUN NN processing
# ! PUNCT . !

# 2. 命名实体识别( Named Entity Recognition)
text = "Apple is looking at buying U.K. startup for $1 billion"
doc = nlp(text)

print("\n 命名实体识别:")
for ent in doc.ents:
print(f"{ent.text:15} {ent.label_:10} {spacy.explain(ent.label_)}")
# 输出:
# Apple ORG Companies, agencies, institutions
# U.K. GPE Countries, cities, states
# $1 billion MONEY Monetary values

# 3. 依存句法分析( Dependency Parsing)
doc = nlp("The cat sat on the mat")
for token in doc:
print(f"{token.text:10} {token.dep_:10} {token.head.text}")
# 输出:
# The det cat
# cat nsubj sat
# sat ROOT sat
# on prep sat
# the det mat
# mat pobj on

# 4. 批量处理(提高效率)
texts = [
"I'm learning NLP",
"Natural language processing is amazing",
"spaCy is fast and efficient"
]
# nlp.pipe()返回生成器,批量处理文本
# batch_size: 每批处理的文本数量
# n_process: 并行进程数(需要先调用 nlp.disable_pipes()禁用某些组件)
docs = list(nlp.pipe(texts, batch_size=2))
for doc in docs:
tokens = [token.lemma_ for token in doc if not token.is_stop and token.is_alpha]
print(tokens)

深入解读: spaCy 的设计哲学与性能优化

spaCy 的设计哲学是"生产优先",这体现在其架构和 API 设计的各个方面:

1. 管道架构的优势

spaCy 使用管道( pipeline)架构,组件按顺序处理:

1
2
3
4
5
6
7
# 查看当前管道的组件
print(nlp.pipe_names)
# 输出:['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']

# 禁用某些组件以提高速度(如果不需要)
nlp = spacy.load("en_core_web_sm", disable=['parser', 'ner'])
# 只保留分词、词性标注和词形还原,速度提升 2-3 倍

2. 模型选择策略

spaCy 提供不同大小的模型,需要权衡速度和准确率:

模型 大小 速度 准确率 包含内容 适用场景
sm ~12MB 最快 中等 基础 NLP 组件 快速原型、大规模处理
md ~40MB 中等 较高 sm + 词向量 需要词向量的任务
lg ~560MB 较慢 最高 md + 更大词向量 追求最佳性能

3. 性能优化技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 技巧 1:批量处理(比循环快 10-100 倍)
texts = ["文本 1", "文本 2", ...]
docs = list(nlp.pipe(texts, batch_size=1000))

# 技巧 2:禁用不需要的组件
nlp = spacy.load("en_core_web_sm", disable=['parser', 'ner'])

# 技巧 3:使用多进程(需要先禁用某些组件)
docs = list(nlp.pipe(texts, n_process=4))

# 技巧 4:只处理需要的属性(减少内存)
for doc in nlp.pipe(texts):
# 只访问需要的属性,避免加载所有信息
tokens = [token.text for token in doc]

4. 中文支持

spaCy 支持中文,但需要下载中文模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 下载中文模型
# python -m spacy download zh_core_web_sm

import spacy
nlp = spacy.load("zh_core_web_sm")

text = "我在北京的清华大学学习自然语言处理"
doc = nlp(text)

# 中文分词
tokens = [token.text for token in doc]
print(tokens)
# 输出:['我', '在', '北京', '的', '清华大学', '学习', '自然语言处理']

# 词性标注
for token in doc:
print(f"{token.text} {token.pos_}")

5. 与 NLTK 的详细对比

特性 NLTK spaCy
速度 慢(纯 Python) 快( Cython 优化)
API 设计 函数式,需要手动组合 面向对象,一体化
模型 需要单独下载数据包 模型包含所有组件
定制性 高(可以精细控制) 中等(通过配置)
学习曲线 陡峭(需要理解多个模块) 平缓( API 简洁)
生产部署 需要较多配置 开箱即用
社区支持 学术导向 工业导向

6. 常见问题与解决方案

问题 原因 解决方案
模型下载失败 网络问题或权限问题 使用镜像源或手动下载
内存占用大 加载了大型模型 使用 sm 模型或禁用不需要的组件
处理速度慢 未使用批量处理 使用 nlp.pipe()批量处理
中文分词不准 中文模型较小 使用 jieba 或 pkuseg 进行分词, spaCy 处理其他任务
自定义规则 默认规则不满足需求 使用 Matcher 或 PhraseMatcher 添加规则

7. 实际应用建议

  1. 模型选择:快速原型用 sm,生产环境根据需求选择 md 或 lg
  2. 批量处理:始终使用nlp.pipe()而非循环调用nlp()
  3. 组件禁用:不需要的功能(如 NER 、 parser)应禁用以提升速度
  4. 内存管理:处理大规模数据时,使用生成器而非列表
  5. 错误处理: spaCy 可能对某些文本抛出异常,需要 try-except 处理

8. 进阶用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 自定义分词规则
from spacy.lang.en import English
from spacy.tokenizer import Tokenizer

nlp = English()
# 自定义 tokenizer,不拆分 URL
def custom_tokenizer(nlp):
return Tokenizer(nlp.vocab, rules={})

nlp.tokenizer = custom_tokenizer(nlp)

# 使用 Matcher 提取模式
from spacy.matcher import Matcher

matcher = Matcher(nlp.vocab)
pattern = [{"LOWER": "natural"}, {"LOWER": "language"}, {"LOWER": "processing"}]
matcher.add("NLP", [pattern])

doc = nlp("I'm learning Natural Language Processing")
matches = matcher(doc)
for match_id, start, end in matches:
print(doc[start:end])

spaCy 是生产环境 NLP 的首选工具,理解其设计理念和使用技巧,能够显著提升开发效率和系统性能。

文本表示:从词袋到 TF-IDF

预处理后的文本是词的列表,但机器学习模型需要数值输入。如何把文本转为向量?

词袋模型( Bag of Words, BoW)

把文本表示为词频向量,忽略语序。

示例

1
2
3
4
5
6
7
8
9
10
文档 1: "我 喜欢 机器学习"
文档 2: "我 喜欢 深度学习"
文档 3: "机器学习 和 深度学习 很有趣"

词汇表: ["我", "喜欢", "机器学习", "深度学习", "和", "很有趣"]

向量表示:
文档 1: [1, 1, 1, 0, 0, 0]
文档 2: [1, 1, 0, 1, 0, 0]
文档 3: [0, 0, 1, 1, 1, 1]

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.feature_extraction.text import CountVectorizer

corpus = [
"我 喜欢 机器学习",
"我 喜欢 深度学习",
"机器学习 和 深度学习 很有趣"
]

vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)

print("词汇表:", vectorizer.get_feature_names_out())
print("向量表示:\n", X.toarray())

局限性

  • 高频常见词("的"、"是")权重过高
  • 无法区分重要词和普通词

TF-IDF( Term Frequency - Inverse Document Frequency)

TF-IDF 根据词的重要性加权:

  • TF(词频):词在文档中出现的次数
  • IDF(逆文档频率):词在所有文档中的稀有程度
  • TF-IDF

直觉解释

  • 如果词在某篇文档中频繁出现(高 TF),且在其他文档中很少见(高 IDF),则它对该文档很重要
  • "的"、"是"在所有文档中都常见(低 IDF),所以权重低
  • "量子计算"只在少数文档出现(高 IDF),权重高

Python 实现

TF-IDF 是文本特征提取的核心方法,它将文档转换为数值向量,其中每个维度对应一个词,值表示该词对文档的重要性。 scikit-learn 的 TfidfVectorizer 封装了完整的 TF-IDF 计算流程,包括词频统计、 IDF 计算、归一化等步骤。

核心原理回顾: TF(词频)衡量词在文档中的重要性, IDF(逆文档频率)衡量词的区分度。 TF-IDF = TF × IDF,同时考虑局部重要性和全局稀有性。 scikit-learn 的实现还包含 L2 归一化,确保不同长度的文档可以公平比较。

设计考虑: TfidfVectorizer 默认使用平滑的 IDF 计算(避免除零),支持 n-gram 特征(捕捉短语),可以限制特征数量( max_features)控制维度。稀疏矩阵存储( CSR 格式)节省内存,适合大规模语料。 fit_transform()方法先学习词汇表和 IDF 值,再转换文档,这是典型的 sklearn 模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np

# 示例语料库(文档集合)
# 注意:文本已经分词并用空格分隔(这是 TfidfVectorizer 的输入格式要求)
corpus = [
"我 喜欢 机器学习", # 文档 1
"我 喜欢 深度学习", # 文档 2
"机器学习 和 深度学习 很有趣" # 文档 3
]

# 创建 TF-IDF 向量化器
# TfidfVectorizer 的参数说明:
# - token_pattern: 默认 r"(?u)\b\w\w+\b",匹配 2 个或更多字符的词
# - max_features: 限制特征数量, None 表示使用所有词
# - min_df: 词的最小文档频率,低于此值的词被忽略(可以是整数或比例)
# - max_df: 词的最大文档频率,高于此值的词被忽略(用于过滤停用词)
# - ngram_range: n-gram 范围,默认(1,1)只使用单词,可以设置为(1,2)使用单词和双词
# - norm: 归一化方式,默认'l2'( L2 范数归一化)
# - smooth_idf: 是否平滑 IDF,默认 True(避免除零, IDF = log((N+1)/(df+1)) + 1)
vectorizer = TfidfVectorizer()

# 步骤 1: fit - 学习词汇表和 IDF 值
# 这一步会:
# 1. 构建词汇表(所有唯一的词)
# 2. 统计每个词在每个文档中的出现次数( TF)
# 3. 统计每个词出现在多少个文档中(文档频率 DF)
# 4. 计算每个词的 IDF 值: IDF = log((N+1)/(df+1)) + 1( smooth_idf=True 时)
# 其中 N 是文档总数, df 是包含该词的文档数
vectorizer.fit(corpus)

# 步骤 2: transform - 将文档转换为 TF-IDF 向量
# 这一步会:
# 1. 对每个文档,计算每个词的 TF(词频)
# 2. 将 TF 与 IDF 相乘得到 TF-IDF 值
# 3. 对每个文档的向量进行 L2 归一化: v_norm = v / ||v||_2
# 4. 返回稀疏矩阵( CSR 格式),形状为(文档数, 词汇表大小)
X = vectorizer.fit_transform(corpus)
# X 是稀疏矩阵,形状:(3, 6) - 3 个文档, 6 个词

# 查看 TF-IDF 矩阵(转换为密集矩阵,小数据集可以这样做)
# toarray()将稀疏矩阵转换为 numpy 数组
# 每一行是一个文档的 TF-IDF 向量,每一列对应一个词
tfidf_matrix = X.toarray()
print("TF-IDF 矩阵:\n", tfidf_matrix)
print("\n 特征名(词汇表):", vectorizer.get_feature_names_out())

# 输出示例:
# TF-IDF 矩阵:
# [[0.57735027 0.57735027 0.57735027 0. 0. 0. ] # 文档 1
# [0.57735027 0.57735027 0. 0.57735027 0. 0. ] # 文档 2
# [0. 0. 0.40824829 0.40824829 0.57735027 0.57735027]] # 文档 3
#
# 特征名: ['我', '喜欢', '机器学习', '深度学习', '和', '很有趣']

# 解释矩阵的含义:
# - 文档 1("我 喜欢 机器学习"):
# * "我"、"喜欢"、"机器学习"的 TF-IDF 值都是 0.577(约等于 1/√ 3)
# * 这是因为 L2 归一化:三个词权重相等,归一化后每个都是 1/√ 3
# - 文档 2("我 喜欢 深度学习"):
# * "我"、"喜欢"、"深度学习"的 TF-IDF 值都是 0.577
# - 文档 3("机器学习 和 深度学习 很有趣"):
# * "机器学习"和"深度学习"的 TF-IDF 值较低( 0.408),因为它们在多个文档中出现( IDF 较低)
# * "和"和"很有趣"的 TF-IDF 值较高( 0.577),因为它们只在文档 3 中出现( IDF 较高)

# 查看 IDF 值(逆文档频率)
# idf_属性存储每个词的 IDF 值,形状为(词汇表大小,)
print("\nIDF 值:")
for word, idf in zip(vectorizer.get_feature_names_out(), vectorizer.idf_):
print(f" {word}: {idf:.4f}")

# 查看词汇表到索引的映射
# vocabulary_是一个字典,键是词,值是在特征矩阵中的列索引
print("\n 词汇表索引映射:", vectorizer.vocabulary_)
# 输出:{'我': 0, '喜欢': 1, '机器学习': 2, '深度学习': 3, '和': 4, '很有趣': 5}

# 对新文档进行转换(使用已学习的向量化器)
new_doc = ["我 喜欢 自然语言处理"]
new_vector = vectorizer.transform(new_doc)
print("\n 新文档的 TF-IDF 向量:", new_vector.toarray())
# 注意:新文档中的"自然语言处理"不在词汇表中,会被忽略
# 输出:[[0.70710678 0.70710678 0. 0. 0. 0. ]]
# 只有"我"和"喜欢"有值,且经过 L2 归一化

深入解读: TF-IDF 的数学原理与实现细节

TF-IDF 看似简单,但 scikit-learn 的实现包含许多细节和优化:

1. TF-IDF 公式的变体

scikit-learn 使用的 TF-IDF 公式与标准公式略有不同:

标准公式

scikit-learn 公式( smooth_idf=True 时):

区别: - 平滑处理避免除零错误 - +1 项:确保 IDF 值始终为正,即使词出现在所有文档中

2. TF 的计算方式

scikit-learn 默认使用子线性 TF 缩放( sublinear_tf=False 时使用原始词频):

如果设置sublinear_tf=True,则使用:

这可以降低高频词的影响,突出稀有词的重要性。

3. L2 归一化的作用

默认情况下, TfidfVectorizer 会对每个文档的向量进行 L2 归一化:

为什么归一化? - 公平比较:不同长度的文档可以公平比较(否则长文档的向量模长更大) - 余弦相似度:归一化后,向量内积等于余弦相似度,便于计算文档相似度 - 数值稳定:避免向量模长过大导致的数值问题

4. 稀疏矩阵的优势

fit_transform()返回的是稀疏矩阵( CSR 格式),而非密集矩阵:

优势: - 内存效率: TF-IDF 矩阵通常非常稀疏(大部分元素为 0),稀疏矩阵只存储非零元素 - 计算效率:矩阵运算可以跳过零元素,加速计算

示例:假设有 1000 个文档,词汇表大小 10000,密集矩阵需要 1000 × 10000 × 8 字节=80MB,而稀疏矩阵可能只需要几 MB 。

5. 参数调优建议

参数 默认值 调优建议 影响
max_features None 根据任务设置(如 1000-10000) 控制特征维度,防止过拟合
min_df 1 设置为 2 或 0.01(比例)过滤罕见词 减少噪声特征
max_df 1.0 设置为 0.8-0.95 过滤停用词 自动过滤高频低义词
ngram_range (1,1) (1,2)捕捉短语 提升表达能力但增加维度
sublinear_tf False True 降低高频词影响 突出稀有词
norm 'l2' 'l1'或 None 根据任务选择 影响向量分布

6. 常见问题与解决方案

问题 原因 解决方案
内存溢出 词汇表过大或文档过多 使用 max_features 限制特征数,或使用 HashingVectorizer
新文档中的词被忽略 词汇表固定,新词不在其中 使用 HashingVectorizer 或定期重新 fit
TF-IDF 值全为 0 文档中的所有词都不在词汇表中 检查预处理流程,确保分词正确
计算速度慢 文档数量或词汇表过大 使用 HashingVectorizer 或增量学习
维度灾难 词汇表过大导致特征维度爆炸 使用 max_features 、 min_df 、 max_df 限制

7. 与其他方法的对比

方法 优点 缺点 适用场景
TF-IDF 简单、可解释、无需训练 无法捕捉语义、维度高 文本分类、信息检索
Word2Vec 捕捉语义、维度低 需要预训练或训练时间 文本相似度、语义分析
BERT 上下文相关、性能强 计算开销大、需要 GPU 复杂 NLP 任务
HashingVectorizer 内存效率高、支持新词 不可解释、可能冲突 大规模流式数据

8. 实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 更完整的 TF-IDF 配置示例
vectorizer = TfidfVectorizer(
max_features=5000, # 限制特征数量
min_df=2, # 词至少出现在 2 个文档中
max_df=0.8, # 词最多出现在 80%的文档中(过滤停用词)
ngram_range=(1, 2), # 使用单词和双词
sublinear_tf=True, # 使用子线性 TF 缩放
norm='l2', # L2 归一化
smooth_idf=True # 平滑 IDF
)

# 处理大规模语料时的内存优化
# 方法 1:使用 HashingVectorizer(不需要存储词汇表)
from sklearn.feature_extraction.text import HashingVectorizer
hasher = HashingVectorizer(n_features=10000, norm='l2')
X = hasher.transform(corpus)

# 方法 2:分批处理
chunk_size = 1000
for i in range(0, len(corpus), chunk_size):
chunk = corpus[i:i+chunk_size]
X_chunk = vectorizer.transform(chunk)
# 处理 X_chunk...

9. 性能优化技巧

  1. 预处理优化:在向量化前完成所有文本预处理,避免重复计算
  2. 特征选择:使用max_featuresmin_df/max_df限制特征数量
  3. 稀疏矩阵操作:使用 scipy.sparse 的矩阵运算,避免转换为密集矩阵
  4. 并行处理: TfidfVectorizer 支持 n_jobs 参数进行并行计算
  5. 增量学习:对于流式数据,考虑使用partial_fit()方法

TF-IDF 是文本特征提取的经典方法,虽然简单但非常有效。理解其原理和实现细节,能够帮助你在实际项目中正确使用和调优。

文本表示方法对比

方法 优点 缺点 适用场景
词袋模型 简单、快速 丢失语序、稀疏、无法捕捉语义 文本分类、简单检索
TF-IDF 突出重要词 仍无法捕捉语义相似性 信息检索、关键词提取
Word2Vec 稠密向量、捕捉语义 无法处理一词多义 文本相似度、情感分析
BERT 上下文相关、语义理解强 计算开销大 复杂 NLP 任务、问答系统

现代 NLP 任务更多使用 Word2Vec 、 BERT 等深度学习方法,但 TF-IDF 在简单任务中仍有用武之地。

实战案例:构建文本分类器

现在我们用前面学的预处理技术,构建一个完整的情感分类器。

任务描述

判断电影评论的情感是正面还是负面。

数据准备

使用 scikit-learn 自带的影评数据(实际项目中可替换为自己的数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from sklearn.datasets import fetch_20newsgroups
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import jieba
import re

# 假设我们有一个简单的中文影评数据集
reviews = [
"这部电影太精彩了,强烈推荐",
"剧情拖沓,演技尴尬,浪费时间",
"导演功力深厚,画面唯美,值得一看",
"烂片,看了十分钟就想离场",
"演员表现出色,剧情引人入胜",
"完全是烂俗套路,毫无新意",
"特效震撼,故事感人,五星好评",
"无聊至极,不推荐",
"这是我今年看过最好的电影",
"差评,根本不值票价",
"演技炸裂,每个镜头都是精心设计",
"剧情漏洞百出,逻辑混乱",
"音乐动人,情感真挚,催人泪下",
"浪费我两个小时,强烈不推荐",
"制作精良,诚意满满,良心之作",
"看完只想骂人,烂透了",
"笑点密集,轻松愉快,适合全家观看",
"尴尬癌都犯了,实在看不下去",
"细节丰富,伏笔精妙,值得二刷",
"毫无亮点,彻底失望"
]

labels = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] # 1=正面, 0=负面

预处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 加载停用词
def load_stopwords():
stopwords = set([
'的', '了', '在', '是', '我', '有', '和', '就', '不', '人',
'都', '一', '一个', '上', '也', '很', '到', '说', '要', '去',
'你', '会', '着', '没有', '看', '好', '自己', '这'
])
return stopwords

def preprocess(text):
# 去除标点
text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', ' ', text)
# 分词
tokens = jieba.lcut(text)
# 去停用词
stopwords = load_stopwords()
tokens = [w for w in tokens if w not in stopwords and len(w) > 1]
return ' '.join(tokens)

# 预处理所有评论
processed_reviews = [preprocess(review) for review in reviews]
print("预处理示例:")
print(f"原始: {reviews[0]}")
print(f"处理后: {processed_reviews[0]}")

特征提取与模型训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
processed_reviews, labels, test_size=0.3, random_state=42
)

# TF-IDF 向量化
vectorizer = TfidfVectorizer(max_features=100)
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

# 训练朴素贝叶斯分类器
classifier = MultinomialNB()
classifier.fit(X_train_tfidf, y_train)

# 预测
y_pred = classifier.predict(X_test_tfidf)

# 评估
print(f"准确率: {accuracy_score(y_test, y_pred):.2f}")
print("\n 分类报告:")
print(classification_report(y_test, y_pred, target_names=['负面', '正面']))

预测新评论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def predict_sentiment(text):
processed = preprocess(text)
tfidf = vectorizer.transform([processed])
prediction = classifier.predict(tfidf)[0]
proba = classifier.predict_proba(tfidf)[0]

sentiment = "正面" if prediction == 1 else "负面"
confidence = proba[prediction] * 100

print(f"评论: {text}")
print(f"预测: {sentiment} (置信度: {confidence:.1f}%)")
print()

# 测试新数据
predict_sentiment("这部电影非常精彩,推荐大家去看")
predict_sentiment("太烂了,完全是浪费时间")
predict_sentiment("还可以,中规中矩")

完整代码整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import jieba
import re
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report

# 数据
reviews = [
"这部电影太精彩了,强烈推荐", "剧情拖沓,演技尴尬,浪费时间",
"导演功力深厚,画面唯美,值得一看", "烂片,看了十分钟就想离场",
"演员表现出色,剧情引人入胜", "完全是烂俗套路,毫无新意",
"特效震撼,故事感人,五星好评", "无聊至极,不推荐",
"这是我今年看过最好的电影", "差评,根本不值票价",
"演技炸裂,每个镜头都是精心设计", "剧情漏洞百出,逻辑混乱",
"音乐动人,情感真挚,催人泪下", "浪费我两个小时,强烈不推荐",
"制作精良,诚意满满,良心之作", "看完只想骂人,烂透了",
"笑点密集,轻松愉快,适合全家观看", "尴尬癌都犯了,实在看不下去",
"细节丰富,伏笔精妙,值得二刷", "毫无亮点,彻底失望"
]
labels = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]

# 预处理
stopwords = set(['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都'])

def preprocess(text):
text = re.sub(r'[^\u4e00-\u9fa5]', ' ', text)
tokens = [w for w in jieba.lcut(text) if w not in stopwords and len(w) > 1]
return ' '.join(tokens)

processed = [preprocess(r) for r in reviews]

# 训练
X_train, X_test, y_train, y_test = train_test_split(processed, labels, test_size=0.3, random_state=42)
vectorizer = TfidfVectorizer()
X_train_vec = vectorizer.fit_transform(X_train)
X_test_vec = vectorizer.transform(X_test)

clf = MultinomialNB()
clf.fit(X_train_vec, y_train)

# 评估
y_pred = clf.predict(X_test_vec)
print(f"准确率: {accuracy_score(y_test, y_pred):.2f}")

# 预测新样本
def predict(text):
vec = vectorizer.transform([preprocess(text)])
return "正面" if clf.predict(vec)[0] == 1 else "负面"

print(predict("电影拍得很棒,非常感动")) # 正面
print(predict("太差了,不想看")) # 负面

模型改进方向

  1. 扩充数据集: 20 个样本太少,实际需要数千到数万样本
  2. 调整停用词:根据任务定制停用词表
  3. 尝试其他模型:逻辑回归、 SVM 、 LSTM
  4. 使用预训练模型: BERT 中文模型(如 bert-base-chinese
  5. 考虑否定词:处理"不好"、"不推荐"等否定结构

❓ Q&A: NLP 基础常见问题

Q1: 中文分词和英文分词的本质区别是什么?

A: 英文单词间有天然分隔符(空格),分词只需按空格切分(复杂情况才需处理缩写如 "don't")。中文是连续字符流,没有明确边界,需要算法判断哪些字组成词:

1
2
英文: "I love NLP" → 天然分隔 → ["I", "love", "NLP"]
中文: "我爱 NLP" → 需算法 → ["我", "爱", "NLP"] 或 ["我爱", "NLP"]

中文分词的难点:

  • 歧义消解:"乒乓球拍卖" → "乒乓球/拍卖" 还是 "乒乓/球拍/卖"?
  • 新词识别:"奥利给"、"yyds" 等网络词汇不在词典中
  • 领域适应:医疗、法律等领域有专门术语

Q2: 词干化和词形还原应该选哪个?

A: 看场景:

场景 推荐方法 原因
信息检索、搜索引擎 词干化 快速,允许过匹配("running" 和 "runner" 都匹配 "run")
文本分类、情感分析 词形还原 准确,避免产生不存在的词
实时系统 词干化 速度优先
学术研究 词形还原 质量优先

实例对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from nltk.stem import PorterStemmer, WordNetLemmatizer

stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()

words = ["studies", "studying", "better", "worse"]

for w in words:
print(f"{w}: stem={stemmer.stem(w)}, lemma={lemmatizer.lemmatize(w, pos='v')}")

# studies: stem=studi, lemma=study
# studying: stem=studi, lemma=study
# better: stem=better, lemma=better (需要指定 pos='a'才能还原为 good)
# worse: stem=wors, lemma=worse

Q3: TF-IDF 的 IDF 为什么要取对数?

A: 三个原因:

  1. 数值稳定:假设总文档数 ,某词出现在 1 个文档中,;另一词出现在 10 个文档中,。差距过大会导致数值不稳定。

  2. 符合人类直觉:词的重要性不应线性增长。出现在 1 篇 vs 2 篇文档的区别,比出现在 100 篇 vs 101 篇的区别更显著。

  3. 匹配信息论:在信息论中,事件概率为 的信息量是 。词出现在 篇文档的概率是 $ n/N$

对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import math

N = 10000 # 总文档数
docs_contain = [1, 10, 100, 1000]

print("不取对数:")
for n in docs_contain:
print(f" 出现在 {n} 篇: IDF = {N/n}")

print("\n 取对数:")
for n in docs_contain:
print(f" 出现在 {n} 篇: IDF = {math.log(N/n):.2f}")

# 不取对数: 10000, 1000, 100, 10 (线性衰减)
# 取对数: 9.21, 6.91, 4.61, 2.30 (对数衰减,更平滑)

Q4: 为什么要去除停用词?会不会丢失信息?

A: 去除停用词有明确好处:

  • 降维:词汇表从 10 万降到 5 万,节省存储和计算
  • 提升效果:高频低义词("的"、"了")会掩盖关键词

但确实可能丢失信息,尤其是:

  • 情感分析:"不好" 中的 "不" 是停用词但表否定
  • 问答系统:"who"、"when" 是疑问词,不应删除
  • 短文本:微博、评论本来就短,再删停用词就没什么了

权衡策略

  1. 任务相关停用词表(情感分析保留否定词)
  2. 对比实验(有/无停用词的模型效果)
  3. 深度学习模型可以不去停用词(模型自己学会忽略)

Q5: 词袋模型丢失了语序,为什么还能工作?

A: 对于某些任务,语序不是最关键的:

文本分类示例

1
2
正面评论: "电影很好,我喜欢"
负面评论: "我喜欢好电影,但这部很差"

词袋模型看到:

1
2
正面: {电影:1, 很:1, 好:1, 我:1, 喜欢:1}
负面: {我:1, 喜欢:1, 好:1, 电影:1, 但:1, 这:1, 部:1, 很:1, 差:1}

虽然丢失了语序,但"差"、"但"的出现本身就是强信号。

但在这些任务中词袋模型会失效

  • 机器翻译:"猫追老鼠" ≠ "老鼠追猫"
  • 问答系统:"谁打了谁" 需要知道主宾关系
  • 文本生成:生成的句子需要符合语法

现代 NLP 用 RNN 、 Transformer 等模型保留语序信息。

Q6: jieba 的精确模式、全模式、搜索引擎模式有什么区别?

A: 三种模式的切分粒度不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import jieba

text = "我来到北京清华大学"

print("精确模式:", jieba.lcut(text, cut_all=False))
# ['我', '来到', '北京', '清华大学']
# → 适合文本分析,每个词只出现一次

print("全模式:", jieba.lcut(text, cut_all=True))
# ['我', '来到', '北京', '清华', '清华大学', '华大', '大学']
# → 所有可能的词都输出,有冗余

print("搜索引擎模式:", jieba.lcut_for_search(text))
# ['我', '来到', '北京', '清华', '华大', '大学', '清华大学']
# → 在精确模式基础上,对长词再切分

使用场景

  • 精确模式:文本分类、情感分析(默认选择)
  • 全模式:关键词提取、词云(需要更多候选词)
  • 搜索引擎模式:搜索召回("清华大学"被切为"清华"+"大学"+"清华大学",用户搜"清华"也能匹配)

Q7: BERT 这么强大,还需要学习传统预处理吗?

A: 必须学,原因有四:

  1. 轻量级场景:嵌入式设备、实时系统跑不动 BERT(参数量上亿)
  2. 数据稀缺时:标注数据少于 1000 条时,简单模型 + TF-IDF 可能比 BERT 微调效果好
  3. 可解释性: TF-IDF 可以告诉你哪些词重要, BERT 是黑盒
  4. 成本考虑:训练一次 BERT 可能花费数千元 GPU 费用,朴素贝叶斯几秒钟

实际项目流程

1
2
3
第一步:用 TF-IDF + 逻辑回归建立 baseline( 1 小时)
第二步:评估效果,如果满足需求就部署(节省成本)
第三步:不满足才上 BERT(几天调参 + 数千元成本)

Q8: 中文预处理时要不要转繁体为简体?

A: 看数据来源:

场景 是否转换 原因
用户输入(搜索、评论) 转换 用户可能混用,统一避免"臺灣"和"台湾"被当作不同词
历史文献、古籍 不转换 繁体字承载语义信息("乾坤" ≠ "干坤")
台湾、香港数据 看下游任务 如果和大陆数据混合训练,建议转换

转换工具

1
2
3
4
5
6
from opencc import OpenCC

cc = OpenCC('t2s') # 繁体转简体
text_traditional = "我愛臺灣"
text_simplified = cc.convert(text_traditional)
print(text_simplified) # "我爱台湾"

注意:转换可能有歧义,如"乾燥" → "干燥",但 "乾隆" → "乾隆"(不应转换)。

Q9: 如何评估分词质量?

A: 三个指标(需要人工标注的标准答案):

  1. 准确率( Precision):分出来的词有多少是对的 $$

P = {}

R = {}

F1 = 2 $$

示例

1
2
3
4
5
6
7
8
9
10
11
原句: "我爱自然语言处理"
标准答案: ["我", "爱", "自然语言处理"]
模型输出: ["我", "爱", "自然", "语言", "处理"]

正确切分: "我", "爱" (2 个)
模型总词数: 5
标准答案总词数: 3

准确率: 2/5 = 0.4
召回率: 2/3 = 0.67
F1: 2 × (0.4 × 0.67) / (0.4 + 0.67) = 0.5

实际中更常用下游任务指标(如分类准确率)评估:分词好 → 分类效果好。

Q10: 实际项目中预处理流程应该怎么设计?

A: 遵循这个检查清单:

第一步:分析数据特征

1
2
3
4
5
6
7
8
# 检查数据长度分布
import pandas as pd
df = pd.DataFrame({'text': your_texts})
df['length'] = df['text'].apply(len)
print(df['length'].describe())

# 检查是否有特殊字符
print(df['text'].apply(lambda x: bool(re.search(r'[^\u4e00-\u9fa5a-zA-Z0-9]', x))).sum())

第二步:确定预处理步骤

1
2
3
4
5
短文本(<50 字):保守去停用词(避免信息丢失)
长文本(>200 字):积极去停用词 + 过滤低频词
含 emoji/表情:看任务,情感分析可能需要保留
含 URL/邮箱:替换为特殊标记 <URL> <EMAIL>
含数字:分类任务可删除,命名实体识别要保留

第三步:模块化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class TextPreprocessor:
def __init__(self, remove_stopwords=True, min_word_len=1):
self.remove_stopwords = remove_stopwords
self.min_word_len = min_word_len
self.stopwords = self.load_stopwords()

def load_stopwords(self):
# 加载停用词
return set(['的', '了', ...])

def clean(self, text):
# 清洗文本
text = re.sub(r'http\S+', '<URL>', text)
text = re.sub(r'\S+@\S+', '<EMAIL>', text)
return text

def tokenize(self, text):
# 分词
return jieba.lcut(text)

def filter_tokens(self, tokens):
# 过滤
if self.remove_stopwords:
tokens = [w for w in tokens if w not in self.stopwords]
tokens = [w for w in tokens if len(w) >= self.min_word_len]
return tokens

def process(self, text):
text = self.clean(text)
tokens = self.tokenize(text)
tokens = self.filter_tokens(tokens)
return tokens

# 使用
preprocessor = TextPreprocessor(remove_stopwords=True, min_word_len=2)
processed = preprocessor.process("我在北京的清华大学学习 NLP")
print(processed)

第四步: A/B 测试

对比不同预处理方案的下游效果:

1
2
方案 A: 去停用词 + 词形还原 → 准确率 85%
方案 B: 保留停用词 + 只小写化 → 准确率 87% ✓

记住:预处理没有银弹,一切以下游任务效果为准

总结与展望

文本预处理是 NLP 的基础设施,虽然深度学习降低了手工特征工程的需求,但理解这些经典方法仍然重要。你已经掌握了:

  • NLP 的发展脉络和应用场景
  • 中英文文本预处理的完整流程
  • jieba 、 NLTK 、 spaCy 等工具的使用
  • 词袋模型和 TF-IDF 的原理与实现
  • 端到端的文本分类实战案例

下一篇文章将介绍词向量与语言模型,探讨 Word2Vec 、 GloVe 、 ELMo 如何把词表示为稠密向量,以及预训练语言模型的原理。敬请期待!


推荐资源

代码仓库:本文完整代码已上传至 GitHub(请根据实际情况替换链接)

如果觉得有帮助,欢迎点赞分享!有任何问题欢迎在评论区讨论。

  • 本文标题:自然语言处理(一)—— NLP 入门与文本预处理
  • 本文作者:Chen Kai
  • 创建时间:2024-02-03 09:00:00
  • 本文链接:https://www.chenk.top/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94-NLP%E5%85%A5%E9%97%A8%E4%B8%8E%E6%96%87%E6%9C%AC%E9%A2%84%E5%A4%84%E7%90%86/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论