[sumo-rl] 機械学習で交差点の信号制御を最適化してみた

[sumo-rl] 機械学習で交差点の信号制御を最適化してみた

大規模交通シミュの「SUMO」とその機械学習用ラッパー「SUMO-RL」で信号間隔の制御を最適化してみました。
2026.09.30

はじめに

僕は某まちづくりゲームが好きなんですが、どう作っても渋滞します。調べたところ、信号制御のシミュレーションを機械学習(強化学習)で最適化できるライブラリがあったので、遊んでみました。

使うもの

名前 バージョン 備考
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 はオープンソースの大規模な交通シミュレータです。

https://sumo.dlr.de/docs/index.html

たくさんの機能や補助ツールがあり、ちゃんと勉強するとかなり時間を要しそうです。

また、このシミュレータを上からラップして、機械学習のAPIを提供するのがSUMO-RLです。

https://github.com/LucasAlegre/sumo-rl

今回は主にこれらのシミュレータ・ツールを使い、交差点パフォーマンスを機械学習で最適化してみます。

インストール、環境構築

  1. uvでプロジェクトの初期化
uv init --app --no-readme --python 3.12 .
  1. 依存関係の追加
uv add "sumo-rl>=1.4.5" eclipse-sumo "stable-baselines3[extra]>=2.3" pandas matplotlib
  1. SUMO, sumo-rl系のバージョンを確認
uv pip list | grep -Ei "sumo|traci"
  • 全てのバージョンが揃っていることを確認する(sumo-rl除く)
  1. 動作確認
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"
  1. GUI用にXQuartzを追加
brew install --cask xquartz

単純な十字路で実験

まずは最低限の学習を回す

以下を作成します。

minimal_train.py
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 ができているはずです。確認のために以下のスクリプトを作成します。

minimal_gui_check.py
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

[トレーニング前 (ランダム)]
output-before

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

ランダムで信号制御しているときは交通量の多い東西方向が詰まってしまっていますが、トレーニング後では比較的改善されている様子がわかります。それでも若干詰まり気味ですが。(学習数が少ない?)

ちなみに今回の学習パラメータは以下のとおりです。

パラメータ 値 何が変わるか コード中の設定箇所
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ステップ 結果]
output-3

序盤で少し不安な感じでしたが、中盤から詰まらなくなった気がします。実際に学習曲線を出力して確認します。

以下は5,000stepと50,000stepの学習曲線です。50,000stepは若干過剰かもしれないですが、5,000stepはまだ収束しきれていないような具合に見えます。

learning_curve_compare

より複雑な交差点で試してみる

実はSUMOでは交差点の形も任意に決めることができます。また、実際の地図から道路をインポートする機能までついています。

https://sumo.dlr.de/docs/Tutorials/OSMWebWizard.html

ただし、このブログで地図を使っていいか(権利的に)怪しいのと、特定の地点を指差してこの交差点は効率悪いです!と言うことになるのが憚られるのでここでは使いません。
neteditというSUMOに同梱のツールを使用して、かわりに交差点設定ファイルを直接作成します。

https://sumo.dlr.de/docs/Netedit/index.html

交差点作成

export SUMO_HOME="$(uv run python -c 'import sumo; print(sumo.SUMO_HOME)')"
uv run netedit --new

--new のオプションで新規作成です。ファイル名は任意ですが、拡張子は .net.xml で作るのが良いと思います。
ちょっと癖のあるUIですが、最低限は以下のようにして作成できます。

  1. リボンからNetwork > Edge と選択します。

スクリーンショット 2026-09-30 10.31.26

  1. 左のペインでは以下を設定します。
項目名 デフォルト 設定値の意味
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
  1. 道路を作成します。赤い丸の位置にエッジ(=道路端)をクリックで設置できるので、始点と終点をクリックします。片側ずつの設置になります。対向車線は終点から始点をクリックしてあげると繋がります。(数字はクリック順序)

スクリーンショット 2026-09-30 11.15.46

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

スクリーンショット 2026-09-30 12.53.27

  1. 続いて、信号機の設定をします。

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

スクリーンショット 2026-09-30 12.58.54

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

スクリーンショット 2026-09-30 13.03.59

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

スクリーンショット 2026-09-30 13.08.06

  1. 作成した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
  1. 最後に、minimal_train.pyとminimal_gui_check.pyの、net_fileとroute_file の参照をこの作成したファイルに差し替えます。

さらなる改善

今回の道路は交差点の車線数が多かったり、接続数が多いため、そもそもの信号フェーズが微妙です。そのため、信号を調整します。やらなくてもいいです。

  1. 車線の編集

リボンからConnection Modeボタンを押して車線編集ツールを起動します。

スクリーンショット 2026-09-30 15.59.04

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

スクリーンショット 2026-09-30 16.02.39

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

スクリーンショット 2026-09-30 16.03.48

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

スクリーンショット 2026-09-30 16.43.02

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

スクリーンショット 2026-09-30 17.23.52

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

スクリーンショット 2026-09-30 17.26.10

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

スクリーンショット 2026-09-30 17.28.47

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

スクリーンショット 2026-09-30 17.30.23

  1. netファイルを保存して、再度routeファイルを生成すればOKです。

[複雑な交差点 学習前]
output-5

学習

今回の道路は信号が2つあるので、single_agent=True の現状だと正しく学習されません。以下のようにしてマルチエージェント化します。

  1. パッケージ追加
uv add supersuit
  1. パッケージバージョンを固定する

uv add すると整合性の取れていないバージョンがインストールされたので、以下のようにtomlに2行追加し、固定します。

pyproject.toml
[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", # ここを追加
]

  1. パッケージのバージョン同期
uv sync
  1. マルチエージェント対応のスクリプトを作成

PettingZoo というライブラリでマルチエージェントの環境を作るため、何点か変更があります。また、PettingZoo形式→Stable Baseline3形式への変換も追加しています。

PettingZoo はマルチエージェント対応の機械学習インターフェースです。
https://pettingzoo.farama.org/index.html

minimal_train_multiagent.py
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に差し替える必要があります。

[複雑な交差点 学習後]
output-6

おわりに

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

この記事をシェアする

関連記事