简介qlstm 量子 LSTM 示例压缩包面向对量子机器学习与循环神经网络感兴趣的开发者、学生和研究人员重点展示如何将量子电路思路融入 LSTM并与 PyTorch 经典 LSTM 做对照实验。包内共 10 个文件以 3 个 Python 脚本、3 张 PNG 训练曲线图、2 个文本依赖清单、1 个 Jupyter Notebook 和 1 个说明文档为主整体约 84KB体量轻便便于快速阅读源码、复现实验和查看结果。内容围绕词性标注任务展开包含量子 LSTM 与经典 LSTM 的实现入口、Qiskit/PennyLane 等相关依赖配置以及量子模型、经典模型和二者对比的训练过程图示可帮助读者理解量子层与经典循环单元的结合方式掌握环境搭建、训练评估和结果对照的基本流程。目前已有 423 人学习适合作为量子机器学习入门、课程实验和小型课题复现的参考案例对希望把量子计算与深度学习结合起来的读者较为友好。1. 量子 LSTM 到底在做什么从一份 Jupyter Notebook 说起第一次看到 qlstm 这个名字很多人以为要上量子计算机训练 LSTM心里先凉一半。真拆开这份 Jupyter Notebook 示例会发现做法朴素得多把 LSTM 单元里那四组Wx Uh b的仿射变换换成四条参数化量子线路隐藏状态 h 从一串实数变成量子态在 Z 基上的测量期望。全程跑在状态向量模拟器上一台普通笔记本的 CPU 就能算完每个量子门是什么、参数怎么更新Notebook 里一步步摊开。它适合两类人想搞明白量子机器学习怎么和时序模型拼接的学生以及手上有个小规模序列任务、想先拿模拟器验证想法再决定要不要上真机的工程师。缺点是规模小别指望它替你做长序列预测。2. 实现路线与技术选型qlstm 的“量子”到底加在哪一层2.1 三条路线VQC 门控、复值隐藏状态、整网量子化“量子 LSTM”这个词在不同项目里指的东西差很远动手前先认清这份示例走的是哪条路不然读代码会一直在找不存在的东西。第一条是VQC 门控替换保留 LSTM 的 cell state 递推公式c_t f_t * c_{t-1} i_t * g_t只把四个门里的线性层换成变分量子线路Variational Quantum Circuit下文简称 VQC输入是当前步特征x_t和上一步隐藏状态h_{t-1}输出是对每个量子比特测 PauliZ 得到的期望值。这条路线对经典 LSTM 的改动最小参数量也最少是教学型示例最常见的写法qlstm 基本就是它。第二条是复值隐藏状态把 h 写成复数向量用振幅和相位编码信息递推时用酉矩阵代替实数权重矩阵。它其实可以完全跑在经典 numpy 上量子只是数学形式上的类比优点是能拿复数注意力那套东西来套缺点是解释性差初学者看不出量子门在哪。第三条是整网量子化直接堆一串量子线路当作序列编码器后面接经典分类头本质上已经不是 LSTM 了。这种写法在论文里常见在示例项目里很少见因为它没有可对照的经典基线。这份示例选第一条路线的理由很实际能对照。同一份数据跑一遍经典 LSTM 得到一条 loss 曲线再跑一遍 QLSTM 得到另一条两条线放在同一张图里你立刻能看出量子门控到底带来了什么、丢掉了什么。代价是表达力受限——四五个量子比特能编码的信息量本来就不大别指望它在真实业务数据上打赢调好参的经典模型。我一般把它定位成“教学 原型验证”而不是生产模型。路线隐藏状态形式是否可跑在纯 CPU相对经典 LSTM 的改动适用场景VQC 门控替换测量期望向量是只换线性层教学、小规模原型复值隐藏状态复数向量是换递推算子理论研究、信号处理整网量子化量子态是模拟器整个结构论文实验2.2 技术栈拆解Notebook 里都装了什么这类示例的依赖很少核心就三样量子线路框架、数值计算、画图。常见组合是 PennyLane 做线路定义和自动微分NumPy 做数据预处理Matplotlib 出 loss 曲线和预测对比图Jupyter 负责把每一步的中间结果摊在单元格里。之所以用 PennyLane 而不是别的框架是因为它对 autograd 的支持最省事——qnode 直接返回可导的期望值不需要手写梯度这对一份教学示例很关键读者不用先学一遍伴随法。需要注意模拟器不是真机。default.qubit用状态向量模拟n 个量子比特要存 2^n 个复数振幅n4 时是 16 个n10 就是 1024 个再往上内存和时间都会失控。示例里量子比特数一般压在 4 到 6 之间不是随意定的是被模拟器逼的。目录结构大致是下面这个形状我按这个顺序读qlstm/ ├── README.md # 环境说明 运行顺序 ├── requirements.txt # pennylane / numpy / matplotlib / jupyter ├── notebooks/ │ ├── 01_qlstm_demo.ipynb # 主示例数据 → 建模 → 训练 → 画图 │ └── 02_classical_baseline.ipynb # 经典 LSTM 对照组 ├── src/ │ ├── encoding.py # 角度编码、数据预处理 │ ├── qlstm_cell.py # 四条 VQC 门 cell 前向 │ └── train.py # 训练循环、参数初始化 └── data/ └── series.csv # 小规模时序数据src/和notebooks/分开是关键设计Notebook 负责讲和看模块负责复用和测试。如果你要二次开发改src/里的东西Notebook 只用来验证反过来把逻辑全写进单元格后面想跑批量实验就得复制粘贴这种代码我见过太多改到第三版就没人敢动了。2.3 数据怎么进量子线路角度编码与前置投影经典向量要变成量子线路能吃的输入得先编码。示例里用的是角度编码Angle Embedding把一个特征值映射成一个旋转角作用在 RY 门上。特征有几个就用几个量子比特或者几个量子比特叠着转。import pennylane as qml from pennylane import numpy as pnp N_QUBITS 4 # 隐藏状态维度同时也是测量输出的维度 N_LAYERS 2 # 变分层数层数越多表达力越强也越容易过拟合 dev qml.device(default.qubit, wiresN_QUBITS) def encode(x, h): 把经典向量塞进量子态x 是当前步输入特征h 是上一步隐藏状态 # 输入特征先做一次经典投影压到 N_QUBITS 维见下方说明 qml.AngleEmbedding(x, wiresrange(len(x)), rotationY) # 隐藏状态正好占满所有比特和输入编码角度叠加 qml.AngleEmbedding(h, wiresrange(N_QUBITS), rotationY)这段代码有两个参数值得盯住。rotationY决定编码基换成X或Z会改变输入在布洛赫球上的落点实践中 Y 旋转对角度型特征最直观也最不容易在一开始就把态压到某个极点上。wiresrange(len(x))里的len(x)必须小于等于N_QUBITS超了会直接抛维度错误这是新手第一个卡点。提示现实数据的特征维度很少正好等于量子比特数。我一般会在 cell 前面挂一层经典Linear(d_in, N_QUBITS)把任意维输入投到固定维度再编码。这一步不参与“量子”但能让整个模型对上任何数据集是示例能直接换数据的关键。3. 核心模块拆解一个 QLSTM cell 的四条量子线路3.1 经典 LSTM 基线先留一个能对照的版本读量子版之前先把经典 cell 写成十行以内的 numpy 版本。不是为了用它做预测是为了有一个“同样的公式、不同的门”的参照物后面看量子版的输出范围、梯度行为时心里有底。import numpy as np def sigmoid(z): return 1.0 / (1.0 np.exp(-z)) def lstm_cell_classic(W, x_t, h_prev, c_prev): 标准 LSTM 单元W 是 dict四个门各一个权重矩阵shape 均为 (dH, H) xh np.concatenate([x_t, h_prev]) f sigmoid(xh W[f]) # 遗忘门 i sigmoid(xh W[i]) # 输入门 g np.tanh(xh W[g]) # 候选状态 o sigmoid(xh W[o]) # 输出门 c_t f * c_prev i * g h_t o * np.tanh(c_t) return h_t, c_t关键在xh W[f]这一行经典 LSTM 的全部表达力都在这个矩阵乘法里权重可以取任意实数维度可以很大。量子版要替换的正是它。注意g用 tanh 而另外三个用 sigmoid这个不对称是 LSTM 的原始设计量子版如果改掉两组结果就没法比较了。3.2 VQC 门控实现从 AngleEmbedding 到 StronglyEntanglingLayers把一个门的线性层换成量子线路结构是三段编码 → 变分层 → 测量。变分层用StronglyEntanglingLayers它由若干层旋转门加环形 CNOT 组成参数是每个旋转门的角度可训练的部分全在这里。qml.qnode(dev) def vqc_gate(theta, x, h): 一个门对应的参数化量子线路 theta: shape (N_LAYERS, N_QUBITS, 3)每层每比特三个旋转角 x, h : 经典输入会被编码成旋转角 encode(x, h) qml.StronglyEntanglingLayers(theta, wiresrange(N_QUBITS)) # 对每个比特测 PauliZ 期望输出 N_QUBITS 个落在 [-1, 1] 的实数 return [qml.expval(qml.PauliZ(w)) for w in range(N_QUBITS)]三个参数各管一件事。theta的形状是(层数, 比特数, 3)每个三元组对应一个比特上的 RX、RY、RZ 三个旋转角层数决定了线路深度也决定了能拟合多复杂的函数x和h是经典数据不参与训练只影响量子态的初态返回的expval是测量期望数学上等于ψ|Z|ψ值域固定为[-1, 1]。注意这一步输出范围只有[-1, 1]而经典 LSTM 的预激活值可以是任意实数。如果直接把量子输出喂给 sigmoid门控会被死死卡在 0.27 到 0.73 之间训练几乎不动。这是量子 LSTM 最容易踩的坑下一章会给处理方式。四个门共用同一套线路结构但各有一份独立参数。f、i、o 三个门需要[0, 1]区间的值走 sigmoidg 需要[-1, 1]走 tanh。量子输出天然落在[-1, 1]看起来和 tanh 更配但四个门还是得统一处理否则 f 和 g 的量纲对不上c_t f * c i * g会直接被带偏。3.3 前向传播门控值如何拼成 cell state把四条线路的输出拼起来就是量子版的 cell 前向。这一步的公式和经典 LSTM 完全一致区别只在门控值的来源。def qlstm_cell(params, x_t, h_prev, c_prev, scale4.0): params: shape (4, N_LAYERS, N_QUBITS, 3)索引 0/1/2/3 依次是 f/i/g/o # 四个门各跑一次 qnodestack 成 (4, N_QUBITS) raw pnp.stack([vqc_gate(params[k], x_t, h_prev) for k in range(4)]) # 放大系数把 [-1,1] 拉到 [-4,4]让 sigmoid 有发挥空间 z scale * raw f sigmoid(z[0]) i sigmoid(z[1]) g pnp.tanh(z[2]) o sigmoid(z[3]) c_t f * c_prev i * g h_t o * pnp.tanh(c_t) return h_t, c_tscale4.0是这段代码里唯一一处“非教科书”的改动也是最不能省的一处。它把量子测量的窄输出区间拉宽让 sigmoid 有机会输出接近 0 或 1 的值门控才真的能“关”或“开”。这个系数可以当成超参调也可以写成可学习的标量——我一般先固定 4.0 跑通确认 loss 能降之后再考虑放开。params的第一维索引顺序要固定死f/i/g/o 不能乱。我在第一次实现时就因为把 g 和 o 写反了loss 曲线看起来能降但预测出来的序列整体相位是错的查了快两个小时才定位到这一行。3.4 反向传播parameter-shift 与参数张量形状量子线路的参数梯度不能直接走经典反向传播需要用 parameter-shift 规则把某个旋转角平移 ±π/2两次前向的期望值之差除以 2就是该参数的梯度。PennyLane 的 autograd 接口把这层封装掉了你只要保证参数是pnp.array并且整个前向链路里没有不可导的操作qml.AdamOptimizer就能直接用。opt qml.AdamOptimizer(stepsize0.05) # 小随机数初始化避免一开始所有比特都在极点上 params 0.1 * pnp.random.randn(4, N_LAYERS, N_QUBITS, 3) def sequence_loss(p, x_seq, y_seq): 沿时间步展开逐步累加 MSE隐状态和时间步长度相关不能并行 h pnp.zeros(N_QUBITS) c pnp.zeros(N_QUBITS) loss 0.0 for t in range(len(x_seq)): h, c qlstm_cell(p, x_seq[t], h, c) loss loss pnp.mean((h - y_seq[t]) ** 2) return loss / len(x_seq) for epoch in range(30): for x_seq, y_seq in loader: params, loss opt.step_and_cost(sequence_loss, params, x_seq, y_seq) print(fepoch {epoch:02d} loss {loss:.6f})参数量算一下4 × 2 × 4 × 3 96个可训练角度加上前置经典投影层总共一两百个参数。这个规模和隐藏维度 4 的经典 LSTM 在同一量级但梯度的行为完全不同——经典 LSTM 的梯度可以靠矩阵乘法的链式法则一路传回去量子侧的梯度受线路深度和纠缠结构影响深了就衰减得厉害。这是量子机器学习里常说的贫瘠高原barren plateau问题在 4 个量子比特的小线路上还感觉不明显一旦加到 8 比特、4 层梯度就会小到 Adam 也推不动。sequence_loss里的 for 循环没法省。LSTM 的时间维本来就是串行的量子版更是如此——每个时间步要跑四次 qnode步长 30 的序列一个样本就是 120 次线路执行。这就是为什么示例的数据集都很小跑得慢不是代码写得烂是结构决定的。4. 跑起来Notebook 的分步执行与结果验证4.1 环境准备与内核注册依赖不多但虚拟环境和 Jupyter 内核一定要提前隔离好不然装到系统 Python 里后面想换 PennyLane 版本会很难受。# 建独立环境Python 3.10 左右都可以 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # 装核心依赖不锁版本装当前版即可 pip install pennylane numpy matplotlib jupyter # 把环境注册成 notebook 内核避免 Jupyter 用错解释器 python -m ipykernel install --user --name qlstm --display-name Python (qlstm) # 启动 jupyter lab notebooks/01_qlstm_demo.ipynb装完之后在 Notebook 里第一件事不是跑模型是验环境新建一个单元格import pennylane as qml; print(qml.__version__)能打印出版本号、不带任何 warning 才算通。内核没注册对是最常见的翻车点现象是pip install明明成功了Notebook 里import pennylane却报 ModuleNotFoundError原因就是 Jupyter 用的还是系统 Python不是刚才那个虚拟环境。右上角内核名字选Python (qlstm)就能解决。4.2 配置单元格超参怎么定示例一般会把所有可调的东西堆在第二个单元格方便你一处改、全篇生效。我按下面这几个值的量级来理解参数建议值作用调大的后果N_QUBITS4隐藏维度、测量输出维度模拟器内存指数上涨N_LAYERS2变分层数、表达力训练变慢梯度衰减SEQ_LEN2040时间步长度每步四次 qnode线性变慢BATCH_SIZE816批大小内存吃紧收益有限LR0.05Adam 步长超过 0.1 容易震荡SCALE4.0门控放大系数太大直接饱和到 0/1这套值不是拍脑袋来的是被模拟器约束出来的。N_QUBITS每加 1状态向量长度翻倍SEQ_LEN每加 10一个 epoch 的线路执行次数就多 40 次一组。想跑大一点先改N_LAYERS到 3 试试别一上来就加比特数。4.3 执行顺序与中途检查点Notebook 要按单元格顺序从头跑跳着跑会拿到上一次训练残留的全局变量结果对不上还查不出原因。我一般按四段走环境与配置import 全部依赖打印版本号设随机种子。数据与编码生成或加载series.csv做归一化切成(seq_len, n_features)的滑动窗口然后画一张原始序列图确认数据没读错。模型与参数量构造params打印params.shape应该是(4, N_LAYERS, N_QUBITS, 3)再跑一次vqc_gate打印输出确认是一串落在[-1, 1]的数。训练与评估跑训练循环每个 epoch 打印 loss最后画预测曲线对照真值。第二段的图一定要画。我见过数据加载把时间列当特征读进去的例子模型“训练得挺好”loss 一路降结果是在拟合时间戳换个数据集就崩。看一眼原始序列三十秒的事。4.4 结果判读和经典基线怎么比“跑通了”不是 loss 降了就算。三个判据一起看第一loss 曲线要能明显下降起始值大约在 0.2 到 0.3归一化后的 MSE收敛后应该落到 0.02 以下。如果卡在 0.25 一动不动先查SCALE再查编码有没有把输入压到同一个角度上。第二预测曲线要和真值大致贴合。量子门控的表达力有限允许整体有轻微相位偏移但不允许出现“预测恒定值”——那是模型退化成常数输出了通常意味着门控全部饱和。第三和02_classical_baseline.ipynb的经典 LSTM 对比。同数据、同序列长度、同隐藏维度下经典版一般会收敛更快、loss 更低量子版能追到接近但很少反超。如果量子版明显更差且差距很大不是模型不行是超参没配好尤其scale和LR这一对。5. 避坑量子 LSTM 训练里最容易翻车的五件事5.1 门控与梯度类现象loss 从第一轮开始就贴在 0.25 附近三十个 epoch 之后基本没动预测出来是一条近乎水平的线。原因量子线路输出是 PauliZ 期望值值域[-1, 1]直接进 sigmoid 只能得到 0.27 到 0.73 之间的值。四个门都挤在这个窄区间里c_t f * c i * g变成了一个固定的低通滤波模型学不到东西梯度也传不回去。解决在 sigmoid / tanh 之前乘一个放大系数示例里是 4.0。调这个值的时候盯着第一轮 loss 看如果 loss 一开始就接近 0 或者直接 NaN说明放大过头饱和了往 2.0 降。现象训练过程中 loss 突然变成nan后面全是nan怎么重启都没用。原因sequence_loss沿时间步累加隐藏状态经过tanh(c_t)后虽然被压住但c_t本身在长序列上会累积到很大反向传播时梯度爆炸。量子侧参数少不代表数值就不会出问题前向里还有 sigmoid 和除法的组合。解决两处下手。一是把序列长度从 40 降到 20先确认短序列能收敛二是在c_t更新后加一句裁剪把c_t限在[-5, 5]。梯度裁剪也可以用qml.AdamOptimizer外面套一层pnp.clip对梯度做处理。这个坑我在第一次跑长序列时踩过调了半天学习率最后发现是 cell state 溢出了。5.2 维度、设备与可复现类现象一运行就报 wires 数量对不上或者AngleEmbedding抛维度错误。原因输入特征维度大于N_QUBITSwiresrange(len(x))要的比特数超过了设备声明。真实数据三五个特征很常见示例数据只有一两个直接换数据必崩。解决在 cell 前面加一层经典线性投影Linear(d_in, N_QUBITS)把输入先压到固定维度再编码。这层是可训练的也跟着一起优化不破坏端到端结构。现象一个 epoch 跑十几分钟笔记本风扇狂转top一看单核跑满。原因default.qubit是状态向量模拟器计算量随量子比特数指数增长同时每个时间步要执行四次 qnode梯度计算走 parameter-shift 还要再前向两次。时间步、批大小、比特数三个维度乘起来开销是乘法级的。解决优先减N_QUBITS和SEQ_LEN别先动N_LAYERS层数对表达力的影响比另外两个小。另外可以把设备换成 PennyLane 的lightning.qubit同样是状态向量模拟但有 C 后端速度能快一截接口完全兼容只改qml.device那一行。别指望 GPU这类模拟器基本都在 CPU 上跑。现象固定了np.random.seed(42)两次运行的 loss 曲线还是不一样。原因只 seed 了 numpy 的全局随机但 PennyLane 的参数初始化用的是pnp.random两者不是同一个随机源如果线路里有采样型测量还会再引入一层随机。解决在配置单元格里统一 seednp.random.seed(0)和pnp.random.seed(0)都写一遍参数初始化用pnp.random.randn而不是np.random.randn。测量全用expval别用sample或probs后接argmax后者不可导梯度会断。提示这几条里最耗时间的是第一条和第三条。门控饱和不会报错只是安静地不学模拟器慢也不会报错只是安静地等。遇到“能跑但结果不对”先打印一次vqc_gate的输出看看数值分布比盲调超参快得多。6. 二次开发把示例改成你自己的量子时序模型这份示例最适合当骨架用改三个地方就能套到自己的任务上。第一处是输入层。原示例默认特征维度等于量子比特数真实数据基本不满足。在qlstm_cell前面加一层经典投影把任意维输入压到N_QUBITSdef qlstm_forward(params, proj_w, proj_b, x_seq): proj_w: (d_in, N_QUBITS)把原始特征投到量子比特维度 h pnp.zeros(N_QUBITS) c pnp.zeros(N_QUBITS) outs [] for x_t in x_seq: x_t pnp.tanh(x_t proj_w proj_b) # 投影 压到 [-1,1] 附近 h, c qlstm_cell(params, x_t, h, c) outs.append(h) return pnp.stack(outs)这层的输出范围要控制住因为后面还要做角度编码值太大等于把量子态直接转到极点上线路再深也救不回来所以用 tanh 兜一下。第二处是任务头。示例做的是回归输出就是 h。换成分类只要在后面接一层Linear(N_QUBITS, n_class)加 softmaxloss 换成交叉熵。注意分类任务对门控饱和更敏感scale要重新调我一般在分类任务上从 3.0 起调。第三处是设备与规模。跑通之后如果嫌慢把qml.device(default.qubit, ...)换成lightning.qubit接口不用动。想验证模型是不是真的靠量子部分在学做一个消融把四条 VQC 全换成等参数量的经典线性层其他结构不变跑同一份数据。如果经典替换版的 loss 明显更低说明你的量子线路深度不够或者编码方式不适合这个数据这时候加N_LAYERS比加数据更有用。改造点改哪里验证方式换数据集data/ 投影层维度先画原始序列确认读取正确改任务类型sequence_loss 输出头看混淆矩阵不只看 loss提速度qml.device换后端对比单 epoch 耗时验收益VQC 换经典层做消融同参数量、同数据对比 loss有个习惯我一直在用新建任何量子时序实验先拿 20 条样本做过拟合测试跑到 loss 接近 0 再上全量数据。如果 20 条都拟合不了问题一定在结构或者缩放系数上跟数据量、超参搜索都没关系。这个测试帮我省下的时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取