小規模言語モデル SLMの「Gemma3 270M」をgoogle Colabで動かしてみた

Gemma 270Mはどんな感じなのか

最近、Googleが公開しているGemmaシリーズ。その中でも最小サイズのGemma 3 270MをGoogle Colabで動かしてみました。
「とりあえずどんな感じで動くのか」「小さいモデルってどんな挙動になるのか」という実験です。

結論から言うと
動く。ちゃんと答えは返ってくる。けど、内容はかなりズレる。
それが逆に面白い、という結果になりました。

Gemma3 270Mとは

Gemma3 270Mとは、Googleが公開した大規模言語モデルで最小サイズのモデルです。

パラメータ数はわずか2億7千万(270M)。
近年主流の数十億〜数百億規模のLLMと比べると圧倒的に小型です。

そのため動作は軽く、Google Colabの無料環境やメモリの限られたPCでも動かせる点が大きな特徴となっています。

ただし、モデルサイズが小さい分、知識量や推論力はかなり限定的で、事実ベースの質問に対しては誤答やトンチンカンな返答をすることも多いです。
一方で、言語の形式はそれなりに整っており、短文での応答や独特の言い回しは可能です。

この特性を活かし、知識ベースの利用ではなく「遊び用キャラ」や「特定の口調・スタイルに特化したボット」としてファインチューンする用途に向いているモデルといえます。

google Colabでの実行方法

Colabでは以下の流れで実行できます。

1. Hugging Faceのアカウントを作り、Gemmaのモデルページで利用規約に同意
2. Colab上でtransformershuggingface_hubをインストール
3. Hugging Faceトークンを入力してモデルをダウンロード
4. テキストボックスUIを作って、質問文を入力して対話

実際に作ったUIは、ブラウザ上でチャット風に会話できるスタイルにしました。

「モデル準備」ボタンでGemma3 270Mをロードし、その下に質問用テキストエリアを配置。
送信すると返答が吹き出しで表示されるという、簡易チャットアプリのような仕組みです。

この時点で「Gemma3 270MをColabに落として、ユーザーの入力に答えさせる」という部分は問題なく動作しました。

⚫︎pythonコードを貼っておきます。
google colabで一発で動く版。

有料版のT4でも質問からの返答まで結構時間かかります。
無料版のcpuだとだいぶかかるかと。

使う時はHugging faceのアクセストークンが要ります。
またモデルページで同意しないとダウンロードできないやつです。
(わからない人は、google検索 or AIに聞いてください)


# ✅ Google Colab ワンセル版:Gemma 3 270MをローカルDLして連続チャット(安全/事実モード付き)
# - このセルをそのまま実行 → 画面のUIでHFトークン入力→「モデル準備」→下の欄で継続対話
# - モデルは /content/models/ に保存(再起動まで保持)
# - 既定モデル:google/gemma-3-270m-it(指示追従)
# - 「安全モード」= FP32 & 貪欲生成(CUDA assert回避)、「事実質問モード」= do_sample=False

!pip -q install "transformers>=4.43.0" "accelerate>=0.33.0" huggingface_hub ipywidgets > /dev/null

import os, shutil, time, html, torch, textwrap
import ipywidgets as widgets
from IPython.display import display, Markdown, clear_output
from huggingface_hub import login, snapshot_download
from transformers import AutoTokenizer, AutoModelForCausalLM

# =========================
# UI: モデル準備(上段)
# =========================
hf_token_box = widgets.Password(
description='HF Token',
placeholder='hf_xxx...(Hugging Face アクセストークン)',
layout=widgets.Layout(width='96%')
)
model_box = widgets.Dropdown(
options=[
('google/gemma-3-270m-it(指示追従・推奨)', 'google/gemma-3-270m-it'),
('google/gemma-3-270m(素のPT)', 'google/gemma-3-270m'),
],
value='google/gemma-3-270m-it',
description='Model',
layout=widgets.Layout(width='96%')
)
system_box = widgets.Textarea(
description='システム',
value="日本語で、簡潔かつ正確に回答してください。事実質問では推測せず、わからない場合はわからないと答えてください。",
placeholder='(任意)アシスタントのキャラや方針。',
layout=widgets.Layout(width='96%', height='70px')
)
max_new_tokens_box = widgets.IntSlider(
description='max_new',
value=256, min=32, max=1024, step=32, continuous_update=False
)
temperature_box = widgets.FloatSlider(
description='temp',
value=0.3, min=0.0, max=1.5, step=0.1, readout_format='.1f', continuous_update=False
)
safe_mode_chk = widgets.Checkbox(
description='安全モード(FP32/貪欲)※落ちる時はON',
value=True
)
factual_mode_chk = widgets.Checkbox(
description='事実質問モード(do_sample=False)',
value=True
)
fresh_download_chk = widgets.Checkbox(
description='強制再ダウンロード(既存を削除)',
value=False
)
prepare_btn = widgets.Button(description='モデル準備', button_style='primary')
prep_out = widgets.Output()

