Digital Reactor
機械学習

プロンプトを書き換えずにLLMの挙動を変える:Function VectorとSteering Vectorの実力と限界

プロンプトを書き換えずにLLMの挙動を変える:Function VectorとSteering Vectorの実力と限界

はじめに

対義語を返す、表記ゆれを正規化する、といった小さな変換をLLM(大規模言語モデル)に任せるとき、定番なのは「Q: hot / A: cold」「Q: big / A: small」のような例文を数組プロンプトの先頭に貼り、最後に本題の1語だけを「Q: deep / A:」の形で置く書き方です。モデルは例文の並びから「対義語を答えるタスクだ」と汲み取り、deepにshallowを返します。例文で挙動を教えるこの書き方をfew-shotと呼び、以降この記事では、この形のプロンプトを対義語プロンプトと呼びます。全体で51トークンのうち45トークン、9割近くが例文です。1回の呼び出しなら誤差ですが、月100万回呼ぶ機能なら、月4,500万トークンを例文のためだけに払う計算になります。

この例文の働きを、ベクトル1本で置き換えられるという研究があります。Function Vectors(Todd et al., 2023)です。例文つきプロンプトを処理しているモデルの内部から「いま実行しているタスク」を表すベクトルを取り出しておき、例文のないプロンプトの処理中にそれを注入すると、例文を一度も見せていないのにタスクが実行されるという結果が報告されています。同じ発想で生成の話題や感情を動かす技術に、Steering Vector(ステアリングベクトル)があります(Turner et al., 2023)。

手元のCPUとgpt2-medium(3.55億パラメータ)で、この置き換えがどこまで効いて、どこで壊れるかを確かめました。例文5本つきで82.5%だった対義語の正解率は、例文を外すと0%になります。そこへベクトルを1本足すだけで、正解率は7割近くまで戻りました。ただし効くのは注入する層とベクトルの作り方を正しく選んだときだけで、ステアリングも係数を上げすぎると文章そのものが壊れます。うまくいく条件と壊れる条件の両方を、実際の数値で見ていきます。

few-shotとベクトル注入の対比。例文5組つきのプロンプトでは対義語の正解率82.5%。例文なしのプロンプトでも、途中の層にFunction Vectorを加算すると67.5%まで戻る。

対象読者:

  • few-shotの例文で動かしている定型変換・分類タスクのプロンプトコストを下げたい方
  • Function Vectorやactivation steeringが実務に持ち込める技術なのか、根拠を持って見積もりたい機械学習エンジニア
  • プロンプト・モデル内部への介入・追加学習という3つの手段を、可逆性や監査のしやすさで比べたい方

記事のポイント:

  • 例文つきプロンプトの最終トークン位置で平均した残差ストリーム hh の注入では、正解率は0%から12.5%までしか戻りません。因果効果で選んだ10ヘッドから作るFunction Vectorは、同じ40問を67.5%まで戻します
  • ヘッドの因果効果は、出力を平均に差し替えるだけではほとんど見えません。例文の対応を崩したプロンプトにパッチする、Function Vectors論文の測り方まで実装します
  • Steering Vectorは係数0.5で文を保ったまま効きはじめ、係数2で効果が最大になり、その先は効果ごと文が壊れます。壊れの進み方も、効果とは別の指標で数値化します

対義語プロンプト1本を分解する

導入で触れた対義語プロンプトの実物を、まず1本そのまま見ます。例文5組とクエリ1語からできていて、このようにプロンプト内の例示から振る舞いを学ばせる使い方をin-context learning(ICL)と呼びます。

Q: hot
A: cold

Q: big
A: small

Q: early
A: late

Q: win
A: lose

Q: true
A: false

Q: deep
A:

gpt2-mediumのトークナイザでこのプロンプトは51トークンになり、末尾の「Q: deep\nA:」だけなら6トークンです。差の45トークンが例文の代金です。モデルはこれを読み、最後のトークン「:」の位置で次のトークンを予測します。このプロンプトでの予測1位は「 shallow」で、正解です。

