1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Arduino UNO Qのライフゲームで長寿命パターンをAI(遺伝的アルゴリズム)で探索する

1
Posted at

はじめに

Conway's Game of Life(ライフゲーム)は、シンプルな3つのルールから驚くほど複雑な挙動を生み出すセルオートマトンである。初期パターン次第で、数ステップで消滅するものもあれば、何百ステップも複雑な変化を続けるものもある。

Arduino UNO Qには8×13のLEDマトリクスが搭載されており、ライフゲームをリアルタイムで可視化できる。しかし、「どの初期パターンが長く生き残り、かつ複雑な挙動をするか」を手探りで探すのは非効率である。

本記事では、遺伝的アルゴリズム(GA)とサロゲートモデルを組み合わせ、長寿命かつ多様性の高い初期パターンを自動探索する仕組みを実装する。

ライフゲームの基本実装(セルオートマトンのルール・LED表示)については前回の記事を参照とする。本記事ではAI探索部分に絞って解説する。

※推論やモデル生成のアルゴリズム部分に関しては、Claude Code による実装を実施した。

動作環境

項目 内容
ハードウェア Arduino UNO Q
開発・実行環境 Arduino App Lab
LEDマトリクス 8×13
Python 3.13
主要ライブラリ onnxruntime、numpy

App LabはArduino UNO Q向けのアプリケーション実行環境である。PythonスクリプトとArduinoスケッチをまとめたzipをデプロイするだけで動作する。

ライフゲームの実装

基本的な実装は前回の記事を参照とする。

トーラス型(周期境界条件) の採用

8×13のグリッドを トーラス(ドーナツ面) として扱う。左端と右端、上端と下端がつながっているイメージである。端のセルも常に8近傍を持つため、境界での特別扱いが不要となる。

ROWS = 8
COLS = 13
TOROIDAL = True

def _next_gen(g):
    new = [[0] * COLS for _ in range(ROWS)]
    for r in range(ROWS):
        for c in range(COLS):
            n = 0
            for dr in (-1, 0, 1):
                for dc in (-1, 0, 1):
                    if (dr, dc) == (0, 0):
                        continue
                    if TOROIDAL:
                        # トーラス境界:端を反対側に折り返す
                        n += g[(r + dr) % ROWS][(c + dc) % COLS]
            if g[r][c]:
                new[r][c] = 1 if n in (2, 3) else 0
            else:
                new[r][c] = 1 if n == 3 else 0
    return new

トーラス型にすることで小さなグリッドでも複雑なパターンが長く持続しやすくなる。固定境界(端は常に死セル)では端に近いセルが消えやすく、短命なパターンが増える傾向がある。

フィットネス関数の設計

GAでは各個体(初期パターン)の「良さ」をスカラー値で評価するフィットネス関数が核心となる。今回は以下の2指標の重み付き和を用いる。

fitness = 0.6 × (longevity / 1000) + 0.4 × diversity

指標① 生存世代数(longevity)

初期パターンから何ステップでサイクルに突入するかを測る。静止(still-life)や消滅もサイクルとして検出される。

  • 最大1000ステップシミュレーション
  • サイクル検出時はそのステップ数をlongevityとする
  • 1000ステップ以内にサイクルなし → longevity = 1000(最大値)

指標② 多様性スコア(diversity)

サイクルに突入したときの周期の長さを正規化した値である。

パターン種別 サイクル周期 diversity
still-life(静止) 1 0.01
blinker(点滅) 2 0.02
複雑な振動子 大きい 大きい
1000ステップ超 1.0
MAX_CYCLE = 100  # 周期の正規化上限

def evaluate(pattern, max_gens=1000):
    grid = _make_grid(pattern)
    seen = {}  # state -> 最初に見たステップ番号
    longevity = max_gens
    cycle_period = max_gens

    for step in range(max_gens):
        state = tuple(tuple(row) for row in grid)

        if state in seen:
            longevity = step
            cycle_period = step - seen[state]  # 実際のサイクル周期
            break

        seen[state] = step
        next_grid = _next_gen(grid)

        if not any(cell for row in next_grid for cell in row):
            longevity = step + 1
            cycle_period = 0   # 全滅:diversity = 0
            break

        grid = next_grid

    diversity = min(cycle_period, MAX_CYCLE) / MAX_CYCLE
    fitness = 0.6 * (longevity / max_gens) + 0.4 * diversity
    return fitness, longevity, diversity

実装のポイント

2指標を独立させた背景

当初、多様性を「サイクル突入までに出現したユニーク状態数」で定義していた。しかしこれだとユニーク状態数 ≒ 生存世代数になってしまう。サイクル検出時点で過去のすべての状態がseenに記録されているため、数学的にlen(seen) == longevityが成立するためである。

サイクル周期を使うことで独立した指標となる。例えば:

  • 「500ステップ生き残ったが、最後はblinker(周期2)」→ longevity高・diversity低
  • 「200ステップで複雑な振動子(周期80)に落ち着いた」→ longevity中・diversity高

この2つは性質が異なるパターンであり、独立した指標でそれぞれを評価できるようになる。

遺伝的アルゴリズムの実装

個体の表現

初期パターンは7×7 = 49セルのバイナリ配列である。これを8×13グリッドの中央に配置してシミュレーションする。

PATTERN_SIZE = 7  # 7×7

