2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実質 PyTorch !? Qiskit ベースで構築する量子ニューラルネットワーク(QNN)の汎用型モジュールを作りたい

2
Posted at

ご挨拶

ご無沙汰しております!
所属が変わったため、改めて自己紹介します。
燈株式会社でアルゴリズムエンジニアをしている、自認プーさんの秋田と言います!(わいわい)
それでは本題へ、どうぞ!

目的

この記事がフォーカスするのは、

  • 量子機械学習(量子ニューラルネットワーク)を Qiskit で簡単に実装する

というところにあります。
そもそもの「量子機械学習ってなんぞや?」や、「それぞれのモジュールについて詳しい解説を求む」というのはスコープ外ですので、あくまでも実験等で Qiskit ベースの QNN を簡単に使えるようにすることにだけフォーカスしていきます!

PyTorch

軽く PyTorch についても触れていきます。

何をするライブラリ?

深層学習(ディープラーニング)の要である「ニューラルネットワーク」のモジュールを多数兼ね備えており、これらを駆使してオリジナルのモデルを作成・学習させることができます!
例えばこんな感じで!

import torch
from torch import nn

class MyModel(nn.Module):
    def __init__(self):
        # 親クラスのコンストラクタを呼び出す
        super(MyModel, self).__init__()

        # ネットワークの構築
        self.model = nn.Sequential(
            nn.Linear(13, 32),
            nn.ReLU(),
            nn.Linear(32, 16),
            nn.ReLU(),
            nn.Linear(16, 3)
        )

        # 出力層の活性化関数として Softmax 関数を使用
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        x: torch.Tensor
            入力データに対する予測結果
        """
        # 順伝播の計算
        x = self.model(x)

        # Softmax 関数を適用
        x = self.softmax(x)

        return x

なぜ PyTorch なの?

ディープラーニング系のライブラリは多くあります。
TensorFlowJAX などなど。
結論から言うと、 Qiskit の機械学習用のサブモジュール(Qiskit Machine Learning)が QNN 用に提供しているパイプラインが PyTorch 依存であるからです。
...なんか、しょーもない理由で申し訳ございません🙇‍♂️
しかし、私自身ずっとディープラーニングは PyTorch ベースでの実装を行なっていたこともあり、慣れると使いやすいということももちろんあります!

実行環境

Python に限らずですが、こういうのをやる時の障害として1番目に出てくるのが「環境構築」でしょう...。
ということで、みなさんが簡単にできるように、今回も Google Colaboratory を使ってやってみましょう!

Google Colaboratory

使い方の説明は省きますが、以下の点でこれを選んでいるということはご理解ください!

  • Google アカウントがあれば誰でも面倒な構築をすることなく使える
  • Jupyter Notebook のスタイルなので、セルごとの実行も可視化も簡単にできる

インストール

Qiskit 系のライブラリはデフォルトで入っていないので、1番最初のセルに以下のコードを書いてそのまま実行してください!

!pip install qiskit qiskit-machine-learning pylatexenc

入れたのは3つで、

  • qiskit (2.3.0)
  • qiskit-machine-learning (0.9.0)
  • pylatexenc (2.10)

になります(カッコ内は自分が実行した段階でのバージョンになります)。
最後の pylatexenc は、 Qiskit の量子回路を描画する上で必要なものではありますが、今回は特に無くても問題ないです!

結論

先に結論から言うと、以下のようなクラスを作成すると完成になります!

Estimator

class QEstimator(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int,
        observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QEstimator, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の定義
        self.qnn_est = self.set_estimator(seed, observables)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_est(x)

        return out

    def set_estimator(
        self, seed: int, observables: list[SparsePauliOp]
    ) -> TorchConnector:
        """
        Estimator の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値
        observables: list[SparsePauliOp]
            observable のリスト

        Returns
        ----------
        qnn: TorchConnector
            Estimator の QNN 層
        """
        # Estimator のインスタンスを用意
        estimator = Estimator(seed=seed)

        # QNN をセットアップ
        qnn_base = EstimatorQNN(
            circuit=self.qc,
            estimator=estimator,
            observables=observables,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn

Sampler

class QSampler(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int, output_dim: int
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QSampler, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の出力次元を設定
        self.output_dim = output_dim

        # 量子層の定義
        self.qnn_sam = self.set_sampler(seed)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_sam(x)

        return out

    def set_sampler(self, seed: int) -> TorchConnector:
        """
        Sampler の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値

        Returns
        ----------
        qnn: TorchConnector
            Sampler の QNN 層
        """
        # Sampler のインスタンスを用意
        sampler = Sampler(seed=seed)

        # QNN をセットアップ
        qnn_base = SamplerQNN(
            circuit=self.qc,
            sampler=sampler,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters,
            output_shape=self.output_dim
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn

QNN 層

class QuantumNeuralNetwork(nn.Module):
    def __init__(
        self, input_dim: int, num_reuploads: int,
        net_type: Literal["Estimator", "Sampler"], seed: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1,
        observables: Optional[list[SparsePauliOp]] = None,
        output_dim: Optional[int] = None
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QuantumNeuralNetwork, self).__init__()

        # 入出力層の次元を設定
        self.input_dim = input_dim
        if net_type == "Sampler" and output_dim is not None:
            self.output_dim = output_dim

        # 量子回路を設計
        qcs = self.build_reuploading_circuit(
            self.input_dim, num_reuploads, feature_map_reps, ansatz_reps
        )
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]
        self.qcs = [self.qc, self.feature_map, self.ansatz]

        # observable を定義
        if net_type == "Estimator" and observables is not None:
            self.observables = observables

        # QNN を構築
        if net_type == "Estimator":
            self.qnn = QEstimator(self.qcs, seed, self.observables)
        elif net_type == "Sampler":
            self.qnn = QSampler(self.qcs, seed, self.output_dim)
        else:
            pass

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn(x)

        return out

    def build_reuploading_circuit(
        self, num_qubits: int, num_reuploads: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1
    ) -> list[QuantumCircuit]:
        """
        QNN 用の量子回路を設計する関数

        Parameters
        ----------
        num_qubits: int
            量子ビット数
        num_reuploads: int
            Re-Upload の回数
        feature_map_reps: int = 1
            特徴マップのリピート数
        ansatz_reps: int = 1
            ansatz のリピート数

        Returns
        ----------
        out: list[QuantumCircuit]
            量子回路のリスト
        """
        # 量子回路を初期化
        qc = QuantumCircuit(num_qubits)

        # 特徴マップと ansatz の定義
        feature_map = zz_feature_map(num_qubits, reps=feature_map_reps)
        ansatz = real_amplitudes(
            num_qubits, entanglement="linear", reps=ansatz_reps
        )

        # Re-Upload
        for _ in range(num_reuploads):
            qc.compose(feature_map, inplace=True)
            qc.compose(ansatz, inplace=True)

        # 量子回路をまとめてリスト化
        out = [qc, feature_map, ansatz]

        return out

使い方

# ニューラルネットワーク全体像(Estimator を使用することを想定)
class MyModel(nn.Module):
    def __init__(
        self, input_dim: int, quantum_input_dim: int, output_dim: int,
        num_reuploads: int, seed: int, observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(MyModel, self).__init__()

        # 各層次元設定
        self.input_dim = input_dim
        self.quantum_input_dim = quantum_input_dim
        self.output_dim = output_dim

        # observables の定義
        self.observables = observables

        # 入力層の定義
        self.linear_in = nn.Linear(self.input_dim, self.quantum_input_dim)

        # 量子層の定義
        self.estimator = QuantumNeuralNetwork(
            self.quantum_input_dim, num_reuploads, net_type="Estimator", seed,
            observables=self.observables
        )

        # 出力層の定義
        self.linear_out = nn.Linear(len(self.observables), self.output_dim)

        # 活性化関数の定義
        self.tanh = nn.Tanh()

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 入力層の順伝播計算
        h = self.linear_in(x)
        h = self.tanh(h)

        # 量子層の順伝播計算
        q_out = self.estimator(h)

        # 出力層の順伝播計算
        out = self.linear_out(q_out)

        return out

詳細

これだけじゃ何がなんだかわかりませんよね。
順を追って説明します。

Estimator?Sampler?

まず、 Qiskit には Estimator という機能と Sampler という機能の2種類が存在します。

  • Estimator
    • 量子回路に対して「物理量(期待値)」を計算するもの
    • 量子回路と Observable (ハミルトニアンなど)を与えて期待値 $\langle \psi | O | \psi \rangle$ を返す
    • 典型的な用途
      • QNN の損失関数評価
      • VQE
      • 量子化学・最適化
  • Sampler
    • 量子回路を実行して「測定結果の分布」を返すもの
    • 量子回路を実行して ビット列の出現確率を返す
    • 典型的な用途
      • QAOA のビット列サンプリング
      • 回路の挙動確認
      • 分類やサンプリング系アルゴリズム

基本的には、 QNN に使用するのは Estimator で良いと考えて大丈夫です。
アルゴリズムの探索や、「実はこういうケースでは Sampler を使った方が表現力が増すのでは?」みたいな実験を行いたい場合に関しては、 Sampler を使ってみるのも面白いかもしれないですね!
QNN の文脈では、量子回路の入力はデータの次元数に対応するので、説明変数の数だけの量子ビットを使うことになります。
しかし、 EstimatorSampler では出力するものが上記より根本的に異なるので、以下の注意が必要です。

  • Estimator
    • 出力次元は基本的には $1$
    • observables を与えてあげることで、出力次元をコントロールできる
  • Sampler
    • 出力次元は基本的には $2^n$ (ただし $n$ は入力次元)
    • output_shape を与えてあげることで、出力次元をコントロールできる

Sampler 用のクラスを設計し、どちらも選択できるモジュールにはしますが、今回は素直に Estimator を使ってみることにしましょう!

具体的な設計

EstimatorSampler も流れとしてはほとんど同じなので、ここでまとめて整理してみましょう。
まず、1番最初にやらなければいけないことは量子回路の設計です。
基本的な量子回路の設計方法については割愛しますが、わからない場合は筆者の過去の記事(Qiskitを使った量子最適化アルゴリズムの応用へ 〜パート1 量子計算の基礎〜)を参照いただければと思います!

量子回路

QNN で基本とされている構成は以下の通りです。

qnn_structure.jpeg

拝借元: https://qiskit-community.github.io/qiskit-machine-learning/tutorials/01_neural_networks.html

Feature Map にてデータを量子回路上にエンコードし、 Ansatz によってヒルベルト空間上で線形変換を行い、最後の測定で非線形変換を行うという流れになります。
これを古典的なニューラルネットワークの1層に準えると、

  • 線形層
    • Feature Map + Ansatz
  • 活性化関数
    • 測定

となります。
「測定はわかるけども、 Feature Map と Ansatz とはなんぞや?」という方も多いかと思います。
...そういうものだと思いましょう!
Feature Map については一応過去にそれっぽい記事(初めてQiskitを使ってからQiskitで量子機械学習ができるようになるまで 〜パート4〜)を上げているのですが、B4 $\rightarrow$ M1 の時期の筆者が書いた拙い説明なので役に立つのか...。
Ansatz は PQC (VQA)を語る上では避けて通れない部分も大いにあるのですが、簡単に言うとパウリゲートなどの簡易的なものではなく、複雑なエンタングルメントをパラメータで管理し、逐一更新していく「ミキサー」の役割を担っています(少なくとも筆者はそのような認識です...)。
では、プログラムレベルで作成してみましょうか!

# 必要なライブラリをインポート
from qiskit import QuantumCircuit
from qiskit.circuit.library import real_amplitudes, zz_feature_map

# 量子ビット数
num_qubits = 4

# 量子回路を初期化
qc = QuantumCircuit(num_qubits)

# Feature Map と Ansatz の回路の深さ(繰り返し数)を設定
feature_map_reps = 1
ansatz_reps = 1

# Feature Map と Ansatz の定義
feature_map = zz_feature_map(num_qubits, reps=feature_map_reps)
ansatz = real_amplitudes(
    num_qubits, entanglement="linear", reps=ansatz_reps
)

# Re-Upload による回路の統合
num_reuploads = 3
for _ in range(num_reuploads):
    qc.compose(feature_map, inplace=True)
    qc.compose(ansatz, inplace=True)

いくつかポイントがあります。
まず、大元となる量子回路 qc は初期化する必要がありますね。
次に、 Feature Map と Ansatz の繰り返し数を設定することで回路の深さを調節します。
回路が深ければ表現力の可能性は広がりますが、その分計算量が多くなります。
基本として zz_feature_map を使っていますが、他の Feature Map に変えても良いと思います。
Ansatz の entanglement"linear" にしていますが、ここも "circular""full" に変えてみても面白いかもしれません!
最後に、 Re-Upload ですが、これのイメージとしてはヘッド数を稼いでネットワーク全体の表現力を上げるための仕組みになります。
やっていることはシンプルに、 Feature Map と Ansatz のセットを繰り返し繋げているだけになります。
さて、これで量子回路が完成したので、 Estimator (Sampler)を使っていきましょう!

QNN の基盤

まずは EstimatorSampler のインスタンスを生成します。

# 必要なライブラリをインポート
from qiskit.primitives import StatevectorEstimator as Estimator
# from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN
# from qiskit_machine_learning.neural_networks import SamplerQNN
from qiskit_machine_learning.utils import algorithm_globals

# 乱数シードを固定
seed = 42
algorithm_globals.random_seed = seed

# Estimator のインスタンスを用意
estimator = Estimator(seed=seed)

# observables の設定
obs_01 = SparsePauliOp.from_list([("ZZII", 1.0)])
obs_12 = SparsePauliOp.from_list([("IZZI", 1.0)])
obs_02 = SparsePauliOp.from_list([("ZIZI", 1.0)])
obs_03 = SparsePauliOp.from_list([("ZIIZ", 1.0)])
observables = [obs_01, obs_12, obs_02, obs_03]

# QNN をセットアップ
qnn_base = EstimatorQNN(
    circuit=qc,
    estimator=estimator,
    observables=observables,
    input_params=feature_map.parameters,
    weight_params=ansatz.parameters
)
initial_weights = 0.01 * (
    2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
)

# # Sampler のインスタンスを用意
# sampler = Sampler(seed=seed)

# # 出力次元を設定
# output_dim = 4

# # QNN をセットアップ
# qnn_base = SamplerQNN(
#     circuit=qc,
#     sampler=sampler,
#     input_params=feature_map.parameters,
#     weight_params=ansatz.parameters,
#     output_shape=output_dim
# )
# initial_weights = 0.01 * (
#     2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
# )
# qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

Estimator にしても Sampler にしても、それぞれのインスタンスを用意して observables or output_shape を作って QNN クラスに与えれば良いということになりますね!
initial_weights というのを定義していますが、これは文字通り初期重みであり、これを大きくし過ぎて Barren Plateau の問題に沼らないようにしましょう。
最後に、 TorchConnector というクラスを使って PyTorch で1つの層として動かせるようにしてあげればもう完成のようなものです!
今回は Estimator を使っているので、 observables について少しだけ補足すると、ここの物理量に何かの意味を求めようとしてはいけません。
ぶっちゃけテキトーです。
これはニューラルネットワークの中間層の次元を変化させるものと同じだと思ってください。
出力次元は observables の数と一致します。
では、これらを汎用化を踏まえてクラス化してみましょう。

# 必要なライブラリをインポート
import torch
from torch import nn
from qiskit import QuantumCircuit
from qiskit.circuit.library import real_amplitudes, zz_feature_map
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN, SamplerQNN
from qiskit_machine_learning.utils import algorithm_globals


class QEstimator(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int,
        observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QEstimator, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の定義
        self.qnn_est = self.set_estimator(seed, observables)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_est(x)

        return out

    def set_estimator(
        self, seed: int, observables: list[SparsePauliOp]
    ) -> TorchConnector:
        """
        Estimator の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値
        observables: list[SparsePauliOp]
            observable のリスト

        Returns
        ----------
        qnn: TorchConnector
            Estimator の QNN 層
        """
        # Estimator のインスタンスを用意
        estimator = Estimator(seed=seed)

        # QNN をセットアップ
        qnn_base = EstimatorQNN(
            circuit=self.qc,
            estimator=estimator,
            observables=observables,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QSampler(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int, output_dim: int
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QSampler, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の出力次元を設定
        self.output_dim = output_dim

        # 量子層の定義
        self.qnn_sam = self.set_sampler(seed)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_sam(x)

        return out

    def set_sampler(self, seed: int) -> TorchConnector:
        """
        Sampler の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値

        Returns
        ----------
        qnn: TorchConnector
            Sampler の QNN 層
        """
        # Sampler のインスタンスを用意
        sampler = Sampler(seed=seed)

        # QNN をセットアップ
        qnn_base = SamplerQNN(
            circuit=self.qc,
            sampler=sampler,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters,
            output_shape=self.output_dim
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn

さらに、これらのクラスを自由に選択できるような QNN パイプラインを構築します。

# 必要なライブラリをインポート
from typing import Literal, Optional

import torch
from torch import nn
from qiskit import QuantumCircuit
from qiskit.circuit.library import real_amplitudes, zz_feature_map
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN, SamplerQNN
from qiskit_machine_learning.utils import algorithm_globals


class QEstimator(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int,
        observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QEstimator, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の定義
        self.qnn_est = self.set_estimator(seed, observables)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_est(x)

        return out

    def set_estimator(
        self, seed: int, observables: list[SparsePauliOp]
    ) -> TorchConnector:
        """
        Estimator の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値
        observables: list[SparsePauliOp]
            observable のリスト

        Returns
        ----------
        qnn: TorchConnector
            Estimator の QNN 層
        """
        # Estimator のインスタンスを用意
        estimator = Estimator(seed=seed)

        # QNN をセットアップ
        qnn_base = EstimatorQNN(
            circuit=self.qc,
            estimator=estimator,
            observables=observables,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QSampler(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int, output_dim: int
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QSampler, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の出力次元を設定
        self.output_dim = output_dim

        # 量子層の定義
        self.qnn_sam = self.set_sampler(seed)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_sam(x)

        return out

    def set_sampler(self, seed: int) -> TorchConnector:
        """
        Sampler の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値

        Returns
        ----------
        qnn: TorchConnector
            Sampler の QNN 層
        """
        # Sampler のインスタンスを用意
        sampler = Sampler(seed=seed)

        # QNN をセットアップ
        qnn_base = SamplerQNN(
            circuit=self.qc,
            sampler=sampler,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters,
            output_shape=self.output_dim
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QuantumNeuralNetwork(nn.Module):
    def __init__(
        self, input_dim: int, num_reuploads: int,
        net_type: Literal["Estimator", "Sampler"], seed: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1,
        observables: Optional[list[SparsePauliOp]] = None,
        output_dim: Optional[int] = None
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QuantumNeuralNetwork, self).__init__()

        # 入出力層の次元を設定
        self.input_dim = input_dim
        if net_type == "Sampler" and output_dim is not None:
            self.output_dim = output_dim

        # 量子回路を設計
        qcs = self.build_reuploading_circuit(
            self.input_dim, num_reuploads, feature_map_reps, ansatz_reps
        )
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]
        self.qcs = [self.qc, self.feature_map, self.ansatz]

        # observable を定義
        if net_type == "Estimator" and observables is not None:
            self.observables = observables

        # QNN を構築
        if net_type == "Estimator":
            self.qnn = QEstimator(self.qcs, seed, self.observables)
        elif net_type == "Sampler":
            self.qnn = QSampler(self.qcs, seed, self.output_dim)
        else:
            pass

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn(x)

        return out

    def build_reuploading_circuit(
        self, num_qubits: int, num_reuploads: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1
    ) -> list[QuantumCircuit]:
        """
        QNN 用の量子回路を設計する関数

        Parameters
        ----------
        num_qubits: int
            量子ビット数
        num_reuploads: int
            Re-Upload の回数
        feature_map_reps: int = 1
            特徴マップのリピート数
        ansatz_reps: int = 1
            ansatz のリピート数

        Returns
        ----------
        out: list[QuantumCircuit]
            量子回路のリスト
        """
        # 量子回路を初期化
        qc = QuantumCircuit(num_qubits)

        # 特徴マップと ansatz の定義
        feature_map = zz_feature_map(num_qubits, reps=feature_map_reps)
        ansatz = real_amplitudes(
            num_qubits, entanglement="linear", reps=ansatz_reps
        )

        # Re-Upload
        for _ in range(num_reuploads):
            qc.compose(feature_map, inplace=True)
            qc.compose(ansatz, inplace=True)

        # 量子回路をまとめてリスト化
        out = [qc, feature_map, ansatz]

        return out

QuantumNeuralNetwork クラスの build_reuploading_circuit メソッドで量子回路を作成し、引数で与えた net_type により EstimatorSampler かを選び、それぞれを呼び出します。
Estimator を選択しているのであれば、 observables も与えるようにしており、 Sampler を選択しているのであれば、 output_dim も与えるように設計しました。
これで一旦完成です!

実験

QNN 層を入れたモデルで3次元空間に配置された球体の内外を判定する分類問題を解いてみましょう!

データセット

次のコードでデータを作成します。

# 必要なライブラリをインポート
import warnings

import numpy as np
import pandas as pd

# 警告の無視
warnings.simplefilter("ignore")

# 乱数シードの設定
seed = 42
np.random.seed(seed)

# x, y, z 座標データを300点分作成
x = np.random.normal(0, 1, 300)
y = np.random.normal(0, 1, 300)
z = np.random.normal(0, 1, 300)

# データフレームで保存
df = pd.DataFrame({"x_val": x, "y_val": y, "z_val": z})

# 原点からのユークリッド距離を計算, 距離が1.5以下のものを0, それ以外を1とラベル付け
df["distance"] = np.sqrt(df["x_val"] ** 2 + df["y_val"] ** 2 + df["z_val"] ** 2)
df["label"] = np.where(df["distance"] <= 1.5, 0, 1)

これを可視化すると次のようになります。

dataset.png

これを前処理していきましょう!

前処理

Scikit-Learn の train_test_split 関数を使ってデータを分割し、「学習用」「検証用」「評価用」に分けます。
そして、 PyTorch のモデルで処理できるように Tensor 型に変換したのち、「学習用」と「検証用」データローダを作成します。
今のところの全てのコードは次のとおりです。

# 必要なライブラリをインポート
import warnings

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
import torch
from torch.utils.data import DataLoader, TensorDataset

# 警告の無視
warnings.simplefilter("ignore")

# 乱数シードの設定
seed = 42
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True

# デバイスの指定
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"device:\t{device}\n")

# バッチサイズの指定
batch_size = 16

# x, y, z 座標データを300点分作成
x = np.random.normal(0, 1, 300)
y = np.random.normal(0, 1, 300)
z = np.random.normal(0, 1, 300)

# データフレームで保存
df = pd.DataFrame({"x_val": x, "y_val": y, "z_val": z})

# 原点からのユークリッド距離を計算, 距離が1.5以下のものを0, それ以外を1とラベル付け
df["distance"] = np.sqrt(df["x_val"] ** 2 + df["y_val"] ** 2 + df["z_val"] ** 2)
df["label"] = np.where(df["distance"] <= 1.5, 0, 1)

# 説明変数と目的変数を抽出
exp = df[["x_val", "y_val", "z_val"]].to_numpy()
obj = df["label"].to_numpy()

# データを学習用と検証用と評価に分割
exp_train, exp_test, obj_train, obj_test = train_test_split(
    exp, obj, test_size=0.30, random_state=seed
)
exp_valid, exp_eval, obj_valid, obj_eval = train_test_split(
    exp_test, obj_test, test_size=0.50, random_state=seed
)

# NumPy 配列を PyTorch のテンソルに変換する
exp_train = torch.tensor(exp_train, dtype=torch.float32)
exp_valid = torch.tensor(exp_valid, dtype=torch.float32)
exp_eval = torch.tensor(exp_eval, dtype=torch.float32)
obj_train = torch.tensor(obj_train, dtype=torch.float32).unsqueeze(1)
obj_valid = torch.tensor(obj_valid, dtype=torch.float32).unsqueeze(1)
obj_eval = torch.tensor(obj_eval, dtype=torch.float32).unsqueeze(1)

# 学習用と検証用のデータを TensorDataset と DataLoader に変換する
train_dataset = TensorDataset(exp_train, obj_train)
valid_dataset = TensorDataset(exp_valid, obj_valid)
train_loader = DataLoader(train_dataset, batch_size=batch_size)
valid_loader = DataLoader(valid_dataset, batch_size=batch_size)

では、モデルを作り、学習までしていきましょう。

学習

学習用の関数を作りましょう。
ここの解説は特にしません。
今回のデータと問題に沿った設計となっています。

# 必要なライブラリをインポート
from torch import nn, optim
from tqdm import tqdm


def train(
    model: nn.Module, criterion: nn.Module, optimizer: optim.Optimizer,
    data_loaders: dict, num_epochs: int, device: str
) -> tuple[nn.Module, list, list]:
    """
    モデルの学習を行う関数

    Parameters
    ----------
    model: nn.Module
        学習するモデル
    criterion: nn.Module
        損失関数
    optimizer: optim.Optimizer
        最適化手法
    data_loaders: dict
        学習用・テスト用データの DataLoader を格納した辞書
    num_epochs: int
        学習のエポック数
    device: str
        デバイス

    Returns
    ----------
    model: nn.Module
        学習後のモデル
    train_loss_list: list
        学習用データにおける学習過程の損失を格納したリスト
    test_loss_list: list
        テスト用データにおける学習過程の損失を格納したリスト
    """
    # 学習用・テスト用データの損失を格納するリスト
    train_loss_list = []
    test_loss_list = []

    # 学習ループの実行
    with tqdm(range(num_epochs)) as pbar_epoch:
        # エポックごとのループ
        for epoch in pbar_epoch:
            # エポック数の表示
            pbar_epoch.set_description(f"Epoch {epoch + 1}")

            # フェーズごとのループ
            for phase in ["train", "test"]:
                # フェーズに応じてモデルのモードを切り替え
                if phase == "train":
                    # モデルを学習モードに設定
                    model.train()
                else:
                    # モデルを評価モードに設定
                    model.eval()

                # エポックの損失を初期化
                epoch_loss = 0.0

                # ミニバッチ学習の実行
                for inputs, labels in data_loaders[phase]:
                    # 勾配の初期化
                    optimizer.zero_grad()

                    # デバイスに渡す
                    inputs = inputs.to(device)
                    labels = labels.to(device)

                    # 順伝播の計算
                    with torch.set_grad_enabled(phase == "train"):
                        outputs = model(inputs)
                        loss = criterion(outputs, labels)

                        # 学習モードのときのみ逆伝播の計算とパラメータの更新
                        if phase == "train":
                            loss.backward()
                            optimizer.step()

                    # エポックの損失を更新
                    epoch_loss += loss.item()

                # エポックの平均損失を計算
                epoch_loss /= len(data_loaders[phase])
                if phase == "train":
                    train_loss_list.append(epoch_loss)
                else:
                    test_loss_list.append(epoch_loss)

    return model, train_loss_list, test_loss_list

さらに、学習のロス曲線を描画するための関数を定義しましょう。

# 必要なライブラリをインポート
import matplotlib.pyplot as plt


def plot_data(train_loss_list: list, test_loss_list: list) -> None:
    """
    学習過程の損失をプロットする関数

    Parameters
    ----------
    train_loss_list: list
        学習用データにおける学習過程の損失を格納したリスト
    test_loss_list: list
        テスト用データにおける学習過程の損失を格納したリスト

    Returns
    ----------
    None
    """
    # 横軸の設定
    x = [i + 1 for i in range(len(train_loss_list))]

    # 出力画像の設定
    plt.figure(figsize=(18, 12), tight_layout=True)
    plt.title("Training Loss over Epochs", size=15, color="red")
    plt.grid()
    plt.xlabel("Epoch")
    plt.ylabel("Loss")

    # 学習過程の損失をプロット
    plt.plot(x, train_loss_list, label="Train Loss", color="blue")
    plt.plot(x, test_loss_list, label="Test Loss", color="orange")

    # グラフの表示
    plt.legend(bbox_to_anchor=(1.01, 1), loc="upper left", borderaxespad=0)
    plt.show()

今回のモデルですが、 Estimator を使った QNN 層を中間層に持つだけのシンプルな3層 NN とします。
入力層で $3 \rightarrow 4$ の次元変換を行い、活性化関数として Tanh 関数を使います。
QNN 層では入力と同じ次元数の出力となるよう Observable の数を4つにします。
出力層では、今回は「球体の内側に入るかどうか」なので $4 \rightarrow 1$ で良いと思います。
そのように実装してみましょう!

# 必要なライブラリをインポート
from typing import Literal, Optional

from qiskit import QuantumCircuit
from qiskit.circuit.library import real_amplitudes, zz_feature_map
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN, SamplerQNN
from qiskit_machine_learning.utils import algorithm_globals


class QEstimator(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int,
        observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QEstimator, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の定義
        self.qnn_est = self.set_estimator(seed, observables)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_est(x)

        return out

    def set_estimator(
        self, seed: int, observables: list[SparsePauliOp]
    ) -> TorchConnector:
        """
        Estimator の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値
        observables: list[SparsePauliOp]
            observable のリスト

        Returns
        ----------
        qnn: TorchConnector
            Estimator の QNN 層
        """
        # Estimator のインスタンスを用意
        estimator = Estimator(seed=seed)

        # QNN をセットアップ
        qnn_base = EstimatorQNN(
            circuit=self.qc,
            estimator=estimator,
            observables=observables,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QSampler(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int, output_dim: int
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QSampler, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の出力次元を設定
        self.output_dim = output_dim

        # 量子層の定義
        self.qnn_sam = self.set_sampler(seed)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_sam(x)

        return out

    def set_sampler(self, seed: int) -> TorchConnector:
        """
        Sampler の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値

        Returns
        ----------
        qnn: TorchConnector
            Sampler の QNN 層
        """
        # Sampler のインスタンスを用意
        sampler = Sampler(seed=seed)

        # QNN をセットアップ
        qnn_base = SamplerQNN(
            circuit=self.qc,
            sampler=sampler,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters,
            output_shape=self.output_dim
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QuantumNeuralNetwork(nn.Module):
    def __init__(
        self, input_dim: int, num_reuploads: int,
        net_type: Literal["Estimator", "Sampler"], seed: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1,
        observables: Optional[list[SparsePauliOp]] = None,
        output_dim: Optional[int] = None
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QuantumNeuralNetwork, self).__init__()

        # 入出力層の次元を設定
        self.input_dim = input_dim
        if net_type == "Sampler" and output_dim is not None:
            self.output_dim = output_dim

        # 量子回路を設計
        qcs = self.build_reuploading_circuit(
            self.input_dim, num_reuploads, feature_map_reps, ansatz_reps
        )
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]
        self.qcs = [self.qc, self.feature_map, self.ansatz]

        # observable を定義
        if net_type == "Estimator" and observables is not None:
            self.observables = observables

        # QNN を構築
        if net_type == "Estimator":
            self.qnn = QEstimator(self.qcs, seed, self.observables)
        elif net_type == "Sampler":
            self.qnn = QSampler(self.qcs, seed, self.output_dim)
        else:
            pass

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn(x)

        return out

    def build_reuploading_circuit(
        self, num_qubits: int, num_reuploads: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1
    ) -> list[QuantumCircuit]:
        """
        QNN 用の量子回路を設計する関数

        Parameters
        ----------
        num_qubits: int
            量子ビット数
        num_reuploads: int
            Re-Upload の回数
        feature_map_reps: int = 1
            特徴マップのリピート数
        ansatz_reps: int = 1
            ansatz のリピート数

        Returns
        ----------
        out: list[QuantumCircuit]
            量子回路のリスト
        """
        # 量子回路を初期化
        qc = QuantumCircuit(num_qubits)

        # 特徴マップと ansatz の定義
        feature_map = zz_feature_map(num_qubits, reps=feature_map_reps)
        ansatz = real_amplitudes(
            num_qubits, entanglement="linear", reps=ansatz_reps
        )

        # Re-Upload
        for _ in range(num_reuploads):
            qc.compose(feature_map, inplace=True)
            qc.compose(ansatz, inplace=True)

        # 量子回路をまとめてリスト化
        out = [qc, feature_map, ansatz]

        return out


class MyModel(nn.Module):
    def __init__(
        self, input_dim: int, quantum_input_dim: int, output_dim: int,
        num_reuploads: int, seed: int, observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(MyModel, self).__init__()

        # 各層次元設定
        self.input_dim = input_dim
        self.quantum_input_dim = quantum_input_dim
        self.output_dim = output_dim

        # observables の定義
        self.observables = observables

        # 入力層の定義
        self.linear_in = nn.Linear(self.input_dim, self.quantum_input_dim)

        # 量子層の定義
        self.estimator = QuantumNeuralNetwork(
            self.quantum_input_dim, num_reuploads, net_type="Estimator",
            seed=seed, observables=self.observables
        )

        # 出力層の定義
        self.linear_out = nn.Linear(len(self.observables), self.output_dim)

        # 活性化関数の定義
        self.tanh = nn.Tanh()

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 入力層の順伝播計算
        h = self.linear_in(x)
        h = self.tanh(h)

        # 量子層の順伝播計算
        q_out = self.estimator(h)

        # 出力層の順伝播計算
        out = self.linear_out(q_out)

        return out

モデルが出来たので、学習設定をしていきましょう。
Observable はわりとテキトーに、最適化手法は Adam を使って、損失関数は BCEWithLogitsLoss を使いましょう。
エポックは300くらいにしてみましょう(なかなか終わらないかも...?)。

# observables の設定
obs_01 = SparsePauliOp.from_list([("ZZII", 1.0)])
obs_12 = SparsePauliOp.from_list([("IZZI", 1.0)])
obs_02 = SparsePauliOp.from_list([("ZIZI", 1.0)])
obs_03 = SparsePauliOp.from_list([("ZIIZ", 1.0)])
observables = [obs_01, obs_12, obs_02, obs_03]

# モデルの定義
model = MyModel(
    input_dim=3, quantum_input_dim=4, output_dim=1, num_reuploads=3, seed=seed,
    observables=observables
).to(device)
print(model)

# 最適化手法の設定
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# 損失関数の設定
criterion = nn.BCEWithLogitsLoss()

# エポック数の設定
num_epochs = 300

# DataLoader の辞書
data_loaders = {"train": train_loader, "test": valid_loader}

そして、実際に学習とロス曲線の描画までやってみましょう!

# 学習の実行
model, train_loss_list, test_loss_list = train(
    model, criterion, optimizer, data_loaders, num_epochs, device
)

# 学習過程の損失をプロット
plot_data(train_loss_list, test_loss_list)

評価

「評価用」のデータで精度を見てみましょう!

# 必要なライブラリをインポート
from sklearn.metrics import classification_report

# 評価データを予測
inputs = exp_eval.to(device)
labels = obj_eval.to(device)
outputs = model(inputs)
probs = torch.sigmoid(outputs)
preds = (probs > 0.5).long()

# 予測と正解の評価指標を表示
y_true = labels.cpu().numpy().ravel()
y_pred = preds.cpu().numpy().ravel()
print(
    classification_report(
        y_true, y_pred, target_names=["Inside Sphere", "Outside Sphere"]
    )
)

全部まとめる

# 必要なライブラリをインポート
from typing import Literal, Optional
import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
import torch
from torch import nn, optim
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
from qiskit import QuantumCircuit
from qiskit.circuit.library import real_amplitudes, zz_feature_map
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN, SamplerQNN
from qiskit_machine_learning.utils import algorithm_globals


class QEstimator(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int,
        observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QEstimator, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の定義
        self.qnn_est = self.set_estimator(seed, observables)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_est(x)

        return out

    def set_estimator(
        self, seed: int, observables: list[SparsePauliOp]
    ) -> TorchConnector:
        """
        Estimator の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値
        observables: list[SparsePauliOp]
            observable のリスト

        Returns
        ----------
        qnn: TorchConnector
            Estimator の QNN 層
        """
        # Estimator のインスタンスを用意
        estimator = Estimator(seed=seed)

        # QNN をセットアップ
        qnn_base = EstimatorQNN(
            circuit=self.qc,
            estimator=estimator,
            observables=observables,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QSampler(nn.Module):
    def __init__(
        self, qcs: list[QuantumCircuit], seed: int, output_dim: int
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QSampler, self).__init__()

        # 量子回路のリストを取得
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]

        # 量子層の出力次元を設定
        self.output_dim = output_dim

        # 量子層の定義
        self.qnn_sam = self.set_sampler(seed)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn_sam(x)

        return out

    def set_sampler(self, seed: int) -> TorchConnector:
        """
        Sampler の QNN を用意する関数

        Parameters
        ----------
        seed: int
            乱数シード値

        Returns
        ----------
        qnn: TorchConnector
            Sampler の QNN 層
        """
        # Sampler のインスタンスを用意
        sampler = Sampler(seed=seed)

        # QNN をセットアップ
        qnn_base = SamplerQNN(
            circuit=self.qc,
            sampler=sampler,
            input_params=self.feature_map.parameters,
            weight_params=self.ansatz.parameters,
            output_shape=self.output_dim
        )
        initial_weights = 0.01 * (
            2 * algorithm_globals.random.random(qnn_base.num_weights) - 1
        )
        qnn = TorchConnector(qnn_base, initial_weights=initial_weights)

        return qnn


class QuantumNeuralNetwork(nn.Module):
    def __init__(
        self, input_dim: int, num_reuploads: int,
        net_type: Literal["Estimator", "Sampler"], seed: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1,
        observables: Optional[list[SparsePauliOp]] = None,
        output_dim: Optional[int] = None
    ):
        # 親クラスのコンストラクタを呼び出す
        super(QuantumNeuralNetwork, self).__init__()

        # 入出力層の次元を設定
        self.input_dim = input_dim
        if net_type == "Sampler" and output_dim is not None:
            self.output_dim = output_dim

        # 量子回路を設計
        qcs = self.build_reuploading_circuit(
            self.input_dim, num_reuploads, feature_map_reps, ansatz_reps
        )
        self.qc = qcs[0]
        self.feature_map = qcs[1]
        self.ansatz = qcs[2]
        self.qcs = [self.qc, self.feature_map, self.ansatz]

        # observable を定義
        if net_type == "Estimator" and observables is not None:
            self.observables = observables

        # QNN を構築
        if net_type == "Estimator":
            self.qnn = QEstimator(self.qcs, seed, self.observables)
        elif net_type == "Sampler":
            self.qnn = QSampler(self.qcs, seed, self.output_dim)
        else:
            pass

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 量子層の順伝播計算
        out = self.qnn(x)

        return out

    def build_reuploading_circuit(
        self, num_qubits: int, num_reuploads: int,
        feature_map_reps: int = 1, ansatz_reps: int = 1
    ) -> list[QuantumCircuit]:
        """
        QNN 用の量子回路を設計する関数

        Parameters
        ----------
        num_qubits: int
            量子ビット数
        num_reuploads: int
            Re-Upload の回数
        feature_map_reps: int = 1
            特徴マップのリピート数
        ansatz_reps: int = 1
            ansatz のリピート数

        Returns
        ----------
        out: list[QuantumCircuit]
            量子回路のリスト
        """
        # 量子回路を初期化
        qc = QuantumCircuit(num_qubits)

        # 特徴マップと ansatz の定義
        feature_map = zz_feature_map(num_qubits, reps=feature_map_reps)
        ansatz = real_amplitudes(
            num_qubits, entanglement="linear", reps=ansatz_reps
        )

        # Re-Upload
        for _ in range(num_reuploads):
            qc.compose(feature_map, inplace=True)
            qc.compose(ansatz, inplace=True)

        # 量子回路をまとめてリスト化
        out = [qc, feature_map, ansatz]

        return out


class MyModel(nn.Module):
    def __init__(
        self, input_dim: int, quantum_input_dim: int, output_dim: int,
        num_reuploads: int, seed: int, observables: list[SparsePauliOp]
    ):
        # 親クラスのコンストラクタを呼び出す
        super(MyModel, self).__init__()

        # 各層次元設定
        self.input_dim = input_dim
        self.quantum_input_dim = quantum_input_dim
        self.output_dim = output_dim

        # observables の定義
        self.observables = observables

        # 入力層の定義
        self.linear_in = nn.Linear(self.input_dim, self.quantum_input_dim)

        # 量子層の定義
        self.estimator = QuantumNeuralNetwork(
            self.quantum_input_dim, num_reuploads, net_type="Estimator",
            seed=seed, observables=self.observables
        )

        # 出力層の定義
        self.linear_out = nn.Linear(len(self.observables), self.output_dim)

        # 活性化関数の定義
        self.tanh = nn.Tanh()

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播を行う関数

        Parameters
        ----------
        x: torch.Tensor
            入力データ

        Returns
        ----------
        out: torch.Tensor
            出力データ
        """
        # 入力層の順伝播計算
        h = self.linear_in(x)
        h = self.tanh(h)

        # 量子層の順伝播計算
        q_out = self.estimator(h)

        # 出力層の順伝播計算
        out = self.linear_out(q_out)

        return out


