AI仮想顧客アンケート チャットボット 「アンケ太郎くん」を作った話

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

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

下記の記事が面白かったので、チャットボットに落とし込みできるか試してみました。

https://ascii.jp/elem/000/004/321/4321473/
https://ascii.jp/elem/000/004/321/4321473/2/

ビジネス系は制作してても面白くないですね。

じゃあなんでやるのって話ですが、単なる知的好奇心です。
(もともとアート系なのであしからずです。ちなみにエンタメ系大好きです笑)。

実際のところは、Pythonでコードを書いて、Google colabあたりですっきりプログラミングした方が格好的には良いのですが、それだと多くの人に使ってもらうときに、webサービスかアプリで公開+APIを使って課金、こんな感じで手間がかかります。

手早く公開したかったので、チャットボットで作れば可能かと思い、今回の実装となりました。

あくまでも仮想顧客のデータですので、参考値として利用してください。

「アンケ太郎くん」
noteに投稿しました。
そちらから有料記事で購入お願いします。(300円)↓↓↓

AIマーケッターが仮想顧客にアンケートするチャットボット「アンケ太郎くん」を作った話

********

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

********

AIでできる仮想顧客アンケート

新しい商品やサービスを考えたとき、「これって本当に売れるのだろうか?」「利用者がいるのかなあ」と気になるのは自然なことです。

実際に市場調査を行おうとすると、アンケートの設計、対象者の募集、回答の回収と集計など、多くの手間と費用がかかります。
個人事業や小さなチームでは、すぐに大規模な調査を実施するのは難しいのが現実です。

そんなときに役立つのが仮想顧客アンケートのチャットボットです。

これは実際の顧客データが手元になくても、AIが統計的な平均傾向をもとに「仮想の顧客」を生成し、その人たちがどう答えるかをシミュレーションできる仕組みです。

もちろん本物の実地調査ではありませんが、最初のアイデア検証や方向性の確認には十分な材料になります。

例えば
「1本200円の新作プロテインバーを売りたい」
「15,000円のユーティリティアイアンを販売したい」
といったケースで、想定ターゲットがどう感じるかを数十件から数百件のデータで一度に確認できるのは大きな強みです。

仮想顧客アンケート チャットボットの仕組み

このチャットボットの特徴はシンプルです。ユーザーが売りたい商品やサービスの情報を入力すると、まず「顧客データを持っていますか?」と確認します。

もしCSVやExcelの顧客データを持っていれば、それをアップロードしてAIが活用します。
データがなければ「統計平均」をもとにAIが仮想顧客を自動生成します。

この仮想顧客には、
・年齢帯
・性別
・居住地域
・世帯構成
・収入帯
・ライフスタイル
などが設定されます。

そこに対して
「購買意向(5段階)」「
価格評価(安い/ちょうどいい/高い)」
「利用シーン」
「代替選好(健康志向・時短・ボリューム・価格重視)」などの
アンケート回答が付与されます。

結果は二つの形式で出力されます。ひとつは誰でも読みやすいレポート形式。
もうひとつは機械が扱いやすいJSON形式です。

JSONはそのまま外部ツールで分析にかけられるため、あとからグラフ化や追加集計も容易です。

使い方のステップ 商品入力から出力まで

利用の流れはとてもわかりやすいです。

1. 商品情報を伝える
商品名、価格帯、想定シーンを簡単に入力します。

例:「新作のユーティリティアイアンを15,000円で売りたい。残り距離があるときに使う想定。」

これだけです。
これ以降はわからなければ、何もしないでOK。
勝手にAIが判断して進めてくれます。簡単〜🎵

2. 件数を指定する
30〜50件:軽いテスト
100件:より詳細な比較
300件:分割出力で大規模シミュレーション

3. 顧客データの有無を答える
ある場合:ファイルをアップロード
ない場合:統計平均をもとにAIが仮想顧客を生成

