1. 统计模型分词技术演进之路
第一次接触中文分词时,我被"武汉市长江大桥"这个经典案例难住了。到底该分成"武汉/市/长江/大桥"还是"武汉市/长江/大桥"?传统词典分词在这里束手无策,而统计模型却给出了令人信服的解决方案。统计模型分词的核心思想很简单:字与字之间的组合概率会说话。当"长江"这两个字频繁共同出现时,它们就很可能是同一个词。
这种思路最早可以追溯到N-gram语言模型。记得2013年我做搜索引擎项目时,N-gram还是主流选择。它的优势在于计算简单,只需要统计相邻字的共现频率。比如在1亿字的语料库中,"长"出现50万次,"江"出现30万次,但"长江"一起出现了28万次,这种高共现率就很能说明问题。
但随着业务复杂度提升,我们发现N-gram有两个致命伤:一是无法处理长距离依赖(比如"人工智能"中的"人工"和"智能"间隔较远仍有关联),二是容易受数据稀疏问题困扰。这时候HMM(隐马尔可夫模型)开始进入视野,它通过引入隐藏状态(B/M/E/S标签)建立了更精细的建模框架。
2. N-gram分词实战指南
在实际工程中实现N-gram分词,我推荐使用KenLM这个开源库。它的内存效率极高,能轻松处理十亿级别的语料。下面是用Python训练的典型代码:
from kenlm import Model # 训练语料预处理 corpus = open('corpus.txt').read() with open('processed.txt','w') as f: for sent in corpus.split('\n'): f.write(' '.join(sent) + '\n') # 字间加空格 # 使用KenLM训练二元语法模型 !bin/lmplz -o 2 < processed.txt > bigram.arpa model = Model('bigram.arpa') def segment(text): # 构建全切分有向无环图 dag = {} for i in range(len(text)): dag[i] = [] for j in range(i+1, min(i+5, len(text)+1)): # 最大词长设为4 word = text[i:j] dag[i].append((j, model.score(' '.join(word)))) # 动态规划找最优路径 route = {len(text): (0.0, None)} for i in reversed(range(len(text))): route[i] = max((score + route[j][0], j) for j,score in dag[i]) # 回溯切分 result = [] i = 0 while i < len(text): j = route[i][1] result.append(text[i:j]) i = j return result这个实现有几个工程优化点:
- 限制最大词长为4(中文超过4字的词极少)
- 使用对数概率避免浮点数下溢
- 动态规划时间复杂度O(n^2)
在电商搜索场景实测时,N-gram对热门商品名的识别准确率能达到92%,但对新词(如"冰墩墩")识别率骤降至65%。这时就需要引入回退机制:当二元组概率低于阈值时,退回到一元组频率判断。
3. HMM分词的工程实践
HMM需要解决三个关键问题:
- 状态转移概率(B->M, M->E等)
- 观测概率(某状态下出现特定字的概率)
- 初始状态分布
以人民日报语料为例,状态转移矩阵统计结果通常是:
- B后接E的概率约30%(双字词)
- B后接M的概率约60%(多字词)
- M后接M的概率约20%
- M后接E的概率约80%
用Python实现Viterbi算法时,我习惯用numpy做向量化计算:
import numpy as np def viterbi(obs, states, start_p, trans_p, emit_p): V = [{}] for st in states: V[0][st] = {"prob": start_p[st] * emit_p[st].get(obs[0],1e-10), "prev": None} for t in range(1, len(obs)): V.append({}) for st in states: max_tr_prob = max(V[t-1][prev_st]["prob"]*trans_p[prev_st].get(st,1e-10) for prev_st in states) for prev_st in states: if V[t-1][prev_st]["prob"] * trans_p[prev_st].get(st,1e-10) == max_tr_prob: max_prob = max_tr_prob * emit_p[st].get(obs[t],1e-10) V[t][st] = {"prob": max_prob, "prev": prev_st} break # 回溯 opt = [] max_prob = max(value["prob"] for value in V[-1].values()) previous = None for st, data in V[-1].items(): if data["prob"] == max_prob: opt.append(st) previous = st break for t in range(len(V)-2, -1, -1): opt.insert(0, V[t+1][previous]["prev"]) previous = V[t+1][previous]["prev"] return opt实际应用中要注意数据平滑问题。我常用Good-Turing平滑处理未登录词,将出现次数r的词概率估计为(r+1)*N(r+1)/N(r),其中N(r)是出现r次的词数。
4. CRF分词的进阶技巧
CRF相比HMM的最大优势是可以自由设计特征模板。在智能客服系统中,我设计的特征模板包括:
- 当前字及其前后2个字
- 当前字的偏旁部首
- 是否数字/英文/标点
- 在常用姓氏列表中
- 在地名词典中
使用CRF++训练时的特征模板示例:
# Unigram U00:%x[-2,0] U01:%x[-1,0] U02:%x[0,0] U03:%x[1,0] U04:%x[2,0] # Bigram B在金融领域文本中,我发现加入这些特征后F1值提升了7%:
- 是否在股票名称列表
- 是否在金融术语词典
- 是否包含货币符号(如¥,$)
对于实时性要求高的场景,可以用模型裁剪技术。通过移除权重绝对值小于阈值的特征,能使模型大小减少40%而精度仅下降1-2%。
5. 算法选型决策树
根据多年实战经验,我总结出这个选型流程图:
| 考虑维度 | N-gram | HMM | CRF |
|---|---|---|---|
| 训练数据量 | >1亿字 | >5000万字 | >1000万字 |
| 实时性要求 | <5ms | <20ms | <50ms |
| 新词发现能力 | 弱 | 中 | 强 |
| 领域适应性 | 需要重新训练 | 部分可迁移 | 特征可复用 |
| 硬件资源 | 内存<1GB | 内存<4GB | 需要GPU加速 |
具体建议:
- 搜索引擎建议用N-gram+HMM混合模型,兼顾速度与精度
- 智能客服首选CRF,应对大量口语化表达
- 社交媒体文本分析可用HMM,平衡性能和资源消耗
在硬件部署方面,N-gram模型可以轻松部署到嵌入式设备。去年我们就把一个2-gram模型移植到了智能手表上,内存占用仅18MB,分词速度达到5000字/秒。
6. 最新技术演进观察
近年来预训练语言模型给传统统计方法带来了新思路。我在实验中发现,用BERT提取的字向量作为CRF的输入特征,能使OOV(未登录词)识别率提升35%。具体做法是:
from transformers import BertModel import torch bert = BertModel.from_pretrained('bert-base-chinese') def get_bert_features(text): inputs = tokenizer(text, return_tensors="pt") with torch.no_grad(): outputs = bert(**inputs) return outputs.last_hidden_state.squeeze(0) # 将BERT输出与传统特征拼接 crf_features = np.concatenate([bert_features, handcraft_features], axis=1)这种混合方法在医疗文本分词任务中达到了96.3%的F1值,比纯CRF提高了4.2个百分点。不过需要注意,这会增加10倍以上的计算开销。
另一个趋势是统计方法与深度学习融合。比如LSTM-CRF模型,既保留了CRF的序列建模能力,又通过LSTM自动学习特征表示。在2023年的实验中,这种架构在微博文本分词上取得了当前最佳效果。