首页 / 第九章
09自然语言处理
语言是人类智能的皇冠。自然语言处理(NLP)研究如何让计算机理解、生成人类语言。 本章从中文分词、TF-IDF 检索、n-gram 模型,到文本分类、实体识别与情感分析, 全部以纯 Python 手写实现,并在最后回望词向量、Transformer 与大语言模型。
9.1 自然语言处理概述
自然语言处理(Natural Language Processing, NLP)融合语言学、计算机科学与人工智能, 目标是实现人与计算机之间用自然语言的有效通信。它的困难源于语言的歧义性、变异大、 上下文依赖与知识依赖:"我要炒肉丝"既可以是点菜,也可以是要下厨。
NLP 的分析层次
| 层次 | 任务 |
|---|---|
| 形态学 | 分词、词性还原、形态切分 |
| 语法学 | 词性标注、句法分析、依存分析 |
| 语义学 | 词义消歧、语义角色标注、语义表示 |
| 语用学 | 结合语境理解言外之意、对话管理 |
典型应用与技术范式变迁
- 规则时代(1950s–1980s):人工语法规则、有限状态机;
- 统计时代(1990s–2010):n-gram、HMM、CRF、统计机器翻译;
- 深度学习时代(2013–2018):word2vec、RNN/LSTM、Seq2Seq、注意力;
- 预训练大模型时代(2018 至今):BERT、GPT、Transformer 统一了绝大多数任务。
9.2 文本预处理与中文分词
英文文本天然由空格分词,预处理通常包括小写化、去标点、去停用词、词形还原。 而中文句子的词之间没有空格,分词是中文 NLP 的第一道关卡,也是歧义高发地。
9.2.1 正向 / 逆向最大匹配分词
基于词典的最大匹配法简单经典:设定最大词长 L,从句首(或句尾)依次尝试长度为 L、L−1、… 的片段, 在词典中命中即切出。经典歧义句"研究生命的起源"能清楚展示两者差异:
"""中文分词:正向最大匹配 FMM 与逆向最大匹配 BMM"""
DICT = {"研究", "研究生", "生命", "科学", "起源", "的", "自然语言处理",
"人工智能", "大学", "学生"}
MAX_LEN = 5 # 词典中最长词的字数
def fmm(text):
"""正向最大匹配:从左向右扫描。"""
result, i = [], 0
while i < len(text):
length = min(MAX_LEN, len(text) - i)
for L in range(length, 0, -1):
word = text[i:i + L]
if word in DICT or L == 1: # 单字兜底
result.append(word)
i += L
break
return result
def bmm(text):
"""逆向最大匹配:从右向左扫描,通常对汉语更准确。"""
result, i = [], len(text)
while i > 0:
length = min(MAX_LEN, i)
for L in range(length, 0, -1):
word = text[i - L:i]
if word in DICT or L == 1:
result.append(word)
i -= L
break
return result[::-1]
if __name__ == "__main__":
for sentence in ["研究生命的起源", "大学生研究人工智能"]:
f, b = fmm(sentence), bmm(sentence)
print(f"原句 :{sentence}")
print(f"FMM :{' / '.join(f)}")
print(f"BMM :{' / '.join(b)}")
print(f"两种切分{'一致' if f == b else '不一致(存在切分歧义)'}\n")
原句 :研究生命的起源 FMM :研究生 / 命 / 的 / 起源 BMM :研究 / 生命 / 的 / 起源 两种切分不一致(存在切分歧义) 原句 :大学生研究人工智能 FMM :大学生 / 研究 / 人工智能 BMM :大学 / 生 / 研究 / 人工智能 两种切分不一致(存在切分歧义)
FMM 把"研究生命"误切成"研究生/命";BMM 在第一句给出了正确结果,却在第二句失误。 实践中用 FMM/BMM 双向比对,不一致时用词频或统计模型消歧; 现代分词工具(jieba、THULAC、HanLP)采用前缀词典 + 统计(HMM/CRF)+ Viterbi, 还能识别未登录词(新词、人名)。
9.2.2 英文预处理
"""英文文本预处理:分句、分词、停用词过滤、简单词干提取"""
import re
STOPWORDS = {"a", "an", "the", "is", "are", "was", "to", "of",
"and", "in", "on", "it", "this", "that"}
def tokenize(text):
text = text.lower()
words = re.findall(r"[a-z]+", text) # 正则提取单词
return [w for w in words if w not in STOPWORDS]
def simple_stem(word):
"""极简后缀去除(教学用;生产中用 NLTK 的 PorterStemmer)。"""
for suf in ["ing", "edly", "ed", "ly", "s"]:
if word.endswith(suf) and len(word) - len(suf) >= 3:
return word[:-len(suf)]
return word
def bag_of_words(text):
from collections import Counter
return Counter(simple_stem(w) for w in tokenize(text))
if __name__ == "__main__":
doc = ("Natural language processing is a subfield of AI. "
"It studies how computers process human languages.")
print("分词结果:", tokenize(doc))
print("词袋统计:", dict(bag_of_words(doc)))
分词结果: ['natural', 'language', 'processing', 'subfield', 'ai',
'studies', 'how', 'computers', 'process', 'human', 'languages']
词袋统计: {'natural': 1, 'language': 2, 'process': 2, 'subfield': 1,
'ai': 1, 'studi': 1, 'how': 1, 'comput': 1, 'human': 1}
9.3 TF-IDF 与文档检索
词袋模型只数词频,但"的、是"这类高频词没有区分度。TF-IDF 的思想是: 一个词在某文档中出现得多(TF 大)、而在全部文档中出现得少(IDF 大),它对该文档就越有代表性。
IDF(t) = log( N / (1 + DF(t)) ),N 为文档总数,DF(t) 为含词 t 的文档数
TF-IDF(t,d) = TF(t,d) · IDF(t)
把文档表示成 TF-IDF 向量后,可用余弦相似度做检索与查重:
"""TF-IDF 向量化 + 余弦相似度:迷你搜索引擎"""
import math
import re
from collections import Counter
def words(text):
return re.findall(r"[\u4e00-\u9fa5]{2,}|[a-z]+", text.lower())
class TfidfVectorizer:
def fit_transform(self, docs):
tokenized = [words(d) for d in docs]
self.vocab = sorted({w for d in tokenized for w in d})
N = len(docs)
df = Counter()
for d in tokenized:
for w in set(d):
df[w] += 1
self.idf = {w: math.log(N / (1 + df[w])) + 1 for w in self.vocab}
vectors = []
for d in tokenized:
tf = Counter(d)
vec = [tf.get(w, 0) * self.idf[w] for w in self.vocab]
vectors.append(vec)
return vectors
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb + 1e-12)
if __name__ == "__main__":
docs = [
"人工智能研究如何让机器感知与推理",
"机器学习是人工智能的核心技术之一",
"自然语言处理让计算机理解人类语言",
"神经网络是机器学习的重要模型",
"今天天气晴朗适合户外运动",
]
vzer = TfidfVectorizer()
vectors = vzer.fit_transform(docs)
query = "人工智能 机器学习"
q_vec = [words(query).count(w) * vzer.idf[w] if w in vzer.idf else 0
for w in vzer.vocab]
scores = sorted(((cosine(q_vec, v), i) for i, v in enumerate(vectors)),
reverse=True)
print(f"查询:{query!r}\n按相关度排序:")
for score, i in scores:
print(f" {score:.3f} 文档{i+1}:{docs[i]}")
查询:'人工智能 机器学习' 按相关度排序: 0.745 文档2:机器学习是人工智能的核心技术之一 0.464 文档1:人工智能研究如何让机器感知与推理 0.393 文档4:神经网络是机器学习的重要模型 0.000 文档3:自然语言处理让计算机理解人类语言 0.000 文档5:今天天气晴朗适合户外运动
包含两个查询词的文档 2 排在最前,与查询无关的天气文档得分为 0—— 这就是 2000 年代搜索引擎、文档查重、新闻推荐的基础原理。
9.4 n-gram 语言模型
语言模型衡量一句话出现的概率,是语音识别、输入法、机器翻译、文本生成的共同基础。 n-gram 模型基于马尔可夫假设:一个词只依赖它前面 n−1 个词。以二元(bigram)为例:
句首用 <s>、句尾用 </s> 标记。下面的程序同时完成三件事:
估计 bigram 概率、给新句子打分、贪心生成句子。
"""Bigram 语言模型:句子概率 + 贪心生成"""
from collections import defaultdict
corpus = ["猫 追 老鼠", "狗 追 猫", "猫 爬 树", "狗 叫", "老鼠 跑"]
BOS, EOS = "<s>", "</s>"
bigram = defaultdict(lambda: defaultdict(int))
unigram = defaultdict(int)
for line in corpus:
toks = [BOS] + line.split() + [EOS]
for a, b in zip(toks, toks[1:]):
bigram[a][b] += 1
unigram[a] += 1
def P(word, prev):
# 加一平滑,避免零概率
return (bigram[prev][word] + 1) / (unigram[prev] + len(unigram))
def sentence_prob(sentence):
toks = [BOS] + sentence.split() + [EOS]
p = 1.0
for a, b in zip(toks, toks[1:]):
p *= P(b, a)
return p
def generate():
"""贪心解码:每步选条件概率最大的词。"""
out, cur = [], BOS
while cur != EOS:
nxt = max(bigram[cur], key=lambda w: bigram[cur][w])
if nxt == EOS:
break
out.append(nxt)
cur = nxt
return " ".join(out)
if __name__ == "__main__":
s1 = "猫 追 老鼠"
s2 = "老鼠 追 狗"
print(f"P({s1!r}) = {sentence_prob(s1):.5f}")
print(f"P({s2!r}) = {sentence_prob(s2):.5f} (罕见组合,概率更低)")
print("贪心生成句子:", generate())
P('猫 追 老鼠') = 0.00667
P('老鼠 追 狗') = 0.00145 (罕见组合,概率更低)
贪心生成句子: 猫 追 老鼠
符合语言习惯的句子概率明显更高——输入法和语音识别正是用语言模型在多个候选中挑选最合理的句子。 n-gram 的缺点是无法捕捉长距离依赖、且参数随 n 指数膨胀,后来被神经语言模型取代。 模型好坏可用困惑度 Perplexity(=2平均交叉熵)衡量,越低越好。
9.5 文本分类:朴素贝叶斯
文本分类是 NLP 最常见的任务:垃圾邮件识别、情感分类、新闻归类。 第七章的朴素贝叶斯套用到"词"特征上,就是经典的多项式朴素贝叶斯文本分类器, 配合拉普拉斯平滑,在短文本任务上至今是很强的基线。
"""电影评论情感二分类:多项式朴素贝叶斯(拉普拉斯平滑)"""
from collections import defaultdict
import math
train_data = [
("这部 电影 剧情 精彩 演员 表演 出色", "正面"),
("非常 喜欢 这部 电影 很 精彩", "正面"),
("剧情 无聊 表演 糟糕 浪费 时间", "负面"),
("电影 太 糟糕 了 非常 失望", "负面"),
]
class MultinomialNBText:
def fit(self, data):
self.wc = defaultdict(lambda: defaultdict(int))
self.doc_count = defaultdict(int)
vocab = set()
for text, label in data:
self.doc_count[label] += 1
for w in text.split():
self.wc[label][w] += 1
vocab.add(w)
self.V = len(vocab)
self.total = {c: sum(self.wc[c].values()) for c in self.doc_count}
self.N = len(data)
def predict(self, text):
scores = {}
for c in self.doc_count:
log_p = math.log(self.doc_count[c] / self.N) # 先验
for w in text.split():
tf = self.wc[c][w] + 1 # 拉普拉斯平滑
log_p += math.log(tf / (self.total[c] + self.V))
scores[c] = math.exp(log_p)
z = sum(scores.values())
return sorted(((c, p / z) for c, p in scores.items()),
key=lambda kv: -kv[1])
if __name__ == "__main__":
model = MultinomialNBText()
model.fit(train_data)
for text in ["剧情 精彩 表演 出色", "剧情 无聊 很 失望"]:
probs = model.predict(text)
pred, conf = probs[0]
detail = ",".join(f"{c}:{p:.3f}" for c, p in probs)
print(f"评论:{text}\n -> {pred}(置信度 {conf:.2f})[{detail}]\n")
评论:剧情 精彩 表演 出色 -> 正面(置信度 0.84)[正面:0.840,负面:0.160] 评论:剧情 无聊 很 失望 -> 负面(置信度 0.70)[负面:0.696,正面:0.304]
工业级做法是把本章的 TF-IDF 与第七章的 SVM/逻辑回归、或朴素贝叶斯组合成
TfidfVectorizer + LogisticRegression 流水线;深度学习时代则以预训练模型微调为主。
9.6 命名实体识别(规则法)
命名实体识别(Named Entity Recognition, NER)从文本中抽出人名、地名、机构名、时间、 数字等专有实体,是知识图谱、信息抽取、问答系统的前置任务。早期系统依赖实体词典 + 后缀规则: 机构名常以"大学、公司、医院、银行"结尾,地名常以"市、省、县"结尾。
"""基于后缀规则的中文命名实体识别(教学版)"""
SUFFIX = {
"ORG": ["大学", "公司", "医院", "银行", "研究院"],
"LOC": ["市", "省", "县", "自治区"],
}
# 标点与常见动词视为实体的左边界
BOUNDARY = set(",。;:、 ,; 位在于到")
def find_entities(text):
entities, seen = [], set()
for label, suffixes in SUFFIX.items():
for suf in suffixes:
start_search = 0
while True:
idx = text.find(suf, start_search)
if idx == -1:
break
end = idx + len(suf)
# 从后缀向左找最近边界
left = idx - 1
while left >= 0 and text[left] not in BOUNDARY and end - left <= 6:
left -= 1
entity = text[left + 1:end]
if entity and entity not in seen:
seen.add(entity)
entities.append((entity, label))
start_search = end
return entities
if __name__ == "__main__":
text = "北京大学位于北京市,华为公司总部在深圳市。"
print("原文:", text)
for entity, label in find_entities(text):
label_zh = {"ORG": "机构名", "LOC": "地名"}[label]
print(f" {entity} ——> {label_zh}")
原文: 北京大学位于北京市,华为公司总部在深圳市。 北京大学 ——> 机构名 华为公司 ——> 机构名 北京市 ——> 地名 深圳市 ——> 地名
规则法可解释、零样本,但依赖词表、难处理歧义与新实体。现代 NER 用 BIO/BIOES 标注 + CRF/BiLSTM-CRF/BERT 序列标注,在公开测评上 F1 可达 90% 以上。
9.7 情感分析(词典法)
情感分析判断文本的褒贬倾向,应用于评论挖掘、舆情监控。最简单的词典法用情感词得分求和, 并处理两个关键语言现象:否定词反转极性("不喜欢"为负)、程度副词加强("非常好"更强)。
"""基于情感词典的情感分析(支持否定反转、程度副词加强)"""
LEXICON = {"好": 1, "优秀": 1, "喜欢": 1, "精彩": 1, "满意": 1,
"糟糕": -1, "差": -1, "失望": -1, "讨厌": -1, "无聊": -1}
NEGATION = {"不", "没", "没有", "别", "不是"}
DEGREE = {"很": 1.5, "非常": 2.0, "特别": 1.8, "有点": 0.5}
def sentiment_score(text):
score = 0.0
neg, degree = False, 1.0
for w in text.split():
if w in NEGATION:
neg = True # 否定作用于下一个情感词
elif w in DEGREE:
degree = DEGREE[w]
elif w in LEXICON:
s = LEXICON[w] * degree * (-1 if neg else 1)
score += s
neg, degree = False, 1.0 # 计分后复位
return score
def polarity(score):
if score > 0.2: return "正面"
if score < -0.2: return "负面"
return "中性"
if __name__ == "__main__":
reviews = [
"我 不 喜欢 这部 很 糟糕 的 电影",
"这部 电影 很 精彩 我 非常 喜欢",
"电影 时长 两个 小时",
]
for r in reviews:
s = sentiment_score(r)
print(f"得分 {s:+.1f} {polarity(s)} | {r.replace(' ', '')}")
得分 -2.5 负面 | 我不喜欢这部很糟糕的电影 得分 +3.0 正面 | 这部电影很精彩我非常喜欢 得分 +0.0 中性 | 电影时长两个小时
词典法无需训练数据、可解释,但不懂反讽("这电影好看到我睡着了")。 工业界普遍采用预训练模型微调或直接调用大模型进行方面级、细粒度情感分析。
9.8 从词向量到大语言模型
本章前半部分是统计 NLP 的经典方法。近十年,NLP 经历了两场革命:
① 分布式表示:词向量(2013)
One-hot 表示中任意两个词都"正交"、毫无语义关系。word2vec 用 Skip-gram/CBOW 把词映射为稠密向量, 使语义相近的词在向量空间中邻近,甚至出现著名的向量类比:
② Transformer 与预训练大模型(2017 至今)
- Transformer:以自注意力机制并行建模任意距离的词间关系,是现代 NLP 的统一架构;
- BERT(2018):双向掩码语言模型预训练,擅长理解类任务(分类、NER、问答);
- GPT 系列:自回归生成预训练,催生了对话、写作、代码生成等生成式应用;
- 大语言模型(LLM):千亿参数 + 指令微调 + 人类反馈强化学习(RLHF), 以"提示词"统一了几乎所有 NLP 任务,并通过 RAG、工具调用与第八章的专家知识相结合。
"""使用 HuggingFace Transformers 调用现代中文模型
需先安装:pip install transformers torch
首次运行会自动下载模型(需要联网)。"""
try:
from transformers import pipeline
# 情感分析:一行代码加载预训练模型
clf = pipeline("sentiment-analysis",
model="uer/roberta-base-finetuned-dianping-chinese")
print(clf("这家餐厅环境优雅,菜品精致,服务也非常周到。"))
# 输出形如:[{'label': 'positive (stars 5)', 'score': 0.98}]
# 文本生成:提示词驱动
gen = pipeline("text-generation", model="Qwen/Qwen2.5-0.5B")
print(gen("用三句话解释什么是人工智能:", max_new_tokens=120))
except ImportError:
print("请先安装 transformers 与 torch,再运行本例。")
✦ 学习路径建议
经典方法(分词、TF-IDF、n-gram、NB)帮你建立对语言现象的直觉与"没有 GPU 也能工作"的基线; 再学习注意力机制的数学原理、亲手微调一个 BERT 文本分类器;最后理解提示工程、RAG 与智能体编排。 工具会更替,语言分析的层次思想不会过时。
本章小结
- NLP 分形态、语法、语义、语用四个层次;歧义与上下文依赖是核心难点。
- 中文分词是首要任务:FMM/BMM 基于词典,双向比对可发现歧义;现代工具以统计序列模型为主。
- TF-IDF 衡量词对文档的区分度,配合余弦相似度可构建迷你搜索引擎。
- n-gram 用马尔可夫假设建模句子概率,是统计语言模型的基石。
- 朴素贝叶斯 + 词袋是文本分类的强基线;实体识别可由后缀规则起步;情感词典法支持否定反转与程度加强。
- 现代 NLP 以词向量、Transformer、预训练大模型为主线,理解与生成走向统一。