正解判定はこの次トークン1個で行います。答えの語の先頭サブワード(「 shallow」のように先頭スペース込みで符号化したときの最初のトークン)と、モデルの予測1位が一致すれば正解とする、緩めの基準です。短い語はほぼ1トークンに収まるので、実用上は完全一致に近い判定になります。

モデルにはgpt2-medium(Transformerブロックが24層、内部のベクトルは1,024次元)を選びました。事前の小さな測定でfew-shot正解率が82.5%出て、CPUで動くサイズとしてはこれで足りると判断したためです。自前で用意した対義語72組のうち評価用の40問での数字で、例文なしのzero-shot(「Q: deep\nA:」だけを渡す)は同じ40問で0%でした。zero-shotのモデルはタスクが何かを知らされていないので、当然の結果です。

モデルの内部では、この1,024次元のベクトルが各トークン位置に1本ずつあり、24層のブロックがそれを順に書き換えていきます。この層から層へ受け渡されるベクトルの列を残差ストリーム(residual stream)と呼びます。アテンションもMLPも、残差ストリームへの加算として働きます。最後のトークン位置の残差ストリームは、層を経るごとに「対義語タスクをやっている」「クエリはdeep」といった予測に必要な情報を集め、最終層でそれが次トークンの確率に読み出されます。

ここで調べたいベクトル hh を定義します。例文つきプロンプトを1本流し、層 \ell の出力の最終トークン位置のベクトルを取り出します。これをプロンプト40本(クエリも例文の組み合わせも毎回変える)で繰り返して平均したものが hh_\ell です。transformersなら1回のforwardで全層ぶん取れます。

out = model(input_ids=ids, attention_mask=mask, output_hidden_states=True)
# hidden_states[l] は第 l ブロックの出力 (batch, seq, 1024)。最終実トークン位置を平均
h_vecs = torch.stack([out.hidden_states[l][rows, last_idx].mean(dim=0)
                      for l in range(1, n_layers + 1)])

平均を取るのが要点です。プロンプトごとに違う成分(クエリがdeepかhotか、例文がどれか)は40本の平均でならされて消え、全プロンプトに共通する成分が残ります。実際に抽出した層12の hh は、先頭の5成分が (0.86, 0.17, 1.82, 2.07, 0.02)(0.86,\ -0.17,\ 1.82,\ 2.07,\ 0.02)、ノルムが164.9という、見た目にはただの実数の並びです。この1,024個の数の中に「対義語タスク」が入っているかは眺めても分からないので、注入して確かめます。

タスクは残差ストリームに乗っているか

hh_\ell が本当にタスクを運んでいるなら、例文のないzero-shotプロンプトの処理中にそれを足せば、モデルは対義語を返し始めるはずです。やることは単純で、「Q: deep\nA:」を流しながら、層 \ell の出力の最終トークン位置に hh_\ell を加算するだけです。transformersではforward hookで書けます。

def batch_predict(model, enc, last_idx):
    """バッチ forward し、各行の最終実トークン位置の argmax トークンを返す。"""
    with torch.no_grad():
        logits = model(input_ids=enc.input_ids, attention_mask=enc.attention_mask).logits
    rows = torch.arange(len(last_idx))
    return logits[rows, last_idx].argmax(dim=-1)


def add_at_last_token(block, vec, last_idx):
    def hook(module, inputs, output):
        hs = output[0] if isinstance(output, tuple) else output
        rows = torch.arange(hs.shape[0])
        hs[rows, last_idx] += vec
    return block.register_forward_hook(hook)


acc_h_by_layer = []
for l in range(n_layers):
    hk = add_at_last_token(blocks[l], h_vecs[l], last_zs)   # blocks = model.transformer.h
    preds = batch_predict(model, enc_zs, last_zs)
    hk.remove()                                             # 外せば元のモデルに戻る
    acc_h_by_layer.append(accuracy(preds, golds))

enc_zs はzero-shotプロンプト40本をまとめてトークナイズしたもの、golds は正解語の先頭トークンです。層スイープの本体はこの十数行で、評価40問の正解率を層ごとに測りました。

