Transformers 中的 CANINE:字符级无分词 Transformer 的架构解析与实用指南
CANINE(Character Architectures with No tokenization In Neural Encoders)是一种完全跳过分词(tokenization-free)的 Transformer 编码器:它不做 subword / wordpiece 切分,而是直接以「字符」为基本输入单元,在原始 Unicode 码点(codepoint)上完成训练与推理。本文以 docs/source/en/model_doc/canine.md 为骨架,结合本仓库内 configuration_canine.py、modeling_canine.py 与 tokenization_canine.py 的源码实现,讲清它的设计动机、"下采样-上采样"的高效编码管线、配置与分词器细节,并给出可直接运行的嵌入生成与下游微调示例。读完你会掌握如何在多语言、复杂拼写或含噪声文本场景下落地这一无需词表的模型方案。
为什么需要"无分词"的模型
传统预训练语言模型依赖分词器把文本切分成 subword 或 wordpiece。这带来两处天然的痛点:
- 词表与规则耦合语言:对分词规则复杂或不一致的语言,同一个词在不同拼写/书写系统中切分结果差异很大,OOV 与形态切分错误会沿模型层层放大;
- 噪声输入脆弱:打字错误、非常规拼写会直接破坏分词结果,进而劣化表征。
CANINE 的答案是"以字符为单位":模型处理的是原始 Unicode 字符序列,不需要词表、不需要语言相关的分词器。理论上对任意书写系统的任意文本都能工作,且天然对拼写噪声更鲁棒。
字符输入的代价是序列显著变长,为此 CANINE 使用了一个关键技巧——输入早期下采样(downsampling):先在浅层编码器中对字符上下文建模,再把相邻若干字符压缩成一个"分子(molecule)"交给深层 Transformer,避免深层网络逐字符处理带来的开销,从而在保留字符级信息的同时维持可接受的训练与推理效率。
原始 CANINE checkpoint 发布在 Google 组织下,模型卡片中的 google/canine-c 与 google/canine-s(见 configuration_canine.py)即为其典型代表。
架构核心:字符编码 → 下采样 → 深层编码 → 上采样
CanineModel 的骨架定义在 modeling_canine.py,前向流程在 forward。与普通 BERT 不同,它在一条序列上串联了 三个 Transformer 编码器 与两条卷积变换:
- 字符嵌入(CanineEmbeddings):为每个 Unicode 码点生成向量。由于全 Unicode 共有 1,114,112 个码点,直接建大词表不可行,CANINE 采用 多重哈希嵌入(multiple hashing):把
hidden_size切分成num_hash_functions份 shard,每个 hash 函数用一份独立的num_hash_buckets大小的嵌入表。_hash_bucket_tensors中通过((input_ids + 1) * prime) % num_buckets计算桶号(素数表_PRIMES支持最多 16 个 hash 函数),最后把各 shard 的向量torch.cat回完整维度,见 modeling_canine.py。位置嵌入与 token type 嵌入随后叠加,经 LayerNorm 与 dropout 输出。 - 初始浅层字符编码器(initial char encoder):单层、带块状局部注意力的 Transformer,负责对整条字符序列做轻量上下文建模,见 initial_char_encoder 的构造。局部注意力把长度为 stride 的相邻字符切成块,只允许块内互相 attend(第一位置是否全局 attend 可通过参数开关),把字符级计算的复杂度约束在可接受范围,见 CanineAttention 的局部注意力分支。
- 字符到分子下采样(CharactersToMolecules):将
[CLS]单独保留后,对剩余字符序列做 kernel=rate、stride=rate 的 Conv1d(等价于每downsampling_rate个字符平均为一个向量),得到长度为char_seq_len / downsampling_rate的"分子序列",见 modeling_canine.py。 - 深层 BERT 编码器(deep encoder):在压缩后的分子序列上运行完整的深层 Transformer(默认 12 层),分子序列比字符序列短 4 倍,这正是效率来源。
- 分子到字符上采样:用
_repeat_molecules把每个分子向量按downsampling_rate复制回字符长度(不足一个 window 的余数位置通过重复最后一个分子补齐,见 modeling_canine.py),再与第 2 步的字符编码在特征维拼接,随后用upsampling_kernel_size宽的 ConvProjection(Conv1d,kernel=4)把hidden_size*2投影回hidden_size,见 modeling_canine.py。 - 最终浅层字符编码器(final char encoder):在上采样回字符分辨率的表示上再跑一层全注意力 Transformer,得到逐字符的最终输出
last_hidden_state;同时池化器CaninePooler取序列首 token 过 Linear+Tanh 得到pooler_output(该线性层权重在预训练的 next sentence prediction 目标中训练),见 modeling_canine.py。
注意:由于模型输出的是字符级表示,因此 token 分类类任务的标签应理解为"字符类别"而非词级别标签;源码与文档一致指出(ConvProjection 中的注释与 NotImplementedError):按字符位置做 MLM 的 CanineForMaskedLM 目前尚未支持——CANINE 的预训练利用了 MLM 与 NSP 目标,但在本仓库中它主要用于下游任务微调。
配置详解:CanineConfig 参数表
配置类 CanineConfig 定义于 configuration_canine.py,model_type = "canine"。它完整继承了 BERT 风格公共参数,并新增了 CANINE 特有参数:
| 参数 | 默认值 | 含义 |
|---|---|---|
downsampling_rate |
4 |
深层编码器前字符序列的下采样倍率(每 N 个字符 → 1 个分子) |
upsampling_kernel_size |
4 |
卷积上采样投影层的核宽,用于把维度从 hidden_size*2 投影回 hidden_size |
num_hash_functions |
8 |
多重哈希嵌入所用的哈希函数个数,每个哈希函数各有一份独立嵌入矩阵 |
num_hash_buckets |
16384 |
哈希桶总数(每个哈希函数嵌入表的行数) |
local_transformer_stride |
128 |
初始浅层编码器局部注意力的分块步长/宽度;默认 128 便于 TPU/XLA 内存对齐 |
公共参数与默认值(同文件 L57-L75):hidden_size=768、num_hidden_layers=12(作用于深层编码器)、num_attention_heads=12、intermediate_size=3072、hidden_act="gelu"、hidden_dropout_prob=0.1、attention_probs_dropout_prob=0.1、max_position_embeddings=16384、type_vocab_size=16、initializer_range=0.02、layer_norm_eps=1e-12;特殊 token id 定义为 pad_token_id=0、bos_token_id=0xE000、eos_token_id=0xE001——它们正是 tokenization_canine.py 中 Unicode 私有使用区(Private Use Area)的特殊码点。
创建配置与随机初始化模型:
>>> from transformers import CanineConfig, CanineModel
>>>
>>> # Initializing a CANINE google/canine-s style configuration
>>> configuration = CanineConfig()
>>>
>>> # Initializing a model (with random weights) from the google/canine-s style configuration
>>> model = CanineModel(configuration)
>>>
>>> # Accessing the model configuration
>>> configuration = model.config
需注意
downsampling_rate必须能整除序列各维度;下采样使用nn.Conv1d(kernel=downsampling_rate, stride=downsampling_rate)(CharactersToMolecules),而上采样核宽upsampling_kernel_size决定卷积投影每次覆盖的字符窗口数量。
分词器即"字符切分器":CanineTokenizer 原理
CanineTokenizer(tokenization_canine.py)是一个纯字符切分器,核心逻辑只有两步:_tokenize 用 list(text) 把文本切分成单个字符,_convert_token_to_id 用 ord(token) 把字符转成 Unicode 码点整数。词表空间即为全 Unicode:vocab_size 返回 1114112(UNICODE_VOCAB_SIZE),不需要下载任何 vocab.txt 之类的词表文件。
特殊符号被定义在 Unicode 私有使用区内,确保永远不会与真实字符冲突:
| 特殊码点 | 值 | 说明 |
|---|---|---|
PAD |
0x0000 |
[PAD] |
CLS / BOS |
0xE000 |
[CLS] / [BOS] |
SEP / EOS |
0xE001 |
[SEP] / [EOS] |
MASK |
0xE003 |
[MASK] |
RESERVED |
0xE004 |
[RESERVED] |
因此源码注释中特别强调:不设 [UNK] 类字符——任何码点天然在"词表"内,不存在未知词。分词器还实现/继承并适配了 build_inputs_with_special_tokens、get_special_tokens_mask、create_token_type_ids_from_sequences 等接口(本仓库中借助 token_type_ids_pattern="all_zeros"、special_tokens_pattern="cls_sep" 等声明驱动基类的特殊 token 处理逻辑,见 tokenization_canine.py)。单条序列可直接处理任意长度文本;model_max_length 默认 2048,而 CanineConfig.max_position_embeddings 为 16384,超长截断与 padding 在调用分词器时完成。
动手生成句子嵌入:三种调用入口
模型文档展示了用 Pipeline 与 AutoModel 生成句嵌入,并指出同样流程也支持命令行入口。以下片段可直接运行(示例文本来自模型文档):
方式一:Pipeline
from transformers import pipeline
pipeline = pipeline(
task="feature-extraction",
model="google/canine-c",
device=0,
)
pipeline("Plant create energy through a process known as photosynthesis.")
方式二:AutoModel(手工码点编码)
import torch
from transformers import AutoModel
model = AutoModel.from_pretrained("google/canine-c", device_map="auto")
text = "Plant create energy through a process known as photosynthesis."
input_ids = torch.tensor([[ord(char) for char in text]])
outputs = model(input_ids)
pooled_output = outputs.pooler_output
sequence_output = outputs.last_hidden_state
这里 ord(char) 手工把每个字符映射为码点——这正是 CANINE"不需要 tokenizer"的体现。若要走命令行方式,本仓库的 transformers CLI 通过子命令组织(见 cli/transformers.py),其中 serve 子命令即可将加载好的模型(含 pipeline)封装为对外推理服务(见 cli/serve.py);仓库内部还提供了把原始 TF checkpoint 转成 PyTorch 的脚本 convert_canine_original_tf_checkpoint_to_pytorch.py。
方式三:配合 AutoTokenizer 做批量编码(推荐)
CANINE 可以带 tokenizer 使用,也可以完全不使用。但对于批量推理与训练,模型文档明确建议使用 tokenizer 统一补齐(padding)与截断(truncation),保证 batch 内序列等长:
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer("google/canine-c")
model = AutoModel.from_pretrained("google/canine-c")
inputs = ["Life is like a box of chocolates.", "You never know what you gonna get."]
encoding = tokenizer(inputs, padding="longest", truncation=True, return_tensors="pt").to(model.device)
outputs = model(**encoding)
CanineTokenizer 与 CanineConfig、CanineModel 等均已注册进 Auto API(见 models/auto/auto_mappings.py、models/auto/tokenization_auto.py 与 models/auto/modeling_auto.py),因此 AutoModel.from_pretrained("google/canine-c")、AutoConfig 与 AutoTokenizer 均可直接工作。
下游任务模型清单与输出结构
CanineModel 之外,仓库为常用任务提供了带任务头的模型(全部定义于 modeling_canine.py),Auto 映射覆盖情况如 models/auto/modeling_auto.py 所示:
| 模型类 | 任务头部与用法 | 对应源码 |
|---|---|---|
CanineModel |
基础编码器:输出逐字符 last_hidden_state 与 pooler_output |
CanineModel |
CanineForSequenceClassification |
池化输出上接 Linear 分类头,适用于 GLUE 类任务;num_labels==1 时按回归(MSE)计算损失 |
modeling_canine.py |
CanineForMultipleChoice |
输入展平为 (batch, num_choices, seq),池化输出上接 Linear→1 打分后对 choice 维度求交叉熵 |
modeling_canine.py |
CanineForTokenClassification |
直接使用逐字符序列输出接分类头;标签按字符而非词计算 | modeling_canine.py |
CanineForQuestionAnswering |
序列输出上接 qa_outputs 预测 span 起止 logits,平均 start/end 交叉熵作为损失 |
modeling_canine.py |
CanineModelOutputWithPooling |
CANINE 专属输出结构,额外携带浅层/深层三组编码器的 hidden_states 与 attentions |
modeling_canine.py |
CanineModelOutputWithPooling 与标准 BaseModelOutputWithPooling 略有差异(其自定义 docstring 也说明了这一点):由于模型内部串联了三个编码器,开启 output_hidden_states=True 时返回的 hidden_states 元组包含三组状态,且深层编码器对应的状态长度为 seq_len // downsampling_rate(分子分辨率),浅层编码器对应状态保持字符分辨率。这一特性也被模型测试用例显式校验:test_modeling_canine.py 中按"浅层 2 个输入/输出状态 + 深层 num_hidden_layers+1 个状态 + 最终浅层 2 个状态"的规律断言各层形状(见 tests/models/canine/test_modeling_canine.py)。
分类、多选、token 分类、QA 等模型的输入参数均包含 input_ids / attention_mask / token_type_ids / position_ids / inputs_embeds,与 BERT 家族一致;不同任务仅在 labels 形态上不同(序列级标量、choice 下标、逐字符标签、起止位置对)。CaninePreTrainedModel 声明了 supports_gradient_checkpointing = True(modeling_canine.py),长序列微调时可通过开启梯度检查点来降低显存占用。
使用注意与最佳实践
- 可不用分词器,但批量推荐用它:单条输入可手工构造码点张量;批量训练/推理务必用
AutoTokenizer设置padding="longest"、truncation=True,否则长度不一致会导致attention_mask语义错乱、批次内无法对齐。 - 定位是"微调基座":模型文档明确指出 CANINE 主要面向下游任务微调使用;预训练阶段以 masked language modeling 与 next sentence prediction 为目标。二者不应混淆——当前 PyTorch 实现中面向掩码字符预测的 MLM 任务头尚不支持(对应路径抛出
NotImplementedError)。 - 长度与效率:单条最长受
max_position_embeddings=16384约束,默认model_max_length=2048;超长输入请先截断。字符级输入天然较长,深层编码器只在 1/4 分辨率的分子序列上运行,这是模型在"逐字符 + 高效"之间取得平衡的关键设计。 - 对齐与正确性:
downsampling_rate与upsampling_kernel_size、local_transformer_stride需与预期序列长度配合使用;默认 128 的 stride 是为了 TPU/XLA 内存对齐而设定,见 configuration_canine.py。分子层注意力掩码由_downsample_attention_mask用 MaxPool 对字符掩码做下采样得到(modeling_canine.py)。 - 验证与转换:仓库在 tests/models/canine 提供建模与分词器的完整测试;若需从 Google 原始 TF checkpoint 转换权重,可参考转换脚本 convert_canine_original_tf_checkpoint_to_pytorch.py。
总而言之,CANINE 为"分词规则复杂、噪声多、语种混杂"的文本场景提供了一个词表无关的编码器方案:以全 Unicode 字符为输入、用哈希嵌入控制参数规模、用浅层局部注意力 + 下采样/上采样卷积换取效率,并以三阶段编码器输出既可用于句级任务(pooler_output)也可用于逐字符任务(last_hidden_state)的双分辨率表示。开发者可直接通过 AutoModel 与 AutoTokenizer 按本文示例完成嵌入生成与各下游任务的微调接入。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00