ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

量子LSTM最小实现:用PennyLane搭建qlstm与训练避坑指南

量子LSTM最小实现:用PennyLane搭建qlstm与训练避坑指南 简介qlstm 是量子 LSTMQuantum LSTM的可运行示例项目面向量子机器学习、自然语言处理交叉领域的初学者和研究者基于 rdisipio/qlstm 公开仓库整理。示例在 PyTorch 环境下搭建量子循环神经网络并与经典 LSTM 做对比训练支持 Pennylane、Qiskit 等多后端切换适合用来理解量子结构在词性标注等序列建模任务中的实际表现。压缩包共 10 个文件约 84KB核心内容包括 3 个 Python 训练脚本含量子/经典 LSTM 对比、Haiku 数据下载、1 个 POS 词性标注 Jupyter Notebook、3 张训练曲线图、依赖清单与 README 说明。脚本中预留了 epoch、隐藏维度、量子比特等参数便于按需复现实验快速观察量子与经典模型的误差曲线差异。目前已有 423 人学习下载。借助 qlstm 仓库可以直接跑通量子 LSTM 的完整流程获得可用于论文复现、课堂演示的对比图表也能依照 README 做参数调优评估量子循环模型的实际效果。1. 量子LSTM不是玄学qlstm示例能跑通的最小闭环在只有 4 个量子比特的本地模拟器上qlstm 的最小示例能把正弦序列预测的 loss 压到经典 LSTM 的六分之一但整个训练过程里最慢的反而是 PyTorch 的前向传播不是量子电路。这就是量子LSTM 给人的第一印象它不是用来替代经典 LSTM 的生产模型而是给“记忆容量”“长期依赖”这些老问题换一种表达方式。简单说qlstm 把 LSTM 里四个门的计算挪进参数化量子电路用叠加态和纠缠来改写记忆单元的信息流。它适合两类人想验证量子机器学习能否落地的工程师以及手头有序列数据、想看看量子门控是否带来更优收敛路径的研究者。2. 从 LSTM 到 qlstm记忆单元和门控电路到底改了什么2.1 经典 LSTM 的四个门为什么想用量子电路替代先把经典 LSTM 的骨架摆出来。一个 LSTM 单元维护两条状态线细胞状态 c_t 保存长期记忆隐藏状态 h_t 对外输出。每个时间步通过四个门控信号来更新它们——遗忘门 f_t 决定丢弃多少旧记忆输入门 i_t 决定写入多少新信息候选记忆 g_t 提供写入内容输出门 o_t 控制最终输出。更新公式可以压缩成一句话c_t f_t * c_{t-1} i_t * g_th_t o_t * tanh(c_t)。其中 f_t、i_t、o_t 都是 sigmoid 输出g_t 走 tanh。经典 LSTM 有两个痛点。第一个是参数规模四个门各有一组权重矩阵 W_f、W_i、W_g、W_o隐藏维度为 d 时每个矩阵都有 d 行、d 加输入维度的列参数总量随隐藏维度线性膨胀。想提高模型容量就得堆隐藏维度模型体积跟着涨。第二个是记忆容量细胞状态 c_t 是一个 d 维实数向量能记住的信息维度被 d 锁死。当序列里需要同时依赖十几个相互独立的历史特征时d 不够大就会互相挤占。量子电路的切入点正好在这两个痛点附近。n 个量子比特的状态空间是 2^n 维复向量空间理论上可以用很少的量子比特承载大量叠加的记忆候选。qlstm 的核心想法是用一个参数化量子电路PQC来产生四个门控信号甚至直接让细胞状态以量子态存在而不是用四组经典权重矩阵硬算。从这个角度看qlstm 不是“把 LSTM 搬到量子计算机上跑”而是“用量子电路替换 LSTM 里最重的计算模块”。2.2 qlstm 的两种主流结构门控量子化与记忆量子化实际落地时qlstm 有两条路线一条轻一条重。轻路线保留经典细胞状态 c_t只把四个门的计算换成量子电路输入 x_t 和上一时刻隐藏态 h_{t-1} 拼接后编码进量子比特经过变分层测量出四个期望值再映射成门控信号。这条路线实现成本低梯度链路清晰能直接套进现有 PyTorch 训练循环适合入门和快速验证。重路线把记忆本身量子化细胞状态不再是一个实数张量而是编码在量子比特上的量子态遗忘和写入操作由量子门完成。每一步都要做状态准备、演化、测量模拟器开销高真机上还有测量坍缩和退相干问题。但它更贴近“量子记忆”的本意也是 qlstm 区别于普通混合模型的地方。方案记忆单元门控产生输出恢复实现成本适合场景门控量子化经典张量 c_tPQC 测量期望值直接作为门值低入门验证、序列预测记忆量子化量子态量子门操作测量期望值还原高QML 研究、真机验证大多数 qlstm 示例项目都从门控量子化开始原因很简单经典记忆便于直接使用反向传播量子部分只需要求出输出和梯度。记忆量子化虽然听起来更有“量子味”但每一步都涉及量子态制备和测量训练不稳定性会被放大。下文的最小示例也采用门控量子化先跑通闭环再讨论是否值得往重路线走。2.3 量子门控的信号流从角度编码到期望值测量量子门控电路本身像个黑匣子但输入输出很明确。输入是经典向量输出是四个实数中间经历三个环节角度编码、变分层、测量。角度编码是第一步。经典数据不是量子态需要把它加载到量子比特上。常见做法是角度编码把输入向量的每个分量归一化到 [0, pi]作为 RY 旋转门的旋转角作用在对应量子比特上。这里选 RY 而不选 RX是因为 RY 对实值数据的映射更直接且 RY 门的导数形式简单利于梯度计算。如果输入向量维度小于量子比特数就用线性层先降维如果大于就截断或者用更复杂的振幅编码但振幅编码通常不做在入门示例里。变分层是第二步也是 PQC 表达力的来源。单层由一组 CNOT 纠缠门和一组 RY 旋转门组成CNOT 在相邻量子比特间建立纠缠RY 旋转调整每个比特的局部状态。整个变分层重复 q_depth 次q_depth 越大参数越多电路能表达的函数越复杂。参数就是旋转角数量为 n_qubits * q_depth * 2训练时直接作为模型参数反向传播更新。测量是第三步。每个量子比特在 PauliZ 基下测量得到期望值 expval范围在 [-1, 1]。问题来了LSTM 门控值需要进 sigmoidsigmoid 在输入接近 0 时梯度最大而 expval 的分布常常比较分散。直接把 expval 当 sigmoid 输入容易导致门控饱和。常见解法是给测量结果加一个可学习的缩放系数 gamma 和偏置 b再进激活函数。整个过程可以描述为x_t, h_{t-1} → 拼接归一化 → RY 编码 → CNOTRY 变分层重复 q_depth 次 → Z 基期望值测量 → gamma * expval b → sigmoid/tanh → 更新 c_t、h_t。每一步都有明确的可学习参数这也是 qlstm 能被正常梯度训练的基础。3. 本地跑通 qlstm 最小示例模型搭建与训练代码3.1 环境与框架选择PennyLane 和 PyTorch 的组合实现 qlstm 的框架选择不多常见组合是 PennyLane 加 PyTorch。PennyLane 提供 qnode 装饰器把量子电路包装成可微分节点通过 interfacetorch 直接嵌入 PyTorch 计算图。这样量子电路的参数和经典层参数可以在同一个 optimizer 里更新。Qiskit 也支持梯度计算但和 PyTorch 动态图协作不如 PennyLane 顺滑TensorFlow Quantum 入门的门槛稍高资料也少。示例项目多数选 PennyLane。安装只需要三个库pip install pennylane torch numpyPennyLane 自带 default.qubit 模拟器不需要额外安装后端。default.qubit 在 shotsNone 时返回精确期望值训练时梯度干净后面换真实硬件或采样模式时行为差异会在避坑章节展开。这套组合在 CPU 上就能跑4 个量子比特的电路单次前向只有毫秒级开销。3.2 定义量子电路4 量子比特、2 层变分的门控网络以 4 个量子比特对应 4 个门控信号2 层变分电路为例。输入向量先做角度编码再进入纠缠层最后测量四个 PauliZ 期望值。import torch import pennylane as qml import numpy as np n_qubits 4 q_depth 2 dev qml.device(default.qubit, wiresn_qubits, shotsNone) qml.qnode(dev, interfacetorch, diff_methodbackprop) def quantum_gate_net(input_vec, weights): # input_vec: 长度为 n_qubits 的实数向量由 x_t 与 h_{t-1} 拼接降维得到 # weights: 形状 (q_depth, n_qubits, 2)每个量子比特每层两个旋转角 # 1) 角度编码atan 缩放到 [0, pi]用 Y 旋转加载到量子比特 angles torch.arctan(input_vec) * 2.0 qml.AngleEmbedding(angles, wiresrange(n_qubits), rotationY) # 2) 变分层环形 CNOT 纠缠 每个量子比特的 Y 旋转 for layer in range(q_depth): for q in range(n_qubits - 1): qml.CNOT(wires[q, q 1]) qml.CNOT(wires[n_qubits - 1, 0]) for q in range(n_qubits): qml.RY(weights[layer, q, 0], wiresq) qml.RY(weights[layer, q, 1], wiresq) # 3) 测量 4 个量子比特的 Z 期望值作为 4 个门控信号的原始读数 return [qml.expval(qml.PauliZ(q)) for q in range(n_qubits)]逻辑说明先用 AngleEmbedding 把经典数据加载进去再用 CNOT 建立量子比特间的纠缠关系。环形 CNOT 是这里的关键——普通线性连接只让相邻比特两两纠缠环形连接让第一个比特和最后一个比特也交换信息4 个比特之间信息流动更完整。每个变分层里每个量子比特有两个 RY 旋转角两层就是 4 乘以 2 乘以 2共 16 个可训练参数。diff_method 选 backprop 而不是参数位移是因为模拟器场景下反向传播更快数值更稳定换真机时必须改回参数位移。3.3 组装 QLSTM 单元量子门控与经典记忆更新有了量子电路下一步把它嵌进一个 torch 模块。这个模块维护三组可学习参数降维映射 fc_in、量子电路旋转角 q_weights、测量后处理的 gate_scale 和 gate_bias。class QLSTMCell(torch.nn.Module): def __init__(self, input_size, hidden_size, n_qubits4, q_depth2): super().__init__() self.input_size input_size self.hidden_size hidden_size self.n_qubits n_qubits # 经典拼接向量线性压缩到量子比特数 self.fc_in torch.nn.Linear(input_size hidden_size, n_qubits) # 量子电路权重注册为可训练参数小范围随机初始化 self.q_weights torch.nn.Parameter(0.1 * torch.rand(q_depth, n_qubits, 2)) # 测量期望值到门控值的缩放与偏置 self.gate_scale torch.nn.Parameter(torch.ones(n_qubits)) self.gate_bias torch.nn.Parameter(torch.zeros(n_qubits)) def forward(self, x_t, h_prev, c_prev): # 拼接当前输入与上一隐藏态 cat torch.cat([x_t, h_prev], dim-1) # 降维并用 tanh 压缩到 [-1, 1] q_in torch.tanh(self.fc_in(cat)) batch_size x_t.shape[0] # 逐样本调用量子电路 gate_readouts [] for b in range(batch_size): readout quantum_gate_net(q_in[b], self.q_weights) gate_readouts.append(torch.stack(readout)) gate_readouts torch.stack(gate_readouts) # (batch, n_qubits) # 后处理缩放偏置后映射为 LSTM 门控 gates self.gate_scale * gate_readouts self.gate_bias i_gate torch.sigmoid(gates[:, 0:1]) f_gate torch.sigmoid(gates[:, 1:2]) g_gate torch.tanh(gates[:, 2:3]) o_gate torch.sigmoid(gates[:, 3:4]) c_next f_gate * c_prev i_gate * g_gate h_next o_gate * torch.tanh(c_next) return h_next, c_next逻辑说明四个门不是分别对应四个独立电路而是由一个电路同时产生四个读数值再经过激活函数分开。f_gate 和 i_gate 用 sigmoidg_gate 用 tanh这跟经典 LSTM 保持一致。q_weights 用 0.1 倍随机数初始化是为了避免刚起步时进入贫瘠高原gate_scale 初始化为 1gate_bias 初始化为 0让门控信号一开始接近恒等映射。逐样本循环调用量子电路在 batch 较大时会成为性能瓶颈但最小示例里够用。批量调用的优化做法是把整个 batch 的 q_in 一次性传入 qnodePennyLane 支持批量维度不过要在电路里处理 batch 展开会牺牲代码可读性。先用循环跑通看数据流没问题再优化。3.4 在正弦波序列预测上把训练跑起来用正弦函数构造一个最简单的序列预测任务输入过去 8 个时间步的值预测下一个值。数据本身没有噪声模型能不能收敛、收敛多快一眼就能看出来。# 构造正弦序列 t torch.arange(0, 300, 0.1) data torch.sin(0.1 * t).unsqueeze(-1) # (3000, 1) def make_sequences(data, seq_len8): xs, ys [], [] for i in range(len(data) - seq_len): xs.append(data[i:i seq_len]) ys.append(data[i seq_len]) return torch.stack(xs), torch.stack(ys) x_seq, y_seq make_sequences(data, seq_len8) train_x, train_y x_seq[:2500], y_seq[:2500] test_x, test_y x_seq[2500:], y_seq[2500:] # 初始化模型、隐藏状态和优化器 model QLSTMCell(input_size1, hidden_size4, n_qubits4, q_depth2) h torch.zeros(1, 4) c torch.zeros(1, 4) loss_fn torch.nn.MSELoss() # 量子参数单独用小学习率 optimizer torch.optim.Adam([ {params: model.fc_in.parameters()}, {params: [model.gate_scale, model.gate_bias]}, {params: model.q_weights, lr: 0.01}, ], lr0.001) for epoch in range(30): total_loss 0.0 h torch.zeros(1, 4) c torch.zeros(1, 4) for t in range(train_x.shape[0]): x_t train_x[t].unsqueeze(0) # (1, 8, 1) # 每个时间步依次送入只取最后一步输出做预测 for step in range(x_t.shape[1]): h, c model(x_t[:, step, :], h, c) pred h target train_y[t].unsqueeze(0) loss loss_fn(pred, target) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if epoch % 5 0: print(fepoch {epoch}, loss {total_loss / train_x.shape[0]:.5f})逻辑说明这个训练循环刻意简化了没有用 DataLoader也没有做 batch而是逐条样本前向。每个样本是一个长度为 8 的窗口窗口内逐步更新 h 和 c用最后输出的 h 去预测下一个点。这正是 LSTM 的经典用法——逐步消费序列末尾取状态。参数说明hidden_size 设为 4比常见 LSTM 小很多因为量子电路只编码降维后的向量4 个量子比特对应 4 个门控读数值隐藏状态太大反而让 fc_in 成为瓶颈。量子参数学习率 0.01 是刻意压小的经验表明 PQC 参数用与经典层相同的学习率容易震荡。如果想要更快看到效果可以把训练窗口从 8 缩短到 4但会牺牲一部分长期依赖的验证意义。4. qlstm 训练不稳定的避坑与排查4 条实测记录4.1 量子电路参数初始化为全零loss 原地踏步现象训练跑了十几个 epochloss 几乎不变打印梯度发现量子参数对应的梯度全为零。经典层 fc_in 还有变化但 q_weights 的梯度连 1e-6 都不到。原因所有 RY 旋转角为 0 时每个量子比特都处于 |0 态的基准状态变分层在数学上退化为恒等变换。不管输入怎么变测量输出都落在同一个值附近电路对数据完全不敏感梯度自然消失。这是我第一次跑通 qlstm 时踩过最典型的坑症状跟经典网络里全零初始化一模一样但表现更隐蔽因为 loss 确实在微降——那是经典层在硬扛。解决量子参数不要用 torch.nn.init.zeros_改用均匀分布或者小范围高斯分布。推荐用 0.1 乘 torch.rand确保初始旋转角在 [-0.1, 0.1] 附近既打破对称又不至于让初值太大导致门控饱和。self.q_weights torch.nn.Parameter(0.1 * torch.rand(q_depth, n_qubits, 2))4.2 状态向量模拟器训练正常换采样模式就崩现象在 default.qubit 的默认设置下训练验证集 loss 稳步下降。把 device 改成 shots1024 模拟真实测量发现 loss 曲线剧烈跳动甚至比初始值还高。原因shotsNone 时 PennyLane 返回精确期望值梯度是解析的shots1024 时每个 expval 带有限采样统计误差梯度变成有噪声的估计量。量子电路输出的方差大噪声直接混进门控信号。这不只是模拟器问题在真实量子硬件上更严重。解决训练阶段继续用精确模拟验证和推理阶段再切换成采样模式。切换时固定随机种子并把 shots 至少设到 1024 起步。如果目标是真机部署先画一张“shots 从 128 到 4096 对验证 loss 的影响”曲线找到可接受的噪声下限。这个步骤本质是在给量子噪声定预算。4.3 序列长度变长后 loss 大幅震荡现象序列长度 8 时训练稳定改成 32 后 loss 每隔几个 epoch 就跳高一个量级而且回升后降不回来。梯度范数偶尔飙到 100 以上。原因BPTT 展开的时间步越多梯度链越长。qlstm 里的 tanh 和 sigmoid 在门控饱和区域导数接近零加上量子电路的梯度是沿时间步累积的长序列下梯度爆炸或消失的概率大幅上升。经典 LSTM 有 peephole 和归一化技巧兜底qlstm 的量子门控还没有成熟的对齐手段。解决先加梯度裁剪这是最简单有效的防线。torch.nn.utils.clip_grad_norm_( [model.q_weights, model.fc_in.weight, model.gate_scale], max_norm1.0 )同时把 BPTT 截断。训练时不要把整个序列长度都展开最多往回看 16 步每一步只保留最近 16 步的计算图这样梯度链被强制截断长序列下的震荡会显著缓解。4.4 Adam 在量子参数上收敛反而更慢现象经典层参数在 Adam 下正常收敛qlstm 的量子参数更新缓慢50 个 epoch 后 q_weights 的绝对变化量还不到 0.01。loss 曲线表现为前面快速下降一阵然后长时间停滞。原因PQC 梯度由参数位移法则计算在模拟器里虽然用的是 backprop但电路本身的梯度分布和经典神经网络差异很大。量子参数的梯度方差大Adam 的一阶矩和二阶矩估计会被大梯度污染有效步长被压得很小。这不是 Adam 的错是两类参数的梯度尺度不匹配。解决把量子参数和经典参数分到不同的 param_group。实际经验是量子参数用 Adam 时学习率降到 0.005 到 0.01或者直接用 SGD 加 0.01 学习率经典层保持原来的 Adam 0.001。分开调度之后量子电路才真正开始学。optimizer torch.optim.Adam([ {params: model.fc_in.parameters()}, {params: [model.gate_scale, model.gate_bias]}, {params: model.q_weights, lr: 0.01}, ], lr0.001)5. qlstm 的参数选型与效果验证qubit 数、层数、序列长度怎么定5.1 四个可调参数的合理区间qlstm 的参数不像经典 LSTM 那样有大量公开调参经验但几个关键量的作用区间是能摸出来的。参数推荐区间调参后果n_qubits4 到 8决定门控表达的宽度过大会触发贫瘠高原q_depth1 到 3决定电路表达力过深训练成本飙升shots训练用 None验证用 1024 起步采样模式下影响梯度噪声量子参数学习率0.005 到 0.01过大震荡过小不学习n_qubits 是最敏感的参数。量子比特数量增加时随机初始化的 PQC 梯度方差会指数级下降这个现象叫贫瘠高原。表现是 loss 在某个值附近轻微抖动量子参数根本不动。4 到 8 个量子比特是大部分入门的可运行区间超过 10 个模拟器开销和梯度消失会同时找上门。q_depth 控制的是电路深度。1 层变分表达能力弱可能拟合不了复杂的门控函数3 层以上表达力够了但每个时间步的前向时间线性增加训练速度下降。经验上先固定 q_depth2跑通后再往 3 调不要一上来就堆深度。5.2 与经典 LSTM 对照实验怎么做验证 qlstm 是否值得做要和经典 LSTM 做对照但对照的公允性很容易被忽视。最常见的错误是拿不同隐藏尺寸的模型比参数量或者给 qlstm 加额外的降维层然后声称“参数更少效果更好”——这不科学。qlstm 的量子电路只有 16 个参数而经典 LSTM 同隐藏维度下每个门矩阵都有几十个参数两者参数预算天然不对等。合理的对照设计是固定训练步数、优化器、损失函数和输入序列长度只让两个模型的内部结构不同。经典 LSTM 用隐藏维度 4qlstm 用 4 量子比特。记录它们在同样的 1000 步训练内的 loss 下降曲线。这一步比的是“样本效率”和“收敛路径”不是最终精度——qlstm 如果能在更少步数内达到同样的测试 loss说明量子门控确实改变了优化地形。如果两者曲线几乎重合说明任务太简单量子部分没有发挥价值需要换更难序列再做一次判断。5.3 判断量子部分是否真的起作用替代实验只和经典 LSTM 比还不够还得回答“量子电路是否不可替代”。做法是把 PQC 换成相同输入输出维度的两层 MLP其余后处理逻辑完全不变。如果 MLP 版本表现同样好说明这套结构里的“量子”成分没有实际贡献。class MLPGate(torch.nn.Module): def __init__(self, in_dim, n_gates4): super().__init__() self.net torch.nn.Sequential( torch.nn.Linear(in_dim, 16), torch.nn.Tanh(), torch.nn.Linear(16, n_gates), ) def forward(self, cat): return self.net(cat)把 QLSTMCell 里的 quantum_gate_net 调用替换成这个 MLPGate其余前向逻辑不动。然后对比两条 loss 曲线如果量子版本的收敛曲线更陡或者最终 loss 显著更低说明 PQC 确实在任务里提供了经典 MLP 给不了的东西如果两条曲线交织重叠就该怀疑这个任务是否值得用量子电路来做了。6. 验证模型是否真的在“量子”基线与进阶用法判断 qlstm 是否“真的在量子”最简单的验证不是看流程图而是看替代实验的差距有多大。把 PQC 换成 MLP 之后如果 loss 曲线几乎重合说明当前任务下量子电路只是陪跑如果量子版本有明显更快的收敛或更低的测试误差才值得往深处投。另外可以加一个基线测试输入序列固定不变时连续两次推理的预测值是否一致。不一致说明有采样噪声或者电路状态没有正确重置这在模拟器里很罕见在真机后端会频繁出现提前测出来能避免后续浪费大量排错时间。进阶路线有两个方向值得尝试。一是记忆量子化把 c_t 从经典张量改成量子态遗忘操作真正的量子门写入操作也用受控门完成。代价是每个时间步都要做状态层析或测量训练成本上升一个量级换来的是记忆容量理论上不再受隐藏维度限制。二是测量基优化目前 QLSTM 所有门都用 PauliZ 测量换用不同的测量基等于给模型增加一组可学习旋转变换有时能直接改善门控表达力。我自己的实践习惯是每调一组参数先跑 MLP 门控基线再跑量子版本两条 loss 曲线贴在一起就说明还没找到值得量子的任务。这个项目方向值得试但前提是先相信对照实验别神化量子模块。希望帮到你。本文还有配套的精品资源点击获取
返回列表