この記事は、note・Zennにも掲載しています。
前回はAIの出力を自動チェックする仕組みが、4回すり抜けられた話を書きました。今回は逆に、検査が効きすぎた話です。
AIに書かせた記事を自動で検査する仕組みを作った初日、記事が消えました。
禁止カテゴリの語が入っている、という判定でした。危ない表現が混ざったのかと思って中身を見たら、引っかかっていたのは自分で付けた注意書きでした。
※18歳未満の方の閲覧はご遠慮ください。
規制のある分野なので、記事の末尾にこの一文を自動で付ける処理を入れていました。正しいことをするために付けた文章が、正しさを守るための検査に殺されたわけです。
この種の誤作動は、あとから考えると当たり前なのですが、作っている最中はまず気づきません。同じ構造は他の分野にもあるので、記録しておきます。
なぜ引っかかったのか
検査は、禁止カテゴリごとに語を並べて、本文に含まれていないかを見るだけの単純なものでした。
年齢に関するカテゴリでは、こう書いていました。
r"\d{1,2}歳"
年齢の表記をまとめて拾う、という発想です。ところが注意書きには「18歳未満」が入っています。禁止したい年齢も、書いておくべき注意書きも、同じ「数字+歳」という形をしています。
免責文ほど、禁止語に似る
これが一番の教訓でした。
どの分野でも、免責や注意書きは「禁止されていること」に言及する形で書かれます。
| 分野 | 書いておくべき文 | 検査が拾ってしまう語 |
|---|---|---|
| 年齢制限のある分野 | 「18歳未満の方の閲覧はご遠慮ください」 | 年齢の表記 |
| 医療・健康 | 「効果を保証するものではありません」 | 効果・保証 |
| 金融 | 「元本を保証するものではありません」 | 元本保証 |
| 広告 | 「個人の感想であり、効果を示すものではありません」 | 効果 |
禁止語のリストと、免責文の語彙は、必ず重なります。 構造上、避けられません。
「こういう表現を書いてはいけない」というルールがある分野では、「書いてはいけない表現に言及しない免責文」を作るほうが難しいのです。
対処1:検知する範囲を絞る
まず、拾いすぎていた部分を狭めました。
# 変更前:年齢の表記をすべて拾う
r"\d{1,2}歳"
# 変更後:禁止対象の年齢だけを拾う
r"(?<!\d)(?:[0-9]|1[0-7])歳"
そもそも問題なのは特定の年齢層だけで、それ以外は拾う必要がありませんでした。「そのカテゴリで本当に禁止されているのは何か」を、法律の条文まで戻って確認すると、検知範囲はかなり絞れます。
雑に広く取ると、後で誤作動の形で返ってきます。
対処2:検査する前に、正当な文を取り除く
範囲を絞っても、まだ足りませんでした。免責文には、どうしても禁止語そのものが出てきます。
そこで、本文ではなく「検査用のコピー」を作り、そこから正当な定型文を先に消してから検査するようにしました。
SAFE_PHRASES = [
r"18歳以上(であること)?(を確認)?",
r"18歳未満[^。<]{0,30}?(ご遠慮|禁止|お断り|できません|いただけません|ください)",
]
scan_target = body # 本文はそのまま
for phrase in SAFE_PHRASES:
scan_target = re.sub(phrase, "", scan_target) # 検査用コピーから免責文を消す
for pattern, reason in BLOCK_PATTERNS:
if re.search(pattern, scan_target): # 消したあとで検査する
...
ポイントは、本文を書き換えないことです。免責文は当然そのまま残す必要があります。消すのは、あくまで検査のために作った一時的なコピーだけです。
この「本文」と「検査対象」を分ける発想がなかったので、最初は免責文を消すか、検査を弱めるかという、どちらも間違った二択で悩んでいました。
再発:文言を変えたら、また消えた
これで解決したと思っていたのですが、しばらくして同じ症状が再発しました。
原因は、ボタンの文言を変えたことでした。注意書きの語尾を「ご遠慮ください」から別の言い方に変えたところ、除去リストの正規表現に一致しなくなったのです。
日本語の語尾は、驚くほど種類があります。
ご遠慮ください/お控えください/お断りしています/禁止しています
できません/いただけません/ご利用いただけません/対象外です
除去リストは、このすべてを吸収できていませんでした。
# 語尾のゆれをまとめて吸収する
r"(ご遠慮|遠慮|禁止|お断り|できません|いただけません|ください|不可|対象外|ご利用いただけ)"
ここで分かったのは、除去リストは、文言を変更するたびに更新が必要だということです。
自分で付ける定型文と、それを検査から除外するリストは対になっています。 片方だけ直すと壊れる。この関係をコードのコメントに書いておかないと、半年後の自分は必ず忘れます。
通ってほしいケースを、テストに書く
再発してから、「通ってほしいケース」をテストに追加しました。
must_pass = [
"18歳未満の方はご利用いただけません",
"18歳未満の方の閲覧はご遠慮ください",
"未成年者の閲覧はできません",
"18歳以上の方を対象としています",
]
must_block = ["(実際に禁止したい表現)"]
禁止ワードの検査を作るとき、テストはたいてい「弾けるか」だけを見ます。でも実際の運用で困るのは、弾きすぎのほうでした。
弾きすぎは、静かに壊れます。エラーも出ず、ただ記事が減っていく。気づいたときには「なぜか最近うまく動いていない」という状態になっています。
私の場合、語尾のバリエーションを14パターン書き出してテストにしました。そこまでやって、ようやく落ち着きました。
まとめ
- 免責文と禁止語の語彙は、構造上かならず重なる。 正しい記載ほど検査に引っかかる
- 検知範囲は、法律や規約の条文まで戻って絞る。雑に広く取ると誤作動になる
- 本文と検査対象を分ける。 検査用のコピーを作り、そこから正当な定型文を先に取り除く
- 定型文と除去リストは対になっている。 文言を変えたら、必ず両方直す
- テストには「通ってほしいケース」を必ず書く。弾きすぎは、エラーを出さずに静かに壊れる
禁止ワードの検査は、作るだけなら30分で書けます。運用に耐える形にするのに時間がかかるのは、ほとんどが誤作動への対処でした。
規制のある分野(医療・金融・法律・成人向けなど)で、AIに文章を書かせる仕組みを個人で作っています。うまくいかなかった部分も含めて書いています。
次は「検査を強くしたら、正常なものまで弾き始めた」について書きます。