例文なしプロンプトにhまたはFunction Vectorを層ごとに注入したときの対義語正解率。破線はfew-shotの0.82、点線はzero-shotの0。hは層12・13で0.125まで持ち上がるにとどまり、FVは層4から13の広い範囲で0.5前後から0.675の正解率を示す。

結果(図の青い線)は層に強く依存しました。前半の層(1〜10)ではまったく効かず0%のまま、層11〜13で持ち上がって層12・13の12.5%が最大、それより深い層では5%前後に落ちます。数字としては40問中5問で、few-shotの82.5%にはほど遠いものの、例文を1本も見せていないプロンプトが対義語を返し始めたこと自体は再現できました。残差ストリームにタスクの情報が乗っているという論文の主張は、この小さいモデルでも確認できます。

効く層が中間に限られるのには、両側にそれぞれ理由があります。浅い層の hh には、例文からタスクを集約する計算がまだ終わっておらず、足せるだけの信号が入っていません。深すぎる層に足すと、今度は答えを組み立てる計算がすでに終わっていて、足した情報を消費する層が残っていません。Function Vectors論文でも、深い層への注入は効かないと報告されています。

それでも12.5%は物足りない数字です。hh には40本のプロンプトに共通する成分が全部入っています。「対義語タスクである」という情報のほかに、「Q:とA:が交互に並ぶ形式」「答えは英単語1語」といった成分も乗っていて、タスクの信号はその中に埋まっています。純度の高いベクトルにするには、タスクを運んでいる部品を特定して、そこだけを取り出す必要があります。それが次のFunction Vectorです。

どのアテンションヘッドがタスクを運んでいるか

Function Vectors論文の中心的な主張は、タスクを運んでいるのは少数のアテンションヘッドであり、そのヘッドの出力だけを足し合わせたベクトルのほうが、残差ストリームの丸ごとの平均より素性がよい、というものです。まず、そのヘッドを特定します。

どのヘッドが効いているかは、因果効果で測ります。gpt2-mediumのアテンションは1層あたり16ヘッドで、各ヘッドは64次元の出力を書きます。実装上は、16ヘッドの出力を連結した1,024次元のベクトル zz が出力射影c_projに入る直前に並んでいるので、そこをhookで切り出せばヘッド単位で操作できます。あるヘッドの出力を、例文つきプロンプト40本での平均値に差し替えて(mean ablation)、例文つきプロンプトの正解率がどれだけ下がるかを測ります。プロンプトごとに変わる情報をそのヘッドが運んでいるなら、平均への差し替えでその情報が消え、正解率が下がるはずです。

def replace_z_head(attn_cproj, head, d_head, new_val, last_idx):
    def hook(module, args):
        z = args[0].clone()
        rows = torch.arange(z.shape[0])
        z[rows, last_idx, head * d_head:(head + 1) * d_head] = new_val
        return (z,) + args[1:]
    return attn_cproj.register_forward_pre_hook(hook)

24層×16ヘッドの全384通りを評価プロンプト32本で測ると、予想に反してほとんど何も起きませんでした。正解率の低下は最大でも3.1ポイント(32問中1問)で、どのヘッドを平均に差し替えても正解率が動きません。考えてみれば理屈に合った結果です。

タスクの情報を運ぶヘッドは、同じタスクのプロンプト40本でほぼ同じ出力を書いているはずなので、平均に差し替えても何も消えません。クエリ固有の情報のほうは複数のヘッドが重複して運んでいれば、1個を消しても残りが補います。差し替えたつもりで、どちらの情報も壊せていませんでした。

Function Vectors論文は、この問題を壊れたプロンプトを使って回避しています。例文のQとAの対応をシャッフルして崩したプロンプト(hotの答えがfalseになっているような例文列)を用意すると、同じ32問の正解率は84.4%から12.5%に落ち、正解トークンの平均確率は1.9%になります。この崩れたプロンプトの最終トークン位置に、正しい例文で測っておいたヘッドの平均出力を1ヘッドぶんだけパッチして、正解トークンの確率がどれだけ回復するかを測ります。タスクの信号を書き戻せるヘッドだけがこの測り方で浮かび上がり、論文はこの回復量をcausal indirect effect(CIE)と呼びます。以降のヒートマップと上位ヘッドの選定はCIEで行いました。

