少しだけ変えて、あとは残す
大きなモデルには、何十億もの数があります。その全部を学習するには、大量のメモリと時間が要ります。LoRA のようなアダプターは、大きな数のそばに数個の数を足し、その足した数だけを学習します。
このレッスンでは、その数を数えます。このラボにモデルを学習させるものはないので、計算を実行して確かめます。
このレッスンのゴール
メモ
小さなアダプターが、大きなモデルを安く変えられるわけを説明できる。
キーワード
| ことば | いみ |
|---|---|
| パラメータ | モデルが学んだ数一つ |
| 凍結 | 学習のあいだ、変えないこと |
| アダプター | 学習する、小さな新しい数のまとまり |
| ランク | アダプターの二つの表の細さ |
| LoRA | 細い表二つでできたアダプター |
学生が入力するセルは、このプレビューでは空白です。
層は、数の表
モデルの中で、層の大半は数の表です。6 行 6 列の表には、6 × 6 で 36 個のパラメータがあります。100 × 100 の表なら、10,000 個です。
全部を学習するとは、すべての表のすべての数を変えることです。だから費用がかかります。
細い表二つで、同じ大きさの変化を作る
6 × 1 の表に 1 × 6 の表を掛けると、6 × 6 の表になります。凍結した表と同じ形なので、足し合わせられます。
アダプターが持つ数は 6 と 6 で、36 個ではなく 12 個です。細い辺の長さがランクです。ランクが大きいほど多く持ち、費用もかかります。
例: 100 × 100 の表一つ
| ランク | アダプターの数 | 10,000 に対する割合 |
|---|---|---|
| 1 | 1 × (100 + 100) = 200 | 2% |
| 4 | 4 × 200 = 800 | 8% |
| 8 | 8 × 200 = 1,600 | 16% |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
「アダプターは、モデルを小さくした写しだ」と考えがちです。そうではありません。大きなモデルはそのまま残り、アダプターは出力に足されます。アダプターを外せば、元のモデルに戻ります。
モデル用の補助を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答に含まれるトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用の補助を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
アダプターの計算を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '// 凍結された 6 × 6 の表 W と、細い表が二つ。B は 6 × 1、A は 1 × 6。' 'const rows = 6, cols = 6, rank = 1;' 'const B = [1, 2, 0, 1, 0, 3];' 'const A = [2, 0, 1, 0, 1, 1];' > adapt4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
アダプターの計算を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function delta(i, j) {' ' let sum = 0;' ' for (let k = 0; k < rank; k++) sum = sum + B[i * rank + k] * A[k * cols + j];' ' return sum;' '}' 'const full = rows * cols;' 'const adapter = rank * (rows + cols);' >> adapt4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
アダプターの計算を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"凍結された表: \" + rows + \" × \" + cols + \" = \" + full + \" 個\");' ' console.log(\"アダプター: \" + rows + \" × \" + rank + \" と \" + rank + \" × \" + cols + \" で \" + adapter + \" 個\");' ' const first = [];' ' for (let j = 0; j < cols; j++) first.push(delta(0, j));' ' console.log(\"0 行目の変化: \" + first.join(\" \"));' ' console.log(\"学習する割合: \" + Math.floor((adapter * 100) / full) + \"%\");' ' const q = \"小さなアダプターを学習するほうが、モデル全体を学習するより安いのは、なぜですか。短い一文で答えてください。\";' ' const sample = \"変わるのは小さなアダプターの表だけなので、保存して更新する数がずっと少なくて済むからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"返事: \" + reply.text);' '}' 'main();' >> adapt4.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
アダプターの数を数える
docker exec -w /home/node/project agent node adapt4.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力されたもの
凍結した表の大きさ、アダプターの大きさ、変化の一行、学習する割合が出ます。A と B の数は架空で、本物のアダプターでは学習で決まります。
最後の行は、短いモデルの呼び出しか、「記録済みサンプル」です。