一覧CS-15-3 · AI・機械学習の入口

言語モデルの生成と検索拡張

トークンへの分割、次の候補の選択、検索結果の根拠確認を小さなデータで追います。

説明

言語モデルは入力文をトークン列として扱い、文脈に続くトークンの候補分布から次を選びます。トークンは単語と一対一とは限りません。候補の確率が高いことと、その内容が資料に裏付けられていることも別です。

小さな語彙と候補表で、文字列とトークンIDの対応、温度による候補分布の変化、候補を追加した後の文脈を確認します。後半では架空サービスの二つの資料を検索し、版と設定値を照合します。検索で渡した資料から何を根拠にできるかを調べます。

1文字列をトークンへ分ける

説明用の語彙を使い、長い一致を先に採る分割を定義します。実際のトークナイザーの語彙や分割規則はモデルごとに異なります。

import re語彙に一致する部分を抽出; vocabulary = {'再':1,'起動':2,'する':3,'。':4,'再起動':5}説明用語彙とID; text = '再起動する。'; pieces = re.findall('再起動|起動|する|再|。',text)長い候補から順に一致させて分割; ids = [vocabulary[piece] for piece in pieces]IDへの変換; pieces,ids,''.join(pieces) == text分割と復元の照合

今回は再起動が一つのトークンです。語彙を変えれば再と起動へ分かれることもあります。ID5という数の大小自体が語の意味や優先順位を表すわけではありません。

2語彙を変えた場合

再起動を登録しない語彙では、同じ文を再・起動・する・句点に分けます。

alternative = ['再','起動','する','。']別の分割; [vocabulary[piece] for piece in alternative],len(pieces),len(alternative),len(text.encode('utf-8'))ID列・トークン数・バイト数

同じ文でもトークン数は3または4、UTF-8では18バイトです。文字数・バイト数・トークン数は別の量です。この小語彙の例から特定モデルのトークン数を推定することはできません。

3候補スコアを分布へ変える

文脈「制限は」に続く候補を100・200・不明とし、説明用スコア2・1・0をsoftmaxへ渡します。最大値を引いて指数計算の桁あふれを抑えます。

import math指数関数; candidates = ['100','200','不明']; logits = [2.0,1.0,0.0]候補とスコア; softmax = lambda scores,temp: [math.exp((s-max(scores))/temp)/sum(math.exp((v-max(scores))/temp) for v in scores) for s in scores]温度付きsoftmax; probabilities = softmax(logits,1.0)温度1で計算; list(zip(candidates,probabilities)),sum(probabilities)分布と合計

候補100が約0.665で最大です。ここでは仕組みを確認するためスコアを固定しています。100が資料上の正解であるという意味は、この分布だけからは得られません。

確率の合計の表示が1からわずかにずれるのは、浮動小数点計算の丸めによります。

4温度と選び方

温度を下げると最大候補へ集中し、上げると平らな分布へ近づきます。greedy選択は最大候補を選びます。

[(temperature,[round(p,3) for p in softmax(logits,temperature)]) for temperature in [0.5,1.0,2.0]]温度ごとの分布,candidates[max(range(3),key=lambda i: probabilities[i])]最大確率の候補

温度0.5では100への集中が強くなります。温度は資料の真偽を検証する仕組みではなく、候補の選び方へ影響する値です。温度0をこの割り算の式へ直接入れることはできません。

5選んだ候補を文脈へ追加する

次の候補分布は、最初の入力だけでなく、それまでに選んだ候補にも依存します。小さな遷移表でこの関係を確認します。

context = ['制限は','100']現在の文脈; next_tokens = {'100':{'件':0.9,'秒':0.1},'200':{'件':0.8,'秒':0.2}}説明用の次候補表; next_tokens[context[-1]],context+['件']次候補と追加後の列

100を追加した後の候補は件と秒です。候補を一つずつ追加することで文章が伸びます。実際の言語モデルは、この表のように末尾一語だけでなく、利用できる文脈を使って分布を計算します。

6参照する資料を検索する

架空サービスAtlasの旧版v1は上限100件、新版v2は200件とします。質問に含むサービス名と版が何個一致するかで順位を付けます。

documents = [{'id':'d1','service':'Atlas','version':'v1','limit':100},{'id':'d2','service':'Atlas','version':'v2','limit':200}]版ごとの資料; query = {'service':'Atlas','version':'v2'}質問の対象; ranked = sorted(documents,key=lambda doc: sum(doc[key] == value for key,value in query.items()),reverse=True)一致数で順位付け; ranked検索結果

