点数は同じなのに、外れている案件が違った
一言でいうと
エージェントを直したあと、スコア1つで良くなったかを判断してはいけません。スコアが同じでも、中で1つが直って1つが壊れていたなら、それは良くなったのではなく、変わったのです。
なぜ必要なのか
問い合わせの分類ルールに「返品」を加えました。それまで「返品したいです」がその他に落ちていたのを、返金として捉えさせるためでした。ゴールデンセットで測ると、正解率はそのままでした。83パーセントから83パーセント。
「そのままなら、少なくとも悪くはなっていない」と考えて、リリースしました。2日後、「配送料は返金されますか」という問い合わせが、すべて配送チームに行っていました。
ルールを直すときに、検査の順序も変えたので、1つが直って1つが壊れました。6件のうち5件が合っているのは同じなのに、合っている5件が違いました。
スコアは、その事実を隠します。スコアは個数で、私たちが知る必要があったのは、名前でした。
どう動くのか
評価を正しく行うには、3つを分けて見る必要があります。
- 品質: ゴールデンセットで何件当たったか。そして、どの件を間違えたか。
- コスト: 1件を処理するのに、ノードが何回動いたか。
- 経路: 同じ入力が通る道が変わったか。
3つとも、同じ実行で測れます。ノードを1枚包めば済みます。
def instrument(name, fn):
def wrapped(state):
started = time.perf_counter()
update = fn(state)
row = PROFILE.setdefault(name, {"calls": 0, "written": 0, "ms": 0.0})
row["calls"] += 1
row["written"] += len(update or {})
row["ms"] += (time.perf_counter() - started) * 1000.0
return update
return wrapped
ここで重要な選択が1つあります。時間も測っておきますが、判定には使いません。同じコード・同じ入力でも、時間はマシンとそのときの負荷によって変わります。「遅くなった」を時間で判定すると、リグレッション検査がぶれ、ぶれる検査は、やがてオフにされます。判定には回数とサイズを使います。時間は、人が覗き込むときの手がかりとして使います。
ゴールデンセットは小さくてよい。ただし変わってはいけない
ゴールデンセットは、「手で正解を付けた入力のまとまり」です。6件でも役に立ちます。条件は、毎回同じものを測ることです。
そのため、ゴールデンセットはコードやファイルに埋め込んでおき、直すときは、直したという事実そのものを記録します。実行のたびにサンプルを新しく選ぶと、昨日と今日を比べられません。
もう1つ。ゴールデンセットには、間違える件を残しておきます。全部当たるゴールデンセットは、「今うまくいっている」ことしか語らず、何も見つけられません。実際に間違える件が入っていてこそ、直したときに直ったものが見えます。
リグレッションはスコアではなく集合で見る
2つのバージョンを比べるときに見るべきものは、この2つです。
- 新しく壊れたもの(newly broken): 旧バージョンで合っていたものが、新バージョンで間違える
- 新しく直ったもの(newly fixed): 旧バージョンで間違えていたものが、新バージョンで合う
2つが同じ数なら、スコアはそのままです。ところが、この2つの重みは、たいてい同じではありません。すでにうまくいっていたものが壊れるほうが、はるかに痛いです。ユーザーがすでにその動作に頼っているからです。そのため、「新しく壊れたものは0か」を先に尋ね、次に「新しく直ったものはあるか」を尋ねる順序が、実務で安全です。
コストと経路もリグレッションする
品質がそのままでも、コストは増えることがあります。分岐を1つ増やせば、ノードがもう1回動き、そのノードが外部を呼び出すなら、その分お金が出ていきます。そのため、ノードの呼び出し回数もリグレッション指標として残します。時間と違って、この数値は同じ入力でいつも同じです。
経路も同じです。同じラベルが出ても、別の道を通って出たなら、それは別の動作です。今は答えが同じに見えても、次の変更で違う方向に分かれます。足跡(trace)を比べると、これが見えます。
現場での姿
1つ目: スコアだけを記録する。上の事故です。どの件が間違ったかを残さないと、振り返れません。
2つ目: ゴールデンセットが実行のたびに変わる。ランダムなサンプルで測ると、数値が毎回違って、リグレッションなのかサンプルのせいなのかを分けられません。
3つ目: 時間でパフォーマンスのリグレッションを判定する。検査がぶれて、ぶれる検査は無視され、結局オフにされます。
4つ目: 観測をあとから付けようとする。ノードを包む場所は、グラフを作るときが最も安いです。あとで付けると、ノードごとに直す必要があり、1か所でも抜けると、そのノードだけが帳簿に残りません。
実務で本当に大切なこと
- スコアと一緒に、間違えた件の名前を残します。振り返れる唯一の記録です。
- リグレッションは、新しく壊れたものと新しく直ったもので分けます。壊れたほうを先に見ます。
- 判定には回数とサイズを使い、時間は手がかりとしてだけ使います。
- コストと経路もリグレッション指標です。答えが同じでも、道が変わったなら、変わったのです。
次のラボですること
/root/work/ageval/evalkit.pyを、1ステップずつ育てます。まず、問い合わせを分類するグラフを作り、ノードを包んで、呼び出し回数と書いたキーの数を帳簿に残します。どのノードが最も忙しいかを取り出し、手で正解を付けたゴールデンセットで、正解率と間違えた件のリストを出します。次に、ルールを直した2つ目のバージョンを作って2つのバージョンを比べ、スコアは同じなのに、どの件が壊れて直ったかを、名前で分けます。最後に、呼び出し回数と経路の変化まで測って、記録に残します。