Quixer 量子 Transformer 完全教程
逐步深入理解 GPT 架构
microgpt 教程¶
本文翻译自 https://karpathy.github.io/2026/02/12/microgpt/
仅用200行纯Python代码(零依赖),即可训练并推理GPT——这已经是我能压缩到的极致了。
2026-02-12 07:00:00
这是我的新艺术项目 microgpt 的简要指南。一个200行的纯Python单文件,无任何依赖,能够训练和推理GPT。该文件包含了所需的完整算法内容:文档数据集、分词器、自动微分引擎、GPT-2风格的神经网络架构、Adam优化器、训练循环和推理循环。其他一切都只是效率问题。这个脚本是多个项目(micrograd、makemore、nanogpt等)以及十年执着于将LLM简化到其本质的结晶。
相关链接:
- GitHub Gist 完整源代码:microgpt.py
- 网页版:https://karpathy.ai/microgpt.html
- Google Colab 笔记本:点击打开
以下是我带领感兴趣的读者逐步讲解代码的指南。
数据集¶
大型语言模型的燃料是一个文本数据流,可选地分成多个文档集。在生产级应用中,每个文档可能是一个互联网网页,但对于microgpt,我们使用一个更简单的例子——32,000个名字,每行一个:
import os # os.path.exists
import random # random.seed, random.choices, random.gauss, random.shuffle
random.seed(42) # 在混沌中建立秩序
# 让输入数据集 `docs`:list[str] 文档列表(例如名字数据集)
if not os.path.exists('input.txt'):
import urllib.request
names_url = 'https://raw.githubusercontent.com/karpathy/makemore/master/names.txt'
urllib.request.urlretrieve(names_url, 'input.txt')
docs = [l.strip() for l in open('input.txt').read().strip().split('\n') if l.strip()] # list[str] 文档列表
random.shuffle(docs)
print(f"num docs: {len(docs)}")
数据集看起来像这样,每个名字是一个文档:
emma
olivia
ava
isabella
sophia
charlotte
mia
amelia
harper
... (大约32,000个名字)
模型的目标是学习数据中的模式,然后生成具有相似统计特征的新文档。提前剧透:脚本运行结束时,我们的模型将生成(『幻觉』!)新的、看似合理的名字。抢先看:
sample 1: kamon
sample 2: ann
sample 3: karai
sample 4: jaire
sample 5: vialan
sample 6: karia
sample 7: yeran
sample 8: anna
sample 9: areli
sample 10: kaina
sample 11: konna
sample 12: keylen
sample 13: liole
sample 14: alerin
sample 15: earan
sample 16: lenne
sample 17: kana
sample 18: lara
sample 19: alela
sample 20: anton
看起来平平无奇,但从ChatGPT这样的模型角度来看,你与它的对话只是一个看起来有点特别的『文档』。当你用提示初始化文档时,从模型的角度来看,它的响应只是文档的统计式补全。
分词器¶
在后台,神经网络处理数字而不是字符,所以我们需要一种方法将文本转换为整数标记ID序列,反之亦然。像tiktoken(由GPT-4使用)这样的生产分词器为了效率在字符块上操作,但最简单的可能的分词器只是为数据集中的每个唯一字符分配一个整数:
# 让有一个分词器将字符串翻译为离散符号并反向翻译
uchars = sorted(set(''.join(docs))) # 数据集中的唯一字符变成标记ID 0..n-1
BOS = len(uchars) # 特殊“序列开始”(BOS)标记的标记ID
vocab_size = len(uchars) + 1 # 总的唯一标记数,+1是BOS
print(f"vocab size: {vocab_size}")
在上面的代码中,我们收集数据集中所有唯一字符(即所有小写字母a-z),排序后每个字母按其索引获得一个ID。请注意,整数值本身没有任何意义;每个标记只是一个离散符号。代替0、1、2,它们完全可以是不同的emoji。此外,我们还创建了一个特殊标记BOS(序列开始),它充当分隔符:告诉模型『一个新文档在这里开始/结束』。训练期间,每个文档两侧都被BOS包裹:[BOS, e, m, m, a, BOS]。模型学会BOS启动一个新名字,另一个BOS结束它。因此,最终词汇表大小为27(26个可能的小写字母a-z加上1个BOS标记)。
自动微分¶
训练神经网络需要梯度:对于模型中的每个参数,我们需要知道『如果我把这个数稍微调高一点,损失会上升还是下降,幅度多大?』。计算图有多个输入(模型参数和输入标记),但汇聚到单个标量输出:损失(我们将在下面精确定义损失是什么)。反向传播从该单个输出开始,沿着图反向工作,计算损失相对于每个输入的梯度。它依赖微积分中的链式法则。在生产中,像PyTorch这样的库会自动处理。这里,我们在一个叫Value的类中从头实现:
import math # math.log, math.exp
class Value:
__slots__ = ('data', 'grad', '_children', '_local_grads')
def __init__(self, data, children=(), local_grads=()):
self.data = data # 在前向传播中计算的此节点的标量值
self.grad = 0 # 损失相对于此节点的导数,在反向传播中计算
self._children = children # 计算图中此节点的子节点
self._local_grads = local_grads # 此节点相对于其子节点的局部导数
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, (self, other), (1, 1))
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data, (self, other), (other.data, self.data))
def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),))
def log(self): return Value(math.log(self.data), (self,), (1/self.data,))
def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),))
def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),))
def __neg__(self): return self * -1
def __radd__(self, other): return self + other
def __sub__(self, other): return self + (-other)
def __rsub__(self, other): return other + (-self)
def __rmul__(self, other): return self * other
def __truediv__(self, other): return self * other**-1
def __rtruediv__(self, other): return other * self**-1
def backward(self):
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._children:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1
for v in reversed(topo):
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad
我意识到这是数学和算法上最密集的部分,我为此录制了一个2.5小时的视频:micrograd视频。简而言之,Value包装单个标量数字(.data)并跟踪它是如何被计算出来的。把每个运算想象成一块小乐高积木:它接受一些输入,产生一个输出(前向传播),并且知道它的输出相对于每个输入会如何变化(局部梯度)。这就是自动微分需要每块积木提供的全部信息。其他一切只是链式法则——把积木串在一起。
每次你对Value对象进行数学运算(加法、乘法等),结果是一个新的Value,它记住其输入(_children)和该运算的局部导数(_local_grads)。例如,__mul__记录$\frac{\partial(a \cdot b)}{\partial a} = b$和$\frac{\partial(a \cdot b)}{\partial b} = a$。完整的乐高积木集:
| 操作 | 前向 | 局部梯度 |
|---|---|---|
a + b |
$$a + b$$ | $$\frac{\partial}{\partial a} = 1, \quad \frac{\partial}{\partial b} = 1$$ |
a * b |
$$a \cdot b$$ | $$\frac{\partial}{\partial a} = b, \quad \frac{\partial}{\partial b} = a$$ |
a ** n |
$$a^n$$ | $$\frac{\partial}{\partial a} = n \cdot a^{n-1}$$ |
log(a) |
$$\ln(a)$$ | $$\frac{\partial}{\partial a} = \frac{1}{a}$$ |
exp(a) |
$$e^a$$ | $$\frac{\partial}{\partial a} = e^a$$ |
relu(a) |
$$\max(0, a)$$ | $$\frac{\partial}{\partial a} = \mathbf{1}_{a > 0}$$ |
backward()方法以反向拓扑顺序遍历此图(从损失开始,到参数结束),在每一步应用链式法则。如果损失是$L$,节点$v$有一个子节点$c$,局部梯度为$\frac{\partial v}{\partial c}$,那么:
如果你对微积分不太熟悉,这看起来可能有点吓人,但这本质上只是以直观的方式将两个数相乘。可以这样理解:『如果汽车的速度是自行车的两倍,自行车的速度是步行人的四倍,那么汽车的速度就是步行人的2 × 4 = 8倍。』链式法则就是同样的思想:你沿着路径乘以变化率。
我们从在损失节点设置self.grad = 1开始,因为$\frac{\partial L}{\partial L} = 1$:损失相对于自身的变化率就是1。从那里开始,链式法则沿着回到参数的每条路径乘以局部梯度。
注意+=(累加,不是赋值)。当一个值在图中的多个地方被使用时(即图有分支),梯度沿着每条分支独立流回,必须求和。这是多变量链式法则的结果:如果$c$通过多条路径对$L$有贡献,总导数是来自每条路径的贡献之和。
backward()完成后,图中的每个Value都有一个.grad包含$\frac{\partial L}{\partial v}$,这告诉我们微调该值会使最终损失如何变化。
以下是一个具体例子。注意a被使用了两次(图有分支),所以它的梯度是两条路径之和:
a = Value(2.0)
b = Value(3.0)
c = a * b # c = 6.0
L = c + a # L = 8.0
L.backward()
print(a.grad) # 4.0 (dL/da = b + 1 = 3 + 1, 经由两条路径)
print(b.grad) # 2.0 (dL/db = a = 2)
这正是PyTorch的.backward()给出的结果:
import torch
a = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(3.0, requires_grad=True)
c = a * b
L = c + a
L.backward()
print(a.grad) # tensor(4.)
print(b.grad) # tensor(2.)
这和PyTorch的loss.backward()运行的是相同算法——只是在标量上而非张量(标量数组)上运行——算法上完全一致,显著更小更简单,但当然效率低得多。
让我们详细说明.backward()的计算结果。自动微分计算出,如果L = a*b + a,且a=2、b=3,那么a.grad = 4.0告诉我们a对L的局部影响。如果你微调输入a,L会朝什么方向变化?这里L对a的导数是4.0,意味着如果我们增加a一小点(比如0.001),L将增加大约4倍(0.004)。同样,b.grad = 2.0意味着同样的微调b会增加L大约2倍(0.002)。换句话说,这些梯度告诉我们每个输入对最终输出(损失)的影响方向(正或负取决于符号)和陡峭程度(大小)。这让我们能够迭代地微调神经网络参数来降低损失,从而改进模型的预测。
参数¶
参数是模型的知识。它们是一个大集合的浮点数(为自动微分用Value包装),从随机开始,在训练期间迭代优化。每个参数的确切角色将在我们下面定义模型架构后更有意义,但现在我们只需要初始化它们:
n_embd = 16 # 嵌入维度
n_head = 4 # 注意力头数
n_layer = 1 # 层数
block_size = 16 # 最大序列长度
head_dim = n_embd // n_head # 每个头的维度
matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]
state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd), 'lm_head': matrix(vocab_size, n_embd)}
for i in range(n_layer):
state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd)
params = [p for mat in state_dict.values() for row in mat for p in row]
print(f"num params: {len(params)}")
每个参数从高斯分布中抽取的小随机数初始化。state_dict将它们组织成命名矩阵(借用PyTorch的术语):嵌入表、注意力权重、MLP权重和最终输出投影。我们还将所有参数展平为单个列表params,以便优化器稍后遍历它们。在我们的小模型中,总共4,192个参数。GPT-2有16亿,现代LLM有数千亿。
架构¶
模型架构是一个无状态函数:它接收一个标记、一个位置、参数以及来自前面位置的缓存键/值,并返回logits(分数),表示模型认为接下来应该出现哪个标记。我们遵循GPT-2的设计,做了一些简化:用RMSNorm代替LayerNorm,无偏置,用ReLU代替GeLU。首先是三个小辅助函数:
def linear(x, w):
return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
linear是矩阵-向量乘法。它接收向量x和权重矩阵w,计算w每行的点积。这是神经网络的基本构建块:一个可学习的线性变换。
def softmax(logits):
max_val = max(val.data for val in logits)
exps = [(val - max_val).exp() for val in logits]
total = sum(exps)
return [e / total for e in exps]
softmax将原始分数向量(logits,范围从$-\infty$到$+\infty$)转换为概率分布:转换后所有值在$[0, 1]$范围内且求和为1。我们先减去最大值以保证数值稳定性(数学上不改变结果,但防止exp溢出)。
def rmsnorm(x):
ms = sum(xi * xi for xi in x) / len(x)
scale = (ms + 1e-5) ** -0.5
return [xi * scale for xi in x]
rmsnorm(均方根归一化)将向量重新缩放,使其值的均方根为1。这防止激活值在网络中流动时膨胀或缩小,从而稳定训练。它是原始GPT-2中使用的LayerNorm的更简单变体。
现在看模型本身:
def gpt(token_id, pos_id, keys, values):
tok_emb = state_dict['wte'][token_id] # 标记嵌入
pos_emb = state_dict['wpe'][pos_id] # 位置嵌入
# 隐藏状态:包含 n_embd 个标量 Value 对象,代表当前位置的上下文信息
x = [t + p for t, p in zip(tok_emb, pos_emb)] # 联合标记和位置嵌入
x = rmsnorm(x)
for li in range(n_layer):
# 1) 多头注意力块
x_residual = x
x = rmsnorm(x)
# 注意力块:这是GPT相对于MLP的核心创新
q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
v = linear(x, state_dict[f'layer{li}.attn_wv'])
# 将当前K/V加入缓存,与历史对比
keys[li].append(k) # 记住所有历史的key
values[li].append(v) # 记住所有历史的value
x_attn = []
# 注意力头是注意力块内部的并行处理单元
for h in range(n_head):
# 对每个注意力头计算
hs = h * head_dim # 头的起始位置
# 切分维度:每个头处理维度的一个子集
q_h = q[hs:hs+head_dim] # 当前查询
k_h = [ki[hs:hs+head_dim] for ki in keys[li]] # 所有历史键
v_h = [vi[hs:hs+head_dim] for vi in values[li]] # 所有历史值
# ⭐ 核心计算:点积匹配 + Softmax = 注意力权重
attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 for t in range(len(k_h))]
attn_weights = softmax(attn_logits) # 权重都 ∈ [0,1] 且求和为1
# 加权求和历史信息
head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)]
x_attn.extend(head_out) # 拼接到总输出
# 合并所有头的输出
x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) # 投影回去
x = [a + b for a, b in zip(x, x_residual)] # 残差连接
# 2) MLP块:两层前馈网络
x_residual = x
x = rmsnorm(x)
x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
x = [a + b for a, b in zip(x, x_residual)] # 残差连接
logits = linear(x, state_dict['lm_head'])
return logits
该函数处理一个标记(ID为token_id)在特定位置和时刻(pos_id),以及来自之前迭代的一些上下文,这些上下文总结在keys和values的激活中,称为KV缓存。以下是逐步讲解:
嵌入。 神经网络无法直接处理像5这样的原始标记ID。它只能处理向量(数字列表)。所以我们为每个可能的标记关联一个可学习的向量,将其作为神经签名输入。标记ID和位置ID各自从各自的嵌入表(wte和wpe)中查找一行。这两个向量相加,给模型一个既编码标记是什么又编码它在序列中在哪里的表示。现代LLM通常跳过位置嵌入,改用其他基于相对位置的方案,例如RoPE。
注意力块。 当前标记被投影为三个向量:查询(Q)、键(K)和值(V)。直观地说,查询(Q)说『我在找什么?』,键(K)说『我包含什么?』,值(V)说『如果被选中,我会提供什么?』。例如,在名字『emma』中,当模型在第二个『m』并试图预测接下来会出现什么时,它可能学到一个类似『最近出现了哪些元音?』的查询。早前的『e』会有一个与这个查询很好匹配的键,因此它获得高注意力权重,它的值(关于是元音的信息)流入当前位置。键和值被追加到KV缓存中,以便之前的位置可用。每个注意力头计算其查询与所有缓存键之间的点积(按$\sqrt{d_{head}}$缩放),应用softmax获得注意力权重,然后取缓存值的加权和。所有头的输出被拼接并通过attn_wo投影。值得强调的是,注意力块是位置t处的标记唯一可以『查看』过去0..t-1中标记的确切且唯一的地方。注意力是一种标记通信机制。
MLP块。 MLP是『多层感知器』的缩写,它是一个两层前馈网络:先投影到嵌入维度的4倍,应用ReLU,再投影回来。这是模型每个位置进行大部分『思考』的地方。与注意力不同,这部分计算完全局限于当前时刻t的信息。Transformer将通信(注意力)与计算(MLP)穿插。
残差连接。 注意力和MLP块都将其输出加回其输入(x = [a + b for ...])。这让梯度直接流过网络,使更深的模型可训练。
输出。 最终隐藏状态由lm_head投影到词汇表大小,为词汇表中的每个标记产生一个logit。在我们的情况下,只是27个数字。logit越高 = 模型认为对应标记更可能接下来出现。
你可能会注意到我们在训练期间使用了KV缓存,这不太寻常。人们通常将KV缓存仅与推理关联。但KV缓存在概念上始终存在,即使在训练期间也是如此。在生产实现中,它只是隐藏在高度向量化的注意力计算中,该计算同时处理序列中的所有位置。由于microgpt一次处理一个标记(无批处理维度,无并行时间步),我们显式构建KV缓存。与典型的推理设置不同(KV缓存持有分离的张量),这里缓存的键和值是计算图中的活Value节点,所以我们实际上通过它们进行反向传播。
训练循环¶
现在我们将所有东西串联起来。训练循环重复:(1)选择一个文档,(2)在其标记上运行模型前向传播,(3)计算损失,(4)反向传播获得梯度,(5)更新参数。
# 让有Adam——被祝福的优化器及其缓冲区
learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
m = [0.0] * len(params) # 第一矩缓冲区
v = [0.0] * len(params) # 第二矩缓冲区
# 按顺序重复
num_steps = 1000 # 训练步数
for step in range(num_steps):
# 取单个文档,分词,用特殊BOS标记在两侧包裹
doc = docs[step % len(docs)]
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
n = min(block_size, len(tokens) - 1)
# 将标记序列通过模型前向传播,一路构建到损失的计算图。
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
losses = []
for pos_id in range(n):
token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
logits = gpt(token_id, pos_id, keys, values)
probs = softmax(logits)
loss_t = -probs[target_id].log()
losses.append(loss_t)
loss = (1 / n) * sum(losses) # 文档序列上的最终平均损失。愿你的损失很低。
# 反向传播损失,计算相对于所有模型参数的梯度。
loss.backward()
# Adam优化器更新:根据相应梯度更新模型参数。
lr_t = learning_rate * (1 - step / num_steps) # 线性学习率衰减
for i, p in enumerate(params):
m[i] = beta1 * m[i] + (1 - beta1) * p.grad
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
m_hat = m[i] / (1 - beta1 ** (step + 1))
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
p.grad = 0
print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}")
让我们逐一讲解:
分词。 每个训练步骤选择一个文档,并用BOS在两侧包裹:名字『emma』变成[BOS, e, m, m, a, BOS]。模型的任务是:给定前面的标记,预测每个下一个标记。
前向传播与损失。 我们一次一个地将标记馈入模型,随着进行构建KV缓存。在每个位置,模型输出27个logits,我们通过softmax将其转换为概率。每个位置的损失是正确下一个标记的负对数概率:$-\log p(\text{target})$。这被称为交叉熵损失。直观地,损失衡量预测错误程度:模型对实际接下来发生的内容有多惊讶。如果模型给正确标记分配概率1.0,它完全不惊讶,损失为0。如果分配接近0的概率,模型非常惊讶,损失趋向$+\infty$。我们对文档上每个位置的损失取平均,获得单个标量损失。
反向传播。 一次调用loss.backward()通过整个计算图运行反向传播,从损失一路回到softmax、模型和每个参数。之后,每个参数的.grad告诉我们如何改变它以降低损失。
Adam优化器。 我们可以直接做p.data -= lr * p.grad(梯度下降),但Adam更聪明。它为每个参数维护两个运行平均值:m跟踪最近梯度的均值(动量,像一个滚动的球),v跟踪最近平方梯度的均值(为每个参数自适应调整学习率)。m_hat和v_hat是偏差校正,用于解释m和v被初始化为零且需要预热的事实。学习率在训练过程中线性衰减。更新后,我们为下一步重置.grad = 0。
在1,000步上,损失从约3.3(在27个标记中随机猜测:$-\log(1/27) \approx 3.3$)下降到约2.37。越低越好,最低可能为0(完美预测),所以仍有改进空间,但模型显然在学习名字的统计模式。
推理¶
训练完成后,我们可以从模型中采样新名字。参数被冻结,我们只是在循环中运行前向传播,将每个生成的标记反馈为下一个输入:
# 推理:愿模型对我们胡言乱语
temperature = 0.5 # 在 (0, 1] 中,控制生成文本的『创意』程度,从低到高
print("\n--- 推理(新的、幻觉的名字) ---")
for sample_idx in range(20):
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
token_id = BOS
sample = []
for pos_id in range(block_size):
logits = gpt(token_id, pos_id, keys, values)
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
if token_id == BOS:
break
sample.append(uchars[token_id])
print(f"sample {sample_idx+1:2d}: {''.join(sample)}")
我们用BOS标记开始每个样本,它告诉模型『开始一个新名字』。模型产生27个logits,我们将其转换为概率,并根据这些概率随机采样一个标记。那个标记被反馈为下一个输入,我们重复直到模型再次产生BOS(意思是『我完成了』)或达到最大序列长度。
temperature参数控制随机性。在softmax之前,我们将logits除以温度。温度1.0直接从模型学习的分布中采样。较低的温度(如这里的0.5)锐化分布,使模型更保守,倾向于选择最高概率的选项。接近0的温度将始终选择单个最可能的标记(贪心解码)。较高的温度使分布更平坦,产生更多样但可能不太连贯的输出。
运行它¶
你只需要Python(无需pip install,零依赖):
python train.py
该脚本在我的MacBook上大约需要1分钟。你会看到每一步打印的损失:
train.py
num docs: 32033
vocab size: 27
num params: 4192
step 1 / 1000 | loss 3.3660
step 2 / 1000 | loss 3.4243
step 3 / 1000 | loss 3.1778
step 4 / 1000 | loss 3.0664
step 5 / 1000 | loss 3.2209
step 6 / 1000 | loss 2.9452
step 7 / 1000 | loss 3.2894
step 8 / 1000 | loss 3.3245
step 9 / 1000 | loss 2.8990
step 10 / 1000 | loss 3.2229
step 11 / 1000 | loss 2.7964
step 12 / 1000 | loss 2.9345
step 13 / 1000 | loss 3.0544
...
观察它从~3.3(随机水平)下降到~2.37。这个数字越低,网络对序列中接下来会出现什么的预测就越好。训练结束时,训练标记序列的统计模式知识被提炼在模型参数中。固定这些参数,我们现在可以生成新的、幻觉的名字。你会看到(再次):
sample 1: kamon
sample 2: ann
sample 3: karai
sample 4: jaire
sample 5: vialan
sample 6: karia
sample 7: yeran
sample 8: anna
sample 9: areli
sample 10: kaina
sample 11: konna
sample 12: keylen
sample 13: liole
sample 14: alerin
sample 15: earan
sample 16: lenne
sample 17: kana
sample 18: lara
sample 19: alela
sample 20: anton
作为在你自己电脑上运行脚本的替代方案,你可以直接在 Google Colab笔记本 上运行它,并向Gemini提问相关问题。试着玩一玩这个脚本!你可以尝试不同的数据集,或者训练更长时间(增加num_steps),或增大模型尺寸来获得越来越好的结果。
渐进式构建¶
要像剥洋葱一样逐层看代码构建,建议的演进路径大致如下:
| 文件 | 新增内容 |
|---|---|
train0.py |
二元语法计数表——无神经网络,无梯度 |
train1.py |
MLP + 手动梯度(数值与分析)+ SGD |
train2.py |
自动微分(Value类)——替换手动梯度 |
train3.py |
位置嵌入 + 单头注意力 + RMSNorm + 残差连接 |
train4.py |
多头注意力 + 层循环——完整GPT架构 |
train5.py |
Adam优化器——这就是 train.py |
我创建了一个名为 build_microgpt.py 的Gist,在Revisions中你可以看到所有这些版本以及每步之间的差异。我认为这可能是逐步浏览代码库的一种有帮助的方式——每次只添加一个组件。
现实世界的GPT¶
microgpt包含了训练和运行GPT的完整算法本质。但在这和像ChatGPT这样的生产级LLM之间,有一长串不同的东西。它们都不改变核心算法和整体布局,但它们是使其在规模上实际运作的关键。按相同章节顺序来看:
数据。 代替3.2万个短名字,生产模型在数万亿标记的互联网文本上训练:网页、书籍、代码等。数据被去重、按质量过滤,并在各领域之间精心配比。
分词器。 代替单个字符,生产模型使用子词分词器如BPE(字节对编码),这些分词器学会将经常共同出现的字符序列合并为单个标记。常见词如『the』变成单个标记,稀有词被分解为片段。这给出约10万个标记的词汇表,并且因为模型在每个位置看到更多内容而效率高得多。
自动微分。 microgpt在纯Python中的标量Value对象上操作。生产系统使用张量(数字的大型多维数组)并在GPU/TPU上运行,每秒执行数十亿次浮点运算。像PyTorch这样的库在张量上处理自动微分,像FlashAttention这样的CUDA内核融合多个操作以加速。数学完全一样,只是对应许多标量并行处理。
架构。 microgpt有4,192个参数。GPT-4级别的模型有数千亿。总体上它是一个非常相似的Transformer神经网络,只是宽得多(嵌入维度超过10,000)和深得多(100+层)。现代LLM还融入了一些更多类型的乐高积木并调整了顺序:例子包括RoPE(旋转位置嵌入)替代学习位置嵌入、GQA(分组查询注意力)减少KV缓存大小、门控线性激活替代ReLU、混合专家(MoE)层等。但注意力(通信)和MLP(计算)穿插在残差流上的核心结构被很好地保留。
训练。 代替每步一个文档,生产训练使用大批量(每步数百万标记)、梯度累积、混合精度(float16/bfloat16)和谨慎的超参数调优。训练一个前沿模型需要数千个GPU运行数月。
优化。 microgpt使用Adam和简单的线性学习率衰减,仅此而已。在规模上,优化成为一门独立的学科。模型以降低精度(bfloat16甚至fp8)在大型GPU集群上训练以求效率,这引入了自身的数值挑战。优化器设置(学习率、权重衰减、beta参数、预热计划、衰减计划)必须精确调优,正确的值取决于模型大小、批大小和数据集组成。缩放定律(如Chinchilla)指导如何在固定计算预算下在模型大小和训练标记数之间分配。在规模上将任何这些细节搞错可能浪费数百万美元的计算资源,因此团队在投入完整训练之前会运行大量较小规模的实验来预测正确设置。
后训练。 训练出来的基础模型(称为『预训练』模型)是一个文档补全器,不是聊天机器人。将其转变为ChatGPT发生在两个阶段。首先,SFT(监督微调):你只需将文档替换为策划的对话并继续训练。算法上没有任何改变。其次,RL(强化学习):模型生成响应,它们被评分(由人、另一个『裁判』模型或算法),模型从该反馈中学习。从根本上说,模型仍在文档上训练,但现在这些文档由来自模型本身的标记组成。
推理。 为数百万用户提供模型服务需要自己的工程堆栈:将请求批处理在一起、KV缓存管理和分页(vLLM等)、推测解码以提高速度、量化(以int8/int4运行而不是float16)以减少内存,以及将模型分布到多个GPU。从根本上说,我们仍在预测序列中的下一个标记,但花了大量工程使其更快。
所有这些都是重要的工程和研究贡献,但如果你理解microgpt,你就理解了算法本质。
常见问题¶
模型『理解』任何东西吗? 这是一个哲学问题,但从机制上讲:没有魔法在发生。模型是一个大数学函数,将输入标记映射到下一个标记的概率分布。在训练期间,参数被调整以使正确的下一个标记更可能。这是否构成『理解』取决于你,但机制完全包含在上面的200行中。
它为什么能工作? 模型有数千个可调参数,优化器每一步将它们微调一点点以使损失下降。经过许多步,参数定居为捕获数据统计规律的值。对于名字,这意味着诸如:名字通常以辅音开头,『qu』倾向于一起出现,名字很少有三个连续辅音等。模型不学习显式规则,它学习一个恰好反映这些规则的概率分布。
这与ChatGPT有什么关系? ChatGPT是相同的核心循环(预测下一个标记、采样、重复)大规模扩展,加上后训练使其具有对话能力。当你与它聊天时,系统提示、你的消息和它的回复都只是序列中的标记。模型一次一个标记地补全文档,和microgpt补全名字完全一样。
『幻觉』是怎么回事? 模型通过从概率分布采样来生成标记。它没有真理的概念,它只知道给定训练数据,哪些序列在统计上是看似合理的。microgpt『幻觉』一个像『karia』这样的名字,与ChatGPT自信地陈述一个错误事实是相同的现象。两者都是看似合理的补全,恰好不是真实的。
为什么这么慢? microgpt在纯Python中一次处理一个标量。单个训练步骤需要几秒钟。相同的数学在GPU上并行处理数百万个标量,速度快几个数量级。
我能让它生成更好的名字吗? 可以。训练更长时间(增加num_steps),使模型更大(n_embd、n_layer、n_head),或使用更大的数据集。这些是在规模上同样重要的旋钮。
如果我更换数据集会怎样? 模型将学习数据中的任何模式。换成城市名、宝可梦名、英文单词或短诗的文件,模型将学习生成那些代替。代码的其余部分不需要改变。
量子计算语言生态:从底层汇编到高级框架的层次化解析¶
描述量子原生算法有非常特定的『语言』体系。根据使用场景的不同,这些语言主要分为两大类:理论与数学描述语言(用于论文、设计和交流)和 量子编程语言(用于实际编写、模拟和在硬件上执行)。
以下是具体的分类和代表性语言:
一、 理论与数学描述语言(学术界通用)¶
在论文、教科书和算法设计阶段,研究人员通常使用以下方式来『描述』量子算法:
1. 狄拉克符号 (Dirac Notation / Bra-Ket 记号)¶
- 定位:量子力学的『代数语言』。
- 作用:这是描述量子态(如 $|\psi\rangle$)、量子门(酉算符 $U$)和测量(如 $\langle\phi|U|\psi\rangle$)最严谨、最基础的数学语言。任何量子算法的理论推导都离不开它。
2. 量子回路图 (Quantum Circuit Diagrams)¶
- 定位:量子计算的『视觉语言』或『工程图纸』。
- 作用:正如上一问所述,量子回路模型是主流。研究人员通过绘制水平线(代表量子比特)和在其上的方框/符号(代表量子门,如 H, CNOT, Rz)来直观地描述算法的执行流程。它是学术界交流量子算法事实上的标准。
3. 张量网络 (Tensor Networks)¶
- 定位:描述复杂多体量子态的『图形化代数语言』。
- 作用:对于包含大量纠缠的量子算法(或经典模拟量子算法时),传统的狄拉克符号会变得极其冗长。张量网络(如 PEPS, MPS)通过图形化的节点和连线来表示高阶张量的缩并,是描述变分量子算法(VQE)和量子机器学习底层数学结构的强大工具。
二、 量子编程语言 (Quantum Programming Languages, QPLs)¶
如果要将算法真正交给计算机(模拟器或真实量子芯片)去执行,就需要使用专门的量子编程语言。目前的量子编程生态主要分为三个层次:
1. 量子汇编语言 (Quantum Assembly Languages)¶
这是最接近硬件的底层描述语言,类似于经典计算中的汇编语言。它们直接描述量子门序列和测量操作。
- OpenQASM (Open Quantum Assembly Language):由 IBM 主导开发,目前是行业事实上的标准。高级量子代码最终都会被编译器翻译成 OpenQASM,然后发送给量子硬件执行。它专门用于描述基于量子回路的算法。
- Quil (Quantum Instruction Language):由 Rigetti 公司提出,与 OpenQASM 类似,但特别强调了对经典-量子混合控制流(如根据测量结果实时反馈调整后续量子门)的支持。
2. 高级量子编程语言 / 框架 (High-Level QPLs)¶
这是开发者实际编写算法时使用的语言。由于量子计算目前高度依赖经典计算进行优化和控制(即混合量子-经典架构),绝大多数高级量子语言都是作为经典语言的扩展或库存在的。
- Qiskit (IBM):基于 Python。目前生态最庞大,提供了从高级算法构建(如
QuantumCircuit类)到底层脉冲控制的完整工具链。 - Cirq (Google):基于 Python。设计初衷是为了更精细地控制 NISQ(含噪声中等规模量子)设备,对量子门的时间和物理拓扑映射描述得非常细致。
- PennyLane (Xanadu):基于 Python。特别推荐用于量子机器学习和变分算法(如 VQE, QAOA)。它首创了『量子微分编程』(Quantum Differentiable Programming),可以像训练经典神经网络一样,使用 PyTorch 或 TensorFlow 自动计算量子回路参数的梯度。
- Q# (Microsoft):这是一种真正的领域特定语言 (DSL),语法类似 C# 或 F#。它不是 Python 的库,而是独立的语言。它内置了对量子特性(如自动内存管理、量子态准备、复杂的量子控制流)的原生支持,通常与 Python 宿主程序结合使用。
3. 函数式与形式化验证语言 (学术与前沿探索)¶
用于从数学上严格证明量子算法的正确性或进行高级抽象。
- Quipper:一种基于 Haskell 的函数式量子编程语言,擅长描述和控制极其庞大的量子回路(例如包含数十亿个门的 Shor 算法)。
- Silq:由苏黎世联邦理工学院 (ETH Zurich) 开发,它的特点是拥有自动量子态释放 (Automatic Uncomputation) 机制,能自动处理量子计算中棘手的『垃圾态』清理问题,语法非常接近高级经典语言。
三、 为什么需要『特定』的量子语言?¶
经典编程语言(如 C++ 或 Java)无法直接描述量子算法,因为量子力学有一些反直觉的特性,需要语言层面提供特殊支持:
- 不可克隆定理 (No-Cloning Theorem):在经典语言中,
a = b会复制变量。但在量子语言中,复制未知的量子态是物理禁止的。量子语言必须在类型系统上禁止这种操作。 - 测量的破坏性与概率性:测量量子态会使其坍缩,且结果是概率性的。量子语言需要专门的语法来处理这种『概率分支』和『经典-量子混合控制流』(例如:测量 qubit 0,如果结果是 1,则对 qubit 1 施加 X 门)。
- 纠缠的非局部性:语言需要能够优雅地表达多个量子比特之间的全局关联,而不是简单的独立变量操作。
- 可逆性 (Reversibility):许多量子操作必须是可逆的(酉变换),量子语言(如 Q# 或 Silq)通常提供专门的机制来确保或自动推导操作的可逆性。
总结与建议¶
- 如果是在阅读论文或设计算法逻辑,需要掌握的是 狄拉克符号 和 量子回路图 的读法。
- 如果是想动手实现量子原生算法(尤其是结合机器学习的混合算法,如之前提到的 QPSAN/QLAM),Python + PennyLane 或 Python + Qiskit 是目前最主流、最高效的选择。
- 如果是关注算法如何最终在硬件上运行,了解 OpenQASM 的基本语法会非常有帮助,它是连接软件与硬件的『世界语』。
from IPython.display import YouTubeVideo
YouTubeVideo('RQWpF2Gb-gU', width=600, height=400)
from IPython.display import YouTubeVideo
YouTubeVideo('Dlsa9EBKDGI', width=600, height=400)
- QulacsExamples / GroverSearch.ipynb:使用 Qulacs 实现 Grover 搜索算法,演示量子搜索问题的基本原理与运行过程。
- QulacsExamples / 5.2c1_application_of_QCL_to_classification.ipynb:使用量子回路学习(QCL)进行分类任务,演示量子分类模型的构建与应用。
- AWSLabs / quantum-computing-exploration-for-drug-discovery-on-aws:在 AWS 上探索量子计算在药物发现中的应用,包括量子算法、分子模拟及相关工作流。
- Kasture, Kyriienko, Elfving (2022/2023),Protocols for classically training quantum generative models on probability distributions(Phys. Rev. A 108, 042406) ——用 IQP 及其扩展电路做『经典训练、量子部署』的 QGM 。
- Kurkin, Chabaud, Kolarovszki, Bakó, Zimborás, Dunjko (2026),Universality of Classically Trainable, Quantum-Deployed Boson-Sampling Generative Models(arXiv:2603.11014) ——把同样的范式搬到玻色采样(线性光学)上。
- Shen, Kurkin, Pérez-Salinas, Shishenina, Dunjko, Wang (2024/2025),Variational Quantum Generative Modeling by Sampling Expectation Values of Tunable Observables(OT-EVS,npj Quantum Information) ——用可调观测量的期望值采样连续分布。
- Surov et al. (2021),Quantum semantics of text perception(Sci. Rep. 11, 4193) ——把词语的主观语义感知建模为量子比特态,两个词之间的纠缠度量语义关联。
Claude Code - Dynamic Workflows 相关提示词的范例:
- 用 deepseek-v4-flash 模型来拓展研究广度。用 deepseek-v4-pro 模型来挖掘研究深度。
- 执行代码审计:用 deepseek-v4-flash 模型按文件并行寻找代码缺陷,以 file:line 形式结构化输出;对于每条发现的代码缺陷,再用三个 deepseek-v4-flash 模型的智能体进行对抗验证;对于通过验证的代码缺陷,最后用 deepseek-v4-pro 模型进行深入验证。
To Claude Code:
/deep-research 用 deepseek-v4-flash 模型来拓展研究广度。用 deepseek-v4-pro 模型来挖掘研究深度。通过对 Quixer 的调研,用中文制作一份针对只具有 GPT 架构和量子计算基础概念的人士的教程,一步一步引导这类人士理解、实现并运行 Quixer 。不限制教程的篇幅。确保广度、深度并达成教学目标。
Best regards,
Andrew
关于本教程¶
目标受众¶
本教程面向具备以下背景的读者:
- 熟悉 GPT / Transformer 架构:理解自注意力(Self-Attention)、多头注意力(Multi-Head Attention)、前馈网络(Feed-Forward Network)、层归一化(Layer Normalization)、位置编码(Positional Encoding)、残差连接(Residual Connection)等核心组件。
- 具备量子计算基础概念:理解量子比特(Qubit)、叠加态(Superposition)、酉变换(Unitary Transformation)、测量(Measurement)、量子门(Pauli 门、旋转门等)的基本含义。
你将学到什么¶
完成本教程后,你将能够:
- 解释 Quixer 与经典 GPT / Transformer 的本质区别
- 理解线性酉组合(LCU)和量子奇异值变换(QSVT)的数学原理
- 掌握 Quixer 的完整架构:从词嵌入到最终输出的每一步
- 阅读并理解 Quixer 的 PyTorch 经典模拟源码
- 在自己的机器上搭建环境、训练并运行 Quixer
- 评估 Quixer 的当前局限性和未来发展方向
教程结构¶
本教程采用『原理与真实代码穿插』的组织方式:第一至七章每讲完一个概念(Ansatz 14、LCU、QSVT、完整架构等)就立刻插入真实执行 quixer_model.py 源码的代码块。
| 章节 | 内容 | 建议时间 |
|---|---|---|
| 第一章 | GPT / Transformer 架构回顾 | 20 分钟 |
| 第二章 | 量子计算基础回顾 | 30 分钟 |
| 第三章 | Quixer 宏观概览 | 15 分钟 |
| 第四章 | 块编码(Block Encoding) | 30 分钟 |
| 第五章 | 线性酉组合(LCU) | 45 分钟 |
| 第六章 | 量子奇异值变换(QSVT) | 45 分钟 |
| 第七章 | Quixer 完整架构详解 | 40 分钟 |
| 第八章 | 源码结构与训练流程 | 30 分钟 |
| 第九章 | 环境搭建与模型训练 | 40 分钟 |
| 第十章 | 实验结果与性能分析 | 20 分钟 |
| 第十一章 | 模型推理与预测 | 20 分钟 |
| 第十二章 | 端到端测试与评价 | 20 分钟 |
| 第十三章 | 局限性与开放问题 | 20 分钟 |
| 第十四章 | 后续研究方向与生态 | 20 分钟 |
运行本教程前的准备¶
本教程后续所有代码 cell 都会真实调用 QuixerRepo 中的源码并展示真实输出,而不仅仅是复述代码。为此需要先让 Python 能 import quixer。这里先做最小化准备;详细的环境搭建过程、pip install -e . 的一个真实踩坑记录见第九章。
import os
import sys
from pathlib import Path
QUIXER_REPO = Path("/Users/saintway/Downloads/Quixer/QuixerRepo")
if str(QUIXER_REPO) not in sys.path:
sys.path.insert(0, str(QUIXER_REPO))
# 切换工作目录到 QuixerRepo,这样后面 train_cycle() 写入的 "./trained_models"
# checkpoint 会落在 QuixerRepo/trained_models/ 里,和仓库原有的 4 个 checkpoint 放在一起
os.chdir(QUIXER_REPO)
import torch
import torchquantum as tq
import quixer.quixer_model as qm
import quixer.baseline_models as bm
import quixer.setup_training as st
print("Python:", sys.version.split()[0])
print("当前工作目录:", os.getcwd())
print("torch:", torch.__version__)
print("quixer_model 源文件:", qm.__file__)
print("baseline_models 源文件:", bm.__file__)
print("setup_training 源文件:", st.__file__)
第一章:GPT/Transformer 架构回顾¶
目标:建立一个清晰的经典 Transformer 心智模型,为理解 Quixer 的『量子替代方案』铺路。
1.1 核心处理管线¶
GPT 系列模型的本质是一个下一令牌预测器(Next-Token Predictor)。给定一段序列 $[w_1, w_2, ..., w_n]$,模型预测第 n+1 个令牌的概率分布。其核心处理管线如下:
---
config:
theme: base
themeVariables:
primaryColor: '#FAFAFC'
primaryTextColor: '#222222'
primaryBorderColor: '#28749A'
lineColor: '#28749A'
secondaryColor: '#E9E9E9'
tertiaryColor: '#FFFFFF'
---
flowchart TD
INPUT["输入令牌
"]
INPUT --> EMB["词嵌入 (Embedding)
→ 实数矩阵"]
EMB --> PE["位置编码 (Positional Encoding)
→ 注入序列位置信息"]
PE --> BLOCK
subgraph BLOCK["Transformer 块 (×N 层)"]
direction TB
ATTN["多头自注意力
(Multi-Head Self-Attention)
+ 残差连接 + 层归一化"]
FFN["前馈网络
(Feed-Forward Network)
+ 残差连接 + 层归一化"]
ATTN --> FFN
end
BLOCK --> PROJ["输出投影 (Output Projection)
→ logits"]
PROJ --> SM["Softmax
→ 下一令牌的概率分布"]
1.2 最关键的部分:点积自注意力¶
自注意力是 Transformer 的灵魂。其核心公式为:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V$$展开来解释:
线性投影:输入 $X \in \mathbb{R}^{n \times d}$ 通过三个可训练的权重矩阵 $W_Q$, $W_K$, $W_V$ 投影为查询(Query)、键(Key)、值(Value): $$Q = X W_Q, \quad K = X W_K, \quad V = X W_V$$
注意力分数:计算 Query 和 Key 之间的相似度(点积),得到
n × n的注意力矩阵: $$A = \frac{QK^T}{\sqrt{d_k}}$$Softmax 归一化:将每行的相似度转换为概率分布(和为 1): $$\text{softmax}(A)_{ij} = \frac{\exp(A_{ij})}{\sum_k \exp(A_{ik})}$$
加权聚合:用注意力权重对 Value 矩阵加权求和,实现令牌混合(Token Mixing): $$\text{Output} = \text{softmax}(A) \cdot V$$
1.3 自注意力的本质:令牌混合¶
从更高层次看,自注意力的本质是令牌混合(Token Mixing):
- 输入是
n个令牌的表示 - 每个令牌的输出是所有输入令牌表示的加权平均
- 权重由令牌之间的『相关性』(Query-Key 相似度)决定
- 这是内容相关的混合:混合权重依赖于输入内容本身
1.4 为什么量子计算可能提供更好的混合方式?¶
经典自注意力有两个著名的计算瓶颈:
- 二次复杂度:注意力矩阵是
n × n的,计算和存储复杂度都是O(n²),对长序列不友好 - 参数规模:$W_Q$, $W_K$, $W_V$, $W_O$ 等权重矩阵占据大量参数
量子计算提供了几种潜在的替代方案:
- 酉变换天然保持范数,可能实现更稳定的信息传播
- 量子叠加可以在指数大的希尔伯特空间中编码信息
- 量子干涉可以实现经典计算难以表达的函数
- 量子原语(如 QSVT)可以直接对矩阵施加多项式变换
关键思想:Quixer 不是对『点积自注意力』做量子化改造,而是用全新的量子令牌混合机制来替代整个自注意力模块。
第二章:量子计算基础回顾¶
目标:快速回顾理解 Quixer 所需的量子计算核心概念。
2.1 量子比特与态矢量¶
单量子比特的态可以写作:
$$|\psi\rangle = \alpha|0\rangle + \beta|1\rangle, \quad |\alpha|^2 + |\beta|^2 = 1, \quad \alpha, \beta \in \mathbb{C}$$其中:
- $|0\rangle = \begin{bmatrix} 1 \\ 0 \end{bmatrix}$,$|1\rangle = \begin{bmatrix} 0 \\ 1 \end{bmatrix}$
- $\alpha$ 和 $\beta$ 称为振幅(Amplitude)
- $|\alpha|^2$ 是测量得到 0 的概率
多量子比特系统的态是各量子比特态的张量积。例如,2 量子比特系统:
$$|\psi\rangle = \alpha_{00}|00\rangle + \alpha_{01}|01\rangle + \alpha_{10}|10\rangle + \alpha_{11}|11\rangle$$$q$ 个量子比特的态矢量在 $\mathbb{C}^{2^q}$ 空间中——维数随量子比特数指数增长。
2.2 酉变换与量子门¶
量子计算通过酉变换(Unitary Transformation)操作量子态。酉矩阵 $U$ 满足 $U^\dagger U = I$。酉变换的性质是可逆且保范数($\|U|\psi\rangle\| = \||\psi\rangle\|$)。
常用量子门:
| 门 | 矩阵 | 说明 |
|---|---|---|
| Pauli-X | $$\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}$$ | 量子版 NOT |
| Pauli-Y | $$\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}$$ | |
| Pauli-Z | $$\begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix}$$ | 相位翻转 |
| $$R_X(\theta)$$ | $$e^{-i\theta X} = \begin{bmatrix} \cos(\theta) & -i\sin(\theta) \\ -i\sin(\theta) & \cos(\theta) \end{bmatrix}$$ | 绕 X 轴旋转 |
| $$R_Y(\theta)$$ | $$e^{-i\theta Y} = \begin{bmatrix} \cos(\theta) & -\sin(\theta) \\ \sin(\theta) & \cos(\theta) \end{bmatrix}$$ | 绕 Y 轴旋转 |
| $$R_Z(\theta)$$ | $$e^{-i\theta Z} = \begin{bmatrix} e^{-i\theta} & 0 \\ 0 & e^{i\theta} \end{bmatrix}$$ | 绕 Z 轴旋转 |
| CNOT | $$\begin{bmatrix} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&0&1 \\ 0&0&1&0 \end{bmatrix}$$ | 受控非门 |
2.3 受控门¶
受控门 CU 的行为是:
- 当控制量子比特为 $|0\rangle$ 时,目标量子比特不变
- 当控制量子比特为 $|1\rangle$ 时,对目标量子比特施加 $U$
由于量子力学是线性的,当控制量子比特处于叠加态时,受控门产生纠缠:
$$CU(\alpha|0\rangle + \beta|1\rangle) \otimes |\psi\rangle = \alpha|0\rangle \otimes |\psi\rangle + \beta|1\rangle \otimes U|\psi\rangle$$2.4 测量与期望值¶
测量量子态 $|\psi\rangle$ 中某个可观测量(Observable)$O$($O$ 是厄米矩阵,$O^\dagger = O$):
$$\langle O \rangle_{|\psi\rangle} = \langle\psi|O|\psi\rangle \in \mathbb{R}$$例如,测量 Pauli-Z 的期望值给出量子比特偏向 $|0\rangle$ 还是 $|1\rangle$。
2.5 参数化量子回路(PQC)¶
参数化量子回路(Parameterized Quantum Circuit, PQC)是量子机器学习的核心构件。PQC 由一系列参数化的量子门(如 $R_X(\theta), R_Y(\phi), R_Z(\omega)$)组成,其中角度参数是可训练的。
PQC 结构示例(Ansatz 14):
---
config:
theme: base
themeVariables:
primaryColor: '#FAFAFC'
primaryTextColor: '#222222'
primaryBorderColor: '#28749A'
lineColor: '#28749A'
secondaryColor: '#E9E9E9'
tertiaryColor: '#FFFFFF'
---
flowchart LR
B1["第 1 块:RY
(前向环形)
RY(θ₀)
RY(θ₁)
⋮
RY(θ_{q−1})"]
B2["第 2 块:CRX
(反向环形)
CRX(φ₀)
CRX(φ₁)
⋮
CRX(φ_{q−1})"]
B3["第 3 块:RY
RY(θ′₀)
RY(θ′₁)
⋮
RY(θ′_{q−1})"]
B4["第 4 块:CRX
CRX(φ′₀)
CRX(φ′₁)
⋮
CRX(φ′_{q−1})"]
B1 --> B2 --> B3 --> B4
B1:::ry
B2:::crx
B3:::ry
B4:::crx
classDef ry fill:#f8fbfd,stroke:#28749A,stroke-width:1.5px,color:#222
classDef crx fill:#f5f5f5,stroke:#28749A,stroke-width:1.5px,color:#222
下面直接看 Ansatz 14 回路的具体构造:先是一份教学性复述(帮助理解回路结构的思路),然后真实执行 quixer_model.py 里的原始实现并对比两者的差异。
import itertools
def ansatz_14_torchquantum_specification(n_qubits, layers=1):
"""
构建 Ansatz 14 的回路规格。
每层包含 4 个块,共 4 * n_qubits 个参数:
- 块1: RY 旋转(所有量子比特)
- 块2: CRX 向前环形连接 (i → i+1)
- 块3: RY 旋转(所有量子比特)
- 块4: CRX 向后环形连接 (i → i-1)
"""
enc = []
counter = itertools.count(0)
for _ in range(layers):
# 块1: RY 旋转
for i in range(n_qubits):
enc.append({
"input_idx": [next(counter)],
"func": "ry",
"wires": [i]
})
# 块2: CRX 向前环形
for i in range(n_qubits - 1, -1, -1): # 反向遍历
enc.append({
"input_idx": [next(counter)],
"func": "crx",
"wires": [i, (i + 1) % n_qubits]
})
# 块3: RY 旋转
for i in range(n_qubits):
enc.append({
"input_idx": [next(counter)],
"func": "ry",
"wires": [i]
})
# 块4: CRX 向后环形
for last_qubit in range(n_qubits - 1, -1, -1):
i = (last_qubit - 1) % n_qubits # 控制量子比特
j = last_qubit
enc.append({
"input_idx": [next(counter)],
"func": "crx",
"wires": [i, j]
})
return enc
# 真实执行上面的复述版本,并与 quixer_model.py 中的真实实现对比
paraphrase_spec = ansatz_14_torchquantum_specification(n_qubits=3, layers=1)
real_spec = qm.ansatz_14_torchquantum_specification(n_qubits=3, layers=1)
print(f"复述版本生成 {len(paraphrase_spec)} 个门操作(预期 4*3*1={4*3*1})")
print("前 6 个门(两版本相同):")
for gate in paraphrase_spec[:6]:
print(" ", gate)
print("\n复述版本与真实源码是否逐项完全一致:", paraphrase_spec == real_spec)
diffs = [(i, p, r) for i, (p, r) in enumerate(zip(paraphrase_spec, real_spec)) if p != r]
print(f"不一致的门数量:{len(diffs)} / {len(real_spec)}")
for i, p, r in diffs:
print(f" 第 {i} 个门 — 复述版本: {p} | 真实源码: {r}")
真实执行发现的细节差异:复述版本与真实源码在『块4:CRX 向后环形』的具体控制/目标比特配对上并不完全相同(虽然两者的整体设计思路——正向环形 + 反向环形——是一致的)。这提醒我们:教学性复述适合理解算法思路,但涉及具体连接方式的细节仍应以真实源码为准。本教程从这里开始,所有实际调用都会 import quixer.quixer_model as qm 使用真实源码(如上面 real_spec 所示),而不是手写复述版本。
Ansatz 14 的环形连接设计解读:
n_qubits = 4 时一轮的 CRX 连接:
块2: CRX 向前环形 块4: CRX 向后环形
q0 ←── q3 q0 ──→ q3
↑ ↓
q1 ←── q0 q1 ──→ q0
↑ ↓
q2 ←── q1 q2 ──→ q1
↑ ↓
q3 ←── q2 q3 ──→ q2
这种双向环形连接确保信息可以在所有量子比特之间传播。
2.6 后选择(Postselection)¶
后选择是量子计算中的一个重要技术:只保留那些某个辅助测量结果为特定值的执行分支,丢弃其他分支。
在回路图中,后选择通常表示为控制量子比特末端的 $\langle 0|$ 标记:
|0⟩ ──[U_PREP]──●──[U_PREP†]── ⟨0|
│
|ψ⟩ ───────────[U_SEL]─────────
这意味着:只有当控制量子比特的最终测量结果是 $|0\rangle$ 时,我们才保留这次执行的结果。后选择使得我们可以在酉回路框架中实现非酉操作(如矩阵的非酉线性组合)。
2.7 关键概念:块编码(Block Encoding)¶
块编码是理解整个 Quixer 架构的最重要的前置概念。我们将其单独放在第四章深入讲解。这里先给出直觉:
块编码是一种用更大的酉矩阵来『编码』一个任意(可能非酉)矩阵的技术。具体来说,如果对某个矩阵 $M$ 存在酉矩阵 $U_M$ 使得 $M$ 是 $U_M$ 的左上角子矩阵,即 $M = (\langle 0| \otimes I) U_M (|0\rangle \otimes I)$,我们就说 $U_M$ 是 $M$ 的一个块编码。
第三章:Quixer 宏观概览¶
目标:在深入技术细节之前,先建立对 Quixer 的全局理解。
3.1 一句话定义¶
Quixer(Quantum Transformer)是 Quantinuum 于 2024 年 6 月发布的量子 Transformer 模型。其核心创新在于:用两种量子计算原语——线性酉组合(LCU, Linear Combination of Unitaries)和量子奇异值变换(QSVT, Quantum Singular Value Transformation)——完全替代了经典 Transformer 的点积自注意力机制。
3.2 Quixer 与 GPT 的本质区别¶
| 维度 | GPT / 经典 Transformer | Quixer |
|---|---|---|
| 令牌混合方式 | $\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$ 点积注意力 | LCU:酉变换的加权叠加 |
| 非线性来源 | ReLU / GELU 前馈网络 | QSVT:矩阵的多项式变换 |
| 表示空间 | $\mathbb{R}^d$(实向量空间) | $\mathbb{C}^{2^q}$(希尔伯特空间) |
| 参数形式 | 实矩阵 $W_Q$, $W_K$, $W_V$ | 复数权重 + 量子门旋转角度 |
| 混合机制 | 内容相关($QK^\top$ 相似度) | 参数化酉回路(结构先验) |
3.3 Quixer 的完整处理管线¶
---
config:
theme: base
themeVariables:
primaryColor: '#FAFAFC'
primaryTextColor: '#222222'
primaryBorderColor: '#28749A'
lineColor: '#28749A'
secondaryColor: '#E9E9E9'
tertiaryColor: '#FFFFFF'
---
flowchart TD
INPUT["输入令牌
(窗口大小 = )"]
S1["步骤 1:经典词嵌入
"]
S2["步骤 2:角度参数化
(线性变换)
将这些角度作为 PQC 的旋转门参数"]
S3["步骤 3:酉令牌嵌入
(Unitary Token Embedding)
每个令牌获得一个酉矩阵表示
经典模拟中:量子回路作用于"]
S4["步骤 4:LCU 令牌混合
其中
(可训练的复数权重)
满足"]
S5["步骤 5:QSVT 非线性多项式变换
多项式系数是可训练的"]
S6["步骤 6:量子前馈网络
(Quantum Feed-Forward)
再次应用 PQC
共享参数的单层 Ansatz 14"]
S7["步骤 7:测量
在所有量子比特上测量
Pauli-X、Y、Z 的期望值
→ 个实数测量值"]
S8["步骤 8:经典输出投影
两层 MLP:Linear → ReLU → Linear"]
OUTPUT["输出 logits
↓ Softmax ↓
下一令牌预测"]
INPUT --> S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> OUTPUT
classDef input fill:#FFFFFF,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef classical fill:#FAFAFC,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef quantum fill:#E9E9E9,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef output fill:#FFFFFF,stroke:#28749A,stroke-width:1.5px,color:#222222
class INPUT,OUTPUT input
class S1,S2,S8 classical
class S3,S4,S5,S6,S7 quantum
3.4 为什么要这样设计?¶
Quixer 的设计哲学(引自原论文第 2.1 节):
"Our focus in this work is not to quantise the dot product self-attention, but instead propose a novel form of token mixing built from quantum primitives."
翻译:我们不试图用量子回路逐模块地『翻译』经典自注意力,而是从量子计算的基本原语出发,构建一种全新的令牌混合范式。
这意味着 Quixer 的思路是量子原生(Quantum-Native)的:从量子计算能高效做什么出发,而不是从经典 Transformer 有什么出发。
3.5 重要声明:经典模拟¶
Quixer 的官方 GitHub 仓库(github.com/Quantinuum/Quixer)提供的仅仅是经典模拟实现——所有量子操作都用 PyTorch 张量在 CPU/CUDA 上模拟,没有真正的量子硬件后端。这就像在普通计算机上运行量子回路的仿真器。截至 2026 年 5 月,虽然 Quantinuum 宣称已将 Quixer 部署到其 H 系列量子硬件上,但该声明缺乏独立的第三方验证。
第四章:块编码(Block Encoding)¶
目标:深入理解块编码——这是 LCU 和 QSVT 的基础。
4.1 动机:为什么需要块编码?¶
量子计算的基本操作是酉变换。但机器学习中涉及的许多矩阵操作——如线性组合、非线性激活——本质上是非酉的。
块编码(Block Encoding)是一个巧妙的桥接技术:它允许我们用酉矩阵的左上角子矩阵来表示任意(可能非酉的)矩阵。
4.2 形式化定义¶
给定一个矩阵 $M$(不一定方阵,不一定酉),如果存在酉矩阵 $U$ 使得:
$$M = (\langle 0^a| \otimes I) \; U \; (|0^a\rangle \otimes I)$$其中:
- $a$ 是辅助量子比特的数量
- $|0^a\rangle$ 是 $a$ 个辅助量子比特全部处于 $|0\rangle$ 的状态
- 等式右端表示:将 $U$ 作用于 $|0^a\rangle \otimes |\psi\rangle$,然后在辅助量子比特上后选择 $|0^a\rangle$
则称 $U$ 是 $M$ 的一个块编码,$M$ 是 $U$ 的左上角子矩阵。
4.3 直观理解¶
想象 $U$ 是一个分块矩阵:
$$U = \begin{bmatrix} M & \cdot \\ \cdot & \cdot \end{bmatrix}$$当我们:
- 把辅助量子比特初始化为 $|0^a\rangle$
- 把数据量子比特初始化为 $|\psi\rangle$
- 对整个系统施加 $U$
- 测量辅助量子比特,只保留结果为 $|0^a\rangle$ 的分支
那么数据量子比特经历的有效变换正好是 $M$:
$$|\psi\rangle \xrightarrow{\text{block-encoded } U} M|\psi\rangle \quad (\text{归一化前})$$4.4 块编码的经典类比¶
这个思想并不是量子力学的专利。在经典计算中也有类似的做法:
经典类比 1(嵌入矩阵):如果你想用正交矩阵表示非正交的线性变换,可以将其嵌入一个更高维的正交矩阵中。
经典类比 2(条件执行):就像程序中的条件分支——
if flag == 0: result = M @ vector # 这种『如果前置条件满足就执行 M』的逻辑
块编码用叠加态的线性性质将这种『条件执行』延拓为对所有分支同时运算。
经典类比 3(分块矩阵):就像你可以在一个更大的矩阵中『隐藏』一个小的子矩阵,块编码在一个更大的酉矩阵中『隐藏』了非酉的 $M$,而量子力学的线性性和后选择机制将这种『隐藏』变成了可执行的回路。
4.5 块编码的关键性质¶
- 谱性质:如果 $M$ 的奇异值在 $[0, 1]$ 内,块编码存在且可用标准技术构造
- 多项式变换:如果我们能对块编码 $U_M$ 做某些操作,就能对整个 $M$ 做相应的矩阵多项式变换——这就是 QSVT 的核心思想
- 线性组合:如果我们能构造一系列矩阵的块编码,就能构造它们线性组合的块编码——这就是 LCU 的核心思想
第五章:线性酉组合(LCU, Linear Combination of Unitaries)¶
目标:掌握 LCU 的完整数学原理和回路实现。
5.1 回到经典 Transformer 的令牌混合¶
在经典 Transformer 中,每个令牌的输出是所有令牌 Value 的加权平均:
$$\text{Output}_i = \sum_{j=1}^n \alpha_{ij} V_j, \quad \sum_j \alpha_{ij} = 1$$权重 $\alpha_{ij}$ 是 softmax 归一化的注意力分数。
5.2 LCU 的量子类比¶
Quixer 用酉变换的加权叠加来类比这种混合:
$$M = \sum_{j=0}^{n-1} b_j \, U_j$$其中:
- $U_j \in \mathbb{C}^{2^q \times 2^q}$ 是第 $j$ 个令牌对应的酉矩阵(由参数化量子回路生成)
- $b_j \in \mathbb{C}$ 是可训练的复数权重
- $n$ 是上下文窗口大小
- 满足归一化条件:$\sum_j |b_j| = 1$
直觉类比:经典注意力是 $\sum \alpha_{ij} V_j$(向量的加权平均);LCU 令牌混合是 $\sum b_j U_j$(酉矩阵的复数加权线性组合)。$V_j$ 是向量,$U_j$ 是矩阵(更强大的表示)。
5.3 SELECT-PREPARE 回路:从数学到回路¶
LCU 的关键问题是:如何在量子回路中实现这种『酉变换的非酉组合』? 答案是通过 SELECT-PREPARE 回路架构。
准备阶段(PREPARE)¶
首先,构造一个酉变换 $U_{\text{PREP}}$ 作用于控制寄存器(大小为 $\lceil \log_2 n \rceil$ 个量子比特),将 $|0\ldots 0\rangle$ 准备为所需的系数叠加态:
$$U_{\text{PREP}} |0\rangle = |a\rangle = \sum_{j=0}^{n-1} a_j |j\rangle$$其中 $a_j$ 是复数振幅,$|j\rangle$ 是控制寄存器的计算基态。
选择阶段(SELECT)¶
然后,构造一个受控酉变换 $U_{\text{SEL}}$:
$$U_{\text{SEL}} = \sum_{j=0}^{n-1} |j\rangle\langle j| \otimes U_j$$解读:$U_{\text{SEL}}$ 的每一『块』对应控制寄存器的一个计算基态:
- 当控制寄存器为 $|0\rangle$ 时,对数据寄存器施加 $U_0$
- 当控制寄存器为 $|1\rangle$ 时,对数据寄存器施加 $U_1$
- ...
- 当控制寄存器为 $|n-1\rangle$ 时,对数据寄存器施加 $U_{n-1}$
完整的 LCU 回路 $U_M$¶
组合这两个操作:
$$U_M = (U_{\text{PREP}}^\dagger \otimes I) \; U_{\text{SEL}} \; (U_{\text{PREP}} \otimes I)$$然后在控制寄存器上后选择 $|0\rangle$:
$$M = (\langle 0| \otimes I) \; U_M \; (|0\rangle \otimes I)$$数学上可以证明(见论文附录 A.2 的引理 1):
$$M = (\langle 0| \otimes I) \; U_M \; (|0\rangle \otimes I) = \sum_{j=0}^{n-1} |a_j|^2 \, U_j$$5.4 为什么要设计成这种形式:直观的数学推导¶
让我们一步一步追踪态在回路中的演化,理解为什么最终结果正好是 $\sum |a_j|^2 U_j$。
初始态:控制寄存器在 $|0\rangle$,数据寄存器在 $|\psi\rangle$: $$|\Phi_0\rangle = |0\rangle \otimes |\psi\rangle$$
步骤 1:应用 $U_{\text{PREP}} \otimes I$。控制寄存器变为叠加态 $|a\rangle = \sum a_j |j\rangle$: $$|\Phi_1\rangle = \left(\sum_{j=0}^{n-1} a_j |j\rangle\right) \otimes |\psi\rangle$$
步骤 2:应用 $U_{\text{SEL}}$。每个 $|j\rangle$ 分支上数据寄存器被施加对应的 $U_j$: $$|\Phi_2\rangle = \sum_{j=0}^{n-1} a_j |j\rangle \otimes U_j |\psi\rangle$$
步骤 3:应用 $U_{\text{PREP}}^\dagger \otimes I$。这『解纠缠』控制寄存器。考虑后选择 $|0\rangle$: $$|\Phi_3\rangle = |0\rangle \otimes \left(\sum_{j=0}^{n-1} a_j \langle 0|U_{\text{PREP}}^\dagger|j\rangle \cdot U_j |\psi\rangle\right)$$
由于 $U_{\text{PREP}}^\dagger |j\rangle$ 投影到 $\langle 0|$ 给出 $a_j^*$(因为 $U_{\text{PREP}}|0\rangle = \sum a_j|j\rangle$),所以: $$\langle 0|U_{\text{PREP}}^\dagger|j\rangle = a_j^*$$
因此: $$|\Phi_3\rangle = |0\rangle \otimes \sum_{j=0}^{n-1} |a_j|^2 \, U_j |\psi\rangle$$
后选择成功概率 $p_{\text{success}} = \|\sum_{j} |a_j|^2 U_j |\psi\rangle\|^2$。
这完美地实现了:酉矩阵的线性组合,系数为 $|a_j|^2$(非负实数)。
5.5 复数系数的引入¶
但实数系数 $|a_j|^2$ 的表达力有限!为了实现复数系数 $b_j \in \mathbb{C}$,Quixer 在每个令牌的酉回路前加入了一个相位门:
$$U_j' = e^{i\gamma_j} \cdot U_j$$这样,最终的有效混合变为:
$$M = \sum_{j=0}^{n-1} \underbrace{e^{i\gamma_j} |a_j|^2}_{b_j \in \mathbb{C}} \, U_j$$其中 $b_j = e^{i\gamma_j} |a_j|^2$ 是可训练的复数系数,满足 $\sum |b_j| = 1$。
5.6 后选择概率与资源开销¶
虽然 LCU 回路使用了辅助量子比特和测量的组合实现了看似非酉的操作,但这并非没有代价:
- 后选择成功率 $p_{\text{success}} = \|\sum b_j U_j|\psi\rangle\|^2$ 并不总是 1
- 在实际量子硬件上,你需要执行 $O(1/p_{\text{success}})$ 次测量才能获得一个有效样本
- 当系数 $|b_j|$ 分布极不均匀(某个系数远大于其他系数)时,成功率可能接近 1;当系数分布均匀时,成功率可能较低
5.7 LCU 与经典注意力的深层对比¶
| 属性 | 经典注意力 | LCU 令牌混合 |
|---|---|---|
| 混合对象 | Value 向量 $V_j$ | 酉矩阵 $U_j$ |
| 混合权重 | $$\operatorname{softmax}(QK^\top/\sqrt{d_k})$$(内容相关) | $b_j$(可训练参数) |
| 权重空间 | 实数、正数、和为 1 | 复数、L1 范数为 1 |
| 复杂度 | $O(n^2d)$(经典计算) | $$O(n \cdot 4^q)$$(经典模拟)或 $$O(n \cdot \mathrm{poly}(q))$$(量子实现) |
| 信息传播 | 点积交互 | 酉变换的干涉效应 |
5.8 一个具体的数值例子¶
假设窗口大小 $n = 3$,意味着我们有 3 个令牌,每个令牌有一个对应的酉矩阵 $U_0, U_1, U_2$。
# 经典注意力(抽象表示)
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
attn = softmax(Q @ K.T / sqrt(d_k))
output = attn @ V # 每个输出是 V 的加权平均
# Quixer 的 LCU(抽象表示)
U = [U_0, U_1, U_2] # 每个令牌的酉矩阵
b = [0.5+0.0j, 0.3+0.1j, 0.1+0.0j] # 可训练复数权重
M = b[0]*U[0] + b[1]*U[1] + b[2]*U[2] # 酉矩阵的线性组合
output_state = M @ |ψ_in⟩ # 混合算子作用于量子态
注意关键区别:
- 经典注意力中,softmax 的归一化是对『行』施加的(每个查询的输出权重之和为 1)
- LCU 中,归一化是对整个系数向量施加的($\sum|b_j|=1$)
- 这意味着 LCU 对所有令牌使用同一组全局混合权重,而不是每个令牌有独立的注意力分布——这是一种全局令牌混合而非内容相关的自适应混合
5.9 LCU 的经典模拟:真实代码¶
下面直接执行 quixer_model.py 里的 apply_linear_combination_of_unitaries(先看教学性复述,再执行真实源码并对比)。这里用到的酉矩阵 $U_j$ 就是上面 2.5 节里已经跑过的 Ansatz 14 回路。
def apply_linear_combination_of_unitaries(
initial_states, # [batch_size, 2**n_qubits]
pqc_parameters, # [batch_size, n_tokens, n_pqc_params]
parameterized_quantum_circuit, # TorchQuantum GeneralEncoder
torchquantum_device,
n_qubits,
lcu_coefficients, # [batch_size, n_tokens], 复数
):
"""
经典模拟 LCU: M|ψ⟩ = (Σ b_j U_j) |ψ⟩
关键思路:
1. 将初始态复制 n_tokens 份
2. 每份分别施加对应令牌的 PQC
3. 用复数系数加权求和
"""
batch_size, n_tokens, _ = pqc_parameters.shape
# 复制初始态:每个 batch 元素的态复制 n_tokens 次
states = initial_states.repeat(1, n_tokens).reshape(-1, 2**n_qubits)
# states: [batch_size * n_tokens, 2**n_qubits]
# 展平 PQC 参数
flat_params = pqc_parameters.reshape(-1, pqc_parameters.shape[-1])
# flat_params: [batch_size * n_tokens, n_pqc_params]
# 将态加载到量子设备中
torchquantum_device.reset_batch_size(batch_size * n_tokens)
# 对每份态施加对应令牌的 PQC
parameterized_quantum_circuit(
torchquantum_device,
flat_params.float()
)
# 提取演化后的态
evolved = torchquantum_device.get_states_1d()
evolved = evolved.reshape(batch_size, n_tokens, 2**n_qubits)
# 加权求和: Σ b_j |ψ_j⟩
mixed = torch.einsum("bti,bt->bi", evolved, lcu_coefficients)
return mixed # [batch_size, 2**n_qubits]
关于上面的复述版本:它在 torchquantum_device.reset_batch_size(...) 这一步使用了一个真实 torchquantum.QuantumDevice API 中并不存在的方法(真实源码用的是 set_states(...)),如果直接调用会报 AttributeError。这是复述用来突出算法思路时引入的简化偏差。下面直接执行真实源码中的 apply_linear_combination_of_unitaries,用一组很小的样例输入(n_qubits=3、window=4、batch=2)观察真实运行结果,包括函数内部已有的 shape 调试 print。
# 真实执行 quixer_model.py 中的 apply_linear_combination_of_unitaries
DEMO_N_QUBITS = 3
DEMO_WINDOW = 4
DEMO_BATCH = 2
DEMO_ANSATZ_LAYERS = 1
demo_pqc = tq.GeneralEncoder(
qm.ansatz_14_torchquantum_specification(DEMO_N_QUBITS, DEMO_ANSATZ_LAYERS)
)
demo_qdev = tq.QuantumDevice(n_wires=DEMO_N_QUBITS, bsz=DEMO_BATCH * DEMO_WINDOW)
# 初始态 |000>,对每个 batch 元素都一样
demo_initial_states = torch.zeros(DEMO_BATCH, 2**DEMO_N_QUBITS, dtype=torch.complex64)
demo_initial_states[:, 0] = 1.0
# 随机的 PQC 角度参数与 LCU 系数(真实使用中来自 embedding_to_angles / 可训练参数)
demo_pqc_parameters = torch.rand(DEMO_BATCH, DEMO_WINDOW, 4 * DEMO_N_QUBITS * DEMO_ANSATZ_LAYERS)
demo_lcu_coefficients = torch.rand(DEMO_BATCH, DEMO_WINDOW, dtype=torch.complex64)
lcu_output = qm.apply_linear_combination_of_unitaries(
demo_initial_states,
demo_pqc_parameters,
demo_pqc,
demo_qdev,
DEMO_N_QUBITS,
demo_lcu_coefficients,
)
print("\napply_linear_combination_of_unitaries 最终返回 shape:", lcu_output.shape, lcu_output.dtype)
print("每个 batch 元素的态范数(LCU 后一般不再是 1):", torch.linalg.vector_norm(lcu_output, dim=-1))
设计洞察:经典模拟中并没有真正构造 SELECT-PREPARE 回路。代码采用了一种更高效的方式——直接枚举每个酉矩阵 $U_j$,分别计算 $U_j \lvert \psi \rangle$,然后做复数加权的线性求和。这是因为在经典计算机上,显式计算每个 $2^q$ 维态矢量并求和比模拟一个完整的 LCU 回路(需要额外的 $\log_2 n$ 个控制比特)开销更小。在真实量子计算机上,你才需要 SELECT-PREPARE 来利用量子并行性。
第六章:量子奇异值变换(QSVT, Quantum Singular Value Transformation)¶
目标:掌握 QSVT 的原理,理解它如何为量子回路提供非线性。
6.1 动机:量子回路缺少非线性¶
在经典深度学习中,网络成功的一个关键因素是非线性激活函数(ReLU、GELU、tanh 等)。没有非线性,多层网络等价于单层线性变换。
但量子力学本质上是线性的——薛定谔方程是线性方程,量子门是酉(线性)变换。那么如何在量子回路中引入非线性?
QSVT 提供了一种方法:对块编码的矩阵施加多项式变换。多项式是非线性的(在矩阵意义上:$M^2 \neq cM$),因此 QSVT 为量子计算提供了一种『矩阵级』的非线性机制。
6.2 QSVT 的数学定义¶
给定一个矩阵 $M$ 和它的块编码酉矩阵 $U_M$,QSVT 允许我们构造一个回路来实现 $M$ 的任意奇偶性匹配的多项式:
$$P_c(M) = c_d M^d + c_{d-1} M^{d-1} + \cdots + c_1 M + c_0 I$$条件是:
- 有界性:$|P_c(x)| \leq 1$ 对所有 $x \in [-1, 1]$ 成立(多项式在 $[-1,1]$ 上有界)
- 奇偶性约束:$\text{parity}(P_c) = d \bmod 2$(多项式的奇偶性必须与其次数一致)
6.3 QSVT 回路结构¶
QSVT 的核心思想是:交替应用 $U_M$、$U_M^\dagger$ 和投影算子 $\Pi_\phi$。
奇数次多项式($d$ 为奇数)¶
$$\Pi_{\phi_1} U_M \left[ \prod_{k=1}^{(d-1)/2} \Pi_{\phi_{2k}} U_M^\dagger \Pi_{\phi_{2k+1}} U_M \right]$$偶数次多项式($d$ 为偶数)¶
$$\prod_{k=1}^{d/2} \Pi_{\phi_{2k-1}} U_M^\dagger \Pi_{\phi_{2k}} U_M$$其中:
- $\Pi_\phi = e^{i\phi(2\Pi - I)} = e^{i\phi Z}$ 是辅助量子比特上的相位旋转
- $\{\phi_1, \phi_2, \ldots, \phi_d\}$ 是与多项式系数 $\{c_0, c_1, \ldots, c_d\}$ 一一对应的相位角
- $U_M$ 块编码 $M$,$U_M^\dagger$ 块编码 $M^\dagger$
6.4 相位角度 $\{\phi_k\}$ 的计算¶
从多项式系数 $\{c_k\}$ 到 QSVT 相位角度 $\{\phi_k\}$ 的映射是非平凡的——需要通过数值算法计算。这个算法在量子计算领域是标准化的,常用工具(如 QSPPACK、pyqsp)提供实现。
关键理解:在 Quixer 的经典模拟中,由于模拟的是『QSVT 的数学效果』而非『QSVT 回路』,因此不需要计算相位角度——直接用多项式系数 $c_k$ 做代数计算即可(详见本章末尾的真实代码)。
6.5 奇偶性约束的解除¶
QSVT 的核心约束是:多项式的奇偶性必须等于其次数的奇偶性。即:
- 如果 $d = 3$(奇数),那么 $P_c(x)$ 只能包含奇次项:$c_3 x^3 + c_1 x$($c_2 = c_0 = 0$)
- 如果 $d = 2$(偶数),那么 $P_c(x)$ 只能包含偶次项:$c_2 x^2 + c_0 I$($c_1 = 0$)
如何获得任意奇偶性的多项式? 论文提供了一种技巧:引入一个额外的辅助量子比特来控制两种奇偶性多项式的线性组合:
$$P_{\text{arbitrary}}(M) = \alpha \cdot P_{\text{even}}(M) + \beta \cdot P_{\text{odd}}(M)$$这本质上是用 LCU 的方法组合两个 QSVT 回路——量子原语的组合!
6.6 QSVT 与经典非线性激活的类比¶
| 经典 | 量子(QSVT) |
|---|---|
| σ(Wx) = ReLU(Wx) | P(M) = c₀I + c₁M + c₂M² + ... |
| 逐元素非线性 | 矩阵级多项式非线性 |
| 作用于向量 | 作用于整个矩阵 |
| 简单的代数操作 | 需要块编码 + 交替门回路 |
| 计算开销可忽略 | 多项式次数 d 倍于一次块编码调用 |
核心理解:QSVT 的『非线性』不是经典意义上的逐元素非线性(如对每个元素取 max(0, x)),而是矩阵多项式带来的非线性。$M^2 \neq cM$(除非 M 是数量矩阵的倍数),所以即使二次多项式 $M^2$ 也是『矩阵非线性』的。
6.7 一个简单的例子:d=2 的 QSVT¶
假设 $M$ 是一个 2×2 的 Hermitian 矩阵:
$$M = \begin{bmatrix} 0.5 & 0.2 \\ 0.2 & -0.3 \end{bmatrix}$$二次 QSVT 的变换($d=2$,$c_0=0.1, c_1=0, c_2=0.9$,满足偶次约束):
$$P(M) = 0.1 I + 0.9 M^2$$手动计算: $$M^2 = M \cdot M = \begin{bmatrix} 0.29 & 0.04 \\ 0.04 & 0.13 \end{bmatrix}$$
$$P(M) = 0.1\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} + 0.9\begin{bmatrix} 0.29 & 0.04 \\ 0.04 & 0.13 \end{bmatrix} = \begin{bmatrix} 0.361 & 0.036 \\ 0.036 & 0.217 \end{bmatrix}$$注意 $P(M)$ 与 $M$ 的关系不是简单的缩放——这就是矩阵多项式的『非线性』效果。它改变了矩阵的谱(特征值分布),这与经典 FFN 中 ReLU 改变向量激活模式的效果类似。
6.8 QSVT 的经典模拟:真实代码¶
下面直接执行 quixer_model.py 里的 apply_qsvt_and_lcu(先看教学性复述,再执行真实源码),复用上面 5.9 节 LCU 部分构造的样例输入。
def apply_qsvt_and_lcu(
initial_states, # [batch_size, 2**n_qubits]
pqc_parameters,
parameterized_quantum_circuit,
torchquantum_device,
n_qubits,
lcu_coefficients,
qsvt_polynomial_coefficients, # [degree + 1]
):
"""
经典模拟 QSVT+LCU: P(M)|ψ⟩ = (Σ c_k M^k) |ψ⟩
通过反复调用 apply_lcu 来构建 M 的幂次。
"""
# 初始化累加器:c_0 I |ψ⟩ = c_0 |ψ⟩
accumulated = qsvt_polynomial_coefficients[0] * initial_states
# monomial 追踪 M^{k-1} |ψ⟩
monomial = initial_states
for k in range(1, len(qsvt_polynomial_coefficients)):
# M^k |ψ⟩ = M · (M^{k-1} |ψ⟩)
monomial = apply_lcu(
monomial, pqc_parameters,
parameterized_quantum_circuit,
torchquantum_device, n_qubits, lcu_coefficients
)
# 累加:c_k · M^k |ψ⟩
accumulated = accumulated + qsvt_polynomial_coefficients[k] * monomial
# 归一化(按系数 L1 范数)
norm = torch.linalg.vector_norm(qsvt_polynomial_coefficients, ord=1)
return accumulated / norm
关于上面的复述版本:其内部调用了 apply_lcu(...),但复述版本自己定义的函数名其实叫 apply_linear_combination_of_unitaries——apply_lcu 这个名字在这段代码里从未被定义过,如果真的调用会直接报 NameError。下面改为执行真实源码中的 apply_qsvt_and_lcu(degree=2,即调用 2 次 LCU),继续用上一步的样例输入,可以看到函数内部已有的调试 print 完整展示了 QSVT 多项式逐项累加的过程。
# 真实执行 quixer_model.py 中的 apply_qsvt_and_lcu(复用上一个 cell 的 demo_* 样例输入)
DEMO_DEGREE = 2
demo_qsvt_coefficients = torch.rand(DEMO_DEGREE + 1)
qsvt_lcu_output = qm.apply_qsvt_and_lcu(
demo_initial_states,
demo_pqc_parameters,
demo_pqc,
demo_qdev,
DEMO_N_QUBITS,
demo_lcu_coefficients,
demo_qsvt_coefficients,
)
print("\napply_qsvt_and_lcu 最终返回 shape:", qsvt_lcu_output.shape, qsvt_lcu_output.dtype)
print("QSVT 多项式系数:", demo_qsvt_coefficients)
为什么这样设计? 经典模拟中直接使用多项式求值的代数公式而非构造 QSVT 回路,避免了相位角度 $\{\phi_k\}$ 的繁琐计算。但对于
degree = 3(论文默认值),这意味着需要调用apply_lcu共1+2+3 = 6次。模型的参数数量不直接增加(只有d+1个可训练系数),但计算量随d线性增长。
第七章:Quixer 完整架构详解¶
目标:逐步骤理解 Quixer 从输入到输出的完整数据流。
7.1 架构全景图¶
Quixer 架构全景图
---
config:
theme: base
themeVariables:
primaryColor: '#FAFAFC'
primaryTextColor: '#222222'
primaryBorderColor: '#28749A'
lineColor: '#28749A'
secondaryColor: '#E9E9E9'
tertiaryColor: '#FFFFFF'
---
flowchart TD
INPUT["输入序列:
[The, cat, sat, on, mat, <eos>]"]
S1["1. TOKEN EMBEDDING
每个令牌映射到 维实向量"]
S2["2. ANGLE PARAMETRIZATION
实向量 → PQC 旋转角度
"]
S3["3. UNITARY TOKEN EMBEDDING
对每个令牌 :
• 初始化为
• 施加 → 获得
• 编码了令牌 的量子表示"]
S4["4. LCU TOKEN MIXING
在经典模拟中的等价操作:
对 直接计算"]
S5["5. QSVT NONLINEAR TRANSFORM
多项式次数 = qsvt_polynomial_degree
系数 可训练"]
S6["6. L2 NORMALIZATION
"]
S7["7. QUANTUM FEED-FORWARD
单层 Ansatz 14 PQC
"]
S8["8. MEASUREMENT
所有量子比特上测量:
、、
→ 个实数期望值"]
S9["9. CLASSICAL OUTPUT HEAD
→ ReLU
→ "]
OUTPUT["输出:词汇表大小的 logits
→ 下一令牌概率"]
INPUT --> S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> OUTPUT
classDef input fill:#FFFFFF,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef classical fill:#FAFAFC,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef quantum fill:#E9E9E9,stroke:#28749A,stroke-width:1.5px,color:#222222
classDef output fill:#FFFFFF,stroke:#28749A,stroke-width:1.5px,color:#222222
class INPUT,OUTPUT input
class S1,S2,S9 classical
class S3,S4,S5,S6,S7,S8 quantum
7.2 步骤详解¶
步骤 1-2:经典词嵌入 + 角度参数化¶
这是模型中最『经典』的部分。输入令牌索引通过标准的 nn.Embedding 映射为 dense 向量,然后通过一个线性层映射为 PQC 的旋转参数:
# 伪代码
x: [batch_size, n_tokens] # 令牌索引
emb = embedding(x) # [batch_size, n_tokens, d_emb]
angles = linear(dropout(emb)) # [batch_size, n_tokens, n_pqc_params]
其中 n_pqc_params = 4 * n_qubits * n_ansatz_layers(Ansatz 14 每个量子比特每层 4 个旋转参数)。
步骤 3:酉令牌嵌入¶
对于每个令牌 $j$,用其角度 $\theta_j$ 参数化一个 PQC(Ansatz 14),将其作用于初始态 $|0\ldots0\rangle$:
$$|\psi_j\rangle = \text{PQC}(\theta_j) |0\ldots0\rangle$$在经典模拟中,这意味着对 $|0\ldots0\rangle$(即振幅向量 [1, 0, 0, ..., 0])施加一系列旋转门和受控旋转门,得到 $2^q$ 维的复数态矢量。
# 伪代码(经典模拟中的实际计算)
initial_state = torch.zeros(batch_size, 2**n_qubits, dtype=torch.complex64)
initial_state[:, 0] = 1.0 + 0.0j # |0...0⟩
# 对每个令牌应用 PQC
states = apply_pqc(initial_state, angles) # [batch_size, n_tokens, 2**n_qubits]
下面用真实的 ansatz_14_torchquantum_specification 输出,通过 TorchQuantum 自带的 tq2qiskit_parameterized(Quixer 的依赖里本来就有 qiskit)把它转换成一个参数化的 Qiskit QuantumCircuit,再用 Qiskit 内置的 circuit.draw() 画图——不需要手写任何布局代码,门的先后顺序、控制/目标比特、并行度都由 Qiskit 自动处理。这里用的是 Quixer 论文/run.py 实际默认的量子比特数 n_qubits=6:
from torchquantum.plugin import tq2qiskit_parameterized
n_qubits_demo = 6
ansatz_14_spec = qm.ansatz_14_torchquantum_specification(n_qubits_demo, layers=1)
qdev_demo = tq.QuantumDevice(n_wires=n_qubits_demo, bsz=1)
ansatz_14_circuit, ansatz_14_params = tq2qiskit_parameterized(qdev_demo, ansatz_14_spec)
print(f"{len(ansatz_14_params)} 个参数,回路深度 = {ansatz_14_circuit.depth()}")
ansatz_14_circuit.draw(output="mpl", style={"name": "bw"})
步骤 4:LCU 令牌混合¶
在经典模拟中,这一步直接计算混合:
# 伪代码
# 将初始态复制 n_tokens 份
replicated = initial_state.repeat(1, n_tokens) # [B, n_tokens * 2**n_qubits]
# 对每份施加对应的 PQC(不同令牌不同参数)
evolved = apply_pqc_all_tokens(replicated, all_angles)
evolved = evolved.reshape(B, n_tokens, 2**n_qubits)
# 加权求和
mixed = einsum("bti,bt->bi", evolved, lcu_coefficients)
步骤 5:QSVT 非线性变换¶
在经典模拟中,QSVT 通过迭代应用 LCU 来构建 $M^k$:
# 伪代码:P(M)|0⟩ = Σ c_k M^k |0⟩
accumulated = c[0] * initial_state # 常数项 c_0 I
monomial = initial_state
for k in range(1, degree + 1):
# M^k |0⟩ = M · M^{k-1} |0⟩
monomial = apply_lcu(monomial, ...) # 乘以 M
accumulated += c[k] * monomial # 累加 c_k M^k |0⟩
# 归一化
result = accumulated / norm(c, ord=1)
步骤 6:L2 归一化¶
LCU + QSVT 的『有效计算』本质上是非酉的(因为是酉矩阵的加权组合,不保持范数),所以需要对量子态做 L2 归一化。这是一个关键操作——在真正的量子硬件中,这种归一化需要利用后选择概率进行,成本较高。经典模拟中直接向量归一化即可。
在 PyTorch 代码中实现为:
state = torch.nn.functional.normalize(state, dim=-1)
步骤 7:量子前馈网络¶
一个共享参数的单层 PQC 作用在所有 batch 的量子态上:
# 伪代码
shared_params = ff_params.repeat(1, batch_size)
final_state = apply_single_layer_pqc(normalized_state, shared_params)
这里的设计很重要:与令牌嵌入的 PQC(每个令牌独立参数)不同,前馈 PQC 使用单一参数集,类似于经典 FFN 对每个位置使用相同的权重矩阵。
步骤 8:测量¶
对每个量子比特测量 Pauli-X, Y, Z 的三个期望值:
$$e_X^{(i)} = \langle \psi | X_i | \psi \rangle$$$$e_Y^{(i)} = \langle \psi | Y_i | \psi \rangle$$$$e_Z^{(i)} = \langle \psi | Z_i | \psi \rangle$$总共 $3q$ 个实数值。这三个期望值形成了一个完整的单量子比特态的布洛赫球表示(Bloch Vector)。
# 伪代码
measurements = []
for qubit in range(n_qubits):
measurements.append(measure_x(qubit)) # X 期望值
measurements.append(measure_y(qubit)) # Y 期望值
measurements.append(measure_z(qubit)) # Z 期望值
# 结果: [batch_size, 3 * n_qubits]
为什么测量 X, Y, Z 三者? 经典上,一个量子比特的密度矩阵可以写作 $\rho = \frac{1}{2}(I + xX + yY + zZ)$,其中 $(x, y, z) = (\langle X\rangle, \langle Y\rangle, \langle Z\rangle)$ 完全确定了单量子比特态。因此这三个期望值提供了量子态的完整局部信息。
步骤 9:经典输出投影¶
$$f_{\text{out}}(x) = W_2 \cdot \text{ReLU}(W_1 \cdot x + b_1) + b_2$$两层 MLP 将 $3q$ 维的测量向量映射到 vocab_size 维的 logits。
# 代码
output = Sequential(
Linear(3*n_qubits → embedding_dimension),
ReLU(),
Linear(embedding_dimension → vocabulary_size)
)(measurements) # [batch_size, vocabulary_size]
7.3 Quixer 的返回值¶
Quixer 的 forward 方法返回一个元组 (logits, mean_prob):
- logits:
[batch_size, vocabulary_size]— 用于 CrossEntropyLoss 的标准分类 logits - mean_prob: 标量 — QSVT+LCU 之后、量子前馈网络之前的态矢量的 L2 范数的 batch 平均值。论文将其称为『最终概率』(final probability),它反映了通过 LCU+QSVT 混合后有多少『概率质量』得以保留(因为非酉操作会导致范数衰减)。
7.4 完整实现:Quixer 类(真实代码)¶
把上面 7.1-7.3 节讲的 9 个步骤组装起来,就是 quixer_model.py 里的 Quixer 类。下面先看一份教学性复述(__init__+forward),再导入并真实运行 quixer_model.py 里的原始实现,做一次端到端的前向传播冒烟测试。
class Quixer(torch.nn.Module):
def __init__(
self,
n_qubits: int, # 量子比特数(默认 6)
n_tokens: int, # 上下文窗口大小(默认 32)
qsvt_polynomial_degree: int, # QSVT 多项式次数(默认 3)
n_ansatz_layers: int, # PQC 层数(默认 4)
vocabulary_size: int, # 词表大小
embedding_dimension: int, # 嵌入维度(默认 512)
dropout: float, # Dropout 率(默认 0.2)
batch_size: int, # 批次大小(默认 20)
device: torch.device,
):
super().__init__()
# === 参数推导 ===
n_pqc_parameters = 4 * n_qubits * n_ansatz_layers
n_polynomial_coefficients = qsvt_polynomial_degree + 1
# === 经典嵌入 + 角度参数化 ===
self.embedding = nn.Embedding(vocabulary_size, embedding_dimension)
self.embedding_to_angles = nn.Linear(
embedding_dimension, n_pqc_parameters
)
self.dropout = nn.Dropout(dropout)
# === 量子设备(经典模拟) ===
self.torchquantum_device = tq.QuantumDevice(
n_wires=n_qubits, bsz=batch_size
)
# === 令牌 PQC(Ansatz 14) ===
token_ansatz = ansatz_14_torchquantum_specification(
n_qubits, n_ansatz_layers
)
self.token_pqc = tq.GeneralEncoder(token_ansatz)
# === 可训练参数 ===
# QSVT 多项式系数(实数)
self.qsvt_coefficients = nn.Parameter(
torch.randn(n_polynomial_coefficients)
)
# LCU 混合系数(复数!)
self.lcu_coefficients = nn.Parameter(
torch.randn(n_tokens, dtype=torch.complex64)
)
# === 量子前馈网络 ===
ff_ansatz = ansatz_14_torchquantum_specification(n_qubits, layers=1)
self.quantum_feedforward = tq.GeneralEncoder(ff_ansatz)
self.ff_parameters = nn.Parameter(
torch.randn(1, n_pqc_parameters)
)
# === 测量 ===
self.measure_all_x_y_z = tq.MeasureMultipleTimes(
[list(range(n_qubits))] * 3, # 三组:X, Y, Z
["pauli_x", "pauli_y", "pauli_z"]
)
# === 经典输出头 ===
self.output_head = nn.Sequential(
nn.Linear(3 * n_qubits, embedding_dimension),
nn.ReLU(),
nn.Linear(embedding_dimension, vocabulary_size),
)
def forward(self, x):
"""
x: [batch_size, n_tokens] — 令牌索引
返回: (logits, mean_probability)
"""
batch_size = x.shape[0]
# === 1. LCU 系数准备 ===
lcu_coeffs = self.lcu_coefficients.repeat(batch_size, 1)
lcu_coeffs = lcu_coeffs / lcu_coeffs.abs().sum(dim=1, keepdim=True)
# L1 归一化:Σ |b_j| = 1
# === 2. 经典嵌入 + 角度参数化 ===
emb = self.embedding(x) # [B, n_tokens, d_emb]
angles = self.embedding_to_angles(
self.dropout(emb)
) # [B, n_tokens, n_pqc_params]
# === 3. 初始量子态 |0...0⟩ ===
initial_state = torch.zeros(
batch_size, 2**self.n_qubits,
dtype=torch.complex64, device=x.device
)
initial_state[:, 0] = 1.0 + 0.0j
# === 4. QSVT + LCU:核心量子令牌混合 ===
qsvt_lcu_state = apply_qsvt_and_lcu(
initial_state, angles, self.token_pqc,
self.torchquantum_device, self.n_qubits,
lcu_coeffs, self.qsvt_coefficients,
)
# === 5. 记录后选择概率 ===
probabilities = torch.linalg.vector_norm(
qsvt_lcu_state, dim=-1
) # L2 范数 × 1
# === 6. L2 归一化 ===
qsvt_lcu_state = torch.nn.functional.normalize(
qsvt_lcu_state, dim=-1
)
# === 7. 量子前馈网络 ===
# 将归一化态加载到量子设备
self.torchquantum_device.reset_batch_size(batch_size)
self.torchquantum_device.set_states(qsvt_lcu_state)
# 应用单层 PQC
ff_params = self.ff_parameters.repeat(1, batch_size)
self.quantum_feedforward(
self.torchquantum_device, ff_params.float()
)
# === 8. 测量 ===
expectation_values = self.measure_all_x_y_z(
self.torchquantum_device
)
# 重整为 [batch_size, 3 * n_qubits]
measurements = expectation_values \
.reshape(3, batch_size, self.n_qubits) \
.moveaxis(0, 1) \
.reshape(batch_size, -1)
# === 9. 经典输出投影 ===
logits = self.output_head(measurements)
return logits, probabilities.mean()
关于上面的复述版本:作为教学示意,它省略/简化了一些实现细节——例如从未保存 self.n_qubits(forward 里却用到了它)、measure_all_x_y_z 的构造方式也和真实 tq.MeasureMultipleTimes 的调用签名不同——如果直接实例化并跑 forward 会报错。下面导入并真实运行 quixer_model.py 里的 Quixer 类本身,使用缩小的超参数(n_qubits=3, n_tokens=8, qsvt_polynomial_degree=2, n_ansatz_layers=1, embedding_dimension=16,对比论文默认的 n_qubits=6, n_tokens=32, degree=3, n_ansatz_layers=4, embedding_dimension=512)做一次前向传播冒烟测试——这组缩小的超参数也是本教程第九章训练演示会用到的配置。
# 真实执行 quixer_model.py 中的 Quixer 类(缩小超参数,用于本教程贯穿始终的演示)
QUIXER_DEMO_HPARAMS = dict(
n_qubits=3,
n_tokens=8, # 上下文窗口大小,即 run.py 中的 "window"
qsvt_polynomial_degree=2,
n_ansatz_layers=1,
embedding_dimension=16,
dropout=0.1,
batch_size=4,
)
DEMO_VOCAB_SIZE = 50 # 这里只是冒烟测试,用一个玩具词表大小;第九章会换成真实 PTB 词表大小
quixer_demo_model = qm.Quixer(
n_qubits=QUIXER_DEMO_HPARAMS["n_qubits"],
n_tokens=QUIXER_DEMO_HPARAMS["n_tokens"],
qsvt_polynomial_degree=QUIXER_DEMO_HPARAMS["qsvt_polynomial_degree"],
n_ansatz_layers=QUIXER_DEMO_HPARAMS["n_ansatz_layers"],
vocabulary_size=DEMO_VOCAB_SIZE,
embedding_dimension=QUIXER_DEMO_HPARAMS["embedding_dimension"],
dropout=QUIXER_DEMO_HPARAMS["dropout"],
batch_size=QUIXER_DEMO_HPARAMS["batch_size"],
device=torch.device("cpu"),
)
demo_token_ids = torch.randint(
0, DEMO_VOCAB_SIZE, (QUIXER_DEMO_HPARAMS["batch_size"], QUIXER_DEMO_HPARAMS["n_tokens"])
)
demo_logits, demo_mean_prob = quixer_demo_model(demo_token_ids)
n_params = sum(p.numel() for p in quixer_demo_model.parameters())
print("\n真实 Quixer 类前向传播结果:")
print(" logits.shape =", demo_logits.shape)
print(" mean_probability =", demo_mean_prob.item())
print(" 可训练参数总数 =", n_params)
# run.py 的核心逻辑 —— 精确复制自当前磁盘上 QuixerRepo/run.py(本地已把 epochs 从 30 改成了 5)
import argparse
quixer_hparams = {
"qubits": 6,
"layers": 3,
"ansatz_layers": 4,
"window": 32,
"epochs": 5,
"restart_epochs": 30000,
"dropout": 0.10,
"lr": 0.002,
"lr_sched": "cos",
"wd": 0.0001,
"eps": 1e-10,
"batch_size": 32,
"max_grad_norm": 5.0,
"model": "Quixer",
"print_iter": 50,
}
lstm_hparams = {
"layers": 2,
"window": 32,
"residuals": False,
"epochs": 5,
"restart_epochs": 30000,
"dropout": 0.30,
"lr": 0.002,
"lr_sched": "cos",
"wd": 0.0001,
"eps": 1e-10,
"batch_size": 32,
"max_grad_norm": 5.0,
"print_iter": 50,
}
fnet_hparams = {
"layers": 2,
"window": 32,
"epochs": 5,
"restart_epochs": 30000,
"dropout": 0.10,
"lr": 0.002,
"lr_sched": "cos",
"wd": 0.0001,
"eps": 1e-10,
"batch_size": 32,
"max_grad_norm": 5.0,
"model": "FNet",
"print_iter": 50,
}
transformer_hparams = {
"layers": 1,
"heads": 1,
"window": 32,
"epochs": 5,
"restart_epochs": 30000,
"dropout": 0.10,
"lr": 0.001,
"lr_sched": "cos",
"wd": 0.0001,
"eps": 1e-10,
"batch_size": 32,
"max_grad_norm": 5.0,
"model": "Transformer",
"print_iter": 50,
}
# 嵌入维度配置
classical_embedding_dimensions = [96, 128]
quantum_embedding_dimensions = [512]
# 模型注册表
model_map = {
"Quixer": (quixer_hparams, quantum_embedding_dimensions),
"Transformer": (transformer_hparams, classical_embedding_dimensions),
"LSTM": (lstm_hparams, classical_embedding_dimensions),
"FNet": (fnet_hparams, classical_embedding_dimensions),
}
available_models = list(model_map.keys())
# 解析命令行参数 —— 在笔记本里用显式参数列表代替 sys.argv
args = argparse.ArgumentParser(
prog="Quixer", description="Runs the Quixer model and/or classical baselines"
)
args.add_argument(
"-m", "--model", default="Quixer", choices=available_models, nargs="*", help="Model(s) to run.",
)
args.add_argument("-d", "--device", default="cpu", help="Device to run training on.")
parsed = args.parse_args(["-d", "cpu", "-m", "Quixer", "Transformer", "LSTM", "FNet"])
device_name = parsed.device
models_to_run = parsed.model if type(parsed.model) is list else [parsed.model]
print("device_name :", device_name)
print("models_to_run:", models_to_run)
print("model_map keys:", list(model_map.keys()))
print("\nquixer_hparams =", quixer_hparams)
print("classical_embedding_dimensions =", classical_embedding_dimensions)
print("quantum_embedding_dimensions =", quantum_embedding_dimensions)
关键观察:
- Quixer 的嵌入维度固定为 512,而经典基线是 96 或 128——Quixer 需要更大的嵌入维度来提供足够的参数给 PQC
- 对每个配置运行 10 个随机种子,确保统计显著性
- Quixer 默认使用 6 个量子比特、4 层 ansatz、3 次 QSVT 多项式
8.2 模型复杂度分析¶
| 组件 | 参数量 | 计算复杂度 |
|---|---|---|
| Embedding | $$\mathrm{vocab\_size} \times d_{\mathrm{emb}}$$ | $$O(B \cdot n \cdot d_{\mathrm{emb}})$$ |
| Embedding → Angles | $$d_{\mathrm{emb}} \times (4q n_{\mathrm{layers}})$$ | $$O(B \cdot n \cdot d_{\mathrm{emb}} \cdot 4q n_{\mathrm{layers}})$$ |
| LCU(每步) | $n$(复数系数) | $$O(B \cdot n \cdot 4^q)$$ |
| QSVT((d) 次) | $d+1$(多项式系数) | $$O(B \cdot d \cdot n \cdot 4^q)$$ |
| Quantum FF | $4q$(角度参数) | $$O(B \cdot 4^q)$$ |
| 测量 | $3q$(期望值读取) | $$O(B \cdot q \cdot 4^q)$$ |
| 输出头 | $$3q \times d_{\mathrm{emb}} + d_{\mathrm{emb}} \times \mathrm{vocab}$$ | $$O(B \cdot 3q \cdot d_{\mathrm{emb}} + B \cdot d_{\mathrm{emb}} \cdot \mathrm{vocab})$$ |
关键瓶颈:所有量子操作的计算复杂度都是 $O(4^q)$——这是经典模拟量子回路的根本限制。$q=6$ 时,$4^6 = 4096$(尚可处理);$q=10$ 时,$4^{10} \approx 10^6$(勉强可处理);$q=20$ 时,$4^{20} \approx 10^{12}$(完全不可处理)。这是 Quixer 当前限制在 6 个量子比特的根本原因。
8.3 baseline_models.py — 经典基线模型¶
run.py 中除了 Quixer,还会训练三个经典基线模型用于对比:Transformer、FNet、LSTM。它们共享同一个"下一词元预测"接口——输入 [batch_size, n_tokens] 的词元 id,输出 (logits, None)(None 是为了和 Quixer 的 (logits, mean_probability) 接口保持一致,经典模型没有后选择概率)。下面逐个真实导入并执行 quixer/baseline_models.py 中的类。
8.3.1 PositionalEncoding¶
标准的正弦位置编码(与 PyTorch 官方 Transformer 教程 一致),被 Transformer 和 FNet 共用;LSTM 不需要位置编码(其顺序信息来自循环结构本身)。
# 真实执行 baseline_models.py 中的 PositionalEncoding
BASELINE_EMB_DIM = 16 # 与前面 QUIXER_DEMO_HPARAMS["embedding_dimension"] 保持一致,方便对比
pos_enc = bm.PositionalEncoding(d_model=BASELINE_EMB_DIM)
demo_embeddings = torch.zeros(2, QUIXER_DEMO_HPARAMS["n_tokens"], BASELINE_EMB_DIM)
pos_encoded = pos_enc(demo_embeddings)
print("PositionalEncoding 输出 shape:", pos_encoded.shape)
print("位置 0 的编码前 4 维:", pos_encoded[0, 0, :4])
print("位置 1 的编码前 4 维:", pos_encoded[0, 1, :4])
8.3.2 Transformer¶
标准的因果(causal)nn.TransformerEncoder:embedding → 位置编码 → 带下三角 mask 的多层自注意力 → 只取最后一个位置的输出 → 线性投影到词表。create_model() 里固定用 hid_dim = 4 * emb_dim(前馈层维度是嵌入维度的 4 倍,与原始 Transformer 论文的比例一致)。
# 真实执行 baseline_models.py 中的 Transformer
transformer_demo_model = bm.Transformer(
emb_dim=BASELINE_EMB_DIM,
hid_dim=4 * BASELINE_EMB_DIM,
n_heads=1,
n_layers=1,
vocab_size=DEMO_VOCAB_SIZE,
dropout=0.1,
)
transformer_logits, transformer_aux = transformer_demo_model(demo_token_ids)
n_params = sum(p.numel() for p in transformer_demo_model.parameters())
print("Transformer logits.shape:", transformer_logits.shape, " 第二个返回值:", transformer_aux)
print("Transformer 可训练参数总数:", n_params)
8.3.3 FNet¶
用傅里叶变换替代自注意力做词元混合(torch.fft.fft2(x).real,取实部):embedding → 位置编码 → 若干层 [LayerNorm(FFT(x)+x) → LayerNorm(FeedForward(x)+x)] → 取最后一个位置 → 投影到词表。相比 Transformer,FNet 不含任何可训练的混合参数(FFT 是固定变换),所以参数量通常更少。
# 真实执行 baseline_models.py 中的 FNet
fnet_demo_model = bm.FNet(
emb_dim=BASELINE_EMB_DIM,
hid_dim=4 * BASELINE_EMB_DIM,
n_layers=1,
vocab_size=DEMO_VOCAB_SIZE,
dropout=0.1,
)
fnet_logits, fnet_aux = fnet_demo_model(demo_token_ids)
n_params = sum(p.numel() for p in fnet_demo_model.parameters())
print("FNet logits.shape:", fnet_logits.shape, " 第二个返回值:", fnet_aux)
print("FNet 可训练参数总数:", n_params)
8.3.4 LSTM¶
最朴素的基线:embedding → 多层 LSTM → 取最后一个时间步的隐藏状态 → 投影到词表。注意一个源码细节:self.project = nn.Linear(emb_dim, vocab_size),但 LSTM 的输出维度其实是 hid_dim——如果 hid_dim != emb_dim 会在 forward 里直接报形状不匹配的错误。这不是 bug 被隐藏了,而是 create_model() 里固定传入 hid_dim=hyperparams["dimension"](即恒等于 emb_dim)来避免触发它——下面的调用同样保持 emb_dim == hid_dim。
# 真实执行 baseline_models.py 中的 LSTM(hid_dim 必须等于 emb_dim,见上面的说明)
lstm_demo_model = bm.LSTM(
emb_dim=BASELINE_EMB_DIM,
hid_dim=BASELINE_EMB_DIM,
n_layers=1,
vocab_size=DEMO_VOCAB_SIZE,
dropout=0.1,
)
lstm_logits, lstm_aux = lstm_demo_model(demo_token_ids)
n_params = sum(p.numel() for p in lstm_demo_model.parameters())
print("LSTM logits.shape:", lstm_logits.shape, " 第二个返回值:", lstm_aux)
print("LSTM 可训练参数总数:", n_params)
第九章:环境搭建与模型训练¶
目标:在你的机器上成功安装并训练 Quixer。
9.1 环境要求¶
| 依赖 | 精确版本 | 说明 |
|---|---|---|
| Python | ≥ 3.11 | 严格要求 |
| PyTorch | 2.3.0(推荐) | 核心深度学习框架 |
| torchtext | 0.18.0 | 文本处理 |
| torchvision | 0.18.0 | 随 PyTorch 安装 |
| torchquantum | ≥ 0.1.8, < 0.2 | 量子回路模拟 |
| Qiskit | < 1.0 | 禁用 1.x 版本(API 重构) |
| Qiskit Aer | 0.13.3 | 量子模拟后端 |
| Qiskit IBM Provider | 0.10.0 | IBM 量子硬件接口 |
| Qiskit IBM Runtime | 0.20.0 | IBM 量子运行时 |
| datasets | ≥ 3.2, < 3.3 | Hugging Face 数据集 |
| tqdm | ≥ 4.67, < 4.68 | 进度条 |
| CUDA | 可选 | GPU 加速(训练约 100 倍快) |
9.2 踩坑预警:版本兼容性问题¶
Qiskit 1.0 进行了颠覆性的 API 重构(移除了元包架构,引入了 V2 原语)。Quixer 的代码是在 Qiskit < 1.0 的环境下编写的。如果你直接 pip install qiskit(会安装 ≥1.0),Quixer 将无法运行。
因此强烈建议创建独立的虚拟环境。
9.3 安装步骤(macOS/Linux)¶
本机的 QuixerRepo/.venv311 虚拟环境(2.6GB)已经按照下面的步骤创建好了,所有依赖也已装好——这是一次性步骤,不需要重复执行。下面这段是记录当初怎么做的(供在新机器上从零搭建时参考),紧接着的代码 cell 会真实验证这个已存在的环境现在是否仍然可用。
#!/usr/bin/env bash
set -euo pipefail
# 1. 进入工作目录
cd /Users/saintway/Downloads/Quixer
# 2. 创建存放克隆仓库的目录
mkdir -p QuixerRepo
cd QuixerRepo
# 3. 克隆 Quixer 仓库
git clone https://github.com/Quantinuum/Quixer.git .
# 4. 创建 Python 3.11 虚拟环境
/opt/homebrew/bin/python3.11 -m venv .venv311
# 5. 激活虚拟环境,并升级 pip/依赖工具
source .venv311/bin/activate
python -m pip install --upgrade pip setuptools wheel
# 6. 安装 PyTorch 2.3.0 及配套包(CPU 版本)
python -m pip install torch==2.3.0 torchvision==0.18.0 torchtext==0.18.0 \
--index-url https://download.pytorch.org/whl/cpu
# 7. 安装 Quixer 所需的量子和数据包依赖
python -m pip install 'torchquantum>=0.1.8,<0.2' 'qiskit<1.0' \
'qiskit-aer==0.13.3' 'qiskit-ibm-provider==0.10.0' \
'qiskit-ibm-runtime==0.20.0' 'datasets>=3.2,<3.3' 'tqdm>=4.67,<4.68'
# 8. 以可编辑模式安装 quixer 包本身(详见下方代码 cell 中关于这一步的真实踩坑记录)
python -m pip install -e .
这段脚本记录了本机 .venv311 最初是如何创建的(仅供参考,不再重复执行)。下面开始是真实执行的验证 cell:确认这个已经建好的环境现在依然可用,并演示 pip install -e . 这一步实际遇到的一个真实问题。
import subprocess
print("sys.executable:", sys.executable)
pip_show = subprocess.run(
[sys.executable, "-m", "pip", "show", "quixer"],
capture_output=True, text=True, cwd=str(QUIXER_REPO),
)
print("\n$ pip show quixer\n" + pip_show.stdout)
pip show quixer 确认包"已安装"(pip install -e . 显示 Successfully installed quixer-0.1.0,不报错)。但这里有一个真实踩坑:pyproject.toml 用 py-modules = ["quixer"] 声明它,这是给"单个 .py 文件模块"用的写法;而 quixer/ 实际上是一个没有 __init__.py 的目录(一个隐式命名空间包)。setuptools 生成的可编辑安装 finder 只会去找 quixer/__init__.py 或 quixer.py,两者都不存在,于是这个 finder 实际上什么也找不到。下面从 QuixerRepo 以外的目录(也就是本笔记本所在的 /Users/saintway/Downloads/Quixer)起一个全新的子进程,验证仅凭 pip install -e . 是否真的能 import quixer:
# 在一个全新的子进程里,从 /Users/saintway/Downloads/Quixer(不是 QuixerRepo)尝试 `import quixer`
# 不做任何 sys.path 处理,只依赖 `pip install -e .` 本身
check_script = "import quixer\nprint('import quixer 成功,quixer.__path__ =', list(quixer.__path__))\n"
result = subprocess.run(
[sys.executable, "-c", check_script],
capture_output=True, text=True,
cwd="/Users/saintway/Downloads/Quixer",
)
print("returncode:", result.returncode)
print("STDOUT:", result.stdout)
print("STDERR (末尾几行):")
print("\n".join(result.stderr.strip().splitlines()[-5:]))
如上面真实输出所示,returncode 非 0——单靠 pip install -e . 并不能让 import quixer 在任意目录下工作。这正是本教程『运行本教程前的准备』一开始就手动 sys.path.insert(0, str(QUIXER_REPO)) 的原因:这是目前真正有效的修复方式(README 里写的 pip install -e . 命令本身没错,只是这个仓库的 pyproject.toml 打包配置对目录形式的包声明有误)。
最后,确认一下这个已存在的 .venv311 环境本身(依赖版本)依然完好:
import qiskit
import torchtext
import datasets as hf_datasets
torchtext.disable_torchtext_deprecation_warning()
print("torch :", torch.__version__)
print("torchtext :", torchtext.__version__)
print("torchquantum :", tq.__version__ if hasattr(tq, "__version__") else "(无 __version__ 属性)")
print("qiskit :", qiskit.__version__)
print("datasets :", hf_datasets.__version__)
print("\n环境验证通过:所有 Quixer 所需依赖均可在当前 kernel(.venv311)中正常导入。")
# === 步骤 1:创建虚拟环境 ===
conda create -n quixer python=3.11 -y
conda activate quixer
# === 步骤 2:安装 PyTorch 2.3.0(CPU 版本) ===
conda install pytorch==2.3.0 torchvision==0.18.0 \
torchtext==0.18.0 torchaudio==2.3.0 cpuonly -c pytorch -y
# 或者 CUDA 版本(如果有 NVIDIA GPU):
# conda install pytorch==2.3.0 torchvision==0.18.0 \
# torchtext==0.18.0 torchaudio==2.3.0 pytorch-cuda=12.1 -c pytorch -y
# === 步骤 3:克隆仓库 ===
git clone https://github.com/Quantinuum/Quixer.git
cd Quixer
# === 步骤 4:安装 Quixer 及其依赖 ===
pip install -e .
# === 步骤 5:验证安装 ===
python -c "import torch; import qiskit; import torchquantum; \
print(f'PyTorch {torch.__version__}'); \
print(f'Qiskit {qiskit.__version__}'); \
print('安装成功!')"
常见错误与解决方案:
| 错误 | 原因 | 解决 |
|---|---|---|
ModuleNotFoundError: No module named 'qiskit.providers' |
Qiskit 1.0+ 移除了旧 API | 降级到 pip install qiskit<1.0 |
RuntimeError: CUDA out of memory |
GPU 显存不足 | 减小 batch_size 或用 -d cpu |
ImportError: cannot import name 'get_tokenizer' |
torchtext 版本不对 | 确保 torchtext==0.18.0 |
AttributeError: 'QuantumDevice' object has no attribute 'set_states' |
torchquantum 版本问题 | 确保 torchquantum>=0.1.8,<0.2 |
9.4 运行训练:真实执行 setup_training.py¶
run.py 默认会跑 10 个随机种子 × 多个嵌入维度 × 4 个模型的完整训练——量子回路的经典模拟很慢,完整跑一遍可能要数小时甚至更久(参见下面 9.5 节的时间估算)。本教程不直接调用那个完整扫描,而是手动、逐个调用 setup_training.py 里的底层函数,用大幅缩小的超参数(小 batch、小 window、少量数据、2 个 epoch)跑一次真实、可验证的端到端训练,重点是看到真实的 loss 数字和 checkpoint 保存过程,而不是追求训练出能用的模型。
为什么不能只是简单调小
run.py的超参数直接跑?setup_dataset()返回的是完整 Penn Treebank 数据;train_epoch/evaluate会遍历传入张量里的所有 batch(n_batches = iterator.shape[0] - window_size)。缩小window/batch_size反而会增加每个 epoch 的 batch 数量(batch_nr_of_elements变小)。所以下面会先拿到完整数据,再手动切片成一小段,之后再喂给训练/评估函数。
下面依次真实执行 setup_training.py 中的每一个函数。
# 真实执行 setup_training.py 中的 epoch_time / seed / initialise_weights
print("epoch_time(0.0, 125.0) =", st.epoch_time(0.0, 125.0), " # (分钟, 秒)")
st.seed(42)
print("调用 st.seed(42) 后,torch 生成的一个随机数:", torch.rand(1).item())
before_std = quixer_demo_model.embedding.weight.std().item()
st.initialise_weights(quixer_demo_model)
after_std = quixer_demo_model.embedding.weight.std().item()
print(
f"initialise_weights 应用到 7.4 节的 quixer_demo_model 前后,"
f"embedding.weight 标准差: {before_std:.4f} -> {after_std:.4f}"
f"(Embedding 层被重新初始化为 std=0.02 的正态分布)"
)
# 真实执行 setup_training.py 中的 batchify_s2s / get_batch_s2s(用手工小张量演示,不需要真实数据集)
toy_data = torch.arange(0, 30) # 30 个"词元" id: 0..29
toy_batch_size = 2
toy_window_size = 3
toy_batched = st.batchify_s2s(
toy_data, batch_size=toy_batch_size, window_size=toy_window_size, pad_token_id=-1, device="cpu"
)
print("batchify_s2s 输出 shape:", toy_batched.shape)
print(toy_batched)
x0, y0 = st.get_batch_s2s(toy_batched, i=0, window_size=toy_window_size)
print("\nget_batch_s2s(i=0):")
print(" x.shape =", x0.shape, "\n x =\n", x0)
print(" y.shape =", y0.shape, "\n y =", y0)
真实执行发现的细节:x0.shape 是 [6, 3],第一维是 6,不是我们传入的 batch_size=2!原因是 batchify_s2s 把数据先按 batch_size × window_size 分组,get_batch_s2s 再用 .T 把它变成 [batch_size * window_size, window_size]。也就是说,喂给模型 forward() 的实际 batch 维度是 batch_size * window_size,而不是超参数表里写的 batch_size。
顺带验证了一个容易担心的问题:Quixer 构造函数里的 batch_size 参数会被用来创建 self.torchquantum_device = tq.QuantumDevice(n_wires=..., bsz=batch_size),那如果这个 batch_size 和 forward() 实际收到的批大小(这里的 batch_size * window_size)不一致会不会报错?实测不会——torchquantum 的 QuantumDevice.set_states(...) 会根据传入张量的行数自动调整设备内部的批大小,构造函数里的 bsz 只是初始值,并非强约束。
接下来加载真实 Penn Treebank 数据(已在本机缓存,走本地缓存不需要联网下载),然后切片成一小段用于训练演示。
import time
# 真实执行 setup_training.py 中的 setup_dataset —— 加载真实 Penn Treebank(本机已缓存,走本地缓存)
t0 = time.time()
vocab, (train_iter, val_iter, test_iter), PAD_TOKEN = st.setup_dataset(
torch.device("cpu"), batch_size=QUIXER_DEMO_HPARAMS["batch_size"], window_size=QUIXER_DEMO_HPARAMS["n_tokens"]
)
print(f"setup_dataset 耗时: {time.time() - t0:.1f}s")
print("词表大小 len(vocab):", len(vocab))
print("PAD_TOKEN id:", PAD_TOKEN)
print("完整 train_iter.shape:", train_iter.shape)
print("完整 val_iter.shape :", val_iter.shape)
print("完整 test_iter.shape :", test_iter.shape)
# 手动切片成一小段用于训练演示(原因见 9.4 开头的说明)
WINDOW = QUIXER_DEMO_HPARAMS["n_tokens"]
N_TRAIN_STEPS = 24
N_EVAL_STEPS = 12
train_small = train_iter[: WINDOW + N_TRAIN_STEPS]
val_small = val_iter[: WINDOW + N_EVAL_STEPS]
test_small = test_iter[: WINDOW + N_EVAL_STEPS]
print("\n切片后 train_small.shape:", train_small.shape, f"({N_TRAIN_STEPS} 个训练 batch)")
print("切片后 val_small.shape :", val_small.shape, f"({N_EVAL_STEPS} 个验证 batch)")
print("切片后 test_small.shape :", test_small.shape, f"({N_EVAL_STEPS} 个测试 batch)")
9.4.1 create_model:为四个模型分别构造超参数字典¶
create_model() 从一个共享的超参数字典里按 hyperparams["model"] 分派到对应的模型类(见 8.3 节代码)。这里为四个模型分别配置一份缩小版超参数(对应真实词表大小 len(vocab),而不是之前 7.4 节冒烟测试用的玩具词表 DEMO_VOCAB_SIZE=50),并额外加上 train_cycle 需要的优化器/调度器相关字段(lr、wd、eps、lr_sched、max_grad_norm、epochs、seed)。
DEMO_TRAINING_EXTRAS = dict(
lr=1e-3,
wd=1e-4,
eps=1e-10,
lr_sched="cos",
restart_epochs=100,
max_grad_norm=5.0,
epochs=2,
seed=42,
window=WINDOW,
)
demo_hparams_by_model = {
"Quixer": dict(
model="Quixer",
qubits=QUIXER_DEMO_HPARAMS["n_qubits"],
layers=QUIXER_DEMO_HPARAMS["qsvt_polynomial_degree"],
ansatz_layers=QUIXER_DEMO_HPARAMS["n_ansatz_layers"],
dimension=QUIXER_DEMO_HPARAMS["embedding_dimension"],
dropout=QUIXER_DEMO_HPARAMS["dropout"],
batch_size=QUIXER_DEMO_HPARAMS["batch_size"],
**DEMO_TRAINING_EXTRAS,
),
"Transformer": dict(
model="Transformer", dimension=16, heads=1, layers=1, dropout=0.1, **DEMO_TRAINING_EXTRAS
),
"LSTM": dict(model="LSTM", dimension=16, layers=1, dropout=0.1, **DEMO_TRAINING_EXTRAS),
"FNet": dict(model="FNet", dimension=16, layers=1, dropout=0.1, **DEMO_TRAINING_EXTRAS),
}
demo_models = {}
for model_name, hparams in demo_hparams_by_model.items():
model = st.create_model(hparams, torch.device("cpu"), len(vocab))
st.initialise_weights(model)
n_params = sum(p.numel() for p in model.parameters())
demo_models[model_name] = model
print(f"{model_name:12s} 创建成功,可训练参数总数 = {n_params}")
9.4.2 单独调用 train_epoch / evaluate¶
在跑完整的 train_cycle 之前,先单独调用一次 train_epoch 和 evaluate,看真实的 loss 数值(用 demo_models["Quixer"],跑在切片后的 train_small/val_small 上)。
import math
standalone_model = demo_models["Quixer"]
standalone_hparams = demo_hparams_by_model["Quixer"]
optimizer = torch.optim.Adam(
standalone_model.parameters(),
lr=standalone_hparams["lr"],
weight_decay=standalone_hparams["wd"],
eps=standalone_hparams["eps"],
)
loss_function = torch.nn.CrossEntropyLoss()
train_loss = st.train_epoch(
standalone_model, train_small, optimizer, loss_function,
standalone_hparams["max_grad_norm"], None, standalone_hparams["window"],
)
val_loss = st.evaluate(standalone_model, val_small, loss_function, standalone_hparams["window"])
print(f"\n单独调用一次 train_epoch: loss = {train_loss:.4f} (ppl = {math.exp(train_loss):.1f})")
print(f"单独调用一次 evaluate : loss = {val_loss:.4f} (ppl = {math.exp(val_loss):.1f})")
9.4.3 train_cycle:完整的多轮训练 + checkpoint 保存/加载¶
train_cycle 把 train_epoch/evaluate 包装成完整的多轮训练循环:每轮结束后,如果验证集 loss 创新低就把模型权重存到 ./trained_models/(这里已经 os.chdir 到了 QuixerRepo,所以真实落盘在 QuixerRepo/trained_models/,文件名带时间戳,不会和仓库里已有的 4 个 checkpoint 冲突);训练结束后重新加载最优 checkpoint,再分别在验证集/测试集上评估一次。下面各用一个全新初始化的模型(不复用上一节 standalone_model 的训练状态)分别跑一次 Quixer 和 Transformer,epochs=2。
# train_cycle 完整跑一次 Quixer(全新初始化,与上面 9.4.2 的 standalone_model 无关)
fresh_quixer = st.create_model(demo_hparams_by_model["Quixer"], torch.device("cpu"), len(vocab))
st.initialise_weights(fresh_quixer)
st.seed(demo_hparams_by_model["Quixer"]["seed"])
quixer_test_loss = st.train_cycle(
fresh_quixer, demo_hparams_by_model["Quixer"], train_small, val_small, test_small
)
print("\nQuixer train_cycle 返回的 test_loss:", quixer_test_loss)
# train_cycle 完整跑一次 Transformer 基线,作对比
fresh_transformer = st.create_model(demo_hparams_by_model["Transformer"], torch.device("cpu"), len(vocab))
st.initialise_weights(fresh_transformer)
st.seed(demo_hparams_by_model["Transformer"]["seed"])
transformer_test_loss = st.train_cycle(
fresh_transformer, demo_hparams_by_model["Transformer"], train_small, val_small, test_small
)
print("\nTransformer train_cycle 返回的 test_loss:", transformer_test_loss)
9.4.4 get_train_evaluate:run.py 实际使用的闭包¶
run.py(见 9.6 节)并不直接调用 create_model/train_cycle,而是调用 get_train_evaluate(device) 拿到一个闭包 train_evaluate,再对着完整数据集和完整超参数反复调用它。本教程没有直接调用这个闭包跑全量训练——原因见本节开头的说明(setup_dataset() 内部会加载完整 PTB,且不接受"只用前 N 个 batch"这样的参数,无法在闭包内部做我们上面手动做的切片)。这里只检视一下它的类型,确认它确实是一个可调用对象:
train_evaluate = st.get_train_evaluate(torch.device("cpu"))
print("get_train_evaluate 返回类型:", type(train_evaluate))
print("是否可调用:", callable(train_evaluate))
print(
"\n真实用法(run.py 里,不在本教程中执行):"
"\n train_evaluate({**quixer_hyperparams, 'model': 'Quixer', 'dimension': 512, 'seed': 1234})"
)
# 顺带确认 train_cycle 在上面真实写入了新的 checkpoint 文件
checkpoint_files = sorted((QUIXER_REPO / "trained_models").glob("*.pt"))
print(f"\nQuixerRepo/trained_models/ 目前共有 {len(checkpoint_files)} 个 checkpoint 文件:")
for f in checkpoint_files:
print(" ", f.name, f"({f.stat().st_size / 1e6:.2f} MB)")
9.4.5 结构忠实但缩小规模的 run.py 主循环¶
真实 run.py(8.1 节复现的核心逻辑)的主循环是 for model_name in models_to_run: for embedding_dimension in embedding_dimensions: for seed in torch.randint(..., size=(10,)),对每个组合都调用未切片的 train_evaluate(hyperparameters)(即完整 PTB + 论文默认超参数)。下面复现同样的三层循环结构,但做两处改动:种子数量从 10 降到 1;对每个模型改用上面 9.4.1 节的缩小超参数 + create_model/train_cycle 手动 pipeline(而不是未切片的 train_evaluate 闭包)。Quixer 和 Transformer 在 9.4.3 节已经真实训练过,这里直接复用那次的结果而不重复训练;LSTM 和 FNet 在这里首次真实训练。
DEMO_SEEDS_PER_MODEL = 1 # 真实 run.py 是 10
demo_run_results = {
"Quixer": quixer_test_loss, # 复用 9.4.3 节的真实训练结果
"Transformer": transformer_test_loss, # 复用 9.4.3 节的真实训练结果
}
for model_name in available_models:
hyperparameters, embedding_dimensions = model_map[model_name]
embedding_dimension = embedding_dimensions[0] # 真实 run.py 会遍历全部维度,这里演示只取第一个
if model_name in demo_run_results:
print(
f"{model_name:12s} 已在 9.4.3 节用缩小超参数真实训练过,"
f"直接复用 test_loss = {demo_run_results[model_name]:.4f},跳过重复训练"
)
continue
for seed_value in torch.randint(high=1000000, size=(DEMO_SEEDS_PER_MODEL,)).tolist():
demo_hp = dict(demo_hparams_by_model[model_name])
demo_hp["seed"] = seed_value
print(
f"\n=== 运行 {model_name}(真实 run.py 会用 embedding_dimension={embedding_dimension},"
f"这里的演示用的是缩小超参数 dimension={demo_hp['dimension']})==="
)
model = st.create_model(demo_hp, torch.device("cpu"), len(vocab))
st.initialise_weights(model)
st.seed(seed_value)
test_loss = st.train_cycle(model, demo_hp, train_small, val_small, test_small)
demo_run_results[model_name] = test_loss
print("\n所有模型的演示 test_loss 汇总(缩小超参数、极小数据切片,仅用于验证 pipeline 能跑通):")
for name in available_models:
print(f" {name:12s}: {demo_run_results[name]:.4f}")
# === 仅运行 Quixer(CPU) ===
python run.py -d cpu -m Quixer
# === 运行 Quixer + 经典基线(CPU) ===
python run.py -d cpu -m Quixer Transformer LSTM FNet
# === GPU 版本(如果有 CUDA) ===
python run.py -d cuda -m Quixer
# === 仅运行经典基线(用于对比) ===
python run.py -d cpu -m Transformer LSTM FNet
9.5 训练过程详解¶
训练过程将:
- 下载数据:自动从 Hugging Face Hub 下载 Penn Treebank 数据集
- 构建词表:使用
basic_english分词器,构建包含<pad>、<unk>、<eos>特殊令牌的词表 - 批量化:将数据按
window=32的上下文窗口组织为批次 - 多轮训练:默认 40 个 epoch,使用 Adam 优化器,Cosine Annealing 学习率调度
- 自动保存:将每个 epoch 的最优模型保存到
./trained_models/目录 - 多种子评估:对每个配置运行 10 个不同的随机种子
单次完整训练(1 个模型 + 1 个嵌入维度 + 10 个种子 + 40 epochs)在 CPU 上大约需要 6-12 小时,在 GPU 上大约需要 10-20 分钟。
9.6 训练过程中的输出解读¶
Epoch: 01 | Time: 2m 30s
Train Loss: 5.832 | Train ppl: 341.2
Val. Loss: 5.651 | Val. ppl: 284.5
Epoch: 10 | Time: 2m 15s
Train Loss: 4.912 | Train ppl: 135.8
Val. Loss: 5.012 | Val. ppl: 149.9
...
FINAL TRAINED MODEL STATS:
Val. Loss: 4.897 | Val. ppl: 133.8
Test Loss: 4.912 | Test ppl: 135.7
其中:
- Loss:Cross-Entropy Loss(越低越好)
- ppl(Perplexity,困惑度):$\text{ppl} = e^{\text{loss}}$,衡量模型对下一令牌预测的不确定性。困惑度越低,模型越『自信』且准确。作为参考:随机猜测的困惑度 ≈ 词表大小(Penn Treebank 约 10,000)
9.7 实验设计解读¶
run.py 中 Quixer 和经典基线的超参数:
| 参数 | Quixer | Transformer | LSTM | FNet |
|---|---|---|---|---|
| 嵌入维度 | 512 | 96, 128 | 96, 128 | 96, 128 |
| 层数 | 3 | 3 | 3 | 3 |
| 窗口 | 32 | 32 | 32 | 32 |
| Epochs | 40 | 40 | 40 | 40 |
| Batch | 20 | 20 | 20 | 20 |
| Dropout | 0.2 | 0.2 | 0.2 | 0.2 |
| 量子比特 | 6 | N/A | N/A | N/A |
| Ansatz 层数 | 4 | N/A | N/A | N/A |
为什么 Quixer 的嵌入维度高达 512?
Quixer 不使用多头注意力,其『表达能力』主要来自:(1) PQC 在 $2^6=64$ 维希尔伯特空间中的演化;(2) 经典嵌入向高维角度参数的线性映射。需要 $d_{\text{emb}} \to n_{\text{pqc\_params}} = 4 \times 6 \times 4 = 96$ 的映射。512 维嵌入提供了充足的过完备表示能力。经典 Transformer 用 128 维嵌入则因为多头注意力提供了足够的交互表达能力。
第十章:实验结果与性能分析¶
目标:理解 Quixer 目前的性能水平及其含义。
10.1 Penn Treebank 结果¶
Quixer 在 Penn Treebank 语言建模任务上的表现(论文表 1):
| 模型 | 测试困惑度 (Test PPL) | 参数量 |
|---|---|---|
| LSTM (128d) | ~100-110 | ~6M |
| FNet (128d) | ~110-120 | ~1M |
| Transformer (128d) | ~95-105 | ~4M |
| Quixer (512d) | ~135-145 | ~10M |
谨慎解读:论文声称 Quixer 表现『competitive with an equivalent classical baseline』(与等效的经典基线有竞争力)。但这一声明在对抗验证中以 0-3 被否定——三个独立验证者均无法确认 Quixer 在相同参数量条件下接近经典基线的性能。Quixer 的实际表现显著弱于同等参数规模的经典 Transformer。
本教程实机演示 vs. 上表基准的关系:第九章跑的小规模演示(3 量子比特、8 词元窗口、2 个 epoch、几十个 batch)只是为了验证代码流程能端到端跑通、产出真实 loss 和 checkpoint,不是对上表基准数字的复现尝试——数据量和训练规模相差几个数量级,演示得到的 loss/ppl 数字本身没有参考价值,评估 Quixer 真实性能请以上表、原论文和下面 10.3 节的 Aalto 复现结果为准。
10.2 为什么 Quixer 不如经典模型?¶
这涉及几个根本原因:
量子态矢量维度限制:$q=6$ 意味着量子态只有 64 维,远小于经典 Transformer 的隐藏维度。即使有 512 维的嵌入『外衣』,真正进行令牌混合的空间只有 64 维。
梯度消失问题(Barren Plateau):参数化量子回路在随机初始化时,梯度指数级趋近于零。这是量子机器学习的根本性挑战——Quixer 无法幸免。原论文第 6 节将此明确列为开放问题。
有限上下文:窗口仅 32 个令牌——后续研究 QMamba 指出这是 Quixer 的一大瓶颈。
经典模拟的近似:经典模拟中 QSVT 通过直接计算 $M^k|0\rangle$ 来实现,免去了构造 SELECT-PREPARE 回路和计算相位角度 $\{\phi_k\}$ 的困难。但这意味着,经典模拟并不能完全代表真实量子硬件上的行为(后者可能因为干涉效应产生更多表达力,也可能因为噪声产生更差表现)。
10.3 Aalto 大学复现结果¶
2024 年 9 月,Aalto 大学的一篇学士论文在经典模拟环境下独立复现了 Quixer 实验。关键发现:
- Quixer 可以在 Penn Treebank 上训练并获得合理的结果
- 训练极其缓慢(CPU 上需要数小时到数天)
- 性能低于经典 Transformer 基线
- 论文确认了梯度消失问题的存在
10.4 性能评估的正确视角¶
评估 Quixer 的性能时,以下几点非常重要:
概念验证 > 性能竞赛:Quixer 的价值在于证明了量子原语可以构建可训练的语言模型,而不是与 GPT-4 竞争。
经典模拟 ≠ 量子实现:当前所有 Quixer 结果都来自经典模拟,其计算瓶颈($O(4^q)$)完全不同于真实量子硬件的瓶颈。经典模拟中表现一般的模型在量子硬件上可能有完全不同的行为。
第一代产品:Quixer 是量子 Transformer 领域的开创性工作。第一个晶体管计算机也无法与现代 CPU 比性能。
框架价值:论文强调 Quixer 是一个框架——其参数化组件可以被替换为不同结构,从而产生一类全新的量子 Transformer。比较『单个实例』的性能是不全面的。
第十一章:模型推理与预测¶
目标:用第九章真实训练好的模型(
fresh_quixer、fresh_transformer)做真实推理——观察 Quixer 到底『预测』出了什么,并实现一个简单的自回归文本生成循环。
11.1 从 logits 到词元预测¶
Quixer.forward 返回的 logits(形状 [batch, vocabulary_size])本身只是未归一化的分数,要变成『预测』还需要几步:
- Softmax:把 logits 转换成一个合法的概率分布 $p(v) = \dfrac{e^{\text{logit}_v}}{\sum_{v'} e^{\text{logit}_{v'}}}$。
- Argmax(贪心预测):概率最高的词元就是模型认为『最可能的下一个词』——评估 Top-1 准确率、以及本章 11.3 节的贪心解码都基于它。
- Top-k:只看概率最高的 k 个候选词,比单一 argmax 更能反映模型的『犹豫程度』——如果真实词元落在 Top-5 而不是 Top-1,说明模型『方向大致正确,但不够自信』。
对 Quixer 而言还有第三个量:mean_prob(第 7.3 节介绍过)。它不是词表上的概率分布,而是 LCU+QSVT 混合之后量子态矢量 L2 范数的批平均值——一个介于 0 和 1 之间的标量,反映了非酉操作导致的『概率质量』保留比例。经典基线模型(Transformer/LSTM/FNet)没有这个量,forward 直接返回 None 作为第二个值。
11.2 真实执行:单步预测对比(Quixer vs Transformer)¶
从 9.4 节切片后的真实 Penn Treebank 测试数据 test_small 里取出一个真实样本,分别喂给 fresh_quixer 和 fresh_transformer(两者都已经过 9.4.3 节的 train_cycle 训练并重新加载了各自的最优 checkpoint),对比两者的 Top-5 预测:
import torch.nn.functional as F
TOPK = 5
# 选 eval_batch_idx=N_EVAL_STEPS-1(即最后一个可用 batch):batchify_s2s 会在数据最前面
# 用 window_size-1 个 <pad> 撑出第一份上下文(第 5.6/9.5 节提到的边界处理),batch_idx=0 时
# 刚好会取到这段纯 padding 的上下文——选最后一个 batch 可以绕开这个边界效应,拿到一段真实文本。
eval_batch_idx = N_EVAL_STEPS - 1
sample_row = 0
x_eval, y_eval = st.get_batch_s2s(test_small, eval_batch_idx, WINDOW)
itos = vocab.get_itos()
context_ids = x_eval[sample_row].tolist()
true_next_id = y_eval[sample_row].item()
context_text = " ".join(itos[t] for t in context_ids)
true_next_text = itos[true_next_id]
print(f"上下文窗口(WINDOW={WINDOW} 个真实 PTB 测试集词元):")
print(" ", context_text)
print("\n真实的下一个词元:", repr(true_next_text))
for model_name, model in [("Quixer", fresh_quixer), ("Transformer", fresh_transformer)]:
model.eval()
with torch.no_grad():
logits, mean_prob = model(x_eval[sample_row : sample_row + 1])
probs = F.softmax(logits[0], dim=-1)
top_probs, top_ids = probs.topk(TOPK)
mean_prob_str = f"{mean_prob.item():.4f}" if mean_prob is not None else "N/A(经典基线无此量)"
print(f"\n[{model_name}] mean_prob = {mean_prob_str}")
print(f"[{model_name}] Top-{TOPK} 预测:")
for rank, (p, tid) in enumerate(zip(top_probs.tolist(), top_ids.tolist()), start=1):
hit = " <-- 命中真实词" if tid == true_next_id else ""
print(f" {rank}. {itos[tid]!r:15s} p={p:.4f}{hit}")
11.3 自回归文本生成:贪心解码¶
run.py 和 9.4 节都只做『单步下一词预测』的训练/评估,从没有让模型真正连续生成一段文本。这里手动实现一个最简单的自回归生成循环:每一步用当前的 WINDOW 个词元预测下一个词元,把预测结果拼接到序列末尾,再丢弃最旧的一个词元以保持窗口大小不变(因为 Quixer/经典基线的 forward 都要求输入正好是 WINDOW 个词元),重复 GENERATE_STEPS 次。
GENERATE_STEPS = 20
def greedy_generate(model, context_ids, steps, window):
model.eval()
generated = list(context_ids)
mean_probs = []
for _ in range(steps):
window_ids = generated[-window:]
x_step = torch.tensor(window_ids, dtype=torch.long).unsqueeze(0) # [1, window]
with torch.no_grad():
logits, mean_prob = model(x_step)
next_id = logits[0].argmax().item()
generated.append(next_id)
mean_probs.append(mean_prob.item() if mean_prob is not None else float("nan"))
return generated, mean_probs
quixer_generated, quixer_mean_probs = greedy_generate(fresh_quixer, context_ids, GENERATE_STEPS, WINDOW)
transformer_generated, _ = greedy_generate(fresh_transformer, context_ids, GENERATE_STEPS, WINDOW)
print("原始上下文:")
print(" ", " ".join(itos[t] for t in context_ids))
print(f"\n[Quixer] 贪心解码生成的 {GENERATE_STEPS} 个新词元:")
print(" ", " ".join(itos[t] for t in quixer_generated[len(context_ids):]))
print(f"\n[Transformer] 贪心解码生成的 {GENERATE_STEPS} 个新词元:")
print(" ", " ".join(itos[t] for t in transformer_generated[len(context_ids):]))
11.4 mean_prob 在推理阶段的含义¶
如何看待上面生成的文本:
fresh_quixer/fresh_transformer只在 9.4 节的极小数据切片(几十个 batch、2 个 epoch)上训练过,远未收敛——生成的文本大概率是重复或不连贯的词元序列,这是预期之中的结果,不代表 Quixer 的真实生成能力上限。这里的重点是验证生成 pipeline(滑动窗口 + 贪心解码)本身能真实跑通,具体文本质量的评估请参考第十章的论文基准数字。
下面看 Quixer 在生成过程中每一步的 mean_prob 如何变化。回顾第五、六章:LCU 和 QSVT 都是非酉操作(分别通过 PREPARE-SELECT 的后选择、以及 QSVT 多项式变换的振幅衰减实现),因此量子态矢量的范数会随着这些操作逐步衰减;mean_prob 就是这个衰减后范数的批平均值,也近似对应了真实量子硬件上做后选择测量时的成功概率 $p_{\text{success}}$(第十一章 11.1.5 节提到的采样开销正来自于这里)。
print("Quixer 每一步生成时的 mean_prob(LCU+QSVT 归一化后的态矢量范数均值):")
for step, p in enumerate(quixer_mean_probs, start=1):
print(f" 步骤 {step:2d}: mean_prob = {p:.4f}")
print(
f"\n均值: {sum(quixer_mean_probs) / len(quixer_mean_probs):.4f}"
f" 最小值: {min(quixer_mean_probs):.4f}"
f" 最大值: {max(quixer_mean_probs):.4f}"
)
print(
"\n注意:mean_prob 越低,意味着如果这是在真实量子硬件上运行,"
"后选择丢弃的采样比例就越高——这是 Quixer 在真实硬件部署时的核心开销来源之一。"
)
第十二章:端到端测试与评价¶
目标:搭建一个完整的端到端评价流程——测试集困惑度、词元级准确率、checkpoint 保存/加载回归测试——并把结果汇总成一份最终评价报告。
12.1 测试集困惑度评估¶
train_cycle(9.4.3 节)在训练结束后已经自动算过一次测试集 loss(存在 quixer_test_loss/transformer_test_loss 里)。这里把 st.evaluate 包装成一个独立、可复用的评价函数,重新跑一遍同样的评估,并验证它与 train_cycle 内部算出的数字完全一致——这本身就是一个有用的正确性检查:如果两次算出的数字不一致,说明模型状态或数据切片之间出现了不该有的差异。
import math
loss_function_eval = torch.nn.CrossEntropyLoss()
def evaluate_perplexity(model, data, window):
loss = st.evaluate(model, data, loss_function_eval, window)
return loss, math.exp(loss)
quixer_eval_loss, quixer_eval_ppl = evaluate_perplexity(fresh_quixer, test_small, WINDOW)
transformer_eval_loss, transformer_eval_ppl = evaluate_perplexity(fresh_transformer, test_small, WINDOW)
print(
f"[Quixer] test loss = {quixer_eval_loss:.4f} ppl = {quixer_eval_ppl:.1f}\n"
f" 与 9.4.3 节 train_cycle 内部算出的 quixer_test_loss={quixer_test_loss:.4f} 一致: "
f"{math.isclose(quixer_eval_loss, quixer_test_loss)}"
)
print(
f"\n[Transformer] test loss = {transformer_eval_loss:.4f} ppl = {transformer_eval_ppl:.1f}\n"
f" 与 9.4.3 节 train_cycle 内部算出的 transformer_test_loss={transformer_test_loss:.4f} 一致: "
f"{math.isclose(transformer_eval_loss, transformer_test_loss)}"
)
12.2 词元级 Top-1 / Top-5 准确率¶
困惑度是一个『整体』指标,不容易直觉地判断『模型每次预测对不对』。这里换一个更直观的指标:遍历 test_small 里的每一个 batch,统计模型的 Top-1(argmax)和 Top-5 预测命中真实下一词元的比例。
def compute_accuracy(model, data, window, topk=5):
model.eval()
n_batches = data.shape[0] - window
correct_top1 = 0
correct_topk = 0
total = 0
with torch.no_grad():
for batch_idx in range(n_batches):
x, y = st.get_batch_s2s(data, batch_idx, window)
logits, _ = model(x)
top1_pred = logits.argmax(dim=-1)
topk_pred = logits.topk(topk, dim=-1).indices
correct_top1 += (top1_pred == y).sum().item()
correct_topk += (topk_pred == y.unsqueeze(1)).any(dim=1).sum().item()
total += y.numel()
return correct_top1 / total, correct_topk / total, total
accuracy_results = {}
for model_name, model in [("Quixer", fresh_quixer), ("Transformer", fresh_transformer)]:
top1_acc, top5_acc, n_tokens_eval = compute_accuracy(model, test_small, WINDOW, topk=5)
accuracy_results[model_name] = (top1_acc, top5_acc)
print(f"[{model_name}] 在测试集切片上评价了 {n_tokens_eval} 个词元预测:")
print(f" Top-1 准确率: {top1_acc:.2%}")
print(f" Top-5 准确率: {top5_acc:.2%}")
12.3 Checkpoint 回归测试:保存/加载一致性¶
9.4.3 节的 train_cycle 已经把最优权重存到了 ./trained_models/,并在训练结束后重新 load_state_dict 回 fresh_quixer。这里做一个独立的回归测试,直接验证『保存 -> 重新加载』这个流程本身不会改变模型的行为:把 fresh_quixer 当前权重存到一个临时文件,用同样的超参数新建一个模型实例并加载这个临时文件,再对同一份真实输入比较两者的输出是否完全一致。
import tempfile
from pathlib import Path as _Path
regression_test_input = x_eval[:2] # 复用 11.2 节的真实测试数据
fresh_quixer.eval()
with torch.no_grad():
reference_logits, reference_mean_prob = fresh_quixer(regression_test_input)
with tempfile.TemporaryDirectory() as tmpdir:
checkpoint_path = _Path(tmpdir) / "regression_test_checkpoint.pt"
# 真实踩坑:state_dict() 里混入了 torchquantum_device.states 这个缓冲区——它是量子设备内部的
# 态矢量,每次 forward 都会被 set_states 按当前输入的 batch 大小重新覆写,并不是训练学到的参数。
# 如果加载到一个从未跑过 forward、构造时 batch_size 超参数不同的全新实例上,形状对不上,
# load_state_dict 会报 size mismatch。这里把它过滤掉,只保存/加载真正的可训练参数。
trainable_state_dict = {
k: v for k, v in fresh_quixer.state_dict().items() if "torchquantum_device" not in k
}
torch.save(trainable_state_dict, checkpoint_path)
reloaded_quixer = st.create_model(demo_hparams_by_model["Quixer"], torch.device("cpu"), len(vocab))
reloaded_quixer.load_state_dict(torch.load(checkpoint_path), strict=False)
reloaded_quixer.eval()
with torch.no_grad():
reloaded_logits, reloaded_mean_prob = reloaded_quixer(regression_test_input)
logits_match = torch.allclose(reference_logits, reloaded_logits, atol=1e-6)
mean_prob_match = torch.allclose(reference_mean_prob, reloaded_mean_prob, atol=1e-6)
print("Checkpoint 保存/重新加载后,输出是否与内存中的原模型完全一致:")
print(" logits 一致: ", logits_match)
print(" mean_prob 一致: ", mean_prob_match)
print(f" logits 最大差值: {(reference_logits - reloaded_logits).abs().max().item():.2e}")
12.4 端到端评价报告汇总¶
把本章算出的困惑度、Top-1/Top-5 准确率汇总成一张表,和 9.4.5 节缩小规模的 demo_run_results 放在一起看:
print("=" * 70)
print("端到端评价报告汇总(本教程缩小规模的真实结果,不是论文基准数字)")
print("=" * 70)
print(f"{'模型':<14}{'Test PPL':>12}{'Top-1 Acc':>12}{'Top-5 Acc':>12}")
for model_name in ["Quixer", "Transformer"]:
ppl = quixer_eval_ppl if model_name == "Quixer" else transformer_eval_ppl
top1_acc, top5_acc = accuracy_results[model_name]
print(f"{model_name:<14}{ppl:>12.1f}{top1_acc:>12.2%}{top5_acc:>12.2%}")
print("=" * 70)
print(
"\n注意:以上数字来自本教程 9.4 节切片后的极小数据集(几十个 batch、2 个 epoch),"
"\n仅用于验证推理/评价 pipeline 的正确性,不能反映 Quixer 在完整 Penn Treebank 上的真实性能——"
"\n后者请参考第十章 10.1 节的论文基准表和 10.3 节的 Aalto 大学复现结果。"
)
第十三章:局限性与开放问题¶
目标:诚实评估 Quixer 当前的局限性,识别值得关注的开放问题。
13.1 已确认的局限性¶
13.1.1 梯度消失(Vanishing Gradients)¶
这是 Quixer 最根本的挑战。参数化量子回路(PQC)普遍存在 Barren Plateau 现象:当回路深度或量子比特数增加时,梯度以指数方式趋近于零。
具体到 Quixer:
- Ansatz 14 使用 4 层,在 6 量子比特下仍然可训练
- 但扩展到更多量子比特或更深回路时,梯度消失将变得严重
- 这限制了模型规模的扩展
原论文原文(第 6 节):
"Finding an instance of Quixer that does not suffer from vanishing gradients while being too expressive for classical simulation is left to future work."
翻译:找到一个既无梯度消失问题、又超出经典模拟能力的 Quixer 实例,是留给未来工作的开放问题。
13.1.2 经典可模拟性(Classical Simulability)¶
当前所有 Quixer 实验使用的量子回路(6 量子比特、4 层 PQC、3 次 QSVT)在经典计算机上可以精确模拟。这意味着:
- 没有任何量子优势——经典计算机完全可以模拟这些回路
- 要证明量子优势,需要扩大到经典不可模拟的规模
- 但扩大规模又面临梯度消失问题——两难困境
13.1.3 有限上下文窗口(32 令牌)¶
在 Penn Treebank 实验中,Quixer 的上下文窗口仅为 32 个令牌。这远小于现代 LLM 的数万到数十万令牌上下文。后续工作 QMamba(ICAART 2025)以 Quixer 为对比基线,指出这个限制是其核心瓶颈之一。
13.1.4 对经典输出头的强依赖¶
当前模型最后的『经典输出头』(两层 MLP,从 3q 维到 vocab_size 维)承担了大量的计算工作。没有这个输出头,模型的性能会大幅下降。这引发了一个问题:量子部分到底学到了多少,还是经典 MLP 在做大部分『重活』?
13.1.5 未解决的量子硬件部署¶
- 论文中的资源估算(§3.5)考虑了后选择概率和门复杂度,但实际硬件部署尚未被独立验证
- Quantinuum 2025 年 6 月的博客宣称已将 Quixer 部署到真实硬件,但缺少性能数据和第三方验证
- 后选择导致的 $O(1/p_{\text{success}})$ 采样开销在实际中可能非常高
13.2 关键开放问题¶
这些问题不仅对 Quixer 重要,对整个量子机器学习领域也至关重要:
量子优势边界:量子比特数、回路深度、QSVT 多项式次数——需要达到什么规模的组合,才能在语言建模任务上产生可验证的量子优势?
梯度消失的解决:是否存在某种回路结构或初始化策略,能从根本上避免 PQC 的 Barren Plateau?
LCU 系数的可解释性:$b_j = e^{i\gamma_j}|a_j|^2$ 究竟代表了什么语义?能否像经典注意力的 softmax 权重那样被可视化和解读?
与经典架构的融合:Quixer 使用纯粹的 LCU+QSVT 替代注意力——但如果采用混合策略(部分量子、部分经典)会更好吗?
扩展到更大模型:能否设计一种『量子蒸馏』方案,用小规模量子 Transformer 指导大规模经典 Transformer 训练?
第十四章:后续研究方向与生态¶
目标:了解 Quixer 启发了哪些后续工作,以及你可以在哪些方向上贡献。
14.1 QMamba:从 Transformer 到状态空间模型¶
QMamba(ICAART 2025)是 Quixer 最直接的后续工作。它基于以下观察:
- Quixer 在 Penn Treebank 上的上下文窗口仅 32 个令牌
- Mamba(选择性状态空间模型)在长序列建模上表现出色
- 将 Mamba 的选择性 SSM 机制移植到量子域可能克服 Transformer 的上下文限制
QMamba 是首个量子 Mamba 模型,以 Quixer 为对比基线,代表了量子序列建模从 Transformer 范式向 SSM 范式的转移。
14.2 Quixer 框架的可扩展性¶
原论文第 5 节特别强调了 Quixer 作为框架的价值:
"its parameterised components can be substituted with fixed structures to yield new classes of quantum transformers."
翻译:Quixer 的参数化组件可以被替换为固定结构,从而产生全新类别的量子 Transformer。
具体来说,你可以在以下组件上做文章:
| 组件 | 当前选择 | 替代方案 |
|---|---|---|
| 酉嵌入回路 | Ansatz 14 | 其他 PQC ansatz、Hardware-Efficient Ansatz |
| LCU 系数结构 | 可训练复数权重 | 固定权重(如相等权重、基于令牌距离的衰减权重) |
| QSVT 多项式 | 通用多项式 | 特定函数的多项式近似(如 sigmoid、tanh) |
| 测量方案 | X, Y, Z 全测量 | 稀疏测量、自适应测量、基于任务的测量选择 |
14.3 相关项目与工具链¶
| 项目 | 说明 | 链接 |
|---|---|---|
| TorchQuantum | MIT 开发的量子机器学习框架,Quixer 的核心依赖 | github.com/mit-han-lab/torchquantum |
| PennyLane | Xanadu 的量子机器学习库,支持自动微分和 QSVT | pennylane.ai |
| Qiskit | IBM 的量子计算框架(注意版本 < 1.0) | qiskit.org |
| Lambeq | Quantinuum 的量子 NLP 框架 | github.com/Quantinuum/lambeq |
| pyqsp | QSVT 相位角度计算工具 | github.com/ichuang/pyqsp |
| QSPPACK | QSVT 的 MATLAB 实现 | github.com/qsppack/QSPPACK |
- 成功安装并运行 Quixer(按照第九章)
- 在
quixer_model.py中添加print语句,观察每个步骤的 Tensor Shape - 修改
run.py中的超参数(如减少 epochs 到 5,观察训练过程)
阶段 2:小改动
- 修改 LCU 系数初始化方式(如改为均匀分布)
- 改变 QSVT 多项式次数(改为 1 或 5,观察性能变化)
- 将
embedding_dimension从 512 改为 256,观察效果
阶段 3:组件替换
- 用不同的 PQC ansatz 替换 Ansatz 14
- 尝试不同的测量方案(如只测量 Z 期望值)
- 实现一个无需输出头的『纯量子』版本
阶段 4:新模型设计
- 参考 Quixer 框架设计你自己的量子 Transformer 变体
- 探索 LCU+QSVT 的组合在分类、序列标注等任务上的应用
- 研究混合量子-经典架构:用 Quixer 作为『注意力替代』插入经典 Transformer
14.5 追踪最新进展¶
由于该领域发展迅速,建议关注以下渠道:
- arXiv 量子物理学(quant-ph)和机器学习(cs.LG)子类别
- Quantinuum 官方博客及研究页面
- 量子机器学习会议:QML(Quantum Machine Learning)、QTML(Quantum Techniques in Machine Learning)
- ICAART(International Conference on Agents and Artificial Intelligence)— QMamba 发表于此
附录 A:术语对照表¶
| 英文 | 中文 | 说明 |
|---|---|---|
| Ansatz | 拟设 / 回路模板 | 参数化量子回路的固定结构 |
| Barren Plateau | 贫瘠高原 | 梯度指数衰减到零的现象 |
| Block Encoding | 块编码 | 用酉矩阵的子块表示任意矩阵 |
| Expectation Value | 期望值 | $\langle \psi \lvert O \rvert \psi \rangle$,可观测量的平均测量结果 |
| Feed-Forward Network (FFN) | 前馈网络 | 逐位置的经典非线性变换 |
| Linear Combination of Unitaries (LCU) | 线性酉组合 | $\sum_j b_j U_j$ |
| Pauli Matrices (X, Y, Z) | 泡利矩阵 | 基本量子门/可观测量 |
| Penn Treebank (PTB) | 宾州树库 | 经典语言建模基准数据集 |
| Perplexity (PPL) | 困惑度 | $\exp(\text{cross-entropy loss})$ |
| Postselection | 后选择 | 仅保留满足条件的测量结果 |
| Quantum Singular Value Transform (QSVT) | 量子奇异值变换 | 对块编码矩阵施加多项式变换 |
| SELECT-PREPARE | 选择-准备回路 | LCU 的标准回路实现 |
| Token Mixing | 令牌混合 | 不同令牌表示之间交换信息 |
| Unitary | 酉(矩阵/变换) | 满足 $U^\dagger U = I$ 的复矩阵 |
附录 B:关键公式汇总¶
| 公式 | 编号 | 说明 |
|---|---|---|
| $$\text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V$$ | (1) | 经典点积自注意力 |
| $$M = \sum_{j=0}^{n-1} b_j U_j$$ | (2) | LCU 令牌混合 |
| $$U_{\text{SEL}} = \sum_j |j\rangle\langle j| \otimes U_j$$ | (3) | SELECT 酉变换 |
| $$U_{\text{PREP}}|0\rangle = \sum_j a_j |j\rangle$$ | (4) | PREPARE 酉变换 |
| $$U_M = (U_{\text{PREP}}^\dagger \otimes I) U_{\text{SEL}} (U_{\text{PREP}} \otimes I)$$ | (5) | LCU 完整回路 |
| $$M = (\langle 0| \otimes I) U_M (|0\rangle \otimes I)$$ | (6) | 块编码投影 |
| $$P_c(M) = \sum_{k=0}^d c_k M^k$$ | (7) | QSVT 多项式变换 |
| $$|P_c(x)| \leq 1, \forall x \in [-1,1]$$ | (8) | QSVT 有界性约束 |
| $$\text{parity}(P_c) = d \bmod 2$$ | (9) | QSVT 奇偶性约束 |
附录 C:推荐阅读路径¶
如果你只想快速了解¶
- 第三章:Quixer 宏观概览(15 分钟)
- 第七章:Quixer 完整架构详解(40 分钟)
如果你想深入理解原理¶
- 第一章:GPT/Transformer 回顾
- 第四章:块编码
- 第五章:LCU
- 第六章:QSVT
- 第七章:完整架构
如果你想动手实现¶
- 直接跳到第九章:环境搭建与动手运行
- 遇到不理解的地方回查对应章节
- 配合各章节穿插的源码讲解阅读代码
如果你想做研究¶
- 完整阅读所有章节
- 仔细阅读原论文 arXiv:2406.04305
- 研究第十三章的开放问题,选择你感兴趣的方向
- 阅读 QMamba 论文了解最新进展
参考资料¶
- Khatri, N., Matos, G., Coopmans, L., & Clark, S. (2024). Quixer: A Quantum Transformer Model. arXiv:2406.04305. https://arxiv.org/abs/2406.04305
- Quixer 官方代码库. https://github.com/Quantinuum/Quixer
- Quantinuum Blog. "Announcing Quixer — Quantinuum's State-of-the-Art Quantum Transformer". https://www.quantinuum.com/blog/announcing-quixer
- Quantinuum Blog. "Our Hardware is Now Running Quantum Transformers". https://www.quantinuum.com/blog/our-hardware-is-now-running-quantum-transformers
- Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS 2017.
- Lee-Thorp, J., et al. (2022). FNet: Mixing Tokens with Fourier Transforms. NAACL 2022.
- Stenzel, L., & Kölle, M. (2025). QMamba: Quantum Selective State Space Models for Language Modeling. ICAART 2025.
- Sim, S., et al. (2019). Expressibility and entangling capability of parameterized quantum circuits for hybrid quantum-classical algorithms. arXiv:1905.10876. (Ansatz 14 的来源)
- Gilyén, A., et al. (2019). Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics. STOC 2019. (QSVT 的原始论文)
- Childs, A. M., & Wiebe, N. (2012). Hamiltonian simulation using linear combinations of unitary operations. QIC 2012. (LCU 的原始论文)
- Shah, J. (2024). A Comparative Study of Classical and Quantum Transformer Models and Their Applications [Bachelor's thesis, Aalto University]. Aaltodoc. https://aaltodoc.aalto.fi/server/api/core/items/a3998701-7b17-48e2-a74e-bcf87d927c26
- TorchQuantum. https://github.com/mit-han-lab/torchquantum
- PennyLane QSVT 文档. https://docs.pennylane.ai/en/stable/code/api/pennylane.qsvt.html
教程版本:v1.0(2026 年 5 月)
基于工作流:deep-research(94 个搜索/验证代理,1,928,809 tokens)
声明:本教程基于对 Quixer 论文、源代码和第三方资料的深入研究编写。所有核心声明均经过 3 票对抗验证(共验证 25 项声明,确认 13 项,否定 12 项)。教程中的代码解析基于 Apache 2.0 许可证下的官方开源实现。
致谢:感谢 Quantinuum 团队开源 Quixer 代码和数据,使社区能够学习、复现和在此基础上继续创新。