■ はじめに

強化学習用Pythonコードをまとめるにあたり、どうしても避けて通れないのがCNNです。

これがわかると、画像からデータが取れるので、ゲーム解くからカメラ画像の分析までかなりのことができる様になります。

しかし、ここには深い沼があります。

僕もまだ勉強中なので、全てとはいかないですが、分かっている限りお伝えします

いつもの様に、以下のコードはColabで動かす事を前提にしています。



■先ずは基本から…

CNNは画像をに入力するやつで、「観測画像の形状(H, W, C)」を持っています。

この観測の shape を調べる最小コード

import gymnasium as gym

env = gym.make("MiniGrid-Empty-5x5-v0") # 例
obs, info = env.reset()

print(obs.shape)

出力例:(15, 15, 3)

これの意味は、高さ, 幅, チャンネル)になります。

出力例の場合:
15 × 15 :視野サイズ
3 :オブジェクトI

これは、(height, width, channel)に相当し、
人間にわかりやすい並びです。

でも、CNN(Stable-Baselines3 中身は PyTorch)は、次の順番を期待してます。

(channel, height, width)

ここで VecTransposeImage の出番
(H, W, C) → (C, H, W) に変換します

■でも、現実は甘くない

これで済めば、

「VecTransposeImageさん、優秀!」

で済むのですが、そんな甘くはないのが世の常です。

VecTransposeImageは、全能ではなく、
ちゃんと前提条件があります。

【前提条件】
・観測が 画像(Image space)
・dtype が uint8
・値域が [0, 255]
・形状が (H, W, C)

であることを前提に設計されています。

なので例えば、僕のSLAM型コードの場合
VecTransposeImageを使わず、自作ラッパーを書いてます。

理由はコチラ

① 最初はVecTransposeImageを使った
最初は公式の推奨通り、

env = VecTransposeImage(env)

を使っていた。

しかし、SLAM型では次のような前処理を行っていた。

・観測画像を float32 に変換
・CNN学習を安定させるため 正規化
・マップ表現を追加した多チャネル画像

この時点で、観測はもはや「uint8画像」ではなくなっていた。

② エラーは出ないが、挙動が怪しい

・学習が不安定
・報酬が伸びない
・デバッグしづらい

原因を調べると、
VecTransposeImageは「dtypeや値域」を暗黙に想定しており、
こちらの前処理と噛み合っていなかった。

③ 結論:自分で transpose した方が安全
そこで、
・dtype
・正規化
・チャネル構成
を完全に把握できるよう、シンプルなカスタムラッパーを作ることにした。

【結論】何をCNNに渡しているかを、100%把握するには自作ラッパーが必要

■自作ラッパーに潜む罠

と、自作ラッパーを作ることになったのですか…

①ラッパーを作る

「CNNには CHW が必要らしい」

Wrapper内で obs = obs.transpose(...) 

ここで、サンプルコードを見て「VecTransposeImageも入れた方が良さそう」と思ってしまいました。

←余計なことの結果:2回 transpose

②エラーが出なかった

運がいいのか悪いのか、エラーがでないで、何が起きるのかと言うと、

・本来やりたい変換
 (H, W, C) → (C, H, W)

・2回やるとどうなる?
 (H, W, C) → (C, H, W) → (W, C, H)  
 ← もはや画像ではない

または、環境によっては:
(n_envs, C, H, W) → (n_envs, H, W, C)

最初の状態に戻ったり、別の並びになる

ここで、一番怖いポイントは
「エラーが出ない」
「学習も普通に始まる」

けど

「報酬が伸びない」
「エージェントが賢くならない」

何かおかしい事はわかりましたが、ここでこう考えて沼りました:

「報酬設計が悪いのかな?」
 違う
「SLAMのアイデアが悪いのかな?」
 違う

こんな感じで、この沼から出るのにだいぶんかかりました。

③実際“画像の並びが壊れているだけ”

本来CNNが見るべき画像は(C, H, W)

しかし2回変換した結果、
チャンネル順が崩れた疑似画像になる

CNNは「画像として意味のない配列」を
必死に学習していただけだった。

④ 学んだこと

画像前処理は「どこで・何回やっているか」を必ず1か所に集約するべき