v2のd2が先頭になります。検索は参照候補を選ぶ段階です。単語の一致数だけの検索は意味の理解や網羅性を保証しないため、取得した資料の対象と版を確認します。

7候補文を資料と照合する

候補文がv2の上限100と述べた場合、同じサービス・同じ版の資料の値と一致するかを確認します。

claim = {'service':'Atlas','version':'v2','limit':100}検証する主張; evidence = [doc for doc in ranked if doc['service'] == claim['service'] and doc['version'] == claim['version']]同じ対象と版へ限定; evidence,[doc['limit'] == claim['limit'] for doc in evidence]資料と値の一致

資料d2の値200と候補の100は一致しません。生成候補として100が高確率でも、資料との不一致は解消しません。引用IDを付けただけで内容の裏付けが成立するわけではありません。

8必要な資料が取得されなかった場合

検索対象からv2を除き、残ったv1だけで同じ質問に答えられるかを調べます。

retrieved = documents[:1]旧版だけを取得; matching = [doc for doc in retrieved if doc['service'] == query['service'] and doc['version'] == query['version']]質問に合う資料を抽出; matching一致する根拠の有無

一致資料は空です。v1の100をv2へ流用する根拠はありません。検索拡張生成では、取得の漏れ・版の違い・生成時の取り違えをそれぞれ確認し、資料にない内容を確定した情報として扱わない設計が必要です。

まとめ

トークン数は語彙と分割方式に依存し、文字数やバイト数とは異なります。

候補分布と温度は次の選択を左右しますが、主張の真偽を保証しません。

検索結果の対象・版・値を確認し、生成した主張と資料が実際に対応しているかを照合します。

操作履歴と解説を振り返れます。

Python 3

1
import re; vocabulary = {'再':1,'起動':2,'する':3,'。':4,'再起動':5}; text = '再起動する。'; pieces = re.findall('再起動|起動|する|再|。',text); ids = [vocabulary[piece] for piece in pieces]; pieces,ids,''.join(pieces) == text
(['再起動', 'する', '。'], [5, 3, 4], True)
2
alternative = ['再','起動','する','。']; [vocabulary[piece] for piece in alternative],len(pieces),len(alternative),len(text.encode('utf-8'))
([1, 2, 3, 4], 3, 4, 18)
3
import math; candidates = ['100','200','不明']; logits = [2.0,1.0,0.0]; softmax = lambda scores,temp: [math.exp((s-max(scores))/temp)/sum(math.exp((v-max(scores))/temp) for v in scores) for s in scores]; probabilities = softmax(logits,1.0); list(zip(candidates,probabilities)),sum(probabilities)
([('100', 0.6652409557748218), ('200', 0.24472847105479764), ('不明', 0.09003057317038046)], 0.9999999999999999)
4
[(temperature,[round(p,3) for p in softmax(logits,temperature)]) for temperature in [0.5,1.0,2.0]],candidates[max(range(3),key=lambda i: probabilities[i])]
([(0.5, [0.867, 0.117, 0.016]), (1.0, [0.665, 0.245, 0.09]), (2.0, [0.506, 0.307, 0.186])], '100')
5
context = ['制限は','100']; next_tokens = {'100':{'件':0.9,'秒':0.1},'200':{'件':0.8,'秒':0.2}}; next_tokens[context[-1]],context+['件']
({'件': 0.9, '秒': 0.1}, ['制限は', '100', '件'])
6
documents = [{'id':'d1','service':'Atlas','version':'v1','limit':100},{'id':'d2','service':'Atlas','version':'v2','limit':200}]; query = {'service':'Atlas','version':'v2'}; ranked = sorted(documents,key=lambda doc: sum(doc[key] == value for key,value in query.items()),reverse=True); ranked
[{'id': 'd2', 'service': 'Atlas', 'version': 'v2', 'limit': 200}, {'id': 'd1', 'service': 'Atlas', 'version': 'v1', 'limit': 100}]
7
claim = {'service':'Atlas','version':'v2','limit':100}; evidence = [doc for doc in ranked if doc['service'] == claim['service'] and doc['version'] == claim['version']]; evidence,[doc['limit'] == claim['limit'] for doc in evidence]
([{'id': 'd2', 'service': 'Atlas', 'version': 'v2', 'limit': 200}], [False])
8
retrieved = documents[:1]; matching = [doc for doc in retrieved if doc['service'] == query['service'] and doc['version'] == query['version']]; matching
[]

演習完了