XChat は入力画面、Runner は実行サービス、XSLM はモデル です。クライアント は質問を整形しHTTP で送ります。
最初の版は端末 内で動き、会話履歴 は送りません。次の実験として小さな文脈 に合う履歴を追加できます。
要求が通る道 — XChat から重みまで
XChat で hi と打って Enter を押すと、こんなことが順に起こります。
xchat.py が質問を学習 の形式で包みます。"Q: hi" の次の行に "A: "。
JSON {"prompt ": …, "tokens ": 24} を作り、http://127.0.0.1:8765/generate に POST します。
Runner の do_POST が本文を読んで検査し、generate を呼びます。
generate がトークナイザー で番号を作り、メモリー の GPT でトークンを1つずつ引き、改行 で止まります。
Runner が {"text": …} で応答し、xchat.py が XSLM> の後ろに表示します。
この道のどこか1か所でも切れると、会話はできません。だから問題が起きたら、道に沿って1つずつ確かめます。Runner は起動しているか(curl /health )、生成はできるか(curl /generate)、XChat のアドレスとポート は合っているか。
XChat クライアント — 入力ループと HTTP 要求
xchat.py は2つの部分からなります。
ask(question): 要求を1つ送り、答えを返します。Request でアドレス・本文・ヘッダーを入れた POST 要求を作り、urlopen で送ってから json.load で応答の本文を読み、text を取り出します。timeout=120 は、2分以内に答えがなければあきらめるという意味です。遅いエミュレーター で長い生成を待てるよう、余裕をもたせました。
入力ループ: while True で You> を表示し続け、1行ずつ読みます。/exit なら終わり、空行は飛ばし、それ以外は ask に渡して XSLM> の後ろに答えを表示します。
質問ごとに settings.json を読み直します。プロンプトの形式("Q: " … "A: ")はクライアントがつけます。サーバー が形式をつけるように作れば、クライアントは形式を知らなくてもかまいません。どちらにしても、形式は1か所だけで決めないとずれてしまいます。
エラーの扱い — 偽の答えを作らない
xchat.py はエラーをこう分けて扱います。
EOFError・KeyboardInterrupt: Ctrl+D (入力の終わり)や Ctrl+C を押したものです。ループを終えて bash に戻ります。
URLError: Runner に接続できませんでした(Connection refused など)。Runner が止まっているか、ポートが違います。
HTTPError: 接続はできたものの、Runner が 400 のようなエラーコードで答えました。URLError の一種なので、同じ行で捕まえます。
TimeoutError: 決めた時間内に答えが来ませんでした。
どの場合も "Runner: エラーの内容" を表示して、次の質問を待ちます。接続できないときに、あらかじめ決めておいた文を代わりに見せるような形で隠しません。そうしてこそ、何が壊れたのかがすぐにわかります。Runner を止めて質問してみて(エラーが出るのが正常)、もう一度起動して会話が戻るか確かめてください。
会話の記録をつける — 文脈の長さの限界
今の XChat は質問ごとに独立しています。前に言ったことをモデルは知りません。実際のチャットボット が前の会話を覚えているように見えるのは、毎回これまでの会話全体をプロンプトの前につけて送っているからです。モデルそのものには記憶がありません。
XSLM でやってみると、すぐに壁にぶつかります。下の実験は、会話1回(Q: hi / A: hi friend.)の後に新しい質問 cat をつけます。プロンプトは21トークンですが、モデルは最後の12トークンしか見ないので、実際に見る文章は "iend." からです。前の会話は切り捨てられます。
だから会話の記録をつけるには、こんなことを一緒に行います。
古い会話から捨てて、文脈の長さに収めます。
context を大きくしたモデルを新しく学習します(位置の表が大きくなり、計算が遅くなります)。
学習資料にも何往復も続く会話を入れないと、モデルはその形を学びません。
次の挑戦: xchat.py に history のリスト を置き、これまでの質問と答えをつけて送ってみてください。トークン数を数えて、12を超えたら前の部分を捨てるようにします。
AIに聞いてみる
自分で確かめる lab/context.py
import json
from pathlib import Path
from tokenizer import BPE
tok = BPE(**json.loads(Path('data/tokenizer.json').read_text()))
size = json.loads(Path('settings.json').read_text())['context']
history = 'Q: hi\nA: hi friend.\nQ: cat\nA: '
ids = tok.encode(history)
print(len(ids), 'tokens; the model sees the last', size)
print(repr(tok.decode(ids[-size:])))
cd ~/xslm
mkdir -p lab
micro lab/context.py
python3 -m lab.context
実行結果
21 tokens; the model sees the last 12
'iend.\nQ: cat\nA: '
実際の会話モデルはどう作るのか — 事前学習の次の段階
XSLM は Q/A 形式の文章で一から学習しました。ChatGPT ・Claude のような会話モデルは、ふつう複数の段階を経ます。
事前学習 (pre-training): とても多くの文章で次のトークン当てをします。XSLM の train.py がすることと同じで、規模だけが違います。
指示学習(SFT 、instruction tuning): 質問と良い答えの組でさらに学習し、指示に従う習慣をつけます。ユーザーとアシスタントの順番を分ける会話の形式(チャットテンプレート )も、このとき身につけます。
選好学習(RLHF など): 人がより良いと選んだ答えの方へ傾くように磨きます。
最新の情報が必要なら、学習し直す代わりに RAG を使います。質問で文書を検索して見つけ、プロンプトに一緒に入れ、モデルはその文書を見ながら答えます。資料の形式に [DOC] のような文書の欄を置き、文書を参考に答える形を前もって学習しておけば、あとで検索結果をその欄に入れて使えます。
AIに聞いてみる
次の挑戦 — XSLM を育てる方法
完成した XSLM で試せる実験です。1つずつ変えて、損失と答えを記録してください。
資料を増やす: 質問をいろいろな言い方で、答えを同じ形式で、数十〜数百行書きます。
学習を増やす: 学習率 ・学習回数を変えながら、学習と検証の損失を比べます。
モデルを大きくする: width を16に、context を24に変えます。パラメーター 数と1歩あたりの時間がどう変わるかを測ります。
ブロックを積む: model.py のアテンション + MLP を関数 にまとめ、重み をブロックごとに別に作って2回通るようにします。
ヘッドを分ける: 幅8を4マスずつ2つのヘッドに分けて別々にアテンションし、つなげます。
top-p を加える: generate に、累積確率 で候補を切るオプションを作ります。
ストリーミング ・会話の記録: Runner と XChat につけてみます。
ここまでできるようになったら、自分のコンピューターで PyTorch で同じ構造を書き直してみてください。テンソル で計算すれば、数千倍速く、数千万のパラメーターを学習できます。Andrej Karpathy の nanoGPT ・build-nanogpt もよい次の教材です。
入力して実験
Runnerを動かしたままxchat.pyを書き実行。You>でhi、/exitで終了してからkill します。
cd ~/xslm
micro xchat.py
python3 xchat.py
hi
/exit
kill "$(cat runner.pid)"
ターミナルで実習する
xchat.py 手入力する完全なソース
inputで読んでHTTP POSTを送り、実際の回答や接続エラーを表示します。
import json
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
def ask(question):
cfg = json.loads(Path('settings.json').read_text())
body = {'prompt': 'Q: ' + question + '\nA: ', 'tokens': cfg['tokens']}
request = Request('http://127.0.0.1:' + str(cfg['port']) + '/generate',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
with urlopen(request, timeout=120) as response:
return json.load(response)['text']
if __name__ == '__main__':
print('XChat -> Runner -> XSLM | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSLM>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
コードを1行ずつ読み解く
xchat.py ask — 質問を1つ送る
def ask(question):
cfg = json.loads(Path('settings.json').read_text())
body = {'prompt': 'Q: ' + question + '\nA: ', 'tokens': cfg['tokens']}
request = Request('http://127.0.0.1:' + str(cfg['port']) + '/generate',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
with urlopen(request, timeout=120) as response:
return json.load(response)['text']
質問ごとに settings.json を読み、トークン数とポートを得ます。質問を "Q: " + 質問 + 改行 + "A: " で包み、学習の形式に合わせます。Request に data を渡すと POST になり、本文は JSON のバイト 、ヘッダーは Content-Type です。with urlopen(…) は応答を読み終えたあと接続を閉じてくれます。json.load が応答を辞書 として読み、ask は text だけを返します。
xchat.py 会話ループ
if __name__ == '__main__':
print('XChat -> Runner -> XSLM | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSLM>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
開始の案内を表示し、while True で質問を受け取ります。input は You> を表示して Enter までの1行を読み、strip で前後の空白を消します。/exit なら break で終わり、空の質問は飛ばします。Ctrl+D は EOFError、Ctrl+C は KeyboardInterrupt として入ってきて、やはり終わります。Runner の接続エラーとタイムアウトは知らせて、次の質問を待ちます。
理解を確認
XChat→Runner→重み→文章を説明。Runner停止時は接続エラーになり、再起動で戻るか確認します。
困ったとき killをXChat内に入力しないでください。/exitで戻り、最後にプロジェクトを書き出します。
よくある質問
XChat で kill と打ったのに Runner が止まりません。
You> に打った文字はコマンド ではなく、質問として Runner に送られます。まず /exit か Ctrl+D で XChat を終えて bash のプロンプト($)に戻ってから、kill コマンドを打ちます。
Runner: <urlopen error [Errno 111] Connection refused> と出ます。
Runner が止まっているか、まだ起動中です。cat runner.log で起動の文言(XSLM Runner: …)を確かめ、curl -s http://127.0.0.1:8765/health が答えるかを見てから、もう一度質問してください。再読み込みをしたなら、Runner を起動し直す必要があります。
答えが質問と関係ありません。接続が間違っているのですか?
XSLM> の後ろに何かしら生成された文章が出たなら、接続は合っています。内容はモデルの実力です。同じ質問を python3 generate.py --prompt で直接実行してみてください。設定と seed が同じなら、Runner を通した答えとまったく同じになります。