この記事は、note・Zennにも掲載しています。
前回、AIは「見ていないもの」の感想を平気で書く という記事を書きました。今回はその続きで、書かせた文章を検査する側の話です。
AIに記事を書かせて自動で公開する仕組みを作ったとき、生成された文章を検査して、危ないものを止める層を用意しました。
法律に触れうる表現が入っていたら、記事ごと捨てる。誇大な表現なら、その語だけ置き換える。そういう二段構えです。
作ったときは、それなりに固いつもりでいました。実際には、4回すり抜けられました。 その記録です。
やられ方には共通点があって、それが一番の学びでした。
前提:何を止めようとしていたか
扱っていたのは、表現に強い制約がある分野の商品紹介でした。分野の詳細はここでは伏せますが、止めるべきカテゴリが法律で決まっていると思ってください。
検査は、正規表現で語を探す、という単純なものです。禁止カテゴリごとに語を並べて、生成された本文に含まれていないかを見る。
BLOCK_PATTERNS = [
(MINOR_PATTERN, "カテゴリA(法律で明確に禁止)"),
(NONCONSENT_PATTERN, "カテゴリB(同意のない設定)"),
(SUBSTANCE_PATTERN, "カテゴリC(薬物)"),
]
素朴ですが、動きます。動いているように見える、というのが厄介なところでした。
1回目:語が伏せ字になっていた
最初のすり抜けは、商品タイトルでした。
販売プラットフォームは、際どい語を伏せ字にして掲載します。「○」「●」「◯」「*」などで、1〜2文字が置き換わる。
つまり、こちらが辞書に登録した語とは一致しません。 人間が読めば同じ意味だと分かりますが、文字列としては別物です。
対処は、伏せ字を文字クラスで吸収することでした。
# 「◯◯生」のように一部が伏せ字になった形も同じものとして扱う
r"女子[○●◯〇**×]{1,2}生|女[○●◯〇**×]{1,2}生"
ただ、このとき気づいたことのほうが重要でした。語を一つずつ追いかけるのは、終わりがありません。 伏せ字のパターンは無限に作れます。
そこで方針を変えて、その設定を思わせる周辺語ごと除外するようにしました。特定の一語ではなく、その状況を示す語(場所、行事、持ち物など)をまとめて弾く。こちらのほうが、伏せ字を追うより確実でした。
2回目:動詞が伏せ字になっていた
同じ構造で、今度は別のカテゴリでやられました。
1回目の経験があったので、名詞の伏せ字は想定していました。動詞の伏せ字は想定していませんでした。
r"犯[○●◯〇**×]れ|犯[○●◯〇**×]す"
品詞が変わると、自分の中で別の問題に見えてしまう。同じ手口なのに、一度目の対処が二度目に効かなかったのは、対処を「語の修正」としてやっていたからでした。
3回目:業界の略語を知らなかった
3回目は、薬物に関するものでした。
一般的な語は登録していました。医学的な名称も、日常的な言い方も入っていた。それでも抜けたのは、その界隈だけで通じる略語が使われていたからです。
自分の語彙の外にある言葉は、辞書に入れようがありません。これは調べ方の問題で、「自分が知っている言い方」を並べた時点で、辞書は穴だらけになります。
対処としては、実際に流通しているタイトルを大量に眺めて、知らない語を拾うようにしました。机の上で考えるのをやめた、ということです。
4回目:タイトルではなく、タグに入っていた
これが一番危なかった。
タイトルには何も問題がありませんでした。 検査は当然、素通りします。そして公開されました。
あとで気づいたのは、商品のメタデータ(分類タグ)に、禁止カテゴリがそのまま入っていたことです。私はタイトルと本文しか見ていませんでした。
# タイトルだけでなく、分類タグも検査する
if not hit:
hit = re.search(GENRE_EXCLUDE_PATTERN, "、".join(product.get("genres", [])))
ここで学んだのは、こういうことです。
検査する場所を、こちらの都合で決めてはいけない。危ないものは、こちらが見ていない場所から入ってくる。
1〜3回目は「語の追加」で済みました。4回目だけは、設計の抜けでした。
検査を広げたら、今度は誤検知が始まった
タグまで見るようにしたら、正常な商品が大量に弾かれ始めました。
原因は、タグに業界の一般語が含まれていたことです。見た目には禁止カテゴリを思わせるのに、実際にはその分野で広く使われている分類名でした。これを弾くと、まともな商品がほとんど残りません。
そこで、場所ごとに検査の強さを変えました。
| 検査する場所 | 適用する強さ |
|---|---|
| 生成された本文 | 全カテゴリ(こちらが書いた文章なので厳しく) |
| 商品名 | 重大カテゴリのみ(外から来た名前なので緩く) |
| 分類タグ | 重大カテゴリ+禁止カテゴリ。ただし一般語は明示的に除外 |
# 「美」が前に付く場合は一般語なので通す、といった除外を入れる
r"(?<!美)少女"
一つのパターンを全部の場所に使い回すと、必ずどこかで破綻します。
すり抜けを、テストに変える
4回やられて決めたのは、すり抜けを見つけたらテストにすることでした。
must_block = ["(伏せ字を含む例)", "(動詞が伏せ字の例)", "(略語の例)"]
must_pass = ["(一般語を含む正常な例)", "18歳未満の方はご利用いただけません"]
大事なのは must_pass(通ってほしいケース)のほうです。これがないと、フィルタを強くしすぎたことに気づけません。 私は一度、範囲を広げすぎて候補の半分近くを落としていたことがありました。弾きすぎは、見逃しと同じくらい運用を壊します。
まとめ
- 伏せ字は必ず来る。語を追うより、状況ごと除外するほうが速い
- 同じ手口でも、品詞が変わると別物に見える。対処は「語の追加」ではなく「形の吸収」として設計する
- 自分の語彙の外にある言い方は、辞書に入らない。実物を大量に見て拾う
- タイトル以外の場所(メタデータ)も検査する。 危ないものは、見ていない場所から入る
- 検査を広げると誤検知が増える。場所ごとに強さを変える
- すり抜けはテストにする。通ってほしいケースを必ず書く
そして、これが一番の結論です。
この種の検査は、一度作って終わりにはなりません。 運用しているかぎり、新しいすり抜けは出てきます。完全にすることを目標にせず、人間が最後に見る場所を必ず残しておくことのほうが現実的でした。
私の場合は、投稿を自動公開せず下書きで止めていました。それが最後の砦でした。逆に言えば、そこを自動公開にした瞬間、この検査層が唯一の防波堤になります。
規制のある分野(医療・金融・法律・成人向けなど)で、AIに文章を書かせる仕組みを個人で作っています。うまくいかなかった部分も含めて書いています。
次は「自分で付けた注意書きで、自分の記事が消えた話」を書きます。