TL;DR
- 強化学習とは、ニューラルネットワークを使って意思決定を行い、不確実な条件下のシステムを制御する AI ベースの制御アルゴリズムです。たとえば JEPX で裁定取引をする蓄電池などが対象になります。
- JEPX スポット市場(一日前市場)で裁定取引を行う 4 MW / 10 MWh の系統用蓄電池を、PPO というアルゴリズムでスケジューリングしました。
- SOC の範囲・1日1サイクル・定格出力といった物理的な制約は、行動マッピングという仕組みで「そもそも破れない」ようにしています。報酬のペナルティも、事後的な補正も使っていません。結果は評価した全日で違反ゼロです。
- エージェントは完全予見最適(線形計画法で求めた理論上の最大値)の 88.1% に到達しました。最適解が分からないような、より不確実なシステムに対しても、強化学習が十分に信頼できる意思決定を出せることを示しています。
0. はじめに
お久しぶりです。SCのムロです。これまでの記事では時系列予測(過去のデータから未来を当てること)を中心に扱ってきました。もちろんそれは AI の重要な役割のひとつですが、ふと考えました。AI が力を発揮できる場面は他にもたくさんあって、そのひとつがシステムの制御、つまりスケジューリングや意思決定ではないか、と。
VPP にはスケジューリングしたいリソースが大量にあります。主に太陽光と蓄電池ですが、それぞれに固有の特性や制約があり、さらに JEPX や EPRX といった変動の激しい日本の電力市場も考慮しなければなりません。線形ソルバーのような従来型の制御アルゴリズムでは、どうしても良い解にたどり着けない領域が出てきます。AI の分野でそこを担えるのが強化学習です。この記事では、強化学習とはそもそも何なのかを掘り下げつつ、系統用蓄電池への適用例を簡単なデモでお見せします。
1. なぜ蓄電池に強化学習なのか
日本の系統用蓄電池は、複数の場所から同時に収益を得られます。JEPX のスポット市場と、EPRX の需給調整市場(一次・二次・三次調整力)です。1日分のエネルギーとサイクルの予算を、これらの商品にどう配分するか — これは不確実性の高い、連続した意思決定の問題です。
市場のルールと蓄電池の物理をすべて1つの最適化問題として書き下すことは可能ですが、かなり重たい作業になりますし、ルールが変わるたびにやり直しになります。そこで本番の問題に挑む前に、**最小限の概念実証(PoC)**を作りました。知りたかったのはこの2点です。強化学習エージェントは市場参加のスケジュールをそもそも作れるのか。そして、理論上到達できる水準と比べてどのくらい良いのか。
つまりこの記事には2つの狙いがあります。ひとつは強化学習が実際にどう動いているのかを、機械学習の前提知識なしで順を追って説明すること。もうひとつは、実際のエネルギー問題で最善の答えにどこまで迫れるのかを示すことです。後者ができるのは、このデモの作り方のおかげです。今回の問題に限っては最善の答えを厳密に計算できるので、エージェントを正直に採点する基準が存在します。
記事はそこそこ長いので、気になるところから読んでいただいて大丈夫です。
- 2章 — 強化学習の仕組みをざっくりと。
- 3章 — 今回の題材(JEPX裁定取引をする蓄電池)。
- 4〜7章 — 強化学習システムの構成要素(環境・状態・行動・報酬)。
- 8〜9章 — ニューラルネットワークの学習方法と、結果を何と比べるか。
- 10章 — 結果。
- 11章 — まとめと今後の展開。
2. 強化学習をひとことで
「人工知能」は大きく分けると次のように整理されます。正解ラベルのあるデータに当てはめる教師あり学習(価格予測、画像分類など)、ラベルのないデータから構造を見つける教師なし学習(消費パターンによる顧客のクラスタリングなど)、そして強化学習です。強化学習だけは問題の形がまったく違います。こちらには環境があり、取りうる手があり、その手がどれくらい良かったかを表す点数がある、という構図です。
強化学習の構成要素は、チェスのようなゲームで考えるのが一番わかりやすいです。
| 用語 | チェスでは | 今回のプロジェクトでは |
|---|---|---|
| 環境 | 盤面とルール | 蓄電池、市場、1日48コマ(30分単位) |
| 状態 | すべての駒の位置 | SOC、使ったサイクル、時刻、その日の価格 |
| 行動 | 1手の合法手 | 充電・放電・停止と、その量 |
| 報酬 | 駒を取る、チェックメイト | 取引で得た利益(円) |
| 方策 | 手を選ぶもの | 学習済みのニューラルネットワーク |
| エージェント | プレイヤー | ニューラルネットワークが実際に動いているもの |
強化学習の構成の仕組みを表す図。元リンク。[1]
学習とは、シミュレーション上のチェスを何千回も指して、勝ちにつながった判断を少しずつ強めていくことです。蓄電池の場合は、1日を1エピソードとして何千回も回します。エージェントは JEPX の価格と蓄電池の制約を知った上で、どうすれば収益が最大になるかを判断し、良かった判断が学習されていきます。
この方法の利点は、数式で書き下しにくいパターンまで見つけられることです。複数市場への同時参加などがまさにそうですし、エネルギー分野の外ではロボットの歩行などが代表例です。
下の動画はもうひとつの例です。レーシングコースのゴールまで最速でたどり着く方法を、車が学習していきます。スコアの良かった車の方向へ、ニューラルネットワークの重みが少しずつ更新されていく様子が見られます。
3. 今回解く問題
まずはとても単純な設定から始めます。日本にある一般的な 4 MW / 10 MWh の系統用蓄電池で、他の収入源も併設電源もありません。話を簡単にするため、JEPX スポット市場の裁定取引のみを行います。つまり安いときに買って、高いときに売るだけです。
エージェントには理想的な条件を与えます。その日の48コマの価格をすべて知っている、完璧な予測を持っている状態です。その情報をもとに、次の2つの制約を守りながら利益が最大になる充放電スケジュールを組みます。
- SOC は常に 5%〜95% の範囲内
- 充放電は 1日あたり1サイクル(1 EFC: 等価フルサイクル) まで
価格を事前に知っているのは現実的ではありません。一日前市場が約定するのは受渡日の前日の朝です。したがってこのデモが解いているのはスケジューリングの問題であって、予測の問題ではありません。自然な発展としては、実績値の代わりに予測値を入力することになります。
左: JEPX スポット市場の平均的な1日の形。太陽光が昼間の価格を押し下げ、夕方の立ち上がりが価格を押し上げます。右: 夏の約3分の1の日で、昼間の価格は 0.01 円/kWh の下限まで落ちています。この昼と夕方の差こそが、蓄電池のビジネスそのものです。
価格が既知なので、この問題には厳密に求められる最適解があります。線形計画法で解けるからです。この設定にしたのはまさにそのためで、強化学習が最善の解にどこまで近づけるかを見たかったのです。「エージェントは1日あたり110,932円を稼ぎました」と言われても誰も良し悪しを判断できませんが、「理論上取れたはずの利益の88.1%を取れました」なら意味のある数字になります。
4. 環境(Environment)
ここでは環境を定義します。蓄電池が置かれている条件、いわばゲームのルールです。環境はエージェントが生きているシミュレータであり、行動を受け取り、物理と市場のルールを適用し、新しい状況と報酬を返します。この中には学習される要素は一切ありません。人間が書く部分であり、同時にエージェントが触れる唯一の相手でもあります。
これから何度も出てくる2つの言葉があります。
- エピソードは、最初から最後までの1回の実行。ここでは受渡1日です。
- ステップは、エピソードの中の1つの意思決定ポイント。ここでは1コマ(日本の電力市場で使われる30分の単位)なので、1エピソードは48ステップです。
つまり1エピソードは1日であり、充電するか、放電するか、何もしないかを選ぶ機会が48回ある、ということです。1日が終わるとエピソードは終了し、環境は別の日にリセットされて、エージェントはまた最初からやり直します。
| 項目 | 定義 |
|---|---|
| エピソード | 受渡1日 = 30分 × 48コマ。48コマ目で終了 |
| ステップ | 1コマ分の出力指令 |
| 蓄電池 | 4 MW / 10 MWh、充電効率 0.96、放電効率 0.95、SOC 5〜95% |
| 符号の規約 | 出力 > 0 は充電(系統から買う)、< 0 は放電(系統へ売る) |
| 制約 | SOC 5〜95%、1日 1 EFC 以内、|出力| ≤ 4 MW |
| 日の抽出 | 学習中は毎エピソードランダムに1日(i.i.d.) |
| 初期 SOC | 学習中はランダム(境界からの立て直しを覚えさせるため)、評価時は 50% 固定 |
この表のうち、見た目より重要な4行について補足します。
効率。 充電効率 0.96、放電効率 0.95 というのは、蓄電池が完璧な容れ物ではないという意味です。系統から 1 MWh 買っても蓄電池に入るのは 0.96 MWh で、しかも貯めた分のうち取り出せるのは 95% だけです。往復効率は 0.96 × 0.95 = 0.912。つまり取引する電力の約9%は途中で失われます。価格差がこの損失より大きくなければ、裁定取引は成立しません。
1日1サイクル。 1サイクルとは、容量いっぱいの充電と放電を1回ずつ行うことです。サイクルは蓄電池を劣化させるので、運用上は燃料のように予算として管理します。ここで使う指標は**等価フルサイクル(EFC)**で、動かしたエネルギーを向きに関係なく合計し、容量の2倍で割ります。10 MWh の蓄電池に 10 MWh 入れて 10 MWh 出したら、20 ÷ (2 × 10) = 1.0 EFC です。これは運用で使っている KPI とまったく同じ定義にしてあります。シミュレータと運用チームでサイクルの数え方が違っていたら、この記事の数字すべてに注釈が必要になってしまうからです。
学習中の初期 SOC はランダム。 もし毎回ちょうど 50% から始めたら、エージェントは満タンに近い蓄電池も空に近い蓄電池も見たことがないまま育ち、いざその状態になったとき何をすればいいかわかりません。開始点をランダムにすることで、範囲全体を経験させます。評価時は 50% に固定し、どの戦略も同じ条件で比べられるようにします。
日付は順番ではなくランダムに抽出。 カレンダー順に学習すると、1回分の経験がまるごと1〜2か月の連続した期間になってしまいます。全部夏、あるいは全部冬という偏りです。日をばらばらに抽出すれば、どの学習バッチも1年の公平なサンプルになります。
実装は標準的な Gymnasium の環境(Python で強化学習をやるときの定番ライブラリ)なので、Stable-Baselines3 のアルゴリズムをそのまま載せられます。
class JepxArbitrageEnv(gym.Env):
"""Observation (52,): [norm_soc, norm_efc, sin_t, cos_t, log1p(price)[48]].
Action (1,) in [-1, 1]: fraction of the *currently feasible* charge/discharge power.
"""
self.action_space = spaces.Box(-1.0, 1.0, shape=(1,), dtype=np.float32)
self.observation_space = spaces.Box(-np.inf, np.inf, shape=(4 + 48,), dtype=np.float32)
ちなみにサイクル予算の管理は、毎ステップ実行されるこの1行だけです: efc += |Δe| / (2C)。
5. 状態空間(State space)— 52個の変数
状態とは、エージェントが何かを決める前に見ることを許されたすべて、つまりある瞬間の蓄電池の状況を丸ごと記述したものです。ニューラルネットワークが見るのはこれだけなので、ここに入れなかったものはエージェントにとって存在しないのと同じですし、役に立たないものを入れればノイズとして無視することを覚えさせる羽目になります。
今回は52個の数字になりました。
| # | 変数 | 定義 | 範囲 |
|---|---|---|---|
| 0 | norm_soc |
(e − E_min) / (E_max − E_min) — 範囲内での相対位置 |
0〜1 |
| 1 | norm_efc |
daily_efc / 1.0 — その日のサイクル予算の消費率 |
0〜1 |
| 2 | sin_t |
sin(2π·t/48) |
−1〜1 |
| 3 | cos_t |
cos(2π·t/48) |
−1〜1 |
| 4–51 | log1p(price)[48] |
その日の48コマの価格(対数変換) | 実数 |
要するに、SOC・使ったサイクル・時刻・その日の価格です。
学習しやすくするために、数字の扱いにいくつか工夫をしています。
スケールをそろえる。 ある入力が 0.5 で隣の入力が 60,000 だと、大きいほうが小さいほうを飲み込んでしまい、ネットワークはうまく学習できません。そこで SOC とサイクルはそれぞれの上限に対する割合で表し、価格は log1p(1 + 価格 の対数)を通します。これでたまに出る 300 円/kWh のスパイクが、ごく普通の 12 円/kWh の午後とだいたい同じ大きさに圧縮されます。
時刻は円環である。 単純な連番だと、48コマ目 → 0コマ目(23:30 → 00:00)が巨大なジャンプに見えてしまいます。実際には次のコマなだけなのに。時計の角度の sin と cos で表すことで、この人工的な不連続をなくします。
48本の価格をどう並べるかで結果が変わる。 選択肢は2つあります。
-
fixed— ベクトルは常に[0コマ目の価格, ..., 47コマ目の価格]。エージェントは「今がどこか」をsin_t/cos_tから読み取る必要があります。 -
rolling— ベクトルは現在のコマから始まります。[今の価格, 次の価格, ...]で、末尾は埋めます。
同じ48個の数字、同じ情報量、違うのは並べ方だけです。
6. 行動空間(Action space)— たった1つの数字、そこが肝
状態がエージェントの「見るもの」なら、行動は「やること」です。このコマで変えられる唯一のもの、と言ってもいいでしょう。価格も時計も、その日の他のことはすべてエージェントに対して勝手に起きます。これだけが唯一の返事です。
なぜ1日分のスケジュールをまとめて出すのではなく、1つの数字なのか。それはエージェントが1コマごとに順番に判断していくからです。スケジュールは事前に書かれるものではなく、48回の連続した判断の結果として現れます。しかも各判断は、それまでの判断が残した状態の上で下されます。
エージェントが出力するのは、たった1つのスカラー a ∈ [−1, 1] です。
生の行動 a
|
意味 |
|---|---|
a > +不感帯 |
充電。a = +1 でその時点で可能な最大の充電
|
|a| ≤ 不感帯 |
停止(不感帯 = 0.35) |
a < −不感帯 |
放電。a = −1 でその時点で可能な最大の放電
|
ここで大事なのは「その時点で可能な」という部分です。素朴に設計すると、行動はそのコマで取引するエネルギー量そのもの(実行可能かどうかは問わない)にして、無理な場合は後から辻褄を合わせることになります。そうではなく、a は今この瞬間に物理的に許される範囲に対する割合なのです。
これが、強化学習エージェントの判断を必ず SOC とサイクルの制約の内側に収めるための仕掛けです。エージェントはその制約があることを前提に、最善の判断を学びます。他にも報酬にペナルティを足すといった方法はありますが、個人的にはこれが一番安全だと思います。
6.1 すべての制約を1つの区間にまとめる
行動を何かに変換する前に、環境は「このコマの終わりに蓄電池のエネルギーがどこにあってよいか」を計算します。
c = self.battery.capacity_mwh
# Remaining half-cycle budget, expressed as battery-side MWh of movement
b = 2.0 * c * max(0.0, self.cfg.max_efc_per_day - self.daily_efc)
e_hi = min(self.e_max, self.e + self.e_step_chg, self.e + b)
e_lo = max(self.e_min, self.e - self.e_step_dis, self.e - b)
制約は3つ、それぞれ1行です。
-
self.e_max/self.e_min— SOC の範囲(9.5 / 0.5 MWh) -
self.e ± self.e_step_*— 1コマあたりの定格出力の制限(+1.920 / −2.105 MWh) -
self.e ± b— サイクル予算の残り
3つとも同じ量に対する上限と下限なので、最も低い上限と最も高い下限を取れば、すべてが1つの実行可能区間 [e_lo, e_hi] にまとまります。今どの制約が一番きついかを気にする必要はなく、自動的にそれが効きます。この区間を系統側の出力 [p_lo, p_hi] に変換し、行動はその中から1点を選ぶだけです。
dead = self.cfg.deadband
if a > dead: return p_hi * (a - dead) / (1.0 - dead)
if a < -dead: return -p_lo * (a + dead) / (1.0 - dead)
return 0.0
ここが肝で、この写像は合法な行動しか出せません。 ネットワークが何を言おうと — 妥当な +0.3 でも、振り切った +1.0 でも、学習初回のただのノイズでも — 出てくるのは蓄電池が物理的に実行できるものです。禁止すべき違法な行動がそもそも存在しないのです。学習中も、評価中も、見たことのない日でも同じです。よくある2つの代替手段と比べてみます。
| アプローチ | 保証 |
|---|---|
| 報酬にペナルティ | 違反を避けることを学習するだけ。防ぐ仕組みはない |
| 事後的に行動をクリップ | 出力は実行可能になるが、実際には起きなかった行動で勾配が計算される |
| 実行可能集合へ写像する | 違反が起こりえず、勾配も現実と一致する |
6.2 具体例
実際の環境から取った値です。
| 状態 |
p_lo(放電) |
p_hi(充電) |
理由 |
|---|---|---|---|
| SOC 50%、サイクル消費 0 | −4.000 MW | +4.000 MW | 効いているのは定格出力だけ |
| SOC 90%、サイクル消費 0.85 | −4.000 MW | +1.042 MW | 充電が SOC 上限で頭打ち |
| SOC 50%、サイクル消費 0.98 | −0.760 MW | +0.833 MW | サイクル予算がほぼ尽きている |
| 任意の SOC、サイクル消費 1.00 | 0 MW | 0 MW | 予算切れ。停止しか選べない |
重要なのは最後の行です。予算を使い切ると実行可能区間は1点に潰れ、ネットワークが何を出力しようとエージェントは物理的に取引できなくなります。
ある評価日の様子。緑の帯は、その各コマで行動マッピングが出力しうるすべて(a が −1 から +1 まで)で、オレンジの線がエージェントが実際に選んだ値です。日の前半は帯が定格出力いっぱいに広がっていますが、40コマ目を過ぎるとその日のサイクル予算が尽きて帯が閉じていきます。オレンジの線が帯の内側にあるのは補正されたからではなく、構造上そうならざるを得ないからです。
7. 報酬(Reward)— 蓄電池を「倉庫」として扱う
いちばん単純な報酬は現金収支でしょう。充電したら払い、放電したら受け取る。しかしこの設計はすぐ破綻します。充電が常にマイナスの報酬になるので、エージェントは充電しなくなり、充電しないエージェントは放電もしないからです。
解決策は、蓄電池をレジではなく倉庫として扱うことです。在庫を仕入れるのは損ではありません。現金が在庫に変わるだけで、その在庫には価値があります。そこで報酬を2つの部分に分けます。現金の精算と、タンクの中身の評価額の変化です。
r_t \;=\; \underbrace{-\,1000\,P_t\,\Delta t\,p_t}_{\text{cash settlement / 現金精算}}
\;+\; \underbrace{1000\,\eta_d\,\bar{p}\,\Delta e_t}_{\text{inventory mark-to-market / 在庫の時価評価}}
\Delta e_t \;=\;
\begin{cases}
P_t\,\Delta t\,\eta_c, & P_t > 0 \quad \text{(charging / 充電)}\\[4pt]
P_t\,\Delta t\,/\,\eta_d, & P_t \le 0 \quad \text{(discharging / 放電)}
\end{cases}
| 記号 | 意味 |
|---|---|
P_t |
このコマの出力 [MW]。正が充電 |
Δt |
1コマの長さ = 0.5 h |
p_t |
このコマの JEPX 価格 [円/kWh] |
p̄ |
その日48コマの平均価格 |
Δe_t |
損失を差し引いて実際に出入りしたエネルギー [MWh] |
η_c, η_d
|
充電効率 / 放電効率(0.96 / 0.95) |
第1項はただのお金のやり取りです。系統から取った分は払い、系統へ渡した分は受け取ります。第2項が倉庫の会計で、タンクに入れたエネルギーはその日の平均価格で資産として計上し、取り出した分は同じ価格で帳簿から落とします。
2つを代入して整理すると、声に出して読める形になります。
-
放電で得られるのは
1000 · Δt · |P| · (p_t − p̄)→ その日の平均より高く売れば報酬。 -
充電で得られるのは
1000 · Δt · |P| · (η_c η_d · p̄ − p_t)→ 往復効率 0.912 を差し引いたうえで、平均より安く買えば報酬。
往復の取引を1つ追ってみると具体的になります。その日の平均が 12 円/kWh、価格が 5 のときに系統から 1 MWh 買い、あとで 25 のときに売ったとします。
| 現金 | 在庫 | 報酬 | |
|---|---|---|---|
| 5 円/kWh で 1 MWh 買う | −5,000 | +10,944 | +5,944 |
| 残った 0.912 MWh を 25 円/kWh で売る | +22,800 | −10,944 | +11,856 |
| 合計 | +17,800 | 0 | +17,800 |
在庫の2項は大きさが同じで符号が逆なので、打ち消し合います。エージェントが受け取る報酬の合計は 17,800 円で、これは往復取引の現金利益とぴったり一致します。倉庫の会計はお金を生み出してはいません。やっているのは知らせを前倒しすることです。何時間も先の売却を待たなくても、買った瞬間に「今のは良い判断だった」と伝えられます。
ここから言えることが2つあります。
- 在庫はその日の平均価格で評価され、いくらで買ったかは見ていません。 これは意図的です。各日は前日の残りの SOC から始まるので、平均で評価しておけば、その「タダの電気」を平均価格で売り払っても報酬はゼロになります。平均を上回ってはじめて報酬になるわけです。
- 報酬を時間的にばらしても、最善の戦略は変わりません。 これは期待ではなく定理です。在庫の項は1日を通して必ず打ち消し合うので、どんなスケジュールでも報酬の合計は純粋な現金利益と等しくなります。会計を悪用して得をすることはできません。この手法はポテンシャルベースの報酬整形と呼ばれます。
8. 学習 — エージェントはどうやって賢くなるのか
最初、エージェントは何も知りません。ネットワークの中身はただの乱数なので、最初の充放電はまったくのノイズです。その日の最高値で買い、下限で売り、午前中のうちにサイクル予算を使い切ります。学習とは、そのノイズを判断力に変えていく作業で、動く・採点する・調整するという3つを数百回繰り返すだけのループです。
8.1 2つのニューラルネットワーク
ここで言うニューラルネットワークは、要するに関数です。数字が入って数字が出てくる。その間にある何千もの重みが、学習で変わっていく部分です。PPO はこれを2つ使います。どちらも5章の52個の状態の数字を読み、どちらも小さなネットワークです。64ユニットの隠れ層が2層、重みは合計で15,000個ほど。参考までに、大規模言語モデルは数千億個です。
| ネットワーク | 入力 | 出力 | 答える問い |
|---|---|---|---|
| 方策(Policy) | 52個の状態の数字 | 1つの数字 μ | 「今、何をすべきか?」 |
| 価値(Value) | 同じ52個の数字 | 1つの数字 | 「ここから1日の終わりまでに、いくら稼げそうか?」 |
本当に欲しいのは方策のほうです。学習が終わったとき、方策そのものが戦略になります。価値ネットワークは学習を助ける道具で、最後には捨てられます。
なぜ2つ目が必要なのか。 エージェントが 11:00 に充電して、その日が 50,000 円で終わったとします。11:00 の充電は良い判断だったのでしょうか。50,000 円という数字だけでは判断できません。たまたま価格が高い日で、どんな戦略でも同じくらい稼げたかもしれないからです。知りたいのは、その判断がその状況から期待できる水準より良かったかどうかです。この期待値を供給するのが価値ネットワークの役目で、方策ネットワークはそれを上回る行動を学んでいきます。
8.2 1コマの流れ
学習中の30分1コマで何が起きているかを追ってみます。数字は説明用ですが、順番はコードがやっていることそのものです。
- 環境が状態を渡す。 SOC 50%、サイクル消費ゼロ、22コマ目(11:00 — 太陽光が価格を押し下げている時間帯)、そしてその日の48本の価格。合計52個の数字です。
- 方策が提案する。 その52個を読んで、1つの数字を出します。たとえば μ = +0.42。ざっくり「今できる充電の40%くらいで充電」という意味です。
- 行動はそのまま実行されず、サンプリングされる。 学習中、エージェントは提案どおりには動きません。μ を中心とした釣鐘型の分布(幅は σ ≈ 0.37)からランダムに行動を引きます。今回は +0.55 かもしれませんし、同じ状態でも次は +0.18 かもしれません。この意図的なランダムさが探索です。自分がすでに信じていることしかやらないエージェントは、「あと2コマ待ったほうが良かった」ことに永遠に気づけません。σ 自体も学習されるパラメータなので、自信がつくにつれてエージェントは少しずつ迷わなくなります。
- 行動が出力に変わる。 +0.55 は6章の行動マッピングを通り、その瞬間に物理的に可能な範囲に対する割合へ変換されます。つまり実際の充電指令になり、ネットワークが何を言おうと SOC の範囲もサイクル予算も破れません。
- 環境が精算する。 SOC が上がり、サイクルのカウンタが進み、このコマの報酬が価格と追加されたエネルギーの価値から計算されます(7章)。
- 価値ネットワークが見解を書き残す。 今見た状態について「ここからなら1日の終わりまでに12,000円くらい」といった具合です。
ここまでの状態・行動・報酬・予測がすべて記録され、時計は次のコマへ進みます。これが48回でシミュレーション上の1日です。
1コマの流れ。方策が提案し、偶然が「提案からどれだけずれるか」を決め、行動マッピングが「出てきたものは必ず蓄電池が実行できる」ことを保証します。これが48回繰り返されて1日分になります。
8.3 32日分の経験から1回の更新へ
エージェントが学習するのは、シミュレーション32日分の結果が溜まってからです。そこで採点が始まります。価値ネットワークが期待していた金額と、方策ネットワークの行動が実際に稼いだ金額の比較です。たとえばこんな具合です。
| 価値ネットワークの予測 | 実際の結果 | 判定 |
|---|---|---|
| ここから12,000円 | 15,000円 | +3,000 — 期待より良い |
| ここから12,000円 | 9,000円 | −3,000 — 期待より悪い |
期待より良い結果になった行動は、方策ネットワークの重みの調整に使われ、次に似た状況になったときに選ばれやすくなります。
価値ネットワークも同時に賢くなります。 その予測は実際に起きたことと比較され、ずれが修正に使われます。これによって価値ネットワークは将来の収益をより正確に見積もれるようになり、その期待はどんどん厳しくなっていきます。方策ネットワークは、その上がり続けるハードルに追いつくために行動を改善せざるを得ません。この方策と価値のやり取りこそが、学習を効率的かつ効果的にしている部分です。
同じ32日分のデータは10回ほど繰り返し使われたあと、捨てられます。PPO は今の戦略が生み出した経験からしか学べないからです。同じデータセットを何度でも再利用できる教師あり学習と比べて、強化学習に膨大な試行が必要なのはこれが理由です。
同じ学習を、乱数の初期値だけ変えて3回繰り返したもの。エージェントは最初は損を出し、70,000ステップあたりで単純ルールを追い抜き、最善の結果のすぐ下で横ばいになります。横ばいこそが収束した姿です。
32日分を何回やるのか。 50万ステップはおよそ325回分の「動く・採点する・調整する」にあたり、シミュレーション上の10,400日、つまり28年分の運用経験に相当します。これが数分の計算に収まります。
8.4 データの分割
データは目的別に分ける必要があります。学習中に一度も見ていない日で結果を検証するためです。そうしないと、答えを見ながら試験を受けるようなものになってしまいます。分割はランダムではなくカレンダー順に行います。隣り合う日は価格が非常に似ているため、ランダムに分けると検証用の日が実質的に学習へ漏れてしまうからです。
| 分割 | 日数 | 期間 | 用途 |
|---|---|---|---|
| 学習(train) | 548 | 2024-04-01 → 2025-09-30 | 学習 |
| 検証(validation) | 182 | 2025-10-01 → 2026-03-31 | ハイパーパラメータの選定 |
| テスト(test) | 142 | 2026-04-01 → 2026-08-20 | 結果の報告のみ |
9. 何と比較するのか
エージェントは2つの別の制御戦略と比べます。ひとつは最善の解(今回の裁定取引では線形計画法)、もうひとつは、強化学習なら当然超えてほしい簡単なベンチマークです。
どちらも評価専用です。状態にも、報酬にも、学習のどこにも登場しません。エージェントはこの2つを一度も見ていません。
① 完全予見最適 — 天井。 線形計画法は古典的なソルバーで、目的関数と制約をすべて線形の式で書けるとき、良さそうな解ではなく証明付きの最善解を返します。価格が既知の蓄電池裁定取引はまさにその種の問題なので、scipy.optimize.linprog で各日の最も利益の大きいスケジュールを約2ミリ秒で計算できます。どんな戦略もこれを超えられません。 だからこれは目標であると同時に検査にもなります。もしエージェントがある日にこれを上回ったように見えたら、「素晴らしいエージェントだ」ではなく「バグがある」が正しい結論です。
② top-N ルール — 床。 その日の安い N コマで定格いっぱいに買い、高い N コマで定格いっぱいに売り、あとは何もしない。制約は同じものを守り、numpy で4行、学習は一切ありません。会議で懐疑的な同僚が出してきそうな案であり、「そもそもエージェントは必要なの?」という至極もっともな問いでもあります。この記事では N を最も強い値にして載せています。ベンチマークを強く見せるぶんには比較がエージェントに厳しくなるだけなので、間違える方向として安全だからです。裁定取引で誰もが最初に思いつく程度のルールなので、強化学習がこれに勝てないなら、やるべきことがまだ相当あるということになります。
もうひとつ、裏側で静かに走っているチェックがあります。最適解のスケジュールを環境自身の step() に通し直し、そこで報告される収益がソルバーの値と1円単位で一致するか確かめています。シミュレータとソルバーが蓄電池について食い違っていたら、これで気づけます。
10. 結果
実際の JEPX スポット価格(九州エリア)、3シード、50万ステップ、テスト142日分です。
| 戦略 | 円/日 | 最適比 | SOC 最小–最大 | 最大 EFC | 停止コマ数 | 違反 |
|---|---|---|---|---|---|---|
| 完全予見最適 | 124,176 | 100.0 | 5.00–95.00 | 1.000 | 36.1 | 0 |
| PPO(調整後: rolling 価格・広い不感帯) | 110,932 | 88.1(87.7–88.4) | 5.00–95.00 | 1.000 | 25.0 | 0 |
| top-6 ルール | 102,470 | 82.4 | 5.00–95.00 | 1.000 | 39.4 | 0 |
| PPO(初期版: fixed 価格・狭い不感帯) | 91,934 | 71.6(70.5–72.6) | 5.00–95.00 | 1.000 | 16.3 | 0 |
表の読み方です。
- 3シードとは、学習全体を乱数の初期値を変えて3回やり直したという意味です。かっこ内はその3回のばらつきを示します。たまたま良かった1回では何も証明できません。
- 停止コマ数は、48コマのうち何も動かさなかったコマの数です。利益とは関係なく、スケジュールの形を表します。最適解は1日の4分の3を何もせずに過ごしますが、エージェントはその約2倍の頻度で取引しています。
- 最大 EFC は、各戦略が最も重く使った日の値です。きっちり 1.000 ということは、どの戦略も少なくとも1日はサイクル予算を使い切り、そしてどれ一つとして超えなかったということです。
典型的な評価日での、エージェントと最善スケジュールの比較。上: 価格。2段目: それぞれが何をしたか(定格に対する割合)。3段目: その結果の SOC。どちらも 95% の上限ぎりぎりまで使い、どちらも制約を一度も超えていません。下: そこまでに稼いだ金額。最適解(青)は少数の大きなかたまりで動き、エージェント(オレンジ)は細かい動きを数多く重ねて、ほぼ同じ合計に到達しています。
制約
| チェック項目 | 結果 |
|---|---|
| SOC 下限 / 上限の違反 | 0 / 0 |
| EFC > 1.0 の日 | 0 |
| 観測された SOC の範囲 | 5.000 – 95.000 % |
| 観測された最大 EFC | 1.000 |
| シミュレータのサイクル計算 vs 運用のレインフロー計数 | 一致(〜1e-16) |
| エージェントが最適解を上回った日(バグの兆候) | 0 |
ペナルティゼロ、事後的な補正ゼロ、違反ゼロ。しかも設計書で主張したのではなく、全日で実測しています。
価格の並べ方だけで17ポイント
| price_view | 円/日 | 最適比(最悪〜最良シード) |
|---|---|---|
fixed |
91,129 | 71.4(67.4–75.1) |
rolling |
110,932 | 88.1(87.7–88.4) |
| top-6 ルール | 102,470 | 82.4 |
アルゴリズムも報酬も制約も学習量も、そして48個の数字そのものも同じで、変えたのは並び順だけです。fixed では PPO は4行のルールに負けます。rolling では勝ちます。シードのばらつきを見ると差は明白で、fixed で最も運の良かった実行ですら、rolling で最も運の悪かった実行に遠く及びません。
fixed は安定性も大きく劣ります。3回の実行が 67.4% から 75.1% まで散らばるのに対し、rolling は1パーセントポイントの中に収まります。自分が今どこにいるかを入力から読み取れないエージェントは、初期値の運次第になってしまうのです。
理由は位置の不変性にあります。rolling なら「3コマ先の価格」は常にインデックス3にあるので、同じ重みが1日中同じ意味を持ちます。fixed では、時計が取りうる48通りの値それぞれについて、ベクトルの読み方を学び直さなければなりません。
11. まとめと今後
強化学習エージェントが、到達可能な最大値にかなり近い制御解を出せることを確認しました。もちろん現実の、より不確実性の高い場面では理想的な最大値そのものを計算することはできません。だからこそ、十分な確度で最適な戦略に近づいていける手段として強化学習は有効だと言えます。
もうひとつ確認できたのは、物理的な制約を行動そのものに埋め込むやり方です。これによってすべての解が常に SOC とサイクルの制約の内側に収まり、それでいてエージェントは良い戦略を学べました。一般的には制約を報酬関数のペナルティとして書きますが、その場合エージェントが危険なミスを犯す余地が残ります。今回の方法は、そもそもそれが起こりえない構造になっています。
このデモはあえて、最適解がすでに分かっていて計算もできる問題を選びました。それがエージェントを正直に測る唯一の方法だったからです。しかし、強化学習が本当に力を発揮するのはそこではありません。
今後取り組みたいのは、数式で書き下しにくいほうの問題です。一次調整力への入札価格の決定や、1台の蓄電池の1日のサイクル予算を JEPX と EPRX に同時に配分すること。商品どうしが影響し合い、ルールも変わり続ける領域です。
[1] Musaddiq, A., Olsson, T., & Ahlgren, F. (2023). Reinforcement-Learning-Based Routing and Resource Management for Internet of Things Environments: Theoretical Perspective and Challenges. Sensors, 23(19), 8263. https://doi.org/10.3390/s23198263




