# QCNN line-by-line implementation walkthrough **English** · 简体中文 This page explains the QCNN (arXiv:1911.01117, ICLR 2020) implementation line by line: first an end-to-end minimal example, then a segment-by-segment reading of the sources of the classical basis (`qcnn.py`) and the quantum building blocks (`qcnn_layer.py`). Every code block is runnable (`tests/core/test_qcnn.py` is built from these snippets). For the algorithm overview see the quantum convolutional neural network (QCNN) page; for the API reference see [qcnn](../api/algorithms/qml/qcnn.rst) and [qcnn_layer](../api/algorithms/qml/qcnn_layer.rst). ## 1. End-to-end minimal example ```python import random from oracq.algorithms.qml.qcnn import ConvSpec, convolution_forward from oracq.algorithms.qml.qcnn_layer import qcnn_sampled_layer # 5×5 single-channel input, 2×2 kernel ×2 output channels, capReLU cap 3.0, 2×2 max pooling. 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)] # input image (H×W×D flattened) kernel = [random.uniform(-1, 1) for _ in range(2 * 2 * 1 * 2)] # 4-tensor kernel flattened classical = convolution_forward(x, kernel, spec) # classical mirror (reference for comparison) pooled, stats = qcnn_sampled_layer(x, kernel, spec, samples=40000, eta=0.0, seed=1) ``` ## 2. The classical basis line by line (`qcnn.py`) ### ConvSpec — the layer specification (dimension rules of paper §3.3–3.4) ```python @dataclass(frozen=True) class ConvSpec: input_shape: tuple # (H^l, W^l, D^l): the three dimensions of the input tensor kernel_shape: tuple # (h, w, D^l, D^{l+1}): the 4-tensor kernel cap: float = 5.0 # capReLU cap C (§5.1.6: provides a known max f for the conditional rotation) pool: int = 1 # pooling window P (Eq. 39) pool_kind: str = "max" # "max" or "average" (the two overwrite rules of §5.2.2) ``` `output_shape` follows Eq. (4) of the paper: `H^{l+1} = H^l − h + 1` (no padding/stride); `pooled_shape` divides the output spatial dimensions by the pooling window (`__post_init__` enforces divisibility — a 5×5 input with a 2×2 kernel gives a 4×4 output, exactly divisible by 2). ### im2col — the matrix form of Eq. (6) ```python def im2col(x, spec): h, w, _ = spec.input_shape kh, kw, d, _ = spec.kernel_shape rows = [] for i in range(h - kh + 1): # each output row corresponds to one receptive field for j in range(w - kw + 1): row = [] for dd in range(d): # stack channel by channel (column-major vectorization, paper §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: row-major, one receptive field per row ``` Each row `A^l_p` is the vectorization of the receptive field at position (i,j); {obj}`kernel_columns ` expands each kernel into one column of `F^l` in the same (d, ki, kj) order — the two orders agree, so the inner product is exactly the convolution output `Y^{l+1}_{p,q}`. ### convolution_forward — the classical mirror ```python 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] # convolution + capReLU (§5.1.6) return pool_tensor(y, spec) # the pooling of Eq. 39 ``` `cap_relu = min(max(x, 0), cap)` — the paper uses the capped ReLU so that `max_pq f(Y_pq)` is known a priori (the cap constant), allowing the conditional rotation (Eq. 28) to use a fixed normalization. ### QCNNQRAM — the row-tree data structure (§4.3 + §5.2.2) ```python self.leaf_start = 1 while leaf_start < self.width: # complete binary tree, leaves start at leaf_start leaf_start *= 2 self.tree_size = 2 * leaf_start # 1-based heap: root is 1, internal nodes store squared partial sums ``` `_write(p, r, value)` writes one leaf and refreshes the squared partial sums up the ancestors — a single-point update costs logarithmically (the O(log² n) update of Theorem 4.3 in the paper). `update_with_pooling` implements the online overwrite: `max` writes only when the new value is higher (paper §5.2.2, "update the leaf if the new sampled value is higher"), and `average` amortizes with a state count ("replace the actual value by the new averaged value"). ## 3. The quantum building blocks line by line (`qcnn_layer.py`) ### vector_angle_tables — from data to runtime banks ```python angles[(index << width) | node] = round(angle * scale) % (1 << angle_width) ``` Line by line, node by node: `angle = 2·atan2(√S_right, √S_left)` (the RY half-angle convention, amplitude ratio √(S_right/S_node)); quantized into an `angle_width`-bit angle word. Signs go into a separate `signs` bank (key `(index<` — **index in the high bits, consistent with the bank key**); synthesize controlled RYs with bitwise weighting (each set angle-word bit k applies `ry(2π·2^k/2^aw)`); un-query to clean the work. The sign kickback at the end: query `signs` addressed by the already-prepared `target`, apply a Z on the flag, then un-query — negative components are left with only a −1 phase. ### qcnn_inner_product — Eqs. (16)–(19) ```python for register in (b["p"], b["q"], b["flag"]): b.h(register) # uniform superposition Σ|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"]) # amplitude √P_pq lands on the flag=0 branch ``` This is exactly the three-line derivation of Eq. (16) in the paper: branch loading of |A_p⟩/|F_q⟩ → Hadamard interference → measurement probability P_pq=(1+⟨A_p|F_q⟩)/2 on flag=0. Validation against Eq. (20): the joint probability of (p,q,flag=0) after summing out (p,q,vec,work) = (1+⟨A_p|F_q⟩)/(2·rows·columns), with a measured worst-case deviation of 4×10⁻⁵. ### qcnn_sampled_layer — the sampling driver of Eqs. (34)–(39) ```python values[(p, q)] = cap_relu(inner, spec.cap) # Y_pq=(2P_pq-1)||A_p||||F_q|| after activation weight = {key: value * value for key, value in values.items()} # sampling probability ∝ f(Y)^2 (Eq. 34); each sample yields the triple (p, q, f(Y)): if value < eta: # Eq. 36: pixels below η are treated as unsampled continue i, j = p // ow, p % ow # Eq. 7: recover the row number (i^{l+1}, j^{l+1}) index = (i // spec.pool) * pw + (j // spec.pool) # Eq. 39: pooling-region mapping pooled[index][q] = max(pooled[index][q], value) # QRAM online overwrite (§5.2.2) ``` {obj}`quantized_prepared_state ` is the classical mirror of the circuit semantics (level-by-level quantized angle rotations + signs); the driver uses it to recover all of Y — the circuit and the mirror are compared amplitude by amplitude in tests (1e-9), guaranteeing that the distribution modeling introduces no extra approximation. ## 4. Validation matrix (all passing) | Experiment | Scale | Result | |---|---|---| | im2col vs naive convolution | 5×5×1, kernel 2×2×1×2 | element-wise 1e-12 | | forward + pooling vs naive | same + 2×2 max | element-wise 1e-12 | | QRAM rows/norms/overwrite | 16 rows | exact; max keeps the higher value, average gives (2+4)/2=3 | | row-preparation circuit vs normalized row | with negative components | worst 3e-4 (= the lower bound of aw=10 angle quantization) | | circuit vs quantized mirror | same | 1e-9 | | inner-product probability vs Eq. 20 | 2×2 rows/columns | 4e-5 | | max-pooling sampled recovery | 40000 samples | differs from the classical maximum by ≤5e-3 | | η→∞ | 1000 samples | all zero (Eq. 36) | | average convergence | 60000 samples | to the f²-weighted expectation within ≤2e-2 |