層×ヘッドのヒートマップ。各セルは、ラベルを崩したプロンプトにそのヘッドの平均出力をパッチしたときの正解トークン確率の回復量。層10から12のヘッドに効果が集中し、層12・ヘッド4が突出して大きい。

効果は中間層の数ヘッドに集中しました。上位5ヘッドを並べます。

順位ヘッド正解トークン確率の回復正解率の回復
1層12・ヘッド4+4.1ポイント+21.9ポイント
2層11・ヘッド8+2.4ポイント+9.4ポイント
3層10・ヘッド4+1.4ポイント+9.4ポイント
4層10・ヘッド7+0.8ポイント0.0ポイント
5層18・ヘッド12+0.5ポイント−3.1ポイント

1ヘッドのパッチだけで正解率を21.9ポイント動かす層12・ヘッド4が突出していて、上位3ヘッドは層10〜12に固まっています。4位以下は確率をわずかに押し上げるだけで、正解率は動かないか、むしろわずかに下がります。上位10位のヘッドですら回復は0.3ポイント程度で、そこから漏れた374ヘッドはそれ未満です。全384ヘッドのうち、このタスクを運んでいるのは実質数個でした。

上位10ヘッドの平均出力を、それぞれc_projの該当スライスで残差ストリーム側の1,024次元に射影してから足し合わせたものがFunction Vector(FV)です。

fv = torch.zeros(model.config.n_embd)
for l, hd in top_heads:
    W = blocks[l].attn.c_proj.weight  # out = z @ W + b(バイアスは足さない)
    sl = slice(hd * d_head, (hd + 1) * d_head)
    fv += mean_z[l, sl] @ W[sl, :]

hh との違いは足しているものの範囲です。hh は層の出力の丸ごと平均で、1,024次元の全部にタスク以外の成分も乗っています。FVはCIEで選んだ10ヘッドぶんの寄与だけの和で、次元こそ同じ1,024ですが、中身は選別されています。平均ヘッド出力は hh と同じ抽出用プロンプト40本で測りました。このFVを、hh と同じ層スイープでzero-shotプロンプトに注入した結果が、先ほどの図の赤い線です。

ピークは層8の67.5%で、hh の12.5%に対して5倍以上の開きがあります。効く層の幅も広く、層4から13までのどこに注入しても47.5%以上が出ます。hh と違ってFVは固定のベクトルなので、早い層に足しても残差ストリームに乗ったまま運ばれて、中間層の読み手に届きます。効かなくなるのは層14以降、答えの読み出しが済んだあとに足したときだけです。ノルムは61.5と hh(164.9)の4割以下で、小さいベクトルのほうがよく効いています。残差ストリームの丸ごとではなく、タスクを運ぶ部品の出力だけを足すという論文の設計が、そのまま数字に出ました。

集計値だけでは1件ずつに何が起きたのか見えないので、評価40問から6問を抜き出して、3通りの出力トークンを並べます。

クエリ正解注入なしhh 注入(層12)FV注入(層8)
deepshallowdeepdeepdeep
wetdrywetwetdry
frontbackfrontbackback
acceptrejectyesacceptreject
winlosewinwinlose
arrivedepartarrivearriveleave

注入なしの列がタスクを知らないモデルの素の姿で、「Q: deep\nA:」の続きとしてクエリと同じ語を繰り返すだけです。hh 注入はfrontをbackに直せた一方、acceptでは素の出力yesをおうむ返しのacceptに変えただけで、対義語まで届いていません。FVはwet・accept・winを正しい対義語に変え、deepだけは3通りのどれでも動かせませんでした。

arriveの行は採点の限界も見せています。FVの出力leaveは意味としては対義語なのに、正解語departの先頭トークンと一致しないため不正解と数えており、67.5%はこの意味で辛めの数字です。

