QCNN 逐行实现讲解

English · 简体中文

本页把 QCNN(arXiv:1911.01117,ICLR 2020)的实现逐行讲清:先给端到端最小示例,再逐段读经典基底(qcnn.py)与量子构件(qcnn_layer.py)的源码。所有代码块均可运行(tests/core/test_qcnn.py 即由这些片段构成)。算法概述见量子卷积神经网络(QCNN);API 参考见 qcnn 与 qcnn_layer。

1. 端到端最小示例

import random
from oracq.algorithms.qml.qcnn import ConvSpec, convolution_forward
from oracq.algorithms.qml.qcnn_layer import qcnn_sampled_layer

# 5×5 单通道输入,2×2 核 ×2 个输出通道,capReLU 上限 3.0,2×2 max 池化。
spec = ConvSpec(input_shape=(5, 5, 1), kernel_shape=(2, 2, 1, 2), cap=3.0, pool=2)
random.seed(7)
x = [random.uniform(-1, 1) for _ in range(25)]            # 输入图像(H×W×D 展平)
kernel = [random.uniform(-1, 1) for _ in range(2 * 2 * 1 * 2)]  # 4-张量核展平
classical = convolution_forward(x, kernel, spec)          # 经典镜像(对照参考)
pooled, stats = qcnn_sampled_layer(x, kernel, spec, samples=40000, eta=0.0, seed=1)

2. 经典基底逐行(qcnn.py)

ConvSpec——层规格(论文 §3.3–3.4 的维度规则)

@dataclass(frozen=True)
class ConvSpec:
    input_shape: tuple      # (H^l, W^l, D^l):输入张量三维度
    kernel_shape: tuple     # (h, w, D^l, D^{l+1}):4-张量核
    cap: float = 5.0        # capReLU 上限 C(§5.1.6:为条件旋转提供已知的 max f)
    pool: int = 1           # 池化窗口 P(Eq. 39)
    pool_kind: str = "max"  # "max" 或 "average"(§5.2.2 的两种覆写规则)

output_shape 按论文 Eq. (4):H^{l+1} = H^l − h + 1(无 padding/stride);pooled_shape 按池化窗口整除输出空间维度(__post_init__ 强制整除,5×5 输入配 2×2 核得 4×4 输出,恰好被 2 整除)。

im2col——Eq. (6) 的矩阵化

def im2col(x, spec):
    h, w, _ = spec.input_shape
    kh, kw, d, _ = spec.kernel_shape
    rows = []
    for i in range(h - kh + 1):        # 每个输出行对应一个感受野
        for j in range(w - kw + 1):
            row = []
            for dd in range(d):        # 逐通道堆叠(列优先向量化,论文 §3.4)
                for ki in range(kh):
                    for kj in range(kw):
                        row.append(tensor_get(x, spec.input_shape, i + ki, j + kj, dd))
            rows.append(row)
    return rows                        # A^l:行主序,每行一个感受野

每一行 A^l_p 是位置 (i,j) 感受野的向量化;kernel_columns 用同一 (d, ki, kj) 顺序把每个核展开为 F^l 的一列——两者顺序一致,内积即为卷积输出 Y^{l+1}_{p,q}。

convolution_forward——经典镜像

a = im2col(x, spec)
f = kernel_columns(kernel, spec)
y = [[cap_relu(sum(ar * fc for ar, fc in zip(row, col, strict=True)), spec.cap)
      for col in f] for row in a]     # 卷积 + capReLU(§5.1.6)
return pool_tensor(y, spec)           # Eq. 39 的池化

cap_relu = min(max(x, 0), cap)——论文用带 cap 的 ReLU 保证 max_pq f(Y_pq) 先验已知(cap 常数),条件旋转(Eq. 28)因此可用固定归一化。

QCNNQRAM——行树数据结构(§4.3 + §5.2.2)

self.leaf_start = 1
while leaf_start < self.width:      # 完全二叉树,叶从 leaf_start 起
    leaf_start *= 2
self.tree_size = 2 * leaf_start     # 1-based 堆:根为 1,内部节点存平方部分和

_write(p, r, value) 写一个叶并沿祖先向上刷新平方部分和——单点更新的代价是对数级(论文 Theorem 4.3 的 O(log² n) 更新)。update_with_pooling 实现在线覆写:max 只在新值更高时写(论文 §5.2.2 "update the leaf if the new sampled value is higher"),average 以状态计数做均摊("replace the actual value by the new averaged value")。

