简介这是一份围绕量子LSTM实现的示例代码包面向量子机器学习初学者、序列建模研究者以及希望对照经典LSTM复现实验的开发者重点解决量子循环网络入门与基线比较问题。压缩包共10个文件、约84KB以3个Python脚本和1个Jupyter Notebook为核心辅以2个依赖或说明文本、1份Markdown文档及3张训练结果图整体轻量便于快速浏览与二次修改。内容涵盖Pennylane、Qiskit等量子计算依赖示例、词性标注任务演示以及量子LSTM与PyTorch经典LSTM的训练过程对照Notebook适合交互式阅读图片可直观呈现训练曲线变化。已有423人学习下载可作为了解量子LSTM结构、依赖组织与实验对比思路的入门参考也方便读者在此基础上替换数据集或调整模型参数开展自己的小规模实验。1. 从一个跑不出 loss 曲线的 Notebook 说起qlstm 到底示例了什么前阵子有人丢过来一个压缩包说里面的 Notebook 一跑就停在第一个 cellloss 曲线怎么都不出来。打开一看是一份 qlstm 示例——他用参数化量子线路替换了经典 LSTM 里的一部分线性变换整套逻辑装在 Jupyter Notebook 里逐 cell 演示从编码、构造线路、封装单元到训练评估的完整链路。这类示例的价值不在于刷精度而在于把「量子线路怎么塞进循环神经网络的门控结构」这件事拆开给你看量子层负责特征变换经典层负责门控与状态更新两边靠一个可微的接口对接。适合已经会写 PyTorch 训练循环、想搞清楚混合量子-经典模型怎么落地的人也适合拿它当模板改自己时序任务的开发者。下面按我实际跑通和改过的顺序拆。2. 工程结构与技术选型Notebook 背后其实是一个五层调用栈2.1 目录与文件职责哪些是脚手架哪些是真东西这份示例解开之后并不复杂文件数量不多但职责切得很清楚。常见的组织方式是这样qlstm/ ├── qlstm_demo.ipynb # 主 Notebook全部演示流程 ├── requirements.txt # 依赖清单 ├── data/ │ └── series.csv # 可选示例用的时序数据 └── outputs/ ├── loss_curve.png # 训练曲线 └── pred_vs_true.png # 预测对比图主 Notebook 里通常按 cell 分四段环境与超参、量子线路定义、模型封装、训练与绘图。requirements.txt是关键因为量子框架和深度学习框架的版本耦合比较紧装错版本后面全是玄学报错。data/和outputs/属于可选项示例里如果没有现成数据文件Notebook 会用 NumPy 现场合成一段正弦叠加信号这样别人下载下来不用额外找数据集就能跑。我一般会做的第一件事是把 Notebook 里的超参 cell 单独拎出来看N_QUBITS、N_LAYERS、HIDDEN_DIM、SEQ_LEN、BATCH_SIZE、EPOCHS、LR这几个变量决定了后面所有张量的形状。先把这几个数抄到纸上再往下读能省掉大量「这个维度从哪来的」的困惑。再补一句实际经验不要在 Notebook 里直接pip install。很多人图省事在第一个 cell 写!pip install pennylane结果换台机器或者换内核就装到别的环境去了报错信息还指向一个你根本没装包的 Python。这属于典型的自找麻烦老老实实用requirements.txt建虚拟环境。2.2 技术选型为什么选 PennyLane 的 TorchLayer 而不是手写参数移位混合量子-经典模型的核心难点是梯度。量子线路里的参数更新需要梯度而梯度要穿过经典网络回传。常见三条路第一条是用 TensorFlow Quantum功能全但依赖重TensorFlow 版本一换就容易出问题Notebook 演示场景下不划算。第二条是手写参数移位规则parameter-shift rule自己算线路对每个参数的偏导能跑但是要把量子线路的参数逐个拆开求导代码量翻倍还容易在批量维度上出错。第三条是用 PennyLane 的qml.qnn.TorchLayer把 QNode 直接包成一个nn.Module梯度交给 PyTorch 的自动微分处理在default.qubit这类模拟器上走backprop求导速度也够。这份示例走的是第三条。好处很直接量子层在代码里的存在感和一个nn.Linear差不多你可以把它塞进任何经典结构里调试方式、优化器、学习率调度全都复用 PyTorch 那一套。代价是它绑定了模拟器后端真要上真实量子设备diff_method得换回parameter-shift训练速度会掉一个数量级——这一点心里要有数。选型上还有一个隐含决定qubit 数量压在 4 个左右。原因是状态向量模拟的显存和计算量随 qubit 数指数涨4 qubit 的状态向量只有 16 个复数跑起来毫无压力上到 12 qubit 以上Notebook 里一个 epoch 就能让你等得想砸键盘。示例选小规模不是能力限制是演示节奏的需要。2.3 环境准备一次装好并固定随机种子依赖装法python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install --upgrade pip pip install pennylane torch numpy matplotlib这四五个包就够了。torch装 CPU 版即可量子模拟器本身也不吃 GPU。装完之后在 Notebook 第一个 cell 里固定种子import random import numpy as np import torch import pennylane as qml SEED 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) # 量子线路参数初始化也走 torch 的随机源保持一致 print(pennylane:, qml.__version__) print(torch:, torch.__version__)固定种子的意义在于量子线路的初始参数是随机采样的不固定的话每次重跑 loss 起点都不一样你没法判断某次改动是真有效还是碰巧。我在 Notebook 里习惯把种子设置和版本打印放在同一个 cell这样别人复现出问题时截图里就带着版本信息省一轮来回问。qml.__version__那行别省。不同大版本的TorchLayer在权重形状约定上改过报错信息又很隐晦先把版本亮出来排查时能少走弯路。3. 量子 LSTM 单元拆解一个时间步里数据怎么流3.1 角度编码把拼接后的特征压到 4 个量子比特经典 LSTM 的第一步是把当前输入x_t和上一时刻隐状态h_{t-1}拼起来做一次线性变换得到四个门的前激活值。这份示例在这一步插入了量子线路先用一个经典线性层把拼接向量降到N_QUBITS维再用角度编码把这几个数映射成量子比特的旋转角。import torch import torch.nn as nn import pennylane as qml N_QUBITS 4 N_LAYERS 2 # 状态向量模拟器4 个 qubit 只有 16 个复数跑起来很轻 dev qml.device(default.qubit, wiresN_QUBITS) qml.qnode(dev, interfacetorch, diff_methodbackprop) def quantum_circuit(inputs, weights): # 角度编码每个特征值映射成一个 Y 轴旋转角 qml.AngleEmbedding(inputs, wiresrange(N_QUBITS), rotationY) # 变分线路带纠缠的强纠缠层 qml.StronglyEntanglingLayers(weights, wiresrange(N_QUBITS)) # 逐比特测量 Z 方向期望值输出范围 [-1, 1] return [qml.expval(qml.PauliZ(i)) for i in range(N_QUBITS)]这里几个参数要讲清楚。wiresN_QUBITS决定线路宽度也就是能编码多少个特征rotationY表示用 Ry 门做编码输入值直接当旋转角用。角度编码有个硬约束输入必须落在合理区间否则角度会绕圈不同的输入可能映射到同一个量子态上特征就撞车了。所以pre那个线性层后面通常接一个tanh再乘π/2把值压到(-π/2, π/2)。interfacetorch告诉 PennyLane 这个 QNode 的输入输出都是 torch 张量diff_methodbackprop表示用反向传播求导这是状态向量模拟器上最快的选择。换成真实设备或某些采样后端时这一项必须改成parameter-shift否则直接报错。3.2 变分线路与测量ansatz 的参数量是怎么算出来的StronglyEntanglingLayers是 PennyLane 内置的一个常用 ansatz结构是「旋转层 纠缠层」交替堆叠。它的权重形状是固定的三元组# 形状约定(层数, 比特数, 3) # 每层每个比特有 3 个旋转角Rot 门层间用 CNOT 环形连接 weight_shape {weights: (N_LAYERS, N_QUBITS, 3)} print(weight_shape) # {weights: (2, 4, 3)}参数量就是2 × 4 × 3 24个。这个数字很关键它决定了量子部分的可训练容量。N_LAYERS加大表达能力变强但训练也更难收敛N_LAYERS1时纠缠只发生一次很多时候退化成近似线性的变换量子带来的非线性优势体现不出来。测量部分返回N_QUBITS个 PauliZ 期望值每个值理论上落在[-1, 1]。这一点直接影响后面门控的设计期望值天然有界不需要额外加激活函数去截断但也意味着信息被压在一个较小的动态范围里如果四个门都用同一批期望值线性变换出来容易出现「门开合幅度不够」的问题。我的处理办法是给每个门单独一个线性头让不同门从同一组期望值里抽取不同的线性组合。顺带说一个容易忽略的点StronglyEntanglingLayers要求比特数至少为 2而且当比特数是奇数时最后一层的纠缠模式会和偶数时不同。示例选 4 是安全值你改成 3 或 5 的时候别指望行为完全一致得重新调。3.3 门控融合量子层输出接回输入门、遗忘门、候选态和输出门真正把量子层和 LSTM 缝在一起的是下面这个单元类。思路是量子层输出N_QUBITS维向量用一个线性层升到4 × hidden_dim然后按 LSTM 的标准切法拆成四个门。class QuantumLSTMCell(nn.Module): def __init__(self, input_dim, hidden_dim, n_qubits4, n_layers2): super().__init__() self.hidden_dim hidden_dim # 经典降维把 x_t 和 h_{t-1} 拼起来压到量子比特数 self.pre nn.Linear(input_dim hidden_dim, n_qubits) # 量子层权重形状必须和 ansatz 约定一致 self.qlayer qml.qnn.TorchLayer( quantum_circuit, {weights: (n_layers, n_qubits, 3)} ) # 经典升维量子测量结果 - 四个门 self.post nn.Linear(n_qubits, 4 * hidden_dim) def forward(self, x, state): h_prev, c_prev state # 拼接 压缩 限幅到角度编码的安全区间 z torch.cat([x, h_prev], dim-1) z torch.tanh(self.pre(z)) * (torch.pi / 2) # 过量子线路得到 n_qubits 个期望值 q_out self.qlayer(z) # 升维并切成 i / f / g / o 四个门 gates self.post(q_out) i, f, g, o gates.chunk(4, dim-1) i torch.sigmoid(i) # 输入门 f torch.sigmoid(f) # 遗忘门 o torch.sigmoid(o) # 输出门 g torch.tanh(g) # 候选态 c_new f * c_prev i * g h_new o * torch.tanh(c_new) return h_new, (h_new, c_new)逐段说明。self.pre的输出维度是n_qubits而不是hidden_dim这是整个设计里最容易改错的地方——很多人下意识把它写成hidden_dim然后在qlayer那里拿到形状不匹配的报错。torch.tanh(...) * (torch.pi / 2)这一行是角度编码的限幅把值约束在(-π/2, π/2)避免角度绕圈。qml.qnn.TorchLayer的第二个参数是权重形状字典键名必须和 QNode 的形参名weights对上。这里如果写成(n_qubits, n_layers, 3)运行时会报维度错误而且报错信息不会告诉你是顺序反了得自己对着文档核。gates.chunk(4, dim-1)按最后一维均分四份每份hidden_dim。注意 chunk 的顺序就是 i、f、g、o和经典 LSTM 的约定一致不要自己调换否则模型还是能训但语义完全变了调试时会怀疑人生。self.post是一个纯经典线性层。有人会问既然量子层都上了为什么还要经典层把它升回去因为量子测量只给了n_qubits维信息而门控需要4 × hidden_dim维中间的映射必须由经典层承担。这也说明一个事实这份示例里量子部分承担的是特征变换不是参数量的替代品别指望用量子层减少可训练参数。3.4 时序封装与形状核算batch、seq_len、hidden 三个维度的对齐单元写好了还得在外面套一层按时间步展开的循环。这部分没有任何量子成分就是标准的 PyTorch 写法class QuantumLSTM(nn.Module): def __init__(self, input_dim, hidden_dim, n_qubits4, n_layers2): super().__init__() self.hidden_dim hidden_dim self.cell QuantumLSTMCell(input_dim, hidden_dim, n_qubits, n_layers) self.out nn.Linear(hidden_dim, 1) # 回归到单值预测 def forward(self, x): # x: (batch, seq_len, input_dim) batch, seq_len, _ x.shape h torch.zeros(batch, self.hidden_dim, dtypex.dtype) c torch.zeros(batch, self.hidden_dim, dtypex.dtype) for t in range(seq_len): h, (h, c) self.cell(x[:, t, :], (h, c)) return self.out(h).squeeze(-1) # (batch,)形状链条是这样走的输入(batch, seq_len, input_dim)切时间步后每个x[:, t, :]是(batch, input_dim)pre把它和(batch, hidden_dim)的h拼成(batch, input_dim hidden_dim)降到(batch, n_qubits)量子层逐样本处理输出还是(batch, n_qubits)post升到(batch, 4 * hidden_dim)切完门控c_new和h_new都是(batch, hidden_dim)。有两个细节值得强调。第一torch.zeros那里显式写了dtypex.dtype因为TorchLayer内部对精度敏感如果输入是float32而初始状态是float64拼接时就会报 dtype 不一致这个错在 Notebook 里出现频率极高。第二时间步循环是 Python 的for没有并行化seq_len一大训练就慢。示例里seq_len一般设在 20 到 50 之间够演示又不至于等太久。4. 训练与验证在 Notebook 里把 loss 曲线跑下来4.1 造一份可控的时序数据先让经典模型能学会示例不依赖外部数据集用合成信号更可控。我的做法是叠两个不同频率的正弦再加一点噪声用滑窗切样本import numpy as np import torch def make_series(n1200, noise0.05, seed0): rng np.random.default_rng(seed) t np.arange(n) # 两个频率叠加保证模型必须记住一定历史才能预测准 s np.sin(2 * np.pi * t / 40) 0.5 * np.sin(2 * np.pi * t / 13) s s rng.normal(0, noise, sizen) return s.astype(np.float32) def make_windows(series, seq_len32): xs, ys [], [] for i in range(len(series) - seq_len): xs.append(series[i:i seq_len]) ys.append(series[i seq_len]) x torch.from_numpy(np.stack(xs)).unsqueeze(-1) # (N, seq_len, 1) y torch.from_numpy(np.stack(ys)) # (N,) return x, y series make_series() x, y make_windows(series, seq_len32) split int(0.8 * len(x)) x_train, y_train x[:split], y[:split] x_val, y_val x[split:], y[split:] print(x_train.shape, y_train.shape)seq_len32意味着每个样本看 32 个历史点预测第 33 个。input_dim1因为序列是单变量。这段数据的周期是 40 和 13模型必须同时抓住这两个尺度才能把误差压低正好用来检验隐状态是否真的记住了历史。关键的一步是先跑一个纯经典 LSTM 做基线。如果经典模型都学不动说明数据切法或者学习率有问题这时候去调量子部分纯属浪费。我在 Notebook 里会保留一个ClassicLSTM的 cell训练两三个 epoch 看 loss 能不能降到 0.1 以下能降再换量子版。4.2 训练循环梯度怎么穿过量子线路回来训练循环和普通 PyTorch 没区别这是选TorchLayer带来的最大便利import torch.nn as nn from torch.optim import Adam model QuantumLSTM(input_dim1, hidden_dim16, n_qubits4, n_layers2) opt Adam(model.parameters(), lr5e-3) loss_fn nn.MSELoss() train_losses, val_losses [], [] for epoch in range(30): model.train() opt.zero_grad() pred model(x_train) loss loss_fn(pred, y_train) loss.backward() # 梯度穿过量子层回传到 pre / weights / post opt.step() train_losses.append(loss.item()) model.eval() with torch.no_grad(): val_loss loss_fn(model(x_val), y_val).item() val_losses.append(val_loss) if epoch % 5 0: print(fepoch {epoch:3d} | train {loss.item():.4f} | val {val_loss:.4f})loss.backward()这一行是整个示例的技术核心。它之所以能穿过量子线路是因为diff_methodbackprop让 QNode 在状态向量模拟器上以普通可微计算图的形式存在PyTorch 的 autograd 可以直接对它求导。如果后端换成采样设备这一行会抛异常提示你改求导方式。学习率我一般从5e-3起量子层的参数初始值在[-0.1, 0.1]附近梯度本身不大学习率太小学不动太大又会在初期震荡。hidden_dim16是个折中太小记不住 32 步的历史太大经典层参数量上来量子层那点表达能力又被淹没看不出量子到底起了什么作用。训练轮数不用多。这类合成数据 20 到 30 个 epoch 就能看出趋势如果 loss 一直卡在某个值不动八成不是轮数不够而是学习率、限幅或者量子层宽度出了问题继续加 epoch 只是浪费时间。4.3 结果读取与 Notebook 执行顺序训练完之后画两条曲线训练集和验证集放一张图import matplotlib.pyplot as plt plt.figure(figsize(7, 4)) plt.plot(train_losses, labeltrain) plt.plot(val_losses, labelval) plt.xlabel(epoch) plt.ylabel(MSE) plt.legend() plt.tight_layout() plt.savefig(outputs/loss_curve.png, dpi150)判断标准很简单验证 loss 跟着训练 loss 一起往下走说明泛化正常验证 loss 早早翘头说明hidden_dim太大或者seq_len太长模型在背数据。这份示例的定位是演示σ 到 0.05 量级的噪声下验证 MSE 能压到 0.02 以内就算跑通了。Notebook 的执行顺序是另一个隐形成本。Jupyter 允许你乱序执行 cell定义模型的 cell 跑两次第二次会把model变量重新绑定到一个新实例上但优化器可能还指着旧参数训练时看起来在跑实际什么都没更新。我的血泪经验是改完任何 cell先Kernel → Restart Run All宁可多等三十秒也不要在一堆残留状态上做判断。图表和图例里出现的 loss 数值只有在顺序执行的前提下才有意义。5. 避坑与排查五处最容易翻车的地方5.1 报 dtype 不一致float32 与 float64 打架现象跑到torch.cat([x, h_prev], dim-1)或者qlayer输入那一步报错信息大意是两个张量 dtype 不匹配一个是Float一个是Double。原因TorchLayer内部默认按float64构造权重而torch.zeros不给dtype时默认float32或者反过来数据用 NumPy 生成的是float64模型权重是float32。量子框架对精度比纯经典模型敏感混着用就炸。解决统一在入口处做一次转换。数据侧torch.from_numpy(arr).float()初始状态torch.zeros(batch, hidden, dtypex.dtype)模型整体.to(torch.float32)。我习惯在数据生成函数里就.astype(np.float32)从源头掐断。5.2 权重形状写反报错指向一个不相干的行现象构造TorchLayer之后第一次前向就报维度错误报错位置在qml.StronglyEntanglingLayers内部看起来像是 ansatz 的问题。原因权重形状字典写成了(n_qubits, n_layers, 3)或者(n_layers, 3, n_qubits)。StronglyEntanglingLayers只认(层数, 比特数, 3)顺序反了不会提前提示而是在展开旋转门的时候才暴露。解决把形状单独打印出来核对一遍print({weights: (N_LAYERS, N_QUBITS, 3)})确认第一个维度是层数。如果你改过N_QUBITS或N_LAYERS这两处要同步改别只改一处。5.3 Notebook 重复执行模型参数被静默重置现象loss 曲线每次跑出来的起点都不一样明明固定了种子或者训练了三十轮参数看起来根本没变。原因定义模型和优化器的 cell 被重复执行过。第二次执行model QuantumLSTM(...)会创建一个全新实例而opt如果也重新建了倒是没事怕的是只重跑了模型 cell 没重跑优化器 cell优化器手里攥着旧参数opt.step()更新的是一批已经不在计算图里的张量。解决每次改动后 Restart Run All。想在 Notebook 里做增量实验就把「建模 优化器 训练」打包成一个函数调用一次生成一套全新状态避免依赖 cell 的执行历史。5.4 换成真实后端后训练直接崩现象把default.qubit换成别的后端loss.backward()抛异常提示不支持反向传播。原因diff_methodbackprop只在状态向量模拟器上可用。采样型后端拿不到解析梯度必须走参数移位规则。解决改qml.qnode(dev, interfacetorch, diff_methodparameter-shift)同时在训练循环里把 batch 调小因为参数移位对每个参数要做两次额外线路评估耗时随参数量线性增长。4 qubit 24 个参数还能忍参数一多就得考虑分组或者换回模拟器。5.5 loss 卡住不动不是学习率的问题现象训练几十轮loss 一直在 0.3 附近小幅抖动看不出下降趋势。原因两个可能。一是pre层后面没做角度限幅输入值超出(-π/2, π/2)编码后量子态高度重合量子层输出几乎是常数梯度接近零。二是N_LAYERS1且N_QUBITS太小线路表达能力不足以区分不同输入。解决先检查限幅那行torch.tanh(self.pre(z)) * (torch.pi / 2)有没有被误删。确认没问题后把N_LAYERS从 1 加到 2观察 loss 是否开始下降。还不行就临时把量子层换成nn.Linear(n_qubits, n_qubits)做个对照实验如果经典替换版能训而量子版不能问题就锁定在线路本身。6. 二次开发三种改法与一套验证习惯想在这份示例上做二次开发改动点其实集中在三个地方风险各不相同。改造点改哪里影响风险换编码方式quantum_circuit里的AngleEmbedding特征表达能力振幅编码需要2^n维输入pre层要跟着改换 ansatzStronglyEntanglingLayers换成自定义层参数量与纠缠模式权重形状字典必须同步重算改门控结构QuantumLSTMCell.forward的门计算模型语义改完等于换了个模型旧超参全部失效换编码是最容易上手的。把角度编码换成振幅编码代码只需要几行qml.qnode(dev, interfacetorch, diff_methodbackprop) def quantum_circuit_amp(inputs, weights): # 振幅编码要求输入是 2^n 维且模长为 1这里用 L2 归一化兜底 norm torch.sqrt(torch.sum(inputs ** 2)) 1e-8 qml.AmplitudeEmbedding(inputs / norm, wiresrange(N_QUBITS), normalizeTrue) qml.StronglyEntanglingLayers(weights, wiresrange(N_QUBITS)) return [qml.expval(qml.PauliZ(i)) for i in range(N_QUBITS)]注意pre层的输出维度得从n_qubits改为2 ** n_qubits4 qubit 就是 16 维。编码维度一变pre的参数量翻了四倍训练时间也跟着涨这一层联动关系在改之前就要想清楚。门控结构的改法更激进一些。经典 LSTM 的遗忘门是sigmoid(线性变换)有人会把它换成量子态的保真度或者某个测量概率这样门的开合来自量子线路本身而不是经典层。这么改之后模型语义变了之前调好的学习率、hidden_dim基本都要重调。我的建议是先在纸上把新的门公式写出来确认梯度路径还是通的再动代码。验证习惯这块我踩过的坑值得说一下。第一次改 ansatz 的时候我把层数加到 4训练 loss 确实降得比原来快当时以为找到了甜点结果换了一份数据完全不复现。后来才想明白那次对比没有固定种子也没有跑经典基线。从那以后我每次改量子部分都强制走一遍三步——固定种子重启内核全量执行、同参数跑一次经典 LSTM 基线、同一份数据上跑三次取均值。多花十分钟省掉的是整整一周的自欺欺人。量子模型的效果波动本来就比经典模型大参数初始化、线路结构、编码方式任何一处动了结果都可能天差地别。别指望一次实验就下结论也别因为某次 loss 好看就急着写总结。希望这份拆解能帮你把 Notebook 顺利跑起来少走几步弯路。本文还有配套的精品资源点击获取