「I hate you because」の続きは、ベクトル1本で愛情表現に変わるか

ここまでの対義語は、正解のあるタスクでした。同じ「内部にベクトルを足す」仕組みで、正解のない文章生成の方向を動かす技術がSteering Vectorです。この節の実験の目標を先に置きます。素のgpt2-mediumに「I hate you because」の続きを書かせると、不満や別れ話のような文が出てきます。プロンプトには一切手を触れず、生成中のモデル内部に「憎しみから愛情へ向かう方向」のベクトルを足し続けることで、この続きを愛情表現に変えられるか。それがここで確かめることです。

その方向のベクトルは、対比になる2語から作ります。「 Love」と「 Hate」をそれぞれモデルに読ませ、同じ層・同じトークン位置の残差ストリームを取り出して引き算し、その差を vv と書きます。2語に共通する成分(1語の英単語である、文頭にある、など)は引き算で消え、LoveとHateを分ける成分だけが vv に残る、という発想です。

# " Love" と " Hate" はどちらも1トークン。BOSを前置して同じ位置で差を取る
ids_love = torch.tensor([[tok.eos_token_id] + tok.encode(" Love")])
ids_hate = torch.tensor([[tok.eos_token_id] + tok.encode(" Hate")])
out_love = model(input_ids=ids_love, output_hidden_states=True)
out_hate = model(input_ids=ids_hate, output_hidden_states=True)
steer_vec = (out_love.hidden_states[STEER_LAYER][0, -1]
             - out_hate.hidden_states[STEER_LAYER][0, -1])

実装には落とし穴が1つありました。最初は「Love」「Hate」をそのまま符号化したのですが、GPT-2のトークナイザでは「Hate」だけが2トークンに割れるため、差が位置0と位置1のベクトルの引き算になりました。GPT-2は先頭位置の活性が桁違いに大きく、位置に紐づく成分が差に残って、ノルム3,800超の壊れたベクトルができます。1トークンに収まる表記を選び、文頭を表す特殊トークン(BOS)を前置して同じ位置で引くと、ノルムは77.5に収まりました。注入する層は前半寄りの層8に固定しています(Turnerらも前半寄りの層を使っています)。

生成中は、この vv を係数 cc 倍して、全トークン位置の残差ストリームに足し続けます。係数は効きの強さのつまみで、0なら素のモデルのまま、大きくするほど強く押します。cc を0、0.25、0.5、1、2、4、8、16と振り、4本のプロンプトそれぞれで6回ずつ、40トークンの続きをサンプリング生成しました(do_sampleで温度0.9、top-p 0.95に固定し、係数1水準あたり24本を生成しています)。

数値化の前に、実際の生成がどう変わるかを6水準で見ます。太字がプロンプト「I hate you because(君が憎い、なぜなら)」で、その続きをモデルが生成しています。

係数プロンプト+生成の冒頭
0I hate you because I want to be happy.” She replied “I want to be happy but I’m tired of doing everything for everyone around me.(「幸せになりたいから憎いのよ」と彼女は答えた。「幸せになりたいのに、みんなの世話ばかりで疲れたの」)
0.25I hate you because I love you and you don’t love me. That’s how it feels, that’s how I feel with you. Letting go.(「愛しているのに君は愛してくれない、だから憎い。それがこの気持ちだ。もう手放すよ」。不満の文の骨格のまま、loveの語が混ざりはじめます)
0.5I hate you because you love me. You are beautiful.” I love you because you are amazing.(「君が僕を愛しているから憎い。君は美しい」。愛している、君は素晴らしいから。文はまだ崩れていません)
1I hate you because you love a girl.” “I want to be with you forever. I love you and I want everything to be perfect! I love you and I’m crazy.(「永遠に一緒にいたい。愛している、すべてを完璧にしたい!」。愛情一色になり、同じ文句の繰り返しが増えてきます)
2I hate you because I love your smile.” The Moment is from the Moment! Ra was beautiful, with her husband,(「君の笑顔が大好きだから憎い」。ここまでは通じるものの、この後は名前の羅列に崩れていきます)
16I hate you because Ell Ell First Noir’s Alchemy Ell E, Ell, and, Mur Alaska(続きが訳せません。語の羅列です)

