📝

日本語入力システムSumibiの開発 part2:プロンプトエンジニアリング

2023/05/15に公開

日本語入力システムとプロンプトエンジニアリング

前回の続きです。

https://zenn.dev/kiyoka/articles/japanese-input-method-sumibi-1

私はOpenAIのGPT-3.5/GPT-4のAPIを利用した日本語入力システム"Sumibi"を開発しています。
今回は、OpenAIのAPIをどのように利用しているか(または試行錯誤しているか)について書きます。
現時点では、同じような取り組みを"真面目に?"している人はいないようです。文章を扱う類似システムを作る際に参考になると思いますので公開します。

※ 注意:なお、本記事執筆時点ではgpt-4のウェイトリストに並んでいる状態でgpt-4は使っていません。以下のプロンプトで得られた結果はgpt-3.5-turboのモデルでの結果です。gpt-4だといくつかの課題が解決するかもしれません。

Sumibiプロジェクトについて

開発の背景

昔々、Emacsの日本語入力システムとしてモードレスな日本語入力システムがありました。英数モードと日本語モードの切り替えがなく、英数モードのままCtrl-Jを押すと、ローマ字が漢字に変換されるシステムです。有名なものとしては、"japanese-egg" や "yc.el" がありました。
モードレス入力方式は一度慣れると非常に快適です。皆さんも日本語入力モードのままプログラムの続きを書こうとして、ぐちゃぐちゃな全角文字が入ってしまうといった経験は一度や二度ではないでしょう。こういうイライラした状況が発生しません。
しかし、モードレス入力方式も良いことばかりではありません。日本語には同音異義語という困難な問題があります。
昔の技術では、いくら長文を入力しても、一発で期待する単語が出てくることはありません。折角長文を入力しても適切な漢字が選択されないことによる誤変換が大量に出ました。
2023年現在、GPT-3.5/GPT-4 の登場で、その問題がクリアできるのではないか?と思い、試してみることにしました。

アーキテクチャー

日本語変換はOpenAI APIを使っています。自前の日本語変換サーバーは持っていません。いわゆるGPTプロンプトエンジニアリングによって、ローマ字を日本語に変換します。
クライアントはEmacsのみです。Emacs Lispというテキストエディタの拡張言語を使って実装しています。
image.png

クライアント サーバー アルゴリズム
Emacs Lisp OpenAI API GPT-3.5/GPT-4

プロンプトの実装イメージ

本記事では、Emacs Lispはマイナーな言語なのでPythonで説明します。
※実際にSumibiでもPythonスクリプトでプロンプトを試してみてから、Emacs Lispに組み込むという手順になっています。

ローマ字を日本語に変換依頼するプロンプト

これが、Sumibiのローマ字漢字変換のコアです。
.format( 'watashi no namae ha nakano desu .' ) という部分の入力文字列をGPT-3.5に変換してもらっています。

開発を始めたばかりの頃は、以下のようなプロンプトでした。
One-Shotというやつです。

# -*- coding: utf-8 -*-
import os
import io
import sys
import openai
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
openai.api_key = os.getenv("OPENAI_API_KEY")

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    temperature=0.8,
    n=1,
    messages=[
        {"role": "system", "content": "あなたはローマ字とひらがなを日本語に変換するアシスタントです。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : watashi no namae ha nakano desu ."},
        {"role": "assistant", "content": "私の名前は中野です。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : {0}".format( 'watashi no namae ha nakano desu .' )}
    ]
)
print(response['choices'][0]['message']['content'])

しかし、このようなシンプルなプロンプトでは次のような問題がありました。

  1. 「ん」が、なかなか入力できません。
    例えば、「hanni」と書いた場合は , 「はんい」と認識して欲しいが「はんに」となってしまう
  2. Markdown構文の [name](URL) の括弧の部分が全角になってしまう
  3. Markdown構文の ![name](URL) の括弧の部分が全角になってしまう
  4. Markdown構文の ### が全角になってしまう。または消えてしまう
  5. 英語の文章を与えた時、英語と認識してくれないことがある

このように、実用的に使うには細かい手当てが必要でした。
結果、Sumibi ver1.5.0 の時点では、次のようなプロンプトになっています。
Few-Shotというやつです。