def train(
    model: nn.Module, criterion: nn.Module, optimizer: optim.Optimizer,
    data_loaders: dict, num_epochs: int, device: str
) -> tuple[nn.Module, list, list]:
    """
    モデルの学習を行う関数

    Parameters
    ----------
    model: nn.Module
        学習するモデル
    criterion: nn.Module
        損失関数
    optimizer: optim.Optimizer
        最適化手法
    data_loaders: dict
        学習用・テスト用データの DataLoader を格納した辞書
    num_epochs: int
        学習のエポック数
    device: str
        デバイス

    Returns
    ----------
    model: nn.Module
        学習後のモデル
    train_loss_list: list
        学習用データにおける学習過程の損失を格納したリスト
    test_loss_list: list
        テスト用データにおける学習過程の損失を格納したリスト
    """
    # 学習用・テスト用データの損失を格納するリスト
    train_loss_list = []
    test_loss_list = []

    # 学習ループの実行
    with tqdm(range(num_epochs)) as pbar_epoch:
        # エポックごとのループ
        for epoch in pbar_epoch:
            # エポック数の表示
            pbar_epoch.set_description(f"Epoch {epoch + 1}")

            # フェーズごとのループ
            for phase in ["train", "test"]:
                # フェーズに応じてモデルのモードを切り替え
                if phase == "train":
                    # モデルを学習モードに設定
                    model.train()
                else:
                    # モデルを評価モードに設定
                    model.eval()

                # エポックの損失を初期化
                epoch_loss = 0.0

                # ミニバッチ学習の実行
                for inputs, labels in data_loaders[phase]:
                    # 勾配の初期化
                    optimizer.zero_grad()

                    # デバイスに渡す
                    inputs = inputs.to(device)
                    labels = labels.to(device)

                    # 順伝播の計算
                    with torch.set_grad_enabled(phase == "train"):
                        outputs = model(inputs)
                        loss = criterion(outputs, labels)

                        # 学習モードのときのみ逆伝播の計算とパラメータの更新
                        if phase == "train":
                            loss.backward()
                            optimizer.step()

                    # エポックの損失を更新
                    epoch_loss += loss.item()

                # エポックの平均損失を計算
                epoch_loss /= len(data_loaders[phase])
                if phase == "train":
                    train_loss_list.append(epoch_loss)
                else:
                    test_loss_list.append(epoch_loss)

    return model, train_loss_list, test_loss_list


