********
※LINE対応チャットボット版の
「LINEチャットボット屋」
いろんなチャットボットがあります。
ぜひ、ご覧ください!
***************

***************
モデルマージで2つのAIをひとつに
LLMを使った実験を続けていると、「異なるモデルを混ぜたらどうなるのか?」という素朴な疑問が浮かびます。
今回の実験はまさにその疑問から始まりました。
使用したのは、日本語系の小型LLMである
rinna/japanese-gpt2-smallと
cyberagent/open-calm-smallです。
古いモデルですが、Googleコラボで2つのモデルを実装するので、メモリの関係上仕方ないですね。
どちらも自然な日本語生成に定評のあるモデルで、今回はこれを“モデルマージ”によって融合してみました。
モデルマージとは、簡単に言えば2つのモデルの重み(パラメータ)を混ぜ合わせる技術です。
ちょうど遺伝子を掛け合わせるようなもので、「Aモデルの文体」と「Bモデルの知識」をほどよくブレンドできる可能性があります。
ただし、このマージは「掛け合わせて終わり」ではありません。
組み合わせ方によっては性能が落ちることもあり、偶然うまく混ざるケースもあります。
そんな“相性実験”を経て、1体の融合モデルが誕生しました。
通常はここでハイおしまいなのですが、これだけではつまらないなと。
じゃあ、このモデルをさらに進化アルゴリズムで進化させてみようと単純に思いました。
まさに知的好奇心です。
どうなるのか、知りたいですよね。
えっ、興味ない。
そうですか。私的にはめっちゃ面白い題材なんですが。
そんなこんなで、やってみますかと。
進化アルゴリズムでLoRAを育てる
続けて実験したのが、進化アルゴリズムを使ったLoRA(Low-Rank Adapter)層の進化実験です。
LoRAは、モデルの一部だけを学習させる軽量チューニング手法です。
このLoRAの重みを「遺伝子」とみなし、進化アルゴリズム(GA)で世代交代を繰り返すという計画を立てました。
初期状態では10個のLoRA個体(=微妙に異なる重みセット)を作成しました。
それぞれに同じプロンプトを与えて生成させ、「文の自然さ」「正しい表記」「冗長さの少なさ」をスコア化しました。
スコアの高い個体ほど“適応度”が高いと判断し、上位個体を親として交叉・突然変異を行い、次世代を生み出す。
これを20世代繰り返しました。
設定は以下の通りです。
個体数:10
世代数:20
交叉率:0.5
突然変異率:0.3
AIがAIを選び、より自然に進化していく。
このプロセスをColab上でリアルタイムに見られるのは、なかなか面白かったです。
余談 ドラマのサラブレッドに思うこと
話は変わりますが、先日日曜日の午後9時からのドラマでロイヤルファミリーと言う競馬を題材にしたドラマが始まりました。
非常に面白く、普段はドラマを見ないのですが、最後まで見入ってしまいました。
ドラマの中で出たのは、サラブレッドと言うのは遺伝子をたどっていくと、最終的にたったの3頭に集約されるそうです。
当然ながら、いろんなサラブレッドが交配を重ねてきたでしょうけど、結果として競走馬としての実力を兼ね備えた遺伝子をたどると、その上位3頭になったのでしょう。
つまり、他の馬たちは競走馬としては淘汰されてしまったと言う話ですね。
AIの世界でも、当然この進化アルゴリズムは通用するわけで、サラブレッドの交配は大変ですが、AIの進化はパソコンの中で作業としては完結できる。しかも自動化です。
これはやってみるに値する作業でしょう。
20世代の進化ログを眺めてみる
進化の過程は、ログとして次のように出力されました。
Gen 00 | best=1.600 avg=1.187
Gen 13 | best=2.000 avg=1.273
Gen 19 | best=1.600 avg=1.233
Evolution done. Elapsed ~18.5 min, best=2.000
bestはその世代で最も良かった個体のスコア、
avgは個体群の平均スコアを意味します。
この数字が1.0〜2.0の間で推移し、13世代目で最高値の「2.000」を記録しました。