# -*- coding: utf-8 -*-
import os
import io
import sys
import openai
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
openai.api_key = os.getenv("OPENAI_API_KEY")

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    temperature=0.8,
    n=1,
    messages=[
        {"role": "system", "content": "あなたはローマ字とひらがなを日本語に変換するアシスタントです。"
         "ローマ字の 「nn」 は 「ん」と読んでください。"
         "[](URL)のようなmarkdown構文は維持してください。"
         "# や ## や ### や #### のようなmarkdown構文は維持してください。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : watashi no namae ha nakano desu ."},
        {"role": "assistant", "content": "私の名前は中野です。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : わたしのなまえはなかのです。"},
        {"role": "assistant", "content": "私の名前は中野です。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : ikano toori desu ."},
        {"role": "assistant", "content": "以下の通りです。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : hannishitei shimasu"},
        {"role": "assistant", "content": "範囲指定します"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : We succeeded in taking a photo like this:\n![example](https://www.example.com/dir1/dir2/example.png)"},
        {"role": "assistant", "content": "このような写真を撮ることに成功しました:\n![例](https://www.example.com/dir1/dir2/example.png)"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : ## this is markdown section"},
        {"role": "assistant", "content": "## これはMarkdownのセクションです。"},
        {"role": "user", "content": "ローマ字とひらがなの文を漢字仮名混じり文にしてください。 : {0}".format( 'watashi no namae ha nakano desu .' )}
    ]
)
print(response['choices'][0]['message']['content'])

苦労した点

GPTは自然言語処理が得意だろうということで使ってみましたが、実際に作ってみると人間が1文字ずつ入力する従来の日本語入力システムにそった考え方ではうまくいかないことがわかってきました。

短い文章が苦手

長文であれば、一発で正しい漢字が選択されます。しかし、入力した文章が短いと全く歯が立ちません。例えば「です」などの短いひらがなを書きたいことがよくあります。
今後、できるだけ長い文章になるように、前方や周辺の文章もプロンプトに含める改善を考えています。

記号1文字の入力が特に難しい

「※」 や 「→」 などの記号1文字をOpenAPIのAPI呼び出しで作り出すのはなかなか難しいです。
Sumibiでは、1文字のひらがなや記号を入力できるように、1文字での変換は特別扱いしています。
1文字の場合はOpenAI APIを呼び出さず、ローカルで処理してしまいます。

  • 「a」は「あ」に変換する
  • 「i」は「い」に変換する
  • 「ga」は「が」に変換する
  • 「no」は「の」に変換する
  • 「zl」は「→」に変換する
  • 「zv」は「※」に変換する

予期せぬ副次的効果

片仮名語を英語のスペリングで書ける

ローマ字では書きにくいカタカナ語は、英語のスペルのまま書けば上手くカタカナにしてくれます。
このシステムのアドバンテージは、ユーザーが複数の言語を織り交ぜて入力した場合でも、適切な日本語に変換できる点にあります。英語の単語やフレーズを挟んだテキストも問題なく日本語に変換することが可能です。

  • 入力
python gengo de coding shita script
  • 変換結果
Python言語でコーディングしたスクリプト

英日の翻訳ができる

プロンプトでは「ローマ字とひらがなの文を漢字仮名混じり文にしてください。」とお願いしているだけですが、英語の文章を入力すると日本語に翻訳してくれます。

  • 入力(GPLの文章)
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

*変換結果

ほとんどのソフトウェアや実用的な作品のライセンスは、作品を共有したり変
更することからあなたの自由を奪うように設計されています。一方、GNU一般
公衆利用許諾書は、プログラムのすべてのバージョンを共有したり変更したり
するために、すべてのユーザーにとって自由なソフトウェアであることを保証
することを目的としています。

まとめ

以上のように、GPTを用いた日本語入力システムの開発は、予期せぬ副次的効果として翻訳システムを生み出しました。
これは、システムの柔軟性と多機能性を示すもので、その可能性はまだまだ広がっています。
GPTのもつ潜在能力は、自然言語から自然言語にとどまらず、自然言語からプログラミング言語やグラフ言語への変換能力も持っているため、さらに面白い機能を組み込めるでしょう。

Discussion