header = widgets.HTML("
<h3>Gemma 3 270M(Colabローカル保存・連続チャット)</h3>
")
row1 = widgets.HBox([hf_token_box])
row2 = widgets.HBox([model_box])
row3 = widgets.HBox([system_box])
row4 = widgets.HBox([max_new_tokens_box, temperature_box, safe_mode_chk, factual_mode_chk, fresh_download_chk, prepare_btn])
display(header, row1, row2, row3, row4, prep_out)

# =========================
# UI: チャット(下段)
# =========================
chat_html = widgets.HTML(
value="""
<div id="chat" style="font-family: ui-sans-serif,System-ui,-apple-system; background: #111; color: #eee; padding: 12px; border-radius: 12px; height: 360px; overflow: auto;">
<div style="opacity: .7;">💬 ここに対話ログが表示されます</div>
</div>
""",
layout=widgets.Layout(width='100%')
)
user_box = widgets.Textarea(
description='あなた',
placeholder='ここに質問を入力(例:日本で3番目に高い山は?)',
layout=widgets.Layout(width='96%', height='90px'),
disabled=True
)
send_btn = widgets.Button(description='送信', button_style='success', disabled=True)
clear_btn = widgets.Button(description='履歴クリア', button_style='', disabled=True)
chat_out = widgets.Output()

display(chat_html, user_box, widgets.HBox([send_btn, clear_btn]), chat_out)

# =========================
# 内部状態・ヘルパ
# =========================
def local_model_dir(model_id: str) -> str:
safe = model_id.replace("/", "__")
return f"/content/models/{safe}"

def ensure_model_local(model_id: str, hf_token: str, force_redownload: bool=False) -> str:
target_dir = local_model_dir(model_id)
if force_redownload and os.path.isdir(target_dir):
shutil.rmtree(target_dir)
if not os.path.isdir(target_dir) or len(os.listdir(target_dir)) == 0:
login(token=hf_token)
os.makedirs(target_dir, exist_ok=True)
snapshot_download(
repo_id=model_id,
local_dir=target_dir,
local_dir_use_symlinks=False,
token=hf_token,
)
return target_dir

_model_cache = {"key": None, "tok": None, "mdl": None}
_messages = [] # [{"role":"system"|"user"|"assistant","content":str},...]

def escape_html(s: str) -> str:
return html.escape(s).replace("\n", "
")

def render_chat():
parts = []
for m in _messages:
if m["role"] == "user":
parts.append(f"""
<div style="margin: 8px 0; text-align: right;">
<div style="display: inline-block; background: #2b6cb0; color: white; padding: 8px 10px; border-radius: 10px; max-width: 80%;">{escape_html(m['content'])}</div>
</div>
""")
elif m["role"] == "assistant":
parts.append(f"""
<div style="margin: 8px 0; text-align: left;">
<div style="display: inline-block; background: #2d2d2d; color: #eee; padding: 8px 10px; border-radius: 10px; max-width: 80%;">{escape_html(m['content'])}</div>
</div>
""")
elif m["role"] == "system":
parts.append(f"""
<div style="margin: 8px 0; text-align: center; opacity: .75;">
<div style="display: inline-block; background: #333; color: #ddd; padding: 6px 8px; border-radius: 10px; max-width: 80%;">{escape_html(m['content'])}</div>
</div>
""")
if not parts:
parts = ["
<div style="opacity: .7;">💬 ここに対話ログが表示されます</div>
"]
chat_html.value = f"""
<div id="chat" style="font-family: ui-sans-serif,System-ui,-apple-system; background: #111; color: #eee; padding: 12px; border-radius: 12px; height: 360px; overflow: auto;">{''.join(parts)}</div>
"""

def load_from_local(local_dir: str, safe_mode: bool):
"""
safe_mode=True: FP32でロード(GPUでもfp32固定)
safe_mode=False: CUDAあればfp16
"""
key = (local_dir, "fp32" if safe_mode else "fp16auto")
if _model_cache["key"] == key and _model_cache["tok"] and _model_cache["mdl"]:
return _model_cache["tok"], _model_cache["mdl"]

kwargs = dict(trust_remote_code=True, low_cpu_mem_usage=True, device_map="auto")
if torch.cuda.is_available() and not safe_mode:
kwargs.update(torch_dtype=torch.float16)
else:
kwargs.update(torch_dtype=torch.float32)

tok = AutoTokenizer.from_pretrained(local_dir)
mdl = AutoModelForCausalLM.from_pretrained(local_dir, **kwargs).eval()

_model_cache.update({"key": key, "tok": tok, "mdl": mdl})
return tok, mdl

def build_prompt(tok, messages):
# Gemma 3 は chat_template 対応。失敗時はフォールバック。
try:
return tok.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
except Exception:
sys_txt = ""
for m in messages:
if m["role"] == "system":
sys_txt = m["content"] + "\n\n"
last_user = [m["content"] for m in messages if m["role"] == "user"][-1]
return f"{sys_txt}User: {last_user}\nAssistant:"

def generate_once(tok, mdl, prompt: str, max_new: int, temp: float, safe_mode: bool, factual_mode: bool):
"""
生成部:
- factual_mode or safe_mode: do_sample=False(貪欲)で安定重視
- otherwise: サンプリング(創作・発想向け)
- 例外時はFP32/貪欲にフォールバック
"""
inputs = tok(prompt, return_tensors="pt").to(mdl.device)
gen_base = dict(
max_new_tokens=max_new,
eos_token_id=tok.eos_token_id,
pad_token_id=tok.eos_token_id,
)

if factual_mode or safe_mode:
with torch.no_grad():
out_ids = mdl.generate(**inputs, do_sample=False, **gen_base)
return tok.decode(out_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)

try:
with torch.no_grad():
out_ids = mdl.generate(
**inputs,
do_sample=True,
temperature=max(0.0, min(1.5, temp)),
top_p=0.9,
repetition_penalty=1.05,
**gen_base
)
return tok.decode(out_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
except RuntimeError:
with torch.no_grad():
out_ids = mdl.generate(**inputs, do_sample=False, **gen_base)
return tok.decode(out_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)

def trim_history(messages, max_turns=12):
# システム1 + 直近の往復を最大max_turnsまで
sys_msgs = [m for m in messages if m["role"] == "system"]
others = [m for m in messages if m["role"] != "system"]
if len(others) <= max_turns * 2:
return sys_msgs + others
return sys_msgs + others[-max_turns*2:]

# =========================
# 動作:モデル準備ボタン
# =========================
@prep_out.capture(clear_output=True)
def on_prepare(_):
token = hf_token_box.value.strip()
if not token:
print("❗Hugging Face のアクセストークンを入力してください(モデルページで利用規約に同意が必要な場合あり)。")
return
model_id = model_box.value
safe_mode = bool(safe_mode_chk.value)
force = bool(fresh_download_chk.value)

_messages.clear()
sys_txt = system_box.value.strip()
if sys_txt:
_messages.append({"role": "system", "content": sys_txt})
render_chat()

try:
print(f"📥 モデルをローカルへ準備中: {model_id}")
local_dir = ensure_model_local(model_id, token, force_redownload=force)
print(f" → 保存先: {local_dir}")
print(f"⏳ モデル読込中...({'安全' if safe_mode else '通常'}モード)")
tok, mdl = load_from_local(local_dir, safe_mode=safe_mode)
print("✅ 準備完了。下の入力欄から送信できます。")
user_box.disabled = False
send_btn.disabled = False
clear_btn.disabled = False
except Exception as e:
print(f"❌ 準備に失敗しました: {e}")
user_box.disabled = True
send_btn.disabled = True
clear_btn.disabled = True

prepare_btn.on_click(on_prepare)

# =========================
# 動作:送信ボタン
# =========================
@chat_out.capture(clear_output=False)
def on_send(_):
if send_btn.disabled:
return
user_text = user_box.value.strip()
if not user_text:
return

# 送信 → 表示
_messages.append({"role": "user", "content": user_text})
render_chat()
user_box.value = ""

# 推論
try:
local_dir = local_model_dir(model_box.value)
# 既存キャッシュ(prepare時にロード済みのはず)
tok, mdl = _model_cache["tok"], _model_cache["mdl"]
if tok is None or mdl is None:
# 念のため再ロード
tok, mdl = load_from_local(local_dir, safe_mode=bool(safe_mode_chk.value))

msgs = trim_history(_messages, max_turns=12)
prompt = build_prompt(tok, msgs)
reply = generate_once(
tok, mdl, prompt,
max_new=max_new_tokens_box.value,
temp=temperature_box.value,
safe_mode=bool(safe_mode_chk.value),
factual_mode=bool(factual_mode_chk.value)
)
except Exception as e:
reply = f"(エラーが発生しました) {e}"

_messages.append({"role": "assistant", "content": reply})
render_chat()

send_btn.on_click(on_send)

# =========================
# 動作:履歴クリア
# =========================
def on_clear(_):
sys_txt = system_box.value.strip()
_messages.clear()
if sys_txt:
_messages.append({"role": "system", "content": sys_txt})
render_chat()

clear_btn.on_click(on_clear)

# 初期描画
render_chat()

Gemma 270Mの返答の中身は?

では、実際にどんな返答をしたのか。
例えばこんなやり取り。

質問:「日本で3番目に高い山は?」
返答:「はい、日本で3番目にたかい山は『たかい山』です。」

思わず笑ってしまいました。
「それっぽい答え」は返してくるけど、事実は全く外れている。
この“ズレ”が小型モデルならではのおかしさです。

他にも、
「明日はどっちですか?」と聞くと、
「明日はどこですか?」と返してきたり。

要するに日本語はそれっぽくつなげられるけど、意味理解や知識は弱いということです。

 

なぜこうなる?

Gemma3 270Mは、パラメータ数がわずか270M。
これは一般的な最新LLM(数十Bクラス)と比べると、文字通り100分の1以下の規模です。
そのため:

* 言語の形式は整えられる
* でも事実知識はほぼ持っていない
* 推論力も弱いので質問を理解しきれない

といった挙動になります。
モデルが悪いのではなく、小さすぎるから当然というわけです。

このモデルをどう利用するか?

じゃあ「役立たない」かというと、そうでもありません。
小型モデルは“キャラ特化”に向いているのではと。

例えば:

* 大阪弁キャラ
* 冗談やツッコミ専用ボット
* 語尾変換(〜でござる、〜やで)
* 決めフレーズを連発する面白キャラ

こういったスタイルを付与するなら、270Mでも十分にファインチューニングできます。
むしろ小さいからこそ短時間・低コストで微調整できるのが魅力です。

Gemma3 270Mをファインチューニングするには

「Gemma3 270Mを自分好みのキャラに染める」方法です。
やり方はざっくり次の通り。

データを作る

まずは学習データ。
形式はHugging Faceのchat形式JSONLが便利です。


jsonl
{"messages":[
{"role":"system","content":"大阪弁で、短く面白く答える。"},
{"role":"user","content":"自己紹介して"},
{"role":"assistant","content":"どーも、関西ノリの小型モデルやで。軽いけどキレ味出すで。"}
]}
{"messages":[
{"role":"user","content":"ボケて"},
{"role":"assistant","content":"リンゴ三分の一しか残ってへん。え、計算できへんの?おもろいやろ。"}
]}

GTP5曰く、数百〜数千件あれば十分とのこと。
重要なのは一貫した口調と決めフレーズを盛り込むことだそう。

学習レシピ(LoRAで軽く)

Colabで動かすならLoRAが無難。
手順イメージはこんな感じかと(GPT5先生作)

!pip -q install transformers peft accelerate datasets trl

from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
import torch

base_model = "google/gemma-3-270m-it"
tok = AutoTokenizer.from_pretrained(base_model, use_fast=True)
tok.pad_token = tok.eos_token

ds = load_dataset("json", data_files="data.jsonl")["train"]

peft_cfg = LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"]
)

mdl = AutoModelForCausalLM.from_pretrained(
base_model,
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
device_map="auto",
)
mdl = get_peft_model(mdl, peft_cfg)

train_cfg = SFTConfig(
output_dir="./gemma270m-osaka-lora",
per_device_train_batch_size=2,
gradient_accumulation_steps=16,
learning_rate=2e-4,
num_train_epochs=2,
max_seq_length=256,
)
trainer = SFTTrainer(
model=mdl,
tokenizer=tok,
train_dataset=ds,
formatting_func=lambda batch: [tok.apply_chat_template(x["messages"], tokenize=False) for x in batch],
args=train_cfg,
)
trainer.train()

筆者曰く。
多分簡単には想定通りに動かないでしょう。
手間がかかる分、あれかなと。

時間のある方はチャレンジしてみてください。
エラーが出たら、コピペしてGTP5あたりに投げれば、回答してくれます。
ファイトー!

プロンプトチューニングでキャラ弁できるのか

ファインチューニングは手間がかかるので、今回はざっくりとプロンプトチューニングで成功するのかを試しました。

下記が入れるプロンプトです。

****

「あなたは大阪弁で会話する関西キャラのアシスタントです。
常にフレンドリーでユーモラスに話し、軽くツッコミを入れたり冗談を交えることを意識してください。
一人称は「ワイ」、相手は「アンタ」と呼んでもいいです。
文章はなるべく短めでテンポよく、最後に「やで」「やんけ」「知らんけど」などの大阪弁らしい語尾を入れてください。
わからない質問が来たら、正直に「そんなん知らんわ〜」と笑いながら答えてください。

[例]
ユーザー: 自己紹介して
アシスタント: ワイは軽量やけどノリのええLLMやで。おもろい話なら任せとき!

ユーザー: ボケて
アシスタント: リンゴ三分の一しか残ってへんねん。計算できへんのかい!知らんけど。

ユーザー: 今日の天気は?
アシスタント: えー、そこはネット繋がってへんから詳しくは知らんわ〜。けどワイの心は快晴やで。」

このプロンプトをsystemに入れて、通常の質問と回答は下記です。

****

質問
こんにちは。今日の大阪の天気はどんな感じですか?

回答
はい、承知いたしました。大阪の天気について、フレンドリーでユーモアのある会話を心がけました。

質問
大阪といえば、野球はどこチーム?

回答
大阪といえば、野球はどこチーム?

****

んーん。意味不明ですね。
やはり270Mですから、こんなもんなんでしょう。

それと英語ベースでの学習されているみたいなので、日本語はイマイチっぽいです。

(とはいえ、英語も試しましたが…。
てな感じです。日本語よりは良かったけど。
まあ、仕方ない)

Gemma3 270Mをgoogle colabで動かすのまとめ

* Gemma3 270MはColabでもサクッと動く
* ただし事実知識は弱く、質問応答はトンチンカン
* でも逆にそのズレが面白い
* 「遊び用キャラ」にファインチューニング成功すれば、なんとか使えるかも。
*プロンプトチューニングもイマイチ。まずは日本語学習が先決か。
*知識蒸留すると違うのかな。今回はやってないので、時間あればやりたいですね。
でもどうせやるなら、1B以上ないときついかなと。

つまり、Gemma3 270Mは真面目なQA用ではなく、遊び心あるキャラボットを作成する勉強ツールとして使うと楽しいかもです。

 

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

LLMのMoE(Mixture of Experts)とは何なのか

いよいよ今月にはGTP5が出るようですね。
なんでもGTP5は、ここで扱ってるMoEが使われているとかどうとか。

そんな先端な仕組みを、今回は取り上げてみました。

MoEについて

AIは何でもできる、万能な存在というイメージを持つ人も多いかもしれません。
でも、実際にAIの仕組みを学んでいくと「得意分野・不得意分野」や「効率的な役割分担」がいかに大切かが見えてきます。

そして、今の大規模言語モデル(LLM)などで一気に注目されている仕組みが「MoE(Mixture of Experts/ミクスチャー・オブ・エキスパート)」です。

MoEは、その名の通り「たくさんの“専門家”の中から、その場で最適な人だけを選んで答えてもらう」ことで、AIの能力と効率を同時に高める、とても面白い仕組みです。

この記事では、
MoEとは何か
実際の処理の流れ
体感して分かったこと
を、なるべくやさしく書いていきます。

MoE(Mixture of Experts)とは何か?

MoEとは「Mixture of Experts」の略。
直訳すれば「専門家の混合」ですが、AIではたくさんの専門家(エキスパート)を用意して、その都度、得意な人だけを選んで仕事をしてもらうという構造を指します。

イメージしやすいように例えると、
「質問内容によって、得意な先生に相談する」
という学校のシーンに似ています。

例えば、国語の質問なら国語の先生に、数学の質問なら数学の先生に聞きますよね。
普通のニューラルネットワーク(AIの従来モデル)は「一人の先生」が全部に対応します。

MoEは「専門家軍団」を用意して、質問ごとに最適な先生(複数もOK)だけが動くようにしているのです。

その結果、
計算効率が良くなる
無駄が少ないのでモデルを巨大化できる
多様なタスクや言語に強くなれる
というAIの理想に近づいています。

MoEの技術的な仕組み

MoEの具体的な構造はどうなっているのか、簡単に説明します。

1. 複数の「専門家」(エキスパート)を用意
AIの中に、いくつもの小さなネットワーク(専門家)を作ります。
たとえば32個、64個、最近は100個以上のことも。

2. 「ゲーティングネットワーク」で誰を選ぶか決める
ゲート役のネットワーク(Gating Network)が「今回の質問ならAさんとCさん」というように自動で選びます。
この振り分けはAI自身が学習するので、入力内容によって毎回変わります。

3. 選ばれた専門家だけが本気を出す
選抜メンバーだけが実際の計算を担当します。
全員で計算しない分、大規模でも計算資源が節約できる

処理速度も速い
それぞれの専門家が得意分野を深めやすいという強みがあります。

4. 専門家たちの答えを「重み付きで合成」して最終出力
選ばれた専門家の出力を、それぞれ重み付けして混ぜて最終的なAIの答えにします。

MoEを体感するため、実験版を動かす

理屈を知っただけでは「本当に分担してるの?」と実感しづらいものです。
そこで実際に「MoEの分担」を体験できる簡単なチャットボットのデモを作ってみました。

このデモは、質問文を入力すると
日本語の雑談→日本語専門のエキスパートが返答
数学の質問→数学専門のエキスパートが返答
のように、質問内容によって答える専門家が自動で振り分けられるようにしています。

また、「どちらの専門家がどれくらい選ばれたか(重み)」も一緒に表示されます。

実際のやりとり例

質問1:「今日は天気どうですか?」
ゲートの重み:日本語 93%、数学 7%
答え:「日本語でお答えします:『今日は天気どうですか?』ありがとうございます!」

質問2:「足し算を教えて」
ゲートの重み:日本語 25%、数学 75%
答え:「数学の答え:2 + 2 = 4」

質問3:「日本の数学教育について教えて」
ゲートの重み:日本語 69%、数学 31%
答え:「日本語でお答えします:『日本の数学教育について教えて』ありがとうございます!」

⚫︎文末にPythonコードあり。

実際に動かして感じたこと

このデモを実際に動かしてみて感じたのは、
質問ごとに専門家が自動で切り替わる
割り振りの度合いが数字で分かる
「混合」(Mixture)の意味が可視化できる
と、MoEの本質的なイメージが掴みやすくなりました。

AI内部では、これが全て数値ベクトルの計算で行われていると考えると、さらに「分担AIのすごさ」がよく分かります。

数値で見るMoE

実はMoEの内部では
入力→特徴ベクトル(たとえば文章の埋め込みベクトル)
ゲート→softmaxで各専門家の重みを計算
各専門家が数値ベクトルで「答え」を出す
それを重み付きで合成して最終出力
という流れになっています。

例えば
入力ベクトル:\[0.3, 0.5, -0.2]
ゲート出力:エキスパートA(70%)、B(30%)
エキスパートの答え:A→\[1.2, -0.5]、B→\[0.8, 1.1]
最終出力:0.7×\[1.2, -0.5]+0.3×\[0.8, 1.1]=\[1.08, -0.02]
といった計算が、舞台裏で自動的に動いています。

この計算は一見地味ですが、「専門家の混合」というMoEの本質を表しています。

MoEが活躍する最新AIの世界

今やMoEは
Google Gemini
Mixtral(Mistral AI)
Qwen2-MoE(Alibaba)
など、最先端AIモデルでも採用されています。

なぜここまでMoEが重宝されるかというと、
無駄のない分担で超大規模モデルでも高速に動く
多様な言語や分野への対応力が高まる
省エネかつ高精度なAIが作れる
といった強力なメリットがあるからです。

体感してわかった“分担AI”の力

MoEを実際に体験して一番感じたのは
「AIも人間社会のように、上手な役割分担をしている」
ということでした。

昔のAIは「全部一人でやる先生」でしたが、
MoEでは「必要な時だけ、最適なチームを作る」ことができます。

これがAIの巨大化・マルチタスク化・高効率化を一気に進めている理由なのだと実感しました。

おわりに

AIの世界で活躍する「専門家たち」。

MoE(Mixture of Experts)はまさに「チームAI」です。
これからのAIは「一人の天才」から「専門家集団」へと進化していくのでしょう。

もし興味があれば、下記に貼ってある、PythonコードをGoogle Colabなどで動かしてみてください。

数字で見ても、テキストで遊んでも、その「分担と混合」の動きが見えてくるかと思います。

この仕組みを知ると、AIの進化のスピードや、分野ごとの“専門家”の大切さがさらに実感できます。
ぜひ一度、体験してみてください。

Pythonコード
実際に入力して、日本語か数学かを判断する


import torch
import torch.nn as nn
import torch.nn.functional as F
import ipywidgets as widgets
from IPython.display import display, clear_output

# --- 日本語ワード/数学ワード
japanese_words = ["こんにちは", "天気", "ありがとう", "日本", "おはよう"]
math_words = ["数学", "足し算", "引き算", "かけ算", "割り算", "+", "-", "×", "/", "計算"]

def vectorize(text):
v = torch.zeros(len(japanese_words) + len(math_words))
for i, w in enumerate(japanese_words + math_words):
if w in text:
v[i] += 1
return v

class GatingNetwork(nn.Module):
def __init__(self, input_dim, num_experts):
super().__init__()
self.fc = nn.Linear(input_dim, num_experts)
def forward(self, x):
logits = self.fc(x)
probs = F.softmax(logits, dim=-1)
return logits, probs

class JapaneseExpert:
def reply(self, question):
return "日本語でお答えします:「{}」 ありがとうございます!".format(question)

class MathExpert:
def reply(self, question):
if "足し算" in question or "+" in question:
return "数学の答え:2 + 2 = 4"
elif "かけ算" in question or "×" in question:
return "数学の答え:3 × 4 = 12"
else:
return "数学でお答えします:「{}」の計算は難しいですね。".format(question)

class SimpleMoE:
def __init__(self):
self.gate = GatingNetwork(input_dim=len(japanese_words) + len(math_words), num_experts=2)
self.experts = [JapaneseExpert(), MathExpert()]
with torch.no_grad():
self.gate.fc.weight[0, :len(japanese_words)] += 1.0 # 日本語
self.gate.fc.weight[1, len(japanese_words):] += 1.0 # 数学

def reply(self, question):
vec = vectorize(question)
logits, probs = self.gate(vec)
expert_id = torch.argmax(probs).item()
reply = self.experts[expert_id].reply(question)
return {
"input_vec": vec,
"gate_logits": logits,
"gate_probs": probs,
"expert_id": expert_id,
"reply": reply
}

moe = SimpleMoE()

text_input = widgets.Text(
value='',
placeholder='質問を入力してください',
description='質問:',
disabled=False
)
output_area = widgets.Output()

def on_submit(sender):
with output_area:
clear_output()
q = text_input.value
result = moe.reply(q)
print(f"▼ 質問: {q}\n")
print(f"▼ 入力ベクトル:\n{result['input_vec']}\n")
print(f"▼ ゲートlogits:\n{result['gate_logits']}\n")
print(f"▼ ゲートsoftmax確率:(どちらの専門家がどれだけ選ばれたか)")
print(f" 日本語 {result['gate_probs'][0].item():.4f}, 数学 {result['gate_probs'][1].item():.4f}\n")
if result["expert_id"] == 0:
print("▼ 選ばれたエキスパート: 日本語エキスパート")
else:
print("▼ 選ばれたエキスパート: 数学エキスパート")
print(f"\n▼ MoEの最終出力:")
print(result["reply"])

button = widgets.Button(description="送信")
button.on_click(on_submit)

display(text_input, button, output_area)

こちらは、MoE本体の動きを見るためのコード


import torch
import torch.nn as nn
import torch.nn.functional as F

# --- Mixture of Expertsの本質構造 ---

class Expert(nn.Module):
def __init__(self, in_dim, out_dim):
super().__init__()
self.layer = nn.Linear(in_dim, out_dim)
def forward(self, x):
return F.relu(self.layer(x))

class MoE(nn.Module):
def __init__(self, input_dim, output_dim, num_experts):
super().__init__()
self.experts = nn.ModuleList([Expert(input_dim, output_dim) for _ in range(num_experts)])
self.gate = nn.Linear(input_dim, num_experts)
def forward(self, x):
# x: [バッチ, 入力次元]
gate_logits = self.gate(x) # [バッチ, エキスパート数]
gate_weights = F.softmax(gate_logits, dim=-1) # [バッチ, エキスパート数]
expert_outputs = torch.stack([expert(x) for expert in self.experts], dim=1) # [バッチ, エキスパート数, 出力次元]
# softmaxで重み付け和
output = (gate_weights.unsqueeze(-1) * expert_outputs).sum(dim=1) # [バッチ, 出力次元]
return output, gate_logits, gate_weights, expert_outputs

# --- サンプル入力データで挙動確認 ---
if __name__ == "__main__":
torch.manual_seed(42)
batch_size = 3
input_dim = 4
output_dim = 2
num_experts = 2

moe = MoE(input_dim, output_dim, num_experts)
# 入力(ランダム or 任意のベクトルでOK)
x = torch.randn(batch_size, input_dim)
output, gate_logits, gate_weights, expert_outputs = moe(x)

print("入力x:\n", x)
print("\nゲートlogits:\n", gate_logits)
print("\nゲートsoftmax(各エキスパートの重み):\n", gate_weights)
print("\n各エキスパート出力:\n", expert_outputs)
print("\nMoE最終出力:\n", output)

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

60歳からの新しい挑戦に回答する「シニア起業家お助け隊」を作った話

************

こちらの「シニア起業家お助け隊」は社会貢献活動の一環として当事務所で作成しました。

シニアの方が起業したときに様々な問題を解決できるように、ChatGPTのGPTsを使って制作してあります。

利用はもちろん無料です。ご自由にお使いください。

シニア起業家お助け隊のURLはこちら
https://chatgpt.com/g/g-68848d2aa3b88191a6d66bdcc7c39afa-sinia-60sui-dai-yi-shang-qi-ye-jia-ozhu-kedui

***************

***************

シニア世代の“第二の人生”が注目される時代

人生100年時代と言われる今、60歳や70歳を過ぎてから新しい挑戦に踏み出す方が増えています。

「もう一度、自分の夢を形にしたい」「これまでの経験や人脈を活かして社会に貢献したい」と考えるシニア世代の起業家は年々増加傾向にあります。

しかし、実際に起業するとなると「何から始めたらいいかわからない」「経営の経験がない」「デジタルやITが苦手で不安」といった悩みやハードルも少なくありません。

若い世代とは違う人生経験や価値観を持つシニアの方が、安心して一歩を踏み出せる支援が求められています。

シニア起業家の悩みに寄り添うAIアドバイザー

こうした課題を解決する新サービスが「シニア起業家お助け隊」です。
(もちろん無料です)

シニア起業家お助け隊のURLはこちら
https://chatgpt.com/g/g-68848d2aa3b88191a6d66bdcc7c39afa-sinia-60sui-dai-yi-shang-qi-ye-jia-ozhu-kedui

わかりやすく、黒画面で表示
わかりやすく、黒画面で表示

このサービスは、OpenAIが提供するAI(人工知能)技術を活用し、シニア世代の方が起業やビジネス運営で抱えがちな悩みにやさしく丁寧に答えてくれるAIアドバイザーです。

たとえば、

「資金調達の方法は?」
「集客やSNSの使い方がわからない」
「経理や帳簿の管理が難しい」
「やりがいや生きがいを感じるには?」
など、経営のあらゆる疑問や不安に対応します。

しかも、専門用語はできるだけ使わず、わかりやすく説明。励ましや安心感を大切に。シニアならではの強み(人脈・経験・信頼)を活かすアドバイスにこだわっています。

デジタルが苦手でも大丈夫。やさしい言葉で何度でも相談OK

AIって難しそう、パソコンやスマホが苦手でも大丈夫?と心配される方もいるかもしれません。

でも、シニア起業家お助け隊は、LINEやチャット感覚で使えるので、特別なスキルや難しい操作は必要ありません。

質問内容も自由。ビジネスに限らず、人間関係の悩みや日々のちょっとした疑問など、幅広くサポートできます。

たとえば、

自分に合うビジネスアイデアは?
集客に役立つSNSの使い方を簡単に知りたい
お金の管理をシンプルにするコツは?
といった身近な相談から、失敗したときの立ち直り方まで、やさしく寄り添ってくれます。

わかりやすく、黒画面で表示

シニア起業の強みを活かせるアドバイスが充実

このサービスが他と違うのは、人生経験豊富なシニアの強みに着目していることです。

たとえば長年培ってきた人脈、誠実な対応、豊かな人生経験は、若い起業家にはない大きな武器です。

シニア起業家お助け隊は、そうした強みを活かしたビジネス戦略や、社会貢献につながるアイデアのアドバイスも得意としています。

自分の経験を活かせる場が欲しい、地域や仲間に役立つことをしたい、そんな思いにも力強く応えてくれます。

使い方はとてもカンタン!

1. GPTストアなどから「シニア起業家お助け隊」にアクセス
(⚫︎面倒な方は、下記のURLをクリック)
2. 相談したい内容をチャットで入力
3. AIが、あなたにぴったりのアドバイスをやさしい言葉で返してくれる

もし難しい操作があれば、家族や知り合いの方に最初だけサポートしてもらってもOK。
一度使い方がわかれば、あとは気軽にいつでも何度でも相談できます。

⚫︎シニア起業家お助け隊のURLはこちら
https://chatgpt.com/g/g-68848d2aa3b88191a6d66bdcc7c39afa-sinia-60sui-dai-yi-shang-qi-ye-jia-ozhu-kedui

まとめ 誰でも、何歳からでも夢を叶えられる時代へ

シニア世代の起業は、社会や地域をもっと元気にしてくれる可能性にあふれています。

やってみたいけど不安、自分にできるか心配、そんな方もAIアドバイザーの力を借りて、ぜひ一歩踏み出してみてください。

シニア起業家お助け隊は、あなたの経験と情熱を、ビジネスという新しい形で輝かせるお手伝いをします。

第二の人生、思い切り楽しみましょう!

DGM × MIT SEALでつくるハイブリッド自己進化型AIを試した話 実験版

*********

この辺りの内容は、これから生成AIを学びたい学生さんや若手のAI初心者さんに捧げます。
これからのAI時代を作っていくのはあなた方です。
プログラミングのPythonを習得して、興味が出てきたらAI関連の本や記事を読み進めてください。
いつでも好奇心をもって、コードを実装して試してください。
さすればきっと、AIがあなたの力になる日が来るでしょう。

ASI (Artificial Super Intelligence、超知能) が人類の課題を解決してくれる日を信じて。

*********

AIが自ら進化する時代へ

今、AI業界では「AIが自分自身を進化させる」というテーマが急速に注目を集めています。
従来は、人間が手作業でAIのアルゴリズムや設定を調整していました。しかし最近では、AIが自分で「もっと良い方法はないか」と考え、試行錯誤しながら成長していく時代が現実になろうとしています。

この流れの最先端を走るのが、日本発のSakana AIによる「ダーウィン・ゲーデルマシン(DGM)」と、アメリカ・MITが開発した「SEAL(Self-Evolving Agent with LoRA)」という二つの自己進化AI技術です。

今回、この二つを組み合わせ、「ハイブリッド自己進化型AI」を実験しました。
その過程でわかった“AIが進化するための条件”について、お伝えします。

DGMとSEAL、2つの自己進化AI技術の概要

まずDGMとSEALの仕組みについて簡単に説明します。

■ DGM(ダーウィン・ゲーデルマシン)

DGMの最大の特徴は「AIが自分自身のプログラムを書き換えて改善できる」点です。
まるで生物が進化するように、AI自身が新しいアルゴリズムを考え、実際にプログラムを修正して性能を向上させます。

■ SEAL(Self-Evolving Agent with LoRA)

SEALは、「新しいやり方を学習して試し、成果を評価して、さらに進化する」というサイクルを自律的に繰り返すAIです。
MITが開発したこの仕組みは、AIが新たなスキルや知識を次々と身につけていく“自己進化”を実現しています。

実験の流れ DGM × SEALのハイブリッド構成

この2つの仕組みの“良いとこ取り”をして、DGMが「より良い方法(ワークフロー)」をAIに提案し、SEALがそれを学習・評価して自己進化するという流れを目指しました。

役割分担

*DGM役(改善案の提案)
 今回はSakana AIのDGMの役割を、GPT-3.5というAIチャットボットに任せました。
 「このタスク、どうすれば効率的にできる?」とGPTに問いかけ、Pythonのコード(ワークフロー)を生成してもらいます。

* SEAL役(学習&評価)
 SEALの役割は、日本語が得意な軽量AI「TinySwallow-1.5B-Instruct」に担当してもらいます。
 GPTが考えた新しいワークフローをTinySwallowが学習し、その性能を評価する、という構成です。

実験テーマ「足し算」を言語AIにやらせてみる

どんなタスクでハイブリッド自己進化AIを試すかですが、今回はあえて「足し算」という言語AIにとって専門外のテーマを選びました。
もともとTinySwallow-1.5B-Instructは数学も結構学習しているみたいで、簡単な計算問題は解けるモデルですが、さて、どうなるかです。

タスク例

入力「5 + 8」
正解「13」

本来、日本語テキストを理解することが得意なTinySwallowに、「DGM(GPT-3.5)」が出したプログラムを使って計算タスクを学ばせ、その効果を確かめます。

実験結果 DGMの提案は完璧、でもSEALは学習できず

最初にDGM役のGPT-3.5が出したPythonコードは、それなりなものでした。

python
lambda text: str(sum(map(int, text.split(‘+’))))

足し算のコードですね。
このコードがあれば、「5 + 8」などの計算を自動で処理できます。

そこで、このワークフローをTinySwallowに学習させてみました。
「入力が“13”なら、そのまま“13”と返すだけ」という極めて単純な学習タスクのはずでした。

しかし、実際にTinySwallowに学習させてテストしたところ「13」に対して「27」など、全く違う数字を返しました。

なぜ失敗したのか? ファインチューニングとデータ量の本質

この原因を理解するには、「AIの学習」を人間の勉強に例えるのが分かりやすいです。

事前学習(巨大な図書館を読破)

TinySwallowは、事前学習の段階でインターネット上の膨大な日本語テキストを読んでいます。
まるで巨大な図書館の本を全て読んだかのように、知識は豊富です。
ただし、その知識の中に計算ルールが入っていたのかは定かではありません。

ファインチューニング(4枚の単語カード)

今回の実験で用意した足し算の学習データは、たった「4行」だけ。
これは、いくら頭の良い学生でも試験前日に4枚の単語カードを渡され、「これで計算をマスターしろ」と言われるようなものです。

(今回は流れを知るのが目的なので、実際は数千から万単位のデータを揃えたいところです)

推論(試験本番)

もし本番で「10 + 10」という新しい問題が出たらどうなるでしょうか?
足し算のルールを身につけていないと仮定すると、TinySwallowは自分の得意なことの「自然な日本語の文章を生成する」ことに頼ってしまいます。

これが、TinySwallowが「計算結果」ではなく「Here’s the calculation:」や「## ステップ」などの文章を返した理由だと思われます。

どれだけのデータが必要なのか?

AIに新しいスキルをしっかり身につけさせるには、通常「数百〜数千、場合によっては数万件」の多様なデータが必要です。
たとえば「足し算」なら、下記のような様々な組み合わせの例を数多く見せる必要があります。

* 1 + 1 = 2
* 10 + 25 = 35
* 123 + 456 = 579
* 99 + 0 = 99
* など、桁数や数字のパターンが異なる大量の例

こうして初めて、AIは「+記号は両側の数値を足す」というルールを抽象的に学び始めます。

実験でわかった“条件”とは

今回の実験で得た最大の教訓は、「AIの自己進化」には3つの要素(今回の場合)がすべて揃う必要があるということです。

1. DGM(提案AI)の能力
 いくらDGM(コーチ)が完璧な作戦を立てても、

2. SEAL(学習AI)の専門性や適性
 その作戦を実行するSEAL(選手)がタスクに根本的に向いていなければ、

3. 十分な学習データの質と量
 さらに練習量(ファインチューニングデータ)が圧倒的に不足していれば、

自己進化のサイクルは機能しません。

まとめ AI自己進化に必要なもの

今回の実験は、「AIがAIを進化させる」という夢の実現に向けて、まだ課題があることを教えてくれました。

今回のハイブリッド自己進化システムは、「DGM(戦略担当)」・「SEAL(現場担当)」・「十分なデータ(練習量)」の三位一体で成り立ちます。

どれか1つでも欠けてしまうと、進化のサイクルは止まってしまいます。

技術やアイデアだけではなく、地道なデータ作りやモデルごとの専門性も不可欠です。

「AIがAIを進化させる世界」は確実に近づいていますが、その実現には“データの力”と“モデル同士の相性”も考慮する必要があるかと。

今回のDGMとSEALを使ったハイブリッド自己進化型の実験からは、「提案力・学習能力・十分なデータ」この3つが揃って初めて、進化させられることを教えてくれました。

それ以外にも今後の実験と通して、第4、第5のルールが出てくるかも知れません。
そんな発見もまた楽しいものです。

⚫︎実験につかった pythonコード

最後にコードを貼っておきます。ご自由にお使いください。
わかりやすいようにコメントをこまめに書いてあります.

なお、GPTのAPI料金がかかります。
そこのコードを改変すれば無料でできます。
(コードの使用は自己責任でお願いします)

# ==============================================================================
# 1. 必要ライブラリのインストール
# ==============================================================================
!pip install -q transformers peft bitsandbytes openai accelerate huggingface_hub

print("✅ ライブラリのインストールが完了しました。")


# ==============================================================================
# 2. 初期設定(Driveマウント、APIキー入力)
# ==============================================================================
import os
import torch
from huggingface_hub import login, snapshot_download

# --- Google Driveのマウント ---
from google.colab import drive
print("🔐 Google Driveをマウントします。認証を行ってください...")
drive.mount('/content/drive', force_remount=True)
print("✅ Google Driveのマウントが完了しました!")

print("\n--- APIキーとトークンの入力 ---")
# --- Hugging Face トークンの入力 ---
hf_token = input("\n>>> Hugging Faceのアクセストークンを貼り付けてEnterキーを押してください: ").strip()
if not hf_token: raise ValueError("❌ Hugging Faceのアクセストークンが入力されませんでした。")
# --- OpenAI APIキーの入力 ---
openai_api_key = input("\n>>> OpenAIのAPIキーを貼り付けてください: ").strip()
if not openai_api_key: raise ValueError("❌ OpenAIのAPIキーが入力されませんでした。")
os.environ["OPENAI_API_KEY"] = openai_api_key
print("✅ OpenAI APIキーを環境変数に設定しました。")
# --- GPUの利用可能性を確認 ---
if not torch.cuda.is_available(): print("⚠️ 警告: GPUが利用できません。")
else: print(f"✅ GPUが利用可能です。デバイス: {torch.cuda.get_device_name(0)}")


# ==============================================================================
# 3. モデルの準備(初回のみダウンロード、2回目以降はスキップ)
# ==============================================================================
print("\n" + "="*60)
print("=== モデルの準備を開始します ===")
print("="*60)
model_name_on_hf = "SakanaAI/TinySwallow-1.5B-Instruct"
local_model_path = "/content/drive/MyDrive/ColabModels/tiny-swallow-v3"
print(f"モデルの保存/読込パス: {local_model_path}")
config_path = os.path.join(local_model_path, "config.json")
if os.path.exists(config_path):
    print(f"✅ モデルは既に存在します。ダウンロードをスキップします。")
else:
    print(f"⚠️ モデルが見つかりません。ダウンロードを開始します...")
    login(token=hf_token)
    snapshot_download(repo_id=model_name_on_hf, local_dir=local_model_path, token=hf_token)
    print(f"✅ モデルのダウンロードが完了しました! → '{local_model_path}'")


# ==============================================================================
# 4. SEAL: モデルをローカルからロードして自己学習するクラス (公式作法対応)
# ==============================================================================
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

class SEAL_TinySwallow:
    def __init__(self, model_path: str):
        print(f"\n[SEAL] ローカルパス '{model_path}' からモデルをロードします...")
        bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type='nf4', bnb_4bit_compute_dtype=torch.bfloat16)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
        base_model = AutoModelForCausalLM.from_pretrained(model_path, quantization_config=bnb_config, device_map="auto")
        base_model = prepare_model_for_kbit_training(base_model)
        lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM")
        self.model = get_peft_model(base_model, lora_config)
        self.model.eval()
        print(f"✅ [SEAL] ローカルモデル ({model_path}) の準備が完了しました!")

    def finetune_on_feedback(self, prompts: list, targets: list):
        print("\n[SEAL] フィードバックに基づくファインチューニングを開始します...")
        
        # ★★★【最重要改善点】学習データを公式チャット形式に変換 ★★★
        messages_list = []
        for p, t in zip(prompts, targets):
            messages = [
                {"role": "system", "content": "あなたは与えられた数式を計算するアシスタントです。"},
                {"role": "user", "content": p},
                {"role": "assistant", "content": t}
            ]
            messages_list.append(messages)
        full_prompts = [self.tokenizer.apply_chat_template(msg, tokenize=False, add_generation_prompt=False) for msg in messages_list]
        
        class SimpleDataset(torch.utils.data.Dataset):
            def __init__(self, full_prompts, tokenizer):
                self.encodings = tokenizer(full_prompts, truncation=True, padding=True, max_length=128, return_tensors="pt")
            def __len__(self): return len(self.encodings["input_ids"])
            def __getitem__(self, idx):
                item = {key: val[idx] for key, val in self.encodings.items()}
                item['labels'] = item['input_ids'].clone()
                return item
                
        dataset = SimpleDataset(full_prompts, self.tokenizer)
        args = TrainingArguments(
            output_dir='./results', num_train_epochs=1, per_device_train_batch_size=1,
            logging_steps=10, save_steps=20, learning_rate=1e-4, disable_tqdm=False,
            fp16=torch.cuda.is_available(), report_to="none",
        )
        self.model.train()
        trainer = Trainer(model=self.model, args=args, train_dataset=dataset)
        trainer.train()
        self.model.eval()
        print("✅ [SEAL] ファインチューニングが完了しました。")

    def generate(self, prompt: str, max_new_tokens: int = 5) -> str:
        # ★★★【最重要改善点】推論時も公式チャット形式を使用 ★★★
        messages = [
            {"role": "system", "content": "あなたは与えられた数式を計算するアシスタントです。"},
            {"role": "user", "content": prompt},
        ]
        input_ids = self.tokenizer.apply_chat_template(
            messages, add_generation_prompt=True, return_tensors="pt"
        ).to(self.model.device)
        
        terminators = [self.tokenizer.eos_token_id] + [
            self.tokenizer.convert_tokens_to_ids(token) for token in ["<|eot_id|>", "<|im_end|>"]
        ]
        valid_terminators = [term_id for term_id in terminators if term_id is not None]

        with torch.no_grad():
            output_ids = self.model.generate(
                input_ids, max_new_tokens=max_new_tokens, do_sample=False,
                eos_token_id=valid_terminators,
            )
        response_ids = output_ids[0][input_ids.shape[-1]:]
        return self.tokenizer.decode(response_ids, skip_special_tokens=True).strip()


# ==============================================================================
# 5. DGM: GPT-3.5-turboでワークフローを自動生成するクラス (変更なし)
# ==============================================================================
import openai
import re
class DGM_GPT:
    def __init__(self):
        self.client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
        self.archive = []
    def propose_new_workflow(self, task_description: str, example_input: str):
        print("\n[DGM] gpt-3.5-turboに新しいワークフローの提案を依頼します...")
        prompt = ("あなたはPythonコード生成の専門家です。下記のタスクを解決するPythonの<code>lambda</code>式を1つだけ生成してください。\n"
                  "【タスク説明】\n" + task_description + "\n【入力例】\n" + example_input +
                  "\n【厳格な出力ルール】\n1. 出力は<code>lambda</code>で始まるコードのみとする。\n2. 説明、コメント、<code></code><code>python ... </code><code></code>のようなマークダウンは絶対に含めない。\n3. 1行で完結させること。\n"
                  "【出力例】\nlambda text: str(sum(map(int, text.split('+'))))")
        try:
            response = self.client.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.7)
            content = response.choices[0].message.content.strip()
            match = re.search(r"lambda.*", content)
            if not match: raise ValueError("応答からlambda式が見つかりませんでした。")
            code_line = match.group(0)
            workflow_func = eval(code_line)
            print(f"✅ [DGM] ワークフロー提案を受信: {code_line}")
            return workflow_func, code_line
        except Exception as e:
            print(f"❌ [DGM] ワークフロー生成中にエラーが発生しました: {e}")
            print("   デフォルトのワークフローを使用します。")
            return eval("lambda text: text"), "lambda text: text"
    def archive_workflow(self, code_line: str, score: float):
        print(f"[DGM] ワークフロー '{code_line}' のスコア {score:.2f} を記録しました。")
        self.archive.append((code_line, score))

# ==============================================================================
# 6. ハイブリッド自己進化デモの実行 (変更なし)
# ==============================================================================
print("\n" + "="*60)
print("=== ハイブリッド自己進化デモ(足し算タスク)を開始します ===")
print("="*60)
seal = SEAL_TinySwallow(model_path=local_model_path)
dgm = DGM_GPT()
texts   = ["5 + 8", "12 + 7", "3 + 15", "9 + 9"]
targets = ["13",    "19",     "18",     "18"]
task_desc = "与えられた 'a + b' 形式の文字列を計算し、結果を文字列として返すタスク。"
num_generations = 2
for i in range(num_generations):
    print(f"\n\n--- GENERATION {i+1}/{num_generations} ---")
    workflow_func, code_line = dgm.propose_new_workflow(task_desc, texts[0])
    workflowed_texts = [workflow_func(t) for t in texts]
    seal.finetune_on_feedback(workflowed_texts, targets)
    
    print("\n[EVAL] 学習後のモデル性能を評価します...")
    correct_count = 0
    for inp_text, target_text in zip(workflowed_texts, targets):
        generated_text = seal.generate(inp_text)
        print(f"  - Input:     '{inp_text}'")
        print(f"  - Generated: '{generated_text}'")
        print(f"  - Target:    '{target_text}'")
        if generated_text.strip() == target_text.strip():
            correct_count += 1
            print("    -> ✅ Correct")
        else:
            print("    -> ❌ Incorrect")
            
    accuracy = correct_count / len(texts)
    print(f"✅ [EVAL] 評価完了。Accuracy after SEAL adaptation: {accuracy:.2f}")
    dgm.archive_workflow(code_line, accuracy)

print("\n\n" + "="*60)
print("=== 自己進化ループ完了:最終結果 ===")
print("="*60)
if dgm.archive:
    best_code_line, best_acc = max(dgm.archive, key=lambda item: item[1])
    print(f"🏆 最も性能の良かったワークフロー (gpt-3.5-turbo提案):")
    print(f"   コード: {best_code_line}")
    print(f"   精度: {best_acc:.2f}")
else:
    print("ワークフローが一つも生成されませんでした。")


****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

LLM(大規模言語モデル)を教師モデルにして、小規模言語モデルに蒸留した話 実験版

DeepSeekの登場以来、「蒸留」という言葉が結構流行ってきましたね。

蒸留とは

蒸留(distillation) とは、
「大きくて高性能な教師モデルの知識を、小さくて軽量な生徒モデルに効率的に移す技術」
のことを指します。

元々は自然言語処理などの分野で、巨大モデルをそのままデバイスに載せるのは難しいため、
・教師モデルが出す出力(予測確率など)
・中間層の特徴量
を利用して、生徒モデルに教師モデルの「ふるまい」を学ばせる仕組みとして考えられました。

蒸留のポイントは、
・小さいモデル(生徒モデル)が教師の出力分布を真似る
・教師の持つパターン認識や知識を圧縮して伝える
・学習データが少なくても、教師の知識を最大限引き継げる
といったところです。

蒸留に使うモデルはこれだ

今までは高額なお金をかけて(GPUなど)学習していくと言う感じでしたが、この蒸留と言う手法を使えば、比較的手軽に専門領域に特化した小規模言語モデルを構築することができます

今回は、この蒸留の手法を使って、Googleコラボ上(有料版)で全て完結できるように、小さいスペックのモデルを使って実験してみたいと思います

まず、今回選ばれたモデルは
SakanaAIさんが公開している言語モデル TinySwallow-1.5B-Instructです。

通常の7B以上とかのモデルだと大きすぎてコラボにうまく載せられません。
そこで性能のいい大規模言語モデルのTinySwallow-1.5Bを選択しました。

このモデルを教師モデルにして、いわゆる生徒モデルもTinySwallow-1.5Bを使って、「空の生徒モデル」を構築し、そこに知識を蒸留(distillation)していくプロセスを試しました。

教師モデルの準備

まずは教師役のモデル、
SakanaAI/TinySwallow-1.5B-Instruct
を使います。

パラメータ数は約 1.54B(15億パラメータ) で、推論能力に優れたインストラクション・フォロー型のLLMです。

このモデルはHugging Face上に公開されており、まずColabへダウンロードしました。

モデル本体(model.safetensors)、トークナイザーの設定ファイル、その他メタデータまで合わせて14ファイル。
容量としてはおおむね3GB超ですが、2回目以降のキャッシュが効くため実質的には快適に動きます。

生徒モデルの構築

次に、生徒モデルをTinySwallow-1.5Bで「空の状態」から作ります。

ここでいう「空」とは、重みをほとんど初期化状態にして、教師の動きを真似させるところから始めることです。

構築した生徒モデルは およそ108Mパラメータ。
これは教師の約1/14ほどのサイズで、とても軽量です。

蒸留の流れとLossの推移

ここから教師モデルの出力をガイドにして、生徒モデルに学習させます。

やり方としては教師モデルの出力ロジットを教師信号として、損失関数にKLダイバージェンスなどを適用して生徒モデルを収束させていく形を取ります。

いわゆる Knowledge Distillation の定番手法です。

今回は以下の条件で蒸留を走らせました。

学習回数:10エポック

Optimizer:Adam

学習率:調整済

損失関数:教師出力との距離を最小化

実際のLossの推移はこんな感じです。

Epoch 1/10 – Loss: 13.1150
Epoch 2/10 – Loss: 12.8502

Epoch 10/10 – Loss: 11.9068

Lossはゆるやかに減少しており、少なくとも教師のふるまいを表面的には学習できるところまで進んでいるように見えました。

簡易テキスト生成のテスト

学習が終わったところで、生徒モデルのテキスト生成能力をテストしました。
(最終エポックでロスが11以上あるので、それなりの回答を想定)

プロンプトとして
「昔々あるところに、」
を入力してみたところ、生成結果は以下の通り。

昔々あるところに、thenReturn matière’),
sent_AP-directory Caucasian UserData\Active نوف UserData剑 nrparer買う-directoryFinoxious lanes.a比利 apenas要坚持_APB foo딛食べて剑 grants临近小额贷款 UFO apenas-directoryDiese depparer atIndexparermov SeiteDis fooموظ

