4、深度学习分词:从序列标注到预训练时代
好,咱们进入深度学习分词这个重头戏。说实话,传统方法做到极致也就那样了——CRF 再强,特征工程还是得靠人肉堆。我当年在电商搜索项目里,光商品标题的「iPhone 14 Pro Max 256G 暗紫色」这种组合,规则就写了上百条,最后还是漏了一堆长尾 case。后来换了深度学习,嗯,世界清净多了。
4.1 基于 BiLSTM+CRF 的分词模型
先聊聊最经典的组合:BiLSTM+CRF。说白了,就是把分词当成序列标注任务来做。每个字打一个标签:B(词首)、M(词中)、E(词尾)、S(单字成词)。
为什么用 BiLSTM?因为它能看上下文。你想想看,「南京市长江大桥」这句话,到底是「南京/市长/江大桥」还是「南京市/长江大桥」?只看一个字肯定不行。BiLSTM 从左到右、从右到左各扫一遍,每个位置都能拿到全局信息。
那为什么还要加 CRF?我踩过这个坑。纯 BiLSTM 输出的是每个位置独立的标签概率,它不管标签之间的转移约束。比如 B 后面不能直接跟 S,E 后面不能直接跟 M——这些规则 CRF 天然就能学出来。我在一个医疗文本项目里试过纯 BiLSTM,结果「心肌梗死」被分成了「心/肌梗/死」,加了 CRF 层之后,这种低级错误基本绝迹了。
# 伪代码示意:BiLSTM+CRF 模型结构
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_size, embedding_dim, hidden_dim):
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.bilstm = nn.LSTM(embedding_dim, hidden_dim // 2,
bidirectional=True, batch_first=True)
self.fc = nn.Linear(hidden_dim, tag_size)
self.crf = CRF(tag_size, batch_first=True)
def forward(self, x):
emb = self.embedding(x) # [batch, seq_len, emb_dim]
lstm_out, _ = self.bilstm(emb) # [batch, seq_len, hidden_dim]
emissions = self.fc(lstm_out) # [batch, seq_len, tag_size]
return emissions
def loss(self, x, tags):
emissions = self.forward(x)
return -self.crf(emissions, tags) # 负对数似然损失
4.2 基于 Transformer 的分词模型
BiLSTM 虽然好,但有个硬伤——它按顺序处理,并行度低。Transformer 就不一样了,自注意力机制一下子看到整个序列。2017 年 Google 那篇「Attention is All You Need」出来之后,NLP 界基本就变天了。
Transformer 做分词,核心是用 Encoder 部分。每个字通过自注意力计算与其他所有字的关系,然后输出每个位置的标签。相比 BiLSTM,它有两个明显优势:
- 长距离依赖更强——「今天天气真不错,我们一起去公园散步吧」这种长句,Transformer 能直接捕捉「今天」和「散步」的关联,BiLSTM 得靠隐状态一步步传,容易衰减。
- 训练速度快——并行计算,GPU 利用率高。我试过同样的数据量,Transformer 训练时间只有 BiLSTM 的 1/3 左右。
但注意,Transformer 也有坑。它没有位置信息的先天感知,全靠 Positional Encoding 硬塞。我曾经在古文分词任务上翻过车——「之乎者也」这种虚词位置敏感,Positional Encoding 如果没调好,模型会把「之」和「乎」的语义搞混。后来我换成了相对位置编码(如 Transformer-XL 那种),效果才上来。
4.3 预训练模型(BERT)在分词中的应用
终于聊到 BERT 了。说实话,BERT 出来之后,分词这件事的「天花板」被直接抬高了。为什么?因为 BERT 在海量语料上预训练过,它已经学会了「词」的通用表示。你拿它来做分词,相当于请了个语言学博士来帮你标注——它知道「深度学习」是一个整体,而不是「深/度/学习」。
具体怎么做?很简单:把 BERT 当作特征提取器,后面接一个 CRF 层。输入句子,BERT 输出每个字的向量表示,然后 CRF 做标签解码。这就是经典的 BERT+CRF 范式。
# 使用 HuggingFace Transformers 加载 BERT
from transformers import BertTokenizer, BertModel
import torch.nn as nn
class BertCRF(nn.Module):
def __init__(self, tag_size, bert_path='bert-base-chinese'):
super().__init__()
self.bert = BertModel.from_pretrained(bert_path)
self.fc = nn.Linear(self.bert.config.hidden_size, tag_size)
self.crf = CRF(tag_size, batch_first=True)
def forward(self, input_ids, attention_mask):
outputs = self.bert(input_ids, attention_mask=attention_mask)
sequence_output = outputs.last_hidden_state # [batch, seq_len, 768]
emissions = self.fc(sequence_output) # [batch, seq_len, tag_size]
return emissions
我个人的习惯是,先用 BERT 做微调,而不是从头训练。因为 BERT 已经很强了,你只需要在领域数据上跑几个 epoch 就行。我在金融公告分词项目里,用 BERT+CRF 只训练了 3 个 epoch,F1 值就从 BiLSTM+CRF 的 94.2% 涨到了 97.8%。
| 模型 | 准确率 | 召回率 | F1 值 | 训练时间 |
|---|---|---|---|---|
| BiLSTM+CRF | 93.5% | 92.8% | 93.1% | 2.5 小时 |
| Transformer+CRF | 95.1% | 94.6% | 94.8% | 1.2 小时 |
| BERT+CRF | 97.6% | 97.3% | 97.4% | 3.8 小时 |
你看,BERT 虽然训练时间长了点,但效果提升是实打实的。不过要注意,BERT 模型大,推理速度慢。如果线上要求毫秒级响应,我建议用蒸馏版(如 TinyBERT 或 AlBERT),精度只降 0.5% 左右,速度能快 3-5 倍。
最后总结一下:深度学习分词这条路,从 BiLSTM+CRF 到 Transformer 再到 BERT,本质上是「特征提取能力」的不断升级。BiLSTM 是手动挡,Transformer 是自动挡,BERT 就是自动驾驶——但别忘了,再好的模型也离不开高质量的训练数据。我见过太多人迷信 BERT,结果标注数据一塌糊涂,模型照样翻车。嗯,数据清洗永远是第一位的,这个道理,什么时候都不过时。