The new Qiskit Textbook beta is now available. Try it out now
PyTorchとQiskitを用いた量子古典ハイブリッド・ニューラル・ネットワーク

機械学習 (Machine learning、ML) は、データから一般化された情報を数学的に抽出しようとする学際的分野を自分自身で確立しました。量子コンピューターを利用することで、機械学習を補完するため量子力学の原理を利用したり、あるいはその逆を行う興味深い研究分野が増加しています。 難しい計算を量子コンピューターに任せることで古典MLアルゴリズムを拡張するにせよ、古典MLアルゴリズムを用いて量子アルゴリズムを最適化するにせよ、どちらも量子機械学習(quantum machine learning、QML)の多種多様な傘下にあるのです。

この章では、古典的なニューラル・ネットワークを部分的に量子化して、ハイブリッド量子古典ニューラル・ネットワークを作成する方法を探ります。Qiskit と最先端のオープンソース・ソフトウェアである PyTorch を統合して、簡単な例をコード化します。この例の目的は、Qiskitと既存のMLツールとの統合が容易であることを示し、MLの専門家が量子コンピューティングで何が可能かを探求することを奨励することにあります

目次

  1. どのように動作するのか?
    1.1 導入
  2. 量子はどのように画像を入力するのか?
  3. いざ、コーディング!
    3.1 インポート
    3.2 Qiskitを用いた「量子クラス」の作成
    3.3 PyTorchを用いた「量子古典クラス」の作成
    3.4 データのロードと前処理
    3.5 ハイブリッド・ニューラル・ネットワークの作成
    3.6 ネットワークの学習
    3.7 ネットワークのテスト
  4. 次は何?

1. どのように動作するのか?

図.1 は、本章で構築するフレームワークを描いたものです。最終的には、手書きの数字を分類しようとする、量子古典ハイブリッド・ニューラル・ネットワークを構築します。 この図に示されているエッジはすべて下方を向いていますが、方向性は視覚的に示されていないことに注意してください。

1.1 導入

ここで語られる古典ニューラル・ネットワークの背景には、関連する知識や共通の用語の確立も含まれますが、俄然とてもハイレベルなものです。もし古典ニューラル・ネットワークに深く踏み込みたい場合は、YouTuberによるよくできたビデオシリーズ 3Blue1Brown をご覧ください。または、もし既に古典ネットワークに詳しい場合は 次のセクションにスキップ できます。

ニューロンと重み

ニューラル・ネットワークは、最終的には、ニューロンと呼ばれる小さなビルディング・ブロックを構成することで作られる精巧な機能です。ニューロン は、通常シンプルで計算しやすい非線形の関数で、1つ以上の入力を1つの実数にマップします。ニューロンの一つの出力は、通常他のニューロンへの入力としてコピーされフィードされます。図示する場合、ニューロンをグラフ内のノードとして表し、あるニューロンの出力が他のニューロンへの入力としてどのように使用されるかを示すため、ノード間の有向エッジを描画します。また、グラフ内の各エッジは、しばしば重みと呼ばれるスカラー値に関連付けられることに注意する必要があります。ここでの考え方は、ニューロンへの各入力はそれぞれ異なるスカラー値が乗じられた後、集められて1つの値に処理されるということです。ニューラル・ネットワークの学習の主たる目的は、特定の方法でネットワークが振る舞うよう重みを決めることです。

フィードフォワード・ニューラル・ネットワーク

また、私たちが関心のあるタイプのニューラル・ネットワークは、フィードフォワード・ニューラル・ネットワーク (Feed-Forward Neural Network, FFNN) と呼ばれることにも注目する必要があります。これは、ニューラル・ネットワークにデータが流れるとき、データは一度訪れているニューロンに戻ることがないことを意味します。言い換えるならば、このニューラル・ネットワークを記述するグラフは、有向非巡回グラフ (Directed Acyclic Graph, DAG) だということです。さらに、ニューラル・ネットワークの同じ層にあるニューロンは、それらの間にエッジがないことも条件となります。

層のIO構造

ニューラル・ネットワークへの入力は、古典的な(実数値の)ベクトルです。入力ベクトルの各成分は、ネットワークのグラフ構造に従って、異なる重みを乗じられ、ニューロンの層にフィードされます。 層内の各ニューロンが評価された後、結果は新しいベクトルに集められ、 i番目の成分はi番目のニューロンの出力を記録します。この新しいベクトルは、新しい層の入力として処理され、以下同様に処理されます。ネットワークの最初と最後以外の層は、隠れ層 という標準的用語を使って表現しましょう。

2. 量子はどのように画像を入力するのか?

