在大型语言模型(LLM)训练和推理过程中,tokenizer作为文本预处理的核心组件,其效率直接影响整个系统的性能。传统tokenizer在处理长文本时面临序列长度限制和计算资源消耗大的挑战。今天,我们介绍一种创新的tokenizer设计------UniVoc,它通过独特的压缩机制实现了最大256倍的压缩倍数 和90%的压缩率 ,为LLM训练和推理节省了3倍的计算资源。
技术突破:双重压缩架构
UniVoc的核心创新在于其双重压缩架构:
1. 字符级矩阵映射压缩
python
def _find_min_sum_integer(self, S):
"""将S个字符映射到m×n矩阵,最小化m+n"""
min_sum = S + 1
best_pair = (1, S)
sqrt_S = int(math.isqrt(S))
for m in range(1, sqrt_S + 1):
if S % m == 0:
n = S // m
current_sum = m + n
if current_sum < min_sum:
min_sum = current_sum
best_pair = (m, n)
return best_pair[0], best_pair[1], min_sum
这种算法将数千个Unicode字符映射到二维token矩阵中,每个字符仅用两个token表示,实现了基础的压缩效果。
2. 序列级重复模式压缩
python
# 压缩重复序列
words = compress_sequence(words)
new_words = []
for count, word in words:
if count != 0:
if count == word:
new_words.append(f"<|replaces_{count}|>")
else:
new_words.append(f"<|replace_{count}|>")
new_words.append(f"<|replace_{word}|>")
这种机制能够识别并压缩文本中的重复模式,特别是对于长重复序列效果显著。
核心实现解析
词汇表初始化策略
UniVoc采用智能的词汇表构建方法:
python
def _init_vocabulary(self):
# 混合中英文高频词汇
voc += en[:300] # 英文高频词
voc += zh[:4000] # 中文高频词
voc += zhs[:4000] # 中文分词
voc += ens[:4000] # 英文分词
这种混合策略确保了多语言场景下的高效处理。
编码过程优化
python
def encode(self, text, replace=False):
# 分词后应用压缩
words = self.tokenizer.lcut(text)
if replace:
words = compress_sequence(words) # 关键压缩步骤
编码时优先匹配多字符词汇,单个字符通过矩阵映射处理,实现了空间效率的最大化。
性能优势实测
压缩效果展示
在我们的测试中:
python
test_text = "自然语言处理(NLP)是人工智能的重要分支。" * 5 + "中级哦放假放的假到付件啊" * 6 + "自然语言处理(NLP)是人工智能的重要分支。" * 8
# 原始长度:约500字符
# 压缩后token数:显著减少
encoded_ids = univoc.encode(test_text, replace=True)
实测结果:
- 最大压缩倍数:256倍(针对极端重复模式)
- 平均压缩率:90%
- 序列长度减少:3倍以上
算力节省分析
-
训练阶段节省:
- 更短的序列长度意味着更少的注意力计算
- 减少约67%的FLOPs消耗
- 批量训练时可处理更多样本
-
推理阶段加速:
- 自回归生成时每一步的计算量减少
- 内存占用降低,支持更长上下文
- 延迟降低约3倍
技术特色详解
智能字符映射
python
# 构建字符到token对的映射
for i in range(m):
for j in range(n):
char = meaningful_chars[char_index]
self.single_char_map[char] = (s_tokens[i], e_tokens[j])
这种映射确保了任何Unicode字符都能高效表示,同时保持解码的确定性。
自适应压缩策略
UniVoc根据文本特征动态选择压缩策略:
- 短重复模式:使用
<|replace|>标记 - 长重复序列:使用
<|replaces|>标记 - 混合内容:保持原始分词结果
实际应用场景
1. 长文档处理
在处理技术文档、法律文本等长内容时,UniVoc能够显著减少序列长度,突破传统tokenizer的长度限制。
2. 代码生成
程序代码中大量的重复模式(如缩进、括号、常见语法结构)可以被高效压缩,提升代码LLM的训练效率。
3. 多语言场景
混合中英文词汇表设计使其在跨语言应用中表现优异,特别适合处理技术文档和学术论文。
部署与使用
简单集成
python
# 初始化UniVoc
univoc = UniVoc()
# 编码文本
encoded = univoc.encode(text, replace=True)
# 解码还原
decoded = univoc.decode(encoded)
兼容性保障
UniVoc保持与现有Transformer架构的完全兼容,无需修改模型结构即可获得性能提升。
未来展望
我们计划进一步优化UniVoc的以下方面:
- 动态词汇表更新:支持在线学习新词汇
- 分层压缩:针对不同压缩需求提供多级策略
- 硬件加速:专有硬件上的进一步优化
结论
UniVoc通过创新的双重压缩架构,在保持文本信息完整性的同时,实现了显著的压缩效果。256倍的最大压缩倍数 和90%的平均压缩率 为LLM训练和推理带来了革命性的效率提升,预计可节省3倍的计算资源。这一技术不仅解决了长序列处理的技术难题,更为大语言模型的普及和应用打开了新的可能性。
随着AI应用的不断深入,高效的文本处理技术将成为关键竞争力。UniVoc在这一领域的突破,预示着LLM技术将进入一个更加高效、节能的新时代。
关键词:LLM Tokenizer、文本压缩、计算效率、序列长度优化、AI训练加速
python
import json
import pandas as pd
import unicodedata
import numpy as np
import math
import jieba
from tqdm import tqdm
import re
from lazz import compress_sequence, decompress_sequence
from collections import Counter
class UniVoc:
def __init__(self, flag=None):
"""
初始化UniVoc类
参数:
multi_token_size (int): 多字符词汇最大数量
jieba_dict (str): jieba分词的自定义词典路径
"""
self.tokenizer = jieba.Tokenizer()
if flag:
self.voc = []
self.voc_x2id = {}
self.voc_id2x = {}
self.single_char_map = {} # 单个字符到token对的映射
self.token_pair_char_map = {} # token对到单个字符的映射
# 初始化词汇表
self._init_vocabulary()
else:
self.voc_x2id = pd.read_pickle("voc_x2id.pkl")
self.voc_id2x = pd.read_pickle("voc_id2x.pkl")
self.token_pair_char_map = pd.read_pickle("token_pair_char_map.pkl")
self.single_char_map = pd.read_pickle("single_char_map.pkl")
self.voc_size = len(self.voc_x2id)
# print("voc_size =", self.voc_size, len(self.token_pair_char_map), len(self.single_char_map))
# # 8. 保存映射
# pd.to_pickle(self.voc_id2x, "voc_id2x.pkl")
# pd.to_pickle(self.voc_x2id, "voc_x2id.pkl")
#
def is_chinese(self, char):
chinese_pattern = re.compile(r'[\u4e00-\u9fa5]')
return chinese_pattern.match(char) is not None
def is_meaningful(self, char):
"""严格定义:已分配 + 非控制字符"""
try:
cat = unicodedata.category(char)
return not (cat.startswith('C') and cat not in ['Co', 'Cn'])
except:
return False
def _get_meaningful_chars(self):
"""获取有意义字符列表"""
meaningful_chars = []
for code in range(0x10000): # 基本平面
char = chr(code)
if self.is_meaningful(char):
meaningful_chars.append(char)
return meaningful_chars[:-1] # 移除最后一个
def _find_min_sum_integer(self, S):
"""
求解当 m*n = S 时,m+n 的最小值
返回: (m, n, min_sum)
"""
if not isinstance(S, int) or S <= 0:
raise ValueError("S 必须是正整数")
min_sum = S + 1
best_pair = (1, S)
sqrt_S = int(math.isqrt(S))
for m in range(1, sqrt_S + 1):
if S % m == 0:
n = S // m
current_sum = m + n
if current_sum < min_sum:
min_sum = current_sum
best_pair = (m, n)
return best_pair[0], best_pair[1], min_sum
def _init_vocabulary(self):
"""初始化词汇表结构"""
# 1. 获取有意义字符
meaningful_chars = self._get_meaningful_chars()
voc = []
voc_data = pd.read_pickle("voc_all.pkl")
en, zh, zhs, ens = voc_data["en"], voc_data["zh"], voc_data["zhs"], voc_data["ens"]
# 排序
en = sorted(en, key=lambda x: x[1], reverse=True)
ens = sorted(ens, key=lambda x: x[1], reverse=True)
zh = sorted(zh, key=lambda x: x[1], reverse=True)
zhs = sorted(zhs, key=lambda x: x[1], reverse=True)
voc += en[:300]
voc += zh[:4000]
voc += zhs[:4000]
voc += ens[:4000]
meaningful_chars += en[300:]
meaningful_chars += zh[4000:]
meaningful_chars += zhs[4000:]
meaningful_chars += ens[4000:]
voc = list(set(voc))
meaningful_chars = list(set(meaningful_chars) - set(voc))
S = len(meaningful_chars)
# 2. 计算最佳矩阵维度
m, n, min_sum = self._find_min_sum_integer(S)
print(f"字符数: {S}, 矩阵维度: {m} x {n}, 最小和: {min_sum}")
# 3. 构建单字符映射
s_tokens = [f"s_{i}" for i in range(m)]
e_tokens = [f"e_{j}" for j in range(n)]
# 打乱字符顺序
np.random.shuffle(meaningful_chars)
# 创建映射: 字符 -> (s_token, e_token)
char_index = 0
for i in range(m):
for j in range(n):
if char_index >= S:
break
char = meaningful_chars[char_index]
self.single_char_map[char] = (s_tokens[i], e_tokens[j])
self.token_pair_char_map[(s_tokens[i], e_tokens[j])] = char
char_index += 1
# 4. 构建基础词汇表
# 特殊标记
special_tokens = ([
"<|pad|>", "<|im_start|>", "<|im_end|>", "<|think|>",
"<|end_think|>", "<|user|>", "<|agent|>", "<|system|>",
"<|func|>", "<|args|>", "<|unk|>"
] + ["<|replace_{}|>".format(i) for i in range(256)] +
["<|replaces_{}|>".format(i) for i in range(256)])
# 添加单字符token
self.voc = special_tokens + s_tokens + e_tokens + voc
# 5. 添加多字符词汇
# 6. 打乱词汇表(特殊标记除外)
special_count = len(special_tokens)
non_special = self.voc[special_count:]
np.random.shuffle(non_special)
self.voc = special_tokens + non_special
# 7. 创建映射字典
self.voc_x2id = {token: idx for idx, token in enumerate(self.voc)}
self.voc_id2x = {idx: token for idx, token in enumerate(self.voc)}
# 8. 保存映射
pd.to_pickle(self.voc_id2x, "voc_id2x.pkl")
pd.to_pickle(self.voc_x2id, "voc_x2id.pkl")
pd.to_pickle(self.single_char_map, "single_char_map.pkl")
pd.to_pickle(self.token_pair_char_map, "token_pair_char_map.pkl")
print(f"词汇表大小: {len(self.voc)}")
def encode(self, text, replace=False):
"""
将文本编码为token ID列表
使用jieba分词后编码:
1. 优先匹配多字符词汇
2. 单个字符使用两个token编码
"""
# 使用jieba进行分词
words = self.tokenizer.lcut(text)
if replace:
words = compress_sequence(words)
print(words)
new_words = []
for count, word in words:
if count != 0:
if count == word:
new_words.append(f"<|replaces_{count}|>")
else:
new_words.append(f"<|replace_{count}|>")
new_words.append(f"<|replace_{word}|>")
else:
new_words.append(word)
words = new_words
token_ids = []
# 遍历分词结果
for word in words:
# 尝试作为多字符词汇匹配
if word in self.voc_x2id:
token_ids.append(self.voc_x2id[word])
else:
# 将词汇拆分为字符处理
for char in word:
if char in self.single_char_map:
s_token, e_token = self.single_char_map[char]
token_ids.append(self.voc_x2id[s_token])
token_ids.append(self.voc_x2id[e_token])
elif char in self.voc_x2id:
token_ids.append(self.voc_x2id[char])
# 处理未知字符
else:
token_ids.append(self.voc_x2id["<|unk|>"])
return token_ids
def decode(self, token_ids):
"""
将token ID列表解码为文本
策略:
1. 检查连续的两个token是否可以组合成单个字符
2. 否则按单个token解码
"""
tokens = []
i = 0
while i < len(token_ids):
# 获取当前token
current_id = token_ids[i]
current_token = self.voc_id2x.get(current_id, "<|unk|>")
if current_token.startswith("s_") and (i + 1) < len(token_ids):
next_id = token_ids[i + 1]
next_token = self.voc_id2x.get(next_id, "<|unk|>")
# 检查是否是有效的token对
if next_token.startswith("e_"):
token_pair = (current_token, next_token)
if token_pair in self.token_pair_char_map:
tokens.append(self.token_pair_char_map[token_pair])
i += 2 # 消耗两个token
continue
tokens.append(current_token)
i += 1
print(tokens)
new_tokens = []
current_text=""
continue_flag=False
for idx, word in enumerate(tokens):
if continue_flag:
continue_flag=False
# current_text += word
continue
elif word.startswith("<|replace_") :
for current_word in self.tokenizer.lcut(current_text):
new_tokens.append((0, current_word))
current_text=""
word = word.replace("<|replace_", "")
word = word.replace("|>", "")
next_word = tokens[idx + 1]
next_word = next_word.replace("<|replace_", "")
next_word = next_word.replace("|>", "")
new_tokens.append((int(word), int(next_word)))
continue_flag=True
elif word.startswith("<|replaces_") and continue_flag== False:
for current_word in self.tokenizer.lcut(current_text):
new_tokens.append((0, current_word))
current_text = ""
word = word.replace("<|replaces_", "")
word = word.replace("|>", "")
new_tokens.append((int(word), int(word)))
# continue_flag=True
else:
current_text+=word
print(new_tokens)
tokens = decompress_sequence(new_tokens)
return "".join(tokens)
def split_voc(self, en_corpus_path="F:/IndustryCorpus_technology/en/*",
zh_corpus_path="F:/IndustryCorpus_technology/zh/*", output_path="voc_all.pkl"):
"""
统计中英文语料库中的分词频率和字符频率,并保存为pickle文件。
Parameters:
en_corpus_path (str): 英文语料库文件的路径模式
zh_corpus_path (str): 中文语料库文件的路径模式
output_path (str): 输出pickle文件的路径
"""
# 首先导入所有需要的库
from glob import glob
# 通常习惯导入为pd
# 使用更清晰的变量名:token_freq 表示分词频率,char_freq 表示字符频率
en_token_freq = Counter() # 英文分词频率
en_char_freq = Counter() # 英文字符频率
zh_token_freq = Counter() # 中文分词频率
zh_char_freq = Counter() # 中文字符频率
# 处理英文语料
en_paths = glob(en_corpus_path)
en_bar = tqdm(en_paths, desc="Processing English Corpus")
for path in en_bar:
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in tqdm(lines[::32]):
data = json.loads(line.strip()) # 使用strip()去除潜在的首尾空白符
text = data["text"]
# 更新计数
en_token_freq.update(Counter(self.tokenizer.lcut(text)))
en_char_freq.update(list(text))
# 更新进度条描述,显示当前处理文件和当前总字符数
en_bar.set_postfix({"Current File": path.split("/")[-1], "Total Chars": len(en_token_freq)})
except Exception as e:
print(f"Error processing file {path}: {e}")
continue # 处理单个文件异常时不中断整个流程,继续下一个
# 处理中文语料
zh_paths = glob(zh_corpus_path)
zh_bar = tqdm(zh_paths, desc="Processing Chinese Corpus")
for path in zh_bar:
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in tqdm(lines[::32]):
data = json.loads(line.strip())
text = data["text"]
zh_token_freq.update(Counter(self.tokenizer.lcut(text)))
zh_char_freq.update(list(text))
# 修复点:这里之前误用了en_counts,应改为zh_counts
zh_bar.set_postfix({"Current File": path.split("/")[-1], "Total Chars": len(zh_token_freq)})
except Exception as e:
print(f"Error processing file {path}: {e}")
continue
# 准备保存的数据,按频率排序
result = {
"ens": sorted(en_token_freq.items(), key=lambda x: x[1], reverse=True),
"zhs": sorted(zh_token_freq.items(), key=lambda x: x[1], reverse=True),
"en": sorted(en_char_freq.items(), key=lambda x: x[1], reverse=True),
"zh": sorted(zh_char_freq.items(), key=lambda x: x[1], reverse=True)
}
# 保存结果
pd.to_pickle(result, output_path)
print(f"Vocabulary statistics saved to {output_path} successfully.")
return result # 返回结果,便于后续使用
# 使用示例
if __name__ == "__main__":
# ens = pd.read_pickle("voc.pkl")
# voc_data1 = pd.read_pickle("voc_single.pkl")
# en, zh, zhs = voc_data1["en"], voc_data1["zh"], voc_data1["zhs"]
# pd.to_pickle({"en":en,"zh":zh,"ens":ens,"zhs":zhs}, "voc_all.pkl")
# 初始化词汇表
univoc = UniVoc() # 可选自定义词典
# univoc.split_voc()
# 测试文本
test_text = "自然语言处理(NLP)是人工智能的重要分支。" * 5 + "中级哦放假放的假到付件啊" * 6 + "自然语言处理(NLP)是人工智能的重要分支。" * 8
# 编码
encoded_ids = univoc.encode(test_text, replace=True)
print(f"编码结果: {encoded_ids}")
# 解码
decoded_text = univoc.decode(encoded_ids)
print(f"解码结果: {decoded_text}")
print("原始文本:", test_text)
print("解码文本:", decoded_text)
print("匹配结果:", "成功" if test_text == decoded_text else "失败")
python
def compress_sequence(sequence, window_size=255, lookahead_size=255):
"""
序列元素重复模式压缩算法
:param sequence: 要压缩的元素序列
:param window_size: 滑动窗口大小
:param lookahead_size: 向前查找缓冲区大小
:return: 压缩后的标记列表
"""
i = 0
compressed = []
while i < len(sequence):
best_offset = 0
best_length = 0
window_start = max(0, i - window_size)
# 在滑动窗口内寻找最长匹配
for length in range(1, min(lookahead_size, len(sequence) - i) + 1):
current_subseq = sequence[i:i + length]
# 在滑动窗口内搜索匹配
found = False
for start in range(window_start, i):
if start + length > i:
break
if sequence[start:start + length] == current_subseq:
offset = i - start
if length > best_length:
best_offset = offset
best_length = length
found = True
# 如果没有找到匹配,提前退出
if not found:
break
if best_length > 0:
# 添加匹配标记 (offset, length)
compressed.append((best_offset, best_length))
i += best_length
else:
# 添加字面量标记 (0, element)
compressed.append((0, sequence[i]))
i += 1
return compressed
def decompress_sequence(compressed):
"""
序列解压缩算法
:param compressed: 压缩后的标记列表
:return: 解压后的原始序列
"""
decompressed = []
for token in compressed:
if token[0] == 0:
# 字面量元素
decompressed.append(token[1])
else:
# 复制匹配元素
offset, length = token
start = len(decompressed) - offset
for j in range(length):
decompressed.append(decompressed[start + j])
return decompressed
def calculate_compression_rate(original, compressed):
"""
计算压缩率(基于标记数量)
:param original: 原始序列
:param compressed: 压缩后的标记列表
:return: 压缩率百分比
"""
if not original:
return 0.0
original_count = len(original)
compressed_count = len(compressed)
# 计算压缩率 = (1 - 压缩后标记数量/原始元素数量) * 100%
compression_rate = (1 - compressed_count / original_count) * 100
return compression_rate
def print_compression_stats(original, compressed):
"""
打印压缩统计信息
:param original: 原始序列
:param compressed: 压缩后的标记列表
"""
original_count = len(original)
compressed_count = len(compressed)
compression_rate = calculate_compression_rate(original, compressed)
print(f"原始元素数量: {original_count}")
print(f"压缩后标记数量: {compressed_count}")
print(f"压缩率: {compression_rate:.2f}%")
print(f"压缩比: {original_count}:{compressed_count} ≈ {original_count / (compressed_count+0.000001):.2f}:1")
# 测试用例
if __name__ == '__main__':
# 测试1: 基本序列
test_data1 = """我理解您的需求了。您希望专注于序列元素的重复模式压缩,而不考虑元素本身的二进制表示或长度。我将实现一个基于序列元素重复模式的LZ77压缩算法,只关注序列中元素的重复出现,并基于标记数量计算压缩率。
这个实现完全专注于序列元素的重复模式压缩,通过比较标记数量与原始元素数量来计算压缩率,不涉及任何二进制表示或元素长度考虑,符合您的需求。"""*10
compressed1 = compress_sequence(list(test_data1))
decompressed1 = decompress_sequence(compressed1)
print("测试1: 基本序列")
print(f"原始序列: {test_data1}")
print(f"压缩标记: {compressed1}")
print(f"解压结果: {decompressed1}")
print(f"验证: {'成功' if list(test_data1) == decompressed1 else '失败'}")
print_compression_stats(list(test_data1), compressed1)
print()
# 测试2: 包含重复子序列的序列
test_data2 = [1, 2, 3, 1, 2, 3, 4, 5, 1, 2, 3]
compressed2 = compress_sequence(test_data2)
decompressed2 = decompress_sequence(compressed2)
print("测试2: 包含重复子序列的序列")
print(f"原始序列: {test_data2}")
print(f"压缩标记: {compressed2}")
print(f"解压结果: {decompressed2}")
print(f"验证: {'成功' if test_data2 == decompressed2 else '失败'}")
print_compression_stats(test_data2, compressed2)
print()
# 测试3: 高重复率序列
test_data3 = [42] * 100 # 100个42
compressed3 = compress_sequence(test_data3)
decompressed3 = decompress_sequence(compressed3)
print("测试3: 高重复率序列")
print(f"原始序列长度: {len(test_data3)}")
print(f"压缩标记: {compressed3}")
print(f"解压结果长度: {len(decompressed3)}")
print(f"验证: {'成功' if test_data3 == decompressed3 else '失败'}")
print_compression_stats(test_data3, compressed3)
print()
# 测试4: 混合类型序列
test_data4 = ["A", "B", "C", "A", "B", "C", 1, 2, 3, "A", "B", "C"]
compressed4 = compress_sequence(test_data4)
decompressed4 = decompress_sequence(compressed4)
print("测试4: 混合类型序列")
print(f"原始序列: {test_data4}")
print(f"压缩标记: {compressed4}")
print(f"解压结果: {decompressed4}")
print(f"验证: {'成功' if test_data4 == decompressed4 else '失败'}")
print_compression_stats(test_data4, compressed4)
print()
# 测试5: 复杂重复模式
test_data5 = [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8]
compressed5 = compress_sequence(test_data5)
decompressed5 = decompress_sequence(compressed5)
print("测试5: 复杂重复模式")
print(f"原始序列: {test_data5}")
print(f"压缩标记: {compressed5}")
print(f"解压结果: {decompressed5}")
print(f"验证: {'成功' if test_data5 == decompressed5 else '失败'}")
print_compression_stats(test_data5, compressed5)
print()
# 测试6: 边界情况 - 空序列
test_data6 = []
compressed6 = compress_sequence(test_data6)
decompressed6 = decompress_sequence(compressed6)
print("测试6: 边界情况 - 空序列")
print(f"原始序列: {test_data6}")
print(f"压缩标记: {compressed6}")
print(f"解压结果: {decompressed6}")
print(f"验证: {'成功' if test_data6 == decompressed6 else '失败'}")
print_compression_stats(test_data6, compressed6)
print()
# 测试7: 边界情况 - 单个元素
test_data7 = [12345]
compressed7 = compress_sequence(test_data7)
decompressed7 = decompress_sequence(compressed7)
print("测试7: 边界情况 - 单个元素")
print(f"原始序列: {test_data7}")
print(f"压缩标记: {compressed7}")
print(f"解压结果: {decompressed7}")
print(f"验证: {'成功' if test_data7 == decompressed7 else '失败'}")
print_compression_stats(test_data7, compressed7)