[sumo-rl] 機械学習で交差点の信号制御を最適化してみた
はじめに
僕は某まちづくりゲームが好きなんですが、どう作っても渋滞します。調べたところ、信号制御のシミュレーションを機械学習(強化学習)で最適化できるライブラリがあったので、遊んでみました。
使うもの
| 名前 | バージョン | 備考 |
|---|---|---|
| Python | 3.12.10 | |
| uv | 0.10.0 | |
| SUMO (eclipse-sumo) | 1.27.1 | pipパッケージ。sumo/netconvert/sumo-guiバイナリを同梱 |
| sumolib | 1.27.1 | eclipse-sumoとバージョンを揃える必要あり |
| traci | 1.27.1 | 同上 |
| sumo-rl | 1.4.5 | SUMO用のGymnasium/PettingZoo環境ラッパー |
| Stable-Baselines3 | 2.9.0 | [extra]込み(tensorboard, tqdm等含む) |
| pandas | 3.0.6 | |
| matplotlib | 3.11.2 | |
| macOS | 26.6.2 (Apple Silicon) | |
| XQuartz | 2.8.6 | sumo-guiのウィンドウ表示に必要 |
SUMO について
SUMO はオープンソースの大規模な交通シミュレータです。
たくさんの機能や補助ツールがあり、ちゃんと勉強するとかなり時間を要しそうです。
また、このシミュレータを上からラップして、機械学習のAPIを提供するのがSUMO-RLです。
今回は主にこれらのシミュレータ・ツールを使い、交差点パフォーマンスを機械学習で最適化してみます。
インストール、環境構築
- uvでプロジェクトの初期化
uv init --app --no-readme --python 3.12 .
- 依存関係の追加
uv add "sumo-rl>=1.4.5" eclipse-sumo "stable-baselines3[extra]>=2.3" pandas matplotlib
- SUMO, sumo-rl系のバージョンを確認
uv pip list | grep -Ei "sumo|traci"
- 全てのバージョンが揃っていることを確認する(sumo-rl除く)
- 動作確認
uv run sumo --version
uv run netconvert --version
SUMO_HOME="$(uv run python -c 'import sumo; print(sumo.SUMO_HOME)')" uv run python -c "import sumo_rl, stable_baselines3"
- GUI用にXQuartzを追加
brew install --cask xquartz
単純な十字路で実験
まずは最低限の学習を回す
以下を作成します。
from pathlib import Path
import sumo_rl
from stable_baselines3 import DQN
net_dir = Path(sumo_rl.__file__).parent / "nets" / "single-intersection"
env = sumo_rl.SumoEnvironment(
net_file=str(net_dir / "single-intersection.net.xml"),
route_file=str(net_dir / "single-intersection.rou.xml"),
single_agent=True,
use_gui=False,
num_seconds=1000,
)
model = DQN("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=5000)
model.save("dqn_test")
env.close()
print("done: dqn_test.zip を保存しました")
学習は以下のコマンドを実行することで始まります。そんなに時間はかかりません。
export SUMO_HOME="$(uv run python -c 'import sumo; print(sumo.SUMO_HOME)')"
uv run python minimal_train.py
少し待つと dqn_test.zip ができているはずです。確認のために以下のスクリプトを作成します。
import argparse
from pathlib import Path
import sumo_rl
from stable_baselines3 import DQN
parser = argparse.ArgumentParser()
parser.add_argument(
"--random",
action="store_true",
help="学習済みモデルを使わず、ランダム行動(学習前相当)で動かす",
)
args = parser.parse_args()
net_dir = Path(sumo_rl.__file__).parent / "nets" / "single-intersection"
env = sumo_rl.SumoEnvironment(
net_file=str(net_dir / "single-intersection.net.xml"),
route_file=str(net_dir / "single-intersection.rou.xml"),
single_agent=True,
use_gui=True,
num_seconds=1000,
additional_sumo_cmd="--delay 100", # 1ステップ100ms
)
model = None if args.random else DQN.load("dqn_test.zip")
obs, info = env.reset()
done = False
total_reward = 0.0
while not done:
if model is None:
action = env.action_space.sample()
else:
action, _ = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, info = env.step(int(action))
total_reward += reward
done = terminated or truncated
mode = "random (学習前相当)" if args.random else "trained (学習後)"
print(f"mode: {mode}")
print("total_reward:", total_reward)
env.close()
実行
作成したら、以下のように実行します。
// Before
uv run python minimal_gui_check.py --random
// After
uv run python minimal_gui_check.py
[トレーニング前 (ランダム)]

[トレーニング後の結果]

ランダムで信号制御しているときは交通量の多い東西方向が詰まってしまっていますが、トレーニング後では比較的改善されている様子がわかります。それでも若干詰まり気味ですが。(学習数が少ない?)
ちなみに今回の学習パラメータは以下のとおりです。
| パラメータ | 値 | 何が変わるか | コード中の設定箇所 |
|---|---|---|---|
| single_agent | True | 信号1個だけをGym環境として扱う(複数信号ならマルチエージェントAPIが必要) | minimal_train.py > L17 |
| use_gui | False | GUIなしで高速に実行するか | minimal_train.py > L18 |
| num_seconds | 1000秒 | 1エピソードの長さ。短いほど1エピソードあたりの学習機会が減る | minimal_train.py > L19 |
| delta_time | 5秒 | 何秒おきに行動(信号フェーズ)を選び直すか。短いほど細かい制御ができるが行動数が増える | デフォルト |
| yellow_time | 2秒 | フェーズ切り替え時の黄信号の長さ | デフォルト |
| min_green / max_green | 5秒 / 50秒 | 1フェーズが最低・最長でどれくらい続くか(頻繁な切り替えや極端に長い緑を防ぐ制約) | デフォルト |
| reward_fn | diff-waiting-time | 報酬の定義。前ステップからの累積待ち時間の減少量を報酬にする | デフォルト |
| sumo_seed | "random" | シミュレーションの乱数シード。 | デフォルト |
| policy | MlpPolicy | 全結合ニューラルネットで方策を近似する | minimal_train.py > L22 |
| total_timesteps | 5000 | 学習の総ステップ数。1エピソード200ステップ(1000秒÷5秒)なので、約25エピソードしか学習していない | minimal_train.py > L23 |
| learning_rate | 0.0001 | 1回の更新でどれだけパラメータを動かすか | デフォルト |
| buffer_size | 1,000,000 | リプレイバッファに保持する経験の最大数 | デフォルト |
| learning_starts | 100ステップ | 何ステップ経験を貯めてから学習を始めるか | デフォルト |
| batch_size | 32 | 1回の更新で使うサンプル数 | デフォルト |
| gamma | 0.99 | 将来の報酬をどれだけ重視するか(割引率) | デフォルト |
| train_freq | 4ステップごと | 何ステップおきにネットワークを更新するか | デフォルト |
| target_update_interval | 10,000ステップ | ターゲットネットワークを何ステップおきに同期するか | デフォルト |
| exploration_fraction | 0.1 | 学習全体の何割を、ランダム行動の割合を減らしていく期間に充てるか | デフォルト |
| exploration_final_eps | 0.05 | 探索率(ランダム行動の割合)の最終値 | デフォルト |
| seed | None | 学習の乱数シード。 | デフォルト |
- total_timestepsの考え方
- 行動を選び直す機会(=アクション数) = num_seconds (1000s) / delta_time (5s) = 200回
- total_timesteps (5000step) / 200回 = 25エピソード
学習回数を増やしてみる
お試しで回したのは5,000stepの学習でした。続いて50,000stepで学習を回してみます。エピソード数は10倍の250エピソードです。
[50,000ステップ 結果]

序盤で少し不安な感じでしたが、中盤から詰まらなくなった気がします。実際に学習曲線を出力して確認します。
以下は5,000stepと50,000stepの学習曲線です。50,000stepは若干過剰かもしれないですが、5,000stepはまだ収束しきれていないような具合に見えます。

より複雑な交差点で試してみる
実はSUMOでは交差点の形も任意に決めることができます。また、実際の地図から道路をインポートする機能までついています。
ただし、このブログで地図を使っていいか(権利的に)怪しいのと、特定の地点を指差してこの交差点は効率悪いです!と言うことになるのが憚られるのでここでは使いません。
neteditというSUMOに同梱のツールを使用して、かわりに交差点設定ファイルを直接作成します。
交差点作成
export SUMO_HOME="$(uv run python -c 'import sumo; print(sumo.SUMO_HOME)')"
uv run netedit --new
--new のオプションで新規作成です。ファイル名は任意ですが、拡張子は .net.xml で作るのが良いと思います。
ちょっと癖のあるUIですが、最低限は以下のようにして作成できます。
- リボンからNetwork > Edge と選択します。

- 左のペインでは以下を設定します。
| 項目名 | デフォルト | 設定値の意味 |
|---|---|---|
| numLanes | 1 | 車線数(片側) |
| speed | 13.89 | 制限速度 (m/s) |
制限速度の単位が km/h ではないところに注意が必要です。
| km/h | m/s |
|---|---|
| 10 | 2.78 |
| 20 | 5.56 |
| 30 | 8.33 |
| 40 | 11.11 |
| 50 | 13.89 |
| 60 | 16.67 |
| 70 | 19.44 |
| 80 | 22.22 |
| 90 | 25 |
| 100 | 27.78 |
| 110 | 30.56 |
| 120 | 33.33 |
| 130 | 36.11 |
| 140 | 38.89 |
| 150 | 41.67 |
- 道路を作成します。赤い丸の位置にエッジ(=道路端)をクリックで設置できるので、始点と終点をクリックします。片側ずつの設置になります。対向車線は終点から始点をクリックしてあげると繋がります。(数字はクリック順序)

- ここまでの機能で以下のような道路を作りました。

- 続いて、信号機の設定をします。
信号機のボタンを押してから、信号機を設定する交差点をクリックします。

その後、左のペインに create のボタンがアクティベートされるので、押下します。車線の接続が出てきますが、無視 or ESCでOKです。

下図のようになっていれば完成です。

- 作成したnetファイルを元に、ルートを生成します。以下コマンドです。netファイルとrouファイルのパスは適宜読み替えてください。
export SUMO_HOME="$(uv run python -c 'import sumo; print(sumo.SUMO_HOME)')"
uv run python "$SUMO_HOME/tools/randomTrips.py" \
-n path/to/complex.net.xml \
-r path/to/complex.rou.xml \
-e 1000 \
-p 1.5 \
--fringe-factor 100
- 最後に、minimal_train.pyとminimal_gui_check.pyの、net_fileとroute_file の参照をこの作成したファイルに差し替えます。
さらなる改善
今回の道路は交差点の車線数が多かったり、接続数が多いため、そもそもの信号フェーズが微妙です。そのため、信号を調整します。やらなくてもいいです。
- 車線の編集
リボンからConnection Modeボタンを押して車線編集ツールを起動します。

- この状態で停止線がある側の車線をクリックすると、どの車線に進めるかを選べるようになります。進んでもよい車線に水色の線が繋がるように編集します。

- 当該車線の行き先を決めたら左のペインからOKを押下します。

- 色々調整して、最終的に以下のようになりました。

- 続いて、信号フェーズを編集します。リボンからTraffic light Modeを押下します。その後フェーズを編集したい信号機をクリックします。

- 左のペインにフェーズの情報が表示されます。表の行をクリックすると当該フェーズの信号の状態が見れます。

- この行を選択した状態で、直接図中の車線の行き先を示す線を右クリックすることで、そのフェーズにおけるその信号の表示を変更できます。ペインの表中のG,r,yなどは直接編集する必要はありません。

- 各フェーズについて設定したら、Saveを押下します。

- netファイルを保存して、再度routeファイルを生成すればOKです。
[複雑な交差点 学習前]

学習
今回の道路は信号が2つあるので、single_agent=True の現状だと正しく学習されません。以下のようにしてマルチエージェント化します。
- パッケージ追加
uv add supersuit
- パッケージバージョンを固定する
uv add すると整合性の取れていないバージョンがインストールされたので、以下のようにtomlに2行追加し、固定します。
[project]
name = "sumo-rl-intersection"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = [
"eclipse-sumo>=1.27.1",
"matplotlib>=3.11.2",
"pandas>=3.0.6",
"pettingzoo==1.23.1", # ここを追加
"stable-baselines3[extra]>=2.3",
"sumo-rl>=1.4.5",
"supersuit==3.9.0", # ここを追加
]
- パッケージのバージョン同期
uv sync
- マルチエージェント対応のスクリプトを作成
PettingZoo というライブラリでマルチエージェントの環境を作るため、何点か変更があります。また、PettingZoo形式→Stable Baseline3形式への変換も追加しています。
PettingZoo はマルチエージェント対応の機械学習インターフェースです。
import supersuit as ss
import sumo_rl
from stable_baselines3 import DQN
from stable_baselines3.common.vec_env import VecMonitor
env = sumo_rl.parallel_env(
net_file="path/to/complex.net.xml", # 適宜読み替え
route_file="path/to/complex.rou.xml", # 適宜読み替え
use_gui=False,
num_seconds=1000,
)
env = ss.pad_observations_v0(env) # 信号ごとに観測次元が違うので、大きい方に合わせてパディングする
env = ss.pad_action_space_v0(env) # 行動空間(フェーズ数)も同様に合わせる
env = ss.pettingzoo_env_to_vec_env_v1(env)
env = ss.concat_vec_envs_v1(env, num_vec_envs=1, num_cpus=1, base_class="stable_baselines3")
env = VecMonitor(env)
model = DQN("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=50000)
model.save("dqn_multiagent_test")
print("done: dqn_multiagent_test.zip を保存しました")
結果
※ 再生するためにはminimal_gui_check.pyのL31 でDQN.loadするファイルをdqn_multiagent_test.zipに差し替える必要があります。
[複雑な交差点 学習後]

おわりに
今回はOSSの大規模交通シミュレータ「SUMO」とその機械学習ラッパー「SUMO-RL」を使って、交差点の信号間隔の最適化を機械学習でやってみました。実際にはそもそもの信号フェーズなどももっと最適化しないといけないし、SUMOでは自動車以外のシミュレートも含めることができるので、まだまだ遊べそうだと思いました。