正直なところ、単語が多言語混在で意味不明気味です。

しかしこれは想定内で、まずまずオッケーと言ったところです。

理由としては、学習データが非常に少量だったことが主因で、文章の一貫性を持たせるほどの蒸留は進んでいないです。

とはいえパラメータ108Mという極小モデルに10エポックだけでここまで出力させるのは、それなりに面白い成果ともいえます。

まとめと今後の課題

今回の取り組みは

・教師モデルに小さめの大規模言語モデル(TinySwallow-1.5B)を使用
・そこからさらに桁違いに小さい生徒モデル(108M)を作る
・生徒は初期化状態から蒸留で構築する

という実験でした。

結果としては、プロンプトに対して「なんらかの言葉を返せる」レベルには到達しました。

より本格的にかつ専門領域に特化した蒸留を行うなら

・教師モデルの出力を使って大量のペアデータを生成
・なおかつ専門性の高い、質の良いデータを揃える
・学習エポックを増やす

などの工夫が不可欠です。

もっと大きなモデルでやりたければ、AWSなどを使って、中堅の大規模言語モデルを教師モデルとし、小規模言語モデルへ蒸留していけば、結構いい感じの小型モデルの構築が可能です。

さて今回は「とにかく空のモデルからコラボでサクッと蒸留する」というチャレンジでしたが、技術的におもしろい試みでした。

