量子GANを使って任意の確率分布を少数ゲートで再現する[理論編]

# QGANの概要
前回は任意の確率分布を量子コンピュータで再現する方法の[記事](https://qiita.com/ucc_white/private/b6df173f4aeb8fe19935)を書きました。
この方法の問題点は、必要な量子ゲートの数が、量子ビットの数に応じて指数関数的に増加することでした。
今回ご紹介する論文は、IBMとJPモルガンの共同研究論文[Quantum Generative Adversarial Networks for learning and loading random distributions](https://www.nature.com/articles/s41534-019-0223-2)で、量子機械学習を用いて、任意の確率分布を少数のゲートで再現する方法です。何をやっているかというと、量子敵対生成ネットワーク（QGAN）を使って確率分布を作成しています。では、そもそも敵対的生成ネットワーク(GAN)とは何か?というところから説明していきます。

## 古典GAN
GANとはGeneratorと呼ばれる擬似データ生成部分と、Discriminatorと呼ばれる擬似データかどうかの識別部分を競合させながら学習をする教師なし機械学習です。これらGeneratorとDiscriminatorはニューラルネットで構成されています。GANの原論文をはじめとする古典的なGANは、実在しない画像の疑似生成に応用されることが主流です。Generator部分が画像を生成して、Discriminator部分がそれをオリジナルデータか擬似生成画像かどうか推測します。GANが教師なし学習とされるのは、入力として使われる画像群がどのような画像（例えば、犬や猫）であるかというラベリング情報を与えることなく、Generatorが入力データの画像に類似した画像を生成することができるからです。Discriminatorは、データが擬似データかオリジナルデータかを「識別」するので、正解データが必要なように思えますが、入力データは全て正解ラベル(オリジナルデータ)として定義できるので、教師あり学習のように入力データに事前にラベリングする必要がありません。


現在ではGANの研究が発展し、応用として以下のような使い方が可能となりました。

(1) 低画質の画像から高画質の画像を生成
(2) 文章の情報から画像を生成
(3) 元の画像から別の特徴(絵画を写真風に、など)に変換した画像を生成
(4) 画像以外(動画、音楽、文章など)の生成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/cfda12ad-0558-6d8a-e2ec-7c4c4d94903d.png)
*図(一部編集)の参照: [GAN：敵対的生成ネットワークとは何か　～「教師なし学習」による画像生成](https://www.imagazine.co.jp/gan%EF%BC%9A%E6%95%B5%E5%AF%BE%E7%9A%84%E7%94%9F%E6%88%90%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B%E3%80%80%EF%BD%9E%E3%80%8C%E6%95%99%E5%B8%AB/)

## なぜ確率分布の再現でQGANを使用するか?
この記事の目標は確率分布を量子状態で効率よく表現することです。[前回の記事](https://qiita.com/ucc_white/items/b6df173f4aeb8fe19935)に比べ、今回説明するQGANを使った方法では少ないゲート数で確率分布を表せることを示していきます。古典GANで説明したように、GANといえば画像生成と一般的に認識されていますが、本質的にはオリジナルデータの確率分布の再現を行っています。GANを確率分布の視点で理解するには次の記事([ref1](https://www.bigdata-navi.com/aidrops/2519/),[ref2](https://qiita.com/triwave33/items/a5b3007d31d28bc445c2))が参考になりました。確率分布を量子状態で表したいので、確率分布を生成する部分であるGeneratorを量子版に置き換えることを検討します。(Generator部分だけ量子回路で置き換えたのは、訓練完了後の量子状態生成に使うのはGenerator部分のみだからです。一般にDiscriminatorはモデルの訓練にのみ使用します。)なので、QGANではGeneratorが量子ニューラルネット部分でDiscriminatorが古典ニューラルネット部分となります。量子ニューラルネット部分は変分量子回路(Variational Quantum Circuit; VQC)で実現できます。つまりQGAN全体のアーキテクチャは次のようになっています。量子部分のGeneratorと古典部分のDiscriminatorを競わせながら学習していきます。




![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/26d87e63-c206-ab5e-f088-3fd37be23412.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/2cafaba3-2420-6eff-7679-07c716a4b412.png)




これで任意の確率分布を量子状態で表すことができます。一方、同じ量子状態をQGANを使わずに再帰的な処理で表現することもできますが、この場合、n量子ビットに対して$O(2^n)$個の量子ゲートが必要となり、指数関数的な計算コストがかかります。これでは量子コンピュータを使った計算の高速化から遠ざかってしまうので好ましくありません。再帰的な方法は[こちらの記事](https://qiita.com/ucc_white/items/b6df173f4aeb8fe19935)を参照ください(この方法で任意の測定時の分布を再現できるので覚えておくとためになります)。

一方でQGANでは、下図の量子回路を見るとわかるように、$O(poly(n))$と多項式オーダーのコストで済みます。ではQGANの量子回路の構成を見ていきましょう。論文で紹介されている構成では、量子回路はRYゲートでパラメータ化を行い、CZゲートでエンタングルを作った変分回路になっています。RYとCZの繰り返し数をkとしており、kを増やすことでより広いパラメータ空間を探索することができます。このパラメータ$\theta_{ij}$を変分法によって適切に設定することで、任意の量子状態、すなわち任意の確率分布を作り出すことができます。RYとCZゲートで量子回路を構築した意図は、この構成では変分回路が作用する前の量子状態がどのようなものであっても、$\theta_{ij}=0$であるため、確率振幅に影響を与えないことにあります(CXゲートを使うと$\theta_{ij}=0$でも確率振幅が変わってしまう)。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/3e8c1a4b-2b6a-75a1-002c-81b3fe028468.png)


## 訓練フロー

では量子機械学習QGANの訓練フローをみていきます。この訓練フローは古典GANと量子GANで共通です。また、オリジナルデータはバッチサイズ$m$で分割するとします。<br><br>


- __変数の関数の定義__
  - $x_i$ は学習データをランダムに分割サンプリングすることで得られた、$i$番目のバッチのデータです。
  - $g_i^\theta$ は$i$番目の擬似生成データで、パラメータ$\theta$でのGeneratorから生成されます。
  - $\phi,\theta$はニューラルネットのパラメータです。
  - $D_\phi$はDiscriminatorの分類関数で、入力がオリジナルデータである確信度が0~100%の間で出力されます。
  - $G_\theta$はGeneratorの擬似データ生成関数で、$x_i$と同じ形式のデータが出力されます。
  
- __次の1\~4をエポック数だけ繰り返します。__
 1. オリジナルデータからm個のデータ$\\{x_1, \cdots, x_m \\}$をサンプリング
 2. Generatorからのサンプリングを用いてm個の擬似データ$\\{g_1^\theta, \cdots, g_m^\theta \\}$を生成
 3. オリジナルデータと擬似データを正しく区別できるように、Discriminator目的関数$L_D(\phi,\theta)$を最大化する方向に$\phi$を更新
$$
L_D(\phi,\theta) = \frac{1}{m}\sum_{i=1}^m \left[
 \log{D_\phi(x_i)}+\log\left(1-D_\phi\left(g_i^\theta \right)\right)\right]
$$
 4. 擬似データをオリジナルデータと誤分類させるように、Generator目的関数$L_G(\phi,\theta)$を最小化する方向に$\theta$を更新
$$
L_G(\phi,\theta) = - \frac{1}{m}\sum_{i=1}^m \left[
 \log{D_\phi\left(g_i^\theta\right)}\right]
$$




ここで使っている最適化関数（損失関数）は[Non-Saturating Loss](https://neptune.ai/blog/gan-loss-functions)と呼ばれており、古典GANでの損失関数として採用されているうちの１つです。GANが提案された当初はDiscrimatorとGeneratorで共通の最適化関数を使っていました。最適関数$L_D(\phi, \theta)$をDiscriminatorは最大化、Generatorは最小化しようとするのでmin-max lossと呼ばれます。しかしこの最適化関数は勾配消失により学習が停止しやすい問題があり、今回説明しているnon-saturating lossが提案されました。



[追記] 論文ではGeneratorの最適化関数は最大化すると書いてあるのですが、筆者に質問したら正しくは最小化するとのことで確認が取れました。

では、QGANの場合としてGenerator部分が量子回路になり、擬似生成するデータが確率分布になったらどうなるかを見ていきましょう。


### 0. オリジナルデータ(確率分布)の定義
問題設定として、$\mu=1, \sigma=1$の対数正規分布のサンプリング1000個から区間[0,3]の部分を抽出し、2量子ビットで[00, 01, 10, 11]のbinに集約して表現してバッチサイズ$m$=100で分割したものがオリジナルデータだとします。まずオリジナルのサンプリング$\\{x_1, \cdots, x_m \\}$とGeneratorが作る擬似サンプリング$\\{g_1^\theta, \cdots, g_m^\theta \\}$を同じフォーマットに統一する必要があります。Shot数をバッチサイズmとした2量子ビットでの量子回路での出力は、度数を$F_i$として
> {"00": $F_{00}$, "01": $F_{01}$, "10": $F_{10}$, "11": $ F_{11}$}

となるので、オリジナルデータの出力もこの形式と比較可能なものに加工していきます。

これは`np.random.lognormal`でサンプリングを行ったあと、qiskitのモジュール`discretize_and_truncate`を使えば実現できます。ここでは割愛しますが多次元正規分布の場合も対応しているみたいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/9ed05695-2b71-110e-33a1-251f2791ef8c.png)

```python:上記のグラフ作成
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# from qiskit.aqua.utils.dataset_helper import discretize_and_truncate # qiskit 0.25未満
from qiskit_machine_learning.datasets.dataset_helper import discretize_and_truncate # qiskit 0.25以降


N = 1000   #  サンプリング数  
num_qubits = [2]  # 量子ビット数
bounds = np.array([0., 3.]) # 抽出区間   

mu = 1 # 平均1の対数正規分布をロードしていく 
sigma = 1 # 標準偏差1の対数正規分布をロードしていく
real_data = np.random.lognormal(mean=mu, sigma=sigma, size=N)


# サンプリングから正規化した度数分布を作成
freq, val  = np.histogram(real_data,bins=100,density=True)
xaxis = pd.Series(val).rolling(2).mean()[1:]
# 本来の確率分布をプロット(赤線)
mesh_num = 1000
xrange=np.linspace(real_data.min(),real_data.max(), mesh_num)
log_normal_curve = np.array([np.exp(-(np.log(x)-mu)**2/(2*sigma**2)) /(x*sigma*np.sqrt(2*np.pi)) for x in xrange] )


# 量子ビットに応じたとりうる状態の数でbinを集約
data_trunc, data_grid_trunc , elements_trunc, prob_data = \
    discretize_and_truncate(real_data, bounds, num_qubits,
                                    return_data_grid_elements=True,
                                    return_prob=True, prob_non_zero=True)
# [0, 3]区間で正規化した対数正規分布を作成(赤線)
xrange_bounds = np.extract(xrange  <=  bounds[1], xrange)
log_normal_curve_bounds = np.extract(xrange  <=  bounds[1], log_normal_curve)
partial_area =log_normal_curve_bounds.sum()*np.diff(xrange)[0]


# Plot
# 全区間表示
fig, axes= plt.subplots(1,3,figsize=(15,5)) 
axes[0].bar(xaxis, freq)
axes[0].plot(xrange,log_normal_curve,c="red")
# 区間[0, 3]に限り表示
axes[1].bar(xaxis, freq)
axes[1].plot(xrange,log_normal_curve,c="red")
axes[1].set_xlim(bounds)
# binを["00","01","10", "11"]に集約
axes[2].bar(["00","01","10", "11"], prob_data)
axes[2].plot(xrange_bounds, log_normal_curve_bounds/partial_area, c="red")
```

この問題設定で対象区間[0, 3]のサンプリング数は大体500ちょいなります。正確な値は実行するたびに変わります。

### 1. オリジナルデータのサンプリング
先程の[0, 3]区間のサンプリングにより、実質的なサンプリング数は500ちょいになりました。サンプリング結果は10進数表記で[0, 3, 2, 0, 1, 3, ...]というふうに[0, 1, 2, 3]のいずれかの値をとる配列で、その区間内のサンプリング数だけ格納されています。この例では、バッチサイズを100とすると、500ちょいのサンプル結果を5つのバッチに分割することになります。この場合500番目以降のサンプリングは切り捨てます。以下では、各バッチにおいて次のような度数でサンプリングがされたとします。このサンプルセットが後述のDiscriminator目的関数$L_D(\phi,\theta)$のデータセットになります。

|(出現回数)|0 (00)|1 (01)|2 (10)|3 (11)|
|:-:|:-:|:-:|:-:|:-:|
|バッチ 1|13|45|31|11|
|バッチ 2|15|45|32|8|
|バッチ 3|0|43|42|15|
|バッチ 4|12|40|35|13|
|バッチ 5|5|37|42|15|

この値は実際のサンプリング結果を分割したものですが、見てわかるようにこのバッチサイズだとバッチ毎に値が割とブレます。ニューラルネットの学習ではバッチ分割を行うことで最適化計算が局所解に陥って学習が停滞することを防いでくれます。

### 2. 疑似データの生成
次に量子回路による疑似データの生成を見ていきます。2量子ビットでは[00, 01, 10, 11]の量子状態の度数分布が、shot数をバッチサイズmとして1回の実行で得られます。
これでオリジナルデータのサンプリング結果と比較可能な、Discriminator目的関数$L_D(\phi,\theta)$の有効な入力データセットが以下のように得られます。

|(出現回数)|0 (00)|1 (01)|2 (10)|3 (11)|
|:-:|:-:|:-:|:-:|:-:|
|バッチ 1|1|63|23|13|
|バッチ 2|1|55|21|23|
|バッチ 3|2|62|21|15|
|バッチ 4|1|55|18|26|
|バッチ 5|0|55|24|21|

また、古典GANでは初期分布$p_z(z_i)$からのサンプリングしたノイズ値$z_i$に対してGenerator $G_\theta$で変換を行い疑似データを作ります。つまり初期分布の部分が確率的(毎回$z_i$の出力値が異なる)となり、Generator部分はパラメータ$\theta$が決まってしまえば決定論的(同じ入力なら毎回同じ出力が出る)になります。一方では量子Generatorではランダムノイズzを使わず、代わりに量子コンピュータ自体の測定の確率性を使用します。Initial Guessとして事前分布(正規分布や一様分布などを使う)が量子回路に組み込まれてますが、ここで作られる量子状態は常に固定で、Generator回路は測定を通して確率的になるはずです。事前分布の選び方はハイパーパラメータとして後半で議論します。

### 3. Discriminatorの訓練
Discriminatorの入力は[0, 1, 2, 3]のいずれかの数字(サンプルデータ)であり、この入力がオリジナルデータからサンプルされたものである確信度を出力します。仮に、Discriminatorが十分訓練されている一方で、Generatorが本来の分布に反して常に1を生成してしまう状況にあるとします。この場合、入力が[0,2,3]のいずれかであれば、Discriminatorは確信度100%を出力します。それに対してGeneratorがオリジナルデータの分布を忠実に再現できている場合は、Discriminatorは区別ができないのでどの入力に対しても確信度50%を出力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/db93d4e8-d87a-ec03-881f-befb5d231aea.png)

学習中のDiscriminatorで、取りうる入力値を次のような確信度でスコアリングしたとします。

||0 (00)|1 (01)|2 (10)|3 (11)|
|:-:|:-:|:-:|:-:|:-:|
|確信度|0.581| 0.597|0.498|0.392|

前述のオリジナルデータと擬似データのバッチ1に対してDiscriminatorの最適化関数$ L_D(\phi, \theta)$を計算してみます。$ L_D(D_\phi, G_\theta)$の第1項と第２項はそれぞれ

```math
\begin{eqnarray}
\frac{1}{m}\sum_{i=1}^m \log{D_\phi(x_i)} = \frac{1}{100}\left(
13\log{0.581}+45\log{0.597}+31\log{0.498}+11\log{0.392}\right) \quad \\
\frac{1}{m}\sum_{i=1}^m \log{ \left(1-D_\phi(g_i^\theta)\right)} =\frac{1}{100} \left(
\log{(1-0.581)}+63\log{(1-0.597)}  \qquad  \qquad  \qquad  \qquad  \quad  \\
 \qquad  \qquad  \qquad+23\log{(1-0.497)} +13\log{(1-0.392)} \right)
\end{eqnarray}
```

となり、オリジナルデータの確率分布を正解判定(確信度を高く評価)すると最適化関数が大きくなり、擬似データを正解判定すると最適化関数が小さくなります。これの最適化関数が最大化されるように古典ニューラルネットのパラメータ$\phi$を更新していきます。


### 4. Generatorの訓練

次にGeneratorの最適化関数$L_G(\phi, \theta)$を最小化していきます。先ほどの擬似データに対する確信度に対して、

```math
\begin{eqnarray}
L_G(\phi, \theta) &=& - \frac{1}{m}\sum_{i=1}^m \left[
 \log{D_\phi(\left(g_i^\theta\right)}\right] \\
&=& -\frac{1}{100}\left( \log{0.581}+45\log{0.597}+31\log{0.498}+11\log{0.392} \right) \quad
\end{eqnarray}
```
と定まり、この擬似データの確率分布をDiscriminatorが正解判定すると最適化関数が小さくなります。この最適化関数が最小化されるように量子Generator回路のパラメータ$\theta$を更新していきます。

## 評価指標

２つの確率分布の距離として相対エントロピーがよく使われます。2つの分布が一致している場合に相互エントロピーは0になります。
GeneratorとDiscriminatorの訓練後に生成されるようになった擬似分布が、オリジナルの分布をどのくらい再現しているかは相対エントロピーを用いて定量化することができます。

$$D_{RE}(P||Q) = \sum_i p_i \log{\frac{p_i}{q_i}}$$

ここで、$p_i$は擬似生成された確率分布で、$q\_i$はオリジナルの確率分布です。両者の確率分布が次のように与えられたとしましょう。

|(確率分布)|00|01|10|11|
|---|---|---|---|---|
|擬似生成データ|0.04|0.43|0.42|0.10|
|オリジナルデータ|0.08|0.42|0.37|0.13|

このとき相対エントロピーは
$$
D_{RE} = 0.04 \log\frac{0.04}{0.08} + 0.42 \log\frac{0.43}{0.42} + 
0.42 \log\frac{0.42}{0.37} + 0.10 \log\frac{0.10}{0.13} = 0.02
$$
となります。この値が小さい値になるほど類似した分布になっているといえます。


# QGANのハイパーパラメータ(繰り返し数kと初期分布)

次に量子Generator回路のハイパーパラメータについて議論します。まず、前述の繰り返し数kを増やすと性能向上する傾向があります。そして、先ほど少しだけ述べた「初期分布」の決め方にも設定の余地があります。量子Generator回路では、RYとCZで構成された変分回路の前に初期分布に対応する回路が挿入されます。この初期分布を作る回路の時点で何かしらの分布ができているように設定します。例えば初期分布として一様分布を選択するとした場合は量子Generator回路の先頭にHゲートを入れることになります。他にも初期分布として正規分布や完全にランダムな分布を用意することもできます。これらの初期分布のうちどれを使ったらいいのか、これに関しては論文で紹介されています。実験条件は、3量子ビットによる8状態での確率分布の再現で、繰り返し数kを[1, 2, 3]とし、再現する分布を対数正規分布(Log-normal)、三角分布(Triangular)、双峰分布(Bimodal)として、各条件で10回計算して相対エントロピーの平均$\mu_{RE}$と標準偏差$\sigma_{RE}$を算出しています。詳細なハイパーパラメータは以下のようになっています。


バッチ数およびショット数: 2000
Optimizer: [AMSGRAD](https://qiita.com/ZoneTsuyoshi/items/8ef6fa1e154d176e25b8#amsgrad-2018)
学習rate: $ 10^{-4}$

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/bb0b225b-b8e3-21b7-b948-6ef7806ee425.png)

論文の表の$\mu_{RE}$列に着目すると、双方分布の再現では特に正規分布を使ったときにうまくいっています。
次に論文の図の対数正規分布の再現結果を初期分布別に見てみましょう。一様分布か正規分布を初期分布にしたときに再現がうまくできていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/3f61da35-efc1-22bc-edef-fde030f892a4.png)

そしてその正規分布を初期分布としたときの対数正規分布/三角分布/双峰分布の再現結果(こちらも論文引用)を見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/7482b385-c30d-df77-b4f3-340dd241da8b.png)