4. 出力スタイルを選ぶ
読みやすい文章レポート
JSONファイルでのデータ出力

例えば「プロテインバーを200円で売りたい、40件でシミュレーションして」と伝えるだけで、仮想顧客のプロフィールと回答、さらに集計結果が一度に得られます。

これまで数週間から数ヶ月かかっていた作業が、数分で形になります。

回答の多様性をコントロールする方法

このチャットボットの特徴のひとつは、回答の「多様性」をユーザーが調整できる点です。

生成AIにはtemperatureとtop-pというパラメータがあります。

temperatureは答えの「ゆらぎ」の大きさを意味します。
低い場合(0.3〜0.5):答えが安定し同じ傾向に寄る
普通(0.7〜0.9):バランスよく多様性もある
高い場合(1.0以上):ユニークで幅広い答えが出やすい

top-pは確率的に上位の答えをどこまで拾うかを調整します。
低い場合(0.5〜0.6):一番ありそうな答えに絞る
高い場合(1.0):幅広くいろいろな答えを含める

数値を直接指定しなくても、
「安定した答えが欲しい」
「もっとユニークにして」
と日本語で指示するだけでAIが自動的に数値に変換します。

例えば「安定した答え」なら
temperature=0.4,
top-p=0.6、

「ユニークに」なら
temperature=1.1,
top-p=1.0
に設定されます。

これにより、「しっかりした調査のような結果を見たい」のか、「多様な意見を広く拾いたい」のかを簡単に調整できます。

ビジネス提案の成功確率

アンケ太郎くんでは、アンケート結果をもとに「ビジネス提案の成功確率」も出力されます。
これは単なる数字ではなく、商品企画の方向性を確認するための指標です。

購買意向、価格評価、利用シーン、改善要望などの総合的な傾向から、どの程度市場に受け入れられる可能性があるかをAIが推定します。

顧客が「ちょうどいい」と感じているか、利用シーンが多様か、改善の余地が明確かといった点を見れば、ビジネスの成長余地も読み取れます。

この出力は、実際の市場調査の代替ではありませんが、初期提案の信頼性を高める「仮説の裏付け」として活用できます。

ビジネス提案の成功確率の例

  • 平均購買意向 3.6 → 中程度の前向き姿勢

  • 「ちょうどいい」価格評価が60%と高め

  • 利用シーンが多様で、日常使いとギフト需要の両方をカバー

  • 改善要望は主にラインナップ追加やデザイン改善で解決可能

👉 想定される成功確率:68%

※この数値はAIによる参考推定であり、実際の市場調査・販売結果とは異なる可能性があります。

この数値を参考に、次の 挑戦をするかどうかを判断するのも、わるくはないかと思います。

AI仮想顧客アンケート チャットボット アンケ太郎くんまとめ

仮想顧客アンケート チャットボットは、アイデアの初期段階で「売れるかどうか」をおおまかに確かめたいときに非常に役立ちます。

実際の顧客調査とは違い、すぐに数十件〜数百件の声が確認できるため、企画や仮説づくりのスピードが格段に上がります。

もちろん、出力されるのはあくまで「仮想データ」です。これを最終的な判断の根拠にすることはできません。
しかし「この価格帯は高いと感じる人が多い」「この利用シーンがよく出てくる」といった手がかりを得るには十分です。

最低30件以上あれば統計的にも一定の傾向を読み取ることができ、40〜50件であればバランスの取れたデータになります。

必要であれば300件まで一気にシミュレーションできるため、企画の裏付け資料としては心強い存在になるでしょう。

新しい商品を考えたときは、まずこのチャットボットで「仮想顧客の声」を拾ってみる。

そして、そこで得られた気づきをもとに本格的な調査やテストマーケティングに進めば、より失敗の少ない商品づくりにつながります。

ぜひ皆さん使ってみてください。

「アンケ太郎くん」
noteに投稿しました。
そちらから有料記事で購入お願いします。(300円)↓↓↓