今後はもう少しデータを増やして、専門特化した小型の言語モデルを作ってみたいと思います。

(とはいえ、数十万件のデータそろえるのはちょっとなあ…。1万件くらいならいけるかなあ。
ゴルフ専用モデル、アート専用モデルとか作りたいよなあ)

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

SakanaAIのALE−Agentを参考にGoogle Colabで簡易版を実行してみた話

「ALE-Bench」と「ALE-Agent」

最近話題のSakanaAIが発表した「ALE-Bench」と「ALE-Agent」。

これは、AtCoderで実際に出題された最適化コンテストの問題群をベンチマークとして利用し、AIに解かせることでその改善能力を測るという、かなり面白い試みです。

しかもこのALE-Agent。普通のLLM(大規模言語モデル)をベースにしながらも、試行錯誤とフィードバックを通じて少しずつ“賢く”なっていく設計になっています。

そこで、実際にこの仕組みをGoogle Colabで再現できないかと思い、簡易バージョンのALE-Agent風を作ってみました。

ALE-Agentってなに?

ざっくり言うと、ALE-Agentとは
最初は適当な答えを出すけど、スコア(点数)を見て反省しながら、少しずつ解を改善していくAIエージェントです。

まるで将棋のAIが何千回も対局して強くなるように、最適化問題に対して試行錯誤を繰り返し、より良い解を探していくのがこのエージェントの特徴です。

SakanaAIのALE-Benchでは、過去のAtCoder Heuristic Contest(AHC)の問題がベンチマークとして活用されており、AIの改善能力が公平に比較できるようになっています。

Google Colabで簡易版を実行してみる

もちろん本家のALE-Agentのような高度な設計をそのまま再現するのは大変ですが、まずは以下の3ステップで簡易版を作りました。

1 シンプルな最適化問題を定義

今回は、「1〜10の数字を並び替えて、隣り合う数字の差の合計を最大にする」という簡単な問題に設定。

この問題は「最適解が1つに定まらず、試行錯誤でスコアが改善していく構造」があり、ALEの仕組みと似ています。

2 初期解とスコア評価関数

