第3章:基于正则表达式的基础分词器实现
3.1 说到正则,别把它想得太玄乎
我记得刚入行那会儿,一看到“正则表达式”这几个字就头大。说白了,它就是一套描述字符串模式的规则。你用它对文本做匹配、切分、替换。想想看,我们在Word里用Ctrl+F搜索一个词,那就是最简单的匹配。正则就是把这个能力放大、加上了通配和逻辑。
我举个实际例子吧。假设你要把一句英文按空格和标点切开:
Input: "I love NLP; it's awesome!"
Output: ["I", "love", "NLP", "it's", "awesome"]
人眼一看就明白了,但机器不知道“;”和空格是分界,“'”不是。我们需要给机器一套规则——这就是正则分词器的本质。
3.2 分词器的核心思维:先匹配,再切分
基础分词器就两步:
- 定义切分规则——用正则把“单词”和“分隔符”区分开
- 按规则切分——把文本切成列表
我最早写分词器的时候犯过一个错误:直接按空格和标点一刀切,结果把“don't”切成了“don”和“t”。这显然不是我们想要的。所以,你设计的正则必须保留某些特殊字符(如撇号、连字符),同时切掉真正的分隔符。
核心原则:分词正则 = 保留规则 + 分隔规则。先定义什么是一个合法,再说怎么拆。
3.3 正则引擎的三种“暴力”基础
不管你在Python还是其他语言里,正则引擎都基于三种基础操作。我把它们整理成了一张表,你自己看:
| 操作 | 语法示例 | 含义 |
|---|---|---|
| 连接 | ab |
匹配a后紧跟b |
| 选择 | a|b |
匹配a或b |
| 重复 | a* |
匹配0个或多个a |
听起来简单吧?但组合起来威力巨大。比如\b\w+\b就能匹配一个完整的单词。我建议你先把这三条背下来,不用背上百个元字符,核心就这三个。
3.4 评测指标:好不好的标准是什么?
分词器做完了,怎么知道好不好用?得看三个指标:
- 准确率 (Precision):切出来的里,多少个是对的。
- 召回率 (Recall):本该切出来的里,多少个被找到了。
- F1值:上两个的调和平均。说白了就是综合得分。
我在项目中遇到过一种情况:分词器准确率挺高,但召回率低得可怜。为什么?因为正则写得太严了,少部分生僻词全漏了。这是正则分词器的天生短板——遇到词典外的新词就抓瞎。但别急,后面章节会讲统计和BPE来解决这个问题。
小窍门:开发时可以用#或|||作为特殊分隔符来对齐原文和分词结果,方便肉眼对比。我个人习惯在调试时先跑一个小样本,盯着看五条结果,比跑全量数据有效十倍。
3.5 手写一个正则分词器
咱们直接看代码。下面是我用Python实现的,核心就30行。你找个空文件敲一遍,跑通再说。
import re
class RegTokenizer:
def __init__(self):
# 匹配规则:保留字母、数字、撇号、连字符、下划线
self._pattern = re.compile(r'\b\w[\w\'\-]*\w\b|\w')
def tokenize(self, text):
return self._pattern.findall(text.lower())
# 试试看
tokenizer = RegTokenizer()
sample = "I can't wait for the party — it's gonna be fun!"
tokens = tokenizer.tokenize(sample)
print(tokens)
# 输出: ['i', "can't", 'wait', 'for', 'the', 'party', "it's", 'gonna', 'be', 'fun']
我解释一下正则\b\w[\w'\-]*\w\b|\w:
\b是单词边界,帮我们定位一个单词的开始和结束\w匹配字母、数字、下划线[\w'\-]*匹配零个或多个字母、撇号、连字符- 最后的
|\w是兜底——如果上面的规则匹配不上(比如单个字符),至少匹配一个字母
注意:你们看到的\w默认不包含中文字符。如果你要处理中文,得改成[\u4e00-\u9fff\w]。我曾经为了这个“不兼容”排查了半天,最后发现是Unicode字符类的问题。嗯,这确实是个常踩的坑。
3.6 可视化:正则分词流程
下面这张SVG图直观展示了整个流程。从原始文本输入开始,经过正则匹配引擎和模式库,最后生成序列。你仔细看看箭头之间的依赖关系,就明白为什么匹配是切分的前提了。
3.7 正则分词器的三大硬伤
我用了正则分词器一年,总结了三个逃不过的缺点:
- 无法解决歧义:“这个局长”和“这个局/长”,正则永远选前者,它不理解上下文
- 词典外词处理差:看到新造词“emoji高手”,正则可能会切错
- 语言依赖强:英文分词规则和中文差异巨大,一套正则打不了全场
你可能会问:既然这么多问题,为什么还要学?我的回答是:正则分词是“起点中的起点”。它让你
- 理解什么是分词空间
- 掌握评测指标体系
- 为后面学统计分词和BPE打下最朴素的基础
一句话总结:正则分词器是最好的“入门练手”项目,50行代码让你亲自感受什么是分词。但生产环境里,它只能做Simple Tokenization的前置清洗。
3.8 实战:写一个评测脚本来验证分词质量
光说不练假把式。我通常的做法是准备一份小样本,人工标注正确答案,然后用脚本计算Precision、Recall、F1。下面是一个简洁的实现:
def evaluate_tokenizer(tokenizer, texts, gold_tokens_lists):
total_pred = 0
total_gold = 0
total_correct = 0
for text, gold in zip(texts, gold_tokens_lists):
pred = tokenizer.tokenize(text)
total_pred += len(pred)
total_gold += len(gold)
total_correct += len(set(pred) & set(gold))
precision = total_correct / total_pred if total_pred else 0
recall = total_correct / total_gold if total_gold else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0
return precision, recall, f1
# 举个例子
texts = ["I'll go; you stay.", "can't wait for the party"]
gold = [["i'll", "go", "you", "stay"], ["can't", "wait", "for", "the", "party"]]
prec, rec, f1 = evaluate_tokenizer(RegTokenizer(), texts, gold)
print(f"Precision: {prec:.2f}, Recall: {rec:.2f}, F1: {f1:.2f}")
你运行一下,就知道你的分词器到底什么水平了。我建议你把样本量增加到50条以上,包含标点、缩写、大写、数字、连字符等情况。这样测出来的分数才有意义。
好,这一章就到这里。你只要把正则核心三操作记牢,写一个评测脚本,剩下的就是调参数坑了。下次见。