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

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

PAGE TOP