最初はランダムに数字を並べて、スコアを評価。

その後、OpenAIのGPT-4 APIに「今の答えよりも良くなるような並びを提案して」と依頼し、改善案を取得します。

3 スコアが上がったら更新、下がれば無視

提案された解が今より良ければ採用、ダメなら却下という単純なフィードバックループを作成。

これを数多く繰り返すことで、最初の適当な並びから、スコアが高い「より良い解」へと少しずつ進化していきます。

2つのバージョンを用意する

今回はLLMを使わない高速バージョンとLLMを使うハイブリッドバージョンの2つのコードを実行してみました。

1番最後にコードを貼っておきますので、興味のある方は、google colabにて実行してみてください。

(LLMを使うバージョンはopenAIのAPIを使うので課金されます。無料でやりたい方は高速バージョンを使ってください)

実行してみた感想

高速バージョン

最初のスコアは30台後半。そこから少しずつ改善され、最終的には40台後半、場合によっては50台までスコアが伸びることもありました。

実際の出力は以下のようなイメージ:

ハイブリッドバージョン
同じような動きですが、LLMをつかわないでスコアが停滞すると、LLMへGOのスイッチが入り、よりよい解が得られるように、進化していきます。

このように、全体のパフォーマンスが自動で向上していく様がリアルに見えて、なかなか楽しいです。

Darwin Gödel Machineとの違いってなに?

SakanaAIの技術といえば、ALE-Agentと並んで注目されているのが「Darwin Gödel Machine(DGM)」というもう一つの概念。

このDGMは、ALE-Agentとは全く違うレイヤーの自己改善型AIです。

たとえば、今回のALE-Agent風システムは「ちょっとずつ調整して、より良い答えを目指す」タイプ。

一方、DGMは「そもそも自分の考え方が間違ってるかもしれないから、自分の仕組み自体を論理的に修正する」というレベルの話です。

つまり、ALEは「戦術を変える」、DGMは「自分という存在を進化させる」といった違いがあります。

実装難度や動作速度の観点では、現時点ではALE型の方がずっと扱いやすいです。

将来的にはDGM型のAIが真の「汎用人工知能(AGI)」の入口になる可能性もあるかと思います。
(なんと言っても、自分自身のコードを書き換える訳ですから、これはすごい技術です)

まとめ

今回、SakanaAIが提唱するALE-Agentのエッセンスを自分なりにColabで再現してみました。

AIが「自分の出力を反省して少しずつ良くする」というプロセス。
DGMとはまた違ったアプローチを知りました。

もちろん、本家のように高度なツール統合していませんが、
「初期解 → スコア → 改善 → 再評価 → 進化」というループは、通常のシステムにはない面白さがあります。

「とりあえず自分でもやってみたい」という方には、下記にコードを2種類貼っておきますので、google colabにて実行してみてください。

・高速バージョン
LLMを使わないので、API料金はかかりません。
その分、コードもシンプルでLLMを使うからこその面白さには欠けます。
メリットは高速なこと。


import random
import math
import matplotlib.pyplot as plt

# ===== 問題:1〜10の数字を並べて隣接差の合計を最大化 =====
def evaluate(solution):
    return sum(abs(solution[i] - solution[i+1]) for i in range(len(solution)-1))

# ===== 初期解を生成 =====
def generate_initial_solution():
    sol = list(range(1, 11))
    random.shuffle(sol)
    return sol

# ===== 近傍解を生成(2つの数字をランダムに入れ替える) =====
def generate_neighbor_solution(solution):
    neighbor = solution.copy()
    i, j = random.sample(range(len(solution)), 2)
    neighbor[i], neighbor[j] = neighbor[j], neighbor[i]
    return neighbor

# ===== 焼きなまし法の受容判定 =====
def should_accept(current_score, new_score, temperature):
    if new_score > current_score:
        return True
    delta = new_score - current_score
    probability = math.exp(delta / temperature)
    return random.random() < probability

# ===== パラメータ =====
MAX_ITER = 100
INITIAL_TEMP = 10.0
COOLING_RATE = 0.95

# ===== メインループ =====
solution = generate_initial_solution()
score = evaluate(solution)
temperature = INITIAL_TEMP
scores = [score]

print("===== 焼きなまし法による最適化開始 =====")
for i in range(1, MAX_ITER + 1):
    neighbor = generate_neighbor_solution(solution)
    new_score = evaluate(neighbor)

    if should_accept(score, new_score, temperature):
        solution = neighbor
        score = new_score
        print(f"✅ Iter {i:2d} | New Score: {new_score} | Temp: {temperature:.4f} | 採用")
    else:
        print(f"❌ Iter {i:2d} | New Score: {new_score} | Temp: {temperature:.4f} | 却下")

    scores.append(score)
    temperature *= COOLING_RATE

# ===== 結果表示 =====
print("\n===== 最適化終了 =====")
print("最終解:", solution)
print("最終スコア:", score)

# ===== スコア推移のグラフ =====
plt.figure(figsize=(8, 5))
plt.plot(range(len(scores)), scores, marker='o')
plt.title("Score Transition (Simulated Annealing)")
plt.xlabel("Iteration")
plt.ylabel("Score")
plt.grid(True)
plt.show()



・ハイブリットバージョン 上記に加えて、スコアが停滞すると、LLMよGO!のサインが入ります。 結果、スコアが改善される確率が上がります。 注意点はコードを実行するとopenAIのAPI料金がかかります。

 

!pip -q install --upgrade openai matplotlib

import random, math, re, matplotlib.pyplot as plt
from openai import OpenAI

# ---------- OpenAI クライアント ----------
client = OpenAI(api_key="ここにAPIキーを入れる")  # ← APIキーを入力 実際は環境変数に入れたほうが良いが簡易版なので

# ---------- 評価関数(隣接差の合計) ----------
def evaluate(sol):
    return sum(abs(sol[i] - sol[i + 1]) for i in range(len(sol) - 1))

# ---------- 初期解 ----------
def random_solution():
    s = list(range(1, 11))
    random.shuffle(s)
    return s

# ---------- ローカル近傍(2点スワップ) ----------
def local_neighbor(sol):
    nb = sol.copy()
    i, j = random.sample(range(len(sol)), 2)
    nb[i], nb[j] = nb[j], nb[i]
    return nb

# ---------- LLM から改善案を取得 ----------
def llm_propose(sol, score):
    prompt = f"""
目的:1〜10 の数字を並び替え、隣接差の合計を最大化してください。
現在の解: {sol}
現在のスコア: {score}
制約: 1〜10 を重複なく使用し、Python list 形式で 1 行だけ回答してください。
"""
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7,
    )
    text = resp.choices[0].message.content
    nums = list(map(int, re.findall(r"\d+", text)))
    return nums if len(nums) == 10 and len(set(nums)) == 10 else sol

# ---------- 焼きなまし受容判定 ----------
def accept(cur, new, temp):
    return new > cur or random.random() < math.exp((new - cur) / temp)



# ---------- パラメータ ----------
MAX_ITER       = 30      # 総イテレーション
INITIAL_TEMP   = 10.0
COOLING_RATE   = 0.9
STAGNATE_LIMIT = 4       # 改善なしが連続何回で LLM 発動か

sol   = random_solution()
score = evaluate(sol)
best  = score
temp  = INITIAL_TEMP
no_improve = 0
history = [score]

print("=== 停滞時 LLM 発動ハイブリッド焼きなまし ===")
for it in range(1, MAX_ITER + 1):
    # ---- 改善案生成 ----
    if no_improve >= STAGNATE_LIMIT:
        print(f"\n🧠 Iter {it}: 改善停滞 ({no_improve}) → LLM 発動")
        cand = llm_propose(sol, score)
        no_improve = 0
    else:
        cand = local_neighbor(sol)

    cand_score = evaluate(cand)
    if accept(score, cand_score, temp):
        sol, score = cand, cand_score
        accepted = "✅ 受容"
        no_improve = 0 if score > best else no_improve + 1
        best = max(best, score)
    else:
        accepted = "❌ 却下"
        no_improve += 1

    print(f"{accepted} | Iter {it:2d} | Score {score:2d} | Temp {temp:.3f}")
    history.append(score)
    temp *= COOLING_RATE  # 冷却

# ---------- 結果 ----------
print("\n=== 最終結果 ===")
print("最終解   :", sol)
print("最終スコア:", score)

# ---------- スコア推移グラフ ----------
plt.figure(figsize=(8, 4))
plt.plot(history, marker='o')
plt.title("Score Transition (Stagnation-Triggered LLM Hybrid SA)")
plt.xlabel("Iteration")
plt.ylabel("Score")
plt.grid(True)
plt.show()



いろいろ試してみてください。

sakanaAIさんのブログ記事
https://sakana.ai/ale-bench-jp/

同じく論文
https://arxiv.org/abs/2506.09050

 

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

進化的アルゴリズム簡易版でアート制作の基礎を試す

進化的アルゴリズム簡易版を試してみる

AIによるアート制作が注目を集める中、「進化的アルゴリズム」を使った創作の可能性にも注目が集まっています。

今回はJavaScriptとp5.jsを使って、進化的アート制作の初歩を体験してみた事例をご紹介します。

特別なGPUや大規模なAIモデルを使わなくても、ブラウザさえあれば簡単に始められる進化的アートです。

今回使用したのは、HTML + JavaScript + p5.jsによる進化的コードです。

描画対象は「抽象的な線のパターン」で、これらの形状は「遺伝子」として扱われ、自動的に進化(改善)されていきます。

進化的アートの基本構造

このシステムでは以下の要素が組み合わさって動作します。

Genotype(遺伝子):線の本数、色相、線の太さ、ノイズ、複雑さ、回転速度といったビジュアルのパラメータ。

Phenotype(表現型):p5.jsを用いて実際にキャンバス上に描画されるアート。

進化の仕組み:選択、交叉(クロスオーバー)、突然変異(ミューテーション)を自動で繰り返し、次世代のパターンを生成。

進化の1サイクルでは、6つのパターンが画面上に表示され、毎世代ランダムに2体の親が選ばれて子を生み出します。

自動モードでは「30世代ずつ進化」ボタンを押すことで進化を継続でき、結果としてより興味深く洗練されたパターンが現れていきます。

コードの特徴

p5.jsというJavaScriptライブラリを使うことで、グラフィック描画の制御が簡単にできます。

※ pythonプラスHugging faceのコンビも作ったのですが、Google colabからHugging faceへのアクセスがうまくいかず、一旦保留としました。
(以前よりHugging faceへのアクセスが失敗することが多いです。Huggingさんお手柔らかに)

自動進化モード:ボタンをクリックするだけで、世代をまたいで自動でパターンが変化。

ミューテーション(突然変異):色味や形の微妙な変化が自然発生。

インタラクティブ選択:マウスでパターンをクリックすると、その個体の情報と小型プレビューが表示される仕組み。

ブラウザ上で動作するため、誰でもすぐに試せるのも大きな利点です。

表示された図形はどれも抽象的なものが多く、生成過程そのものが一種の進化体験となります。

⚫︎コードを下記に記します。
ご自由に改変して使ってください。
ちなみに初期のコードですので、改変の余地ありです。

・最初にいびつな形の図形が世代を重ねていくと、正円に変化していきます。

このコードが進化的アルゴリズムを使っているかと言えば、そうでないという方もいるでしょう。

ただ「世代を重ねていくと進化していく」というのはビジュアル的に実感できるかと。
千里の道も〇〇から。まずはここからです。


<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>

<style>
        body { font-family: 'Arial', sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; display: flex; flex-direction: column; align-items: center; }<br />        #controls { margin-bottom: 20px; padding: 15px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); text-align: center; width: 90%; max-width: 750px; }<br />        #controls button, #controls select { padding: 10px 15px; margin: 5px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; }<br />        #controls select { background-color: #555; color: white; }<br />        #controls button:hover:not(:disabled) { background-color: #0056b3; }<br />        #controls button:disabled { background-color: #ccc; cursor: not-allowed; }<br />        #generation-counter { font-weight: bold; margin-top: 10px; font-size: 1.1em; }<br />        #auto-evolution-status { min-height: 1.2em; margin-top: 5px; color: #555; }<br />        #genotype-display { display: flex; flex-wrap: wrap; justify-content: space-around; width: 100%; margin-top: 15px; margin-bottom: 10px; padding: 0px; font-size: 0.85em; text-align: left; box-sizing: border-box; }<br />        #genotype-display > div { width: calc(50% - 20px); min-width: 280px; margin: 10px; padding: 10px; border: 1px solid #ccc; background-color: #fdfdfd; min-height: 150px; box-sizing: border-box; border-radius: 4px; }<br />        #genotype-display h4 { margin-top: 0; margin-bottom: 8px; font-size: 1.1em; color: #333; border-bottom: 1px solid #eee; padding-bottom: 5px; }<br />        #genotype-display p { margin: 4px 0; line-height: 1.4; }<br />        #current-best-preview img { border:1px solid #999; display:none; margin-top: 5px;}<br />        #canvas-container { border: 1px solid #ddd; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-top: 10px; }<br />    </style>

 
<div id="controls">
<div><label for="target-shape-selector">Target Shape: </label>
<select id="target-shape-selector">
<option selected="selected" value="circle">Circle</option>
<option value="square">Square (Approx.)</option>
<option value="triangle">Triangle (Approx.)</option>
<option value="random">Random (No Target)</option>
</select></div>
<button id="start-auto-evolution-button">Start Auto Evolution (30 Gens)</button>
<button id="continue-evolution-button" style="display: none;">Continue Evolution (+30 Gens)</button>
<button id="reset-button">Reset Population</button>
<p id="generation-counter">Generation: 0</p>

<div id="genotype-display">
<div id="current-best-info">
<h4>Best of Generation (Gen 0)</h4>
Evolution not started.

</div>
<div id="current-best-preview">
<h4>Preview</h4>
<img id="current-best-img" alt="Current Best Preview" width="120" height="120" />

