你对着手机说“帮我订一张明天去北京的高铁票”手机立刻照办——这背后是一场持续了六十多年的技术马拉松。自然语言处理NLP的目标只有一个让电脑看懂人话、说人话。听起来简单做起来却难如登天。因为人类语言充满了歧义、省略、反讽和无穷无尽的新说法。今天我就带你完整回顾NLP从“手写规则”到“深度学习”的四个进化阶段。全文会穿插大量可运行的代码示例保证你能一边看一边理解。一、NLP到底在解决哪些问题在聊历史之前我们先搞清楚NLP通常处理哪几类任务。后面每个阶段的技术都是冲着这些任务去的。1.1 文本分类给整段文字贴一个标签。比如判断酒店评论是好评还是差评情感分析判断一封邮件是不是垃圾邮件判断新闻属于体育、财经还是娱乐1.2 序列标注给文本中的每个词打上一个标签。最典型的例子是命名实体识别从一句话里把人名、地名、日期、手机号等找出来。比如下面这个收货地址信息收货人王雷 手机号13812345678 地址西部硅谷大厦6栋201室我们希望自动标出王雷人名、13812345678手机号、西部硅谷大厦地名、6栋201室门牌号。序列标注常用的标记法是BIOBBegin一个实体的开头IInside一个实体的内部OOutside不是实体例如“王雷住在深圳”标注为王(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 个词。N2叫 Bigram只依赖前1个词N3叫 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[i1] 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(), keylambda 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门控循环单元通过引入“门”机制解决了这个问题可以选择性地记住或遗忘信息。LSTMLong Short-Term MemoryGRUGated 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_firstTrue) # 全连接分类层 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还好使。把基础打牢再往上走你会走得更稳。