背景
一昔前は、「量子コンピュータ」というと、すごく未来的なものだと感じていたが、ここ数年で、一気に「民主化」が進んできた感覚がある。この一因として、量子コンピュータ「関連技術」の広がりがある。すなわち「量子コンピュータではないけれど、明らかに関連性がある/派生した技術」が裾野を伸ばし、古典コンピュータと地続きの領域にまで伸びてきている、という状況が生まれている。
アニーリングマシンについて
「アニーリング」とは、元々はセラミックス等の「焼きなまし」を指す。焼結体からなる構造体では、例えば表面の傷や構造の欠陥を、構造体の加熱によって消失させることができる(ことが多い)。概念的に言えば、エネルギー的に不安定なところに引っかかっている状態(=傷)に障壁を超えるだけのエネルギーを与えて、安定な状態(=傷の無い状態)に落としてやる、ということをしているわけだ。このような操作は、もちろん今でも頻繁にセラミックス関連開発の現場では行われているわけだが、より一般化して、「エネルギー的に不安定なところに引っかかりがちなものを安定状態に落とす」ような操作が、「アニーリング」と呼ばれるようになっている。このようなアニーリングの技術を、量子技術を用いて実現したものが、「量子アニーリング」である、、、と認識しておけばいいと思う。ここで微妙に歯切れの悪い書き方をするのは、実際にこの技術が生まれてきた歴史的な背景は、違っているからだ。とはいえ、ユーザー的にはその辺りは割とどうでもいいので、あえて適当に書いた。ユーザーとして重要なのは、量子かそうでないかではなく、何ができるか、である。(もちろん、実機の動作の仕方を知っておくのは、ノイズへの配慮に繋がるので大事ではある。)
アニーリングマシンでできること
前置きが長くなったが、アニーリングマシンがなんなのか、というと、基本的には以下の式を最小化することができる装置である、
$$
H=\sum_{i,j}{Q_{ij}q_iq_j}
$$
ただし、ここで$q_i$は、0または1をとる変数である。この式で表される最適化問題は、QUBO(quadratic unconstrained binary optimization)と呼ばれている。また、数学的に等価な、以下の式で表すことも多い。
$$
H=\sum_{i\neq j}{J_{ij}s_is_j}+\sum_{i}{h_i}{s_i}
$$
ただし、$s_i$は、-1または1をとる変数。これは、Isingモデルと呼ばれる。ちなみに、僕は最初にこの二式を見た時、「QUBOに一次の項が無いのに、なんで等価?」と思ったのだが、$0^2=0$、$1^2=1$であるため、QUBOで$i=j$の時に、$q_i^2=q_i$が成り立つためである。なお、変数の対応に関しては、以下の記事に詳しい。
このコードを実行している環境と必要なライブラリなど
第一世代M1 MacBook Air
Python 3.7.15
amplify 0.10.0
numpy 1.21.6
ここで使うライブラリAmplifyは、Fixstars Amplifyという会社の開発しているもので、ほぼnumpyの記法でアニーリングマシンへの入力データを作り、実機に投げることができる、という代物である。非常に使いやすいので、デファクトスタンダードになる気がする。本記事で使うコードを動かしたい場合は、下記HPにアクセス、ユーザー登録の上、API keyを入手すること。
ライブラリのインストール
一緒にNumpyも入る。
pip install amplify
とても簡単なハミルトニアンを計算させてみる
まずは、自分の頭で考えてもすぐに答えが出せる程度の問題を解いてもらおう。
$$
H=3q_0-q_1+2q_2
$$
というハミルトニアンを最小化するための、$q_i(i=0,1,2)$の組み合わせを考える。これは係数が正である項の$q_i$が0、それ以外が1であれば最小化するので、$q_0, q_2$が0、$q_1$が1になれば良い。
これをコーディングで実装し、アニーリングマシンにジョブを投げて、解いてもらおう。まずは、必要なライブラリを読みこむ。ついでに、client(ジョブを投げる先)も一緒に定義している。この辺りは、定型分(毎回ファイルの頭にコピペしてくる)にしておいて問題ない。
from amplify import BinaryPoly, SymbolGenerator, Solver
from amplify.client import FixstarsClient
client = FixstarsClient()
client.token = 'ここに入手したAPIキーを入力する'
solver = Solver(client)
次は、解きたい問題のサイズを定義して、変数を設定する。
system_size = [3]
gen = SymbolGenerator(BinaryPoly)
q = gen.array(*system_size)
print(q)
#出力:[q_0, q_1, q_2]
このあたりも、ほぼ定型分だと思っておけばいい。ここでポイントになるのは2点あって、1つ目は、問題のサイズの定義をしている、system_sizeのところ。ここで解いている問題は、一次元だが、一般には二次元以上の問題も解くことができるため、問題のサイズをリストで定義して、後から引数にするときに*でバラしている。2つ目のポイントになるのは、SymbolGeneratorの引数にBinaryPolyを入れておくことで、問題をQUBOとして定義している点。ここに、IsingPolyを入れれば、問題をIsingとして定義できる。
これらの行を実行することでQUBOを構成する変数を設定することができた。続いて、ここで定義した変数を使って、ハミルトニアンを定義していく。
H=3*q[0]-1*q[1]+2*q[2]
print(H)
#出力:2 q_0 - q_1 + q_2
見ての通り、普通にpythonの記法で文字式の計算を行っているだけで、ハミルトニアンを定義することができた。先に問題を定義した部分で'array'と書いたことからもわかるように、定義した変数の組はiterableになっていて、いつものpythonの記法で、変数を1つずつ取り出すことができるようになっている。
では、最後にハミルトニアンをアニーリングマシンに投げよう。
results = solver.solve(H)
少し待つと、答えが返ってきて、resultsに格納される。これを可視化しよう。
result = results[0].values
print(result)
print([f'q_{k} = {result[k]}' for k in result.keys()])
これも、amplify特有の記法というよりは、やはりいつものpythonの記法になっている。ポイントとして、resultsの中から、[0].valuesで結果を取り出せることと、取り出した結果は辞書型になっていて、.keys()や.values()で中身を取り出せること。.valuesで取り出したものが辞書型なので、微妙に混乱するが、この辺りもほぼ定型分として使うことができるだろう。
まとめ
以上、使い方をまとめると、
問題を定義する→ハミルトニアンを定義する→ジョブを投げて結果を確認する
という手順で、アニーリングマシンに最適化を行ってもらうことができる。逐一書いていたので記憶に残ってくれていると嬉しいが、実装のほとんどを定型分で済ませることができる。これがFixstars Amplifyの素晴らしいところで、ちょっと慣れてしまえば、頭を使うのは、ハミルトニアンを定義するところだけになる。つまり、研究活動で、一番大事なところだけに集中することができる。
本当は、もう一つくらいサンプル問題の解き方を書きたかったが、長くなりそうなので、一旦区切ることにする。先日、知人とこの話をしていて、「そんなくだらない問題を実機に投げるのはリソース無駄だろう」と言われたのだが、個人的には、くだらない問題でもガンガン使っていくといいと思う。例え話になるが、世の中にスパコンしかなかったら、スパコンの性能は、今ほど高いだろうか?コンピュータには汎用機があり、汎用機で些細な問題が解かれており、技術分野の裾野が広いから、頂点も高くなる。アニーリングマシンも同じことで、裾野を広げるのは大事だ。その中で、役にたつ使い方も見つかってくるだろう。(でも本当に迷惑だったら、Fixstarsさんには教えて欲しいとは思っている。もう少し複雑な問題の解き方を記事にします。)