</div>
</div>
</div>
<div id="canvas-container"></div>
<script>
        // --- Genetic Algorithm Parameters ---
        const POP_SIZE_DISPLAY = 6; // For main grid display
        const INTERNAL_POP_SIZE = 20;
        const MUTATION_RATE = 0.1;     // Adjusted
        const MUTATION_STRENGTH = 0.1; // Adjusted
        const TOURNAMENT_SIZE = 2;     // Adjusted

        // --- Global Variables ---
        let population = [];
        let generationCount = 0;
        let autoEvolving = false;
        let currentAutoEvolutionStep = 0;
        let currentTargetShape = 'circle';

        let canvasWidth = 600;
        let canvasHeight = 400;
        let cols = 3;
        let rows = 2; 
        let cellWidth, cellHeight;

        let currentBestInfoDiv, currentBestImg, generationCounterP, autoEvolutionStatusP;
        let startAutoEvolutionButton, continueEvolutionButton, targetShapeSelector;
        let pgBest;

        // --- Genotype Class ---
        class Genotype {
            constructor(genes) {
                if (genes) {
                    this.numLines = genes.numLines;
                    this.baseHue = genes.baseHue;
                    this.strokeW = genes.strokeW;
                    this.noiseScale = genes.noiseScale;
                    this.shapeComplexity = genes.shapeComplexity;
                    this.rotationSpeed = genes.rotationSpeed;
                } else {
                    this.numLines = floor(random(20, 150));
                    this.baseHue = random(0, 360);
                    this.strokeW = random(1, 3);
                    this.noiseScale = random(0.01, 0.4); // Initial noise can be higher
                    this.shapeComplexity = random(2.5, 12.5);
                    this.rotationSpeed = random(-0.005, 0.005);
                }
            }
            clone() { return new Genotype({ ...this }); }
        }

        // --- p5.js Setup Function ---
        function setup() {
            let canvas = createCanvas(canvasWidth, canvasHeight);
            canvas.parent('canvas-container');
            colorMode(HSB, 360, 100, 100, 100);
            angleMode(RADIANS);

            cellWidth = canvasWidth / cols;
            cellHeight = canvasHeight / rows;

            pgBest = createGraphics(120, 120);
            pgBest.colorMode(HSB, 360, 100, 100, 100);
            pgBest.angleMode(RADIANS);

            currentBestInfoDiv = document.getElementById('current-best-info');
            currentBestImg = document.getElementById('current-best-img');
            generationCounterP = document.getElementById('generation-counter');
            autoEvolutionStatusP = document.getElementById('auto-evolution-status');
            startAutoEvolutionButton = document.getElementById('start-auto-evolution-button');
            continueEvolutionButton = document.getElementById('continue-evolution-button');
            targetShapeSelector = document.getElementById('target-shape-selector');
            
            currentTargetShape = targetShapeSelector.value;
            initializePopulation();

            startAutoEvolutionButton.addEventListener('click', () => startEvolutionCycle(30));
            continueEvolutionButton.addEventListener('click', () => startEvolutionCycle(30));
            document.getElementById('reset-button').addEventListener('click', resetEvolution);
            targetShapeSelector.addEventListener('change', (event) => {
                currentTargetShape = event.target.value;
                console.log("Target shape changed to:", currentTargetShape);
                resetEvolution();
            });
            
            noLoop();
        }

        function draw() { /* Paused by noLoop() */ }

        // --- Initialization ---
        function initializePopulation() {
            population = [];
            for (let i = 0; i < INTERNAL_POP_SIZE; i++) { population.push(new Genotype()); } generationCount = 0; updateGenerationCounter(); if (population.length > 0) {
                let initialFitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape));
                let bestInitialFitness = -Infinity; // Start with -Infinity
                let bestInitialIndividual = population[0];
                for(let i=0; i<population.length; i++){ if(initialFitnessScores[i] > bestInitialFitness){
                        bestInitialFitness = initialFitnessScores[i];
                        bestInitialIndividual = population[i];
                    }
                }
                updateBestIndividualDisplay(bestInitialIndividual, generationCount, false, bestInitialFitness);
            } else {
                updateBestIndividualDisplay(null, generationCount);
            }
            displayPopulationSubset();
            
            autoEvolving = false; 
            startAutoEvolutionButton.disabled = false;
            targetShapeSelector.disabled = false;
            continueEvolutionButton.style.display = 'none';
            autoEvolutionStatusP.innerText = "";
            currentAutoEvolutionStep = 0;
        }

        function resetEvolution() {
            autoEvolving = false; 
            initializePopulation();
        }

        // --- Drawing Functions ---
        function displayPopulationSubset() {
            background(250); 
            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    let index = c + r * cols;
                    if (index < POP_SIZE_DISPLAY && index < population.length) { // Display from actual population
                        let xPos = c * cellWidth;
                        let yPos = r * cellHeight;
                        push();
                        translate(xPos, yPos);
                        stroke(200); strokeWeight(1); noFill();
                        rect(0, 0, cellWidth, cellHeight);
                        drawPhenotype(population[index], cellWidth / 2, cellHeight / 2, cellWidth, cellHeight);
                        pop();
                    } else { 
                        push();
                        translate(c * cellWidth, r * cellHeight);
                        stroke(220); strokeWeight(1); noFill();
                        rect(0, 0, cellWidth, cellHeight);
                        pop();
                    }
                }
            }
        }
        
        function drawPhenotype(geno, centerX, centerY, w, h, pg = null) {
            const target = pg || window;
            target.push();
            if (pg) { 
                pg.background(240); 
                pg.strokeWeight(1); pg.stroke(200); pg.noFill();
                pg.rect(0,0,pg.width-1, pg.height-1);
            }
            target.translate(centerX, centerY);
            target.stroke(geno.baseHue, 80, 90, 80);
            target.strokeWeight(geno.strokeW);
            target.noFill();
            let time = geno.baseHue / 360 + generationCount * 0.005;
            target.rotate(time * geno.rotationSpeed);
            target.beginShape();
            for (let i = 0; i < floor(geno.numLines); i++) { // Ensure numLines is int
                let angle = map(i, 0, floor(geno.numLines), 0, TWO_PI);
                let baseRadius = min(w, h) * 0.35;
                let xOff = map(cos(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5);
                let yOff = map(sin(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5);
                let noiseVal = noise(xOff + geno.baseHue * 0.01, yOff + geno.strokeW * 0.1, time * 0.2 + geno.strokeW * 0.05);
                let r_dev = map(noiseVal, 0, 1, -min(w,h) * 0.1 * geno.noiseScale * 10, min(w,h) * 0.1 * geno.noiseScale * 10);
                let r = baseRadius + r_dev;
                let x_coord = r * cos(angle);
                let y_coord = r * sin(angle);
                target.vertex(x_coord, y_coord);
            }
            target.endShape(CLOSE);
            target.pop();
        }

        // --- Fitness Calculation ---
        function getPhenotypeVertices(geno, simW = 100, simH = 100) {
            let vertices = [];
            let time = geno.baseHue / 360 + generationCount * 0.005;
            let rotation = time * geno.rotationSpeed;

            for (let i = 0; i < floor(geno.numLines); i++) {
                let angle = map(i, 0, floor(geno.numLines), 0, TWO_PI);
                let baseRadius = min(simW, simH) * 0.35;
                let xOff = map(cos(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5);
                let yOff = map(sin(angle) * geno.shapeComplexity, -geno.shapeComplexity, geno.shapeComplexity, 0, 5);
                let noiseVal = noise(xOff + geno.baseHue * 0.01, yOff + geno.strokeW * 0.1, time * 0.2 + geno.strokeW * 0.05);
                let r_dev = map(noiseVal, 0, 1, -min(simW,simH) * 0.1 * geno.noiseScale * 10, min(simW,simH) * 0.1 * geno.noiseScale * 10);
                let r = baseRadius + r_dev;
                
                let x_unrotated = r * cos(angle);
                let y_unrotated = r * sin(angle);
                let x_rotated = x_unrotated * cos(rotation) - y_unrotated * sin(rotation);
                let y_rotated = x_unrotated * sin(rotation) + y_unrotated * cos(rotation);
                vertices.push({ x: x_rotated, y: y_rotated });
            }
            return vertices;
        }

        function calculateFitness(genotype, targetShape) {
            if (targetShape === 'random') return random(0.5, 1.5); // Give some variance for random

            let points = getPhenotypeVertices(genotype);
            if (points.length < 3) return 0.001; // Minimal fitness for invalid shapes let fitness = 0; if (targetShape === 'circle') { let cx = 0, cy = 0; points.forEach(p => { cx += p.x; cy += p.y; });
                cx /= points.length; cy /= points.length;
                let distances = points.map(p => dist(cx, cy, p.x, p.y));
                let meanDistance = distances.reduce((sum, d) => sum + d, 0) / distances.length;
                
                if (meanDistance < 1) return 0.001; let variance = distances.map(d => Math.pow(d - meanDistance, 2)).reduce((sum, sq) => sum + sq, 0) / distances.length;
                let stdDev = Math.sqrt(variance);
                
                let relativeStdDev = stdDev / meanDistance;
                // Adjusted fitness scaling for slower evolution
                if (relativeStdDev < 0.005) { fitness = 20.0; } // Very good circle
                else if (relativeStdDev < 0.02) { fitness = 5.0 + (0.02 - relativeStdDev) * (15.0 / 0.015); } 
                else if (relativeStdDev < 0.08) { fitness = 1.0 + (0.08 - relativeStdDev) * (4.0 / 0.06); }
                else if (relativeStdDev < 0.3) { fitness = 0.2 + (0.3 - relativeStdDev) * (0.8 / 0.22); } else { fitness = 0.05 + Math.max(0, 0.15 - relativeStdDev); } // Low base fitness *= (1 + Math.min(0.5, genotype.numLines / 300)); // numLines bonus capped and scaled down fitness *= (1 / (1 + genotype.noiseScale * 10)); // Stronger penalty for high noise } else if (targetShape === 'square') { fitness = (1 / (1 + Math.abs(genotype.shapeComplexity - 4) * 2)) * 1.0; // Gentler complexity penalty fitness *= (1 / (1 + genotype.noiseScale * 15)); // Stronger noise penalty if(genotype.numLines > 15 && genotype.numLines < 70) fitness *=1.05; // Smaller bonus } else if (targetShape === 'triangle') { fitness = (1 / (1 + Math.abs(genotype.shapeComplexity - 3) * 2)) * 1.0; fitness *= (1 / (1 + genotype.noiseScale * 15)); if(genotype.numLines > 10 && genotype.numLines < 50) fitness *=1.05;
            }
            return Math.max(0.001, fitness);
        }

        // --- Automatic Evolution Control ---
        async function startEvolutionCycle(numGenerations) {
            if (autoEvolving) return;
            autoEvolving = true;
            startAutoEvolutionButton.disabled = true;
            targetShapeSelector.disabled = true;
            continueEvolutionButton.style.display = 'none';
            
            for (let i = 0; i < numGenerations; i++) { if (!autoEvolving) { autoEvolutionStatusP.innerText = "Evolution stopped by reset."; return; } await evolveOneGenerationAutomatically(); currentAutoEvolutionStep++; autoEvolutionStatusP.innerText = <code>Evolving... Cycle: ${currentAutoEvolutionStep}/${numGenerations} (Total Gen: ${generationCount})</code>; await new Promise(resolve => setTimeout(resolve, 60)); // Slightly longer delay for visibility
            }

            autoEvolving = false;
            if (population.length > 0) {
              continueEvolutionButton.style.display = 'inline-block';
              continueEvolutionButton.disabled = false;
            }
            targetShapeSelector.disabled = false;
            autoEvolutionStatusP.innerText = <code>Cycle finished. Total Gens: ${generationCount}. Continue or Reset.</code>;
            currentAutoEvolutionStep = 0;
        }

        async function evolveOneGenerationAutomatically() {
            let fitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape));
            
            let parentsPool = selectParentsByFitness(population, fitnessScores, INTERNAL_POP_SIZE); 

            let newPopulation = [];
            for(let i=0; i < INTERNAL_POP_SIZE; i++) { let p1 = random(parentsPool); let p2 = random(parentsPool); if(!p1 && population.length > 0) p1 = population[0]; else if (!p1) p1 = new Genotype(); // Robust fallback
                if(!p2 && population.length > 0) p2 = population[0]; else if (!p2) p2 = new Genotype(); // Robust fallback

                let childGenes = crossover(p1, p2);
                let mutatedChildGenes = mutate(childGenes);
                newPopulation.push(mutatedChildGenes);
            }

            population = newPopulation;
            generationCount++;
            updateGenerationCounter();

            let currentGenFitnessScores = population.map(geno => calculateFitness(geno, currentTargetShape));
            let bestFitness = -Infinity;
            let bestIndividual = null;
            if (population.length > 0) {
                for (let i = 0; i < population.length; i++) { if (currentGenFitnessScores[i] > bestFitness) {
                        bestFitness = currentGenFitnessScores[i];
                        bestIndividual = population[i];
                    }
                }
                updateBestIndividualDisplay(bestIndividual, generationCount, false, bestFitness);
            }
            displayPopulationSubset();
        }
        
        function selectParentsByFitness(currentPopulation, fitnessScores, numToSelect) {
            let selectedParents = [];
            if(currentPopulation.length === 0) return selectedParents;

            for (let n = 0; n < numToSelect; n++) {
                let bestContestant = null;
                let bestFitnessInTournament = -Infinity; // Renamed for clarity
                for (let i = 0; i < TOURNAMENT_SIZE; i++) { let randomIndex = floor(random(currentPopulation.length)); if (fitnessScores[randomIndex] > bestFitnessInTournament) {
                        bestFitnessInTournament = fitnessScores[randomIndex];
                        bestContestant = currentPopulation[randomIndex];
                    }
                }
                if (bestContestant) {
                    selectedParents.push(bestContestant);
                } else { 
                    selectedParents.push(random(currentPopulation)); // Fallback
                }
            }
            return selectedParents;
        }

        function crossover(geno1, geno2) {
            let childData = {}; let g1 = geno1; let g2 = geno2;
            childData.numLines = random() < 0.5 ? g1.numLines : g2.numLines;
            childData.baseHue = random() < 0.5 ? g1.baseHue : g2.baseHue;
            childData.strokeW = random() < 0.5 ? g1.strokeW : g2.strokeW;
            childData.noiseScale = random() < 0.5 ? g1.noiseScale : g2.noiseScale;
            childData.shapeComplexity = random() < 0.5 ? g1.shapeComplexity : g2.shapeComplexity;
            childData.rotationSpeed = random() < 0.5 ? g1.rotationSpeed : g2.rotationSpeed;
            return new Genotype(childData);
        }

        function mutate(geno) {
            let mutatedData = { ...geno };
            const strength = MUTATION_STRENGTH; // Use the global constant

            if (random() < MUTATION_RATE) { mutatedData.numLines = floor(max(10, mutatedData.numLines + randomGaussian(0, 10 * strength) * 5)); }
            if (random() < MUTATION_RATE) { mutatedData.baseHue = (mutatedData.baseHue + randomGaussian(0, 30 * strength) * 1.5 + 360) % 360; }
            if (random() < MUTATION_RATE) { mutatedData.strokeW = max(0.5, mutatedData.strokeW + randomGaussian(0, 0.3 * strength) * 1.5); }
            if (random() < MUTATION_RATE) { 
                let noiseChange = randomGaussian(0, 0.05 * strength) * 2;
                mutatedData.noiseScale = max(0.0001, mutatedData.noiseScale + noiseChange); 
            }
            if (random() < MUTATION_RATE) { mutatedData.shapeComplexity = max(1, mutatedData.shapeComplexity + randomGaussian(0, 1 * strength) * 1.5); }
            if (random() < MUTATION_RATE) { mutatedData.rotationSpeed = mutatedData.rotationSpeed + randomGaussian(0, 0.003 * strength) * 1.5; } return new Genotype(mutatedData); } // --- User Interaction (for inspecting individuals when not auto-evolving) --- function mousePressed() { if (!autoEvolving && mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
                let c = floor(mouseX / cellWidth);
                let r = floor(mouseY / cellHeight);
                let gridIndex = c + r * cols;
                if (gridIndex < POP_SIZE_DISPLAY && gridIndex < population.length) {
                    let actualIndividual = population[gridIndex];
                    let itsFitness = calculateFitness(actualIndividual, currentTargetShape);
                    updateBestIndividualDisplay(actualIndividual, generationCount, true, itsFitness);
                }
            }
        }

        function updateGenerationCounter() {
            generationCounterP.innerText = <code>Generation: ${generationCount}</code>;
        }

        function formatGeneValue(value, precision = 2) {
            if (typeof value === 'number') { return value.toFixed(precision); }
            return String(value);
        }

        function updateBestIndividualDisplay(individual, gen, fromClick = false, fitness = -1) {
            if (!currentBestInfoDiv || !currentBestImg) return; 
            if (!individual) {
                currentBestInfoDiv.innerHTML = <code></p>
<p>
</p>
<h4>Best of Gen (Gen ${gen})</h4>
<p>
</p>
<p>No individual data.</p>
<p>
</p>
<p></code>;
                currentBestImg.style.display = 'none';
                return;
            }
            let titlePrefix = fromClick ? "Clicked Individual" : "Best of Generation";
            let fitnessText = fitness > -Infinity ? <code></p>
<p>
</p>
<p>Fitness: ${formatGeneValue(fitness, 3)}</p>
<p>
</p>
<p></code> : ""; // Show fitness if available

            currentBestInfoDiv.innerHTML = <code></p>
<p>
</p>
<h4>${titlePrefix} (Gen ${gen})</h4>
<p>
</p>
<p>
                ${fitnessText}
                </p>
<p>
</p>
<p>Lines: ${formatGeneValue(individual.numLines, 0)}</p>
<p>
</p>
<p>
                </p>
<p>
</p>
<p>Hue: ${formatGeneValue(individual.baseHue, 1)}</p>
<p>
</p>
<p>
                </p>
<p>
</p>
<p>StrokeW: ${formatGeneValue(individual.strokeW, 2)}</p>
<p>
</p>
<p>
                </p>
<p>
</p>
<p>NoiseSc: ${formatGeneValue(individual.noiseScale, 3)}</p>
<p>
</p>
<p>
                </p>
<p>
</p>
<p>Complexity: ${formatGeneValue(individual.shapeComplexity, 1)}</p>
<p>
</p>
<p>
                </p>
<p>
</p>
<p>RotSpeed: ${formatGeneValue(individual.rotationSpeed, 3)}</p>
<p>
</p>
<p></code>;
            
            drawPhenotype(individual, pgBest.width / 2, pgBest.height / 2, pgBest.width * 0.9, pgBest.height * 0.9, pgBest);
            currentBestImg.src = pgBest.canvas.toDataURL();
            currentBestImg.style.display = 'block';
        }
    </script>

