1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NGワード検査が、自分の免責文を誤検知した話(正規表現の落とし穴)

1
Posted at

この記事は、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に文章を書かせる仕組みを個人で作っています。うまくいかなかった部分も含めて書いています。

次は「検査を強くしたら、正常なものまで弾き始めた」について書きます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?