AIマーケッターが仮想顧客にアンケートするチャットボット「アンケ太郎くん」を作った話

 

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

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

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

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

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

「プロの焙煎士に聞け!コーヒー・焙煎職人の答え箱」チャットボットを作りました

 

👉 [プロの焙煎士に聞け!コーヒー・ 焙煎職人の答え箱はこちら]

https://chatgpt.com/g/g-68d10e5950c88191aad0b96259c3df38-huronobei-jian-shi-niwen-ke-bei-jian-zhi-ren-noda-exiang

※LINE対応チャットボット版の
「プロの焙煎士に聞け!コーヒー・焙煎職人の答え箱」
はこちらで有料にて販売中!

「プロの焙煎士に聞け!コーヒー・焙煎職人の答え箱」チャットボットを作りました。もちろん無料です。(LINE版は有料)
コーヒー道を極めたい方はぜひアクセスして、本物のコーヒーの淹れ方を習得してください!

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

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

「プロの焙煎士に聞け! 焙煎職人の答え箱」とは何か

コーヒーを自宅で楽しむ人が増える中で、「もっと美味しく淹れたい」「焙煎について詳しく知りたい」という声が高まっています。
そんなニーズに応えるのが、今回公開したChatGPTベースの新サービス「焙煎職人の答え箱」です。

これは、コーヒー焙煎に関するあらゆる疑問を気軽に相談できるAIツール。
プロの焙煎士の知識で、焙煎度合いの違いから器具の特徴、さらには豆の保存方法まで幅広く答えてくれます。

難しい専門書をひも解かなくても、まるで焙煎歴数十年の職人に直接質問できるような感覚で使えるのが最大の魅力です。

コーヒー愛好者が得られる5つのメリット

「コーヒー・焙煎職人の答え箱」を使うことで、どんなメリットがあるのかを整理してみました。

1. 疑問を即解決
「シティローストとフルシティローストの違いは?」などの細かい質問にもすぐ回答。

2. 実践的なアドバイス
家庭用焙煎機の使い方、焙煎時の火加減や排気調整など、すぐに試せるアドバイスが得られます。

3. 初心者から上級者まで対応
初めて焙煎に挑戦する人も、すでに自分の焙煎スタイルを持っている人も、それぞれのレベルに合わせた回答をしてくれます。

4. 学びながら楽しめる
回答には簡単な補足や背景も添えられているため、単なる答え以上に「なるほど」と思える知識が身につきます。

5. いつでもどこでも無料で利用可能
スマホから気軽にアクセスでき、24時間いつでも質問できるのはAIならではの強みです。

よくある質問と答え ― 焙煎にまつわる疑問をプロが解消

ここでは実際に寄せられやすい質問と、それに対する答えのイメージを紹介します。

Q. 浅煎りと深煎りでカフェイン量は変わるの?
焙煎が進むと豆の重量が軽くなるため、同じスプーン1杯で比べると深煎りの方がカフェインはやや少なめ。ただし豆の種類や抽出方法の影響も大きい。

Q. 家庭用フライパンでも焙煎できますか?
可能。ただし火加減の調整が難しいため、最初は焦げやすい。こまめにかき混ぜ、焙煎の進み具合を「音」や「香り」で確かめるのがコツ。

Q. 豆の保存は冷凍がいいの?常温がいいの?
焙煎後2週間以内に飲み切るなら常温でもOK。ただし長期保存なら冷凍推奨。解凍時に結露を防ぐため、小分けにして保存するのがベスト。

こうした具体的な答えがすぐに返ってくるので、初心者にとってはもちろん、知識を整理したい上級者にも役立ちます。

なるほどです!レビュー部分をもう少し掘り下げて、「なぜ役立ったのか」「どんな変化があったのか」まで描写すると読みごたえが出ますね。下記のように書き換えてみました。

使用者の声・体験談レビュー

