ライドシェア¶
このチュートリアルでは、ライドシェアにおける利用者と車のマッチングの最適化を取り扱います。
問題設定¶
このサンプルコードでは、同じ目的地を持つ複数の利用者が出発地点に集合し、車に相乗りして目的地を目指す形式のライドシェアについて考えます。利用可能な車は複数台あり、それぞれが別の場所にあるとします。たとえば、同じオフィスで働いている人々が、そのうちの誰かの車に乗って一緒に通勤するという状況です。
各利用者の出発地点までの移動距離と使用する車の台数をなるべく小さくすることを目的として、最適な利用者への車の割り当てを求めます。
データ¶
データとしては、以下のような情報が必要です。これらは定数として与えられているとします。
- $N$:ライドシェア利用者数
- $M$:使用可能な車の台数
- $C$:車一台あたりの乗車可能人数
- $D$:利用者 $i$ と車 $k$ の間の距離を成分 $d_{ik}$ で表される距離行列
定式化¶
どのような定式化により利用者に車を割り当てる最適な方法が得られるのかを考えます。このチュートリアルにおける目的は以下のような割り当て方を得ることです。
- 各利用者と車の間の移動距離ができるだけ小さい
- 使用する車の台数ができるだけ少ない
変数¶
各利用者にどの車を割り当てるかをバイナリ変数を用いて表します。(利用者数) $\times$ (車の数) 個の変数からなる二次元配列 $q$ を用意し、変数 $q_{ik}$ は人 $i$ が車 $k$ に乗るかどうかを表すこととします。
たとえば、以下は $N=4$, $M=3$ で利用者 $0$ と利用者 $1$ が車 $0$ に、利用者 $2$ と利用者 $3$ は車 $2$ に乗ることを表します。
車 0 | 車 1 | 車 2 | |
---|---|---|---|
利用者 0 | 1 | 0 | 0 |
利用者 1 | 1 | 0 | 0 |
利用者 2 | 0 | 0 | 1 |
利用者 3 | 0 | 0 | 1 |
制約条件¶
続いて、変数が満たすべき制約を考えます。
まず、各利用者は必ずいずれか一台の車に乗ります。これは変数配列において各行の和が $1$ であることを意味します。以下の数式で表されます。
$$ \sum_{k=0}^{M-1}q_{ik}=1\ (\forall i\in\{0,\ldots,N-1\}) $$
次に、実際の乗車人数が乗車可能人数を上回らないことも必要です。これは変数配列において各列の和が $C$ 以下であることを意味します。以下の数式で表されます。
$$ \sum_{i=0}^{N-1}q_{ik}\leq C\ (\forall k\in\{0,\ldots,M-1\}) $$
目的関数¶
最後に、今回の目的である、
- 各利用者と車の間の移動距離ができるだけ小さい
- 使用する車の台数ができるだけ小さい
を表すための目的関数を考えます。
まず、「各利用者と車の間の移動距離ができるだけ小さい」を達成するためには、各利用者と、利用者に割り当てられた車との距離の総和を最小化すれば良いです。これは
$$ \text{minimize}\quad\sum_{i,k}d_{ik}q_{ik} $$
と表すことができます。$\sum_{i,k}d_{ik}q_{ik}$ は、利用者 $i$ が車 $k$ への移動を行う場合に $d_{ik}$ を加算するので、各利用者と利用者に割り当てられた車との距離の総和を表します。
次に、「使用する車の台数ができるだけ小さい」を表す目的関数を考えます。使用する車の台数を直接 $q$ で表すのは難しいので、満員に近い人数が乗っている車の数を増やすことで、使用する車の台数を間接的に削減します。
$$ \text{maximize}\quad\sum_{k}\left(\sum_i\frac{q_{ik}}{C}\right)^2 $$
$\sum_i q_{ik}$ は $k$ 列目の和であり、車 $k$ に乗っている人数と一致します。したがって $\displaystyle\sum_i\frac{q_{ik}}{C}$ は車 $k$ の乗車率を表します。乗車率の二乗和 $\displaystyle\sum_{k}\left(\sum_i\frac{q_{ik}}{C}\right)^2$ は満員に近い車の数が大きければ大きいほど大きくなります。
これら二つの目的関数を足し合わせることで、二つの目的をどちらもできるだけ達成するような目的関数を作成することができます。ただし、「使用する車の台数ができるだけ小さい」ことを表す方の目的関数は最大化問題であるため、-1 を乗じて最小化問題に変換してから足し合わせます。
$$ \text{minimize} \quad \sum_{i,k}d_{ik}q_{ik}-\alpha\sum_{k}\left(\sum_i\frac{q_{ik}}{C}\right)^2 $$
Note¶
$\alpha > 0$ は車の使用台数をどのくらい重視するかを決定するパラメータです。$\alpha$ が0に近いほど移動距離を小さくするように最適化が行われ、$\alpha$ の値が大きいほど車の使用台数を小さくするように最適化が行われます。
num_people = 30
num_cars = 10
car_capacity = 6
次に、利用者と車の初期位置を定め、各利用者と各車の距離を表す行列を作成します。以下の generate_problem
関数は利用者および車の初期位置 (緯度と経度)
をランダムに生成し、距離行列を計算します。緯度と経度の情報から 2 点間の距離を計算するのに、geopy
ライブラリを用いています。
import numpy as np
from geopy.distance import geodesic
def generate_problem(
num_people: int,
num_cars: int,
latitude_range: tuple[float, float],
longitude_range: tuple[float, float],
seed=999,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed)
# 利用者と車の座標を生成
people_coords = rng.uniform(
low=np.array([latitude_range[0], longitude_range[0]]),
high=np.array([latitude_range[1], longitude_range[1]]),
size=(num_people, 2),
)
car_coords = rng.uniform(
low=np.array([latitude_range[0], longitude_range[0]]),
high=np.array([latitude_range[1], longitude_range[1]]),
size=(num_cars, 2),
)
# 距離行列 d を作成 (d[i, k] が利用者 i と車 k の距離)
distances = np.zeros((num_people, num_cars))
for i in range(num_people):
for k in range(num_cars):
distances[i, k] = geodesic(
people_coords[i], car_coords[k]
).m # 単位はメートル
return people_coords, car_coords, distances
generate_problem
関数を用いて、座標を生成します。
# 船橋駅周辺
latitude_range = (35.675500, 35.76)
longitude_range = (139.9, 140.08)
people_coords, car_coords, distances = generate_problem(
num_people, num_cars, latitude_range, longitude_range
)
次に、生成した車と利用者の座標を地図上に可視化します。
import folium
def plot_people_and_cars(
people_coords: np.ndarray,
car_coords: np.ndarray,
latitude_range: tuple[float, float], # 描画範囲 (緯度)
longitude_range: tuple[float, float], # 描画範囲 (経度)
):
m = folium.Map(
[sum(latitude_range) / 2, sum(longitude_range) / 2],
tiles="OpenStreetMap",
zoom_start=12,
)
for latitude, longitude in people_coords:
folium.Marker(
location=(latitude, longitude),
icon=folium.Icon(icon="user", prefix="fa", color="orange"),
).add_to(m)
for latitude, longitude in car_coords:
folium.Marker(
location=(latitude, longitude),
icon=folium.Icon(icon="car", prefix="fa", color="green"),
).add_to(m)
return m
plot_people_and_cars(people_coords, car_coords, latitude_range, longitude_range)
以上で問題が作成できました。以下では、この利用者と車の位置を示すマップに対して、
- 各利用者の車までの移動距離が小さい
- 利用する車の台数が少ない
ような車の割り当て方法を求めることになります。
average = distances.mean()
std = distances.std()
distances: np.ndarray = (distances - average) / std
from amplify import VariableGenerator
gen = VariableGenerator()
q = gen.array("Binary", num_people, num_cars)
目的関数¶
次に目的関数を定義します。目的関数は
$$\sum_{i,k}d_{ik}q_{ik}-\alpha\sum_{k}\left(\sum_i\frac{q_{ik}}{C}\right)^2$$
で表されます。この式の第一項は各利用者の移動距離を短くするように働き、第二項は乗車率の二乗和をできるだけ大きくすることで使用する車の台数を少なくするように働きます。
今回は、目的関数の重みパラメータ $\alpha$ を 1 とします。$\alpha$ を小さくすると各利用者の移動距離がより短くなり、大きくすると使用する車の台数がより少なくなります。
目的関数の第一項は、距離行列 distances
と変数配列 q
の要素ごとの積の総和を取ることで実装できます。第二項の計算のためには、まず各車ごとの乗車率
$\sum_i\frac{q_{ik}}{C}$ を計算します。これは変数配列 q
の列ごとの和を取ったあと車の定員数で割ったものであり、二次元配列の列ごとの和は
sum
メソッドの axis
パラメータに 0 を与えることで計算できます。その後乗車率を 2 乗して和を取れば第二項が計算できます。
from amplify import sum
alpha = 1.0
# 第一項: 利用者の移動距離を短くする
distance_objective = (distances * q).sum()
# 第二項: 乗車率の二乗和を大きくする
occupancies = q.sum(axis=0) / car_capacity # 乗車率
occupancy_objective = (occupancies**2).sum()
objective = distance_objective - alpha * occupancy_objective
制約条件¶
制約条件を作成します。必要な制約は、
- 各利用者には 1 台の車を割り当てる必要がある制約 $\displaystyle \sum_k q_{ik} = 1$
- 各車に乗る人数は、車の定員を超えないという制約 $\displaystyle \sum_i q_{ik} \leq C$
の 2 種類です。
まず、各利用者には 1 台の車を割り当てる必要がある制約 $\displaystyle \sum_k q_{ik} = 1$ は、変数配列 q
の各行に 1 つだけ 1
があるという制約です。これは one_hot
関数を用いて書くことができ、$k$ に関する和を取るために axis
パラメータに 1
を与えることで、二次元配列
q
の各行に対する one-hot 制約を一括で生成できます。
from amplify import one_hot
one_person_one_car_constraints = one_hot(q, axis=1)
また、各車の定員を超えないという制約 $\displaystyle \sum_i q_{ik} \leq C$ は、変数配列 q
の各列の和が
car_capacity
を超えことを意味します。これは less_equal
関数を用いて書くことができ、$i$ に関する和を取るため
axis
パラメータに 0 を与えることで、二次元配列 q
の各列に対する不等式制約を一括で生成できます。
from amplify import less_equal
car_capacity_constraints = less_equal(q, car_capacity, axis=0)
次に、制約条件に対して重みを掛けます。今回使用する組合せ最適化ソルバーである Amplify AE では、制約条件を破った場合に目的関数にペナルティ項を足すことで制約条件を守らせようとします。このペナルティ項が小さすぎると制約条件が守られていな解が出力されるため、制約条件の重みとして適切な値を設定する必要があります。
2 種類の制約条件に対して、それぞれ以下のように適切な重みを見積もります。少し込み入っているため読み飛ばしても構いません。
1 台の車を割り当てる制約の重み¶
各利用者には 1 台の車を割り当てる必要がある制約 one_person_one_car_constraints
に対して、このうち 1 つの制約条件が 1
だけ破られた場合の目的関数の利得を考えてみます。
まず、目的関数の第一項 (利用者の総移動距離) については、1 人の利用者に車を割り当てないことによってその利用者の移動距離の分だけ得をします。また、目的関数の第二項 (乗車率の二乗和) については、1
人の利用者に 2 台の車を割り当てることによって、本来は載せることの出来ないにも関わらず乗車率を上げることができます。乗車率は 0 以上 1 以下なので、第二項で得られる利得は最大でも 1
となります。したがって、目的関数全体として得られる利得は、最大でも (距離行列の最大値) + $\alpha \times 1$ となります。この値を制約条件
one_person_one_car_constraints
の重みとします。
車の定員を超えない制約の重み¶
各車に乗る人数は、車の定員を超えないという制約 car_capacity_constraints
に対して、このうち 1 つの制約条件が 1 だけ破られた
(一人多く乗車してしまった)
場合の目的関数の利得を考えてみます。
まず、目的関数の第一項 (利用者の総移動距離) については、1 台の車に $C+1$ 人を乗せることによって、1 人の利用者の移動距離が本来よりも少し短くなる可能性があります。この差分は最大でも
(距離行列の最大値) です。次に、目的関数の第二項 (乗車率の二乗和) については、1 台の車に $C+1$ 人を乗せることによって、乗車率の 2 乗和が少し増える可能性があります。乗車率の 2
乗和が最も増えるのは、本来 1 人乗っている車と $C$ 人乗っている車が制約条件を破ることによって 0 人の車と $C+1$ 人の車になる場合で、この場合の差分は $\frac{2}{C}$
です。したがって、目的関数全体として得られる利得は、最大でも (距離行列の最大値) + $\alpha \times \frac{2}{C}$ となります。この値を制約条件
car_capacity_constraints
の重みとします。
以上の議論により、次のように制約条件に重み掛けて設定を行います。
one_person_one_car_constraints *= np.max(distances) + alpha * 1
car_capacity_constraints *= np.max(distances) + alpha * 2 / car_capacity
組合せ最適化モデルの構築¶
上記目的関数と制約条件を足し合わせることによって最終的な組合せ最適化モデルを構築できます。
from amplify import Model
model = objective + one_person_one_car_constraints + car_capacity_constraints
組合せ最適化ソルバーの実行¶
今回使用する組合せ最適化ソルバーは Amplify AE です。Amplify AE に対応するソルバークライアントである FixstarsClient
を作成し、タイムアウトを設定します。
上記で作成した組合せ最適化モデルとソルバークライアントを使用して、求解を実行します。
from amplify import FixstarsClient, solve
from datetime import timedelta
client = FixstarsClient()
client.parameters.timeout = timedelta(milliseconds=2000) # タイムアウトは 2000 ms
# client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # ローカル環境等で使用する場合は、Fixstars Amplify AE のアクセストークンを入力してください。
result = solve(model, client)
if len(result) == 0:
# 実行可能解が見つかっていなければ例外を投げる
raise RuntimeError("No feasible solution was found.")
続いて、得られた解を確認します。解における変数配列の値を取得するには、 q.evaluate()
を用います。
q_values = q.evaluate(result.best.values)
変数配列の各行にある num_cars
個の変数は、各利用者がどの車を使用するのかを表すものです。したがって、その行と、1 次元配列
[0, 1,..., num_cars - 1]
との内積を取れば、その行に対応する利用者に割り当てられた車のインデックスを取得することができます。
したがって、変数配列と 1 次元配列 [0, 1,..., num_cars - 1]
との行列積は、利用者 0, 利用者 1, $\ldots$, 利用者 $N-1$
にそれぞれ割り当てられた車のインデックスを表す長さ $N$ の一次元配列となります。
# 車インデックスのリスト (利用者インデックス順)
car_indices = (q_values @ np.arange(num_cars)).astype(int)
# (利用者インデックス, 車インデックス) のタプルのリスト
car_assignment = list(enumerate(car_indices))
for person_index, car_index in car_assignment:
print(f"利用者 {person_index}: 車 {car_index}")
最後に、以下の関数を用いて、変数配列の値から得られた割り当てを可視化します。車と利用者を、割り当てられた車ごとに異なる色で塗り分けます。
import branca.colormap as cm
def plot_result(
car_assignment: list[tuple[int, int]],
people_coords: np.ndarray,
car_coords: np.ndarray,
latitude_range: tuple[float, float], # 描画範囲 (緯度)
longitude_range: tuple[float, float], # 描画範囲 (経度)
):
# マップを用意
m = folium.Map(
[sum(latitude_range) / 2, sum(longitude_range) / 2],
tiles="OpenStreetMap",
zoom_start=12,
)
# 色の用意
colormap = cm.linear.Set1_09.scale(0, len(car_coords)).to_step(len(car_coords)) # type: ignore
# 車のプロット (k 番目の車を色 k で塗る)
for car_index, (latitude, longitude) in enumerate(car_coords):
folium.Marker(
location=(latitude, longitude),
popup=f"car {car_index}",
icon=folium.Icon(
icon="car", prefix="fa", color="white", icon_color=colormap(car_index)
),
).add_to(m)
# 利用者のプロット (車 k に乗る利用者を色 k で塗る)
for person_index, car_index in car_assignment:
latitude, longitude = people_coords[person_index]
folium.Marker(
location=(latitude, longitude),
popup=f"car {car_index}",
icon=folium.Icon(
icon="user",
prefix="fa",
color="white",
icon_color=colormap(car_index),
),
).add_to(m)
return m
plot_result(car_assignment, people_coords, car_coords, latitude_range, longitude_range)
発展的な話題(問題の分割)¶
上では比較的小さな問題を取り扱いました。使用する変数の数は高々数百でしたが、たとえば利用者と車が 100 人 (台) ずつある場合、10000 個のバイナリ変数が必要となり、Amplify AE で解いた場合、解を得るまでに大きな時間がかかったり、解の精度も悪くなることになります。そこで、クラスタリングにより、利用者と車をいくつかのクラスタに分割し、その後クラスタ内で先ほどの割り当て問題を解くという方針が考えられます。$K$ 個のクラスタに分割した場合、割り当て問題を解く際に使用する変数の数はおおよそ $K^2$ 分の 1 になることが見込まれます。
ここでは、利用者と車のクラスタリングを Amplify を用いて行い、その後それぞれのクラスタに対して割り当て問題を解く方法を紹介します。
問題設定¶
先ほどと同様に、以下のようなデータが与えられているとします。ただし、車一台あたりの乗車可能人数 $C$ はクラスタリングでは使用しません。
- $N$:ライドシェア利用者数
- $M$:使用可能な車の台数
- $C$:車一台あたりの乗車可能人数
- $D$:利用者 $i$ と車 $k$ の間の距離を成分 $d_{ik}$ で表される距離行列
これらのデータをもとに、まずクラスタリングを行い、その後それぞれのクラスタに対して割り当て問題の解を求めます。クラスタリングにおいては、各クラスタができるだけひとまとまりの位置に固まるように、利用者と車を均等に $K$ 個に分けることを目標とします。
定式化¶
クラスタリングの定式化方法について説明します。割り当て問題の定式化については既に説明した通りです。添え字に使っているアルファベットが割り当て問題のときと変わっているので注意してください。
変数¶
クラスタ数 $K$ に対して、$N \times K$ 個のバイナリ変数 $x$ と $M \times K$ 個のバイナリ変数 $y$ を用意します。
$x$ は各利用者がどのクラスタに所属するのかを表す変数であり、利用者 $i$ がクラスタ $k$ に所属するとき、$x_{i, k}$ が 1 となります。 $y$ は各車がどのクラスタに所属するのかを表す変数であり、車 $j$ がクラスタ $k$ に所属するとき、$x_{i, k}$ が 1 となります。
制約¶
各利用者および各車はいずれか 1 つのクラスタに所属する必要があるので、$x$ と $y$ の各行の和はすべて 1 である必要があります。
$$ \begin{align*} \sum_{k=0}^{K-1} x_{ik} &= 1 \\ \sum_{k=0}^{K-1} y_{jk} &= 1 \end{align*} $$
各クラスタに所属する利用者と車の数がなるべく均等になるようにしたいので、以下の制約を課します。これは、$x$ の各列の和が N / K に等しく、$y$ の各列の和は M / K に等しいことを意味します。ただし、利用者数 $N$ と車の数 $M$ がクラスタ数 $K$ の倍数でない場合は均等に分けられないため、端数の処理が必要になります。今回の実装においては、$N$ と $M$ が $K$ の倍数であると仮定して、端数の処理は行わないことにします。
$$ \begin{align*} \sum_{i=0}^{N-1} x_{ik} &= \frac{N}{K} \\ \sum_{i=0}^{M-1} y_{jk} &= \frac{M}{K} \end{align*} $$
目的関数¶
次に、目的関数を考えます。各クラスタができるだけひとまとまりの位置に固まるという条件を表現したいです。「同じクラスタに所属する人と車のペア」それぞれの距離の総和を取ったものを最小化することにより、各クラスタに所属する人と車を近づけることができそうです。
同じクラスタに所属する人と車の距離の総和は、距離行列 $D$ を用いて以下の式で表すことができます。$x_{i, k}y_{j, k}$ は利用者 $i$ と車 $j$ がともにクラスタ $k$ に所属する場合に 1、そうでない場合に 0 となるためです。
$$ \sum_{i, j} D_{i, j} \sum_k x_{i, k} y_{j, k} $$
実装¶
以上の定式化を元に、Amplifyを用いて実装していきます。まず、見栄えを良くするために、問題を少し大きく作り直しておきます。利用者の数 $N$ を 60 人、車の数 $M$ を 18 台とします。
num_people = 60
num_cars = 18
people_coords, car_coords, distances = generate_problem(
num_people, num_cars, latitude_range, longitude_range
)
plot_people_and_cars(people_coords, car_coords, latitude_range, longitude_range)
クラスタリングの定式化を行います。クラスタリングの数 $K$ は 3 とします。
num_clusters = 3
変数 $x$ と $y$ を定義します。それぞれ、各利用者および車がどのクラスタに所属するかを表す変数であり、サイズは (利用者あるいは車の数) × (クラスタの数) です。
gen = VariableGenerator()
x = gen.array("Binary", num_people, num_clusters)
y = gen.array("Binary", num_cars, num_clusters)
続いて目的関数を計算します。目的関数は同じクラスタに属する人と車の距離の総和を表し、
$$ \sum_{i, j} D_{i, j} \sum_k x_{i, k} y_{j, k} $$
で表されます。これは、
$$ \sum_{i, j, k} D_{i, j} x_{i, k} y_{j, k} $$
と変形できるため、einsum
関数を用いて書くことができます。
from amplify import einsum, Poly
objective: Poly = einsum("ij,ik,jk->", distances, x, y) # type: ignore
制約条件は以下の 4 種類です。最初の 2 つは $x$ と $y$ の各行についての one-hot 制約であり、残りの 2 つは $x$ と $y$ の各列についての等式制約となっています。
$$ \begin{align*} \sum_{k=0}^{K-1} x_{ik} &= 1 \\ \sum_{k=0}^{K-1} y_{jk} &= 1 \\ \sum_{i=0}^{N-1} x_{ik} &= \frac{N}{K} \\ \sum_{i=0}^{M-1} y_{jk} &= \frac{M}{K} \end{align*} $$
制約には十分な重みを与えておきます。
from amplify import one_hot, equal_to
# x の各行に対する one-hot 制約
people_row_constraints = one_hot(x, axis=1)
# y の各行に対する one-hot 制約
car_row_constraints = one_hot(y, axis=1)
# x の各列の和が N/K に等しい
people_col_constraints = equal_to(x, num_people // num_clusters, axis=0)
# y の各列の和が M/K に等しい
car_col_constraints = equal_to(y, num_cars // num_clusters, axis=0)
constraints = (
np.max(distances)
* (num_people // num_clusters)
* (
people_row_constraints
+ people_col_constraints
+ car_row_constraints
+ car_col_constraints
)
)
目的関数と制約条件を合わせて、組合せ最適化モデルを作成します。
clustering_model = Model(objective, constraints)
イジングマシンの実行¶
これまでと同様に、Amplify AE を実行して求解を行います。
client = FixstarsClient()
# client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
client.parameters.timeout = timedelta(milliseconds=1000) # タイムアウト 1 秒
result = solve(clustering_model, client)
if len(result) == 0:
# 実行可能解が見つからなければ例外を投げる
raise RuntimeError("No feasible solution was found.")
各利用者 / 車がどのクラスターに振り分けられたかは、以下のようにして取得できます。
x_values = x.evaluate(result.best.values)
y_values = y.evaluate(result.best.values)
people_cluster_indices = (x_values @ np.arange(num_clusters)).astype(int)
car_cluster_indices = (y_values @ np.arange(num_clusters)).astype(int)
for person_index, cluster_index in enumerate(people_cluster_indices[:5]):
# 長いので最初の 5 個だけ表示
print(f"利用者{person_index}: クラスター{cluster_index}")
for person_index, cluster_index in enumerate(people_cluster_indices[:5]):
# 長いので最初の 5 個だけ表示
print(f"車{person_index}: クラスター{cluster_index}")
結果を可視化します。利用者と車をクラスターごとに異なる色で塗り分けます。
def plot_clusters(
people_coords,
car_coords,
people_cluster_indices,
car_cluster_indices,
latitude_range,
longitude_range,
):
m = folium.Map(
[sum(latitude_range) / 2, sum(longitude_range) / 2],
tiles="OpenStreetMap",
zoom_start=12,
)
colors = ["red", "blue", "lightgray"]
assert len(colors) == num_clusters
for person_index, cluster_index in enumerate(people_cluster_indices):
latitude, longitude = people_coords[person_index]
folium.Marker(
location=(latitude, longitude),
popup=f"cluster{cluster_index}",
icon=folium.Icon(icon="user", prefix="fa", color=colors[cluster_index]),
).add_to(m)
for car_index, cluster_index in enumerate(car_cluster_indices):
latitude, longitude = car_coords[car_index]
folium.Marker(
location=(latitude, longitude),
popup=f"cluster{cluster_index}",
icon=folium.Icon(icon="car", prefix="fa", color=colors[cluster_index]),
).add_to(m)
return m
plot_clusters(
people_coords,
car_coords,
people_cluster_indices,
car_cluster_indices,
latitude_range,
longitude_range,
)
これで、利用者と車を均等に 3 つのクラスターに分けることができました。続いて、これらのクラスターごとに、利用者に最適に車を割り当てる方法を求めます。
まず、利用者と車のインデックスと距離行列をクラスターごとに分割します。
# 各クラスターに所属する利用者のインデックスのリストを作成
clustered_people_indices = [[] for k in range(num_clusters)]
for person_index, cluster_index in enumerate(people_cluster_indices):
clustered_people_indices[cluster_index].append(person_index)
# 各クラスターに所属する車のインデックスのリストを作成
clustered_car_indices = [[] for k in range(num_clusters)]
for car_index, cluster_index in enumerate(car_cluster_indices):
clustered_car_indices[cluster_index].append(car_index)
# 距離行列を分割
clustered_distances = [
distances[clustered_people_indices[k]][:, clustered_car_indices[k]]
for k in range(num_clusters)
]
クラスター 0 に所属する利用者のインデックスは clustered_people_indices[0]
で取得でき、クラスター 0 に所属する利用者のうち $i$
番目の利用者とクラスター 0 に所属する車のうち $j$ 番目の車との距離は clustered_distances[0][i, j]
により取得できます。これらを用いて、分割問題の求解を行います。手順はこのサンプルコードの前半で解説したのと同じです。
以下の関数は、距離行列を受け取って割り当て問題の求解を行い、利用者のインデックスと車のインデックスのタプルからなるリストを返します。ただし、利用者と車のインデックスは、各クラスター内におけるインデックスであり、利用者や車全体におけるインデックスとは異なることに注意してください。
def assign_cars(distances):
num_people = distances.shape[0]
num_cars = distances.shape[1]
# 問題の正規化
average = distances.mean()
std = distances.std()
distances = (distances - average) / std
# 変数配列の作成
gen = VariableGenerator()
q = gen.array("Binary", num_people, num_cars)
# 目的関数の構築
alpha = 1.0 # 目的関数の重みパラメータ
distance_objective = (distances * q).sum() # 目的関数第一項
occupancies = q.sum(axis=0) / car_capacity # 乗車率
occupancy_objective = (occupancies**2).sum() # 目的関数第二項
objective = distance_objective - alpha * occupancy_objective
# 制約条件の構築
one_person_one_car_constraints = one_hot(q, axis=1)
car_capacity_constraints = less_equal(q, car_capacity, axis=0)
# 制約重みを設定
one_person_one_car_constraints *= np.max(distances) + alpha * 1
car_capacity_constraints *= np.max(distances) + alpha * 2 / car_capacity
# モデルの構築
model = objective + one_person_one_car_constraints + car_capacity_constraints
# ソルバークライアントの設定
client = FixstarsClient()
client.parameters.timeout = timedelta(milliseconds=2000) # タイムアウトは 2000 ms
# client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # ローカル環境等で使用する場合は、Fixstars Amplify AE のアクセストークンを入力してください。
# 求解
result = solve(model, client)
q_values = q.evaluate(result.best.values)
car_indices = (q_values @ np.arange(num_cars)).astype(int)
# (利用者のインデックス, 車のインデックス) からなるリストを返却
return list(enumerate(car_indices))
上記で定義した関数を用いて、クラスターごとに割り当て問題の求解を実行します。
index_pairs_list = []
for d in clustered_distances:
index_pairs_list.append(assign_cars(d))
for k, index_pairs in enumerate(index_pairs_list):
print(f"クラスター {k}: ")
for person_index, car_index in index_pairs:
print(f" 利用者{person_index}: 車{car_index}")
求解を行って得られた index_pairs_list
は利用者のインデックスと車のインデックスを対応させるものですが、このインデックスは各クラスター内でのインデックスとなっています。clustered_people_indices
と
clustered_car_indices
を用いて、全体でのインデックスに復元します。
car_assignment = [
(clustered_people_indices[k][person_index], clustered_car_indices[k][car_index])
for k, index_pairs in enumerate(index_pairs_list)
for person_index, car_index in index_pairs
]
for person_index, car_index in car_assignment:
print(f"利用者{person_index}: 車{car_index}")
最後に、結果を可視化します。
plot_result(car_assignment, people_coords, car_coords, latitude_range, longitude_range)