面白かったらシェア、いいねよろしくお願いします!
国旗を破くと犯罪になるぞ
2026年7月、日本国旗の損壊などを罰する日本国旗損壊罪法が成立しました(7/17成立、7/24公布、8/13 施行)。だとすれば、いちばん安全な旗は、破いても自分で元に戻る旗でしょう。というわけで、破壊すると自己修復するデジタル日章旗を、ブラウザ上で動くように作りました。
クリックで破ける/放置で自己修復するライブデモはこちらです。
何を作ったか
やったことはだいたい 4 つです。Python と PyTorch で Growing Neural Cellular Automata(以下 NCA)を学習し、目標は縦横比 2:3 の日章旗、自己修復する regenerating レジームで訓練しました。推論側は、学習済みの重みだけを書き出して、更新則を Rust から WebAssembly に移植してブラウザで走らせています。重み(約 33KB)は Cloudflare R2 に置き、ブラウザから fetch して毎フレーム canvas に描いています。
元ネタは Mordvintsev らの Growing Neural Cellular Automata(Distill, 2020)です。
Neural Cellular Automata とは
各ピクセルを 1 個の細胞とみなし、全細胞がまったく同じ小さなニューラルネットを共有します。学習パラメータは約 8,300 個しかありません。細胞が見られるのは自分と近傍 3×3 の状態だけ。それでも 1 個のシード細胞から画像全体が立ち上がり、一部を削っても再生します。中央集権的な設計図はどこにもなく、修復のルールは局所的な相互作用の中に分散して埋め込まれている、という点が面白いところです。
状態は [16, H, W] のグリッドです。先頭 4 チャンネルが可視の RGBA(premultiplied)、残り 12 が細胞の隠れ状態になります。1 ステップの更新則は、固定の depthwise 3×3(恒等・Sobel-X・Sobel-Y、これは学習しない)で近傍を知覚して 48 チャンネルにし、1x1 conv 48→128 と ReLU、1x1 conv 128→16(バイアス無し・ゼロ初期化)で差分を出し、各細胞を fire_rate=0.5 の確率でのみ更新し、最後に 3×3 近傍のアルファ最大が 0.1 を超える細胞だけを残す、というものです。境界は循環(トーラス)にしてあります。
知覚が固定で、学習されるのは 1×1 畳み込み 2 層だけ。つまり更新則はピクセルごとの小さな行列積に過ぎず、重い ML ランタイムは要りません。これがブラウザ移植を簡単にしてくれます。
学習レジーム
論文の Experiment 1-3 に対応する 3 種類があり、違いは「どの状態から CA を回し始めるか」だけです。
| regime | バッチの供給元 | 結果 |
|---|---|---|
| growing | 毎回シード | 目標形状には到達するが、放置すると崩壊する |
| persistent | サンプルプール | 到達後も形状を保つ |
| regenerating | プール + 円形の損傷 | 切り取られても再生する |
外から破壊を注入する用途では regenerating が必須です。学習中にわざと形を壊して戻す経験を積ませて初めて、再生能力が身につきます。growing や persistent で訓練したモデルは、一度壊すと元に戻りません。
目標画像はアルファが教師になる
NCA の教師データは目標画像 1 枚だけです。しかもアルファチャンネルがそのまま「細胞が存在すべき領域」の教師になります。そのため不透明な RGB 画像は弾き、透過付き RGBA だけを受け付けるようにしました。
日章旗は正式には縦横比 2:3、日章の直径は縦の 3/5、中央配置です。正方形グリッドに 2:3 の旗をレターボックス配置し、旗の外側は透明にしました。こうすると NCA の正方形グリッドを保ったまま、旗そのものは正しい 2:3 になります。
from PIL import Image, ImageDraw
CANVAS = 360
flag_h = 200
flag_w = flag_h * 3 // 2 # 2:3
disc_d = flag_h * 3 // 5 # 日章 = 縦の 3/5
img = Image.new("RGBA", (CANVAS, CANVAS), (0, 0, 0, 0)) # 全面透明
draw = ImageDraw.Draw(img)
fx0, fy0 = (CANVAS - flag_w) // 2, (CANVAS - flag_h) // 2
draw.rectangle((fx0, fy0, fx0 + flag_w - 1, fy0 + flag_h - 1), fill=(255, 255, 255, 255))
r = disc_d / 2
draw.ellipse((CANVAS/2 - r, CANVAS/2 - r, CANVAS/2 + r, CANVAS/2 + r), fill=(188, 0, 45, 255))
img.save("hinomaru.png")
学習
学習は uv 管理の Python パッケージで回しました。GPU は RTX 5060、CUDA 12.8 です。
uv run nca train --target assets/hinomaru.png --checkpoint artifacts/hinomaru.pt
# regenerating, 8000 steps, 約16分, 最終 loss ~0.0001
損失は可視 4 チャンネルの二乗誤差だけで、隠れチャンネルには一切の教師を与えません。何を表現に使うかは CA に委ねられます。
ブラウザで動かす
重みをフラットバイナリに書き出す
.pt は PyTorch 専用の形式なので、学習される 3 テンソル(hidden.weight, hidden.bias, output.weight)だけを、自己記述ヘッダ付きの little-endian f32 として直列化します。知覚カーネルは固定値なので書き出しません。読み手側が同じ定義を持つ前提にして、二重管理を避けています。
offset type 内容
0 4s マジック "NCAW"
4 u32 フォーマット版
8 u32 channels (16)
12 u32 hidden (128)
16 f32 fire_rate
20 f32[H*C3] hidden.weight (行優先 [hidden, channels*3])
... f32[H] hidden.bias
... f32[C*H] output.weight (行優先 [channels, hidden])
合計で約 8,320 float、およそ 33KB です。マジックと次元をヘッダに入れておくと、読み手が壊れた重みを黙って受け入れずに済みます。
更新則を Rust から wasm に移植する
PyTorch の 1 ステップを 1:1 で Rust に移植します。wasm 線形メモリを JS から読みます。
// 固定 Sobel(/8)。知覚は学習しないので Rust 側に定義を持つ。
let sobel_x = ((tr - tl) + 2.0 * (mr - ml) + (br - bl)) / 8.0;
let sobel_y = ((bl - tl) + 2.0 * (bc - tc) + (br - tr)) / 8.0;
// hidden = relu(W_h · perception + b_h) → delta = W_o · hidden
// 発火抽選は 1 細胞 1 回、noise <= fire_rate のときだけ更新。
// 生存マスクは、更新前(was_alive) と 更新後(is_alive) の両方で生きている細胞だけ残す。
wasm-pack build --target web でビルドし、ワークスペースパッケージとして website から import("nca-wasm") します。
描画
wasm から straight-alpha の RGBA を受け取り、putImageData で描くだけです。CA は成長・再生の余地として周囲の余白を必要とするので、計算はグリッド全体で回しつつ、表示だけ中央をクロップして旗を大きく見せています。canvas のクリックは、その点を円形に破壊するようにしました。学習時に与えている損傷と同じ操作です。
おわりに
固定の知覚と 1×1 畳み込み 2 層、約 8,300 パラメータの極小モデルが、破壊から自律的に形を取り戻す。設計図なしの分散的な修復という点が、生命っぽくて気に入っています。学習から重みエクスポート、Rust/wasm 推論まで一通り書いたので、興味があれば動かしてみてください。