def random_pattern(n):
    return [random.randint(0, 1) for _ in range(n * n)]

設定パラメータ

POP_SIZE      = 20    # 個体数
ELITE_N       = 2     # エリート保存数(上位2個体を次世代に引き継ぐ)
MUTATION_RATE = 0.05  # 突然変異率(各ビットが5%の確率で反転)

選択・交叉・突然変異

def next_generation(population, fitnesses, mutation_rate, elite_n):
    # エリート保存:上位2個体をそのまま次世代へ
    sorted_idx = sorted(range(len(population)), key=lambda i: -fitnesses[i])
    new_pop = [population[i][:] for i in sorted_idx[:elite_n]]

    # ルーレット選択 → 一点交叉 → 突然変異
    while len(new_pop) < len(population):
        p1 = _select(population, fitnesses)  # fitness比例選択
        p2 = _select(population, fitnesses)
        point = random.randint(1, len(p1) - 1)
        child = p1[:point] + p2[point:]      # 一点交叉
        child = [b ^ 1 if random.random() < mutation_rate else b
                 for b in child]             # ビット反転突然変異
        new_pop.append(child)

    return new_pop

サロゲートモデルによる高速スクリーニング

evaluate()は最大1000世代シミュレーションするため重い処理である。20個体すべてを毎世代CPU評価すると1世代に数十秒かかる。そこで**サロゲートモデル(代理モデル)**で事前スクリーニングを行う。

仕組み

  1. 全20個体をサロゲートMLPで高速予測
  2. 予測値上位TOP K = 5個体だけをevaluate()で正確評価
  3. 残り15個体にはサロゲート予測値をfitnessとして使用(GAの選択圧は維持)
SURROGATE_TOP_K = 5

inputs    = [surrogate.pattern_to_input(p) for p in population]
predicted = surrogate.predict(inputs)

# サロゲート上位K個体のみCPU評価
top_k = sorted(range(POP_SIZE), key=lambda i: -predicted[i])[:SURROGATE_TOP_K]
for i in top_k:
    fit, lon, div = evaluate(population[i])

# 残りはサロゲート値で代用
for i in range(POP_SIZE):
    if i not in top_k:
        fitnesses[i] = predicted[i]

CPU評価回数を20→5に削減でき、1世代あたりの処理時間が大幅に短縮される。

モデル構造

入力は7×7パターンを8×8にゼロパディングして平坦化した64次元ベクトルである。

入力 (64) → 全結合 (32) → ReLU → 全結合 (16) → ReLU → 全結合 (1) → Sigmoid

バックエンドの自動選択

利用可能なバックエンドを優先順に自動選択する。

バックエンド 条件
ONNX-CPU onnxruntimeインストール済み(推奨)
TFLite-CPU tflite-runtimeインストール済み
NumPy-CPU 常に使用可能(外部MLライブラリ不要)
trials = [
    ("ONNX-CPU",   self._try_onnx_cpu),
    ("TFLite-CPU", self._try_tflite_cpu),
    ("NumPy-CPU",  self._try_numpy),
]
for name, fn in trials:
    try:
        fn()
        self.backend = name
        break
    except Exception as e:
        logger.warning(f"[{name}] 使用不可: {e}")

実装のポイント

NumPyフォールバック

model_numpy.jsonは重み行列をJSON化したものであり、onnxruntimetflite-runtimeがない環境でも動作する。NumPy(Python標準環境に同梱)だけでMLPの順伝播を実行する。

def _predict_numpy(self, patterns):
    x = np.array(patterns, dtype=np.float32)
    for i, (W, b) in enumerate(zip(self._np_W, self._np_b)):
        x = x @ W + b
        if i < len(self._np_W) - 1:
            x = np.maximum(0, x)  # 中間層:ReLU
    return np.clip(x.flatten(), 0, 1).tolist()

実行結果

動作中のLEDマトリクスの様子を以下に示す。

また、ログの出力例を以下に示す。

04:07:51 INFO life_game_ai: [CPU eval] 個体16: fitness=0.095 longevity=159 diversity=0.000
04:07:56 INFO life_game_ai: [CPU eval] 個体04: fitness=0.412 longevity=412 diversity=0.280
04:08:01 INFO life_game_ai: Gen 5: best_fitness=0.412 longevity=412 diversity=0.280 elapsed=5.1s

上記ではlongevityとdiversityが独立した値を示している。これより「長生きだが単純振動」と「中程度の寿命で複雑な振動子」がそれぞれ評価されていることが確認できる。

まとめ

Arduino UNO QのLEDマトリクス上で、GAとサロゲートモデルを組み合わせた長寿命・高多様性パターンの自動探索を実現した。
今回はテスト的な実装であるが、

  • フィットネスを longevity(生存世代数)diversity(サイクル周期) という独立した2指標で設計した
  • サロゲートモデルによりCPU評価を全体の1/4に削減した
  • バックエンド自動選択によりONNX-CPUからNumPyまで環境を問わず動作する

等により、Arduino環境とLinux環境を並列で使用したAI推論を実施することができた。
これはArduino UNO QがMPU×MCUから構成されるために、1ボードにて表現できたものである。

展望

次の記事では、この探索プロセスをブラウザからリアルタイムで監視する Web GUI(Flask + Canvas) の作成を検討してる。推論中のTOP5パターンの可視化等できる仕組みも実装したい。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?