QUBO行列の中身を見てみよう

Keisuke

はじめに

こんにちは!
普段,アニーリングマシン(厳密にはイジングマシンですが)を使って数理最適化を研究しているkeisukeと申します!
今回は初投稿となるのですが,自分が研究の過程で「これは美しい…!」と感嘆したものについて投稿したいと思います!

TL;DR

  • 巡回セールスマン問題とは?
  • 巡回セールスマン問題をQUBOモデルに落とし込む
  • QUBO行列はどういうものなのか,実際に覗いてみる
  • QUBO行列は美しい!

巡回セールスマン問題(TSP)とは

巡回セールスマン問題,通称TSPとは,複数の都市が与えられており,全ての都市を一度だけ訪問して出発地点に戻ってくるような巡回路の中で,総移動距離が最小となるような都市の訪問順序を求める問題です.
TSPは組合せ最適化問題としては一番有名な問題なので,ご存知の方も多いのではないでしょうか.

ここからは,TSPを定式化していきます.
NN 個の都市の集合を C={c1,c2,...,cN}C'=\{c_1, c_2,...,c_N\} とします.
任意の都市 ci,cj(1i,jN)c_i, c_j (1 \leq i,j \leq N) 間には距離が定義されており,その距離を d(ci,cj)d(c_i,c_j) とします.また,ii 番目の都市 cic_ijj 番目の都市 cjc_j に関するバイナリ変数を xi,jx_{i,j} を以下のように定義します.

