8
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

D-Waveの制約式をイジングモデルに変換するライブラリの仕組みを追ってみた

dwavebinarycsp

量子アニーリングマシンD-Waveの dwavebinarycsp というライブラリを使うと、制約式充足問題を二次形式最小化問題すなちイジングモデル(もしくはQUBO)に変換できます。たとえば、

a, b, c \in \{-1, 1\} \\
abc = 1 \\
a+b+c < 3

といった制約式充足問題があったとするなら、

import dwavebinarycsp

csp = dwavebinarycsp.ConstraintSatisfactionProblem(dwavebinarycsp.SPIN)
csp.add_constraint(lambda a, b, c: a * b * c == 1, ['a', 'b', 'c'])
csp.add_constraint(lambda a, b, c: a + b + c < 3,  ['a', 'b', 'c'])
bqm = dwavebinarycsp.stitch(csp)

print(bqm)
BinaryQuadraticModel({'a': -1.0, 'aux0': -2.0, 'aux1': 0.0, 'b': 2.0, 'c': 1.0, 'aux2': -1.0}, {('aux0', 'a'): 1.0, ('aux1', 'a'): 1.0, ('aux1', 'aux0'): -1.0, ('b', 'a'): -1.0, ('b', 'aux0'): -1.0, ('b', 'aux1'): -1.0, ('c', 'a'): 0.0, ('c', 'aux0'): -1.0, ('c', 'aux1'): -1.0, ('c', 'b'): 1.0, ('aux2', 'a'): 1.0, ('b', 'aux2'): -1.0, ('c', 'aux2'): 1.0}, 0.0, Vartype.SPIN)

となるので、補助変数 aux0 を $x$ 、 aux1 を $y$ 、 aux2 を $z$ 、と表すことにすると

\newcommand{\argmin}{\mathop{\rm arg~min}\limits}
\argmin_{a, b, c \in \{-1, 1\}} -a+2b+c-2x-z-ab+bc+ax+ay+az-bx-by-bz-cx-cy+cz-xy

というイジングモデルに変換できることが分かります。やりましたね。

さて、このライブラリはどうやってこの変換を計算しているのでしょうか。
注目すべきは add_constraint に任意のラムダ式を食わせられるところです。ということは制約式を二乗してN体を分解するといった、代数的な変換操作をしているわけではなさそうですね?
ということで add_constraint した後に制約式をどう保持しているか覗いてみると、

print(csp.constraints)
[Constraint.from_configurations(frozenset({(1, -1, -1), (1, 1, 1), (-1, -1, 1), (-1, 1, -1)}), ('a', 'b', 'c'), Vartype.SPIN, name='Constraint'), Constraint.from_configurations(frozenset({(1, 1, -1), (-1, 1, 1), (1, -1, -1), (-1, -1, 1), (1, -1, 1), (-1, 1, -1), (-1, -1, -1)}), ('a', 'b', 'c'), Vartype.SPIN, name='Constraint')]

と食わせたラムダ関数は消失していて、代わりに解集合が保持されていることが見てとれますね。
実際 add_constraint にラムダ式を食わせると、下記の関数が呼ばれて解集合に変換されています。

class Constraint(Sized):
...
    def from_func(cls, func, variables, vartype, name=None):
...
        configurations = frozenset(config
                                   for config in itertools.product(vartype.value, repeat=len(variables))
                                   if func(*config))

dwavebinarycsp/dwavebinarycsp/core/constraint.py

ということで、次は同じ解集合を持つ最小化問題を見つければいいという話になって、それをしている入り口の関数が stitch です。抜粋します。

def stitch(csp, min_classical_gap=2.0, max_graph_size=8):
...
    bqm = dimod.BinaryQuadraticModel.empty(csp.vartype)
...
    for const in csp.constraints:
...
        pmodel = None
...
        for G in iter_complete_graphs(const.variables, max_graph_size + 1, aux):

            # construct a specification
            spec = pm.Specification(
                graph=G,
                decision_variables=const.variables,
                feasible_configurations=configurations,
                vartype=csp.vartype
            )

            # try to use the penaltymodel ecosystem
            try:
                pmodel = pm.get_penalty_model(spec)
            except pm.ImpossiblePenaltyModel:
                # hopefully adding more variables will make it possible
                continue

            if pmodel.classical_gap >= min_classical_gap:
                break
...
        bqm.update(pmodel.model)

    return bqm

dwavebinarycsp/dwavebinarycsp/compilers/stitcher.py

すなわち。制約式ごとに get_penalty_model で最小化問題への変換を試みて、解を満たすときと満たさないときの最低エネルギーギャップが閾値を超える最小化問題が見つかるまで、補助変数を追加します。そして、それらを足し合わせたものが答というわけです。ぱちぱち。

penaltymodel

それで肝心の get_penalty_model の実装ですが、これは penaltymodel という別のD-Waveライブラリを呼びだしています。
penaltymodel はちょっとややこしくて、インストールパッケージがコアの penaltymodel のほか penaltymodel-cache penaltymodel-maxgap penaltymodel-mip と用意されていて、何をインストールするかによって get_penalty_model の実装を切り替えられるようになっています。

どういうことかと penaltymodel-cache を覗いてみると、 penaltymodel_factory(100) と一番高い優先度が振られていて、これはまぁキャッシュから引いているだけですね。
キャッシュヒットしなかったら MissingPenaltyModel という例外が投げられて、もっと優先度の低いものが呼ばれます。