正しい流れ
(H, W, C)
   ↓ VecTransposeImage
(C, H, W)

やってしまった流れ
(H, W, C)
   ↓ 自作 transpose
(C, H, W)
   ↓ VecTransposeImage
(W, C, H) ← 壊れる

📝 CNN前処理チェックリスト

[ ] transposeは1か所だけか?
[ ] VecTransposeImageと自作処理が被っていないか?
[ ] 観測shapeを print(obs.shape) で確認したか?
[ ] dtypeと値域を把握しているか?

■自作ラッパー完成までの道のり

① 自作ラッパーにしたら、設定も一緒に変わっていない?

初心者が無意識に変えがちなポイント
🔹 dtype が変わる

obs = obs.astype(np.float32)
VecTransposeImage:uint8 前提
自作ラッパー:float32 が自然

👉 CNNの初期挙動・スケールが変わる

🔹 値域が変わる(超重要)

obs = obs / 255.0
[0, 255] → [0.0, 1.0]

👉 同じモデル・同じ報酬でも学習曲線が変わる

🔹 observation_space を更新していない

self.observation_space = spaces.Box(...)
これを忘れると:
実際の obs と
gym が「そうだと思っている shape」がズレる。

👉 静かに壊れる代表例

🔹 transpose の場所が変わった

env wrapper でやった
VecEnv 後でやった
model に渡る直前でやった

👉 2回変換の温床

ここで一文(かなり効く)
自作ラッパーにしたことで、
「transpose以外の条件」も一緒に変わっている可能性がある。

② 学習が変わったのは、transposeのせい?
それとも「一緒に変えた設定」のせい?

VecTransposeImageをやめて自作ラッパーにしたとき、
「うまく学習した/しなくなった」と感じても、
本当に違いは transpose だけか?
dtype や値域も変えていないか?
observation_space は合っているか?
を切り分けないと、原因が分からない。

③ 最小・安全な自作Transposeラッパー

ポイント
transposeしかしない
dtype・値域は触らない
observation_space を必ず更新


🧩 最小構成コード(そのまま掲載OK)


import numpy as np
import gym
from gym import spaces

class SimpleTransposeWrapper(gym.ObservationWrapper):
    """
    (H, W, C) -> (C, H, W)
    余計なことを一切しない安全なTransposeラッパー
    """

    def __init__(self, env):
        super().__init__(env)

        obs_space = env.observation_space
        assert isinstance(obs_space, spaces.Box)

        h, w, c = obs_space.shape

        self.observation_space = spaces.Box(
            low=obs_space.low.min(),
            high=obs_space.high.max(),
            shape=(c, h, w),
            dtype=obs_space.dtype,
        )

    def observation(self, obs):
        return np.transpose(obs, (2, 0, 1))

④ VecTransposeImage と同時に使わないための注意書き

注意書き(初心者向けに強調)
⚠ このラッパーを使う場合、
VecTransposeImageは使わないこと

理由:
2回 transpose される
エラーが出ないことがある
デバッグが困難

⑤ デバッグ用ワンポイント

・形状確認コード
obs = env.reset()
print("obs shape:", obs.shape, obs.dtype)

・VecEnv 使用時:
print("obs shape:", obs.shape)

# (n_envs, C, H, W) になっているか?

■ 結果:

今回は、強化学習でよく出るCNNへの画像渡しを勉強しました。

VecTransposeImageでだいたい良いはずですが、僕のコードの様に相性が悪い場合もありますので、自作ラッパーも選択肢の1つです。

その時、このページが参考になるといいな。

■ 今回の学び:

VecTransposeImageは非常に便利で

「uint8画像をそのままCNNに渡す場合」

に最も力を発揮する。

僕のSLAM型のように観測を加工する場合、自分ラッパーでtransposeを書く方が、学習の挙動を理解しやすくなります。

■ まとめ

別企画の「SLAM使った配送問題」で出てかなり沼ったCNNについて、今回は勉強しました。

結局、自作ラッパーを作る事になりました。

しかし、自作ラッパーは自由度が高い反面、transpose以外の条件も一緒に変わりやすい。

最初は「何もしないラッパー」を作り、
1つずつ変更点を増やしていく方が安全ですね。