OpenAIの最新モデル ChatGPT-5 Thinking


以前の投稿で, ChatGPT-5 についてまとめた. 同じタイミングでもう一つのモデルである, ChatGPT-5 Thinkingというモデルも発表された.

そこで今回もWeasel Programを出力させてみた.

実験結果としては, 非常に複雑なアニメーションを描画できるようになったが, このアニメーションが必要かというと, 疑問が残るようなアニメーションである. もちろん, プロンプトがシンプルすぎる問題もあるかもしれないが, 現段階では性能を持て余しているような印象を受ける.

プロンプトは下記.

Weasel Program を可能な限りリッチな表現を用いたコードで書いて.
その過程をアニメーションとして出力できるようにして.
# --- Weasel Program: Rich + Animated Version (for Google Colab) ---
# Author: (You + ChatGPT)
# Features:
# - Modular design with dataclasses & type hints
# - Elite selection, clonal reproduction with per-char mutation
# - Full history tracking (best/avg fitness, diversity matrix)
# - Matplotlib animation: text overlay, population heatmap, fitness curves
# - Save as HTML (inline) or MP4 (requires ffmpeg)
# - Clean, commented, and parameterised

from __future__ import annotations
import random
import string
from dataclasses import dataclass
from typing import List, Tuple, Dict, Optional

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML, display

# ----------------------------
# 1) Config & Utilities
# ----------------------------

@dataclass
class WeaselConfig:
    target: str = "METHINKS IT IS LIKE A WEASEL"
    alphabet: str = string.ascii_uppercase + " "
    pop_size: int = 100
    mutation_rate: float = 0.05
    elite_fraction: float = 0.2
    max_generations: int = 2000
    seed: Optional[int] = 42

    # Animation params
    fig_width: float = 12
    fig_height: float = 7
    fps: int = 15
    interval_ms: int = 120  # per frame
    show_every: int = 1      # record every k generations

    # Output
    save_html: bool = True
    html_filename: str = "weasel_animation.html"
    save_mp4: bool = False   # MP4 requires ffmpeg in runtime
    mp4_filename: str = "weasel_animation.mp4"


def set_seed(seed: Optional[int]):
    if seed is not None:
        random.seed(seed)
        np.random.seed(seed)


def clamp01(x: float) -> float:
    return max(0.0, min(1.0, x))


# ----------------------------
# 2) Core Weasel Engine
# ----------------------------

class WeaselEngine:
    def __init__(self, cfg: WeaselConfig):
        self.cfg = cfg
        set_seed(cfg.seed)
        # Normalise/validate params
        self.cfg.mutation_rate = clamp01(self.cfg.mutation_rate)
        self.cfg.elite_fraction = clamp01(self.cfg.elite_fraction)

        # Prepare
        self.target = cfg.target
        self.L = len(self.target)
        self.alphabet = cfg.alphabet
        assert all(ch in self.alphabet for ch in self.target), \
            "Target contains characters not in alphabet."

        self.population = [self._random_candidate() for _ in range(cfg.pop_size)]
        self.generation = 0

        # History trackers
        self.best_history: List[str] = []
        self.best_fit_history: List[int] = []
        self.avg_fit_history: List[float] = []
        self.population_match_mats: List[np.ndarray] = []  # (pop, L) boolean

    # --- Genetic primitives ---
    def _random_candidate(self) -> str:
        return "".join(random.choice(self.alphabet) for _ in range(self.L))

    def fitness(self, candidate: str) -> int:
        return sum(c == t for c, t in zip(candidate, self.target))

    def mutate(self, candidate: str) -> str:
        # independently mutate each character with prob mutation_rate
        arr = list(candidate)
        for i in range(self.L):
            if random.random() < self.cfg.mutation_rate:
                # ensure a different char than current
                choices = [ch for ch in self.alphabet if ch != arr[i]]
                arr[i] = random.choice(choices)
        return "".join(arr)

    def select_elite(self, scored: List[Tuple[str, int]]) -> List[str]:
        # sort by fitness desc, keep top K
        k = max(1, int(self.cfg.pop_size * self.cfg.elite_fraction))
        scored.sort(key=lambda x: x[1], reverse=True)
        elite = [c for c, _ in scored[:k]]
        return elite

    def reproduce(self, elite: List[str]) -> List[str]:
        # clone-with-mutation until population size restored
        next_pop = []
        while len(next_pop) < self.cfg.pop_size:
            parent = random.choice(elite)
            child = self.mutate(parent)
            next_pop.append(child)
        return next_pop[:self.cfg.pop_size]

    # --- One evolutionary step ---
    def step(self):
        # Evaluate
        fitnesses = [self.fitness(c) for c in self.population]
        best_idx = int(np.argmax(fitnesses))
        best = self.population[best_idx]
        best_fit = fitnesses[best_idx]
        avg_fit = float(np.mean(fitnesses))

        # Record history
        self.best_history.append(best)
        self.best_fit_history.append(best_fit)
        self.avg_fit_history.append(avg_fit)

        # Store population-match matrix (pop x L) for heatmap
        match_mat = np.array([[c[i] == self.target[i] for i in range(self.L)]
                              for c in self.population], dtype=bool)
        self.population_match_mats.append(match_mat)

        # If finished -> keep final state
        if best == self.target:
            self.generation += 1
            return True

        # Selection & reproduction
        elite = self.select_elite(list(zip(self.population, fitnesses)))
        self.population = self.reproduce(elite)

        self.generation += 1
        return False

    def run(self) -> int:
        # Run until hit target or reach max gens
        for _ in range(self.cfg.max_generations):
            done = self.step()
            if done:
                break
        return self.generation