def plot_data(train_loss_list: list, test_loss_list: list) -> None:
    """
    学習過程の損失をプロットする関数

    Parameters
    ----------
    train_loss_list: list
        学習用データにおける学習過程の損失を格納したリスト
    test_loss_list: list
        テスト用データにおける学習過程の損失を格納したリスト

    Returns
    ----------
    None
    """
    # 横軸の設定
    x = [i + 1 for i in range(len(train_loss_list))]

    # 出力画像の設定
    plt.figure(figsize=(18, 12), tight_layout=True)
    plt.title("Training Loss over Epochs", size=15, color="red")
    plt.grid()
    plt.xlabel("Epoch")
    plt.ylabel("Loss")

    # 学習過程の損失をプロット
    plt.plot(x, train_loss_list, label="Train Loss", color="blue")
    plt.plot(x, test_loss_list, label="Test Loss", color="orange")

    # グラフの表示
    plt.legend(bbox_to_anchor=(1.01, 1), loc="upper left", borderaxespad=0)
    plt.show()


# 警告の無視
warnings.simplefilter("ignore")

# 乱数シードの設定
seed = 42
np.random.seed(seed)
algorithm_globals.random_seed = seed
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True

# デバイスの指定
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"device:\t{device}\n")

# バッチサイズの指定
batch_size = 8

