外观
单词转词元id
约 3213 字大约 11 分钟
设计思路与核心概念
1. 词元化的背景与动机
词元化(Tokenization)是自然语言处理的基础步骤,将原始文本转换为模型可以处理的数字序列,主要解决以下问题:
- 文本数值化:神经网络只能处理数字,需要将文本转换为数字表示
- 词汇表管理:建立文本与数字之间的双向映射关系
- 未知词处理:处理训练时未见过的词汇,避免模型崩溃
- 特殊标记:引入特殊标记如
<|endoftext|>
、<|unk|>
等控制序列
2. 核心设计思想
SimpleTokenizerV2的核心思想是基于正则表达式的简单分词,配合词汇表映射实现文本数值化:
- 正则分割:使用正则表达式将文本分割为词汇和标点符号
- 双向映射:建立字符串到整数和整数到字符串的双向词汇表
- 未知词处理:将词汇表外的词汇统一映射为
<|unk|>
标记 - 可逆性保证:确保编码和解码过程的可逆性
3. 技术架构
词元化流程
原始文本 → 正则分割 → 清理过滤 → 未知词处理 → 词汇表映射 → 数字序列
核心组件
- 正则表达式模式:
r'([,.:;?_!"()\']|--|\s)'
- 词汇表字典:
str_to_int
和int_to_str
- 特殊标记:
<|endoftext|>
、<|unk|>
- 编解码器:
encode()
和decode()
方法
执行流程
1. 整体执行流程图
2. 详细计算流程图
处理步骤详解
步骤 | 操作描述 | 核心方法 | 输入示例 | 输出示例 |
---|---|---|---|---|
1 | 正则分割 | re.split() | "Hello, world!" | ["Hello", ",", " ", "world", "!", ""] |
2 | 清理过滤 | strip() + filter | ["Hello", ",", " ", "world", "!", ""] | ["Hello", ",", "world", "!"] |
3 | 未知词处理 | 条件替换 | ["Hello", ",", "world", "!"] | ["<unk>", ",", "<unk>", "!"] |
4 | 词汇表映射 | 字典查找 | ["<unk>", ",", "world", "!"] | [1247, 4, 1032, 8] |
5 | 解码映射 | 反向字典查找 | [1247, 4, 1032, 8] | ["<unk>", ",", "world", "!"] |
6 | 文本重构 | join() | ["<unk>", ",", "world", "!"] | "<unk> , world !" |
7 | 标点修正 | 正则替换 | "<unk> , world !" | "<unk>, world!" |
详细代码实现
# 步骤1:正则分割
pattern = r'([,.:;?_!"()\']|--|\s)'
tokens = re.split(pattern, text)
# 步骤2:清理过滤
cleaned = [item.strip() for item in tokens if item.strip()]
# 步骤3:未知词处理
processed = [item if item in self.str_to_int else "<|unk|>" for item in cleaned]
# 步骤4:词汇表映射
ids = [self.str_to_int[token] for token in processed]
# 步骤5:解码映射(解码时)
tokens = [self.int_to_str[id] for id in ids]
# 步骤6:文本重构
text = " ".join(tokens)
# 步骤7:标点修正
text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
完整代码实现
单词转词元id.py
# import urllib.request
# url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
# file_path = "the-verdict.txt"
# urllib.request.urlretrieve(url, file_path)
# 导入正则表达式模块
import re
# 定义第二代简单分词器类
class SimpleTokenizerV2:
# 初始化方法,接收词汇表字典
def __init__(self, vocab):
# 创建字符串到整数的映射字典
self.str_to_int = vocab
# 创建整数到字符串的反向映射字典
self.int_to_str = {i: s for s, i in vocab.items()}
# 编码方法:将文本转换为数字序列
def encode(self, text):
# 使用正则表达式分割文本,保留分隔符作为单独元素
# 正则模式匹配标点符号、特殊符号和空白字符
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
# 清理分割后的元素:去除首尾空格,过滤空字符串
preprocessed = [item.strip() for item in preprocessed if item.strip()]
# 处理未知词汇:将不在词汇表的元素替换为<|unk|>
preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]
# 将处理后的每个元素转换为对应的数字标识
ids = [self.str_to_int[s] for s in preprocessed]
return ids
# 解码方法:将数字序列转换回文本
def decode(self, ids):
# 将数字标识转换为对应的字符串,并用空格连接
text = " ".join([self.int_to_str[i] for i in ids])
# 修正标点符号前的多余空格(删除标点前的空格)
# 匹配标点符号前面的一个或多个空格
text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
return text
# -----------------------------------------------------------
# 以下为测试代码部分
# 读取原始文本文件
with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read() # 获取原始文本内容
# 预处理原始文本:分割成基本单元(保留分隔符)
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
# 清理分割结果:去除空格,过滤空字符串
preprocessed = [item.strip() for item in preprocessed if item.strip()]
# 构建词汇表
all_words = sorted(set(preprocessed)) # 去重并排序
# 创建初始词汇字典(整数到token的映射)
vocab = {token: integer for integer, token in enumerate(all_words)}
# 扩展特殊标记
all_tokens = sorted(list(set(preprocessed))) # 再次去重排序
all_tokens.extend(["<|endoftext|>", "<|unk|>"]) # 添加文本结束标记和未知词标记
# 重建词汇字典(包含特殊标记)
vocab = {token: integer for integer, token in enumerate(all_tokens)}
# 构造测试文本
text1 = "Hello, do you like tea?" # 测试句子1
text2 = "In the sunlit terraces of the palace." # 测试句子2
# 使用特殊标记连接两个句子
text = " <|endoftext|> ".join((text1, text2))
# 实例化分词器
tokenizer = SimpleTokenizerV2(vocab)
# 打印编码后的数字序列
print(tokenizer.encode(text))
# 解码并打印重构的文本(展示可逆性)
print(tokenizer.decode(tokenizer.encode(text)))
代码详细解析
1. 类设计架构
SimpleTokenizerV2类结构
class SimpleTokenizerV2:
def __init__(self, vocab):
# 初始化双向映射字典
def encode(self, text):
# 文本编码为数字序列
def decode(self, ids):
# 数字序列解码为文本
设计要点:
- 双向映射:同时维护字符串到整数和整数到字符串的映射
- 模块化设计:编码和解码功能独立,便于测试和维护
- 可扩展性:支持自定义词汇表和特殊标记
2. 核心算法实现
正则表达式分析
pattern = r'([,.:;?_!"()\']|--|\s)'
模式解析:
[,.:;?_!"()\']
:匹配常见标点符号--
:匹配双连字符\s
:匹配空白字符(空格、制表符、换行符等)()
:捕获组,保留分隔符
分割效果示例:
text = "Hello, world!"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
# 结果: ['Hello', ',', '', ' ', 'world', '!', '']
清理和过滤逻辑
preprocessed = [item.strip() for item in preprocessed if item.strip()]
处理步骤:
item.strip()
:去除每个元素的首尾空白if item.strip()
:过滤掉空字符串- 列表推导式:高效处理所有元素
3. 词汇表构建详解
基础词汇表创建
# 步骤1:预处理文本
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
# 步骤2:去重排序
all_words = sorted(set(preprocessed))
# 步骤3:创建映射
vocab = {token: integer for integer, token in enumerate(all_words)}
特殊标记扩展
# 添加特殊标记
all_tokens.extend(["<|endoftext|>", "<|unk|>"])
# 重建词汇表
vocab = {token: integer for integer, token in enumerate(all_tokens)}
特殊标记说明:
<|endoftext|>
:文本结束标记,用于分隔不同文档<|unk|>
:未知词标记,处理词汇表外的词汇
4. 编码过程详解
步骤1:文本预处理
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
步骤2:未知词处理
preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]
处理逻辑:
- 检查每个词是否在词汇表中
- 在词汇表中:保持原词
- 不在词汇表中:替换为
<|unk|>
步骤3:数字映射
ids = [self.str_to_int[s] for s in preprocessed]
5. 解码过程详解
步骤1:数字转字符串
text = " ".join([self.int_to_str[i] for i in ids])
步骤2:标点符号修正
text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
修正原理:
\s+
:匹配一个或多个空白字符([,.:;?!"()\'])
:捕获标点符号r'\1'
:替换为捕获的标点符号(去除前面的空格)
6. 实验配置分析
测试文本设计
text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " <|endoftext|> ".join((text1, text2))
测试目的:
- 基础功能:验证编码解码的正确性
- 特殊标记:测试
<|endoftext|>
的处理 - 未知词:测试词汇表外词汇的处理
- 可逆性:验证编码解码的可逆性
预期输出分析
# 编码结果(示例)
encoded = [1234, 5, 678, 901, 234, 567, 1245, 890, ...]
# 解码结果
decoded = "Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace."
性能特点与优化
1. 算法复杂度分析
时间复杂度
- 编码:O(n),其中n是文本长度
- 解码:O(m),其中m是数字序列长度
- 词汇表构建:O(k log k),其中k是唯一词汇数量
空间复杂度
- 词汇表存储:O(k),双向映射需要2k空间
- 临时存储:O(n),用于存储分割后的词汇列表
2. 优化策略
内存优化
class MemoryEfficientTokenizer:
def __init__(self, vocab):
self.str_to_int = vocab
# 延迟创建反向映射
self._int_to_str = None
@property
def int_to_str(self):
if self._int_to_str is None:
self._int_to_str = {i: s for s, i in self.str_to_int.items()}
return self._int_to_str
批处理优化
def batch_encode(self, texts):
"""批量编码多个文本"""
return [self.encode(text) for text in texts]
def batch_decode(self, ids_list):
"""批量解码多个序列"""
return [self.decode(ids) for ids in ids_list]
3. 扩展功能
词汇表统计
def get_vocab_stats(self):
"""获取词汇表统计信息"""
return {
'vocab_size': len(self.str_to_int),
'special_tokens': [token for token in self.str_to_int.keys()
if token.startswith('<|') and token.endswith('|>')],
'punctuation_count': len([token for token in self.str_to_int.keys()
if token in ',.:;?_!"()\'--'])
}
词汇表保存和加载
import json
def save_vocab(self, filepath):
"""保存词汇表到文件"""
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(self.str_to_int, f, ensure_ascii=False, indent=2)
@classmethod
def load_vocab(cls, filepath):
"""从文件加载词汇表"""
with open(filepath, 'r', encoding='utf-8') as f:
vocab = json.load(f)
return cls(vocab)
4. 错误处理和验证
输入验证
def encode(self, text):
if not isinstance(text, str):
raise TypeError("Input must be a string")
if not text.strip():
return []
# 原有编码逻辑...
解码验证
def decode(self, ids):
if not isinstance(ids, (list, tuple)):
raise TypeError("Input must be a list or tuple of integers")
if not all(isinstance(i, int) for i in ids):
raise TypeError("All elements must be integers")
if not all(i in self.int_to_str for i in ids):
raise ValueError("Unknown token ID found")
# 原有解码逻辑...
与现代分词器的对比
1. 功能对比
特性 | SimpleTokenizerV2 | BPE | WordPiece | SentencePiece |
---|---|---|---|---|
分割方式 | 正则表达式 | 子词合并 | 子词分割 | 统计模型 |
词汇表大小 | 固定 | 可控 | 可控 | 可控 |
未知词处理 | UNK标记 | 子词分解 | 子词分解 | 子词分解 |
多语言支持 | 有限 | 好 | 好 | 优秀 |
实现复杂度 | 简单 | 中等 | 中等 | 复杂 |
2. 优势与局限
优势
- 简单易懂:实现逻辑清晰,便于学习和调试
- 快速原型:适合快速验证想法和概念
- 可控性强:词汇表构建过程完全可控
- 内存友好:对于小规模文本处理效率较高
局限性
- 词汇表爆炸:无法处理大规模文本的词汇表增长
- 未知词问题:简单的UNK处理丢失信息
- 语言局限:主要适用于英语等空格分隔的语言
- 效率问题:对于大规模文本处理效率较低
3. 适用场景
推荐使用
- 教学演示:理解分词基本概念
- 小规模实验:快速验证模型想法
- 特定领域:词汇表相对固定的专业领域
- 原型开发:早期开发阶段的快速迭代
不推荐使用
- 生产环境:大规模商业应用
- 多语言处理:需要处理多种语言的场景
- 开放域任务:词汇表无法预先确定的任务
- 性能要求高:对处理速度有严格要求的场景
实践建议
1. 使用指南
- 词汇表大小:建议控制在10K-50K之间
- 特殊标记:根据任务需求添加必要的特殊标记
- 正则模式:根据文本特点调整正则表达式
- 测试验证:充分测试编码解码的可逆性
2. 调试技巧
- 分步验证:逐步检查分割、清理、映射各环节
- 边界测试:测试空字符串、特殊字符等边界情况
- 可视化:打印中间结果便于调试
- 单元测试:编写完整的单元测试用例
3. 扩展方向
- 子词支持:集成BPE或WordPiece算法
- 多语言:支持中文、日文等非空格分隔语言
- 压缩优化:优化词汇表存储和查找效率
- 并行处理:支持多线程或GPU加速
更新日志
2025/8/18 00:31
查看所有更新日志
bd1d0
-迁移目录于b0f2a
-docs: 完善大模型学习文档 - 增加设计思路与执行流程于dfb81
-update于dc6b2
-update于
版权所有
版权归属:NateHHX