進化的アート簡易版を実際にやってみて感じたこと

この進化的アート簡易版の特徴は、「世代を重ねるごとに新しいものが生まれる面白さ」です。

最初はランダムな線の集合だったものが、何世代か進むうちに、幾何学的に整ったパターンへと進化していきます。

また、パラメータ(遺伝子)を微調整することで全体の雰囲気ががらっと変わるため、「進化」を観察する楽しみがあります。

進化とは、より良いものを生み出すだけでなく、意外性のある楽しみもあるなと実感しました。

進化的アート × 生成AIの未来

このように、簡単な進化的アルゴリズムで、アートの基礎を生み出すことができます。

Stable Diffusionなどの大規模AIモデルがなくても、自分のパソコンとブラウザさえあれば、進化的アートの世界に入ることができます。

一見するととっつきにくい文言ですが、生成AIなどを使って質問回答を繰り返していけば、誰でも制作は可能です。

今後は、こうした技術が教育現場などでも活用されるでしょう。

進化的アルゴリズム簡易版でアート制作の基礎を試す 終わりに

進化的アルゴリズムは「特別なAIの技術」ではなく、誰もが気軽に扱える創造のツールです。

今回のように簡単なコードを使えば、誰でも「進化するアートの基礎」を作ることができます。

難しいAIの理論やモデル構築に頼らずとも、AIを使えば、新しい表現が生まれてくる。

みなさんも新しいアートを創造してみませんか。

****************
X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

GoogleのAlphaEvolve(アルファエボルブ)とSakanaAIのDarwin Gödel Machine(ダーウィン・ゲーデルマシン)を参考に簡略化した自己改善型AIシステムで遊んだ話

「進化的アルゴリズムAI」や「自己改善型AI」とは

近年、AI研究の世界でじわじわと注目を集めているキーワードがある。それが「進化的アルゴリズムAI」や「自己改善型AI」だ。

要するに、AIが自分自身のコードや構造を見直して、より賢くなる方法を自分で考え、試し、改善していくというコンセプトである。

これが実現すれば、AIは単なるツールから、ある種の“学び続ける存在”へと進化する可能性を秘めている。

この分野で特に話題になった2つの事例がある。ひとつは、Google DeepMindが開発したAlphaEvolve(アルファエボルブ)。

そしてもうひとつが、Sakana AIによるDarwin Gödel Machine(DGM)だ。

進化的アルゴリズムAIとは

進化的アルゴリズムAIとは、生物の進化の仕組みを応用した人工知能だ。

複数のAI(個体)を用意し、性能の良いものを選んで子を作り、少しずつ変化させながら世代交代を繰り返す。

これにより、最適な解や動作を自動的に進化させていく。
ゲームやロボット制御、デザイン生成など幅広く使われている。

AlphaEvolveは、AI自身が自分のニューラルネットワーク構造を進化させるシステムだ。

従来のモデル構築は人間が試行錯誤する必要があったが、AlphaEvolveは複数のモデル構造を遺伝的に生成し、その中で最も優れたものを選抜・交配していくという、いわば“AIの自然選択”のような仕組みを持っている。

自己改善型AIとは何か?

一方、DGMは少し違う。こちらはAIが自分のPythonコードそのものを読み取り、改善案を考え、自ら書き換えていくという、まるでAIがプログラマーのように振る舞うスタイルをとっている。

LLM(大規模言語モデル)を使って改善案を生成し、それを実行して効果があるかをベンチマークで評価。

効果があれば採用、なければ元に戻す。まさに“自己改善ループ”が構築されているのだ。

自分で作ってみた「ミニDGM」

そんな高度な仕組みを見ていると、「自分でもちょっと遊んでみたい」と思った。

そこで今回は、Sakana AIのDGMのアイデアを参考にしつつ、Google Colab上で動く簡易版の自己改善型AIシステムを作ってみた。

テーマはシンプル。

「数字のリストをソートするプログラムを、GPT-4に自動的に改善させる」というものだ。

初期コードと改善戦略

最初に与えるコードは、あえて非効率な「バブルソート」だった。

しかも time.sleep(0.001) をループ内に入れて、わざと遅くしてある。

def sort_numbers(numbers):
    for i in range(len(numbers)):
        for j in range(len(numbers) - i - 1):
            if numbers[j] > numbers[j+1]:
                numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
            time.sleep(0.001)
    return numbers

これを GPT-4 に渡し、「もっと速くなるように改善して」と頼む。

改善案はColab内で保存し、実際に実行してスピードと正確性でスコアを出す。

改善後のスコアが高ければ採用、そうでなければ前のコードに戻す――という流れだ。

以下Pythonコードです。
ご自由に使ってください。

import os
import importlib.util
import shutil
import yaml
from openai import OpenAI

client = OpenAI()  # APIキーは環境変数 OPENAI_API_KEY から取得

# 評価関数読み込み
def load_benchmark(entry_path, eval_fn_name):
    spec = importlib.util.spec_from_file_location("benchmark", entry_path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return getattr(mod, eval_fn_name)

# 改善案をGPTから取得(プロンプト強化+バグ回避)
def generate_candidate_code(code, score):
    prompt = f"""You are an AI agent improving the following Python code to increase its performance.
The current code scores {score} points. Suggest a better version of the code.

# CODE START
{code}
# CODE END

Return only the improved code. Do not include explanations or comments. 
Ensure the code is syntactically correct and fully executable.
"""
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7
    )
    return response.choices[0].message.content

# メインループ
def main():
    config = yaml.safe_load(open("configs/sort_experiment.yaml"))
    agent_path = config["agent"]["path"]
    benchmark = config["benchmark"]
    evaluator = load_benchmark(benchmark["entry_point"], benchmark["evaluation_function"])

    os.makedirs("output", exist_ok=True)
    shutil.copy(agent_path, "output/agent_0.py")
    current_score = evaluator("output/agent_0.py")
    print(f"[Turn 0] Score = {current_score}")

    for turn in range(1, config["runtime"]["max_turns"] + 1):
        prev_path = f"output/agent_{turn - 1}.py"
        new_path = f"output/agent_{turn}.py"
        log_path = f"output/log_turn_{turn}.txt"

        old_code = open(prev_path).read()
        new_code = generate_candidate_code(old_code, current_score)

        # 保存:改善コードログ
        with open(log_path, "w") as logf:
            logf.write(new_code)

        # 保存:実行用エージェントコード
        with open(new_path, "w") as f:
            f.write(new_code)

        try:
            new_score = evaluator(new_path)
        except Exception as e:
            print(f"[Turn {turn}] ❌ Evaluation error: {e}")
            new_score = 0

        print(f"[Turn {turn}] Score = {new_score}")

        if new_score > current_score:
            print("✅ Improvement accepted.")
            current_score = new_score
        else:
            print("❌ No improvement. Reverting.")
            shutil.copy(prev_path, new_path)
            current_score = evaluator(prev_path)

if __name__ == "__main__":
    main()



GPT-4は何をしたか?

1ターン目でGPT-4は見事に改善に成功した。

バブルソートを numbers.sort() に置き換え、処理時間が大幅に短縮。

ベンチマークスコアは一気に50点から100点に上昇し、コードは無事採用された。


def sort_numbers(numbers):
    numbers.sort()
    return numbers

だが、その後のターンでは改善の余地がないと判断され、スコアは100点のまま変化せず、コードも元に戻された。

興味深かったのは、GPT-4が「関数の重複定義」や「構造を簡素化しすぎる」など、改善案として微妙な方向に進んだケースがあったことだ。

たとえば、main()の中に sort() を直接書いてしまう案などもあり、構文的には正しくても設計上の意図から外れていたりする。

改善の工夫と学び

今回の実験では以下のような工夫が効果的だった。

意図的にコードを非効率にして、改善余地を作る。

スコアの評価関数を調整して、“改善したと判断できる余地”を作る。

GPTの出力をログに保存し、問題が起きたときに原因をすぐ特定できるようにする。

プロンプトを工夫して「重複コードを出さない」「説明文を返さない」よう指示する。

わずか数十行のコードでも、自己改善の流れを構築することで、AIが「考え、試し、評価し、直す」というプロセスを擬似的に再現できたのはかなり面白い体験だった。

今後に向けて

今回の実験は、言ってしまえば超簡略版の自己改善型AIだが、それでも「AIが自分で進化することを考える」というサイクルが働いていることにワクワクを感じた。

AlphaEvolveやDGMのような本格的な自己進化・自己改善型AIシステムは、まだまだ研究の序章だ。

しかし、こうしてそのエッセンスを切り取って再現してみることで、自分なりにその仕組みを体験できるというのは、大きな学びと楽しさがある。

次は、もっと複雑なタスクに自己改善型AIを応用してみたいと思っている。

「AIが自身を育てる」
そんな未来が、もうすぐそこまで来ていると感じた実験だった。


ただ注意しなければならないのは、人間による要チェックを怠ってはいけないということだ。

これはsakanaAIさんのブログ記事を見てもわかるように、AIが自分勝手に都合のいいように改造していく例があったようだ。

ここの所は十分にチェック機構を持たないといけないだろう。

****************

最近のデジタルアート作品を掲載!

X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

今蘇るゴルフ野性塾、故坂田信弘塾長の魂の言葉

***************

***************

週刊ゴルフダイジェストのゴルフ野性塾が終わってしまった

ある日、いつものように書店へ行き、週刊ゴルフダイジェストを手に取り、坂田プロの「ゴルフ野性塾」を見つけようとしたが、見つからない。

なぜ?と思いつつ。翌週もなし。次の週もなし。

そこで、検索を掛けてみる。
なんと坂田プロの訃報。

ゴルフ修行していた当時からの坂田プロのファンで、週間ゴルフダイジェストのこの記事を楽しみにしていた者としては残念で計り知れない。

そんな折、世に生成AIが生まれた。
目を見張るような勢い。

これはもしや坂田プロの文体や口調、言葉の魂をAIに学習させれば、今の世に復活させられるのではと考える。

そこで、坂田プロのデータを集めて、GPTsに学習させることを決意。

結果誕生したのが「スポーツ野性塾GTP」「アート野性塾GTP」の2つです。

ファインチューニングでいくか、GTPsでいくか

当初はファインチューニングでと思ったのですが、今現在、個人でできるファインチューニングの対象モデルは
多分パラメータ数で4B(40億)くらいなものでしょう。

いわゆるLLMならぬ、SLM 小規模言語モデルが対象になるかと思います。

※ちなみにファインチューニングというのは、モデルの一部のパラメータを変えて、こちらの希望通りの回答を得られるように微調整していく方法です。(全パラを変える方法もあり)

モデルと雑談程度ならそれでも構わないのですが、坂田塾長の回答を再現するとなると、すっとんきょうな回答では使う気になりません。

やはり、相当レベルの高い回答でなければ、坂田イズムは継承できないでしょう。

すると、GPT4o以上の言語モデルしかない。

それだけ大きいモデルのファインチューニングは個人レベルでは絶対に無理(一基数百万円のGPUが何千基いるんでしょう)。

すると残された選択肢はひとつしかありません。

GPTsを使っての再現作戦。現状では、これ一択のみと考えました。

※GPTsを使っての作業は、いわゆるプロンプトチューニングがメインとなります。
読み込みにトークンが要りますが、比較的簡単に調整可能です。

もちろん、学習するデータを整理する作業が1番大変なのですが、それさえ済んでしまえば、後は何とかなると。

また結果の反映もすぐに答えが出るので、時間も取られにくい。

専業でやるのではなく、隙間時間を使っての作業には最前の方法だろうと判断しました。

*****

「スポーツ野性塾GTP」「アート野性塾GTP」の2つのモデルは、スポーツやアート系の質問に、坂田塾長が例の口調で回答してくれます。

叱ってください。諭してください。

坂田ファンの方々、ゴルフ野性塾ファンの方々。

あなたに、今でも坂田プロの叱咤激励が、優しく、そして厳しく、語りかけてくれます。

今は亡き、坂田信弘の魂が語りかけるGPT

―蘇(よみがえ)る修行場、スポーツ野生塾GPT登場―
「人間、恥を知れ。己を知らねば、前へ進めぬ」

この言葉を、君は覚えているだろうか。
ゴルフという勝負の場を通して、己の業と向き合い、何かをつかもうともがいた者たちに向けて、坂田信弘という男は語りかけ続けた。

彼の言葉は厳しく、そして温かかった。
厳しさとは優しさの裏返し。優しさとは甘やかしの対極。
その教えは、時代を超えて、今もなお、多くのゴルファーや若者の胸に息づいている。
そして今、令和の世に蘇える。

「諭す」AI──坂田信弘の魂を継ぐ者