# x, y, z 座標データを300点分作成
x = np.random.normal(0, 1, 300)
y = np.random.normal(0, 1, 300)
z = np.random.normal(0, 1, 300)

# データフレームで保存
df = pd.DataFrame({"x_val": x, "y_val": y, "z_val": z})

# 原点からのユークリッド距離を計算, 距離が1.5以下のものを0, それ以外を1とラベル付け
df["distance"] = np.sqrt(df["x_val"] ** 2 + df["y_val"] ** 2 + df["z_val"] ** 2)
df["label"] = np.where(df["distance"] <= 1.5, 0, 1)

# 説明変数と目的変数を抽出
exp = df[["x_val", "y_val", "z_val"]].to_numpy()
obj = df["label"].to_numpy()

# データを学習用と検証用と評価に分割
exp_train, exp_test, obj_train, obj_test = train_test_split(
    exp, obj, test_size=0.30, random_state=seed
)
exp_valid, exp_eval, obj_valid, obj_eval = train_test_split(
    exp_test, obj_test, test_size=0.50, random_state=seed
)

# NumPy 配列を PyTorch のテンソルに変換する
exp_train = torch.tensor(exp_train, dtype=torch.float32)
exp_valid = torch.tensor(exp_valid, dtype=torch.float32)
exp_eval = torch.tensor(exp_eval, dtype=torch.float32)
obj_train = torch.tensor(obj_train, dtype=torch.float32).unsqueeze(1)
obj_valid = torch.tensor(obj_valid, dtype=torch.float32).unsqueeze(1)
obj_eval = torch.tensor(obj_eval, dtype=torch.float32).unsqueeze(1)

