你对着手机说“帮我订一张明天去北京的高铁票”,手机立刻照办——这背后,是一场持续了六十多年的技术马拉松。
自然语言处理(NLP)的目标只有一个:让电脑看懂人话、说人话。听起来简单,做起来却难如登天。因为人类语言充满了歧义、省略、反讽和无穷无尽的新说法。
今天,我就带你完整回顾NLP从“手写规则”到“深度学习”的四个进化阶段。全文会穿插大量可运行的代码示例,保证你能一边看一边理解。
一、NLP到底在解决哪些问题?
在聊历史之前,我们先搞清楚NLP通常处理哪几类任务。后面每个阶段的技术都是冲着这些任务去的。
1.1 文本分类
给整段文字贴一个标签。比如:
-
判断酒店评论是好评还是差评(情感分析)
-
判断一封邮件是不是垃圾邮件
-
判断新闻属于体育、财经还是娱乐
1.2 序列标注
给文本中的每个词打上一个标签。最典型的例子是命名实体识别:从一句话里把人名、地名、日期、手机号等找出来。
比如下面这个收货地址信息:
收货人:王雷 手机号:13812345678 地址:西部硅谷大厦6栋201室
我们希望自动标出:王雷(人名)、13812345678(手机号)、西部硅谷大厦(地名)、6栋201室(门牌号)。
序列标注常用的标记法是 BIO:
-
B
(Begin):一个实体的开头
-
I
(Inside):一个实体的内部
-
O
(Outside):不是实体
例如“王雷住在深圳”标注为:王(B-人名) 雷(I-人名) 住(O) 在(O) 深(B-地名) 圳(I-地名)
1.3 文本转换
把一种形式的文本变成另一种形式。包括:
-
机器翻译(中文→英文)
-
文本摘要(长文章→短摘要)
-
风格转换(口语→书面语)
理解了这些任务,我们下面来看技术是如何一步步实现它们的。
二、第一阶段:规则系统(1950s–1980s)—— 像教小孩背课文
早期的科学家非常乐观:他们认为语言就是“词典 + 语法规则”。只要把这两样东西写成代码,计算机就能理解语言。
2.1 Georgetown-IBM实验:字典加规则硬翻译
1954年,乔治城大学和IBM搞了一次轰动性的演示:把60多个俄语句子自动翻译成英语。当时的媒体兴奋地预测“三五年内机器翻译将取代人工”。结果呢?六十多年后的今天,机器翻译仍然需要人工校对。
这个系统的工作流程非常直白:
-
查词典:每个俄语单词找到对应的英语单词
-
调语序:按照英语语法规则重新排列单词顺序
-
输出句子
下面用代码模拟这个过程:
# 模拟1954年规则翻译系统的核心逻辑
class RuleBasedTranslator:
def __init__(self):
# 俄语 -> 英语 词典
self.dictionary = {
'automobilu': 'car',
'edet': 'goes',
'bistro': 'fast'
}
def translate(self, russian_text):
# 步骤1:按空格切分单词
tokens = russian_text.split()
# 步骤2:逐个查词典
english_words = [self.dictionary.get(token, token) for token in tokens]
# 步骤3:按英语语序拼接(本例中顺序相同,实际复杂得多)
return ' '.join(english_words)
# 运行示例
translator = RuleBasedTranslator()
print(translator.translate("automobilu edet bistro")) # 输出: car goes fast
这个系统的缺陷非常明显:
-
换个语言就要重写词典和所有语法规则
-
遇到词典里没有的词就直接卡壳
-
无法处理歧义(比如“bank”可以是银行也可以是河岸)
2.2 ELIZA:世界上第一个“心理医生”聊天机器人
1966年,MIT的Joseph Weizenbaum写了一个叫ELIZA的程序。它假装成一位心理医生——心理医生的特点就是不停反问,ELIZA完美模仿了这一招。
它的原理比翻译系统还简单:
-
扫描用户输入,寻找预定义的关键词
-
匹配预设的模式(比如“我感到X”)
-
用固定模板生成回复(把X填进“你为什么感到X?”)
下面是一个迷你ELIZA的实现:
import re
# 迷你版ELIZA聊天机器人
class MiniELIZA:
def __init__(self):
# 规则列表:每个元素是 (正则表达式, 回复生成函数)
self.rules = [
(re.compile(r'.*我感到(.*)', re.IGNORECASE),
lambda m: f'你为什么感到{m.group(1)}?'),
(re.compile(r'.*我(喜欢|讨厌)(.*)', re.IGNORECASE),
lambda m: f'你为什么{m.group(1)}{m.group(2)}?'),
(re.compile(r'.*你好.*', re.IGNORECASE),
lambda m: '你好,今天有什么想聊的吗?'),
]
self.default_response = "嗯,我明白了,请继续说。"
def respond(self, user_input):
# 遍历所有规则,找到第一个匹配的
for pattern, response_func in self.rules:
match = pattern.match(user_input)
if match:
return response_func(match)
return self.default_response
# 测试一下
eliza = MiniELIZA()
print(eliza.respond("我感到焦虑")) # 输出: 你为什么感到焦虑?
print(eliza.respond("我喜欢编程")) # 输出: 你为什么喜欢编程?
print(eliza.respond("你好")) # 输出: 你好,今天有什么想聊的吗?
print(eliza.respond("我的猫生病了")) # 输出: 嗯,我明白了,请继续说。
ELIZA根本不懂“焦虑”是什么,它只是像鹦鹉学舌一样,把你的话里的关键词掏出来再塞回模板。但很多用户居然真的以为它是一位善解人意的心理医生——这说明人类有多容易把简单的模式匹配误解为“智能”。
规则系统的核心缺陷:
-
规则永远写不完。英语光语法规则就有几千条,更别说处理例外了。
-
无法应对歧义和新词。比如“他吃食堂”里的“吃”其实是“去……吃饭”的意思,但词典里只会写“eat”。
-
扩展性极差。加一个新功能就要加一堆新规则,最后系统变成一团乱麻。
三、第二阶段:统计方法(1990s–2000s)—— 让数据说话
到了90年代,计算机变强了,互联网也积累了海量文本数据。科学家们换了个思路:不写规则了,让机器自己从数据里统计规律。这就是“数据驱动”的方法。
统计方法的核心思想很简单:一个词出现的概率,只取决于它前面几个词。你不需要告诉计算机“形容词通常放在名词前面”,它只要看过足够多的句子,自己就能算出“红车”出现的次数比“车红”多得多。
3.1 N-gram模型:猜下一个词是什么
N-gram是统计方法中最基础的语言模型。它的假设是:一个词的概率只取决于它前面的 N-1 个词。
-
N=2
叫 Bigram(只依赖前1个词)
-
N=3
叫 Trigram(依赖前2个词)
下面用中文例子来演示。假设我们有这样几条训练语料(已经分好词,词之间用空格隔开):
现在我们来统计 Bigram(连续两个词的出现次数):
from collections import defaultdict, Counter
# 训练语料(已分词,每个句子是词列表)
corpus = [
['我', '爱', '你'],
['我', '想', '你'],
['我', '爱', '北京'],
['我', '想', '吃', '北京', '烤鸭'],
['我', '想', '去', '北京'],
['北京', '烤鸭', '很', '好吃']
]
# 统计Bigram频次
bigram_counts = defaultdict(Counter)
for sentence in corpus:
for i in range(len(sentence) - 1):
prev_word = sentence[i]
next_word = sentence[i+1]
bigram_counts[prev_word][next_word] += 1
# 查看“我”后面都跟过哪些词
print(dict(bigram_counts['我'])) # 输出: {'爱': 2, '想': 3}
# 根据Bigram模型预测给定前一个词的下一个词(取频次最高的)
def predict_next(prev_word):
if prev_word not in bigram_counts:
return None
return max(bigram_counts[prev_word].items(), key=lambda x: x[1])[0]
print(predict_next('我')) # 输出: '想'(因为“我想”出现3次 > “我爱”2次)
这个简单的模型已经能做“续写”了:给定“我”,它认为后面最可能是“想”。不需要任何语法知识,纯靠统计就能学会词语搭配。
但N-gram也有明显缺陷:
-
如果训练数据里从来没出现过“我 吃 披萨”,模型就认为这个组合的概率是0(尽管它可能是正确的)。
-
为了解决这个问题,后来引入了平滑技术(比如给没见过的组合分配一个很小的概率,例如加一平滑)。
3.2 隐马尔可夫模型(HMM)做词性标注
词性标注就是把每个词标上名词、动词、形容词等。HMM在这个任务上很成功。它的想法是:我们看到的词是“观测值”,背后隐藏着它们的词性序列。通过统计“词性→词性”的转移概率和“词性→词”的发射概率,就可以反推出最可能的词性序列。
HMM的数学细节比较复杂,这里不展开完整代码。你只需要知道:它是在一张概率图上找最优路径,用的算法叫维特比(Viterbi)。
统计方法的进步:
-
泛化能力大大增强:只要训练数据足够多,模型就能自动学到各种语言规律。
-
不再需要人工编写语法规则。
-
但仍然需要人工设计特征——比如词本身、大小写、是否包含数字、前后缀等。特征工程仍然很繁琐。
四、第三阶段:浅层机器学习(2000s–2010s)—— 特征工程的艺术
这个阶段,研究者开始使用更复杂的机器学习模型,比如逻辑回归、支持向量机(SVM)、条件随机场(CRF)。但核心工作依然是特征工程——手工设计各种特征来喂给模型。
4.1 词袋模型 + 逻辑回归做情感分类
词袋模型(Bag-of-Words)是最简单的文本表示方法:忽略词序,只统计每个词出现了几次。
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
# 样本评论(1=好评,0=差评)
reviews = [
"服务很好 环境干净", # 好评
"服务很差 环境脏乱", # 差评
"服务一般 环境还行", # 中评(这里标为1仅作演示)
"太差了 再也不来" # 差评
]
labels = [1, 0, 1, 0]
# 将文本转换为词袋向量(注意:中文需要自己加空格分词,这里已加)
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(reviews)
print("词表:", vectorizer.get_feature_names_out())
# 输出类似: ['一般', '不来', '再也不', '太差', '很好', '很脏', ...]
# 训练逻辑回归模型
model = LogisticRegression()
model.fit(X, labels)
# 预测一条新评论
new_review = ["味道很好 但服务很差"]
X_new = vectorizer.transform(new_review)
pred = model.predict(X_new)
print("预测结果:", "好评" if pred[0]==1 else "差评")
词袋模型简单好用,但它有一个致命缺陷:完全丢失了词序信息。
比如下面两条评论:
-
A: “服务很好 但味道很差” (服务好但味道差)
-
B: “味道很好 但服务很差” (味道好但服务差)
在词袋模型中,两条评论的特征向量完全相同(都包含“服务”“很好”“但”“味道”“很差”)。但它们的实际情感倾向可能完全不同。这显然不合理。
为了解决这个问题,人们引入了n-gram特征:不光统计单个词(1-gram),还统计连续两个词(2-gram)、三个词(3-gram)。这样上面两个句子就能区分开了:
-
A的trigram: ["服务很好", "很好但", "但味道", "味道很差"]
-
B的trigram: ["味道很好", "很好但", "但服务", "服务很差"]
可以看到,两者的trigram集合完全不同。n-gram在一定程度上保留了局部词序信息,算是一种折中方案。
在代码中,只需修改CountVectorizer的参数即可:
# 使用2-gram和3-gram特征
vectorizer = CountVectorizer(ngram_range=(1, 3))
五、第四阶段:深度学习(2010s–至今)—— 自动学习特征,全面超越
深度学习的最大革命是:不再需要人工设计特征。你把原始文本直接扔进神经网络,它自己就能从数据中学习出有用的表示。
5.1 RNN / LSTM / GRU:处理序列数据
文本天然是一个序列(词一个接一个出现)。循环神经网络(RNN) 专门为此设计:它有一个隐藏状态,可以把前一个词的信息传递到下一个词。
但传统RNN有一个问题:长距离依赖。当句子很长时(比如200个词),开头的词对结尾的影响会指数级衰减,模型很难记住。LSTM(长短期记忆网络)和GRU(门控循环单元)通过引入“门”机制解决了这个问题,可以选择性地记住或遗忘信息。
LSTM(Long Short-Term Memory)
GRU(Gated Recurrent Unit)
下面是一个用PyTorch实现的简单LSTM情感分类器(仅结构示意,不含训练代码):
import torch
import torch.nn as nn
# 简单的LSTM情感分类模型
class SimpleLSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super().__init__()
# 词嵌入层:将单词ID映射为稠密向量
self.embedding = nn.Embedding(vocab_size, embed_dim)
# LSTM层
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True)
# 全连接分类层
self.classifier = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
# x的形状: (batch_size, seq_len) 每个元素是单词的ID
emb = self.embedding(x) # (batch, seq_len, embed_dim)
# LSTM输出: 所有时刻的隐藏状态 和 最后一个时刻的隐藏状态
_, (hidden, _) = self.lstm(emb) # hidden形状: (1, batch, hidden_dim)
final_repr = hidden.squeeze(0) # (batch, hidden_dim)
logits = self.classifier(final_repr) # (batch, num_classes)
return logits
关键点:你不需要再手动构造“词是否大写”“是否包含数字”等特征。LSTM会自动学习到哪些信息对分类有用。
5.2 Transformer:彻底改变NLP的架构
2017年,Google发表了论文《Attention Is All You Need》,提出了Transformer架构。它完全抛弃了RNN的循环结构,只使用注意力机制(Attention)。
注意力机制可以理解为:在生成每个输出词的时候,模型会“回看”输入句子里的所有词,并给每个词分配一个注意力分数(权重)。分数越高的词对当前输出影响越大。
比如翻译“我爱你”到“I love you”时:
-
生成“I”的时候,模型重点关注“我”
-
生成“love”的时候,重点关注“爱”
-
生成“you”的时候,重点关注“你”
这非常符合直觉。
Transformer由编码器(Encoder) 和解码器(Decoder) 组成:
-
编码器:把输入句子编码成一系列向量(每个位置包含整个句子的信息)
-
解码器:基于编码器的输出,一步一步生成目标句子
基于Transformer,研究者们开发出了预训练大模型(BERT、GPT等):
-
BERT
:擅长理解任务,如情感分析、问答、命名实体识别
-
GPT
:擅长生成任务,如写文章、对话、代码生成
这些大模型先在超大规模文本上“预训练”,学会通用的语言知识,然后在具体任务上“微调”一小下,效果碾压传统方法。
六、总结与个人看法
我们一路从1950年代走到了2020年代,技术演进可以总结为下面这张表:
阶段 | 核心方法 | 优点 | 缺点 |
规则系统 | 手写词典+语法规则 | 在小任务上可控 | 扩展性极差,规则写不完 |
统计方法 | N-gram、HMM | 自动从数据中学习 | 需要人工设计特征 |
浅层机器学习 | 逻辑回归、SVM、CRF | 效果好于纯统计 | 特征工程费时费力 |
深度学习 | RNN、LSTM、Transformer | 自动学习特征,精度高 | 需要大量数据和算力 |
大模型时代 | 预训练+微调 | 一个模型干所有事 | 训练成本极高,黑盒难解释 |
个人观点(供参考):
-
不要迷信“端到端”
。虽然深度学习省去了特征工程,但在某些垂直领域(比如金融、医疗),人工设计的规则仍然非常有用。很多时候,规则 + 统计 + 深度学习混合使用才是最稳妥的方案。
-
大模型不是终点
。GPT-4很强大,但它仍然会犯低级错误,而且成本高、推理慢。未来一定是“大模型做底座 + 小模型/规则处理具体场景”的搭配。
-
入门NLP,建议从统计方法开始
。很多人一上来就学BERT,结果连词袋模型、TF-IDF都不懂。但实际工作中,数据量小的场景下,朴素贝叶斯可能比BERT还好使。把基础打牢,再往上走,你会走得更稳。