概ねうまく再現できていることがわかります。

# 個人的見解

ここからは論文の解説ではなく個人の解釈です。
ここまでの話だと初期分布は正規分布を使えばいいじゃんと思うかもしれませんが、実は１つ欠点があります。一様分布は$H$ゲートを縦に並べれば作れますし、ランダム分布は無作為に選んだ角度の回転ゲートを並べれば作れます。では、正規分布の初期分布はどう作るのでしょうか？

[Qiskit Document](https://qiskit.org/documentation/stubs/qiskit.circuit.library.NormalDistribution.html)での正規分布の構築方法を見ると、[以前の記事](https://qiita.com/ucc_white/private/b6df173f4aeb8fe19935)で解説した再起的な方法で実装しているようです。この場合だと、結局回路の構築で$O(2^n)$のゲートを使ってしまうことになる気がしています。

```python
# from qiskit.circuit.library import  NormalDistribution # qiskit 0.25未満
from qiskit_finance.circuit.library import NormalDistribution # qiskit 0.25以降

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import Unroller

gate_decomposed = Unroller(['u3', 'cx'])

for i in range(1,6):
    init_dist = NormalDistribution(i)
    qc = PassManager(gate_decomposed).run(init_dist)
    print(f"{i}量子ビット", dict(qc.count_ops()))
    ax = plt.figure(figsize=(7,3)).add_subplot(1,1,1)

    qc.draw("mpl",ax=ax, fold=1)
```
>1量子ビット {'u3': 1}
2量子ビット {'u3': 3, 'cx': 2}
3量子ビット {'u3': 7, 'cx': 6}
4量子ビット {'u3': 15, 'cx': 14}
5量子ビット {'u3': 31, 'cx': 30}
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225547/657c3baf-2d59-d4fb-d4bd-3c6c74acb0d3.png)



