Figure 1: Flow chart of VSQL

\n",
"\n",
"Here we give the whole pipeline to implement VSQL.\n",
"\n",
"1. Encode a classical data point $\\mathbf{x}^i$ into a quantum state $\\left|\\mathbf{x}^i\\right>$.\n",
"2. Prepare a parameterized local quantum circuit $U(\\mathbf{\\theta})$ and initialize its parameters $\\mathbf{\\theta}$.\n",
"3. Apply $U(\\mathbf{\\theta})$ on the first few qubits. Then, obtain a shadow feature via measuring a local observable, for instance, $X\\otimes X\\cdots \\otimes X$, on these qubits.\n",
"4. Sliding down $U(\\mathbf{\\theta})$ one qubit each time, repeat step 3 until the last qubit has been covered.\n",
"5. Feed all shadow features obtained from steps 3-4 to an FCNN and get the predicted label $\\tilde{\\mathbf{y}}^i$ through an activation function. For multi-label classification problems, we use the softmax activation function.\n",
"5. Repeat steps 3-5 until all data points in the data set have been processed. Then calculate the loss function $\\mathcal{L}(\\mathbf{\\theta}, \\mathbf{W}, \\mathbf{b})$.\n",
"6. Adjust the parameters $\\mathbf{\\theta}$, $\\mathbf{W}$, and $\\mathbf{b}$ through optimization methods such as gradient descent to minimize the loss function. Then we get the optimized model $\\mathcal{F}$.\n",
"\n",
"Since VSQL only extracts local shadow features, it can be easily implemented on quantum devices with topological connectivity limits. Besides, since the $U(\\mathbf{\\theta})$ used in circuits are identical, the number of parameters involved is significantly smaller than other commonly used variational quantum classifiers."
]
},
{
"cell_type": "markdown",
"id": "after-shadow",
"metadata": {},
"source": [
"## Paddle Quantum Implementation\n",
"\n",
"We will apply VSQL to classify handwritten digits taken from the MNIST dataset [3], a public benchmark dataset containing ten different classes labeled from '0' to '9'. Here, we consider a binary classification problem for the prupose of demonstration, in which only data labeled as '0' or '1' are used.\n",
"\n",
"First, we import the required packages."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "auburn-compound",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import numpy as np\n",
"import paddle\n",
"from paddle_quantum.circuit import UAnsatz\n",
"from paddle.vision.datasets import MNIST\n",
"import paddle.nn.functional as F\n",
"\n",
"paddle.set_default_dtype(\"float64\")"
]
},
{
"cell_type": "markdown",
"id": "violent-procurement",
"metadata": {},
"source": [
"### Data preprocessing\n",
"\n",
"Each image of a handwritten digit consists of $28\\times 28$ grayscale pixels valued in $[0, 255]$. We first flatten the $28\\times 28$ 2D matrix into a 1D vector $\\mathbf{x}^i$, and use amplitude encoding to encode every $\\mathbf{x}^i$ into a 10-qubit quantum state $\\left|\\mathbf{x}^i\\right>$. To do amplitude encoding, we first normalize each vector and pad it with zeros at the end."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "valuable-criterion",
"metadata": {},
"outputs": [],
"source": [
"# Normalize the vector and do zero-padding\n",
"def norm_img(images):\n",
" new_images = [np.pad(np.array(i).flatten(), (0, 240), constant_values=(0, 0)) for i in images]\n",
" new_images = [paddle.to_tensor(i / np.linalg.norm(i), dtype='complex128') for i in new_images]\n",
" \n",
" return new_images"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "ancient-philosophy",
"metadata": {},
"outputs": [],
"source": [
"def data_loading(n_train=1000, n_test=100):\n",
" # We use the MNIST provided by paddle\n",
" train_dataset = MNIST(mode='train')\n",
" test_dataset = MNIST(mode='test')\n",
" # Select data points from category 0 and 1\n",
" train_dataset = np.array([i for i in train_dataset if i[1][0] == 0 or i[1][0] == 1], dtype=object)\n",
" test_dataset = np.array([i for i in test_dataset if i[1][0] == 0 or i[1][0] == 1], dtype=object)\n",
" np.random.shuffle(train_dataset)\n",
" np.random.shuffle(test_dataset)\n",
" # Separate images and labels\n",
" train_images = train_dataset[:, 0][:n_train]\n",
" train_labels = train_dataset[:, 1][:n_train].astype('int64')\n",
" test_images = test_dataset[:, 0][:n_test]\n",
" test_labels = test_dataset[:, 1][:n_test].astype('int64')\n",
" # Normalize data and pad them with zeros\n",
" x_train = norm_img(train_images)\n",
" x_test = norm_img(test_images)\n",
" # Transform integer labels into one-hot vectors\n",
" train_targets = np.array(train_labels).reshape(-1)\n",
" y_train = paddle.to_tensor(np.eye(2)[train_targets])\n",
" test_targets = np.array(test_labels).reshape(-1)\n",
" y_test = paddle.to_tensor(np.eye(2)[test_targets])\n",
" \n",
" return x_train, y_train, x_test, y_test"
]
},
{
"cell_type": "markdown",
"id": "alive-spanish",
"metadata": {},
"source": [
"### Building the shadow circuit\n",
"\n",
"Now, we are ready for the next step. Before diving into details of the circuit, we need to clarify several parameters:\n",
"- $n$: the number of qubits encoding each data point.\n",
"- $n_{qsc}$: the width of the quantum shadow circuit . We only apply $U(\\mathbf{\\theta})$ on consecutive $n_{qsc}$ qubits each time.\n",
"- $D$: the depth of the circuit, indicating the repeating times of a layer in $U(\\mathbf{\\theta})$.\n",
"\n",
"Here, we give an example where $n = 4$ and $n_{qsc} = 2$.\n",
"\n",
"We first apply $U(\\mathbf{\\theta})$ to the first two qubits and obtain the shadow feature $O_1$.\n",
"\n",
"![qubit0](./figures/vsql-fig-qubit0.png \"Figure 2: The first circuit\")\n",
"Figure 2: The first circuit

\n",
"\n",
"Then, we prepare a copy of the same input state $\\left|\\mathbf{x}^i\\right>$, apply $U(\\mathbf{\\theta})$ to the two qubits in the middle, and obtain the shadow feature $O_2$.\n",
"\n",
"![qubit1](./figures/vsql-fig-qubit1.png \"Figure 3: The second circuit\")\n",
"Figure 3: The second circuit

\n",
"\n",
"Finally, we prepare another copy of the same input state, apply $U(\\mathbf{\\theta})$ to the last two qubits, and obtain the shadow feature $O_3$. Now we are done with this data point!\n",
"\n",
"![qubit2](./figures/vsql-fig-qubit2.png \"Figure 4: The last circuit\")\n",
"Figure 4: The last circuit

\n",
"\n",
"In general, we will need to repeat this process for $n - n_{qsc} + 1$ times for each data point. One thing to point out is that we only use one shadow circuit in the above example. When sliding the shadow circuit $U(\\mathbf{\\theta})$ through the $n$-qubit Hilbert space, the same parameters $\\mathbf{\\theta}$ are used. You can use more shadow circuits for complicated tasks, and different shadow circuits should have different parameters $\\mathbf{\\theta}$.\n",
"\n",
"Below, we will use a 2-local shadow circuit, i.e., $n_{qsc}=2$ for the MNIST classification task, and the circuit's structure is shown in Figure 5.\n",
"\n",
"![2-local](./figures/vsql-fig-2-local.png \"Figure 5: The 2-local shadow circuit design\")\n",
"Figure 5: The 2-local shadow circuit design

\n",
"\n",
"The circuit layer in the dashed box is repeated for $D$ times to increase the expressive power of the quantum circuit. The structure of the circuit is not unique. You can try to design your own circuit."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "derived-workstation",
"metadata": {},
"outputs": [],
"source": [
"# Construct the shadow circuit U(theta)\n",
"def U_theta(theta, n, n_start, n_qsc=2, depth=1):\n",
" # Initialize the circuit\n",
" cir = UAnsatz(n)\n",
" # Add layers of rotation gates\n",
" for i in range(n_qsc):\n",
" cir.rx(theta[0][0][i], n_start + i)\n",
" cir.ry(theta[0][1][i], n_start + i)\n",
" cir.rx(theta[0][2][i], n_start + i)\n",
" # Add D layers of the dashed box\n",
" for repeat in range(1, depth + 1):\n",
" for i in range(n_qsc - 1):\n",
" cir.cnot([n_start + i, n_start + i + 1])\n",
" cir.cnot([n_start + n_qsc - 1, n_start])\n",
" for i in range(n_qsc):\n",
" cir.ry(theta[repeat][1][i], n_start + i)\n",
"\n",
" return cir"
]
},
{
"cell_type": "markdown",
"id": "constant-scottish",
"metadata": {},
"source": [
"When $n_{qsc}$ is larger, the $n_{qsc}$-local shadow circuit can be constructed by extending this 2-local shadow circuit. Let's print a 4-local shadow circuit with $D=2$ to find out how it works."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "roman-radical",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--Rx(2.435)----Ry(5.433)----Rx(5.893)----*--------------X----Ry(0.430)----*--------------X----Ry(3.016)--\n",
" | | | | \n",
"--Rx(4.804)----Ry(2.413)----Rx(4.271)----X----*---------|----Ry(3.770)----X----*---------|----Ry(4.303)--\n",
" | | | | \n",
"--Rx(1.397)----Ry(0.021)----Rx(2.358)---------X----*----|----Ry(5.194)---------X----*----|----Ry(0.938)--\n",
" | | | | \n",
"--Rx(2.457)----Ry(0.133)----Rx(0.743)--------------X----*----Ry(2.042)--------------X----*----Ry(3.704)--\n",
" \n",
"---------------------------------------------------------------------------------------------------------\n",
" \n",
"---------------------------------------------------------------------------------------------------------\n",
" \n"
]
}
],
"source": [
"N = 6\n",
"NSTART = 0\n",
"NQSC = 4\n",
"D = 2\n",
"theta = paddle.uniform([D + 1, 3, NQSC], min=0.0, max=2 * np.pi)\n",
"cir = U_theta(theta, N, NSTART, n_qsc=NQSC, depth=D)\n",
"print(cir)"
]
},
{
"cell_type": "markdown",
"id": "extreme-abuse",
"metadata": {},
"source": [
"### Shadow features\n",
"\n",
"We've talked a lot about shadow features, but what is a shadow feature? It can be seen as a projection of a state from Hilbert space to classical space. There are various projections that can be used as a shadow feature. Here, we choose the expectation value of the Pauli $X\\otimes X\\otimes \\cdots \\otimes X$ observable on selected $n_{qsc}$ qubits as the shadow feature. In our previous example, $O_1 = \\left