キーボードの配列と最適化問題への入門(その2: LLMでひらがなデータの準備)
前回の記事の続きです。この記事だけでも一つの独立した話題になっています。
前回の記事のレイアウト最適化は、あくまで英文を対象にしていました。次の発展として、ローマ字入力による日本語入力について検討したいと考えました。漢字変換の部分は取り扱わないことにすると、ひらがなに対するキー入力の系列を考えることになります。
英文と日本語では違いがいくつかありますが、日本語の方が考慮すべき点が多く準備が大変です。
- データセットの問題: 何を使うべきなのだろうか?
- ローマ字入力の多様性: ローマ字入力はひらがな文に対して一意でない点をどうするか?
- 例えば一般的なIMEだと「ちょ」を入力するために
cho
とtyo
の二通りの入力方式を提供しています。どちらの入力が楽(低コスト)なのかはキー配列によるはずです。
- 例えば一般的なIMEだと「ちょ」を入力するために
全ての課題を一気に解決するのは難しそうなので、この記事ではデータセットの準備に絞って進めていきます。
ひらがなデータセットの作成
典型的なローマ字入力の系列を作るには、典型的な日本語ひらがなの系列データセットを準備する必要があります。まずはそれなりの、大きすぎないデータだと実験もしやすくて良いです。
おそらく定番の手法としては、青空文庫やWikipediaのデータを加工するという方法が考えられると思いますが、今回については前回も利用したdatabricks-dolly-15kを使うことにします。このデータセットはDeepLで日本語化されたものがLLM-jpによって公開されているようです。
しかし、今回欲しいのは平仮名データなので、日本語訳されているからといってそのまま利用することはできません。
既存手法
大西配列の評価時には、いくつかの書籍データをGooラボのひらがな化APIで変換して用いたようです。
LLMになんとかしてもらう
日本語データから平仮名データを作ることはおそらく可能で、辞書を使って形態素解析などすればいいのだと思いますが、ここでは元の英文からLLMで平仮名データを生成してみることにしました。
こんな感じのプロンプトを書き、それに翻訳元の英文を足すことで翻訳・平仮名変換までを一括で行ってもらいます。
あなたのタスクは、与えられた文章を平仮名文に変換することです。
次の指示に従って、与えられたテキストを処理してください:
1. 入力テキストを日本語に翻訳する(すでに日本語の場合はこのステップをスキップ)
a) ただし人名や固有名詞については英単語のままとする
2. 翻訳された(または元の)日本語テキストを以下のルールに従って変換する:
a) 漢字とカタカナをすべて平仮名に変換
b) 平仮名はそのまま
c) 英数字はそのまま
d) 記号や句読点はそのまま
3. 翻訳後のテキストをtranslatedとし、変換後のテキストをconvertedとしたJSON形式で出力する
<example>
入力: Bill Gates (the co-founder of Microsoft) released Windows 1.0 in 1985.
出力: {"translated": "Bill Gates(Microsoftの共同創設者)は、Windows 1.0を1985年にリリースしました。", "converted": "Bill Gates(Microsoftのきょうどうそうせつしゃ)は、Windows 1.0を1985ねんにりりーすしました。"}
</example>
<example>
入力: This is a green pen made by USA.
出力: {"translated": "これはアメリカ製の緑のペンです。", converted: "これはあめりかせいのみどりのぺんです。"}
</example>
では、以下のテキストを変換してください:
{{text}}
プロンプトを埋め込んだPythonコードです。
import json
import sys
import anthropic
from datasets import load_dataset
from retrying import retry
client = anthropic.Anthropic()
PROMPT = """
あなたのタスクは、与えられた文章を平仮名文に変換することです。
次の指示に従って、与えられたテキストを処理してください:
1. 入力テキストを日本語に翻訳する(すでに日本語の場合はこのステップをスキップ)
a) ただし人名や固有名詞については英単語のままとする
2. 翻訳された(または元の)日本語テキストを以下のルールに従って変換する:
a) 漢字とカタカナをすべて平仮名に変換
b) 平仮名はそのまま
c) 英数字はそのまま
d) 記号や句読点はそのまま
3. 翻訳後のテキストをtranslatedとし、変換後のテキストをconvertedとしたJSON形式で出力する
<example>
入力: Bill Gates (the co-founder of Microsoft) released Windows 1.0 in 1985.
出力: {"translated": "Bill Gates(Microsoftの共同創設者)は、Windows 1.0を1985年にリリースしました。", "converted": "Bill Gates(Microsoftのきょうどうそうせつしゃ)は、Windows 1.0を1985ねんにりりーすしました。"}
</example>
<example>
入力: This is a green pen made by USA.
出力: {"translated": "これはアメリカ製の緑のペンです。", converted: "これはあめりかせいのみどりのぺんです。"}
</example>
では、以下のテキストを変換してください:
"""
@retry(wait_fixed=1000 * 30, stop_max_attempt_number=3)
def post_message(text):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=4096,
temperature=0,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": PROMPT + text,
}
],
},
{
"role": "assistant",
"content": [{"type": "text", "text": '{"translated":'}],
},
],
)
return message
if __name__ == "__main__":
if len(sys.argv) == 3:
data_from, data_to = int(sys.argv[1]), int(sys.argv[2])
else:
data_from, data_to = 0, 9 # 適当なデフォルト値
dolly_dataset = load_dataset("databricks/databricks-dolly-15k", split="all")
for idx, data in enumerate(dolly_dataset):
if not (data_from <= idx <= data_to):
continue
print(f"idx = {idx}", file=sys.stderr)
text = data["instruction"] + data["response"]
msg = post_message(text)
try:
res_json = json.loads('{"translated":' + msg.content[0].text, strict=False)
print(res_json["converted"], flush=True)
except json.JSONDecodeError as e:
print("parse failed: skipping", e, msg, file=sys.stderr)
print(flush=True)
プロンプト上の工夫
使用したLLMはAnthropicのClaude 3.5 Sonnetです。かなり手探りでプロンプトを書いたので、これが最適かは怪しいですが、少なくともいくつかポイントがあるようでした。
- 良いモデルを使うこと: 費用だけでいうとClaude3 Haikuが理想ですが、翻訳した上で平仮名変換という2段階のタスクをあまり高品質にこなしてくれませんでした。そこで今回はAnthropicで現在利用可能な最高性能のモデル(3.5 Sonnet)を使っています。
-
Few-shot promptingにすること: 入出力について具体例を与えた方が出力は明らかに安定しました。
- プロンプトで用いている例文はあまり深く考えられたものではなく、人名や数字記号をほどほどにちりばめようとした以上の意味はありません。
-
JSON出力にすること: テキストをそのまま出力させるだけだと、先頭に「こちらが変換結果です」といった、日本語会話としては有益だが今回は欲しくない出力が混ざることがありました。JSONが出力されるように調整すればこの問題は回避できます。
- 最近のClaude APIにはJSON出力を強制する機能が追加されたらしいのですが、今回はより古典的な、prefillという出力内容の一部を固定する仕組みを使ってJSON出力を誘導しています: https://docs.anthropic.com/ja/docs/build-with-claude/prompt-engineering/prefill-claudes-response
-
中間段階を出力に含めること: 最終的な出力には不要な「日本語翻訳した段階のテキスト」をあえてJSON出力に含めるように誘導しています。こうすると最終出力のひらがなの品質が改善しました。
- おそらくですがchain of thought (CoT)になっているのだろうと思います: https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/chain-of-thought
「ひらがな変換」と一口に言っても、細かい仕様はそれほど自明ではありません。例えば、人名をどう扱うか(「ビル・ゲイツ」と変換するか「Bill Gates」のままにするか)といった問題が残っています。この辺りは少し悩みつつも、このプロンプトでは無変換に誘導しています。現実の日本語入力でも、しばしば英単語が混ざるのはよくあることだと考えたからです。この辺りはプロンプトの記述で調整可能な範囲かもしれません。
このスクリプトの実行結果については「評価」の節で紹介しますが、出力をざっと目視する限り、それなりだが完璧とまでは行かない程度に見えました。これ以上の品質を出す方法はよくわかっていません。プロンプトは少し書き方を変えるだけで結果が変わることがあるので、試行錯誤が必要なのかもしれません。
とはいえ、圧倒的な手軽さがあるのがLLMを使うメリットの1つだと実感できました。費用もそれほどかかりませんでした(数ドル程度だったと思います)。プロンプトを洗練させると、さらに良い品質の出力が得られるのかもしれませんが、筆者はプロンプトエンジニアリングに詳しくないので一旦ここまでとします。
スクリプトの細かい考慮点
retryingモジュールを導入しているのは、まれにAnthropic APIが500レスポンスを返すことがあったからです。保険的な意味で数回のリトライを入れています。
また、結果は普通のLinuxコマンドのように、標準出力に流すようにしてあります。
python main.py 0 199 | tee output.txt
ちなみに、実行例のようにパイプする際、バッファリングされると途中経過がよくわからなくなるのでflush=True
が指定してあります。
出力の確認
準備したPythonスクリプトを使うと、少なくとも見た目には相当それらしい出力が得られます。いくつかの入力と出力の組を見てみます。
入力
Why can camels survive for long without water?Camels use the fat in their humps to keep them filled with energy and hydration for long periods of time.
出力
なぜらくだはちょうきかんみずなしでせいぞんできるのでしょうか?らくだはこぶのしぼうをつかって、ちょうきかんえねるぎーとすいぶんをほきゅうしつづけます。
なかなか上手くいっているようです。
入力
Who invented the telephone?In 1876, Alexander Graham Bell was the first to obtain a United States patent for a device that produced a clearly intelligible replica of the human voice on a second device.
出力
だれがでんわをはつめいしましたか?1876ねんに、Alexander Graham Bellがにんげんのこえをべつのそうちであきらかにりかいかのうなふくせいをせいせいするそうちについて、さいしょにべいこくとっきょをしゅとくしました。
こちらは若干怪しいところがあり「あきらかにりかいかのうなふくせい」という文は不自然さがあります。中間段階では 明確に理解可能な複製
と訳しているので、英訳はさておき、少なくともひらがな変換に失敗しているようです。
ただ、それらしいニュアンスが残っているのは面白いなと思います。今回の用途では許容できる品質の出力と言えるかもしれません。
まとめ
この記事では、ひらがなデータセットを英文からLLM経由で生成する方法を整備しました。もっとも、単に指示するだけだと出力はそれほど安定せず、プロンプトにあれこれ工夫をしないと期待通りの挙動が得られませんでした。記事中には手探りながら試行錯誤してポイントをまとめてみました。
挙動はいろいろな意味で粗削りですが、とりあえずの実験には使えそうな、ひらがなの文字列を得ることができました。
Discussion