********
※LINE対応チャットボット版の
「 LINEチャットボット屋」
いろんなチャットボットがあります。
ぜひ、ご覧ください!
********
進化的アルゴリズムを実装する理由
下記の記事が面白かったので、google colabで簡易版を実装してみました。
https://sakana.ai/shinka-evolve/
英語版ですが、翻訳、もしくは生成AIに投げれば解説してくれます。
さて、最近AI技術の中でも注目されているのが「進化的アルゴリズム」です。
上記のSakana AIさんのShinkaEvolveが面白く、これを再現したいなあと。
git hubにコードが公開されているようですが、出力がコードなので、学生さんにどうかなと。
それで、アスキーアートで出力させたら面白いのではと。
今回作ったコードは、生物の進化のように、少しずつ改良を重ねながらより良い形を見つけていくものです。
この考え方をAIと組み合わせ、「ASCII・アスキーアートを進化させる」という少しユニークな実験を行ってみました。
テーマは「猫のアスキーアートを進化させる」
最初はシンプルな記号だけのアートから始まり、世代を重ねるごとにどのように姿を変えていくのかを観察しました。
この記事では、実際の出力や進化の過程を交えながら、その結果を紹介します。
初期アートからのスタート
まず、進化の出発点に置いたのは、次のような非常にシンプルな3行のアートです。
***
* *
***
この段階では、もちろん猫らしさは一切ありません。
しかし、進化アルゴリズムはこのような小さな種(シード)からスタートし、世代ごとに少しずつ改善を重ねながら最終的な形に近づいていくとのこと。
今回の設定は以下の通りです。
試行数:150回
世代上限:30世代
母集団サイズ:5
評価関数では、アートの「密度がちょうど良い(0.25前後)」「左右対称である」「縦に芯がある」などの基準をスコア化し、より美しいアートを残すようにしました。
世代を重ねて進化していく
実際にプログラムを走らせると、初期スコアは 108.73 でした。
そして、最初に登場した猫らしい形がこちらです。

この段階で、すでに「猫らしさ」を感じられるレベルになっています。
そこからさらに世代を重ねるごとに、形は少しずつ複雑で洗練されたものになっていきました。
第2世代(GEN 2)では、耳や顔のディテールが増え、全体のバランスも整ってきました。

第4世代では、より左右対称性が高まり、密度も評価関数の理想値に近づいてきました。
最終的にはスコア 114.82 を記録し、アートとしての完成度がぐっと上がりました。

早期停止の理由と「最終世代」の完成形
設定上は30世代まで進化を続けられるようにしていましたが、今回は 9世代目で早期停止となりました。
これは、「一定期間スコアの改善が見られなければ自動的に終了する」という仕組みが働いたためです。
このような「早期停止」は、無駄な試行を避け、効率的に最終結果へ到達するための仕組みです。
完成した最終世代のアート
そして完成した最終世代のアートがこちらです。

