PennyLaneを用いた量子機械学習によるMNIST生成

# はじめに
近年、量子機械学習という言葉が注目されています。本記事では量子コンピュータと古典コンピュータを組み合わせて計算を行い、MNISTの生成を実施した論文[[1]](#参考文献)で提案されたQC-AAN(Quantum Circuit Associative Adversarial Network)というモデルについて、PyTorchと量子機械学習ライブラリのPennyLaneを用いて、シミュレータ上での実装を試みます。

■分析環境
```
Python 3.9.7
PyTorch 1.7.1
PennyLane 0.20.0
```

# 量子機械学習
量子機械学習という言葉は、半ばバズワードと化しており、明確な定義が定まっていない単語だと感じています。

個人の認識としては、広義には計算過程の一部に何かしらの量子コンピュータによる計算を取り入れた機械学習手法のことを量子機械学習と呼んでいる、と考えております。

すべての計算を量子コンピュータのみで行えることが理想的ではありますが、現在の量子コンピュータの性能では、現実的ではありません。

そのため、近年話題になっているのはノイズありの小規模な量子コンピュータを部分的に利用し、古典コンピュータと組み合わせて実行するような手法です。

量子コンピュータを利用する部分については様々なパターンが存在しています。


# 量子コンピュータを用いたMNISTの生成

今回再現を行う論文[[1]](#参考文献)では、GANを用いて、MNISTの生成を行うのですが、この計算の一部に量子コンピュータを用います。

まずはベースとなる古典コンピュータでの手法について確認します。

## Associative Adversarial Networks
GANの手法の一つにAssociative Adversarial Networks[[2]](#参考文献)と呼ばれるものが存在します。

通常、GANの入力にはランダムなノイズを入力することが多いのですが、この入力の部分をある特定の分布に近づけることによって、より高品質な画像を生成しやすくしようとするものです。

ここでいう特定の分布には、GANの識別器側の特定のレイヤーの出力分布を用います。
これにより、識別に用いられる低次元の特徴ベクトルと、画像生成の元となる生成器への入力ベクトルを近づけようというものです。ここでの学習では制限ボルツマンマシンを利用しています。

一般に、GANでは識別器に比べ、生成器の学習が難しいと言われており、この解決策の一つとして提案されたものです。

![ANのモデル構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/12bc52eb-769c-54c9-8d63-abbf05697202.png)
（図は文献[[2]](#参考文献)より引用）

## QC-AAN
QC-AANでは、先ほどのAssociative Adversarial Networksにおける入力分布の学習部分に量子コンピュータを用います。

生成器への入力に用いるベクトルの分布のみを量子コンピュータで学習するため、少ない量子ビットでも実装が可能なことがポイントです。

論文[[1]](#参考文献)中では8量子ビットの回路を用いており、複数の基底で測定を行い、その結果を結合することで16ビットのデータとして扱っています。

## モデルの全体像

モデルの全体像は以下のようになっています。

![モデル概要](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/c6dcb5f8-db84-106c-7dcc-cf2b6f4ca33c.png)
（図は文献[[1]](#参考文献)より引用）

基本的な構造はGANと同じになっていますが、入力部分がランダムノイズではなく、量子回路からのサンプリングとなっています。また、この量子回路については、出力分布が識別器における低次元の特徴ベクトルと同じ分布になるように学習をおこなっています。

一般のGANでは入力のノイズには連続値を用いることが多いですが、今回の場合、量子回路からのサンプリング結果のベクトルは0/1のバイナリになる点に注意してください。

# 実装

それでは、実装に移っていきます。  
まず最初に、今回使うライブラリのインポートと、いくつかのパラメータについて宣言しておきます。

```python
import torch
from torch import optim
from torch import nn
import torchvision
from torchvision import transforms
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
import pennylane as qml
from pennylane import numpy as np
from matplotlib import pyplot as plt
import random
import math
import time

n_qubits = 8 # 量子回路のビット数
batch_size = 100 # GAN学習時のバッチサイズ
```

また、学習に使うMNISTをロードしておきます。

```Python
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, ), (0.5, ))
])

train_set = MNIST('.', train=True, transform=transform, download=True)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
```

確認のため、トレーニングデータの一部を表示してみましょう。DataLoaderの設定で、shuffle=Trueとしているため、表示される画像は実行の度に変わります。

```Python
def imshow(img):
    img = torchvision.utils.make_grid(img)
    img = img / 2 + 0.5
    npimg = img.detach().numpy()
    plt.figure(figsize=(12, 12))
    plt.imshow(np.transpose(npimg, (1, 2, 0)), cmap='gray')
    plt.show()

iterator = iter(train_loader)
img, _ = next(iterator)
imshow(img[0:8])
```

![mnist_sample](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/69da077c-93bb-05c5-11c7-9fc07b6eb00e.png)


## GAN部分の実装

GAN部分については、古典コンピュータを用いるので、PyTorchで実装します。

モデルの構造については論文[[1]](#参考文献)を参考に作成しますが、ソースコードが公開されておらず、細かなパラメータ設定については情報がないため、ここでは適当に設定しています。

```python
# 重みの初期化用
def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)

# 識別器
class discriminator(nn.Module):
    def __init__(self):
        super(discriminator, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, padding=1, bias=False)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1, bias=False)
        self.conv3 = nn.Conv2d(64, 128, 3, padding=1, bias=False)
        
        # linear1の出力の特徴ベクトルを量子回路の学習に用いる
        self.linear1 = nn.Linear(1152, n_qubits*2)
        self.linear2 = nn.Linear(n_qubits*2, 1)
        
        self.pool = nn.MaxPool2d(2, stride=2)
        self.relu = nn.LeakyReLU(0.2)
        self.sig = nn.Sigmoid()
        self.dropout = nn.Dropout(0.3)
        
        
    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))
        x = self.dropout(x)
        
        x = self.pool(self.relu(self.conv2(x)))
        x = self.dropout(x)
        
        x = self.pool(self.relu(self.conv3(x)))
        x = self.dropout(x)
        
        x = x.reshape(-1, 1152)
        
        # 量子回路の学習のため、sigmoid関数を用いて出力を0〜1にする
        h = self.linear1(x)
        h = self.sig(h) 

        # 最後に識別器としての出力を出す
        y = self.linear2(h)
        y = self.sig(y)
        
        return h, y # 量子回路の学習用にhも返す


# 生成器
class generator(nn.Module):
    def __init__(self):
        super(generator, self).__init__()
        
        # 入力は量子ビット数*2とする
        self.linear1 = nn.Linear(n_qubits*2, 1152) 
        
        self.conv_t1 = nn.ConvTranspose2d(128,64,4, stride=2, padding=1, output_padding=1, bias=False)
        self.conv_t2 = nn.ConvTranspose2d(64,32,4, stride=2, padding=1, output_padding=0, bias=False)
        self.conv_t3 = nn.ConvTranspose2d(32,1,4, stride=2, padding=1, output_padding=0, bias=False)
        
        self.batch_1 = nn.BatchNorm2d(64)
        self.batch_2 = nn.BatchNorm2d(32)
        self.batch_3 = nn.BatchNorm2d(1)
        
        self.relu = nn.LeakyReLU(0.2)
        self.tanh = nn.Tanh()
        self.dropout = nn.Dropout(0.5)
        
    def forward(self, x):
        x = self.relu(self.linear1(x))
        x = self.dropout(x)
        
        x = x.reshape(-1, 128, 3, 3)
        x = self.relu(self.batch_1(self.conv_t1(x)))
        x = self.dropout(x)
        x = self.relu(self.batch_2(self.conv_t2(x)))
        x = self.dropout(x)
        x = self.tanh(self.batch_3(self.conv_t3(x)))
        
        return x
```

## 量子回路の実装

続いて、量子回路部分の実装を行います。

今回は論文[[1]](#参考文献)に倣い、3層の構造としています。以下ではfor文を使わず実装しているため、やや読みづらいですが、ご容赦ください。

ここでは、PyTorchと組み合わせて学習することを考えて、interfaceに"torch"を指定することで、出力をtorch.tensor型にするようにします。

```python
dev = qml.device("default.qubit", wires=n_qubits, shots=1000)

@qml.qnode(dev, diff_method="parameter-shift", interface="torch")
def circuit(params, flag_y=False):
    # 1層目
    for i in range(n_qubits):
        qml.RX(params[2*i], wires=i)
        qml.RZ(params[2*i + 1], wires=i)

    qml.broadcast(qml.CNOT, wires=range(n_qubits), pattern="ring")

    # 2層目
    for i in range(n_qubits):
        qml.RZ(params[(2*n_qubits) + 2*i], wires=i)
        qml.RX(params[(2*n_qubits) + 2*i + 1], wires=i)

    qml.broadcast(qml.CNOT, wires=range(n_qubits), pattern="ring")
    
    # 3層目
    for i in range(n_qubits):
        qml.RZ(params[(4*n_qubits) + 2*i], wires=i)
        qml.RX(params[(4*n_qubits) + 2*i + 1], wires=i)
    
    # 別基底で測定するための部分。回路の最後にRXゲートを入れることで再現
    if flag_y: 
        for i in range(n_qubits):
            qml.RX(math.pi/2, wires=i)
    
    return qml.sample()
```

試しにランダムにパラメータを生成し、回路を作成して、描画してみます。

```Python
# パラメータの作成
depth = 3
params = [random.random() for i in range(2*depth*n_qubits)]

# 回路の作成・描画
fig, ax = qml.draw_mpl(circuit, decimals=2)(params)
plt.show()
```

![量子回路](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/d22a6902-e671-259b-c0e9-82150e052e68.png)


量子回路が作成できた様子がわかると思います。

続いて、回路からのサンプリングをしてみます。

```Python
# 1000ショットの結果のうち、冒頭10件を表示
print(circuit(params)[:10])
```

```Python
tensor([[0, 1, 0, 0, 0, 0, 0, 0],
        [1, 0, 1, 0, 0, 1, 1, 1],
        [1, 0, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 0, 1, 0, 1, 1],
        [1, 0, 1, 0, 1, 1, 1, 1],
        [1, 1, 0, 0, 0, 0, 0, 0],
        [1, 0, 1, 1, 1, 1, 0, 1],
        [1, 0, 1, 1, 1, 1, 1, 1],
        [0, 1, 0, 1, 0, 1, 1, 0],
        [0, 1, 0, 1, 1, 0, 1, 0]])
```

8ビット×10件のデータがtorchのtensor型で出力されている様子が確認できました。

あとは異なる基底での測定結果とtorch.catで結合すれば、量子回路からモデルへの入力をする部分の準備は完了です。
これを関数として用意しておきます。

```Python
def make_z(params):
    z1 = circuit(params)[:batch_size]
    z2 = circuit(params, True)[:batch_size]
    
    z = torch.cat([z1,z2],dim=1).to(torch.float)
    
    return z
```

```Python
# batch_size = 10の場合
tensor([[0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1],
        [1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
        [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1],
        [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1],
        [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1],
        [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
        [1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1],
        [0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1]])
```


## 量子回路学習部分の実装

次に、量子回路の学習部分を作成します。

ここでは、論文[[1]](#参考文献)で用いられている損失関数とは違うのですが、簡単のためにPyTorchの損失関数を使って学習させてみます。

PyTorchの損失関数をそのまま使って、勾配計算ができるため、実装が簡単であることがPennyLaneのよい点です。

まずはパラメータを入力にロスを返す関数を用意します。

```Python
def cost_function(params, D, G):
    # 量子回路の出力
    z = make_z(params)

    # 生成器に入力し、画像を生成
    generated_imgs = G(z)
    # 生成された画像を識別器に入力し、特徴ベクトルを取り出す
    h, y = D(generated_imgs.detach())
    
    # 量子回路の出力と特徴ベクトルの間でロスを計算する
    loss = nn.KLDivLoss()
    output = loss(h, z)
    
    return output
```

次にこれを最小化するように学習する関数を作成します。オプティマイザにはPyTorchに実装されているAdamを指定しています。

```Python
def QCBM_learning(params, D, G):
    # 生成器、識別器は固定
    D.eval()
    G.eval()
    
    # 最適化にはAdamを利用
    opt = torch.optim.Adam([params], lr=0.1)
    steps = 100

    # 最良のパラメータを記録
    best_loss = cost_function(params, D, G)
    best_params = params

    # 指定したステップ数分、パラメータを更新
    for i in range(steps):
        opt.zero_grad()
        loss = cost_function(params, D, G)
        loss.backward()
        opt.step()
        
        if loss < best_loss:
            best_loss = loss
            best_params = params
        
        if (i+1) % (steps//10) == 0:
            print(f"QCBM loss: {loss}")
            
    print(f"best QCBM loss: {best_loss}")

    return best_params
```

これで実装は完了です。

回路部分さえ作ってしまえば、あとはPyTorchライクな記述で量子回路の学習が行えてしまいます。

## 全体の学習部分

最後に、先ほど用意した関数を利用し、全体の学習部分を完成させます。

今回のコードでは以下の3つの学習部分が存在します。

1. 識別器の学習
2. 生成器の学習
3. 量子回路の学習

```Python
from torch.autograd import Variable

# GANの学習エポック数
epochs = 50

# モデルの定義
D = discriminator()
G = generator()

D.apply(weights_init)
G.apply(weights_init)

# オプティマイザの用意
optD = optim.Adam(D.parameters(), lr=0.0004, betas=[0.5, 0.9], weight_decay=1e-5)
optG = optim.Adam(G.parameters(), lr=0.0001, betas=[0.5, 0.9], weight_decay=1e-5)

criterion = nn.BCELoss()

# 教師用ラベル
zero_vector = torch.zeros(batch_size).reshape(batch_size, 1)
one_vector = torch.ones(batch_size).reshape(batch_size, 1)

# 学習設定
smooth = True

loss_D_record = list()
loss_G_record = list()

# QCBMのパラメータ初期化
params = [random.random() * np.pi for i in range(2*depth*n_qubits)]
params = Variable(torch.tensor(params), requires_grad=True)


# 学習
for epoch in range(epochs):
    # epochごとの計算時間を測る
    start_time = time.time()
    
    for i, (batch_x, _) in enumerate(train_loader): 
        D.train()
        G.train()
        
        # 識別器の学習 
        D.zero_grad()
        # 教師データの入力
        h, y = D(batch_x)
        
        # ロスの計算
        if smooth:
            loss_from_real = criterion(y, one_vector*0.9)
        else:
            loss_from_real = criterion(y, one_vector)
    
        # generatorからの生成を入力
        z = make_z(params)
        
        generated_imgs = G(z)
        h, y = D(generated_imgs.detach())
        
        # ロスの計算
        loss_from_fake = criterion(y, zero_vector)
        
        loss_D = loss_from_real + loss_from_fake
        loss_D.backward()
        optD.step()
        
        # -----------
        # 生成器の学習
        G.zero_grad()
        z = make_z(params)
        
        generated_imgs = G(z)
        h, y = D(generated_imgs)
        loss_G = criterion(y, one_vector)
        loss_G.backward()
        optG.step()
        
        # -----------
        # QCBMの学習
        # 論文に倣い、最初の10epochのみ学習
        # また、毎batch学習をすると計算が重いので、100batchごとに学習
        if epoch < 10 and (i+1) % 100 == 0:
            D.eval()
            G.eval()  
            params = QCBM_learning(params, D, G)
        
        # 学習進行状況の表示
        if (i+1) % 100 == 0:
            print(f"epoch:{epoch+1} batch:{i+1}/{len(train_loader)} loss_D:{torch.mean(loss_D):.3f} loss_G:{torch.mean(loss_G):.3f} elapsed_time:{(time.time() - start_time):.3f}sec")
            loss_D_record.append(torch.mean(loss_D))
            loss_G_record.append(torch.mean(loss_G))
    print(f"epoch:{epoch+1} loss_D:{torch.mean(loss_D):.3f} loss_G:{torch.mean(loss_G):.3f} elapsed_time:{(time.time() - start_time):.3f}sec")
    
    # 定期的に生成された画像を確認
    if epoch % 5 == 4:
        imshow(generated_imgs[:5])
```

# 学習結果の確認

モデルの学習結果を確認してみます。

まずは生成された画像の一部を見てみます。

```Python
G.eval()
z = make_z(params)
generated_imgs = G(z)
imshow(generated_imgs[:40])
```
![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/cf69531c-6520-bf38-203a-1b0cfa3078be.png)


それらしい画像は生成されていますが、数字のバリエーションは少なめです。

1が非常に多く、0,3がいくつかみられます。また、少数ではありますが、5や8も生成されています。

それ以外の数字は生成されないような結果になってしまいました。

続いて学習時のロスの変化を確認してみます。

```Python
plt.plot(list(map(float,loss_D_record)))
plt.plot(list(map(float,loss_G_record)))
```

![ロスの変化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/e20b336b-ac74-178d-52a2-9824816e9c01.png)


常に識別器側のロスが低く、生成器側のロスが高めになっています。

長めに200epochほど学習させてみることもしてみましたが、この差は埋まらず、生成される画像の品質にも大きな変化はなかったです。

## 実行結果のばらつき

上記では比較的うまくいった生成例を載せていますが、同じパラメータ設定で実行した別の結果も掲載しておきます。

回路の初期パラメータや、サンプリング結果、DataLoaderのshuffle部分が乱数によって変化したことにより、得られる結果が変わっています。やや怪しい画像も多いです。なかなか学習結果が安定しないといった部分を感じ取っていただければと思います。

![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/6e57b086-3070-1124-7208-8e3ce8428820.png)
![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/6a6907ba-f0d1-2f31-da34-273e966ec974.png)


# 古典モデルとの比較

モデルの性能を評価するため、古典コンピュータのみの生成結果との比較を行います。

比較する際の条件としては、量子回路部分を取り除き、一様分布からの出力に変更します。この際、量子側に合わせて出力がバイナリになっている点に注意してください。

具体的には以下のコードで生成します。整数で生成した後、モデルに入力するためにfloat型に変換しています。

```Python
# [low, high)が範囲になる点に注意
z = torch.randint(low=0, high=2, size=(batch_size, n_qubits*2)).to(torch.float)
```

3回ほど実験した結果、生成された画像は以下のようになりました。

![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/7258add0-018c-94b9-96e8-9311643ab9ed.png)
![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/70399761-4674-4bb1-fa41-0c427ce07962.png)
![学習済みモデルの出力サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/48a234d3-2c04-acf5-bd16-14da8c77d0ec.png)



## 評価指標
ここでは、論文[[1]](#参考文献)で用いられているInception Score(IS)と呼ばれる指標を利用します。ただし、一般的なISとはやや違うものとなっているのでご注意ください。

この指標ではMNISTを本来のラベルで分類できる分類器を用意します。このモデルが完璧にMNISTを分類できると仮定すると、MNISTに含まれている画像に対しては、どれか1つのラベルに対して1を出し、それ以外のラベルに対して0を返すはずです。

そして、ランダムノイズのような画像を入れた場合、すべてのラベルに対して同程度の確率となり、全く予想ができない、ということが予測されます。

これを利用して、もし生成された画像に対して、分類器がいずれかのラベルについて高い値を出すのであれば、それはMNISTに近い画像だと判断するのが基本的な考え方となります。

ISの算出式は以下で定義されます。

$ \mathrm{IS}(G) = \exp (\mathbb{E}_{x \backsim G} [ \mathrm{KL}[p(y|x)||p(y)]])$

$x$は生成器から生成された画像であり、$p(y|x)$は$x$が分類機によって、クラス$y$と分類される確率です。

$p(y)$には生成されたデータに対する分類器の出力ラベルの分布を用いることが一般的ですが、論文[[1]](#参考文献)中ではオリジナルデータのラベルの分布を用いています。

分類器の出力ラベルの分布を用いる理由としては、生成された画像の多様性を評価するためですが、今回は多様性は抜きにして、画像としての品質のみを見極めるため、このような実装にしているのではないかと考えています。

ISで評価する際には、以下の二点について、注意しておく必要があります。

1. 分類機がMNISTを完全に識別できるものと仮定していること
2. 人間にとって似ている保証はなく、モデルが高い確度で識別しているかが基準



## 実験結果

今回はResNetベースのモデルを分類機に用いて評価を行いました。

学習epoch数は50で固定し、100枚の画像を生成して評価します。それぞれのモデルについて3回ずつ実験を行いました。また、分類機の性能を確認するため、トレーニングデータからランダムに100枚サンプリングしたものに対してのスコアも確認しています。

結果としては以下の表のようになりました。ISの取り得る値の範囲は0〜10(＝ラベル数)で、値が高いほど良い結果です。

| DCGAN | QC-AAN | MNIST(トレーニングデータ) |
| ---- | ---- | ---- |
| 8.68 | 9.25 | 9.95 |
| 8.89 | 9.57 | 9.87 |
| 9.28 | 9.11 | 9.86 |

トレーニングデータに対しては高い値が出ているので、分類機の性能については、仮定をある程度満たしていると考えてよいでしょう。

そして、DCGANとQC-AANの比較では、今回の結果ではQC-AANのほうが平均的にはやや高い値となりました。今回は3回しか実験をしておりませんが、あまり結果の安定していない様子が見て取れるため、理想的にはもっと多くの回数の実験を行い、評価するべきだと思います。

個々の結果については、各モデルの実行結果の項で掲載した画像が相当するのですが、QC-AANについては、個人的には一番奇麗に見えた1番の結果よりも、2番のほうがスコアが高いです。この点に関しては主観的な部分もあるのですが、生成モデルの評価の難しいところです。

また、今回は論文[[1]](#参考文献)に倣い、比較対象を古典コンピュータによるAANではなく、一様分布からの入力としている点にも注意が必要です。量子的なアドバンテージではなく、AANの考え方を導入したことによるアドバンテージと考えることもできるため、量子的なアドバンテージを議論するには不十分とも感じます。

## 生成画像の分布

参考までに、QC-AANでスコアが一番高かった2番目のモデルについて、IS算出時に利用した分類器を用いて、生成された画像のラベルの分布を見てみます。

![ラベルの分布](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/1a635944-726d-b22d-80e8-25d6ca198918.png)


生成された画像の半数近くは0であり、8と9がそこそこある、といった感じになっています。

分類器的には2や5の画像に識別される画像もわずかに存在していたようですが、私の目で確認した限りでは、あまり数字らしい姿はしていませんでした。

![2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/4a6a2721-9c32-0047-d445-8cc66b376180.png)　　![5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670216/ef22f9f0-e26e-a1f9-74c2-fd25e97540ed.png)
(左が2、右が5と判別された画像)

今回の実装では、生成画像の多様性といった部分では課題ありといった結果になりました。


# 今回の実装の課題点

今回はQC-AANの論文[[1]](#参考文献)を元にMNISTの生成を実施しましたが、論文[[1]](#参考文献)と大きく異なる実装をした点として、量子回路の学習時の損失関数があります。

量子回路の出力はバイナリであるのに対し、教師データとなる識別器の特徴ベクトルは一般に連続値を取ります。そのため、どんなに学習をしても、完全に一致することはありません。

論文[[1]](#参考文献)中では

$C_q = \max\_{q} \mathbb{E}\_{z \backsim p\_l(z)}[\log q\_{\theta}(z)]$

$p_l(z)$:識別器の特徴ベクトルの分布、$q_{\theta}(z)$:量子回路の出力分布

といった目的関数を用いていますが、連続値の分布と離散値の分布からどのように実装して計算しているのかがよく分からず、実装を断念しています。そのため、今回はPyTorchの既存の関数をそのまま使うことでごまかしています。

この部分をちゃんと作り込めば、よりよい結果が得られるかもしれません。



# まとめ

今回は量子コンピュータを用いた生成モデルの一種であるQC-AANの再現をPyTorchとPennyLaneで実装してみました。

実装を簡単にするために、いくつか元論文[[1]](#参考文献)とは違う実装をしている部分もありますが、正しくMNISTらしい画像が生成されている様子は確認できました。

PennyLaneはPyTorchとも相性がよく、PyTorchに慣れている人であれば、簡単に導入することができると思います。

一方、生成結果の安定感といったところでは少し課題も感じる結果となりました。今回の検証の本筋ではないため、GAN側についてはあまり手を加えていませんが、GAN側の目的関数の工夫等で解決できる部分もあるかと思われます。
今後、量子機械学習を実務などで運用していくことを想定した場合には、古典・量子双方の部分について、しっかりチューニングしていく必要があると思われます。

以上です。


# 参考文献
1. [Manuel S. Rudolph, et al. "Generation of high-resolution handwritten digits with an ion-trap quantum computer." arXiv preprint arXiv:2012.03924 (2020).](https://arxiv.org/abs/2012.03924)

2. [Tarik Arici, and Asli Celikyilmaz. "Associative adversarial networks." arXiv preprint arXiv:1611.06953 (2016).](https://arxiv.org/abs/1611.06953)