xi,j={1(ciの次にcjを訪問するとき)0(上記以外)(1)x_{i,j} = \begin{cases} 1 & (c_iの次にc_jを訪問するとき) \\ 0 & (上記以外) \tag1 \end{cases}

以上より,全ての都市を一度だけ訪問して出発地点に戻って くるような巡回路の総距離 CC は,次の式のように表せます.

C=i=1Nj=1Nd(ci,cj)xi,j(2)C = \sum_{i=1}^{N} \sum_{j=1}^N d(c_i, c_j) x_{i, j} \tag2

なお,TSPには以下の2つの制約があります.

  1. 同時訪問制約:一度に訪れることのできる都市はただ一つのみ
  2. 訪問回数制約:任意の都市にはただ一度しか訪問しない

以上がTSPの問題定義および定式化(とその制約)です.

QUBOモデルに落とし込む

その前に

今回は,QUBOがどういったモデルなのかは割愛させていただきます.
多分,僕がここで書くよりググった方が遥かに分かりやすいから…泣

TSPをQUBOモデルにマッピングする際,以下のようなバイナリ変数を定義します.

xi,j={1(時刻tに都市ciを訪問する)0(上記以外)(3)x_{i,j} = \begin{cases} 1 & (時刻 t に都市 c_i を訪問する)\\ 0 & (上記以外) \tag3 \end{cases}

コスト関数

コスト関数は,ある地点から出発して全ての都市を一度だけ訪問し,元の出発地点に戻ってくるような巡回路の総移動距離です.
エネルギー関数は次のように書けます.

HA=t=1Ni=1Nj=1Nd(ci,cj)xt,ixt+1,j(4)\mathrm{H}_A = \sum_{t=1}^N \sum_{i=1}^N \sum_{j=1}^N d(c_i, c_j) x_{t, i} x_{t+1, j} \tag4

HA\mathrm{H_A} の最小値は hah_a をとります.
hah_a は問題に依存します.

制約項1:同時訪問制約

これは,時刻 tt に訪問する都市はただ一つのみであるという制約です.
本制約をある時刻 tt について定式化すると,次の式のようになります.

i=1Nxt,i=1 (1tN)(5)\sum_{i=1}^N x_{t,i} = 1~(1\leq t\leq N) \tag5

全ての時刻 tt が上の式を満たすときに,最小値をとるようなエネルギー関数 HB\mathrm{H_B} を導入すると,エネルギー関数は次のように書けます.

HB=t=1N(i=1Nxt,i1)2(6)\mathrm{H}_B = \sum_{t=1}^N ( \sum_{i=1}^N x_{t, i} - 1)^2 \tag6

HB\mathrm{H_B} の最小値は 0 をとります.

制約項2:訪問回数制約

これは,ii 番目の都市 cic_i はただ一度しか訪問しないという制約です.
本制約を ii 番目の都市 cic_i について定式化すると,次の式のようになります.

t=1Nxt,i=1 (1iN)(7)\sum_{t=1}^N x_{t,i} = 1~(1 \leq i \leq N) \tag7

全ての都市 cic_i が上の式を満たすときに,最小値をとるようなエネルギー関数 HC\mathrm{H_C} を導入すると,エネルギー関数は次のように書けます.

HC=i=1N(t=1Nxt,i1)2(8)\mathrm{H}_C = \sum_{i=1}^N ( \sum_{t=1}^N x_{t, i} - 1)^2 \tag8

HC\mathrm{H_C} の最小値は 0 をとります.

最終的なエネルギー関数

以上より,最終的なエネルギー関数は,正のハイパーパラメータα を用いて次のように表せます.

H=HA+α(HB+HC)(9)\mathrm{H} = \mathrm{H}_A + \alpha (\mathrm{H}_B + \mathrm{H}_C) \tag9

エネルギー関数 H\mathrm{H} は最小値 hah_a を取り,このとき基底解となります.
基底解が得られたときのスピンが最適解となります.
なお,エネルギー関数 H\mathrm{H} には定数項が出現しますが,本質的にはあってもなくても変わりません.

QUBO行列の中身を見ていく

さて,最終的なエネルギー関数 H\mathrm{H} をQUBO行列として取得します.
QUBO行列の中身はどのようになっているのか見ていきましょう.
今回は,都市数 N=5N=5,全都市間の距離 d(ci,cj)=3d(c_i, c_j)=3,ハイパーパラメータ α=1\alpha=1 の場合でやってみます.

import numpy as np
import plotly.express as px

class TSP:
    def __init__(self):
        self.NUM_CITY = 5 # 都市数
        self.NUM_SPIN = self.NUM_CITY * self.NUM_CITY # QUBO行列に必要なスピン数
        self.distance_matrix = np.array([[0,3,3,3,3],[3,0,3,3,3],[3,3,0,3,3],[3,3,3,0,3],[3,3,3,3,0]]) # 都市間の距離行列
        self.ALPHA = 1 # ハイパーパラメータ α

        # 各スピンに番号を割り振る
        self.spin_index = np.arange(self.NUM_CITY * self.NUM_CITY).reshape(self.NUM_CITY, self.NUM_CITY)

        # コスト関数用のQUBO行列
        self.cost_weight = np.zeros((self.NUM_SPIN, self.NUM_SPIN))
        self.cost_bias = np.zeros((self.NUM_SPIN))
        self.cost_const = np.zeros((1))

        # 制約項のQUBO行列
        self.penalty_weight = np.zeros((self.NUM_SPIN, self.NUM_SPIN))
        self.penalty_bias = np.zeros((self.NUM_SPIN))
        self.penalty_const = np.zeros((1))

    # QUBO行列を作る
    def make_qubo_matrix(self):
        # ========== ここから式(4) ==========
        for t in range(self.NUM_CITY):
            for u in range(self.NUM_CITY):
                for v in range(self.NUM_CITY):
                    if t < self.NUM_CITY-1:
                        idx_i = self.spin_index[t, u]
                        idx_j = self.spin_index[t+1, v]
                    elif t == self.NUM_CITY-1:
                        idx_i = self.spin_index[t, u]
                        idx_j = self.spin_index[0, v]
                    coef  = self.distance_matrix[u, v]
                    if coef == 0:
                        continue
                    self.cost_weight[idx_i, idx_j] += coef
                    self.cost_weight[idx_j, idx_i] = self.cost_weight[idx_i, idx_j]

        # ========== ここから式(6) ==========
        # 二次の項
        for t in range(self.NUM_CITY):
            for u in range(self.NUM_CITY-1):
                for v in range(u+1, self.NUM_CITY):
                    idx_i = self.spin_index[t, u]
                    idx_j = self.spin_index[t, v]
                    coef = 2
                    self.penalty_weight[idx_i, idx_j] += self.ALPHA * coef
                    self.penalty_weight[idx_j, idx_i] = self.penalty_weight[idx_i, idx_j]

        # 一次の項
        for t in range(self.NUM_CITY):
            for u in range(self.NUM_CITY):
                idx = self.spin_index[t, u]
                coef = -1
                self.penalty_bias[idx] += self.ALPHA * coef

        # 定数項
        self.penalty_const[0] += self.ALPHA * self.NUM_CITY

        # ========== ここから式(8) ==========
        # 二次の項
        for u in range(self.NUM_CITY):
            for t in range(self.NUM_CITY-1):
                for tt in range(t+1, self.NUM_CITY):
                    idx_i = self.spin_index[t, u]
                    idx_j = self.spin_index[tt, u]
                    coef = 2
                    self.penalty_weight[idx_i, idx_j] += self.ALPHA * coef
                    self.penalty_weight[idx_j, idx_i] = self.penalty_weight[idx_i, idx_j]

        # 一次の項
        for u in range(self.NUM_CITY):
            for t in range(self.NUM_CITY):
                idx = self.spin_index[t, u]
                coef = -1
                self.penalty_bias[idx] += self.ALPHA * coef

        # 定数項
        self.penalty_const[0] += self.ALPHA * self.NUM_CITY

        # 式(9)のHのQUBO行列を作る
        qubo_matrix = np.zeros((self.NUM_SPIN, self.NUM_SPIN))
        qubo_matrix = self.cost_weight + self.penalty_weight
        for i in range(self.NUM_SPIN):
            qubo_matrix[i, i] = self.cost_bias[i] + self.penalty_bias[i]
        fig = px.imshow(qubo_matrix)
        fig.show()

上記のコードを実行すると,下記のような結果が得られます.
(ちなみにplotlyというpythonライブラリ,ものすごく便利なのでぜひご活用ください!)
各セル(マス?)は各スピン間の相互作用に対応しています.
今回は都市数が5ということで,QUBOモデルに必要なスピン数は 5×5=255 \times 5=25 となります.
ですので,QUBO行列は 25×2525 \times 25 の行列となるわけです.
なお,下図では色と各値が対応しており,その関係は以下のようになっています.

  • 紺色:-2
  • 紫色:0
  • 黄色:2
  • オレンジ色:3

QUBO行列は対称行列となっていることが分かりますね.
これは当然といえば当然です.
なぜなら,スピン ii から見たスピン jj との間の相互作用と,スピン jj から見たスピン ii との間の相互作用は同じになるからです(逆に違ったらおかしいですよね).
それにしてもQUBO,う,美しい…!!!

おわりに

いかがでしたでしょうか?
QUBOは対称行列となっており,それを可視化すると美しいものになるということについて投稿させていただきました.
皆さんも,TSPに限らずいろんなQUBOモデルを可視化してみて,その美しさを味わってみてください!

Keisuke @0816keisuke

このコンテンツにコメントはありません。

empty
ユーザーのみコメントを投稿できます。