# 学習用と検証用のデータを TensorDataset と DataLoader に変換する
train_dataset = TensorDataset(exp_train, obj_train)
valid_dataset = TensorDataset(exp_valid, obj_valid)
train_loader = DataLoader(train_dataset, batch_size=batch_size)
valid_loader = DataLoader(valid_dataset, batch_size=batch_size)

# observables の設定
obs_01 = SparsePauliOp.from_list([("ZZII", 1.0)])
obs_12 = SparsePauliOp.from_list([("IZZI", 1.0)])
obs_02 = SparsePauliOp.from_list([("ZIZI", 1.0)])
obs_03 = SparsePauliOp.from_list([("ZIIZ", 1.0)])
observables = [obs_01, obs_12, obs_02, obs_03]

# モデルの定義
model = MyModel(
    input_dim=3, quantum_input_dim=4, output_dim=1, num_reuploads=3, seed=seed,
    observables=observables
).to(device)
print(model)

# 最適化手法の設定
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# 損失関数の設定
criterion = nn.BCEWithLogitsLoss()

# エポック数の設定
num_epochs = 300

# DataLoader の辞書
data_loaders = {"train": train_loader, "test": valid_loader}

# 学習の実行
model, train_loss_list, test_loss_list = train(
    model, criterion, optimizer, data_loaders, num_epochs, device
)

