简介qlstm 示例包含一套完整的量子 LSTM长短期记忆网络实验代码面向希望入门量子机器学习或对比量子/经典循环网络的算法研究者与学生。压缩包内提供基于 PennyLane 的量子 LSTM 模型、基于 PyTorch 的经典 LSTM 对比实现以及用于词性标注任务的 Jupyter Notebook 演示可运行后直接查看经典与量子模型在相同超参数下的训练损失曲线图直观对比两者的收敛特征。除核心模型外还附带 README.md 说明、requirements.txt 依赖清单和下载/预处理辅助脚本帮助快速搭建环境、复现实验并开展参数调整。整体为 zip 包共 10 个文件以 Python 源码、Notebook、PNG 图片、Markdown 和 TXT 文档为主压缩包仅 84KB轻量而完整。已有 423 人浏览学习适合作为量子 LSTM 研究的起步模板、课题对比基线或课程设计参考。1. qlstm量子LSTM的示例项目不只是把“LSTM”前面加个“量子”第一次拆这个qlstm示例时我本以为它就是把经典LSTM里的矩阵乘法换成量子线路——毕竟公开代码里这类“量子版XX”大多是借了个概念做个demo。但翻完Jupyter Notebook里的实现才发现它真正做的是把LSTM的门控机制整体映射到参数化量子电路PQC上用测量期望值替代门控向量的计算再用经典层包一层输出。这个思路对做时序预测、NLP特征提取的从业者来说价值在于你可以直接看到量子LSTM和经典LSTM在结构上的对应关系而不是被“量子优势”这种大词带着走。这份资源适合两类人一是已经跑过经典LSTM、想低成本入门量子机器学习怎么落地的人二是需要在教学或技术方案里快速搭一个可演示的量子时序模型、但不想从零写线路的人。它能帮你解决的核心问题是量子门控单元到底怎么构建、怎么训练、以及在模拟器上跑通一个最小闭环需要哪些设计决策。2. 量子LSTM的门控映射把遗忘门、输入门、输出门换成测量期望值2.1 经典LSTM的门控为什么能被量子电路替代经典LSTM的核心计算是三个门控向量遗忘门f_t σ(W_f · [h_{t-1}, x_t] b_f)输入门i_t σ(W_i · [h_{t-1}, x_t] b_i)输出门o_t σ(W_o · [h_{t-1}, x_t] b_o)这些门控本质上是对拼接后的向量做线性变换再加σ激活。量子电路里的参数化线路也能做类似的事把x_t和h_{t-1}编码成量子态经过一组带旋转角参数的量子门最后测量某个泡利算符的期望值。期望值落在[-1, 1]区间经过平移缩放后和σ的输出范围[0, 1]非常接近。这个映射关系是这个qlstm示例的立身之本。具体到这个项目的设计选择门控单元用了角度编码angle encoding因为输入特征是几维到几十维的中低维数据角度编码线路深度相对浅模拟器上跑得动。如果是高维数据或者图像块序列一般会考虑幅度编码但那个线路深度和归一化要求高很多不适合做入门示例。2.2 Notebook里的量子门控单元是怎么构建的拆开这个qlstm的Notebook核心组件是一个自定义的量子门控层。它没有用完整的量子LSTM论文里的复杂结构而是做了工程化简化import pennylane as qml import torch import torch.nn as nn n_qubits 4 # 量子线路的比特数对应输入维度的一部分映射 n_qlayers 2 # 参数化量子电路的层数增加表达力 q_depth 6 # 每层内的门深度控制纠缠能力 n_gates 3 # LSTM三个门遗忘、输入、输出 dev qml.device(default.qubit, wiresn_qubits) qml.qnode(dev, interfacetorch, diff_methodadjoint) def quantum_gate(inputs, weights): # 角度编码把输入向量的每个分量旋转到对应比特上 for i in range(n_qubits): qml.RY(inputs[i], wiresi) # 参数化纠缠层 for layer in range(n_qlayers): for i in range(n_qubits - 1): qml.CNOT(wires[i, i 1]) for i in range(n_qubits): qml.RY(weights[layer, i], wiresi) # 测量前两个比特的期望值作为门控信号的原始值 return [qml.expval(qml.PauliZ(i)) for i in range(2)]这段代码逻辑上很清晰先把输入向量约束到4维因为线路固定为4比特用RY门做角度编码然后用CNOT搭纠缠、用可训练的RY参数做变换最后测量两个比特的Z期望值作为门控计算的中间量。需要注意几点输入为什么是4维量子线路的比特数决定了一次能编码的特征维度。示例里取4是因为mnist这类简单时序输入降维后4维够用。如果你的特征是10维可以把n_qubits调到10或者保留4比特、分批编码。为什么用adjoint微分而不是parameter-shiftadjoint方法在模拟器上计算梯度更快但显存占用更高。你如果在自己的机器上爆显存可以换成diff_methodparameter-shift速度慢一些但更稳。CNOT连法用了线性链i到i1这是最保守的纠缠拓扑。你可以在不改变整体结构的情况下换成环形连接最后一比特和第一比特也做CNOT有些任务上效果会好一点。2.3 三个门的参数是共享还是独享这是我看这个示例时比较关注的设计点门控信号从量子线路里出来之后要不要给三个门各配一套量子线路参数。class QLSTMCell(nn.Module): def __init__(self, input_size, hidden_size, n_qubits4): super().__init__() self.input_size input_size self.hidden_size hidden_size # 三个门共享同一个量子线路骨架用不同权重实例化 self.q_weights_f nn.Parameter(torch.randn(n_qlayers, n_qubits) * 0.1) self.q_weights_i nn.Parameter(torch.randn(n_qlayers, n_qubits) * 0.1) self.q_weights_o nn.Parameter(torch.randn(n_qlayers, n_qubits) * 0.1) # 经典输出层 self.out_layer nn.Linear(hidden_size, hidden_size) def forward(self, x, h_prev, c_prev): # x 经过线性变换到4维拼接 h_prev 后进入量子线路 combined torch.cat((x, h_prev), dim-1) combined self.input_proj(combined) # 降维到 n_qubits 维度 f_gate torch.sigmoid(quantum_gate(combined, self.q_weights_f)) i_gate torch.sigmoid(quantum_gate(combined, self.q_weights_i)) o_gate torch.sigmoid(quantum_gate(combined, self.q_weights_o)) c_new f_gate * c_prev i_gate * torch.tanh(self.candidate_layer(combined)) h_new o_gate * torch.tanh(c_new) return h_new, c_new这里的关键决策是三个门各配一套量子权重而不是共享一套再线性变换。原因是纠缠线路的输入输出关系高度非线性三个门如果共享参数梯度更新时彼此干扰会很大训练反而更难收敛。代价是参数数量变成三倍但在这个规模下2层×4比特总共才24个量子参数远小于经典LSTM的矩阵参数量。2.4 为什么测量结果要接sigmoid而不是直接用量子线路输出的期望值在[-1, 1]之间如果你直接把它当代价函数的输入数值范围还凑合但作为LSTM的门控值负值会让细胞状态出现不稳定衰减。示例里对所有量子输出做了一个简单的仿射变换再接sigmoid相当于把期望值从[-1, 1]映射到[0, 1]def quantum_gate_activated(inputs, weights): raw quantum_gate(inputs, weights) # 期望值在 [-1, 1] # 平移缩放把值域映射到 [0, 1] 附近sigmoid 再压缩 scaled 0.5 * (raw 1) return torch.sigmoid(scaled)不要小看这一步如果你直接把原始期望值灌给细胞状态更新公式C经过几轮迭代后符号会来回翻转loss曲线直接发散。这也是很多复现量子LSTM翻车的第一大原因。3. 从Notebook到可训练模型数据编码、降维与梯度计算3.1 输入数据怎么进量子线路从2.2节的代码可以看出量子线路接收的是一个n_qubits维的向量。但在实际时序任务里输入特征通常是几十维比如经济指标、传感器读数、词向量片段。这个qlstm示例的做法是先加一层经典线性降维再接量子线路self.input_proj nn.Linear(input_size hidden_size, n_qubits)也就是把上一时刻的隐藏状态h_{t-1}和当前输入x_t拼接成一个向量然后线性压缩到4维。这个设计的合理性在于角度编码线路的表达能力受比特数限制特征维度超过比特数时就只能丢弃信息第一次做线性压缩等效于一个可训练的“特征筛选器”。这里要留意的是降维层和量子线路之间的梯度流是连续的因为降维层是经典线性层反向传播梯度可以从量子节点的输出一路传回降维层的权重。这是Pennylane的Torch接口自动处理的你不需要手工写梯度传播逻辑。3.2 参数初始化的玄学量子线路的旋转角权重如果初始值设得太大比如N(0, 1)初始测量期望值会分布得很散经过sigmoid后门控值饱和在0或1附近梯度直接消失。这个qlstm示例里用了0.1倍缩放初始化这是我在实际试跑后认为最有价值的细节之一。self.q_weights_f nn.Parameter(torch.randn(n_qlayers, n_qubits) * 0.1)原因参数化量子电路的旋转角权重直接决定线路输出的梯度幅度。初始权重过大线路工作在非线性的饱和区梯度趋近于0初始权重过小输出接近某个固定值同样梯度微弱。0.1这个量级让初始期望值大致分布在[-0.3, 0.3]区间恰好处于敏感区。3.3 batch训练时的量子线路状态管理用Jupyter Notebook跑实验时有一个容易被忽略的坑Pennylane的QNode默认是“每次调用重新构建线路”所以单个样本的推理没有问题。但如果你按batch方式训练比如一个batch 32条序列你不能直接把32个样本同时传入QNode因为线路构建是基于单个样本的。# 错误示范直接批量传入 # q_out quantum_gate(batch_inputs, batch_weights) # 会报维度错误 # 正确做法循环单样本调用或用 qml.map_batch q_outs [] for sample in batch_inputs: q_outs.append(quantum_gate(sample, weights)) q_outs torch.stack(q_outs)这个限制源于量子模拟器的状态向量机制每次计算期望值时整个量子态坍缩到测量基上不同样本之间没有天然并行通道。你可以在Notebook里先把batch循环封装成一个函数后续调用就不需要反复处理维度问题。3.4 梯度计算方式选择与显存控制在Jupyter里训练量子模型最直观的梯度方式是parameter-shift它对任何参数化量子线路都适用不需要额外内存缓存中间态。但速度偏慢因为每个参数需要额外计算两次线路期望值。qlstm示例用的是adjoint微分它在反向传播时复用了前向传播的量子态只计算一次伴随传播速度更快。dev qml.device(default.qubit, wiresn_qubits) quantum_gate qml.QNode( quantum_gate, devicedev, interfacetorch, diff_methodadjoint )实际使用中如果你发现显存或内存爆炸优先把adjoint改成parameter-shift。代价是训练时间可能翻3到5倍但至少不会OOM。如果你的线路深度超过6层、比特数超过8个模拟器计算开销会指数上升建议换回小规模配置先验证收敛性。4. 训练一个最小可用的量子时序模型里面的完整流程4.1 数据准备用简单正弦叠加而非复杂真实数据这个qlstm示例给了足够的数据预处理模板但我建议第一次跑的时候用合成数据比如两个不同频率正弦波的叠加信号。原因有两点合成数据标准答案明确你能判断模型是否真的学到了时序规律真实数据集比如股票、传感器噪声和维度问题会和技术坑混在一起出了问题很难定位。import numpy as np import matplotlib.pyplot as plt t np.linspace(0, 20, 800) data np.sin(0.5 * t) 0.5 * np.sin(2.0 * t 0.3) # 滑动窗口切分 seq_len 8 X, Y [], [] for i in range(len(data) - seq_len): X.append(data[i:i seq_len]) Y.append(data[i seq_len]) X np.array(X).reshape(-1, seq_len, 1) Y np.array(Y).reshape(-1, 1)这里seq_len8意味着模型每次看到8个历史时间步预测第9步。太长的序列会加大量子线路的输入拼接维度这个示例的4比特角度编码下seq_len在4到12之间是安全区间。4.2 从cell到完整模型循环展开与输出层单步QLSTM cell搭好后要把它展开成一个多步模型。这个示例里没有用PyTorch自带的nn.LSTM循环展开机制而是手动写了一个简单循环class QLSTM(nn.Module): def __init__(self, input_size, hidden_size, output_size, n_qubits4): super().__init__() self.cell QLSTMCell(input_size, hidden_size, n_qubits) self.output_layer nn.Linear(hidden_size, output_size) def forward(self, x): batch_size, seq_len, _ x.shape h torch.zeros(batch_size, self.cell.hidden_size) c torch.zeros(batch_size, self.cell.hidden_size) for t in range(seq_len): h, c self.cell(x[:, t, :], h, c) out self.output_layer(h) return out这段代码的处理方式是只取最后一个时间步的隐藏状态做预测倒推式训练。如果你做多步预测可以把每个时间步的h都接输出层然后和对应步的标签算loss。4.3 训练循环与loss曲线判读量子模型训练最怕的就是loss曲线看起来在下降但验证集一塌糊涂。这个示例里用的是均方误差L2Loss优化器是RMSprop。选RMSprop而不是Adam的原因量子线路梯度噪声相对较大RMSprop对梯度幅值的归一化处理比较适合这种情况。model QLSTM(input_size1, hidden_size16, output_size1, n_qubits4) optimizer torch.optim.RMSprop(model.parameters(), lr0.01) loss_fn nn.MSELoss() epochs 200 for epoch in range(epochs): optimizer.zero_grad() pred model(X_tensor) loss loss_fn(pred, Y_tensor) loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch}, loss {loss.item():.4f})我在拆这个Notebook时注意到一个细节hsic隐藏维度设置为16比量子比特数4大得多这意味着量子线路负责提取非线性特征隐藏状态的空间由经典层扩展。这个设计思路值得借鉴——量子部分不需要输出很宽的维度经典层做维度扩展更高效。4.4 训练过程中最常见的“假收敛”现象训练量子LSTM时我遇到过一个非常迷惑的情况loss先快速降到0.05附近然后卡住不动但验证集误差持续震荡升高。排查后发现是量子门控值饱和——三个门的sigmoid输出都在0.99以上相当于门一直全开模型变成了一个退化的线性RNN。解决办法是在loss里加一个轻度的门控正则项惩罚门控值偏离0.5的情况gate_penalty 0.01 * ((f_gate - 0.5) ** 2).mean() loss loss_fn(pred, Y_tensor) gate_penalty加这个正则项后门控值分布会逼向[0.2, 0.8]区间模型有更强的非线性表达能力。如果你的任务里设计上不需要复杂的长期依赖这项正则可以设小一点或直接去掉。5. 避坑指南量子LSTM常见问题与排查记录5.1 训练loss卡在某个值不动且门控值持续为1现象loss前20个epoch快速下降之后纹丝不动打印三个门的sigmoid输出发现f_gate和i_gate都在0.99以上。原因量子线路参数的初始权重过大或过小导致测量到的期望值经过sigmoid后饱和。线路输出的非线性表达能力没有被利用模型退化成近似线性结构。解决检查q_weights初始化把缩放因子从0.1降到0.03同时把门控正则项加上。另外检查输入编码前是否有做标准化如果输入特征的量级过大角度编码会超出[0, 2π]的有效映射范围。5.2 反向传播报错梯度无法连接到量子节点现象运行loss.backward()时报错提示某个Tensor不是计算图的一部分。原因量子线路在Pennylane中需要用interfacetorch包装并且调用方式必须是QNode的Torch兼容模式。如果直接在QNode上调用numpy接口比如直接在Jupyter里跑了一次量子线路并保存了结果之后把这个numpy结果放进Torch计算图梯度链路就断了。解决确保每次前向传播都直接调用QNode不要先把输入转成numpy再转回Tensor。检查QNode装饰器里是否写了interfacetorch没有的话加上。5.3 Jupyter里重复运行cell导致线路堆叠现象同一个Notebook cell多次运行后量子线路的构建时间越来越慢甚至内存飙高。原因Pennylane的QNode在每次定义时会把异常信息记录在缓存里。如果在Jupyter中重复运行定义QNode的cell旧的QNode对象没有被释放但是线路构建的逻辑被重复注册。解决把QNode定义统一放到一个函数里用qml.qnode装饰器在函数内部定义多次运行函数的开销是可控的。不要在一个cell里反复定义同一个名字的QNode。改用新的cell重新训练前用del model释放旧的模型对象。5.4 损失是NaN梯度爆炸现象训练到第几十个epoch时loss突然变成nan。原因角度编码的输入超出了合理范围导致某些旋转角的正弦余弦值剧烈震荡或者学习率太大参数更新跨越了量子的非线性区域。解决先将学习率从0.01降到0.001检查输入特征是否做了MinMax缩放如果问题依旧把n_qlayers从2降到1、q_depth从6降到3减小线路深度。5.5 训练时间比经典LSTM慢太多现象同样规模的时序预测任务这个量子模型的训练时间是经典LSTM的30倍以上但精度并没有提升。原因模拟器上跑量子线路开销本来就大如果你用default.qubit做2层4比特、外层循环跑200个epoch一次训练需要几十分钟是正常的。问题往往出在你把n_qlayers或q_depth设得过大。解决先按示例的默认参数跑通再逐步增加深度。另外如果想验证效果可以把adjoint微分方式保持默认它的训练速度比parameter-shift快很多。如果项目里只需要验证模型设计可以只跑50个epoch看趋势。6. 进阶验证技巧梯度检查与门控行为对照实验6.1 梯度检查验证量子线路反向传播是否正确量子线路的梯度计算是黑匣子尤其你第一次把QNode嵌入到自己的模型里时先用数值梯度验证一下参数更新方向是否正确能省去后面所有排查时间from pennylane import numpy as np def numerical_gradient(weights, inputs, eps1e-4): 对权重参数做中心差分验证反向传播梯度 grad_list [] for i in range(weights.shape[0]): for j in range(weights.shape[1]): w_plus weights.copy() w_minus weights.copy() w_plus[i, j] eps w_minus[i, j] - eps g (quantum_gate(inputs, w_plus)[0] - quantum_gate(inputs, w_minus)[0]) / (2 * eps) grad_list.append(g) return np.array(grad_list)跑一次数值梯度和模型反向传播的梯度对比如果两者相差超过1%检查你的数据流里是否混入了非Tensor计算或者有没有在某个环节做了detach。6.2 门控行为对照实验同一数据跑经典LSTM做对照这个qlstm示例最有教学价值的用法不是单独跑而是和PyTorch内置的经典LSTM在相同数据上做同步实验。我曾经用第三章的正弦叠加数据让两个模型在相同epoch数下训练把隐藏状态h的轨迹打印出来对比。量子LSTM的h轨迹呈现明显的分段线性特征经典LSTM的h轨迹则更平滑——量子门控因为测量期望值的离散本质天然带有更强的非线性切换行为。如果你要做这个对照实验建议固定随机种子确保两个模型从同样的初始状态出发。这个差异对理解“量子LSTM到底改变了什么”非常直观是做方案评审时最有说服力的证据。6.3 参数扫描门控初始缩放因子的敏感度测试在拆这个项目的过程中我最后会做的一次实验是参数扫描把q_weights的初始缩放因子从0.01扫到0.5记录每个配置下的最终loss和收敛速度。结果是0.03到0.15之间最优小于0.01时模型基本上不学习大于0.3时门控饱和风险极高。这个实验成本不高但对后续你自己改模型非常有参考价值。从那以后我每次拿到一个PQC相关的示例项目第一件事不是跑通而是把初始化缩放因子、微分方式、线路深度这三项列成一个对照表强制扫描一遍。这个习惯帮我避免了很多“demo能跑但换了数据就翻车”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?