現状では精度を重視する場合は正規分布を初期分布に、計算コストを重視する場合には一様分布かランダム分布がいいのかもしれません。もしくは事前に一様分布を初期分布として、RYとCZゲートで正規分布を再現するパラメータを事前に計算しておけば、初期分布として正規分布を使う場合も問題ないのかもしれません。

とはいえ、QGANでの回路は訓練により量子Generator部分のパラメータが決定さえしてしまえば、目的の確率分布を少数ゲートで再現することができます。量子モンテカルロ計算をする場合には確率分布と、期待値計算するための関数([Tutorial](https://qiskit.org/documentation/tutorials/finance/10_qgan_option_pricing.html)の例だとPayoff関数)の積を積分します。確率分布を固定してこの関数を関数をいろいろ変える場合には1度だけQGANの訓練をするだけで済むのでメリットはあるかと思います。


# まとめ
この記事でのポイントは以下の点です。
・確率分布を再現するとき、量子GANを使うと再起的な方法よりも少数のゲートで実装でき計算コストが減らせる。
・GANはGenerator部分とDiscriminator部分のニューラルネットで構成され、両者を競わせながら学習している。
・量子GANはGenerator部分を量子ニューラルネットに置き換えたものである。
・量子Generatorのパラメータ$\theta$を最適化することで所望の確率分布を再現する測定結果が得られる。
・パラメータ$\theta$を最適化する過程で古典Discriminatorの学習(パラメータ$\phi$の最適化)が必要
・論文によると量子Generatorの初期分布は正規分布を使うことが推奨されている。

# 次回
次は[量子GANを使って任意の確率分布を少数ゲートで再現する[実装編]](https://qiita.com/private/1fccf6bff9f1f3dc16fa/)でQiskitを使った実装方法を解説していきます。

# Version情報
以下の解説で使用したQiskitのVersionは次の通りです。
(前処理用のライブラリしか使ってないですが。。)

```python
import qiskit
qiskit.__qiskit_version__
```
>{'qiskit-terra': '0.16.3',
 'qiskit-aer': '0.7.3',
 'qiskit-ignis': '0.5.1',
 'qiskit-ibmq-provider': '0.11.1',
 'qiskit-aqua': '0.8.1',
 'qiskit': '0.23.4'}