テックブログ
医療AIの精度は、現場が直した記録で測る — 本番音声56本を毎回機械採点する
目次
This article is also available in English.
医療AIの精度をどう確かめているか、と聞かれたときに答えやすいのは「テスト用の音声を何本通したか」です。数えられますし、増やすほど安心にも見えます。
けれど本数は、精度の証拠になりません。集めた音声に入っていない形の誤りは、何本通しても見つからないためです。私たちは診察の音声から記録の下書きを作っていて、その中の「次回予定」の行の日付を作り直したときに、この差をはっきり測ることになりました。以下は、そのときに組んだ評価の作り方です。
先に、言葉の整理
以降で使う言葉をまとめます。ここだけ読んでも、後の数字の意味が分かるように書いています。
| 言葉 | どういうものか |
|---|---|
| 文字起こし | 音声を文章に変換する処理。同じ音声でも結果は毎回わずかに変わる |
| 評価セット | 精度を測るための「入力と正解」の組。集め方が偏ると、偏った範囲でしか測れない |
| 標本の偏り | 手元に集めた音声の傾向が、実際の診療の傾向とずれていること。本数を増やしても解消しない |
| 回帰 | 直した箇所とは別の場所が、以前より悪くなること |
| 変異注入 | 守りをわざと壊した入力を流し、検査が止まるかを確かめる方法 |
| 母数 | 割合を出すときの分母。これが動くと、率だけが動いて見える |
| 履歴テーブル | 記録の版を1つずつ残す表。隣り合う版を比べると、人が直した箇所が分かる |
| 決定的置換 | 生成された文章の一部を、規則どおりに必ず同じ形へ置き換える処理 |
| 読み取り専用ロール | 書き込みができないデータベースの接続。集計の道具が記録を壊せないようにする |
テストを通った本数は、精度の証拠にならない
次回予定の日付は、患者さんへの案内にそのまま転記される欄です。ここを改善したあと、私たちは本番の記録を数え続けていました。現場の方が次回予定の行を手で直した割合を見る、という測り方です。
数え直したところ、手直しの割合は改善前より上がっていました。4.8%から9.6%へです。直前のテストでは音声20本のうち16本から18本が合格していたので、本数だけを見ていれば「良くなった」と判断したはずです。
差が出た理由ははっきりしています。テスト音声20本には、そのとき見つかった型が入っていました。一方で本番の157件には、同じ診察の中で予定が言い直される会話が含まれていました。
「4週間後に来てください」 「……その日は休みなので、10月19日にしましょう」
このとき、先に言われた「4週間後」を採ると10月12日になります。会話の最後に決まったのは10月19日です。テスト音声にこの形が1本も入っていなければ、何本通しても気づけません。
測る場所を本番に移すと、この手の見落としが自動的に入ってきます。 現場が直した記録には、AIが書いた答えと、人が直した答えの両方が残っているからです。正解を人が作り直す必要がありません。
日付は「優先順位」ではなく「証拠の強さ」で決める
言い直される会話を扱うには、どの日付を採るかの決め方そのものを変える必要がありました。
改善前は「相対表現(4週間後)を、明示された日付(10月19日)より強く採る」という順番でした。音声認識では月の名前が別の月に化けることがあるので、化けにくい相対表現を優先する、という考え方です。この判断自体は間違っていません。
ただしこの並べ方だと、会話の最後に決まった日付が捨てられます。 そこで順番を「どちらが強いか」ではなく、どちらがより確かな証拠かで並べ直しました。
| 段 | 何を見るか | 例 | なぜこの位置か |
|---|---|---|---|
| 1 | 曜日が暦と一致する明示日付 | 「10月19日の日曜に」 | 日付と曜日が両方合う確率は低い。合っていれば聞き間違いではない |
| 2 | 会話の中で最終的に合意された日付 | 「やっぱり10月19日で」 | 言い直しの結論。会話の流れが証拠になる |
| 3 | 「後」「先」つきの相対から±7日以内の明示日付 | 「4週間後……10月19日に」 | 相対表現と近ければ、同じ予定を指している |
| 4 | 曜日から月を訂正した日付 | 「10日の木曜」 | 曜日が合う月が2か月以内に1つだけなら確定できる |
| 5 | 受診の文脈で1つに定まる明示日付 | 「次の受診は10月1日」 | 同じ会話に別件の日付が並ぶときの選別 |
| 6 | 相対表現 | 「4週間後」 | 明示日付が1つも無いときだけ使う |
月単位の言い方は、この表のどこにも入れていません。「1ヶ月後」は診察日と同じ日ではなく「来月のいつか」だからです。月単位で話されたときは日付を作らず、言い方のまま残します。 本番の記録を見ると、月単位に日付を添えた行は3割が現場で直されていた一方、日付の無い月単位の行に人が日付を足した例はありませんでした。
この並びは、そのまま実装の形になっています。上から順に当てて、最初に当たった段の答えを採るだけです。
// 証拠の強さの順に並べた規則。並びそのものが仕様なので、
// 規則を足すときは「どの段に入れるか」も必ずレビューの対象にする
const RULES = [
{ id: 'explicit_with_weekday', note: '日付と曜日が暦と一致する' },
{ id: 'agreed_last', note: '会話の最後に合意された日付' },
{ id: 'explicit_near_relative', note: '相対表現から前後7日以内の明示日付' },
{ id: 'weekday_corrected', note: '曜日から月を訂正できる' },
{ id: 'explicit_unique', note: '受診の文脈で1つに定まる' },
{ id: 'relative_only', note: '明示日付が1つも無いときだけ' },
] as const; // 判定の中身は省いています
// 当たった段の id も一緒に残す。あとで「どの段が効いたか」を数えられる
const decide = (u: Utterance[]) =>
RULES.map((r) => ({ id: r.id, hit: match(r.id, u) })).find((x) => x.hit) ?? null;採った日付だけでなく、どの段で決まったかを記録に残しているのが要点です。週ごとに段の内訳を数えると、現場の話し方の変化が段の分布として現れます。正解率が下がってから調べるのではなく、分布が動いた時点で気づけます。
決まっている値は、AIではなくコードが書く
決め方が固まっても、その答えがそのまま記録に載るとは限りません。AIに「この日付を使ってください」と渡しても、別の日付が書かれる回が残ります。長い指示文の一部は無視されることがあるためです。
そこで、AIが書いた記録の日付と相対表現の語だけを、生成のあとにコードが差し替える形にしました。
触れる範囲を語に限っているのが要点です。行ごと書き換える形も検討しましたが、その形だと医療者が書いた補足や時刻まで消えることがあります。保存済みの臨床の文章を機械が広く書き換えるのは、私たちが最も避けたい操作です。
この種の処理には安全弁を必ず付けます。ここには、一度失敗して学んだ数え方の決まりがあります。
const out = replaceDateWords(line, decided); // 置き換えるのは日付と相対表現の語だけ
// 🔴 安全弁は「候補の数」ではなく「実際に書き換えた数」で数える。
// 候補で数えると、**正しく書けている記録ほど上限に達して全部捨てられる**という、
// 直感と逆の挙動になる(うまくいっている記録でだけ直らない)
if (out.replacedCount > MAX_REPLACEMENTS) {
logger.warn('置換数が上限を超えたため、生成された文章のまま残す', {
replaced: out.replacedCount, // 🔴 本文は載せない
});
return line;
}入力の検査も、通す形を並べるのではなく、危ない文字を弾く形にしています。「氏名は漢字とかな」のような思い込みで作った通過条件は、実データを弾きます。実際、本番の氏名には環境によって形の違う漢字や全角の記号が含まれていました。
56本を9周、毎回すべて機械で採点する
設計を変えるたびに、本番から集めた音声56本を毎回すべて流し直しました。 1本ずつ聞いて判断するのではなく、画面と同じ判定の仕組みに通して、人の答えと一致するかを機械で採点します。
| 周 | 合格 | このとき分かったこと |
|---|---|---|
| 1周目 | 36本中20本 | 明示日付を捨てる形が主因だと確定 |
| 2周目 | 36本中31本 | 「M月のD日」のように助詞を挟む言い方を拾えていなかった |
| 4周目 | 36本中35本 | 同じ会話に別件の予定が混ざる形が残っていた |
| 6周目 | 56本中54本 | 残り2本は同じ音声でも文字起こしが回ごとに変わる例 |
途中で分かったことが2つあります。ひとつは、助詞を挟む言い方が全体の4割を占めていたこと。「10月の8日」という形は本番の161件のうち59件あり、以前の読み取りはこの形を見ていませんでした。テスト音声を集めるだけでは、この比率には辿り着けません。
もうひとつは、同じ音声を流しても文字起こしの結果が毎回わずかに変わることです。56本のうち2本が、回によって「来月」と「10月1日、木曜日」のあいだで揺れました。だから1回の採点結果で良し悪しを断定せず、周回して見ています。
周回はここで終わりではありません。レビューの指摘を直したあと、同じ56本をさらに3周流し直しています。9周目は53本が合格、2本が参考、1本が文字起こしの揺れによる不合格で、人の答えとの食い違いは0件でした。
守りを入れたときは、わざと壊して検査が赤くなるかも確かめています。17種類の壊し方を1つずつ入れて、16種類で検査が止まることを実測しました。
いちばん学びがあったのは、赤くならなかった1種のほうです。 その検査は「呼び出しが近くにあるか」を文字の距離で見ていたため、守るべき条件の外へ呼び出しを動かしても気づけませんでした。レビューでこれを直し、括弧の対応を数えて範囲を切り出す形に変えたうえで、同じ壊し方で赤くなることを確かめています。レビュー後に足した7種類の変異は、すべて赤になりました。
戻して緑に戻るところまで見るのが要点です。赤のままなら、壊した箇所とは別の理由で落ちています。注入の道具には「その文字列が実際に入っていること」を確かめる assert を入れました。壊せていないのに緑を、合格として数えないためです。
検査があることと、実際に止まることは別です。そして「止まった」と「止めたい形で止まった」も、また別です。
公開したあとは、現場が直した記録で測り続ける
いちばん大事にしているのはここです。リリースの前に採点して終わりにすると、次に設計を変えたときの副作用に気づけません。
| テスト音声の合格本数 | 現場が手で直した割合 | |
|---|---|---|
| 何を見ているか | 集めた音声に対する正解率 | 実際の診療で起きたこと |
| 見つかる誤り | 集めた型の中だけ | 想定していなかった型も入る |
| 正解を誰が作るか | 人が1本ずつ作る | 現場の手直しがそのまま正解になる |
| いつ測れるか | リリース前だけ | 毎週ずっと |
記録には変更の履歴が残るので、隣り合う版を比べて「次回予定の行だけが変わった回数」を数えられます。考え方は次の形です。
-- 隣り合う版を並べ、次回予定の行だけが変わった回を数える(列名は簡略化しています)
with pairs as (
select record_id,
next_appointment_line as now_line,
lag(next_appointment_line) over (
partition by record_id order by recorded_at
) as prev_line
from examination_record_history
where recorded_at >= now() - interval '7 days'
)
select count(*) filter (where prev_line is distinct from now_line) as edited,
count(*) as total -- 🔴 母数を必ず一緒に返す
from pairs
where prev_line is not null;集計には読み取りだけを行う専用の権限を使っています。権限の確認は宣言で済ませず、その接続で書き込みを試して、データベースが拒否のコードを返すところまで実測しました。集計の道具が記録を壊せないことは、道具の作りではなく接続の側で保証しておきたいからです。
母数を必ず一緒に出すのは、件数の少ない週に0件だったのか、本当に減ったのかを取り違えないためです。さらに、小さな母数の率はよく振れます。 率だけを並べて増減を語らないよう、幅で見ています。
// 率の幅(母数が小さいほど広がる)。k件/n件から、おおよその範囲を出す
const band = (k: number, n: number, z = 1.96) => {
const p = k / n, d = 1 + (z * z) / n;
const c = p + (z * z) / (2 * n);
const m = z * Math.sqrt((p * (1 - p) + (z * z) / (4 * n)) / n);
return [(c - m) / d, (c + m) / d] as const;
};
// 9 / 77 → 約 6%〜21% 5 / 40 → 約 5%〜26%
// この2週は率だけ見ると 11.7% と 12.5% で「悪化」に見えるが、幅は大きく重なるこの仕組みには、作った直後に助けられました。最初の集計は、77件のうち76件(98.7%)が手直しされたと出したのです。既知の実測は9.6%ですから、桁が違います。
原因は記録の読み取り方の誤りでした。AIが書いた初版を別の見出しで包み直していたため、行を探す処理が必ず「行が無い」と答える状態になっており、全件が手直し扱いになっていました。直したあとの値は77件中9件(11.7%)です。採点する側が壊れていました。
ここから2つを常設にしています。ひとつは、読み取り処理そのものに対するテストです(包み直す変異を入れると赤になり、戻すと緑に戻ることまで実測しました)。もうひとつは、毎週の数字を既知の実測と桁で突き合わせることです。採点の結果は毎週自動で届くので、妥当かを見る人がいなければ、壊れたまま届き続けます。測る仕組みにも、測られる側と同じだけの検査が要ります。
なお、同じ音声でも文字起こしが揺れる件については、その揺れがどこから来るのかを別の記事で測っています。合わせて読むと、評価の設計がしやすくなるはずです(生成AIの答えがぶれるとき、プロンプトのどこを疑うか)。
実務で効く手当て
今回そのまま使える形になったものをまとめます。
| 場面 | 手当て |
|---|---|
| 評価セットを作る | 人が手で直した本番の記録から作る。AIの答えと人の答えが両方そろっている |
| 判定の規則を書く | 証拠の強さの順に並べ、どの段で決まったかを記録に残す。段の分布の変化が早期の警報になる |
| 生成物を機械で書き換える | 触れる範囲を語に限る。安全弁は候補数ではなく実際に書き換えた数で数える |
| 入力を検査する | 通す形を並べず、危ない文字を弾く。通過条件は実データの分布を見てから決める |
| 守りを足す | 壊して赤、戻して緑まで実測する。赤くならなかった1種が、検査の弱点そのもの |
| 率で測る | 母数を必ず併記し、幅で見る。小さな母数の率は大きく振れる |
| 集計の道具を作る | 読み取り専用の接続で動かし、書き込みが拒否されることを実測する |
| 採点を信じる前に | 既知の実測値と桁を突き合わせる。採点する側も壊れる |
まとめ
医療AIの精度の評価で私たちが置いている線引きは、次の3つです。
- 正解は、現場が作ったものを使う。 人が直した記録には、AIの答えと現場の答えが両方残っている
- 決まっている値は、AIに任せない。 暦を数えて出す値はコードが書き、触れる範囲を語に限る
- 公開後も測り続ける。 設計を変えるたびに、次の週の数字で結果が返ってくる状態にしておく
毎週のカルテを見ながら「この欄だけはいつも直している」と感じている箇所は、どの病院にもあると思います。そこは数えられますし、数えれば設計で減らせます。私たちは自分たちの記録で毎週それを回していて、同じ形を院内の様式に合わせて作ることもできます。SOAP記録がどこまで下書きとして使えるか、いまお使いの様式を見せていただくところからでも構いませんので、お気軽にご相談ください。
よくあるご質問
テスト用の音声を増やせば、精度は十分に確かめられますか
本数を増やしても、集めた音声の傾向から外れた誤りは見つかりません。私たちが使っているのは、現場で人が実際に手直しした記録を含む本番の音声です。人が直したということは、AIの答えと現場の答えが両方そろっているということなので、正解を人が作り直さなくても採点できます。
現場の手直しは、どうやって数えているのですか
記録には変更の履歴が残るので、隣り合う版を比べて「次回予定の行だけが変わった回数」を数えます。読み取りだけを行う専用の権限で集計し、件数と割合を毎週まとめて社内に投稿しています。母数となる診察の件数も必ず一緒に出します。件数が少ない週に0件だったときと、本当に減ったときを取り違えないためです。
AIに「この日付を書いて」と指示するのでは足りないのですか
足りませんでした。長い指示文の一部は無視されることがあり、渡した日付と違う日付が書かれる回が残ります。そこで日付と相対表現の語だけを生成後に置き換える形にしました。置き換える範囲を語に限っているので、時刻や補足、医療者が読む本文はそのまま残ります。