進化アルゴリズムらしく、初期はバラバラな出力が多かったのですが、世代を重ねるにつれて平均スコアが安定してきました。
言い回しの崩れが減り、より自然な文体に近づいていくのがログからも見て取れました。
これまた、なかなか面白い様ですね。
実際の出力を見てみよう
最終的に得られた進化済みLoRAを使って生成した出力がこちらです。
紅茶の例
> 生徒から「何でもやってみたけど上手くいかなかった」という声をよく聞きます。では、どうして上手くいかなくなったのか?実は、多くの人がやっていることが間違っているのです。私はこの3つに気をつけています。まずは、茶葉を煎りたてのものにすることです。
コーヒーの例
> 豆に水を加えることで、豆の風味を引き立てることが出来ます。また、お湯を入れることで抽出したコーヒーに含まれる糖分も引き出してくれます。ミルクを加えると味がまろやかになり、栄養価もアップします。
どちらも文法の崩れが少なく、自然な語彙選択ができています。
ただし、「箇条書き」や「番号付き3行」といった構造的指示はまだ弱く、流暢さ優先の傾向が残っています。
とりあえず、この段階でここまで整った出力はよしとしましょう。
次の一歩は構造理解を学ぶ進化へ
今回の進化では「自然さ」と「表記の正確さ」を評価軸にしましたが、次はさらに一歩進んで構造理解を進化に取り込む予定です。
具体的には、
箇条書きを使えたら+0.8点
指定行数を守れたら+0.5点
といった構造スコアを新たに追加し、AIが「文章構造を守る」方向に進化するよう誘導していきます。
モデルマージで生まれた“融合型モデル”に、進化アルゴリズムで磨きをかける。
このハイブリッド実験は、単なる遊びではなく、人間がチューニングしなくても自己最適化していくAIの原型になるのかも知れません。

