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.