3. 量子构件逐行(qcnn_layer.py)

vector_angle_tables——数据到运行时 bank

angles[(index << width) | node] = round(angle * scale) % (1 << angle_width)

逐行逐节点:angle = 2·atan2(√S_right, √S_left)(RY 半角约定,幅度比为 √(S_right/S_node));量化为 angle_width 位角字。符号单独入 signs bank(键 (index<<width)|leaf)。换一层的数据只需重算这两个表——线路对象完全不变(QRAM 语义)。

qcnn_vector_prep——Eq. (15) 的行装载电路

for depth in range(vector_width):          # 树层:bit 从高位到低位
    for prefix in range(1 << depth):
        node = (1 << depth) - 1 + prefix   # 树节点编号(与 qram_state_prep 编址一致)
        node_register = _constant_register(b, f"node_{depth}_{prefix}", vector_width, node)
        if depth:
            with b.control(b["target"][bit + 1:], prefix):   # 已确定的高位做前缀控制
                _prep_node_rotation(b, angles, node_register, bit, angle_width)
        else:
            _prep_node_rotation(b, angles, node_register, bit, angle_width)
        # 对称恢复 node_register(X 反转回去)

_prep_node_rotation 的三步:查询 angles(地址 fuse(node_register, index)——index 在高位,与 bank 键一致);按位加权合成受控 RY(每个置位角字位 k 施加 ry(2π·2^k/2^aw));反查询复净 work。末尾的符号反冲:以已制备的 target 为地址查 signs,Z 旗标后反查——负分量只留下 −1 相位。

qcnn_inner_product——Eq. (16)–(19)

for register in (b["p"], b["q"], b["flag"]):
    b.h(register)                                  # 均匀叠加 Σ|p>|q> (1/√2)(|0>+|1>)
with b.control(b["flag"], 0):
    invoke(b, row_prep, "row", index=b["p"], target=b["vec"], work=b["work"])
with b.control(b["flag"], 1):
    invoke(b, column_prep, "col", index=b["q"], target=b["vec"], work=b["work"])
b.h(b["flag"])                                     # 幅度 √P_pq 落在 flag=0 分支

这正是论文 Eq. (16) 的三行推导:分支装载 |A_p⟩/|F_q⟩ → Hadamard 干涉 → flag=0 的测量概率 P_pq=(1+⟨A_p|F_q⟩)/2。验证对照 Eq. (20):对 (p,q,vec,work) 求和后的 (p,q,flag=0) 联合概率 = (1+⟨A_p|F_q⟩)/(2·行数·列数),实测最差偏差 4×10⁻⁵。

qcnn_sampled_layer——Eq. (34)–(39) 的采样驱动

values[(p, q)] = cap_relu(inner, spec.cap)          # Y_pq=(2P_pq-1)||A_p||||F_q|| 后激活
weight = {key: value * value for key, value in values.items()}
# 采样概率 ∝ f(Y)^2(Eq. 34),每次采样得三元组 (p, q, f(Y)):
if value < eta:      # Eq. 36:低于 η 的像素视为未采样
    continue
i, j = p // ow, p % ow                       # Eq. 7:行号还原 (i^{l+1}, j^{l+1})
index = (i // spec.pool) * pw + (j // spec.pool)   # Eq. 39:池化区域映射
pooled[index][q] = max(pooled[index][q], value)    # QRAM 在线覆写(§5.2.2)

quantized_prepared_state 是电路语义的经典镜像(逐层量化角旋转 + 符号),驱动用它恢复全部 Y——电路与镜像在测试中逐振幅对照(1e-9),保证分布建模不引入额外近似。

4. 验证矩阵(全部通过)

实验

规模

结论

im2col vs 朴素卷积

5×5×1, 核 2×2×1×2

逐元素 1e-12

前向+池化 vs 朴素

同上 + 2×2 max

逐元素 1e-12

QRAM 行/范数/覆写

16 行

精确;max 保高、average (2+4)/2=3

行制备电路 vs 归一化行

含负分量

最差 3e-4(=aw=10 角量化下界)

电路 vs 量化镜像

同上

1e-9

内积概率 vs Eq. 20

2×2 行列

4e-5

max 池化采样恢复

40000 样本

与经典最大值差 ≤5e-3

η→∞

1000 样本

全零(Eq. 36)

average 收敛

60000 样本

到 f² 加权期望 ≤2e-2