量子古典ニューラル・ネットワークを構築するには、パラメータ化された量子回路を使用してニューラル・ネットワークの隠れ層を実装する方法があります。 ここで「パラメータ化された量子回路」とは、各ゲートの回転角が古典入力ベクトルの成分によって指定される量子回路を意味します。ニューラル・ネットワークの前の層からの出力が集められ、パラメーター化された回路の入力として使用されます。これらの量子回路の測定統計は集められ、続くの層の入力として使用することができます。以下に簡単な例を示します:

ここで、$\sigma$ は 非線形関数、$h_i$ は各隠れ層におけるニューロン $i$ の値です。$R(h_i)$ は $h_i$ に等しい角の回転ゲートを表し、$y$はこのハイブリッド・ネットワークから生成される最終予測値になります。

誤差逆伝播法はどうか?

古典MLに詳しいならば、 量子回路が含まれる場合、どのように勾配を計算するのか という疑問がすぐ浮かぶでしょう。これには、最急降下法 といった、強力な最適化手法を列挙する必要があるでしょう。少し技術的になりますが、手短に言えば、量子回路をブラック・ボックスとみなすとき、パラメーターに対するこのブラック・ボックスの勾配は次のように計算できます:

ここで、$\theta$ は量子回路のパラメーターを、$s$ は巨視的シフトを表します。すると、勾配は回路を $\theta+s$ と $\theta - s$ で評価した時の差になります。このように、より大きな誤差伝播ルーチンの一部として、量子回路を体系的に区別することができます。量子回路パラメータの勾配を計算するこの閉形式ルールは、パラメーター・シフト・ルール として知られています。

3. いざ、コーディング !

3.1 インポート

最初に、QiskitやPyTorchを含め、必要で便利なパッケージをインポートします。

import numpy as np
import matplotlib.pyplot as plt

import torch
from torch.autograd import Function
from torchvision import datasets, transforms
import torch.optim as optim
import torch.nn as nn
import torch.nn.functional as F

import qiskit
from qiskit.visualization import *

3.2 Qiskitを用いた「量子クラス」の作成

Qiskitの量子関数をクラスに簡単に導入することができます。初めに、学習する量子パラメータの数と、量子回路で使用したいショットの数を指定します。 この例では、簡単にするため、1つの学習量子パラメーター $\theta$を持つ1量子ビットの回路を使用します。単純化するため回路をハードコードし、角度 $\theta$ の $RY-$ 回転を使用して、回路の出力を学習させます。回路は次のようになります:

$z-$基底で出力を測定するため、期待値 $\sigma_z$ を計算します。 $$\sigma_\mathbf{z} = \sum_i z_i p(z_i)$$ これがハイブリッド・ニューラル・ネットワークとどのように結びついているかは、後ほどご紹介します。

class QuantumCircuit:
    """ 
    This class provides a simple interface for interaction 
    with the quantum circuit 
    """
    
    def __init__(self, n_qubits, backend, shots):
        # --- Circuit definition ---
        self._circuit = qiskit.QuantumCircuit(n_qubits)
        
        all_qubits = [i for i in range(n_qubits)]
        self.theta = qiskit.circuit.Parameter('theta')
        
        self._circuit.h(all_qubits)
        self._circuit.barrier()
        self._circuit.ry(self.theta, all_qubits)
        
        self._circuit.measure_all()
        # ---------------------------

        self.backend = backend
        self.shots = shots
    
    def run(self, thetas):
        job = qiskit.execute(self._circuit, 
                             self.backend, 
                             shots = self.shots,
                             parameter_binds = [{self.theta: theta} for theta in thetas])
        result = job.result().get_counts(self._circuit)
        
        counts = np.array(list(result.values()))
        states = np.array(list(result.keys())).astype(float)
        
        # Compute probabilities for each state
        probabilities = counts / self.shots
        # Get state expectation
        expectation = np.sum(states * probabilities)
        
        return np.array([expectation])

この実装をテストしてみましょう。

simulator = qiskit.Aer.get_backend('qasm_simulator')

circuit = QuantumCircuit(1, simulator, 100)
print('Expected value for rotation pi {}'.format(circuit.run([np.pi])[0]))
circuit._circuit.draw()
Expected value for rotation pi 0.46

3.3 PyTorchを用いた「量子古典クラス」の作成

量子回路を定義できましたので、 PyTorchを使って誤差逆伝播法に必要な関数を作成できます。順伝播と逆伝播 には、Qiskitクラスの要素が含まれています。 逆伝播は、上記で導入した有限差分公式を使用して、分析勾配を直接計算します。