生成が段階的に変わっていくのが読み取れます。係数0.25では不満の文の骨格はそのままに愛情の語が混ざり、0.5で続きが英語として読める文のまま愛情表現に変わります。係数1では愛情一色になる代わりに同じ文句の繰り返しが目立ちはじめ、2では後半が名前の羅列に崩れ、16では英語の体をなしません。効きが強まるのと引き換えに、文章の壊れが進んでいきます。

この「効果」と「壊れ」を、別々の指標で数値化します。効果は「愛情系の語彙が出たか」で、loveの活用形やwonderful・beautiful・happyなど15語を辞書にし、40トークンの生成に1語でも出た割合を取ります。壊れのほうは、生成された補完をステアリングなしの同じモデルで読み直し、1トークンあたりの負の対数尤度(NLL; negative log-likelihood)を測ります。素のモデルから見て「あり得ない」文字列ほど高くなる、流暢さの逆数のような量です。当初は繰り返し率(distinct-2の補数)で壊れを測るつもりでしたが、実際の壊れ方は同じ語のループではなく固有名詞と読点の羅列で、繰り返しの指標では捕まえられませんでした。

ステアリング係数を0から16まで振ったときの、愛情語彙を含む生成の割合(左)と、介入なしのモデルで読み直した1トークンあたりNLL(右)。効果は係数0.25から段階的に上がって係数2の0.96がピーク、その先で崩れる。NLLは係数0.5まで素の2.5前後のままで、係数1から上がりはじめ係数4で8.4に達する。

効果の立ち上がりは早く、係数0の16.7%に対して、0.25で37.5%、0.5で54.2%、1で75.0%と段階的に上がり、係数2の95.8%が最大でした。一方、右のパネルのNLLは係数0.5までは素の2.46とほぼ変わらず(2.30〜2.40)、係数1で3.16、係数2で6.08、係数4で8.36と上がっていきます。係数0.5前後までは、文を保ったまま生成の方向だけが動いている、ということです。先ほどの表の係数0.5の生成がまさにその状態で、係数2の「効いているのに崩れている」状態とは、指標の上でも区別がつきます。

係数4では愛情語彙の割合が37.5%に落ち、係数8で0%になります。文が英語の体をなさなくなり、辞書に載る語そのものが出てこなくなるためです。係数16で16.7%に戻って見えるのは効果ではなく指標の誤作動で、語の羅列の中に偶然LovingやLoveが混ざっただけです。

崩れはじめる位置は、ノルムで考えると見当がつきます。vv のノルム77.5は、注入先である層8の残差ストリームのノルム183.7の4割あります。文が保たれていた係数0.5では残差ストリームの2割ほどの量を足しているのに対し、係数2では同じ規模の量を毎トークン位置に足し続けている計算で、この比が壊さずに押せる量の目安になります。効果と流暢さが両立する幅は係数0.5から1のあたりにあり、そこから2倍動かしただけで壊れる側に入ります。ステアリングの元実験が15億パラメータのGPT2-XLで行われていたことを踏まえると、3.55億のgpt2-mediumではこの幅が特に狭いのだと考えられます。係数を上げるほど効きが強くなる関係は係数2までで、その先は文が崩れて効果ごと消えます。

プロンプト・ベクトル注入・追加学習をどう選ぶか

同じ「モデルの振る舞いを変える」でも、few-shotプロンプト、ベクトル注入、追加学習(LoRAなどのファインチューニング)では、費用の出どころも失敗の出方も違います。受託でこの種の相談を受けるときに見る軸で並べます。

