简介面向量子机器学习入门者与自然语言处理实践者的示例代码包围绕量子LSTM的实现与验证展开适合具备Python与深度学习基础、希望对比经典LSTM与量子循环网络效果的读者。压缩包共10个文件、约84KB以3个Python脚本和1个Jupyter Notebook为核心辅以2份依赖与说明文本、1份Markdown文档及3张训练过程图片覆盖量子线路定义、经典基线、词性标注实验与结果可视化等环节。已有423人学习下载。读者可借助PyTorch经典LSTM与PennyLane/Qiskit后端的两套实现理解量子LSTM的结构差异、训练流程与性能对比并通过现成的训练曲线图快速核对实验现象适合作为课程作业、课题预研或复现实验的起点。1. 量子 LSTM 不是噱头一份能跑通的 Jupyter Notebook 示例把「量子」两个字塞进 LSTM 的人不少真能在 Jupyter Notebook 里从头跑到尾、loss 确实往下掉的不多。这份 qlstm 示例做的事情很具体保留 LSTM 的遗忘门、输入门、输出门这套骨架只把最核心的那次[h_{t-1}, x_t]权重矩阵乘法换成一个含参变分量子线路用 n 个量子比特的 PauliZ 期望值作为输出再由一个经典全连接层升维回 4d 维门控。它适合两类人有 PyTorch 基础、想搞清楚量子层到底插在循环网络哪个位置的工程师以及带课程设计、需要一份可复现 demo 的同学。不适合指望它跑长序列大规模预测——在模拟器上4 到 8 个比特就是它的舒适区再往上时间成本会失控。2. 量子 cell 的内部结构4 个比特怎么替掉一个 4d 维权重矩阵传统 LSTM 的一个时间步本质是一次线性映射加四次激活把x_tm 维和h_{t-1}d 维拼成 md 维向量乘一个(md) × 4d的权重矩阵切成 i、f、o、g 四份前三份过 sigmoid、最后一份过 tanh再做逐元素的门控运算。QLSTM 并没有推翻这个结构它只替换了中间那一刀先把拼接向量压到 n 个角度送进量子线路演化测出 n 个期望值再用一个n × 4d的经典线性层还原成门控信号。这个替换值得做的理由有两个一个在参数效率一个在表达形式。参数效率上n 比特、L 层的线路可训练参数是 2nL 个n4、L2 时只有 16 个而同样把 md 映射到 4d 的经典矩阵在 d8 时就有上百个参数。表达形式上量子线路的状态演化被约束在酉群内可到达的函数空间与无约束的全连接层不同这正是很多人想验证的点。代价也很明确模拟器上每次前向都要做态矢量演化n 每加 1内存翻倍。2.1 前置降维层把 [x_t, h_{t-1}] 压成 n 个旋转角量子线路的输入不是向量是旋转角度。所以 cell 的第一件事是把 md 维的拼接向量映射到 n 维再做一次值域压缩。这一步在经典侧完成用一个nn.Linear(md, n_qubits)就够了关键是后面那个tanh(·) × π/2把任意实数压进[-π/2, π/2]。为什么非得压因为RY(θ)的周期是 2πRY(θ)和RY(θ2π)对应同一个量子态。如果直接把未归一化的特征当角度喂进去两个相差 2π 的输入会得到完全相同的线路输出模型在数值上就分不清它们。这类问题不会报错只会表现为 loss 降到一个平台后纹丝不动属于典型的玄学现场。缩放到[-π/2, π/2]还有一个好处这个区间内sin和cos的导数都不接近零梯度不容易被压死。这里有个隐含约束n 的取值要和后面的升维层配合。测量得到 n 个期望值最终要还原成 4d 维门控所以 n 太小时前置层就是一个严重的信息瓶颈i/f/o/g 四个门共享的信息被压得太狠n 太大时量子模拟器的时间又呈指数上升。经验做法是让 n 与 hidden_dim 同量级d4 配 n4d8 配 n6 到 8。2.2 数据重上传编码与变分 ansatz一层线路里塞两次非线性线路的排布是这份 notebook 里最值得看的部分。常见有两种写法一种是只在开头编码一次后面全是变分层另一种是每一层变分之前都重新编码一次输入也就是数据重上传data re-uploading。这份示例用的是后者结构是每层先做RY(x_i)和RZ(x_i²)编码再做RY(θ)和RZ(θ)变分旋转最后加一圈 CNOT 做纠缠。重上传的意义在于单比特上反复交替「编码 - 旋转 - 编码」能让一个比特表达的函数比单次编码复杂得多理论上限接近傅里叶级数的叠加。RZ(x_i²)这一项是个小技巧把输入的平方也作为一个频率分量相当于给线路一个非线性的入口。因为纯角度编码本质上还是线性的相位旋转非线性主要靠测量和后置经典层来补加一个平方项能让单层的表达能力上一个台阶。CNOT 用的是环形连接第 i 个比特控制第 i1 个最后一个连回第 0 个import pennylane as qml N_QUBITS 4 N_LAYERS 2 dev qml.device(default.qubit, wiresN_QUBITS) qml.qnode(dev, interfacetorch, diff_methodparameter-shift) def quantum_circuit(inputs, weights): # inputs: (N_QUBITS,) 已经缩放到 [-pi/2, pi/2] # weights: (N_LAYERS, N_QUBITS, 2) for layer in range(N_LAYERS): for i in range(N_QUBITS): qml.RY(inputs[i], wiresi) # 数据重上传每层重新编码 qml.RZ(inputs[i] ** 2, wiresi) # 平方项补非线性 for i in range(N_QUBITS): qml.RY(weights[layer, i, 0], wiresi) qml.RZ(weights[layer, i, 1], wiresi) for i in range(N_QUBITS): qml.CNOT(wires[i, (i 1) % N_QUBITS]) # 环形纠缠末位连回首位 return [qml.expval(qml.PauliZ(i)) for i in range(N_QUBITS)]逻辑说明qml.qnode装饰器把普通 Python 函数编译成可微的量子节点interfacetorch告诉它参数来自 PyTorch 张量diff_methodparameter-shift指定用参数移位法则算梯度。循环里weights[layer, i, 0]和[1]分别对应第 layer 层第 i 个比特的 RY 和 RZ 角度所以权重张量的形状必须严格是(N_LAYERS, N_QUBITS, 2)错一位就会在 CNOT 之后报索引越界。CNOT 用取模运算形成闭环好处是任意两个比特之间都有路径坏处是线路深度增加一层深线路下梯度衰减会更明显。参数说明N_QUBITS决定测量输出维度和模拟器开销N_LAYERS决定可训练参数量正好 2×N_QUBITS×N_LAYERS。环形 CNOT 可以换成全连接每对比特都加一个 CNOT表达力更强但深度是 O(n²)n4 时还能接受n8 就该慎重。2.3 测量、后置升维与门控拼接测量只取 PauliZ 的期望值值域天然落在[-1, 1]。这是一个很实用的选择期望值是解析计算的不带采样噪声梯度稳定如果用qml.sample或概率分布做输出就引入了 shot noise反向传播时梯度方差会明显变大训练曲线抖得没法看。拿到 n 个期望值之后后置层nn.Linear(n, 4d)负责把它还原成四个门。切分顺序必须和经典 LSTM 一致也就是i, f, o, g前三路走 sigmoid 压到 (0,1)最后一路走 tanh 压到 (-1,1)然后按c_t f·c_{t-1} i·g、h_t o·tanh(c_t)做更新。这里没什么量子特殊性纯粹是把经典公式抄对。值得留意的是期望值的统计特性。初始化时权重接近零线路接近恒等演化四个期望值都在 0 附近门控信号大约在 0.5此时 cell 的行为接近一个「什么都不记」的均值模型。这解释了一个常见现象训练最开始几百步 loss 几乎不动很多人以为量子层没起作用其实是初始化尺度选得让门控全开在中性位。把q_weights初始化在uniform(-0.1, 0.1)比标准正态更容易起步。2.4 和经典 LSTM 的参数量对照把两者的参数拆开看能更清楚这份示例的定位。下表按 input_dim1、hidden_dim4 估算只统计主干不含 bias组成经典 LSTMQLSTMn4, L2输入到门控(14)×16 80前置层 (14)×44 24核心变换已含在上行量子权重 2×4×2 16升维到门控无4×1616 80合计约 80约 120单看这个规模QLSTM 的参数并没有更少因为多了前置和后置两个经典层。它的价值不在省参数而在于中间那段变换的形式不同以及参数随比特数增长的方式不同——比特数从 4 加到 8量子部分只从 16 涨到 32而经典矩阵会按维度平方增长。真正要评估收益得在具体任务上做消融这一点放到最后一章讲。3. 在 Jupyter 里跑通第一遍从环境到 loss 曲线理论清楚之后剩下的就是让 notebook 的每个 cell 都能顺序执行。这一章按执行顺序给完整代码从依赖装起到画出第一条 loss 曲线。中间任何一步的输出形状不对后面的训练都会以奇怪的方式失败所以每一步都建议单独跑、单独看 shape。3.1 环境与依赖版本对不上的三个高频报错环境上最容易翻车的地方是三个包的版本互相咬合PyTorch、量子线路库、以及绘图用的 matplotlib。安装命令建议锁版本不要图省事直接装最新# 建议在独立虚拟环境里跑避免和其他 notebook 的 torch 版本冲突 python -m venv qlstm_env source qlstm_env/bin/activate # Windows 用 qlstm_env\Scripts\activate pip install torch2.0,2.4 pennylane0.34,0.38 matplotlib numpy逻辑说明把 torch 限制在 2.0 到 2.4 之间是因为量子线路库对 torch 的自动微分接口在不同大版本间有过调整跨过 2.4 之后部分interfacetorch的梯度路径会报符号解析错误。pennylane 限制在 0.34 到 0.38 之间是这一批 notebook 常用的 API 区间qml.qnode的装饰器参数在这些版本里保持一致。虚拟环境是必须的不要全局装。装完之后跑一段自检确认三件事torch 能反向传播、量子设备能创建、qnode 能对 torch 张量求导。这三条里任何一条不成立训练循环里都会以None梯度的形式炸出来import torch, pennylane as qml dev qml.device(default.qubit, wires2) qml.qnode(dev, interfacetorch, diff_methodparameter-shift) def probe(x, w): qml.RY(x, wires0) qml.RY(w, wires0) qml.CNOT(wires[0, 1]) return qml.expval(qml.PauliZ(1)) x torch.tensor(0.3, requires_gradTrue) w torch.tensor(0.7, requires_gradTrue) out probe(x, w) out.backward() print(out.item(), x.grad, w.grad) # 两个 grad 都不应是 None参数说明requires_gradTrue必须显式加在 torch 张量上如果传进去的是torch.tensor(0.3)这种默认不带梯度的标量backward()会静默返回 None。这一步看着琐碎但它是后面所有训练的基石。3.2 定义参数化量子线路与 QLSTM 主体线路部分在 2.2 已经给过这里补上外层的循环包装。整体是三层结构QLSTMCell管一个时间步QLSTM管整个序列的展开外面再套训练循环。这样拆的好处是想换线路只动 cell想换任务只动最外层。import math import torch import torch.nn as nn import pennylane as qml N_QUBITS, N_LAYERS 4, 2 dev qml.device(default.qubit, wiresN_QUBITS) qml.qnode(dev, interfacetorch, diff_methodparameter-shift) def quantum_circuit(inputs, weights): for layer in range(N_LAYERS): for i in range(N_QUBITS): qml.RY(inputs[i], wiresi) qml.RZ(inputs[i] ** 2, wiresi) for i in range(N_QUBITS): qml.RY(weights[layer, i, 0], wiresi) qml.RZ(weights[layer, i, 1], wiresi) for i in range(N_QUBITS): qml.CNOT(wires[i, (i 1) % N_QUBITS]) return [qml.expval(qml.PauliZ(i)) for i in range(N_QUBITS)] class QLSTMCell(nn.Module): def __init__(self, input_dim, hidden_dim, n_qubitsN_QUBITS, n_layersN_LAYERS): super().__init__() self.hidden_dim hidden_dim self.n_qubits n_qubits self.pre nn.Linear(input_dim hidden_dim, n_qubits) # 前置降维 self.q_weights nn.Parameter( torch.empty(n_layers, n_qubits, 2).uniform_(-0.1, 0.1) # 小尺度初始化 ) self.post nn.Linear(n_qubits, 4 * hidden_dim) # 升维到四个门 def forward(self, x_t, state): h_prev, c_prev state z torch.cat([x_t, h_prev], dim-1) # (batch, md) angles torch.tanh(self.pre(z)) * (math.pi / 2) # 压到 [-pi/2, pi/2] # 逐样本调用 qnodeparameter-shift 下最稳妥的写法 outs [torch.stack(quantum_circuit(angles[b], self.q_weights)) for b in range(angles.shape[0])] expvals torch.stack(outs) # (batch, n_qubits) gates self.post(expvals) # (batch, 4*hidden) i, f, o, g gates.chunk(4, dim-1) i, f, o torch.sigmoid(i), torch.sigmoid(f), torch.sigmoid(o) g torch.tanh(g) c_new f * c_prev i * g h_new o * torch.tanh(c_new) return h_new, c_new class QLSTM(nn.Module): def __init__(self, input_dim, hidden_dim): super().__init__() self.cell QLSTMCell(input_dim, hidden_dim) self.head nn.Linear(hidden_dim, 1) def forward(self, seq): batch, steps, _ seq.shape h torch.zeros(batch, self.cell.hidden_dim) c torch.zeros(batch, self.cell.hidden_dim) outputs [] for t in range(steps): h, c self.cell(seq[:, t, :], (h, c)) outputs.append(self.head(h).unsqueeze(1)) return torch.cat(outputs, dim1) # (batch, steps, 1)逻辑说明torch.stack(outs)把n_qubits个标量张量先拼成一个向量再对 batch 维拼成(batch, n_qubits)这一步的形状必须精确否则self.post会把 batch 维当成特征维结果是训练能跑但完全不收敛。gates.chunk(4, dim-1)切出的顺序是 i、f、o、g和标准 LSTM 公式一一对应切错顺序的典型症状是模型能记住却学不会长期依赖。参数说明input_dim是每个时间步的特征数单变量序列填 1hidden_dim是隐藏状态维度也是门控切分后的每份宽度d4 时post输出 16 维uniform_(-0.1, 0.1)的初始尺度直接影响训练前期的门控状态范围放大到 ±1 会让 sigmoid 早早饱和。QLSTM.forward里 h、c 的初始化用全零与经典实现一致。3.3 造数据、训练与看曲线任务用一个最小可验证的正弦序列的单步预测。输入长度 12预测下一个点数据量 200 条左右在笔记本上几分钟能跑完一轮。import numpy as np def make_loader(n_samples200, seq_len12, batch_size20): t np.linspace(0, 8 * np.pi, n_samples seq_len) series np.sin(t) xs [series[i:i seq_len] for i in range(n_samples)] ys [series[i 1:i seq_len 1] for i in range(n_samples)] xs torch.tensor(np.array(xs), dtypetorch.float32).unsqueeze(-1) ys torch.tensor(np.array(ys), dtypetorch.float32).unsqueeze(-1) ds torch.utils.data.TensorDataset(xs, ys) return torch.utils.data.DataLoader(ds, batch_sizebatch_size, shuffleTrue), ds torch.manual_seed(0) # 复现实验的前提 loader, dataset make_loader() model QLSTM(input_dim1, hidden_dim4) opt torch.optim.Adam(model.parameters(), lr0.05) sched torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max30) loss_fn nn.MSELoss() for epoch in range(30): model.train() total 0.0 for xb, yb in loader: opt.zero_grad() pred model(xb) loss loss_fn(pred[:, 1:, :], yb[:, 1:, :]) # 第一个点没有前一步跳过 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() total loss.item() * xb.size(0) sched.step() if epoch % 5 0: print(fepoch {epoch:03d} loss {total / len(dataset):.5f})逻辑说明pred[:, 1:, :]对yb[:, 1:, :]是刻意错开一位的因为第 0 个输出对应的隐藏状态还没吃到任何历史信息拿它算 loss 会拉高基线。clip_grad_norm_不是可选项parameter-shift 算出的梯度在某些初始参数上会突然放大不加裁剪训练曲线会出现单点尖刺然后整体发散。参数说明lr0.05配合余弦退火是这套小模型比较稳的组合batch_size20是在速度和梯度稳定性之间的折中减到 4 会让每个 epoch 慢得难以接受加到 64 又会让 parameter-shift 的梯度估计在小样本上偏噪。T_max30要和总 epoch 数一致否则学习率会在训练中途被退到接近零。跑通之后 loss 大致从 0.4 附近降到 0.01 量级曲线会有明显抖动这是逐样本调用 qnode 带来的批次内梯度不一致属于正常现象。如果三十轮之后还在 0.25 附近打转基本可以判定是角度没归一化或者线路参数没参与优化直接跳到第 5 章排查。4. 参数怎么设比特数、层数、学习率与梯度后端这份 notebook 里能改的超参数就那么几个但每一个的影响都不是线性的。比特数决定模拟开销的上限层数决定表达力和梯度可训练性之间的平衡梯度后端决定你能不能在笔记本上等到结果。这一章按影响从大到小排每个都给出可操作的判断标准。4.1 比特数与特征维度的对齐规则比特数 n 的第一个约束来自硬件无关的数学事实n 个比特的态矢量有 2^n 个复数振幅n4 是 16 个n10 就是 1024 个n20 时单精度下已经接近 GB 级。在default.qubit这种态矢量模拟器上n 超过 12 之后每个 epoch 的时间会从分钟级跳到小时级所以这个示例的定位就是 n ≤ 8。第二个约束来自信息流前置层要把 md 维压到 n 维n 小于 4 时信息瓶颈太窄四个门控共享同一组压缩信息模型连基本的记忆都学不稳。做对比实验时我一般这样配hidden_dim4 配 n4hidden_dim8 配 n6hidden_dim16 配 n8让 n 与 d 保持接近但略小的关系。这样做的好处是升维层nn.Linear(n, 4d)的参数不至于爆炸同时前置层的压缩比不会太极端。还有一个容易被忽略的点n 改变之后q_weights的形状变了已经训练好的权重不能直接迁移。做比特数对比实验时必须每组从头训练并且固定随机种子否则你比较的是初始化差异而不是比特数差异。4.2 层数的边际收益与贫瘠高原层数 L 是这个模型里最反直觉的参数。直觉上层数越多表达力越强实际在变分线路里L 从 2 加到 4 往往还能看到收益加到 8 之后训练难度会陡增loss 卡住不动梯度范数掉到 1e-6 以下参数几乎不再更新。这就是贫瘠高原barren plateau的典型表现——随着比特数和层数增加代价函数梯度的方差呈指数衰减随机初始化下梯度期望趋近于零。工程上的应对办法有三条都不复杂。第一控制总深度n4 时 L 不要超过 3n8 时不要超过 2把表达力的增长交给数据重上传的次数而不是单纯堆层。第二坚持小尺度初始化uniform(-0.1, 0.1)比标准正态更远离高原区因为初始线路接近恒等演化梯度信号更强。第三改用局部可观测量示例里每个比特单独测 PauliZ 已经是局部的如果换成全局的PauliZ(0) PauliZ(1) ...这类多体观测量梯度衰减会更快。判断是否已经进了高原区不用靠猜。训练开始前跑一次梯度检查看第一步的梯度范数量级model.zero_grad() xb, yb next(iter(loader)) loss loss_fn(model(xb)[:, 1:, :], yb[:, 1:, :]) loss.backward() total_norm sum(p.grad.norm() ** 2 for p in model.parameters() if p.grad is not None) ** 0.5 print(f首步梯度范数: {total_norm:.3e})参数说明这个范数在 1e-2 到 1e0 之间属于健康1e-4 以下就要警惕1e-6 以下基本可以确认进入高原。注意必须在zero_grad()之后、step()之前取取完再继续正常训练不要让它影响优化器状态。4.3 梯度方法parameter-shift、backprop 与 adjoint 怎么选diff_method是这份 notebook 里最影响运行时间的一个参数三种常见选择各有明确边界diff_method设备要求单步耗时相对适用场景parameter-shift任意设备含真实量子后端约 2×参数量 次前向默认选择通用性最好backprop支持自动微分的模拟器约 1 次前向只在模拟器上追求速度adjoint支持伴随求导的模拟器介于两者之间线路深、比特多、内存吃紧parameter-shift 的原理是把每个参数分别加上和减去 π/2 各跑一次线路用两次结果的差除以 2 作为该参数的梯度估计。所以参数有 16 个时一次反向传播要跑 32 次线路。这是它慢的根源也是它能在真实设备上用的原因——它不需要访问线路内部的态矢量。backprop 直接对模拟器的态矢量做反向传播速度快一个量级但只在模拟器上成立换到采样设备上立刻失效。如果你的 notebook 只打算在本地跑教学演示把diff_method改成backprop能让整个训练快好几倍前提是设备仍然是default.qubit。adjoint 则是线路很深时的省内存方案它不保存中间态反向时重算时间换空间。我的习惯是调试阶段用 backprop 快速迭代结构定稿前切回 parameter-shift 跑一遍完整结果确认两条路径给出的 loss 曲线趋势一致。不一致通常意味着线路里混入了不可微的操作比如中间用了qml.measure做条件分支。4.4 一张参数速查表把上面几节的结论集中成表方便对照着改参数建议起点调大的代价调小的代价n_qubits4模拟内存 2^n 增长时间随之上升前置层成瓶颈门控信息不足n_layers2参数翻倍梯度方差下降表达力不足loss 下不去hidden_dim4后置层参数 4d·n 线性增长记忆容量太小学不会长依赖lr0.03~0.05梯度噪声被放大曲线震荡发散收敛慢30 轮内看不到效果batch_size16~32逐样本循环epoch 时间线性增加批次梯度太噪更新方向不稳diff_methodparameter-shift慢 2×参数量 倍换 backprop 有设备限制5. 避坑与排查五个把 notebook 跑崩的现场下面这五条都是我在不同机器、不同版本上实际撞过的按现象、原因、解决三段写。前三条发生在线路和环境的层面特征是直接报错或者输出形状诡异后两条不发散也不报错只是白跑反而更难发现。5.1 环境与线路层三个一跑就报错的坑坑一backward()之后所有梯度都是 None。现象是训练循环能跑完但打印model.cell.q_weights.grad全是 Noneloss 三十年如一日。原因通常有两个qnode 的interface没写成torch或者传给 qnode 的inputs是 numpy 数组而不是 torch 张量。参数移位虽然不依赖自动微分框架但梯度要回传到 PyTorch 的计算图里接口声明必须对得上。解决办法是把装饰器固定成qml.qnode(dev, interfacetorch, diff_methodparameter-shift)并且在 cell 里保证angles是由 torch 张量经过torch.tanh得到的中间不要出现.numpy()或float()。坑二线路输出形状是(n_qubits,)却当成(batch, n_qubits)用。现象是self.post(expvals)之后gates.chunk(4, dim-1)报维度错误或者训练虽然能跑但 loss 完全不动。原因是漏了对 batch 维的那次torch.stack。quantum_circuit一次只处理一个样本返回的是长度 n 的列表必须先在列表层面torch.stack得到(n_qubits,)再在 batch 层面torch.stack得到(batch, n_qubits)。这两层 stack 少一层形状就会错位而且不一定立刻报错属于典型的黑匣子式故障。坑三换了个量子设备训练直接崩或者慢十倍。现象是把default.qubit换成带采样或带噪声的设备后要么diff_methodbackprop报不支持要么每个 epoch 从两分钟变成半小时。原因是不同设备支持的求导方式完全不同采样设备只能用 parameter-shift而且每次前向要跑 shots 次采样噪声和耗时同时上升。解决办法是在换设备的同时把diff_method改回parameter-shift把shots先设成 None 用解析期望值跑通确认逻辑无误后再加采样看噪声影响。5.2 训练与结果层两个「不报错但白跑」的坑坑四loss 卡在 0.25 附近期望值全部贴边到 ±1。现象是训练曲线前几轮下降很快然后死死贴在 0.25把四个期望值打印出来看几乎全是 0.999 或 -0.999。原因是角度没有归一化原始特征直接当成了RY的旋转角数值稍大就绕过了好几圈线路输出饱和。解决办法就是 2.1 里那个torch.tanh(self.pre(z)) * (math.pi / 2)把角度钳在[-π/2, π/2]。这个坑的血泪经验是它不会给你任何报错只会让你怀疑量子层没用然后把整个结构推翻重写白花两天。坑五两次运行结果差三成无法复现。现象是同一份 notebook 连跑两遍最终 loss 一个 0.012 一个 0.021曲线形状也不一样。原因有两个叠加没有固定随机种子以及q_weights用了默认初始化。nn.Parameter(torch.randn(...))的尺度是标准正态方差偏大初始门控容易饱和不同种子下差异被放大。解决办法是在 notebook 开头固定torch.manual_seed(0)和np.random.seed(0)并把量子权重改成uniform_(-0.1, 0.1)。做对比实验时这两条必须同时满足否则你比较的是初始化运气。排查顺序建议固定成先看梯度是否为 None再看期望值是否贴边再看首步梯度范数量级最后才怀疑数据。这套顺序能覆盖上面五条里的四条比漫无目的地调参快得多。6. 进阶验证怎么证明这条量子层真的在学东西loss 降下来不等于量子层起了作用。很可能整个模型的表现全部来自前置层、后置层和那个经典 head中间那段线路只是一个昂贵的恒等映射。要证明它不是摆设得做对照。6.1 三个必做的对照实验第一个是随机标签检验把训练集的 y 全部打乱再训练。如果模型在打乱标签上也能把 loss 压到很低说明它在背数据容量相对于数据量过大之前的「收敛」没有意义。正常的现象是打乱后 loss 稳定在方差量级下不去。第二个是经典消融把quantum_circuit换成一个参数量接近的经典线性层比如nn.Linear(n_qubits, n_qubits)加 tanh其余结构完全不动用同样的种子和轮数训练。两条曲线放一张图里对比才有说服力。如果经典版明显更好那这份示例的价值就是教学演示而非性能方案这个结论本身也值得写进报告。第三个是比特数扫描固定其他所有参数让 n 从 2 扫到 8记录最终 loss 和单 epoch 耗时。理想情况下 loss 随 n 上升而下降耗时随 n 指数上升。如果 loss 对 n 完全不敏感说明前置层已经把信息压没了量子层在空转。实验变量观察指标异常信号随机标签y 是否打乱收敛后的 loss打乱后仍能压低说明在背数据经典消融量子层替换同等轮数下 loss经典版碾压说明量子层无增益比特数扫描n_qubitsloss 与单 epoch 耗时loss 对 n 不敏感说明前置层瓶颈6.2 单批过拟合测试与梯度范数监控正式训练前我习惯先做一次单批过拟合取 8 条样本关掉 shuffle学习率调到 0.1跑 200 步看 loss 能不能压到 1e-3 以下。这个测试能在两分钟内回答一个关键问题——模型的容量和梯度路径是否通畅。压不下去问题一定在线路结构或梯度后端跟数据规模无关压得下去但全量训练不收敛问题才在正则化和数据分布上。xb, yb next(iter(loader)) xb, yb xb[:8], yb[:8] opt_fast torch.optim.Adam(model.parameters(), lr0.1) for step in range(200): opt_fast.zero_grad() loss loss_fn(model(xb)[:, 1:, :], yb[:, 1:, :]) loss.backward() opt_fast.step() if step % 40 0: print(fstep {step:03d} loss {loss.item():.6f})参数说明样本数取 8 是为了让 parameter-shift 的调用次数可控200 步大约几十秒lr0.1是刻意调大的过拟合测试要的是速度不是泛化判断阈值取 1e-3正弦这种光滑序列在 8 条样本上完全能达到这个量级。配合梯度范数监控一起看更准如果单批过拟合时梯度范数一直在 1e-3 以上且 loss 稳步下降说明线路是可训练的如果 loss 下降但梯度范数迅速掉到 1e-8说明模型进入了某个饱和区全量训练时大概率会卡住。从那以后我每次拿到一份量子机器学习 notebook都会先把 batch 砍到 8、训练集缩到 20 条看它能不能在 200 步内把 loss 压到 1e-3 以下——压不下去问题基本都在线路或梯度后端不在数据也不在超参数。这套流程帮我省下的时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取