encoder 模块提供了多种量子编码策略,用于将经典数据映射到量子态。编码器是量子机器学习流程中的关键组件,不同的编码策略会影响模型的表达能力和性能。
encoder/
├── __init__.py # 模块导出
├── image_encoder.py # 图像编码器基类
├── amplitude.py # 振幅编码
├── angle.py # 角度编码
├── basis.py # 基态编码
├── FRQI.py # 柔性量子图像表示
├── NEQR.py # 增强量子图像表示
├── QubitLattice.py # 量子点阵编码
└── ZZFeature.py # ZZ 特征编码
AngleEncoder.to_ansatz() 和 ZZFeatureEncoder.to_ansatz() 将编码电路前置于已有 Ansatz,返回独立的基础 Ansatz。输入自动成为电路符号,因此可直接求输入 Jacobian,也可以接入 Torch,训练编码之前的经典网络。原有 encoder(data) 仍返回数值电路,接口和公式不变。
from cqlib.circuit import Parameter
from cqlib_qml.ansatz import Ansatz
from cqlib_qml.encoder import AngleEncoder
body = Ansatz(2, random_state=7)
body.ry(0, Parameter("theta"))
body.cx(0, 1)
body.set_measurement(readouts=[0, 1])
body.set_differentiator("adjoint") # 或 parameter_shift
encoded = AngleEncoder().to_ansatz(body, num_features=2)
# encoded.input_params == ["x_0", "x_1"];body 保持原样
values = encoded.forward([[.2, .4], [.3, -.1]])
encoded.backward()
print(encoded.input_jacobian) # (batch, outputs, inputs)统一签名为 encoder.to_ansatz(ansatz, *, num_features, input_prefix="x")。输入按 x_0, x_1, …, x_10, … 的数字顺序绑定;input_prefix 必须完整匹配 [A-Za-z_][A-Za-z0-9_]*,生成的名称与已有符号冲突时拒绝。num_features 和此入口的 ZZ 重复次数必须是正整数,接受 np.integer,拒绝 Python/NumPy 布尔值。ZZ 构造函数和数值入口的重复次数行为不变。
| 编码 | 宽度要求 | 每层公式 |
|---|---|---|
| Angle classical | Q = num_features |
第 i 位执行 RY(2*x_i) |
| Angle dense | Q = ceil(num_features/2) |
第 i 位依次执行 RY(2*x_2i)、RZ(x_2i+1);最后缺失的相位取 0 |
| ZZ | Q = num_features |
每次重复依次执行全部 H、RZ(2*π*x_i)、各边的 RZZ((π-x_i)*(π-x_j)) |
ZZ 支持 linear、full 和 circular。重复层共享同一组输入符号,输入梯度包含所有出现位置的贡献。保持既有数值编码的边顺序;两比特 circular 包含 (0,1) 和 (1,0) 两条边。dense 的相位是否影响输出取决于后续电路及测量;直接测 Z 可能对相位不敏感,这不代表梯度链路丢失。
组合入口要求源 Ansatz 没有已声明的输入、没有 add_encoder() 附着的数值编码。已有权重角色必须仍完整覆盖源电路符号;未声明角色时按原有符号顺序作为权重。支持空训练电路和零权重;测量可稍后配置,但执行前必须满足现有测量要求。
源 qubit ID 必须是 0…Q−1 的排列。编码特征与源 qubits 的位置顺序对应,返回对象统一编号为 0…Q−1。例如源顺序 [1,0] 中,源 ID 1 映射到返回对象的 ID 0,源 ID 0 映射到 ID 1。readouts 沿用原接口的位置索引语义,Hamiltonian 也保持位置顺序。稀疏编号如 [5,2] 在入口拒绝。
返回对象独立拥有电路、已赋权重、测量、微分配置和 RNG;修改任一对象不影响另一对象。不会继承源子类的其他属性、优化器、训练缓存或累计梯度,也不会消耗源 RNG。新对象始终 training=True、trainable=True,updatable 由是否存在权重决定。
权重初始化和保存恢复的边界如下:
| 接口 | 行为 |
|---|---|
to_ansatz() |
保留完整或部分已有权重,不初始化缺失项 |
Ansatz.forward(X) |
保留已赋权重,仅初始化缺失项;只接受一维/二维输入,单样本输出仍为 (1, out_dim) |
QuantumLayer(encoded) |
完整权重才继承;部分赋值时整组使用 Torch 初始化。需要保留部分值时显式传完整的 initial_weights |
原生 summary / load_params() |
沿用原生结构校验;非空部分权重需补齐后恢复。输出维度须兼容,加载会恢复保存的测量、微分及 RNG 等原生状态 |
Torch state_dict() |
校验实际电路、参数角色、测量和微分配置;优化器单独保存 |
checkpoint 不记录编码器配置身份:例如两比特 ZZ 的 linear 和 full 若生成相同电路,不承诺因名称不同而拒绝。返回基础 Ansatz 的恢复目标应通过相同组合入口重建。
本入口仅覆盖上述 Angle 和 ZZ 编码。振幅、基态、图像编码的可微适配分别评估;现有 QNN/HQNN 调用方式保持不变。Torch 联合训练、冻结和 batch 语义见 Torch 教程。
量子计算机处理的是量子态,而经典数据(如图像、向量)需要被转换为量子态才能输入量子电路。编码器的本质是构造一个酉变换
不同的编码策略将数据映射到量子态的不同自由度:
- 振幅编码:数据 → 量子态振幅
- 角度编码:数据 → 量子门旋转角度
- 基态编码:数据 → 计算基态
- 图像编码:数据 → 量子图像表示(FRQI/NEQR)
| 数据类型 | 推荐编码器 | 量子比特数 | 特点 |
|---|---|---|---|
| 向量数据 | AngleEncoder | 特征数或特征数/2 | 简单高效 |
| 高维向量 | AmplitudeEncoder | log2(特征数) | 指数压缩 |
| 非负整数 | BasisEncoder | log2(最大值+1) | 直接映射 |
| 二值图像 | QubitLattice | 像素数 | 直观 |
| 灰度图像 | FRQI | 2×log2(边长)+1 | 高效 |
| 多灰度图像 | NEQR | 2×log2(边长)+log2(灰度级) | 精确 |
| 核方法 | ZZFeatureEncoder | 特征数 | 适合 SVM |
ImageEncoder 是所有图像编码器的抽象基类,提供图像批次检测和验证功能。
from cqlib_qml.encoder import ImageEncoder
from abc import abstractmethod
class CustomEncoder(ImageEncoder):
def __init__(self, n_pixels):
super().__init__(n_pixels)
def __call__(self, imgs, **kwargs):
# 实现编码逻辑
pass
def _construct_encoder(self):
# 构建编码电路
pass
ImageEncoder._is_imgs_batch() 自动检测输入是否为批次:
| 输入形状 | 判断结果 |
|---|---|
(H, W) |
单张图像 |
(B, H, W) |
批次(单通道) |
(B, H, W, C) |
批次(多通道) |
振幅编码将数据向量
其中
振幅编码使用
import numpy as np
from cqlib_qml.encoder import AmplitudeEncoder
encoder = AmplitudeEncoder()
# 单样本编码(4 维向量 → 2 量子比特)
data = np.array([1.0, 0.0, 0.0, 0.0])
circuits = encoder(data)
print(f"量子比特数: {circuits[0].num_qubits}")
# 多样本编码
data_batch = np.array([[0.5, 0.5, 0.5, 0.5], [1.0, 0.0, 0.0, 0.0]])
circuits = encoder(data_batch)
print(f"样本数量: {len(circuits)}")
print(f"量子比特数: {circuits[0].num_qubits}")
输出:
量子比特数: 2
样本数量: 2
量子比特数: 2
如果数据维度不是 2 的幂次方,振幅编码会自动填充零:
# 3 维向量 → 填充到 4 维 → 2 量子比特
data = np.array([1.0, 0.0, 0.0])
circuits = encoder(data)
print(f"量子比特数: {circuits[0].num_qubits}")
输出:
量子比特数: 2
振幅编码会自动归一化输入向量:
# 自动归一化
data = np.array([2.0, 0.0, 0.0, 0.0])
circuits = encoder(data)
# 实际存储的振幅为 [1.0, 0.0, 0.0, 0.0]
角度编码将数据特征映射为量子门的旋转角度。
经典模式:每个特征对应一个 RY 门:
密集模式:每个量子比特编码两个特征(RY + RZ):
import numpy as np
from cqlib_qml.encoder import AngleEncoder
# 经典模式:每个特征一个量子比特
encoder = AngleEncoder(mode="classical")
data = np.array([0.5, 0.3, 0.7, 0.2])
circuits = encoder(data)
print(f"经典模式量子比特数: {circuits[0].num_qubits}")
# 密集模式:每个量子比特编码两个特征
encoder = AngleEncoder(mode="dense")
circuits = encoder(data)
print(f"密集模式量子比特数: {circuits[0].num_qubits}")
输出:
经典模式量子比特数: 4
密集模式量子比特数: 2
data_batch = np.array([[0.5, 0.3], [0.7, 0.2], [0.1, 0.9]])
encoder = AngleEncoder(mode="classical")
circuits = encoder(data_batch)
print(f"样本数量: {len(circuits)}")
输出:
样本数量: 3
基态编码将非负整数
其中
import numpy as np
from cqlib_qml.encoder import BasisEncoder
encoder = BasisEncoder()
# 编码整数 0-3
data = np.array([0, 1, 2, 3])
circuits = encoder(data)
for i, circ in enumerate(circuits):
print(f"{i} -> {circuits[0].num_qubits} 量子比特")
输出:
0 -> 2 量子比特
1 -> 2 量子比特
2 -> 2 量子比特
3 -> 2 量子比特
量子比特数由最大整数决定:$n = \lceil \log_2(\max(data) + 1) \rceil$
data = np.array([0, 5, 10, 15])
circuits = encoder(data)
print(f"最大值为 15,量子比特数: {circuits[0].num_qubits}")
输出:
最大值为 15,量子比特数: 4
# 负数会报错
data = np.array([-1, 0, 1])
try:
circuits = encoder(data)
except ValueError as e:
print(f"错误: {e}")
输出:
错误: Basis encoding only supports encoding non-negative integers.
FRQI (Flexible Representation of Quantum Images) 将
其中:
-
$\theta_i \in [0, \pi/2]$ 编码像素值 -
$|i\rangle$ 是位置量子比特状态 - 总量子比特数:$2n + 1$($2n$ 个位置量子比特 + 1 个颜色量子比特)
FRQI(
n_pixels: int,
grayscale: int = 2
)
| 参数 | 类型 | 描述 |
|---|---|---|
n_pixels |
int | 像素总数,必须为 |
grayscale |
int | 灰度级数量,默认 2 |
import numpy as np
from cqlib_qml.encoder import FRQI
from cqlib_qml.data.data_preprocess import change_grayscale
# 1. 创建编码器(4x4 图像,2 灰度级)
encoder = FRQI(n_pixels=16, grayscale=2)
# 2. 准备图像(必须符合灰度级要求)
img = np.random.rand(4, 4)
img_quantized = change_grayscale(img, grayscale=2)
# 3. 编码单张图像
circuit = encoder(img_quantized)
print(f"量子比特数: {circuit.num_qubits}")
# 4. 编码批量图像
imgs = np.random.rand(8, 4, 4)
imgs_quantized = change_grayscale(imgs, grayscale=2)
circuits = encoder(imgs_quantized)
print(f"图像数量: {len(circuits)}")
输出:
量子比特数: 5
图像数量: 8
# 4x4 图像 → 16 像素 → 2n + 1 = 5 量子比特
encoder = FRQI(n_pixels=16, grayscale=2)
print(f"4x4 FRQI 量子比特数: {encoder._n_qubits}")
# 8x8 图像 → 64 像素 → 2n + 1 = 7 量子比特
encoder = FRQI(n_pixels=64, grayscale=2)
print(f"8x8 FRQI 量子比特数: {encoder._n_qubits}")
输出:
4x4 FRQI 量子比特数: 5
8x8 FRQI 量子比特数: 7
# 启用量子图像压缩(优化电路)
circuit = encoder(img_quantized, use_qic=True)
NEQR (Novel Enhanced Quantum Representation) 是 FRQI 的增强版本,使用多个颜色量子比特表示灰度值:
其中:
-
$|c_i\rangle$ 是$q$ 个颜色量子比特编码像素值 $q = \log_2(\text{grayscale})$ - 总量子比特数:$2n + q$
NEQR(
n_pixels: int,
grayscale: int = 2
)
| 参数 | 类型 | 描述 |
|---|---|---|
n_pixels |
int | 像素总数,必须为 |
grayscale |
int | 灰度级数量,必须为 2 的幂次方 |
import numpy as np
from cqlib_qml.encoder import NEQR
from cqlib_qml.data.data_preprocess import change_grayscale
# 1. 创建编码器(4x4 图像,4 灰度级)
encoder = NEQR(n_pixels=16, grayscale=4)
# 2. 准备图像
img = np.random.rand(4, 4)
img_quantized = np.rint(change_grayscale(img, grayscale=4) * 3).astype(int)
# NEQR 使用整数颜色索引 0..3;FRQI 使用归一化灰度值
# 3. 编码
circuit = encoder(img_quantized)
print(f"量子比特数: {circuit.num_qubits}")
print(f"颜色量子比特数: {encoder._n_color_qubits}")
输出:
量子比特数: 6
颜色量子比特数: 2
| 特性 | FRQI | NEQR |
|---|---|---|
| 颜色量子比特 | 1 | |
| 灰度级数 | 2 | 任意 2 的幂次方 |
| 精度 | 低 | 高 |
| 量子比特数 |
QubitLattice 将二值图像的每个像素映射到一个量子比特:
其中
QubitLattice(
n_pixels: int
)
| 参数 | 类型 | 描述 |
|---|---|---|
n_pixels |
int | 像素总数 |
import numpy as np
from cqlib_qml.encoder import QubitLattice
# 1. 创建编码器
encoder = QubitLattice(n_pixels=9)
# 2. 准备二值图像(3x3)
img = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
# 3. 编码
circuit = encoder(img)
print(f"量子比特数: {circuit.num_qubits}")
输出:
量子比特数: 9
ZZFeatureEncoder 受量子核方法启发,使用 ZZ 纠缠门创建特征空间:
- 应用 Hadamard 门将所有量子比特置入叠加态
- 用 RZ 门编码特征:$RZ(2\pi x_i)$
- 用 RZZ 门创建纠缠:$RZZ((\pi - x_i)(\pi - x_j))$
ZZFeatureEncoder(
n_repeats: int = 2,
entanglement: str = "linear"
)
| 参数 | 类型 | 描述 |
|---|---|---|
n_repeats |
int | 编码层重复次数 |
entanglement |
str | 纠缠模式:"linear", "full", "circular" |
| 模式 | 描述 | 量子比特连接 |
|---|---|---|
"linear" |
最近邻连接 | 0-1, 1-2, 2-3, ... |
"full" |
全连接 | 所有量子比特两两连接 |
"circular" |
环形连接 | 0-1, 1-2, ..., n-1-0 |
import numpy as np
from cqlib_qml.encoder import ZZFeatureEncoder
# 1. 线性纠缠
encoder = ZZFeatureEncoder(n_repeats=2, entanglement="linear")
data = np.array([0.5, 0.3, 0.7])
circuits = encoder(data)
print(f"线性纠缠量子比特数: {circuits[0].num_qubits}")
# 2. 全连接纠缠
encoder = ZZFeatureEncoder(n_repeats=1, entanglement="full")
circuits = encoder(data)
print(f"全连接纠缠量子比特数: {circuits[0].num_qubits}")
# 3. 环形纠缠
encoder = ZZFeatureEncoder(n_repeats=2, entanglement="circular")
circuits = encoder(data)
print(f"环形纠缠量子比特数: {circuits[0].num_qubits}")
输出:
线性纠缠量子比特数: 3
全连接纠缠量子比特数: 3
环形纠缠量子比特数: 3
data_batch = np.array([[0.5, 0.3, 0.7], [0.2, 0.8, 0.4]])
encoder = ZZFeatureEncoder(n_repeats=1, entanglement="linear")
circuits = encoder(data_batch)
print(f"样本数量: {len(circuits)}")
输出:
样本数量: 2
| 数据类型 | 推荐编码器 | 理由 |
|---|---|---|
| 图像(二值) | QubitLattice | 直观,无需额外量子比特 |
| 图像(灰度) | FRQI | 高效,适合中等灰度 |
| 图像(高精度) | NEQR | 精确,支持多灰度级 |
| 向量(低维) | AngleEncoder | 简单,易于理解 |
| 向量(高维) | AmplitudeEncoder | 指数压缩 |
| 整数标签 | BasisEncoder | 直接映射 |
| 核方法 | ZZFeatureEncoder | 适合 SVM |
| 可用量子比特数 | 推荐编码器 |
|---|---|
| 少(< 10) | AngleEncoder, FRQI |
| 中(10-20) | AmplitudeEncoder, ZZFeatureEncoder |
| 多(> 20) | QubitLattice, NEQR |
| 精度要求 | 推荐编码器 |
|---|---|
| 低 | FRQI(2 灰度级) |
| 中 | AngleEncoder, ZZFeatureEncoder |
| 高 | NEQR(多灰度级) |
import numpy as np
from cqlib_qml.data.data_preprocess import change_grayscale
# 确保数据符合编码器要求
img = np.random.rand(4, 4)
img_quantized = change_grayscale(img, grayscale=2) # 量化到 2 灰度级
# 所有编码器都支持批次处理
imgs = np.random.rand(32, 4, 4)
circuits = encoder(imgs)
from cqlib_qml.data.data_preprocess import get_mnist_dataloader
from cqlib_qml.encoder import FRQI
encoder = FRQI(n_pixels=16, grayscale=2)
train_loader, test_loader = get_mnist_dataloader(
classes=[0, 1],
resize=(4, 4),
encoding=encoder,
batch_size=32,
grayscale=2
)
原因:图像尺寸不是
解决方案:
# 使用 4x4, 8x8, 16x16 等尺寸
RESIZE = (4, 4) # 正确
RESIZE = (8, 8) # 正确
RESIZE = (5, 5) # 错误
原因:图像像素值不符合指定的灰度级数量。
解决方案:
from cqlib_qml.data.data_preprocess import change_grayscale
# 先量化,再编码
img_quantized = change_grayscale(img, grayscale=2)
circuit = encoder(img_quantized)
原因:输入向量全为零。
解决方案:
# 确保输入向量非零
data = np.array([0.0, 1.0, 0.0, 0.0]) # 非零
| 方法 | 描述 |
|---|---|
__call__(data) |
振幅编码 |
| 方法 | 描述 |
|---|---|
__call__(data) |
角度编码 |
| 方法 | 描述 |
|---|---|
__call__(data) |
基态编码 |
| 方法 | 描述 |
|---|---|
__call__(imgs, use_qic=False) |
FRQI 编码 |
| 方法 | 描述 |
|---|---|
__call__(imgs, use_qic=False) |
NEQR 编码 |
| 方法 | 描述 |
|---|---|
__call__(imgs) |
量子点阵编码 |
| 方法 | 描述 |
|---|---|
__call__(data) |
ZZ 特征编码 |
FRQI/NEQR 接受 NumPy 数组和 Tensor;Tensor 会先 detach 并移到 CPU,不保留 torch autograd 关系。FRQI 按每张图像的最大像素值归一化,因此全 0.2 与全 0.8 图像会得到相同量子态;当前默认行为不保存绝对亮度差异。NEQR 接受整数颜色索引。BasisEncoder 接受整数及整数值浮点数组,拒绝空、负数、非整数和非有限输入。
FRQI 的普通与 QIC 编码仅跳过精确零像素,微小正数图像仍按最大像素值归一化;全零图像不执行颜色旋转。FRQI/NEQR 的单像素 QIC 使用无位置控制的颜色门。
振幅编码分别缩放实部与虚部,并对递归子向量使用稳定范数,避免大数与次正规数(例如 1e-310)导致归一化溢出或下溢。低于浮点可表示范围的结果无法保证保留。cqlib 2.0.0b3 模拟极小旋转(例如 RY(2e-150))时可能丢失极小振幅,生成线路的角度正确不代表底层模拟能保留所有尺度。