☰
量子LSTM实战拆解:VQC门控、角度编码与PennyLane训练避坑
2026/10/29 11:32:56 网站建设 项目流程

简介: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 个复数振幅,n=4 时是 16 个,n=10 就是 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", wires=N_QUBITS) def encode(x, h): """把经典向量塞进量子态:x 是当前步输入特征,h 是上一步隐藏状态""" # 输入特征先做一次经典投影,压到 N_QUBITS 维(见下方说明) qml.AngleEmbedding(x, wires=range(len(x)), rotation="Y") # 隐藏状态正好占满所有比特,和输入编码角度叠加 qml.AngleEmbedding(h, wires=range(N_QUBITS), rotation="Y")

这段代码有两个参数值得盯住。rotation="Y"决定编码基,换成"X"或"Z"会改变输入在布洛赫球上的落点,实践中 Y 旋转对角度型特征最直观,也最不容易在一开始就把态压到某个极点上。wires=range(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 均为 (d+H, 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, wires=range(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]区间的值,走 sigmoid;g 需要[-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, scale=4.0): """params: shape (4, N_LAYERS, N_QUBITS, 3),索引 0/1/2/3 依次是 f/i/g/o""" # 四个门各跑一次 qnode,stack 成 (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_t

scale=4.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(stepsize=0.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(f"epoch {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_LEN20~40时间步长度每步四次 qnode,线性变慢
BATCH_SIZE8~16批大小内存吃紧,收益有限
LR0.05Adam 步长超过 0.1 容易震荡
SCALE4.0门控放大系数太大直接饱和到 0/1

这套值不是拍脑袋来的,是被模拟器约束出来的。N_QUBITS每加 1,状态向量长度翻倍;SEQ_LEN每加 10,一个 epoch 的线路执行次数就多 40 次一组。想跑大一点,先改N_LAYERS到 3 试试,别一上来就加比特数。

4.3 执行顺序与中途检查点

Notebook 要按单元格顺序从头跑,跳着跑会拿到上一次训练残留的全局变量,结果对不上还查不出原因。我一般按四段走:

  1. 环境与配置:import 全部依赖,打印版本号,设随机种子。
  2. 数据与编码:生成或加载series.csv,做归一化,切成(seq_len, n_features)的滑动窗口,然后画一张原始序列图,确认数据没读错。
  3. 模型与参数量:构造params,打印params.shape,应该是(4, N_LAYERS, N_QUBITS, 3);再跑一次vqc_gate,打印输出,确认是一串落在[-1, 1]的数。
  4. 训练与评估:跑训练循环,每个 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_QUBITS,wires=range(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,两者不是同一个随机源;如果线路里有采样型测量,还会再引入一层随机。解决:在配置单元格里统一 seed:np.random.seed(0)和pnp.random.seed(0)都写一遍,参数初始化用pnp.random.randn而不是np.random.randn。测量全用expval,别用sample或probs后接argmax,后者不可导,梯度会断。

提示:这几条里最耗时间的是第一条和第三条。门控饱和不会报错,只是安静地不学;模拟器慢也不会报错,只是安静地等。遇到“能跑但结果不对”,先打印一次vqc_gate的输出看看数值分布,比盲调超参快得多。

6. 二次开发:把示例改成你自己的量子时序模型

这份示例最适合当骨架用,改三个地方就能套到自己的任务上。

第一处是输入层。原示例默认特征维度等于量子比特数,真实数据基本不满足。在qlstm_cell前面加一层经典投影,把任意维输入压到N_QUBITS:

def 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)加 softmax,loss 换成交叉熵。注意分类任务对门控饱和更敏感,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 条都拟合不了,问题一定在结构或者缩放系数上,跟数据量、超参搜索都没关系。这个测试帮我省下的时间,比任何调参技巧都多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询