@pm.interface.penaltymodel_factory(100)
def get_penalty_model(specification, database=None):
    """Factory function for penaltymodel_cache.
...
    Raises:
        :class:`penaltymodel.MissingPenaltyModel`: If the penalty model is not in the
            cache.

penaltymodel/penaltymodel_cache/penaltymodel/cache/interface.py

つまり、たとえばこの penaltymodel-maxgap です。

@pm.penaltymodel_factory(-100)  # set the priority to low
def get_penalty_model(specification):
    """Factory function for penaltymodel_maxgap.
...
    Raises:
        :class:`penaltymodel.ImpossiblePenaltyModel`: If the penalty cannot be built.

penaltymodel/penaltymodel_maxgap/penaltymodel/maxgap/interface.py

さらにこの実装を追っていくと、コメントで論文参照となっています。

"""
.. [DO] Bian et al., "Discrete optimization using quantum annealing on sparse Ising models",
        https://www.frontiersin.org/articles/10.3389/fphy.2014.00056/full
.. [MC] Z. Bian, F. Chudak, R. Israel, B. Lackey, W. G. Macready, and A. Roy
        "Mapping constrained optimization problems to quantum annealing with application to fault diagnosis"
        https://arxiv.org/pdf/1603.03111.pdf
"""

penaltymodel/penaltymodel_maxgap/penaltymodel/maxgap/generation.py

……なにかしらの最適化問題を解いているということのようですね。
量子アニーリングマシンは組合せ最適化問題が得意ではあるのですが、量子アニーリングマシンに解かせるための過程で古典マシンで最適化問題を解かなければならないことは、量子アニーリングあるあるみたいです。人生そんなもんです。それでも全体的には計算効率が良くなることを祈りましょう。

さて penaltymodel-maxgap がインストールされていない場合、優先度最低な penaltymodel-mipget_penalty_model が使われます。

@pm.penaltymodel_factory(-150)
def get_penalty_model(specification):
    """Factory function for penaltymodel-mip.
...
    Raises:
        :class:`penaltymodel.ImpossiblePenaltyModel`: If the penalty cannot be built.

penaltymodel/penaltymodel_mip/penaltymodel/mip/interface.py

こちらの実装は Google Optimization Tools を使って、最適化問題を解いているようです。

dimod

さて、手に入れたイジングモデルが正しいかどうか確認してみましょう。
いざD-Waveの実機に投げ……てもいいのですが、問題サイズが小さいので古典的なソルバーで見てしまいましょう。
やはりD-Waveのライブラリ dimod を使うと、

import dimod
import itertools

res = dimod.ExactSolver().sample(bqm)
min_energy = next(res.data(sorted_by='energy')).energy
for sample in itertools.takewhile(lambda sample: sample.energy == min_energy, res.data(sorted_by='energy')):
    print(csp.check(sample.sample), sample.energy, sample.sample)
True -7.0 {'a': 1, 'aux0': -1, 'aux1': -1, 'aux2': -1, 'b': -1, 'c': -1}
True -7.0 {'a': 1, 'aux0': -1, 'aux1': -1, 'aux2': 1, 'b': -1, 'c': -1}
True -7.0 {'a': -1, 'aux0': 1, 'aux1': 1, 'aux2': 1, 'b': -1, 'c': 1}
True -7.0 {'a': -1, 'aux0': 1, 'aux1': 1, 'aux2': -1, 'b': -1, 'c': 1}
True -7.0 {'a': -1, 'aux0': 1, 'aux1': 1, 'aux2': 1, 'b': 1, 'c': -1}

と制約式を満たす解が5つ見つかりました。
あれ3つなはずでは?と思うかもしれませんが、それは補助変数のバリエーションが出てしまっているからで、 (a,b,c) のパターンは3つですね。

エネルギーギャップ

ところでこの問題に補助変数3つもいるの?と思ったりするわけですが、実は stich を呼ぶときに min_classical_gap を小さく指定してやると、

bqm = dwavebinarycsp.stitch(csp, min_classical_gap=.1)

print(bqm)
BinaryQuadraticModel({'a': -0.5, 'aux0': -1.0, 'b': 1.5, 'c': 0.5, 'aux1': -1.0}, {('aux0', 'a'): 1.0, ('b', 'a'): -0.5, ('b', 'aux0'): -1.0, ('c', 'a'): 0.5, ('c', 'aux0'): -1.0, ('c', 'b'): 0.5, ('aux1', 'a'): 1.0, ('b', 'aux1'): -1.0, ('c', 'aux1'): 1.0}, 0.0, Vartype.SPIN)

補助変数が2つで済むようになりました。
ただし最低エネルギーギャップが低いということは、おそらく最適解以外に落ちる可能性が上がってしまうのと、制約付き最小化問題なんかを扱うとき制約式を破りやすくなってしまうので注意が必要です。
そしてエネルギーギャップの大きさなんて二次形式の係数次第なんじゃね?という気もしますが、係数の範囲は ising_linear_ranges ising_quadratic_ranges で指定することができて、デフォルト値は [-2, 2] [-1, 1] となっているようです。

class Specification(object):
...
    def __init__(self, graph, decision_variables, feasible_configurations, vartype,
                 ising_linear_ranges=None,
                 ising_quadratic_ranges=None):
...
                linear_ranges[v] = [-2, 2]
...
                quad_ranges[u][v] = quad_ranges[v][u] = [-1, 1]

penaltymodel/penaltymodel_core/penaltymodel/core/classes/specification.py

実行環境

$ python --version
Python 3.7.0

$ pip list | grep -e dwavebinarycsp -e penaltymodel -e dimod
dimod               0.7.9     
dwavebinarycsp      0.0.9     
penaltymodel        0.15.3    
penaltymodel-cache  0.3.2     
penaltymodel-maxgap 0.4.1     
penaltymodel-mip    0.1.3     

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
8
Help us understand the problem. What are the problem?