量子機械学習ライブラリ(scikit-qulacs)の紹介

この記事は[量子コンピューター Advent Calendar 2022](https://qiita.com/advent-calendar/2022/quantum)の19日目の記事です。

# 自己紹介

大阪大学で量子コンピュータの研究開発をやっておりますエンジニアの森と申します。

今年の5月から大阪大学の[QIQB](https://qiqb.osaka-u.ac.jp)という組織に参加しまして、量子機械学習をやっているのですが、[Qulacs](https://github.com/qulacs/qulacs) や [scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs) の中の人もやっています。
（※実際の実装は大阪大学を始めとした情報学科系の競プロをやっている、つよつよエンジニア達が開発しているんですけどね！）

https://qiqb.osaka-u.ac.jp

https://github.com/qulacs/qulacs

https://github.com/Qulacs-Osaka/scikit-qulacs

[Qulacs](https://github.com/qulacs/qulacs)については、[量子コンピューター Advent Calendar 2022 の 5日目](https://qiita.com/forest1040/items/5736662de999b5e2e844)に新機能の紹介をさせて頂きました。

今日は、量子機械学習のライブラリである [scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs)の紹介をさせて頂こうと思います。

# scikit-qulacs とは

量子回路シミュレータ [Qulacs](https://github.com/qulacs/qulacs) をバックエンドとした量子機械学習アルゴリズムのシミュレータです。 [scikit-learn](https://scikit-learn.org/stable/) の名前をもじっています。

### コンセプト
未だ黎明期にある量子機械学習アルゴリズムを評価するために今一番必要とされているのは、「きれいな」環境での評価です。
そこで [scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs) は、量子機械学習アルゴリズムをノイズの無いシミュレータで簡単に評価するために実装しています。

# 量子機械学習 とは

量子機械学習とは、簡単に言ってしまうと、量子コンピュータを使って機械学習を行うことです。
詳しい説明は、[scikit-qulacsのチュートリアル](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/main/doc/source/notebooks/0_tutorial.ipynb)を見て頂ければと思います。

また丁度、[別のAdvent Calendar](https://qiita.com/fuyu_quant/items/f976dfa018a920c78426)で、量子機械学習と古典の量子機械学習を組み合わせて自然言語処理を行うという面白い取り組みをされている記事がありました。

https://qiita.com/fuyu_quant/items/f976dfa018a920c78426

この記事の[QCL(量子回路学習)とは](https://qiita.com/fuyu_quant/items/f976dfa018a920c78426#1-qcl%E9%87%8F%E5%AD%90%E5%9B%9E%E8%B7%AF%E5%AD%A6%E7%BF%92%E3%81%A8%E3%81%AF)を参照頂ければと思います。

この記事では、量子機械学習の中身の実装に少し触れたいと思います。

# 学習の流れ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/6ae47269-8cd5-3e25-44b8-d6fd7741672d.png)

量子機械学習では、Uinの回路にデータを埋め込み、U(θ)のパラメータ回路(変分回路ansatz)のθを調整することにより学習します。
U(θ)は回転ゲートで構築され、三角関数の組み合わせで関数が近似されるイメージとなります。

## 学習データの埋め込み

[scikit-qulacsのv0.4.1の定義済み回路](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/v0.4.1/skqulacs/circuit/pre_defined.py#L34)からデータ埋め込みの所を見て見ましょう。

```python
    circuit = LearningCircuit(n_qubit)
    for i in range(n_qubit):
        # Capture copy of i by `i=i`.
        # Without this, i in lambda is a reference to the i, so the lambda always
        # recognize i as n_qubit - 1.
        circuit.add_input_RY_gate(i, lambda x, i=i: np.arcsin(preprocess_x(x, i)))
        circuit.add_input_RZ_gate(
            i, lambda x, i=i: np.arccos(preprocess_x(x, i) * preprocess_x(x, i))
        )
```
`add_input_RY_gate()`と`add_input_RZ_gate`でRyとRzの回転ゲートを使ってデータを埋め込んでいます。
`preprocess_x()`は、以下のように回転ゲートとなるため、-1から1の範囲で埋め込んでいます。

```python
    def preprocess_x(x: NDArray[np.float_], index: int) -> float:
        xa = x[index % len(x)]
        return min(1, max(-1, xa))
```
`add_input_RY_gate()`の第二引数に`lambda x, i=i: np.arcsin(preprocess_x(x, i))`のように関数の形で指定しています。
学習時や推論時に指定されたデータが埋め込まれます。
埋め込み時に更に`arcsin()`や`arccos()`で変換して埋め込みます。これは回路やデータによって、調整する必要があります。

このように[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs)では、様々な論文の量子回路を定義済み回路として実装しています。


https://github.com/Qulacs-Osaka/scikit-qulacs/blob/main/doc/source/notebooks/circuit_visualize.ipynb

にjupyter notebook形式で定義済み回路の説明を用意しております。

データは各量子ビットに対して、1個の同じデータが埋め込まれます。そのデータがU(θ)によって変換され出力されます。
量子コンピュータでは、出力は0か1になります。量子機械学習ではサンプリングすることにより、期待値を求めて学習や推論に使用します。
[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs) は、[量子回路シミュレータ](https://github.com/qulacs/qulacs)を使用しており、内部に量子状態を保持しているため、期待値が簡単に取り出せることができます。そのため効率よく量子機械学習を試すことが可能です。

## 学習

[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs) は、[回帰(regressor)](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/v0.4.1/skqulacs/qnn/regressor.py)と[分類(classifier)](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/v0.4.1/skqulacs/qnn/classifier.py)を実装しています。
（他にもカーネル法や生成等も実装していますが。）

回帰/分類を様々なパラメータやOptimizerを使用して学習できるように統一したインターフェイスを用意しています。
それが [Solver](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/v0.4.1/skqulacs/qnn/solver.py) です。

最適化手法として、Nelder-Mead/BFGSを実装しており、これらは [SciPy](https://scipy.org/) の `minimize()` により実行されます。
また、 Adam を使用した誤差逆伝播法も実装しています。

以下に学習部分のコード例を載せます。

```python
from skqulacs.qnn import QNNRegressor

n_qubit = 4
depth = 6
time_step = 0.5
solver = Bfgs()
maxiter= 30
circuit = create_qcl_ansatz(n_qubit, depth, time_step, 0)
qnn = QNNRegressor(circuit, solver)
opt_loss, opt_params = qnn.fit(x_train, y_train, maxiter)
print("trained parameters", opt_params)
print("loss", opt_loss)
```

[scikit-qulacsのチュートリアル](https://github.com/Qulacs-Osaka/scikit-qulacs/blob/main/doc/source/notebooks/0_tutorial.ipynb)　からの抜粋ですが、`create_qcl_ansatz()`で定義済み回路を作成して、`QNNRegressor()`に回路とSolverを指定します。
そして、`fit()`を呼び出せば学習が実行されます。

# パラメータシフト法

学習には、勾配計算が必要になります。量子コンピュータの勾配計算手法として、パラメータシフト法があります。
このパラメータシフト法やQCL(量子回路学習)は、大阪大学の以下の論文で発案されたものです。
[[1803.00745] Quantum Circuit Learning](https://arxiv.org/abs/1803.00745)

パラメータシフト法はパラメータθの目的関数Lの勾配を以下の計算式で求めることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/856ba0cb-9e05-f6a8-05dc-f89772f56944.png)
差分法に似ていますが、パラメータを大きくシフトさせて差分を取るため誤差に強いというメリットがあります。

### Qulacsの実装
https://github.com/qulacs/qulacs/blob/main/src/vqcsim/GradCalculator.cpp

Qulacsには、パラメータシフト法による勾配計算の関数が実装されています。

```c++
#define _USE_MATH_DEFINES
#include "GradCalculator.hpp"

#include <math.h>

#include "causalcone_simulator.hpp"

std::vector<std::complex<double>> GradCalculator::calculate_grad(
    ParametricQuantumCircuit& circuit, Observable& obs,
    std::vector<double> theta) {
    ParametricQuantumCircuit* circuit_copy = circuit.copy();
    UINT parameter_count = circuit_copy->get_parameter_count();

    std::vector<std::complex<double>> grad(parameter_count);
    for (UINT target_gate_itr = 0; target_gate_itr < parameter_count;
         target_gate_itr++) {
        std::complex<double> plus_delta, minus_delta;
        {
            for (UINT q = 0; q < parameter_count; ++q) {
                if (target_gate_itr == q) {
                    circuit_copy->set_parameter(q, theta[q] + M_PI_2);
                } else {
                    circuit_copy->set_parameter(q, theta[q]);
                }
            }
            CausalConeSimulator tmp(*circuit_copy, obs);
            plus_delta = tmp.get_expectation_value();
        }
        {
            for (UINT q = 0; q < parameter_count; ++q) {
                if (target_gate_itr == q) {
                    circuit_copy->set_parameter(q, theta[q] - M_PI_2);
                } else {
                    circuit_copy->set_parameter(q, theta[q]);
                }
            }
            CausalConeSimulator tmp(*circuit_copy, obs);
            minus_delta = tmp.get_expectation_value();
        }
        grad[target_gate_itr] = (plus_delta - minus_delta) / 2.0;
    }
    delete circuit_copy;
    return grad;
};

std::vector<std::complex<double>> GradCalculator::calculate_grad(
    ParametricQuantumCircuit& x, Observable& obs) {
    std::vector<double> initial_parameter;
    for (UINT i = 0; i < x.get_parameter_count(); ++i) {
        initial_parameter.push_back(x.get_parameter(i));
    }
    return calculate_grad(x, obs, initial_parameter);
};
```
`calculate_grad()`に対して、`Observable`で表現される目的関数Lを指定します。

以下のように[CausalConeSimulator](https://qiita.com/forest1040/items/5736662de999b5e2e844#%E3%82%B7%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF)を使用して、期待値を求め勾配計算を行います。
```c++
for (UINT q = 0; q < parameter_count; ++q) {
    if (target_gate_itr == q) {
        circuit_copy->set_parameter(q, theta[q] + M_PI_2);
    } else {
        circuit_copy->set_parameter(q, theta[q]);
    }
}
CausalConeSimulator tmp(*circuit_copy, obs);
plus_delta = tmp.get_expectation_value();
```

# 誤差逆伝播法(backpropagation)

パラメータシフト法は、`2×パラメータ数×forward propagation` のオーダの計算が必要となります。
シミュレータを使う場合、この`forward propagation`の計算に時間がかかるため、効率が悪くなってしまいます。
そこで、[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs) では、ディープニューラルネットワークで使わている誤差逆伝播法を実装しています。

量子機械学習の誤差逆伝播法を扱った論文には、以下があります。

https://arxiv.org/abs/1910.14266

論文の式(3)のような形で勾配を計算します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/d11f78c8-e290-bd9e-624f-868d01a901c3.png)

図にすると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/ff885ba1-903a-e569-4709-2ae9ee14104a.png)

まず一度`forward propagation`を最後まで実行します。
その状態に対して、`Observable`で表現される目的関数Lを適用します。
その値を微分して、`backprop_inner_product()`に渡し各レイヤの勾配計算を行います。

```c++
std::vector<double> ParametricQuantumCircuit::backprop(
    GeneralQuantumOperator* obs) {
    //オブザーバブルから、最終段階での微分値を求めて、backprop_from_stateに流す関数
    //上側から来た変動量 * 下側の対応する微分値 =
    //最終的な変動量になるようにする。

    int n = this->qubit_count;
    QuantumState* state = new QuantumState(n);
    state->set_zero_state();
    this->update_quantum_state(state);  //一度最後までする
    QuantumState* bistate = new QuantumState(n);
    QuantumState* Astate = new QuantumState(n);  //一時的なやつ

    obs->apply_to_state(Astate, *state, bistate);
    bistate->multiply_coef(2);
    /*一度stateを最後まで求めてから、さらにapply_to_state している。
    なぜなら、量子のオブザーバブルは普通の機械学習と違って、二乗した値の絶対値が観測値になる。
    二乗の絶対値を微分したやつと、値の複素共役*2は等しい
    オブザーバブルよくわからないけど、テストしたらできてた
    */

    //ニューラルネットワークのbackpropにおける、後ろからの微分値的な役目を果たす
    auto ans = backprop_inner_product(bistate);
    delete bistate;
    delete state;
    delete Astate;
    return ans;
} 
```

https://github.com/qulacs/qulacs/blob/v0.5.3/src/vqcsim/parametric_circuit.cpp#L260

```c++
std::vector<double> ParametricQuantumCircuit::backprop_inner_product(
    QuantumState* bistate) {
    // circuitを実行した状態とbistateの、inner_productを取った結果を「値」として、それを逆誤差伝搬します
    // bistateはノルムが1のやつでなくてもよい
    int n = this->qubit_count;
    QuantumState* state = new QuantumState(n);
    //これは、ゲートを前から適用したときの状態を示す
    state->set_zero_state();
    this->update_quantum_state(state);  //一度最後までする

    int num_gates = this->gate_list.size();
    std::vector<int> inverse_parametric_gate_position(num_gates, -1);
    for (UINT i = 0; i < this->get_parameter_count(); i++) {
        inverse_parametric_gate_position[this->_parametric_gate_position[i]] =
            i;
    }
    std::vector<double> ans(this->get_parameter_count());

    /*
    現在、2番のゲートを見ているとする
    ゲート 0 1 2 3 4 5
         state | bistate
    前から2番までのゲートを適用した状態がstate
    最後の微分値から逆算して3番までgateの逆行列を掛けたのがbistate
    1番まで掛けて、YのΘ微分した行列を掛けたやつと、bistateの内積の実数部分をとれば答えが出ることが知られている(知られてないかも)
    ParametricR? の微分値を計算した行列は、Θに180°を足した行列/2 と等しい
    だから、2番まで掛けて、 R?(π) を掛けたやつと、bistateの内積を取る
    さらに、見るゲートは逆順である。
    だから、最初にstateを最後までやって、ゲートを進めるたびにstateに逆行列を掛けている
    さらに、bistateが複素共役になっていることを忘れると、bistateに転置行列を掛ける必要がある。
    しかしこのプログラムではbistateはずっと複素共役なので、転置して共役な行列を掛ける必要がある。
    ユニタリ性より、転置して共役な行列 = 逆行列
    なので、両社にadjoint_gateを掛けている
    */
    QuantumState* Astate = new QuantumState(n);  //一時的なやつ
    for (int i = num_gates - 1; i >= 0; i--) {
        QuantumGateBase* gate_now = this->gate_list[i];  // sono gate
        if (inverse_parametric_gate_position[i] != -1) {
            Astate->load(state);
            QuantumGateBase* RcPI;
            if (gate_now->get_name() == "ParametricRX") {
                RcPI = gate::RX(gate_now->get_target_index_list()[0], M_PI);
            } else if (gate_now->get_name() == "ParametricRY") {
                RcPI = gate::RY(gate_now->get_target_index_list()[0], M_PI);
            } else if (gate_now->get_name() == "ParametricRZ") {
                RcPI = gate::RZ(gate_now->get_target_index_list()[0],
                    M_PI);  // 本当はここで2で割りたいけど、行列を割るのは実装が面倒
            } else if (gate_now->get_name() == "ParametricPauliRotation") {
                ClsParametricPauliRotationGate* pauli_gate_now =
                    (ClsParametricPauliRotationGate*)gate_now;
                RcPI =
                    gate::PauliRotation(pauli_gate_now->get_target_index_list(),
                        pauli_gate_now->get_pauli()->get_pauli_id_list(), M_PI);
            } else {
                std::stringstream error_message_stream;
                error_message_stream
                    << "Error: " << gate_now->get_name()
                    << " does not support backprop in parametric";
                throw NotImplementedException(error_message_stream.str());
            }
            RcPI->update_quantum_state(Astate);
            ans[inverse_parametric_gate_position[i]] =
                state::inner_product(bistate, Astate).real() /
                2.0;  //だからここで2で割る
            delete RcPI;
        }
        auto Agate = gate_now->get_inverse();
        Agate->update_quantum_state(bistate);
        Agate->update_quantum_state(state);
        delete Agate;
    }
    delete Astate;
    delete state;
    return ans;
}
```

`backprop()`から渡された`微分値(bistate)`を使って、レイヤの後ろからたどっていきながら(`get_inverse()`を実行する)勾配計算を行います。
勾配計算は、以下の式の右辺の通りに、そのレイヤの状態に対して、回転ゲートの微分値を適用し、
`微分値(bistate)`との内積を計算し求めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/959dee29-7786-821d-c9ae-8478465478de.png)
論文の式(9)になります。

微分値(bistate): 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/fb9b4316-defa-757e-46df-9d17e70c2406.png)

量子状態:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/2c568d73-fd03-3d3d-ef86-84235c216216.png)

回転ゲートの微分値:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116169/db0104fc-fc90-cd22-de7d-425abc23275d.png)

# まとめ

[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs)　の概要、チュートリアルと内部実装を簡単ですが紹介させて頂きましたがいかがでしたでしょうか？
[scikit-qulacs](https://github.com/Qulacs-Osaka/scikit-qulacs)　では、様々な論文の回路を定義済み回路として用意しており、簡単に試すことができますので、是非使ってみてください！！

# おまけ

まだきちんと読めていないのですが、[[2212.02521] Deep quantum neural networks equipped with backpropagation on a superconducting processor](https://arxiv.org/abs/2212.02521) は、実機の量子コンピュータで誤差逆伝播法を行っているそうです。