你对着手机说“帮我订一张明天去北京的高铁票”,手机立刻照办——这背后,是一场持续了六十多年的技术马拉松。
自然语言处理(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还好使。把基础打牢,再往上走,你会走得更稳。