LLMをモデルマージと進化アルゴリズムのハイブリッドで実験してみた話まとめ
モデルマージで性格の違うAIを融合
進化アルゴリズムでLoRAを自動進化
20世代で自然な文体を獲得
次は「構造を理解するAI」へと進化させる(予定?)
AIが進化する瞬間を目の前で観察できるのは、なかなか面白い実験でした。
進化アルゴリズムはさらなる進歩を遂げていくでしょう。
使用したPythonコードです
以下に使ったPythonコードを掲載します。
結構エラーが出たので修正してます。
コード内のコメントもいろいろあって、まあ見逃してください。
そのままコラボにペーストすれば動きます。
APIは使っていません。
hugging faceのアクセストークンでしたか。
それは必要です。
エラー出たら、GTP5先生とかに聞いてください。
!pip -q install transformers==4.44.2 peft==0.12.0 accelerate==0.34.2 bitsandbytes==0.43.3 \
huggingface_hub==0.25.2 sentencepiece ipywidgets
import os, json, torch, textwrap, shutil
from huggingface_hub import login, HfApi, snapshot_download
from IPython.display import display
import ipywidgets as widgets
# UI
token_box = widgets.Password(
description='HF Token:',
placeholder='hf_xxxxx...(読み取り権限でOK)',
layout=widgets.Layout(width='60%')
)
btn = widgets.Button(description='Login to HF', button_style='primary')
out = widgets.Output()
def do_login(_):
with out:
out.clear_output()
token = token_box.value.strip()
if not token.startswith("hf_"):
print("❌ トークン形式が違うっぽい(hf_で始まるか確認してね)")
return
try:
who = HfApi().whoami(token=token)
os.environ["HUGGINGFACE_HUB_TOKEN"] = token # transformers>=4.38 は token=... で渡すのが推奨
login(token=token, add_to_git_credential=False)
print(f"✅ ログインOK:{who.get('name') or who.get('email')}")
except Exception as e:
print("❌ ログイン失敗:", e)
btn.on_click(do_login)
display(token_box, btn, out)
# =========================================================
# ✅ セル3:モデルを“ダウンロードしてから”読み込み(修正版)
# =========================================================
from huggingface_hub import snapshot_download
import os
# モデル指定
model_a_repo = "rinna/japanese-gpt2-small"
model_b_repo = "cyberagent/open-calm-small"
cache_root = "/content/hf_models"
os.makedirs(cache_root, exist_ok=True)
token = os.environ.get("HUGGINGFACE_HUB_TOKEN", None)
assert token, "HFトークンが未設定です(上のセルでログインしてから実行してね)"
def dl(repo_id):
local_dir = os.path.join(cache_root, repo_id.replace("/", "__"))
if not os.path.exists(local_dir):
print(f"⬇️ ダウンロード: {repo_id}")
snapshot_download(
repo_id,
local_dir=local_dir,
token=token,
resume_download=True, # 中断復帰OK
repo_type="model" # モデル用
)
else:
print(f"✅ 既存ディレクトリを利用: {local_dir}")
return local_dir
local_a = dl(model_a_repo)
local_b = dl(model_b_repo)
from transformers import AutoModelForCausalLM, AutoTokenizer
alpha = 0.5
save_dir = "./merged_model"
os.makedirs(save_dir, exist_ok=True)
# トークンを明示的に渡す
model_a = AutoModelForCausalLM.from_pretrained(local_a, torch_dtype=torch.float16, token=token)
model_b = AutoModelForCausalLM.from_pretrained(local_b, torch_dtype=torch.float16, token=token)
# tokenizerはA側を採用(必要なら後でB側と比較)
#tok_a = AutoTokenizer.from_pretrained(local_a, token=token)
#if tok_a.pad_token is None:
#tok_a.pad_token = tok_a.eos_token
# 変更前:
# tok_a = AutoTokenizer.from_pretrained(local_a, token=token)
# 変更後:
tok_a = AutoTokenizer.from_pretrained(local_a, token=token, use_fast=False)
if tok_a.pad_token is None:
tok_a.pad_token = tok_a.eos_token
sd_a = model_a.state_dict()
sd_b = model_b.state_dict()
merged = {}
skipped = []
for k, va in sd_a.items():
vb = sd_b.get(k, None)
if vb is not None and va.shape == vb.shape and va.dtype == vb.dtype:
merged[k] = (1 - alpha) * va + alpha * vb
else:
merged[k] = va
skipped.append(k)
print(f"ℹ️ 形状不一致などでAを採用したパラメータ数: {len(skipped)} / {len(sd_a)}")
if skipped:
print("例:", skipped[:10])
model_a.load_state_dict(merged, strict=False)
model_a.save_pretrained(save_dir)
tok_a.save_pretrained(save_dir)
print("✅ マージ完了 →", save_dir)
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
m = AutoModelForCausalLM.from_pretrained(save_dir, torch_dtype=torch.float16, device_map="auto")
t = AutoTokenizer.from_pretrained(save_dir, use_fast=False) # ← ここ
if t.pad_token is None:
t.pad_token = t.eos_token
prompt = "コーヒーの淹れ方のコツを、優しく短く教えて。"
inputs = t(prompt, return_tensors="pt").to(m.device)
with torch.no_grad():
out_ids = m.generate(
**inputs,
max_new_tokens=80,
do_sample=True,
temperature=0.7,
top_p=0.9,
repetition_penalty=1.1, # 連発を抑える
no_repeat_ngram_size=2, # 短い反復を防ぐ
pad_token_id=t.eos_token_id
)
# プロンプトの長さを元に出力部分だけを切り出す方法(安全)
gen_part = out_ids[0, inputs.input_ids.shape[1]:]
print(t.decode(gen_part, skip_special_tokens=True))
# セルF0:bitsandbytes をアンインストール(量子化は使わない前提)
!pip -q uninstall -y bitsandbytes
# セルFix1:bnb と triton を入れて import エラーを解消
!pip -q install bitsandbytes==0.43.3 triton==2.3.0
# セルE1:環境セットアップ&LoRA付与(bitsandbytes無し)
!pip -q install transformers==4.44.2 peft==0.12.0 accelerate==0.34.2
import os, copy, time, random
import numpy as np
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
BASE_MODEL = "./merged_model" # マージ済みモデル
# 乱数固定
SEED = 42
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
if DEVICE == "cuda":
torch.cuda.manual_seed_all(SEED)
print("Loading merged model...")
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float16 if DEVICE=="cuda" else torch.float32,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, use_fast=False)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# GPT-2系向けのLoRAターゲット
lora_cfg = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["c_attn", "c_proj", "c_fc"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
peft_model = get_peft_model(model, lora_cfg)
peft_model.eval()
print("✅ Ready for evolution (no bitsandbytes).")
# セルE2:ユーティリティ(生成、スコア、遺伝子操作)
# 生成(プロンプトの後ろだけ取り出す)
def generate(peft_model, tokenizer, prompt, max_new_tokens=96):
inputs = tokenizer(prompt, return_tensors="pt").to(peft_model.device)
with torch.no_grad():
out_ids = peft_model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=0.7,
top_p=0.9,
repetition_penalty=1.15,
no_repeat_ngram_size=2,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=tokenizer.eos_token_id,
)
gen_only = out_ids[0, inputs.input_ids.shape[1]:]
return tokenizer.decode(gen_only, skip_special_tokens=True).strip()
# 軽い流暢さスコア
def fluency_score(txt: str) -> float:
L = len(txt)
s = 0.0
if 40 <= L <= 280: s += 1.0 if any(p in txt for p in ["。","!","!","?","?"]): s += 0.2 if "。" in txt and txt.count("。。") == 0: s += 0.2 # 同一記号の連続を軽く減点 for ch in set(txt): if ch != " " and ch*4 in txt: s -= 0.6; break return s # “表記の崩れ”を抑えるための簡易スコア(例:『淹れる』推奨) def orthography_score(txt: str) -> float:
good = ["淹れる","淹れ方","蒸らし","抽出","湯温"]
bad = ["をれる","れ方","お茶をれる","コーヒーをれる","れれる"]
return 0.6*sum(txt.count(w) for w in good) - 1.0*sum(txt.count(b) for b in bad)
# (好きに差し替え可)評価用プロンプト
PROMPTS = [
"紅茶教室の講師として、初心者向けに、3つのコツを箇条書きで短く教えて。",
"コーヒーを美味しく淹れるための手順を、番号付きで3行で説明して。",
"来客に出すお茶のマナーを、やさしく1段落でまとめて。"
]
# 総合評価(平均)
def evaluate_individual(peft_model, tokenizer, lora_sd):
# LoRA適用
with torch.no_grad():
for k,v in peft_model.state_dict().items():
if "lora_" in k and k in lora_sd:
v.copy_(lora_sd[k].to(v.device, dtype=v.dtype))
peft_model.eval()
total = 0.0
for p in PROMPTS:
out = generate(peft_model, tokenizer, p)
total += fluency_score(out) + orthography_score(out)
return total / len(PROMPTS)
# LoRA重みの抽出/適用(state_dictベース)
def extract_lora_state_dict(peft_model):
return {k: v.detach().cpu().clone() for k,v in peft_model.state_dict().items() if "lora_" in k}
def crossover_uniform(a, b, cxpb=0.5):
child = {}
for k in a.keys():
ta, tb = a[k], b[k]
mask = torch.rand_like(ta, dtype=torch.float32) < cxpb
child[k] = torch.where(mask.to(ta.device), ta.to(ta.device), tb.to(ta.device)).cpu()
return child
def mutate_gaussian(sd, mut_pb=0.3, sigma=0.01):
child = {}
for k, t in sd.items():
x = t.clone()
mask = torch.rand_like(x, dtype=torch.float32) < mut_pb noise = torch.randn_like(x, dtype=torch.float32) * sigma x = x + noise * mask child[k] = x return child print("✅ Utils ready.") # セルE3:進化ループ本体(POP=10, GEN=20) POP_SIZE = 10 N_GEN = 20 CXPB = 0.5 # 交叉率 MUT_PB = 0.3 # 突然変異率 MUT_SIG = 0.01 # 変異の強さ SAVE_DIR = "./evo_outputs" os.makedirs(SAVE_DIR, exist_ok=True) base_lora = extract_lora_state_dict(peft_model) def random_individual(): # 初期はベース周りにランダム微擾 return mutate_gaussian(base_lora, mut_pb=0.5, sigma=0.02) population = [random_individual() for _ in range(POP_SIZE)] history = [] best_sd, best_score = None, -1e9 start = time.time() for gen in range(N_GEN): scores = [evaluate_individual(peft_model, tokenizer, ind) for ind in population] gen_best = float(np.max(scores)) gen_avg = float(np.mean(scores)) best_idx = int(np.argmax(scores)) print(f"Gen {gen:02d} | best={gen_best:.3f} avg={gen_avg:.3f}") history.append((gen, gen_best, gen_avg)) if gen_best > best_score:
best_score = gen_best
best_sd = copy.deepcopy(population[best_idx])
# エリート選択(上位20%)
elite_k = max(1, POP_SIZE // 5)
elite_idx = list(np.argsort(scores))[::-1][:elite_k]
elites = [population[i] for i in elite_idx]
# 親プール(トーナメント)
def tournament(k=3):
idxs = random.sample(range(POP_SIZE), k)
return population[max(idxs, key=lambda j: scores[j])]
parents = elites[:]
while len(parents) < POP_SIZE:
parents.append(tournament())
# 次世代
next_pop = elites[:]
while len(next_pop) < POP_SIZE: pa, pb = random.sample(parents, 2) child = crossover_uniform(pa, pb, cxpb=0.5 if CXPB>0 else 1.0)
child = mutate_gaussian(child, mut_pb=MUT_PB, sigma=MUT_SIG)
next_pop.append(child)
population = next_pop
elapsed = (time.time() - start) / 60.0
print(f"\n✅ Evolution done. Elapsed ~{elapsed:.1f} min, best={best_score:.3f}")
# 保存
torch.save(best_sd, os.path.join(SAVE_DIR, "best_lora_state.pt"))
import csv
with open(os.path.join(SAVE_DIR, "evolution_log.csv"), "w", newline="", encoding="utf-8") as f:
w = csv.writer(f); w.writerow(["gen","best","avg"]); w.writerows(history)
print("Saved:", os.path.join(SAVE_DIR, "best_lora_state.pt"))
print("Saved:", os.path.join(SAVE_DIR, "evolution_log.csv"))
# セルE4:ベスト個体で軽く生成プレビュー
best_path = "./evo_outputs/best_lora_state.pt"
assert os.path.exists(best_path), "best_lora_state.pt が見つかりません"
# LoRAを適用
with torch.no_grad():
current = peft_model.state_dict()
best_sd = torch.load(best_path, map_location="cpu")
for k in current.keys():
if "lora_" in k and k in best_sd:
current[k].copy_(best_sd[k].to(current[k].device, dtype=current[k].dtype))
# テストプロンプト
tests = [
"紅茶教室の講師として、初心者向けに、3つのコツを箇条書きで短く教えて。",
"コーヒーを美味しく淹れるための手順を、番号付きで3行で説明して。"
]
for p in tests:
print("\n[PROMPT]", p)
print("[OUTPUT]", generate(peft_model, tokenizer, p))
!zip -r evo_results.zip merged_model evo_outputs
from google.colab import drive
drive.mount('/content/drive')
!mkdir -p /content/drive/MyDrive/evo_backup
!cp -r ./merged_model ./evo_outputs /content/drive/MyDrive/evo_backup/
import pandas as pd, matplotlib.pyplot as plt
df = pd.read_csv('./evo_outputs/evolution_log.csv')
plt.plot(df['gen'], df['best'], label='Best')
plt.plot(df['gen'], df['avg'], label='Average')
plt.xlabel('Generation'); plt.ylabel('Score'); plt.legend(); plt.grid(); plt.show()
研究者は論文を書く。
開発者はブログ記事を書く。
さすれば、科学は進歩する。かな。
****************
最近のデジタルアート作品を掲載!
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
***************