耳のあたりが若干ずれていますが、
最初の「***」からは想像もつかないほど猫らしい形になっています。
線の密度や縦軸の安定感も評価基準に沿っており、美しくまとまったASCII・アスキーアートとしていいんじゃないでしょうか。
AIがアートを育てていく実感
今回の実験を通して感じたのは、「AIがアートを生成する」というよりも、「AIがアートを育てていく」という感覚でした。
人間が行うのは「テーマ」と「条件」の設定だけです。
あとはAIが自動的に候補を出し、評価関数によって優れたものを残しながら、少しずつ形を洗練させていきます。
世代ごとの出力を見ていると、「耳の形が整った」「目がはっきりした」など、少しずつの進化がはっきりと感じ取れます。
これはまるで、AIと一緒に作品を育てているような体験です。
ただし、課題もいくつかあります。
・評価関数の設計によって、結果が大きく変化する
・改善が停滞すると、似たようなアートが繰り返し生成される
・LLMを使うため、APIコストや応答速度の考慮も必要
それでも、短時間でここまでの進化を見せるのは非常に興味深く、AIが「試行錯誤」を通して美を探す過程を目の当たりにできました。
まとめ
今回のgoogle colabの実験を通して、次のような学びが得られました。
・シンプルな初期アートでも、進化を重ねれば見事なアートっぽくなる。
・評価関数の工夫次第で、最終的な形や特徴が大きく変わる。
・早期停止により、効率的にベストな結果を得られる。
・進化の過程を観察すること自体が楽しく、教育的価値もある。
・商用化を考える場合は、APIコストや最適化の設計も重要。
人の手をほとんど介さず、AIが進化というプロセスを経て芸術を生み出す姿は爽快でした。
次は評価基準を変えて「犬」や「ドラゴン」など別のテーマに挑戦したり、チャットボットとして公開し、他の人にも体験してもらえるような形にする予定です。
使用したPythonコード
使用したPythonコードです。
google colabで実装できます。
※openAIのAPI(gpt4o-mini)を使っているので、コードを走らせると若干料金がかかります。
とはいえ、1回走らせて数円でしょう。
(一応、150回の試行で10円位との試算ですが、各自で試算してください)
# -*- coding: utf-8 -*-
"""
Google Colab 用:ASCIIアート進化アルゴリズム(LLM+評価関数)
-----------------------------------------------------------------
■ できること
- テキストボックスから APIキー を入力(Base URL / モデルは固定)
- OpenAI gpt-4o-mini を使って親コードから改良案を生成
- 評価関数でスコア化、良い個体を残して世代交代
- 新規性フィルタ(似た案の量産を回避)
- 試行回数・世代数・母集団サイズなどをパラメータで調整
- LLM を使わない“ローカル変異デモモード”でも動作可
"""
import math, random, json, time, textwrap
import requests
from dataclasses import dataclass
from typing import List, Dict, Any
try:
from IPython.display import display, HTML, clear_output
import ipywidgets as widgets
except Exception:
widgets = None
# =============================
# 設定
# =============================
class Config:
MAX_TRIALS = 150
MAX_GENERATIONS = 30
POP_SIZE = 5
SIMILARITY_THRESHOLD = 0.92
EARLY_STOP_PATIENCE = 5
PATCH_TYPE_PROBS = {"diff": 0.6, "full": 0.25, "cross": 0.15}
TARGET_DENSITY = 0.25
# =============================
# 評価関数
# =============================
def evaluate_art(art: str, target_density: float = Config.TARGET_DENSITY) -> float:
lines = [ln for ln in art.split("\n") if ln.strip()]
if not lines:
return 0.0
area = sum(len(ln) for ln in lines)
stars = sum(ch in "*#@%☆★^/\\_|" for ln in lines for ch in ln)
density = stars / max(1, area)
score = 100.0 * (1.0 - abs(density - target_density))
sym = sum(1 for ln in lines if ln == ln[::-1])
score += sym * 5.0
deco = sum(art.count(s) for s in ["☆", "★", "^", "/", "\\", "_"])
score += min(deco, 10) * 1.5
mid = max(len(ln) for ln in lines) // 2
vertical = 0
for ln in lines:
if mid < len(ln) and ln[mid] in "*|^#@%": vertical += 1 score += vertical * 2.0 return max(score, 0.0) # ============================= # 類似度チェック # ============================= def similarity(a: str, b: str) -> float:
a_lines = [ln.strip() for ln in a.split("\n") if ln.strip()]
b_lines = [ln.strip() for ln in b.split("\n") if ln.strip()]
if not a_lines or not b_lines:
return 0.0
m = sum(1 for i in range(min(len(a_lines), len(b_lines))) if a_lines[i] == b_lines[i])
return m / max(len(a_lines), len(b_lines))
def is_novel(candidate: str, archive: List[str], th: float = Config.SIMILARITY_THRESHOLD) -> bool:
return all(similarity(candidate, prev) < th for prev in archive) # ============================= # ローカル変異 # ============================= def mild_mutation(art: str) -> str:
s = art.replace("* *", "*☆*").replace("---", "^-^")
if random.random() < 0.3: lines = [ln for ln in s.split("\n") if ln] if lines: width = max(len(ln) for ln in lines) box_top = "╔" + "═" * width + "╗" box_bot = "╚" + "═" * width + "╝" new_lines = [ln.ljust(width) for ln in lines] s = "\n".join([box_top] + ["║" + ln + "║" for ln in new_lines] + [box_bot]) return s def fresh_from_scratch() -> str:
return " /\\_/\\\n ( o.o )\n > ^ <\n" def crossover(a: str, b: str) -> str:
a_lines = a.split("\n")
b_lines = b.split("\n")
cut_a = len(a_lines) // 2
cut_b = len(b_lines) // 2
return "\n".join(a_lines[:cut_a] + b_lines[cut_b:])
# =============================
# LLM 設定
# =============================
@dataclass
class LLMConfig:
api_key: str = ""
use_llm: bool = True
base_url: str = "https://api.openai.com"
model: str = "gpt-4o-mini"
timeout: float = 60.0
LLM_SYS_PROMPT = "あなたはASCIIアートを改良するアシスタントです。出力はASCIIアートのみで、コードブロックや説明は不要です。"
def call_llm(cfg: LLMConfig, prompt: str) -> str:
headers = {
"Authorization": f"Bearer {cfg.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": cfg.model,
"messages": [
{"role": "system", "content": LLM_SYS_PROMPT},
{"role": "user", "content": prompt},
],
"temperature": 0.9,
"max_tokens": 500,
}
url = cfg.base_url.rstrip("/") + "/v1/chat/completions"
try:
resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=cfg.timeout)
resp.raise_for_status()
data = resp.json()
return data["choices"][0]["message"]["content"].strip("`\n ")
except Exception as e:
return f"LLM_ERROR: {e}\n" + mild_mutation(prompt)
# =============================
# パッチ生成
# =============================
def generate_patch(parents: List[str], llm_cfg: LLMConfig, theme: str) -> str:
kind = random.choices(list(Config.PATCH_TYPE_PROBS.keys()), weights=list(Config.PATCH_TYPE_PROBS.values()), k=1)[0]
if kind == "diff":
base = random.choice(parents)
user_prompt = f"お題: {theme}\n親アートを少しだけ美しく改良してください。左右対称・密度0.25付近・特殊記号は少量:\n---\n{base}\n---\n出力はASCIIアートのみ。"
return call_llm(llm_cfg, user_prompt)
elif kind == "full":
user_prompt = f"お題: {theme}\n0から新しいASCIIアートを1つ作ってください。3〜10行、各行幅は揃える。左右対称、密度0.25付近。出力はASCIIアートのみ。"
return call_llm(llm_cfg, user_prompt)
else:
if len(parents) >= 2:
a, b = random.sample(parents, 2)
user_prompt = f"お題: {theme}\n親Aと親Bの良い部分を組み合わせ、新しいASCIIアートを生成:\n[A]\n{a}\n[B]\n{b}\n条件: 左右対称、3〜10行、各行幅揃える。出力はASCIIアートのみ。"
return call_llm(llm_cfg, user_prompt)
else:
return mild_mutation(parents[0])
@dataclass
class Individual:
art: str
score: float
def select_parents(pop: List[Individual], k: int = 2) -> List[Individual]:
ranked = sorted(pop, key=lambda x: x.score, reverse=True)
weights = [1.0 / (i + 1) for i in range(len(ranked))]
return random.choices(ranked, weights=weights, k=k)
def run_evolution(initial_art: str, theme: str, llm_cfg: LLMConfig):
archive: List[str] = [initial_art]
population: List[Individual] = [Individual(initial_art, evaluate_art(initial_art))]
while len(population) < Config.POP_SIZE:
seed = fresh_from_scratch()
population.append(Individual(seed, evaluate_art(seed)))
archive.append(seed)
best = max(population, key=lambda x: x.score)
trials_used = len(population)
no_improve = 0
print("[START] 初期ベストスコア:", round(best.score, 2))
print(best.art, "\n---\n")
gen = 0
while gen < Config.MAX_GENERATIONS and trials_used < Config.MAX_TRIALS: gen += 1 parents = select_parents(population, 2) parent_arts = [p.art for p in parents] children: List[Individual] = [] for _ in range(Config.POP_SIZE): cand = generate_patch(parent_arts, llm_cfg, theme) if not cand.strip(): continue if not is_novel(cand, archive): continue sc = evaluate_art(cand) children.append(Individual(cand, sc)) archive.append(cand) trials_used += 1 if trials_used >= Config.MAX_TRIALS:
break
if not children:
cand = mild_mutation(best.art)
sc = evaluate_art(cand)
children.append(Individual(cand, sc))
archive.append(cand)
trials_used += 1
merged = sorted(population + children, key=lambda x: x.score, reverse=True)
population = merged[: Config.POP_SIZE]
if population[0].score > best.score:
best = population[0]
no_improve = 0
else:
no_improve += 1
print(f"[GEN {gen}] trials={trials_used} best={round(best.score,2)}")
print(best.art, "\n---\n")
if no_improve >= Config.EARLY_STOP_PATIENCE:
print("[EARLY STOP] 改善停滞")
break
print("[DONE] 総試行:", trials_used, "世代:", gen)
print("[BEST SCORE]", round(best.score, 2))
print(best.art)
return best
# =============================
# Colab UI
# =============================
if widgets is not None:
api_key_w = widgets.Password(value='', description='API Key:', layout=widgets.Layout(width='70%'))
theme_w = widgets.Text(value='猫(左右対称、美しさ重視)', description='お題:', layout=widgets.Layout(width='70%'))
trials_w = widgets.IntSlider(value=150, min=10, max=500, step=10, description='試行数')
gens_w = widgets.IntSlider(value=30, min=5, max=200, step=5, description='世代上限')
pop_w = widgets.IntSlider(value=5, min=2, max=20, step=1, description='母集団')
init_art_w = widgets.Textarea(value="***\n* *\n***", description='初期アート', layout=widgets.Layout(width='70%', height='120px'))
out = widgets.Output()
def on_run_clicked(_):
with out:
clear_output()
Config.MAX_TRIALS = int(trials_w.value)
Config.MAX_GENERATIONS = int(gens_w.value)
Config.POP_SIZE = int(pop_w.value)
llm_cfg = LLMConfig(api_key=api_key_w.value.strip())
print("[THEME]", theme_w.value)
print("\n[INITIAL]\n" + init_art_w.value + "\n---\n")
best = run_evolution(init_art_w.value, theme_w.value, llm_cfg)
print("\n[RESULT] ベストスコア:", round(best.score,2))
print(best.art)
run_btn = widgets.Button(description='進化を実行', button_style='success')
run_btn.on_click(on_run_clicked)
ui = widgets.VBox([api_key_w, theme_w, widgets.HBox([trials_w, gens_w, pop_w]), init_art_w, run_btn, out])
display(ui)
else:
print("Colab 環境で実行してください。")
いろんなアスキーアート作ってください。
****************
最近のデジタルアート作品を掲載!
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
****************