実際に「コーヒー・焙煎職人の答え箱」を試したユーザーからは、単なる感想にとどまらず、生活や学びに直結する体験談が寄せられています。

自家焙煎歴3ヶ月の方
「焙煎度合いを説明してもらう時に、色や香りの目安まで教えてくれたのが特に役立ちました。専門書には“ミディアムローストは酸味が残る”と書いてあっても、実際にどう見分ければいいのか分からなかったんです。
でも『1ハゼの直後で火を弱めて、色はまだ明るめの茶色、香りは少し酸味が残るフルーティさがある』と具体的に答えてくれたので、初めて自分の焙煎に自信が持てました。」

カフェ経営を目指す方
「焙煎の排気や火加減について相談した時、AIなのに“シティローストなら7分前後で排気を強め、豆の表面が乾いてから一気に仕上げると雑味が出にくい”といった、実際の焙煎現場でしか聞けないようなアドバイスが返ってきました。
独学ではバラバラだった知識が整理され、仕上がりが安定するようになったのが一番の収穫です。正直、経験豊富な焙煎士に横で指導してもらったような感覚でした。」

毎朝ドリップ派の方
「豆の保存方法を聞いたときに、単なる“冷凍が良い”という答えではなく、“小分けにして密封、取り出す時は結露を防ぐために常温で少し戻す”という具体的な流れを教えてくれました。
その通りにしたら、2週間経っても香りがしっかり残っていて、味も鮮明。以前は冷凍しても風味が落ちたのに、保存の仕方ひとつでここまで変わるのかと驚きました。」

利用者の体験談からは、単なる知識提供にとどまらず、焙煎や抽出の「実際の行動の質」を変えてくれる力があることが分かります。

まさに、日々のコーヒー1杯をアップグレードするパートナーと言えるでしょう。

今すぐ試したくなる!コーヒー・焙煎職人の答え箱の魅力的な使い方

せっかくのサービスも、どう使うかで価値が変わります。おすすめの活用方法を紹介しましょう。

焙煎ログの補助ツールとして
自分で焙煎した記録を残す時に「この焙煎度合いは何に近い?」と聞いて整理。

カフェ仲間との会話のネタに
「こんな質問してみたよ」と共有することで、知識の交流やディスカッションにも役立ちます。

日々の小さな疑問を気軽に解消
「今日は蒸し暑いけど、この天候は焙煎に影響する?」など、雑談感覚で聞けるのも魅力です。

学習教材として
焙煎の基礎から応用まで網羅されているので、コーヒー講座の補助教材としても使えます。

「コーヒー・焙煎職人の答え箱」は、単なる質問箱にとどまらず、コーヒーをもっと深く楽しむためのパートナーになってくれるはずです。

「プロの焙煎士に聞け!コーヒー・焙煎職人の答え箱」まとめ

コーヒーの奥深さに触れると、どうしても疑問が次々と湧いてくるものです。そんな時に頼れるのが「コーヒー・焙煎職人の答え箱」。
専門知識を持つAIが、あなたの小さな疑問から本格的な相談まで、わかりやすく応えてくれます。

自宅での一杯をもっと美味しくしたい人、焙煎にチャレンジしたい人、さらにはカフェ開業を目指す人まで、幅広く役立つツールです。

ぜひ一度アクセスして、その便利さと奥深さを体感してみてください。
もちろん無料です。(LINE版は有料)

👉 [プロの焙煎士に聞け! コーヒー・焙煎職人の答え箱はこちら]

https://chatgpt.com/g/g-68d10e5950c88191aad0b96259c3df38-huronobei-jian-shi-niwen-ke-bei-jian-zhi-ren-noda-exiang

※LINE対応チャットボット版の
「プロの焙煎士に聞け!コーヒー・焙煎職人の答え箱」
はこちらで有料にて販売中!

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

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

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

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

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

小規模言語モデル 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/

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

PAGE TOP