観点few-shotプロンプトベクトル注入(FV・ステアリング)追加学習(LoRA等)
トークンコスト例文ぶんを毎回払う払わない(推論コードにhookを足す)払わない
使える環境どこでも(API越しでも)自前でモデルを動かしている場合だけ学習環境と再デプロイが要る
可逆性プロンプトを差し替えれば即戻るhookを外せば即戻る。重みは無傷切り戻しはモデルの再デプロイ
監査のしやすさ例文はログに残り、人が読めるベクトルの中身は人が読めない学習データと差分で説明するが解釈は難しい
壊れ方例文の増減で劣化は緩やか、出力を見れば分かる係数次第で急に破綻する。破綻自体は生成を見れば分かるが、注入し忘れ・層違いは静かに素の挙動へ戻る過学習・他タスクの劣化が静かに進む

トークンコストの節約が動機なら、ベクトル注入の前に確かめることが2つあります。1つは、そもそもAPI越しのモデルには使えないことです。残差ストリームに触るには自前でモデルを動かしている必要があり、その時点で対象はローカルLLMや自社ホスティングのモデルに限られます。

もう1つはPrompt Cachingです。例文が毎回同じなら、キャッシュで例文部分の単価は大きく下がります。何をどれだけ使い回せるかの損益分岐はエージェントに何を覚えさせるかの記事で式にしたとおりで、固定の例文はキャッシュが効く典型例です。ベクトル注入がコスト面で意味を持つのは、キャッシュしてもなお入力長が問題になる場合か、推論を自前で持っていてトークン課金の外にいる場合です。

可逆性はベクトル注入の強みです。追加学習は重みを書き換えるので、問題が出たときの切り戻しがデプロイ作業になります。ベクトル注入はforward時に足すだけで、hookを外せばその瞬間に元のモデルです。A/Bテストで注入あり・なしを同じプロセス内で切り替えることもできます。

監査のしやすさは逆に弱点です。few-shotの例文は人が読めて、レビューも差分管理もできます。ベクトルは1,024個の実数で、中に何が入っているかを直接読む方法がありません。この記事でやったように挙動で検証する(層スイープや係数スイープで正解率や破綻の指標を測る)のが現実的な検査で、内部の値そのものから意味を取り出す話は、活性を入力に分類器を訓練する線形プローブのような「読み取る側」の技術になります。書き込み(ステアリング)と読み取り(プローブ)は同じ活性空間の線形構造を使う表裏の関係です。

壊れ方の違いが問題になるのは、運用に入ってからです。few-shotの劣化は出力に表れて気づきやすく、ステアリングの破綻も生成を読めば分かります。注意すべきは静かな失敗のほうで、注入する層を間違えた・hookの登録が外れていた、という場合、モデルは単に素の挙動に戻るだけでエラーは出ません。zero-shot正解率のような監視指標を1つ置いておかないと、効いていないことに気づけません。

まとめ

例文5本で82.5%出る対義語タスクは、例文を外すと0%になり、残差ストリームを丸ごと平均した hh の注入では12.5%までしか戻りませんでした。ヘッド単位の因果効果で選んで組んだFunction Vectorは同じ40問を67.5%まで戻し、しかも層4から13までの広い範囲で効きます。ベクトル注入そのものより、何を足すかの選別が結果を分けました。ステアリングは係数0.5前後なら文を保ったまま方向を動かせて、係数2の効果最大を過ぎると効果ごと文が壊れます。few-shotの完全な代替ではなく、自前でモデルを動かしていて、注入の質を数値で監視できる場面に限れば例文コストを大きく削れる技術、というのが手を動かした後の評価です。

試すなら順番は本文と同じで構いません。まず自分のタスクでfew-shotとzero-shotの正解率の差を測り、その差がベクトル1本でどれだけ埋まるかを層スイープで確かめてください。hh の層スイープだけなら本文に載せた十数行で書けて、gpt2-medium級のモデルならCPUで数分で回ります。そこで戻り幅が見えたら、ヘッドの因果効果まで踏み込んでFVを組む価値があります。戻り幅が足りなければ、例文を残したままキャッシュで単価を下げる選択に戻るのが、コスト削減という当初の目的に合っています。

関連記事

← 技術ブログ一覧へ