# 学習過程の損失をプロット
plot_data(train_loss_list, test_loss_list)

# 評価データを予測
inputs = exp_eval.to(device)
labels = obj_eval.to(device)
outputs = model(inputs)
probs = torch.sigmoid(outputs)
preds = (probs > 0.5).long()

# 予測と正解の評価指標を表示
y_true = labels.cpu().numpy().ravel()
y_pred = preds.cpu().numpy().ravel()
print(
    classification_report(
        y_true, y_pred, target_names=["Inside Sphere", "Outside Sphere"]
    )
)

結果

ロス曲線
loss.png

評価

                precision    recall  f1-score   support

 Inside Sphere       0.48      0.45      0.47        22
Outside Sphere       0.50      0.52      0.51        23

      accuracy                           0.49        45
     macro avg       0.49      0.49      0.49        45
  weighted avg       0.49      0.49      0.49        45

う〜ん、難しいですね...。
学習用データでのロス曲線が安定的に下がっている一方で、検証用の方はパッと見で単調増加。
検証用と評価用のデータセットがそれぞれ45個ずつなので、データ数が少なくて分布が異なっていた可能性がありますね。
とはいえ、学習用データについてはちゃんと学習できているようですし、 QNN そのものは正常に働いているので大丈夫ですね!

まとめ

QNN の正体は、 Feature Map と Ansatz によって作られる量子回路でした。
Qiskit に用意された Estimator を使って物理量を推論するフレームワークを、 PyTorch のモデルの中に統合するような形で、実質 PyTorch として QNN を扱うことができるようになりました!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?