# ----------------------------
# 3) Animation Renderer
# ----------------------------

class WeaselAnimator:
    def __init__(self, engine: WeaselEngine):
        self.eng = engine
        self.cfg = engine.cfg

    def _collect_frames(self):
        """
        Run the engine but capture states every `show_every` generations
        for animation efficiency.
        """
        # Reset (engine already initialised)
        frames = []
        # capture initial state
        frames.append(self._snapshot())

        while self.eng.generation < self.cfg.max_generations:
            done = self.eng.step()
            if self.eng.generation % self.cfg.show_every == 0 or done:
                frames.append(self._snapshot())
            if done:
                break
        return frames

    def _snapshot(self) -> Dict:
        g = self.eng.generation
        data = {
            "gen": g,
            "best": self.eng.best_history[-1] if self.eng.best_history else self.eng.population[0],
            "best_fit": self.eng.best_fit_history[-1] if self.eng.best_fit_history else 0,
            "avg_fit": self.eng.avg_fit_history[-1] if self.eng.avg_fit_history else 0.0,
            "match_mat": self.eng.population_match_mats[-1] if self.eng.population_match_mats else
                         np.zeros((self.cfg.pop_size, self.eng.L), dtype=bool)
        }
        return data

    def animate(self):
        frames = self._collect_frames()
        T = self.eng.target
        L = len(T)

        # Figure layout: 3 rows
        fig = plt.figure(figsize=(self.cfg.fig_width, self.cfg.fig_height))
        gs = fig.add_gridspec(3, 1, height_ratios=[1.2, 1.2, 1.1], hspace=0.4)

        # --- Top: Target + Best with coloured glyphs ---
        ax_text = fig.add_subplot(gs[0, 0])
        ax_text.axis("off")
        title = ax_text.text(0.5, 1.05, "", ha="center", va="bottom", fontsize=18, transform=ax_text.transAxes)
        target_line = ax_text.text(0.5, 0.72, f"TARGET: {T}", ha="center", va="center", fontsize=20, family="monospace")
        best_line_objs = []
        # prepare positions for monospace effect
        x0, y0 = 0.5, 0.35
        dx = 0.018 * max(1, 28 / L)  # adjust spacing vs length
        # place empty chars for best
        for i in range(L):
            t = ax_text.text(x0 + (i - L/2) * dx, y0, " ", ha="center", va="center",
                             fontsize=22, family="monospace")
            best_line_objs.append(t)
        subtitle = ax_text.text(0.5, 0.05, "", ha="center", va="bottom", fontsize=12, transform=ax_text.transAxes)

        # --- Middle: Heatmap (pop x L) of matches ---
        ax_heat = fig.add_subplot(gs[1, 0])
        hm = ax_heat.imshow(np.zeros((self.cfg.pop_size, L)),
                            aspect="auto", interpolation="nearest", vmin=0, vmax=1)
        ax_heat.set_title("Population Match Heatmap (rows: individuals, cols: positions)")
        ax_heat.set_xlabel("Position")
        ax_heat.set_ylabel("Individuals")
        cbar = fig.colorbar(hm, ax=ax_heat, fraction=0.025, pad=0.02)
        cbar.set_label("Match (1) / Mismatch (0)")

        # --- Bottom: Fitness curves ---
        ax_fit = fig.add_subplot(gs[2, 0])
        best_line, = ax_fit.plot([], [], label="Best Fitness")
        avg_line, = ax_fit.plot([], [], label="Average Fitness")
        ax_fit.set_xlim(0, max(10, len(frames)-1))
        ax_fit.set_ylim(0, L + 1)
        ax_fit.set_xlabel("Generation")
        ax_fit.set_ylabel("Fitness (matches)")
        ax_fit.legend(loc="lower right")

        # Precompute sequences for lines
        gens = [f["gen"] for f in frames]
        bests = [f["best_fit"] for f in frames]
        avgs = [f["avg_fit"] for f in frames]

        # Update function
        def update(frame_idx):
            F = frames[frame_idx]
            gen, best, best_fit, avg_fit, match_mat = F["gen"], F["best"], F["best_fit"], F["avg_fit"], F["match_mat"]

            # Title / subtitles
            title.set_text(f"Weasel Program — Generation {gen}")
            subtitle.set_text(f"Best fitness: {best_fit}/{L}  |  Avg fitness: {avg_fit:.2f}  |  Mutation: {self.cfg.mutation_rate:.2f}  |  Pop: {self.cfg.pop_size}")

            # Update best line (character colouring by correctness)
            for i, ch in enumerate(best):
                color = "#2ca02c" if ch == T[i] else "#777777"
                best_line_objs[i].set_text(ch)
                best_line_objs[i].set_color(color)

            # Heatmap update
            hm.set_data(match_mat.astype(int))

            # Fitness curves (progressively reveal)
            best_line.set_data(gens[:frame_idx+1], bests[:frame_idx+1])
            avg_line.set_data(gens[:frame_idx+1], avgs[:frame_idx+1])

            ax_fit.set_xlim(0, max(10, gens[min(frame_idx+1, len(gens)-1)]))
            return (*best_line_objs, hm, best_line, avg_line, title, subtitle, target_line)

        anim = FuncAnimation(fig, update, frames=len(frames),
                             interval=self.cfg.interval_ms, blit=False)

        # Display inline HTML (robust in Colab)
        if self.cfg.save_html:
            html = anim.to_jshtml(default_mode='reflect')
            with open(self.cfg.html_filename, "w", encoding="utf-8") as f:
                f.write(html)
            display(HTML(html))
            print(f"[Saved HTML] {self.cfg.html_filename}")

        if self.cfg.save_mp4:
            try:
                anim.save(self.cfg.mp4_filename, writer="ffmpeg", fps=self.cfg.fps, dpi=160)
                print(f"[Saved MP4] {self.cfg.mp4_filename}")
            except Exception as e:
                print("[MP4 save failed] Is ffmpeg available? Error:", e)

        plt.show()
        return anim


# ----------------------------
# 4) Run: tweak parameters here
# ----------------------------

cfg = WeaselConfig(
    target="METHINKS IT IS LIKE A WEASEL",  # 変えたい場合はここ
    pop_size=120,
    mutation_rate=0.055,
    elite_fraction=0.25,
    max_generations=2000,
    seed=7,
    fig_width=13,
    fig_height=8,
    fps=15,
    interval_ms=120,
    show_every=1,
    save_html=True,              # HTMLで保存・表示(Colab向け)
    html_filename="weasel_animation.html",
    save_mp4=True,              # TrueにしてffmpegがあればMP4保存
    mp4_filename="weasel_animation.mp4",
)

engine = WeaselEngine(cfg)
animator = WeaselAnimator(engine)
anim = animator.animate()

# --- Tips ---
# - 文字種を増やしたいなら:cfg.alphabet = string.ascii_uppercase + string.ascii_lowercase + " !?."
# - 難易度を上げるなら:長い target、低い mutation_rate、低い elite_fraction
# - 収束が遅いなら:mutation_rate を 0.03–0.07 で微調整、pop_size を 100–300 に

参考文献

  • Dawkins, R. (1996). The blind watchmaker: Why the evidence of evolution reveals a universe without design. WW Norton & Company.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です