日本語らしくないテキスト生成結果を排除する
はじめに
日本語のプロンプトでテキストを生成すると、特別な指示がない限り日本語で出力されるのが普通です。
しかし、表現力を高めるために生成AIのパラメーターを設定すると、必ずしもそうではなくなり、自由な表現の結果として不自然に外国語が使われることがあります。
筆者はテキスト生成のためにOpenAI APIを利用し、その生成内容に多様性を持たせようと試行錯誤しています。
その結果、一見すると文字化けに近い内容が出力される頻度が高くなり、その対策をどのようにしたかをまとめました。
表現力とは?
openAI APIのテキスト出力で使う Create chat completion
を例にすると以下のようなパラメーターが関わっていきます。
- Temperature
- Frequency Penalty
- Presence Penalty
- Top-p (筆者は使っていないです)
詳しくはopenAI APIのドキュメントを参照してください。
これらの最大値は2.0で、数値が大きければ大きいほど表現力が高くなると言える挙動になります。
しかしながら日本語のプロンプトに対して日本語のみで表現するくらいなら外国語も含めてテキストを生成したほうが表現力は高くなる?という感じで解釈することがあるようです。
この現象は多くのシーンで問題になっているようで、一言でいえば生成に利用しているmodelの学習不足と言えてしまうようです。
筆者は低コストで納得のいく出力さえもらえればよしとしているため、利用しているモデルはテキスト生成では最安値のgpt-4o-miniを使っています。
これ以外の選択肢もあるにはあるのですが、コストが段違いになるため利用できません。
なので筆者は表現力が弱いモデルでうまく表現していこうとする中で、外国語を利用されている場合だけは排除するということをしています。
日本語の文章で許される外国語とは?
これまたややこしくなる話です。
そもそもとして日本人が普通に利用している文字種が多すぎます。
「ひらがな・カタカナ・漢字・数字・アルファベット・記号・絵文字」
さらに、複数の外国語を含めた顔文字。これが一番ややこしい。
なので、ぱっと見は日本語の文章にみえて、実は外国語が使われているみたいなケースもあったりするのがめんどくさくなります。
それらを正しく排除して日本語100%の文章をお願いしますというプロンプトを用意したとしても、そもそも表現力の弱いモデルに表現力を持たせるパラメーターをつけても期待する動作は試行錯誤はしていますがほぼ無理だと感じました。
このような試行錯誤の中で思ったことがあります。
それは一般的には外国語はカタカナで表現するため、外国語をその国の文字で使うことはあまりないということです。
いわゆる和製英語もこれに当てはまると思います。
また、日本人が日常で利用している外国語はほとんどが英語で、それ以外の国の言語をカタカナで表現することもないということです。
例えば「この野菜はとても新鮮です」を外国語が混じる表現にすると・・・・
- 和製英語が混ざったコメント
「この野菜はとてもフレッシュです」 - 英語が混ざった微妙に不自然なコメント
「この野菜はとてもFreshです」 - 英語が混ざった不自然なコメント
「このVegetables are fresh」 - 英語以外が混ざった不自然なコメント
「この野菜はとてもсвежий ихです」 - 英語以外をカタカナにしても不自然なコメント
「この野菜はとてもスェジーフです」 - 英語以外が混じる顔文字付きのコメント
「この野菜はとてもフレッシュです(゚д゚)」
英語以外は即おかしいし、英語だとしてもアルファベットになると不自然になる傾向があります。
しかし顔文字の一部では外国語が使われていても不自然にはならないです。
生成した出力からいかに外国語を排除するか
まず顔文字は全面的にNGとします。
これを許容してしまうとまともに外国語を排除できなくなってしまったためです。
ですので、前述した通り「ひらがな・カタカナ・漢字・数字・アルファベット・記号・絵文字」はOKとしてこれ以外はNGとして出力内容をチェックしていくことにしました。
筆者が利用している開発言語はGoでこれらの文字種かどうかは1文字ずつチェックしています。
以下がそのコードになります。
var japaneseRegex = regexp.MustCompile(`[\p{Hiragana}\p{Katakana}\p{Han}\p{Latin}\p{P}\p{S}\p{N}\p{Z}\x{30FC}\x{200D}\x{000A}\x{000D}]`)
func isJapanese(text string) bool {
for i, r := range text {
if !japaneseRegex.MatchString(string(r)) && !isEmoji(r) {
return false
}
}
return true
}
func isEmoji(r rune) bool {
return (r >= 0x1F000 && r <= 0x1FFFF) || // 絵文字の範囲(追加の絵文字)
(r >= 0x2600 && r <= 0x26FF) || // 雑多な記号(天気や天体など)
(r >= 0x2700 && r <= 0x27BF) || // ダイングバット(装飾的な記号)
(r >= 0xFE00 && r <= 0xFE0F) || // バリエーションセレクタ(絵文字のスタイル変更)
(r >= 0x1F1E6 && r <= 0x1F1FF) || // 地域指標記号(国旗など)
r == 0x200D // ゼロ幅連結子(ZWJ、絵文字の組み合わせに使用)
}
日本語として認められそうな文字種に特殊な記号を含めたパターンと絵文字に特化したパターンとに分けてチェックしています。
\p{Emoji}\p{Emoji_Presentation}
のような形式で絵文字も簡易的なパターンとして表現できるようなのですが、goの標準ライブラリでは非対応だったので自分自身で文字コードの範囲指定をしています。
また、生成内容は目でもチェックし、怪しいものがチェックから漏れていたり、チェックしすぎてまともな文章をNGとしている場合はその都度パターンに追加する運用をしています。
現状のチェックで利用しているパターンについての説明は以下の通りです。
日本語文字
-
\p{Hiragana}
: ひらがな文字をマッチします。 -
\p{Katakana}
: カタカナ文字をマッチします。 -
\p{Han}
: 漢字をマッチします。
ラテン文字
-
\p{Latin}
: ラテン文字 (A-Z, a-z) をマッチします。
記号
-
\p{P}
: 記号 (句読点など、例:., !, ?) をマッチします。 -
\p{S}
: 記号 (通貨記号など、例:$, %, &) をマッチします。 -
\p{N}
: 数字 (0-9) をマッチします。 -
\p{Z}
: 区切り文字 (スペース、タブなど) をマッチします。
特定の文字
-
\x{30FC}
: 長音記号 (ー) をマッチします。 -
\x{200D}
: ゼロ幅連結子 (ZWJ) をマッチします。 -
\x{000A}
: 改行コードをマッチします。 -
\x000D}
: 復帰コードをマッチします。
不本意な出力があったら再生成するプロンプトにしたらいいのでは?
プロンプトで期待値から外れるものは再生成するように指示することはできないのか?と考えるてはいました。
しかしながら、これは難しいようです。
なぜならば、そもそもとして表現力が弱いが故に外国語を利用し始めているので期待値のチェック自体がザルです。
さらに外国語についての指定(日本語以外とする場合も同様)がプロンプトに必要になってきますし、表現力を高めるとプロンプトの指示を無視しはじめることも考えられます。
ですので、期待値とはいえない文章を自作のチェックによりNGとするほうがまだ楽な気がしています。
おわりに
ぶっちゃげると、もっといいやり方があるならぜひ知りたいです。。。
場当たり的に解決策を作ってはいますがスマートさがイマイチだとは思っています。
生成AIを触り始めて出力内容をうまく作ろうとする作業はちょっとした闇の中を手探りで練り回るイメージがあります。
まだまだ勉強不足だとも感じていますが、しっかりとしたものを求めるのであれば学習量が多い最新のモデルを常に利用するのがいいとは思います。
しかし、低予算だとそうはいかず。。。
もしかすると、自分の都合がいいモデルを自作するのがいいかもしれませんが、それはまた別の機会に。
Discussion