class HybridFunction(Function):
    """ Hybrid quantum - classical function definition """
    
    @staticmethod
    def forward(ctx, input, quantum_circuit, shift):
        """ Forward pass computation """
        ctx.shift = shift
        ctx.quantum_circuit = quantum_circuit

        expectation_z = ctx.quantum_circuit.run(input[0].tolist())
        result = torch.tensor([expectation_z])
        ctx.save_for_backward(input, result)

        return result
        
    @staticmethod
    def backward(ctx, grad_output):
        """ Backward pass computation """
        input, expectation_z = ctx.saved_tensors
        input_list = np.array(input.tolist())
        
        shift_right = input_list + np.ones(input_list.shape) * ctx.shift
        shift_left = input_list - np.ones(input_list.shape) * ctx.shift
        
        gradients = []
        for i in range(len(input_list)):
            expectation_right = ctx.quantum_circuit.run(shift_right[i])
            expectation_left  = ctx.quantum_circuit.run(shift_left[i])
            
            gradient = torch.tensor([expectation_right]) - torch.tensor([expectation_left])
            gradients.append(gradient)
        gradients = np.array([gradients]).T
        return torch.tensor([gradients]).float() * grad_output.float(), None, None

class Hybrid(nn.Module):
    """ Hybrid quantum - classical layer definition """
    
    def __init__(self, backend, shots, shift):
        super(Hybrid, self).__init__()
        self.quantum_circuit = QuantumCircuit(1, backend, shots)
        self.shift = shift
        
    def forward(self, input):
        return HybridFunction.apply(input, self.quantum_circuit, self.shift)

3.4 データのロードと前処理

以上をすべてをまとめる:

MNIST データ・セット の2種類の数字(0または1)のイメージを分類するための、簡単なハイブリッド・ニューラル・ネットワークを作成します。まずMNISTをロードし、0と1を含む画像をフィルタリングします。これらは、分類するニューラル・ネットワークの入力として扱われます。

学習データ

# Concentrating on the first 100 samples
n_samples = 100

X_train = datasets.MNIST(root='./data', train=True, download=True,
                         transform=transforms.Compose([transforms.ToTensor()]))

# Leaving only labels 0 and 1 
idx = np.append(np.where(X_train.targets == 0)[0][:n_samples], 
                np.where(X_train.targets == 1)[0][:n_samples])

X_train.data = X_train.data[idx]
X_train.targets = X_train.targets[idx]

train_loader = torch.utils.data.DataLoader(X_train, batch_size=1, shuffle=True)
n_samples_show = 6

data_iter = iter(train_loader)
fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 3))

while n_samples_show > 0:
    images, targets = data_iter.__next__()

    axes[n_samples_show - 1].imshow(images[0].numpy().squeeze(), cmap='gray')
    axes[n_samples_show - 1].set_xticks([])
    axes[n_samples_show - 1].set_yticks([])
    axes[n_samples_show - 1].set_title("Labeled: {}".format(targets.item()))
    
    n_samples_show -= 1

テスト・データ

n_samples = 50

X_test = datasets.MNIST(root='./data', train=False, download=True,
                        transform=transforms.Compose([transforms.ToTensor()]))

idx = np.append(np.where(X_test.targets == 0)[0][:n_samples], 
                np.where(X_test.targets == 1)[0][:n_samples])

X_test.data = X_test.data[idx]
X_test.targets = X_test.targets[idx]

test_loader = torch.utils.data.DataLoader(X_test, batch_size=1, shuffle=True)

ここまでで、データをロードし、1つの学習パラメータを含む量子回路を作成するクラスをコーディングしました。この量子パラメータは、他の古典パラメータとともに古典ニューラル・ネットワークに入力され、ハイブリッド・ニューラル・ネットワークを形成します。また、逆伝播・順伝播関数を作ることにより、誤差逆伝播法を実現しニューラル・ネットワークを最適化することができました。最後に、PyTorchが提供する最適化技術を使って、パラメーターの学習を開始できるよう、ニューラル・ネットワーク・アーキテクチャーを指定する必要があります。

3.5 ハイブリッド・ニューラル・ネットワークの作成