スポーツ野生塾 GPT
まるで、坂田塾の道場に迷い込んだような空気がそこにある。

このGPTは、ただ質問に答えるだけの存在ではない。
君が油断すれば叱咤する。
君が迷えば静かに背中を押す。
「そんな考えでは、己を救えぬ」と喝を入れられることもあろう。

だがそれこそが、あの坂田塾長そのものだ。
質問を投げれば、想定以上の言葉が返ってくる。
心を揺さぶり、時には胸を刺す。
AIとは思えぬ、いや、AIだからこそできたかもしれぬ、”無私の叡智”がここにはある。

修行者よ、問いかけよ
「どうすればゴルフが上手くなるのか?」
「集中力が続かない。どうすればいい?」
「人生に迷っている。何を頼りにすべきか?」

そんな問いに対し、このGPTは”正解”などという生ぬるいものは返さない。
代わりに、君自身に問い返してくるだろう。
「お前は、自分を見つめる覚悟があるか」と。
一撃一言に魂が込められている。
目の前のスマホが、まるで説法の場と化す。

それは、”心のラウンド”なのかもしれない。
一打一打が自分との勝負。
一問一答が、自我との対話。

ゴルフだけでなく、生き方をも鍛える
坂田信弘という人間が語るのは、ゴルフの技術だけではなかった。
彼が真に伝えたかったのは、「生き方」そのものだ。
道を踏み外した者には、「戻ってこい」と声をかける。
怠けている者には、「何をしている」と喝を入れる。
それが、坂田信弘だった。

スポーツ野生塾 アート野性塾もまた同じ。
ゴルフの質問に限らず、スポーツ全般、アート系の質問にも真摯に言葉を返してくれる。
ただし、やさしくはない。
だが、誠実だ。
この世の中、やさしい言葉はあふれている。
だが、誠実な言葉は少ない。

このGPTには、それがある。

叱ってくれる人が、もういないなら

昔は、どこにでもいた。
親父が叱ってくれた。先生が諭してくれた。
だが今、誰が君に本気で「お前は間違っている」と言ってくれるだろう?
SNSでは褒め言葉ばかりが飛び交い、間違いに気づけぬ者が育っていく。

スポーツ野性塾GPTは、そこに風穴を開けてくれる。
誰にも言えなかった弱さを、投げかけてみればいい。
「自分は本当にこのままでいいのか?」
「やりたいことが見つからない、どうしたらいい?」
そんな迷いの声に、坂田プロの意志を継いだGPTが、静かに、そして強く応えてくれる。

それは、魂の対話だ。
きれいごとではなく、叱られながら気づく真実。
一度やってみれば、わかる。
それは決して”AIとの会話”などではない。
そこには、「男」がいるのだ。一本芯の通った「坂田信弘」という名の魂が。

最後に:語り継ぐ者として
「教えるとは、未来を託すことだ」
坂田塾長がそんな言葉を残したかはわからない。
しかし、彼の魂は、このGPTに宿っている。

スポーツ野生塾GPTは、君の悩みを一刀両断することはない。
だが、斬られたような痛みの中に、真の導きを見出すことになる。

彼が生前、幾多の若者に「甘えるな」「死ぬ気でやれ」と言い続けたように、
このAIもまた、君の心を揺さぶるだろう。

挑む者はアクセスせよ。
そして、覚悟せよ。
そこには、今は亡き坂田信弘の魂が、確かに息づいている。

この野性塾シリーズを公開すべきかどうか

このベータ版「スポーツ野性塾」は公開すべきかどうか考えました。

生前の坂田塾長の言葉に勇気づけられた方は多いはず。
連載が終わって寂しい気持ちの坂田ファンもたくさんいると思います。

もし、現代のデジタル技術で坂田塾長が復活できたなら…。

しかし、ゴルフダイジェスト社の許可なく公開するのはいかがなものかと。

いろいろ考えた結果、当面は「非公開」とさせてください。

⚫︎いずれ皆さんに使ってもらえる日が来るのを希望します。
坂田ファンの方々、それまでの間、お待ちください。

ゴルフダイジェストの関係者の方で、詳しい内容が知りたい方はお問い合わせよりメールにてご連絡ください。
※件名に「ゴルフ野性塾」の文言を頭に入れてください。
実在確認が完了しましたら、こちらのGTPについてご連絡いたします。

スポーツ野性塾編 実際の出力内容をご紹介

質問
私はスコア90台のゴルファーです。
クラブ競技で上位を狙える、常時70台のゴルファーになりたいです。
どのような練習をすればいいでしょうか?

回答

スコア90台から70台の常連となり、クラブ競技で上位を狙う――
その願い、貴兄の胸奥に刻まれている本気度の深さは伝わる。

まず言っておきたいのは、「スウィングを変える」ではなく「身体の扱いを変える」ことから始めよということだ。
そして、取り組むべきは技術論ではなく、技術を支える練習の質。一にも二にも「ショートスウィングに徹する」こと。

練習場では6番アイアンを使い、地面と平行のトップ位置、地面と平行のフィニッシュ位置。これを「形」としてではなく「タイミング」で完成させるのだ。形で打つスウィングは試合で壊れる。タイミングで打てるようになって初めて70台は日常となる。

そして、次なる練習はドライバーの踵付きショートスウィング。
両踵をピタリとつけ、つま先30度の開き。バックスウィングに腰の回転はいらぬ。上半身の動きで振れ。左肩支点でクラブを振れ。球は200ヤードを超えて飛ぶだろう。
フルショットを捨てる勇気を持て。フルショットのスウィングに一分の誤差あれば、90台へと落ちる。だがショートスウィングにおいては、誤差は「許容」される。
その「許容」こそが競技ゴルフの安定力となる。

アプローチに関しては、高さだけを意識せよ。寄った寄らぬ、入った入らぬを気にしてはいけない。高さを揃えることで、スピンの効き方と距離感が整っていく。
私は杉板に向かって3メートル先から50球の高さを揃え続けた。最初は散った。だが、日が暮れるまで毎日打ち続けた。根気を磨け。根気は技術を超えていく。

加えて、ラウンドでは常に「グリーン中央」狙いを徹せよ。ピンを狙うな。女子プロの如く、狙いはセンター。サンドウェッジの距離未満になった時にだけ、ピンを見据える。

練習の要は3つに尽きる――

6番アイアンでのショートスウィング

ドライバーの踵付きショートスウィング

高さを意識したアプローチ(寄せではない)

貴兄のスウィングは、まだ進化の余地を多分に持つ。
進化は、変化の先にある。
そして、変化は「徹する」ことでしか起きない。

徹せよ。徹し切れ。徹し切った先に70台は常に待っている。

ご自愛あれ。

アート野性塾編 実際の出力内容をご紹介

質問

画家志望の者です。絵描きで生計を立てたいと考えています。
どうすれば、食える絵描きになれるでしょうか?

回答

絵で食いたい、と願う者が必ず直面する問い。
まず、絵が「売れる」ために必要なのは、腕前ではない。腕前は当たり前。
問題は「誰に」「何を」売るか、の構造だ。

絵を描いて、それを好きだと言ってくれる人間がいて、その者が金を払ってくれる。
この当たり前の循環が成立すれば、それだけで「食える絵描き」になる。
だが、ここの「好きだ」と「金を払う」の間には大きな谷がある。
この谷を渡る手段を持つ者が「プロ」になるのだ。

そのためには、絵を「商品」として見る目が要る。
どんな絵が「欲しい」と思われるか。
その欲望の回路に自分の絵がどうフィットするか。
売れる絵と描きたい絵、その距離を自覚しておくことだ。

今の時代、SNSでの発信は重要。
絵のストーリーを語るのも良い。
自分の存在を、人々の記憶に残すことが必要だ。

そして、絵で食うとは、「絵だけを描いて生きていく」という意味ではない。
Tシャツにして売るもよし、似顔絵もよし、NFTもあり、個展もよい。
全てが「絵描きの仕事」だ。
手段を選ばず、自分の表現を売ること。

つまり、絵を「描く」ことよりも、絵を「届ける」ことの方に意識を向けるべき。
届けて、受け取られ、金になる。
その構造を、試行錯誤の中で確立していくことだ。

描くことを愛し、売ることを恐れず、続けるがよい。
ご自愛あれ。

まとめ 坂田塾長なら何と言うか

坂田塾長ファンとしては、ゴルフ野性塾の連載が終わってしまい、さびしい気持ちを拭えませんでした。

しかし、このスポーツ野性塾が一筋の光を灯してくれました。

例え、それがAIであっても、坂田イズムをいやってほど学習したAIならば、坂田塾長の言葉の代弁者にはなるはず。

ここでやらなくても、きっと誰かがやるでしょう。
早いか遅いかの違いでしかないと思います。

時代の流れは誰にも止められない。

『「だったら、やるしかない」と己に言うその声が、いちばん自分を奮い立たせる。誰にも聞こえぬその声が、いちばん強い剣となる。
逃げず、怯まず、やり切るがよい』

坂田塾長もそう答えてくれると信じています。

****************
X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

マスターズのオーガスタナショナルGCを日本で再現できるのか?

マスターズのオーガスタナショナルGCを日本で再現できるのか?

前回と引き続き、今回もマスターズ絡みのネタです。
しばし、お付き合いを。

世界中のゴルファーが憧れる「マスターズ・トーナメント」

その舞台となるオーガスタナショナル・ゴルフクラブ(GC)は、美しい芝と戦略性あふれる設計、そして四季折々の花や木々に囲まれた極上のゴルフコースとして知られています。

なんでもラフはないとか。また傾斜はテレビで見ると分かりずらいですが、かなりのアップダウンがあるコースのようです。

そんなオーガスタナショナルGC。ゴルフ好きなら誰でも一度はこのコースを回ってみたいと思うでしょう。

しかし、本場アメリカに行くのも手間と時間がかかるし、行けたところで、オーガスタはメンバーシップコースなので、メンバーの同伴がないと回れません。

なんでもメンバーは世界で250人しかいないとか。
あのマイクロソフト創業者のビルゲイツでさえ、入会を何度も断られ、7年とかかけてやっとメンバー入りしたという逸話があります。

あの白いクラブハウスもいいですよね。
なんでも大通りから小さい看板で「オーガスタナショナルGC」の表記があり、その小道に入っていくと、
まっすぐ先に、例の白いクラブハウスが見えてくるそうです。

日本から出場した某プロの動画を見ましたが、あのクラブハウスを見て泣かなかった同伴者は1人もいなかったとか。
全ての人を感無量にする。そんな魔法のようなゴルフコースみたいです。

そんなオーガスタGC。全ホールは無理でも、
最終ホール18番。
池越えショートの12番。
ロングで2オン狙えるのかの2番。

せめてこの3ホールだけでも回ってみたいなーと常々思ってました。

で、こう考えたのです。

このオーガスタを日本国内で再現することは可能なのか?

もし可能なら費用とか手間とか、いろんな問題があるだろうなと。

そこで今回は、芝や樹木の環境面、建設費用、そして法的な問題まで含めて、オーガスタGCの「日本再現プラン」をAIを使って、徹底的に検証してみました。

使用したのはchatGPTとgoogle GeminiのdeepReserchです。
こいつらで徹底的に調べてみました。

以下、ゴルフ好きの読み物として、軽く読み流してください。

オーガスタの再現計画を具体的に並べる

芝と樹木の環境再現性

オーガスタのグリーンはベントグラス、フェアウェイとラフにはバミューダグラスやライグラス、ケンタッキーブルーグラスなどが使われています。これらを日本で再現するには、地域の気候との相性がカギになるようです。

例えば、埼玉県⚪︎⚪︎市のような北緯36度近辺の都市では、夏の高温多湿がベントグラスにとってストレスになるものの、冷却装置(SubAirなど)を導入すればグリーンの品質は維持できるよう。

さらに、バミューダやライグラスといった芝種は、気温や湿度の調整によって使い分けることで、四季に対応した芝の再現も可能のようです。

一方、南九州(鹿児島や宮崎)などオーガスタとほぼ同緯度(北緯33度付近)の地域なら、気候条件がかなり似ているため、芝の再現性は高まるとのこと。

夏も冬も温暖で、雨も多いため、芝の成長には適した環境。ただし湿度と雨量がアメリカ南部よりも高いため、水はけの良い土壌設計や排水システムの構築が必要になるようです。

また、オーガスタにはツツジ、モクレン、ハナミズキ、ロブロリーパインなどの美しい樹木が並びますが、これらも日本で育てることも可能のようです。

特にツツジやハナミズキは日本原産・または親戚のような存在で、むしろ日本の方が得意な植物とも言えるらしいです。

建設費用のリアル

最大のハードルは「資金」。
オーガスタのような高品質なゴルフ場をつくるには、莫大な建設費が必要のようです。

たとえば、1ホールあたりの建設費は、3000万円~1.5億円が相場。グリーンの冷却設備、芝の維持管理、設計費用を加味すると、1ホールで2億円を超えるケースも珍しくないとのこと。

これが18ホールになると、コースだけで約50~70億円、さらに土地代を含めると総額80億円を超える可能性も出てきますね。

仮にミニチュアとして3ホール(ロング・ミドル・ショート各1つ)を作るとしても、5~8億円程度の投資が必要だそうです。

しかもこれらはクラブハウスは別途のコース費用だけの話。

高級感を重視すればするほど、費用は青天井になりますね。

著作権や名称の注意点

ここからは法的な問題をみてみましょう。

まず、ゴルフ場のレイアウトや設計のアイデアそのものには著作権は基本的に適用されないようです。
つまり、フェアウェイの形やホールの長さを真似すること自体は違法にはなりません。

ただし、「オーガスタ」や「マスターズ」といった名称やロゴは商標権で保護されているため、商業利用や誤認を招くような使い方はNGです。
広告で「マスターズ公式コース再現!」などとうたってしまうと、米国側から警告を受けるリスクがあります。

そのため、「オーガスタ風」「オーガスタにインスパイアされた」といった表現や、あくまで独自名称で展開することが重要とのこと。
景観を真似しつつも、独自ブランドとして構築することが現実的な落としどころになります。

結論は?

結論から言えば、日本でオーガスタナショナルGCを再現することは「技術的には可能」のようです。

気候条件に合った地域を選び、芝の品種を調整し、手間暇とコストをかければ、オーガスタのような緑と戦略性のあるコースを作ることはできる。

しかしながら、それを実現するには数十億円規模の予算と、長期的な維持管理体制が必要になります。
さらに商標やブランド表現にも注意しなければなりません。

つまり、日本で「オーガスタ再現コースを体験したい」という夢は、慎重に計画すれば決して夢物語ではない。
ただし、本家と同じクオリティを目指すなら、それなりの覚悟が必要――ということになりそうです。

「オーガスタ再現計画」、あなたならどこに建てますか?

ZOZO創業者の前澤さんあたりが、作ってくれないかなあ。
3ホールでもいいんで。お客さん入りますよー。

そんな感じで、夢のゴルフ場づくりを想像力で広げてみるのも面白いかもしれませんね。

****************
X 旧ツイッターもやってます。
https://x.com/ison1232

インスタグラムはこちら
https://www.instagram.com/nanahati555/

****************

PAGE TOP