さあ、あなただけの人工知能を作ってみましょう。
このブラウザーで言語モデルを手入力します。データ、小さなGPTの学習、Runner、XChatまで完成させます。学習済みの重みや外部AI APIは使いません。
XSMLの概要
開発全体の流れを理解します。
XSMLはExtra Small の意味で、SONUBが開発用に名付けました。発音は엑세스엠엘。一般的な頭文字はXSLMですが、この講座ではXSMLを使います。目標は流暢さより全工程の完成です。
文章→番号→GPT学習→→XChatと進みます。が提供するのは計算とファイルであり、学習済みの頭脳ではありません。
入力して実験
端末を開いて命令を入力し、各部分の役割を説明しましょう。短い英語例は後で自分の言語に置き換えられます。
python3 -c "print('XSML: text -> tokens -> GPT -> Runner -> XChat')"
理解を確認
自分のを学習・保存・再読込し、XChatで使えれば完成です。
困ったとき
Pythonの変数・リスト・ループ・関数・クラスが難しければ先に復習しましょう。
準備運動:バイグラムで単語を数える
実際の開発につながる計画
最初はおもちゃのように小さく作り、原理を理解してから少しずつ大きくしていきます。
文を集める
学びたいテーマの文章を集めます。最初は10文だけでも大丈夫です。
次の単語の回数を数える
ある単語の後にどの単語が何回出たかを表にまとめます。
確率スコアを計算する
選んだ単語のすぐ後に、どの単語が何回来たかを数えます。文の終わりも1回として数えます。次に、それぞれの回数を合計で割ります。「空は」の後に「青い」が3回、「曇り」が1回来たなら、「青い」は 3 ÷ 4 = 75%、「曇り」は 25% です。
二つの選択方法を比べる
毎回いちばん高い候補を選ぶか(貪欲法)、割合どおりにくじを引きます(サンプリング)。くじ引きでも、同じ単語がまた出ることがあります。文の終わりが選ばれたら止まります。このステップのどこにも、その文が本当かどうかを確かめる部分はありません。
自分の基準で改善する
より良い文章、より多くのデータ、別の選択ルールを加えて、自分だけの人工知能に育てます。
はじめての方は、この手順でどうぞ
この実習では、すぐ前の単語1つだけを見る、とても小さなを作ります。どの単語のあとにどの単語が来たかを数え、その回数をに変えてから、重みのついたサイコロをふって次の単語を選びます。
- 用の文はそのままにして「もう一度生成」を押しましょう。学習用の文にある単語だけでできた、新しい文が出てきます。
- 確率の式を見ましょう。前の単語のあとに次の単語が来た回数を、前の単語が出た回数で割ったものが確率です。
- 回数の表では、行が前の単語、列が次の単語です。マスの色がこいほど、その組み合わせがよく出ています。
- 選択方法をに変えて、もう一度押しましょう。毎回同じ文が出ます。にもどしてを上げると、文がだんだんへんになります。
- 学習用の文に、自分の文を1つ足してみましょう。はこの回数がすべてなので、表と確率がすぐに変わります。
もっと知りたい方へ Pythonで自分で作る(AIコース) 次の単語はどうやって選ばれるの?
小さな確率計算機
表の割合は温度1のときの値です。温度は各単語が選ばれる確率を変えるだけで、数えた回数や、事実かどうかは変えません。
開始文の最後の単語に続く候補がまだありません。
| 前の単語次の単語 | |
|---|---|
単語が多いため、最初の8個だけ表示します
核心となる概念
考え方はかんたんです。たくさんの文を読んで、どの単語のすぐ後にどの単語が来るかを数えます。文が終わるところも数に入れます。その回数が小さな表になり、この表だけでも、は次の単語を推測できます。このページで作るモデルは、まさにそれです。
本物のも同じ考え方で動いています。ただ、ずっと大きいのです。小さな回数の表の代わりに、例から何十億個もの数を学びます。これをと呼びます。そして直前の一つの単語だけでなく、質問全体とそれまでの会話を見ます。保存してある文を探して取り出しているわけでもありません。それでも最後のステップは、あなたが作るものと同じです。候補ごとに点数をつけて、一つを選びます。
この実習と実際の言語モデルの比較
| この実習(バイグラム) | ニューラル言語モデル | |
|---|---|---|
| 見る範囲 | 直前の単語一つだけ | 与えられた長い文脈 |
| 学ぶもの | 続いた回数の表 | 数値の重み |
| 埋め込み・attention | いいえ | はい |
| 自分で事実を確認 | いいえ | いいえ |
FAIと参考文献
FAIを参考にブラウザー用Pythonへ書き直しました。元のFAIはPyTorch・MPSと大きなモデルを使います。環境資料の確認日:2026-10-18。
開発環境
ブラウザー内のPythonを確認します。
v86の32bit AlpineにはPythonが入っています。標準だけで実習でき、pip・・は不要です。
FAIのPyTorchはと自動微分を担当し、MPSは対応Macの加速機能です。このでは使いません。Pyodideの通常や常駐プロセスには制約があるため、全実習をAlpineで行います。
入力して実験
3命令でPythonの版、x86構造、readyを確認します。以降もこの端末を使います。
python3 --version
uname -m
python3 -c "import json, math, random, http.server; print('ready')"
理解を確認
torchもMPS設定も不要な理由を説明してください。
困ったとき
pip install torchは不要です。起動失敗は接続を確認して再試行します。
プロジェクト構成
フォルダーとデータを作ります。
~/xsmlにソース、dataに資料、checkpointsに重みを置きます。常にこのフォルダーで実行します。
原文は一行にQ:質問とA:回答。prepare.pyは重複を除き5件ごとに1件を検証用に分けます。検証資料をの学習に混ぜず、個人情報でなく自作の例を使います。
入力して実験
フォルダーを作り、下の各ソースをnanoで同名ファイルに入力して保存します。の句読点に注意してprepare.pyを実行します。
mkdir -p ~/xsml/data ~/xsml/checkpoints
cd ~/xsml
nano settings.json
nano data/raw.txt
nano prepare.py
python3 prepare.py
wc -l data/train.txt data/val.txt
settings.json — 手入力する完全なソース
widthはベクトル幅、contextは文脈長、learning_rateは更新量です。
{
"width": 8,
"context": 12,
"merges": 12,
"steps": 20,
"learning_rate": 0.03,
"seed": 7,
"tokens": 24,
"max_tokens": 64,
"port": 8765
}
data/raw.txt — 手入力する完全なソース
一行に質問と回答を一組。短い自作の英語例です。
Q: hello | A: hello friend.
Q: hi | A: hi friend.
Q: name | A: i am xsml.
Q: sun | A: the sun is warm.
Q: cat | A: a cat can nap.
Q: dog | A: a dog can run.
Q: red | A: red is a color.
Q: blue | A: blue is a color.
Q: learn | A: i learn from text.
Q: code | A: i can learn code.
Q: moon | A: the moon is in the sky.
Q: rain | A: rain is water.
Q: book | A: i can read a book.
Q: tea | A: tea is warm.
Q: bye | A: bye friend.
prepare.py — 手入力する完全なソース
UTF-8読込、重複除去、文単位分割、学習・検証の保存を行います。
from pathlib import Path
lines = [line.strip() for line in Path('data/raw.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
lines = list(dict.fromkeys(lines))
assert len(lines) >= 5, 'write at least five different Q/A lines'
assert all(' | A:' in line and line.startswith('Q:') for line in lines)
train = [line for i, line in enumerate(lines) if i % 5 != 0]
val = [line for i, line in enumerate(lines) if i % 5 == 0]
for name, rows in [('train', train), ('val', val)]:
text = '\n'.join(row.replace(' | A:', '\nA:') for row in rows) + '\n'
Path('data/' + name + '.txt').write_text(text, encoding='utf-8')
print(name, len(rows))
理解を確認
pwdとlsを確認。例は学習12件、検証3件になります。
困ったとき
FileNotFoundErrorならcd ~/xsmlとls dataを確認。Pythonは空白4個。~/xsml-referenceは比較用で自動入力されません。
BPEトークナイザー
自分のBPE辞書を学習します。
文字から始め、隣り合う頻出ペアを順に合併します。encodeも同じ順序で規則を適用します。
改行は合併せず未知文字はID 0です。完全な単位実装ではありません。辞書と規則を一緒に保存し、学習後に別の辞書へ交換しません。
入力して実験
tokenizer.pyを入力・実行し、JSONを調べます。mergeで一致時は2位置、その他は1位置進む様子を追います。
cd ~/xsml
nano tokenizer.py
python3 tokenizer.py
python3 -m json.tool data/tokenizer.json
tokenizer.py — 手入力する完全なソース
mergeで合併し、trainで順序を学び、encodeで再適用、decodeで復元します。
import json
from collections import Counter
from pathlib import Path
def merge(ids, pair, new_id):
result, i = [], 0
while i < len(ids):
if tuple(ids[i:i + 2]) == tuple(pair):
result.append(new_id)
i += 2
else:
result.append(ids[i])
i += 1
return result
class BPE:
def __init__(self, vocab, merges):
self.vocab, self.merges = vocab, merges
self.chars = {c: i for i, c in enumerate(vocab) if len(c) == 1}
@classmethod
def train(cls, text, count):
vocab = ['\ufffd'] + sorted(set(text) - {'\ufffd'})
ids = [vocab.index(c) for c in text]
merges = []
for _ in range(count):
pairs = Counter((a, b) for a, b in zip(ids, ids[1:])
if '\n' not in vocab[a] + vocab[b])
if not pairs:
break
pair, frequency = pairs.most_common(1)[0]
if frequency < 2:
break
new_id = len(vocab)
vocab.append(vocab[pair[0]] + vocab[pair[1]])
merges.append([list(pair), new_id])
ids = merge(ids, pair, new_id)
return cls(vocab, merges)
def encode(self, text):
ids = [self.chars.get(c, 0) for c in text]
for pair, new_id in self.merges:
ids = merge(ids, pair, new_id)
return ids
def decode(self, ids):
return ''.join(self.vocab[i] for i in ids)
def state(self):
return {'vocab': self.vocab, 'merges': self.merges}
if __name__ == '__main__':
settings = json.loads(Path('settings.json').read_text())
text = Path('data/train.txt').read_text(encoding='utf-8')
tokenizer = BPE.train(text, settings['merges'])
Path('data/tokenizer.json').write_text(
json.dumps(tokenizer.state(), ensure_ascii=False), encoding='utf-8')
assert tokenizer.decode(tokenizer.encode(text)) == text
print('vocab:', len(tokenizer.vocab), 'merges:', len(tokenizer.merges))
print('roundtrip: OK', tokenizer.encode('Q: hi\nA:'))
理解を確認
roundtrip: OKと原文復元を確認。未知文字は�になります。
困ったとき
頻出ペアがなければ合併ゼロでも正常です。辞書変更後は重みも新しく学習します。
GPTの構造とコード
小さなTransformerと微分を書きます。
トークンと位置のを足します。QueryとKeyで関連度を求め、Valueを混ぜます。
1ヘッド・1ブロックで、現在までのkeysだけを見るため未来は見えません。残差・正規化・2層MLP・出力層を通ります。簡潔にするため、学習変数なしの正規化、dropoutなしです。
入力して実験
engine.py、model.pyの順に入力します。Valueは局所微分を記憶し、backwardは逆順に勾配を加算します。
cd ~/xsml
nano engine.py
nano model.py
python3 engine.py
python3 -c "from model import GPT; m=GPT(10, 8, 12); print(len(m.forward([1, 2])), len(m.forward([1, 2])[0]))"
engine.py — 手入力する完全なソース
dataは値、gradは勾配、parentsは局所微分です。softmaxは確率化します。
import math
class Value:
def __init__(self, data, parents=()):
self.data = float(data)
self.grad = 0.0
self.parents = parents
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, ((self, 1), (other, 1)))
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data,
((self, other.data), (other, self.data)))
def __pow__(self, power):
return Value(self.data ** power,
((self, power * self.data ** (power - 1)),))
def __neg__(self):
return self * -1
def __sub__(self, other):
return self + (-other)
def __truediv__(self, other):
return self * (other ** -1)
__radd__ = __add__
__rmul__ = __mul__
def exp(self):
result = math.exp(self.data)
return Value(result, ((self, result),))
def log(self):
return Value(math.log(self.data), ((self, 1 / self.data),))
def relu(self):
return Value(max(0, self.data), ((self, float(self.data > 0)),))
def backward(self):
order, seen, stack = [], set(), [(self, False)]
while stack:
node, expanded = stack.pop()
if expanded:
order.append(node)
elif node not in seen:
seen.add(node)
stack.append((node, True))
stack.extend((parent, False) for parent, _ in node.parents)
for node in order:
node.grad = 0.0
self.grad = 1.0
for node in reversed(order):
for parent, derivative in node.parents:
parent.grad += derivative * node.grad
def softmax(values):
peak = max(v.data for v in values)
exps = [(v - peak).exp() for v in values]
total = sum(exps)
return [v / total for v in exps]
def linear(x, weights):
return [sum(a * b for a, b in zip(x, row)) for row in weights]
def norm(x):
mean = sum(x) / len(x)
variance = sum((v - mean) ** 2 for v in x) / len(x)
return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]
if __name__ == '__main__':
x = Value(3)
y = x * x + 2 * x
y.backward()
print('y =', y.data, 'dy/dx =', x.grad)
assert y.data == 15 and x.grad == 8
model.py — 手入力する完全なソース
shapesで寸法を確認。すべての重みが学習対象で、stateとloadは数値を扱います。
import random
from engine import Value, linear, norm, softmax
class GPT:
def __init__(self, vocab_size, width, context, seed=7):
self.width, self.context = width, context
rng = random.Random(seed)
shapes = {
'token': (vocab_size, width), 'position': (context, width),
'query': (width, width), 'key': (width, width),
'value': (width, width), 'project': (width, width),
'expand': (2 * width, width), 'contract': (width, 2 * width),
'head': (vocab_size, width),
}
self.weights = {
name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
for _ in range(rows)] for name, (rows, cols) in shapes.items()
}
self.parameters = [v for matrix in self.weights.values()
for row in matrix for v in row]
def forward(self, ids):
assert 0 < len(ids) <= self.context
w, keys, values, output = self.weights, [], [], []
for position, token in enumerate(ids):
x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
h = norm(x)
q = linear(h, w['query'])
keys.append(linear(h, w['key']))
values.append(linear(h, w['value']))
scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
for k in keys]
attention = softmax(scores)
mixed = [sum(p * v[i] for p, v in zip(attention, values))
for i in range(self.width)]
x = [a + b for a, b in zip(x, linear(mixed, w['project']))]
hidden = [v.relu() for v in linear(norm(x), w['expand'])]
x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]
output.append(linear(norm(x), w['head']))
return output
def loss(self, inputs, targets):
logits = self.forward(inputs)
losses = [-softmax(row)[target].log()
for row, target in zip(logits, targets)]
return sum(losses) / len(losses)
def state(self):
return {name: [[v.data for v in row] for row in matrix]
for name, matrix in self.weights.items()}
def load(self, state):
assert set(state) == set(self.weights), 'checkpoint keys'
for name, matrix in self.weights.items():
assert len(state[name]) == len(matrix), 'checkpoint rows'
for row, saved in zip(matrix, state[name]):
assert len(saved) == len(row), 'checkpoint columns'
for value, number in zip(row, saved):
value.data = float(number)
理解を確認
微分検査は15.0と8.0、形状検査は2 10です。未来トークンが過去の出力を変えない理由を説明します。
困ったとき
行列サイズとzipの長さを確認。全層の重みが逆伝播で学習します。
学習とチェックポイント
学習して保存位置から続けます。
[a,b]から[b,c]を予測します。交差エントロピーは正解のの負の対数で、真実性の保証ではありません。
区間選択→勾配消去→順伝播・逆伝播→勾配制限→SGD。毎更新後に重み・設定・辞書・回数・乱数を保存します。は同じ検証先頭区間だけの簡易診断です。
入力して実験
train.pyを書き、短い学習の後さらに5回実行してresumeを見ます。速度は機器によります。
cd ~/xsml
nano train.py
python3 train.py --steps 20
python3 train.py --steps 5
train.py — 手入力する完全なソース
資料ハッシュで変更を検出。仮ファイルとos.replaceで完全な保存を保ちます。SGDにモメンタムはありません。
import argparse
import hashlib
import json
import math
import os
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def main():
cfg = json.loads(Path('settings.json').read_text())
parser = argparse.ArgumentParser()
parser.add_argument('--steps', type=int, default=cfg['steps'])
args = parser.parse_args()
assert args.steps > 0, '--steps must be positive'
token_state = json.loads(Path('data/tokenizer.json').read_text())
tokenizer = BPE(**token_state)
train_text = Path('data/train.txt').read_text(encoding='utf-8')
val_text = Path('data/val.txt').read_text(encoding='utf-8')
fingerprint = hashlib.sha256((train_text + '\0' + val_text).encode()).hexdigest()
train, val = tokenizer.encode(train_text), tokenizer.encode(val_text)
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
rng, step = random.Random(cfg['seed']), 0
checkpoint = Path('checkpoints/model.json')
if checkpoint.exists():
saved = json.loads(checkpoint.read_text())
assert saved['settings'] == cfg and saved['tokenizer'] == token_state, 'changed settings/tokenizer: start a new checkpoint'
assert saved['data_hash'] == fingerprint, 'changed data: start a new checkpoint'
model.load(saved['weights'])
step = saved['step']
rng.setstate((saved['random'][0], tuple(saved['random'][1]), saved['random'][2]))
n = model.context
assert len(train) > n and len(val) > n, 'add more text'
print('parameters:', len(model.parameters), 'resume:', step, flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
checkpoint.parent.mkdir(exist_ok=True)
for _ in range(args.steps):
start = rng.randrange(len(train) - n)
window = train[start:start + n + 1]
for p in model.parameters:
p.grad = 0
loss = model.loss(window[:-1], window[1:])
loss.backward()
size = math.sqrt(sum(p.grad * p.grad for p in model.parameters))
scale = min(1.0, 1.0 / (size + 1e-8))
for p in model.parameters:
p.data -= cfg['learning_rate'] * p.grad * scale
step += 1
state = {'settings': cfg, 'tokenizer': token_state, 'data_hash': fingerprint,
'weights': model.state(), 'step': step, 'random': rng.getstate()}
temporary = checkpoint.with_suffix('.tmp')
temporary.write_text(json.dumps(state, ensure_ascii=False), encoding='utf-8')
os.replace(temporary, checkpoint)
print('step:', step, 'loss:', round(loss.data, 4), flush=True)
print('validation:', round(model.loss(val[:n], val[1:n + 1]).data, 4), flush=True)
print('saved:', checkpoint, flush=True)
if __name__ == '__main__':
main()
理解を確認
model.jsonと継続回数を確認。異なる区間のは上下します。
困ったとき
Ctrl+Cで停止しても完了済み更新は残ります。設定や資料変更時は書き出した後、旧チェックポイントを別名にして再学習します。
テキスト生成
予測から文章を作ります。
最新のを入れ、1トークンずつ追加します。 0は最大点、それ以外は確率抽選。top_k 0は全候補です。
seedは再現用。改行か上限で止まり、新しい文章だけ返します。学習時のQ:/A:形式を使います。
入力して実験
generate.pyを書き、同じ重みで温度、候補数、seedを一つずつ変えます。
cd ~/xsml
nano generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
generate.py — 手入力する完全なソース
構造と重みを復元し、文脈を切って反復抽選します。
import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def load():
saved = json.loads(Path('checkpoints/model.json').read_text())
cfg = saved['settings']
tokenizer = BPE(**saved['tokenizer'])
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
model.load(saved['weights'])
return model, tokenizer, cfg
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
raise ValueError('invalid sampling parameters')
ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
for _ in range(tokens):
logits = model.forward(ids[-model.context:])[-1]
ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
if temperature == 0:
token = ranked[0]
else:
candidates = ranked[:top_k] if top_k else ranked
peak = logits[candidates[0]].data
weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
token = rng.choices(candidates, weights=weights)[0]
ids.append(token)
if tokenizer.vocab[token] == '\n':
break
result.append(token)
return tokenizer.decode(result)
if __name__ == '__main__':
model, tokenizer, cfg = load()
parser = argparse.ArgumentParser()
parser.add_argument('--prompt', default='Q: hi\nA:')
parser.add_argument('--tokens', type=int, default=cfg['tokens'])
parser.add_argument('--temperature', type=float, default=0.8)
parser.add_argument('--top-k', type=int, default=8)
parser.add_argument('--seed', type=int, default=cfg['seed'])
args = parser.parse_args()
assert 1 <= args.tokens <= cfg['max_tokens']
print(generate(model, tokenizer, args.prompt, args.tokens,
args.temperature, args.top_k, args.seed))
理解を確認
同じ入力と設定なら再現できます。最初が改行なら空の回答もあります。
困ったとき
不自然な結果は小規模学習では普通です。形式・未知文字・lossを確認します。
RunnerとAPI
実際のHTTPサーバーを作ります。
Runnerは重みを一度読み、順に要求を処理します。GET /healthは状態、POST /generateは文章を返します。
127.0.0.1はゲストLinux自身です。はここで実行し、の住所欄とは区別します。nohupと&はタブが生きている間だけ有効で、systemdは不要です。
入力して実験
runner.pyを作り起動。直後にPIDを保存し、ログ・health・生成を確認します。
cd ~/xsml
nano runner.py
nohup python3 -u runner.py > runner.log 2>&1 &
echo $! > runner.pid
cat runner.log
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/generate -H 'Content-Type: application/json' -d '{"prompt":"Q: hi
A:","tokens":8,"temperature":0.8}'
runner.py — 手入力する完全なソース
モデルを共有し、入力を検証してUTF-8 JSONを返します。
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from generate import load, generate
def main():
model, tokenizer, cfg = load()
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body, ensure_ascii=False).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == '/health':
self.reply(200, {'status': 'ok', 'model': 'XSML'})
else:
self.reply(404, {'error': 'not found'})
def do_POST(self):
if self.path != '/generate':
return self.reply(404, {'error': 'not found'})
try:
length = int(self.headers.get('Content-Length', '0'))
if not 0 < length <= 4096:
raise ValueError('request size')
data = json.loads(self.rfile.read(length))
prompt = data['prompt']
tokens = data.get('tokens', cfg['tokens'])
temperature = data.get('temperature', 0.8)
top_k = data.get('top_k', 8)
seed = data.get('seed', cfg['seed'])
if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
raise ValueError('prompt')
if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
raise ValueError('tokens')
if type(top_k) is not int or top_k < 0 or type(seed) is not int:
raise ValueError('top_k/seed')
if type(temperature) not in (int, float):
raise ValueError('temperature')
result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
self.reply(200, {'text': result, 'model': 'XSML'})
except (ValueError, KeyError, TypeError):
self.reply(400, {'error': 'invalid request'})
address = ('127.0.0.1', cfg['port'])
print('XSML Runner:', address, flush=True)
HTTPServer(address, Handler).serve_forever()
if __name__ == '__main__':
main()
理解を確認
status okとtextを確認。空JSONは400で拒否されます。
困ったとき
接続拒否はログと重みを確認。ポート使用中なら旧PIDを停止。再学習後はRunnerを再起動します。
XChat
自分の会話クライアントを完成させます。
XChatは入力画面、Runnerは実行サービス、XSMLはです。は質問を整形しで送ります。
最初の版は端末内で動き、会話履歴は送りません。次の実験として小さな文脈に合う履歴を追加できます。
入力して実験
Runnerを動かしたままxchat.pyを書き実行。You>でhi、/exitで終了してからkillします。
cd ~/xsml
nano 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 -> XSML | /exit')
while True:
try:
question = input('You> ').strip()
if question == '/exit':
break
if question:
print('XSML>', ask(question))
except (EOFError, KeyboardInterrupt):
break
except (URLError, TimeoutError) as error:
print('Runner:', error)
理解を確認
XChat→Runner→重み→文章を説明。Runner停止時は接続エラーになり、再起動で戻るか確認します。
困ったとき
killをXChat内に入力しないでください。/exitで戻り、最後にプロジェクトを書き出します。