NEAT PyTorchパイプラインを使って、ニューラルネットワーク・アーキテクチャを構築できます。量子層(すなわち量子回路)を挿入する場合、ネットワークは次元に対して互換性が必要です。この例の量子回路はパラメータを1つ含んでいるので、ネットワークがニューロンをサイズ1まで縮小するようにしなければなりません。最終的に、2層の全結合層からなる典型的な畳み込みニューラル・ネットワークを作成します。全結合層の最後のニューロンの値は、$\theta$パラメーターとして量子回路に与えられます。回路測定は、$\sigma_z$の測定によってもたらされる0または1の最終予測値として与えられます。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.dropout = nn.Dropout2d()
        self.fc1 = nn.Linear(256, 64)
        self.fc2 = nn.Linear(64, 1)
        self.hybrid = Hybrid(qiskit.Aer.get_backend('qasm_simulator'), 100, np.pi / 2)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = self.dropout(x)
        x = x.view(1, -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        x = self.hybrid(x)
        return torch.cat((x, 1 - x), -1)

3.6 ネットワークの学習

ハイブリッド・ネットワークを学習させる全ての材料が揃いました!複数のエポックに渡り学習させるため、PyTorchの最適化法学習率損失関数/目的関数 などを指定することができます。この例では、Adam最適化ツール 、学習率0.001、負の対数尤度損失関数 を使用します。

model = Net()
optimizer = optim.Adam(model.parameters(), lr=0.001)
loss_func = nn.NLLLoss()

epochs = 20
loss_list = []

model.train()
for epoch in range(epochs):
    total_loss = []
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        # Forward pass
        output = model(data)
        # Calculating loss
        loss = loss_func(output, target)
        # Backward pass
        loss.backward()
        # Optimize the weights
        optimizer.step()
        
        total_loss.append(loss.item())
    loss_list.append(sum(total_loss)/len(total_loss))
    print('Training [{:.0f}%]\tLoss: {:.4f}'.format(
        100. * (epoch + 1) / epochs, loss_list[-1]))
Training [5%]	Loss: -0.7380
Training [10%]	Loss: -0.9153
Training [15%]	Loss: -0.9282
Training [20%]	Loss: -0.9393
Training [25%]	Loss: -0.9463
Training [30%]	Loss: -0.9515
Training [35%]	Loss: -0.9585
Training [40%]	Loss: -0.9637
Training [45%]	Loss: -0.9713
Training [50%]	Loss: -0.9657
Training [55%]	Loss: -0.9710
Training [60%]	Loss: -0.9758
Training [65%]	Loss: -0.9765
Training [70%]	Loss: -0.9804
Training [75%]	Loss: -0.9873
Training [80%]	Loss: -0.9887
Training [85%]	Loss: -0.9894
Training [90%]	Loss: -0.9873
Training [95%]	Loss: -0.9904
Training [100%]	Loss: -0.9870

学習グラフをプロットします。

plt.plot(loss_list)
plt.title('Hybrid NN Training Convergence')
plt.xlabel('Training Iterations')
plt.ylabel('Neg Log Likelihood Loss')
Text(0, 0.5, 'Neg Log Likelihood Loss')

3.7 ネットワークのテスト

model.eval()
with torch.no_grad():
    
    correct = 0
    for batch_idx, (data, target) in enumerate(test_loader):
        output = model(data)
        
        pred = output.argmax(dim=1, keepdim=True) 
        correct += pred.eq(target.view_as(pred)).sum().item()
        
        loss = loss_func(output, target)
        total_loss.append(loss.item())
        
    print('Performance on test data:\n\tLoss: {:.4f}\n\tAccuracy: {:.1f}%'.format(
        sum(total_loss) / len(total_loss),
        correct / len(test_loader) * 100)
        )
Performance on test data:
	Loss: -0.9847
	Accuracy: 100.0%
n_samples_show = 6
count = 0
fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 3))

model.eval()
with torch.no_grad():
    for batch_idx, (data, target) in enumerate(test_loader):
        if count == n_samples_show:
            break
        output = model(data)
        
        pred = output.argmax(dim=1, keepdim=True) 

        axes[count].imshow(data[0].numpy().squeeze(), cmap='gray')

        axes[count].set_xticks([])
        axes[count].set_yticks([])
        axes[count].set_title('Predicted {}'.format(pred.item()))
        
        count += 1

4. 次は何?

ハイブリッド・ニューラルネットワークを作成することは確かに可能ですが、実際に何らかの恩恵があったでしょうか?

事実、このネットワークの古典的な層は、量子層がなくとも完全に良く(実際には、より良く)学習されます。さらに、ここで学習された量子層は、エンタングルメントを全く生成しないことに気が付いたかもしれません。つまり、この特定のアーキテクチャーを拡張する際は、古典的にシミュレーション可能であるということです。これは、ハイブリッド・ニューラル・ネットワークを使用して量子優位性を実現したい場合は、このコードを拡張し、より洗練された量子層を組み込むことから始める必要があることを意味します。

この演習のポイントは、興味のある要素が実際にあるかを調べるため、MLと量子コンピューティングの技術の統合を考えることでした。PyTorchとQiskitのおかげで、これは少し楽になっています。

import qiskit
qiskit.__qiskit_version__
{'qiskit-terra': '0.14.2',
 'qiskit-aer': '0.5.2',
 'qiskit-ignis': '0.3.3',
 'qiskit-ibmq-provider': '0.7.2',
 'qiskit-aqua': '0.7.3',
 'qiskit': '0.19.6'}