見出し画像

GPT4を使ったドキュメンテーション翻訳

(この記事はThomas Capelleのオリジナル記事を、シバタアキラが翻訳、編集、加筆しました。)

はじめに

W&Bでは、日本でのビジネス展開に伴い、製品ドキュメンテーションの翻訳を進めて来ました。W&Bのドキュメンテーションは、今年2023年の初頭に大幅に改訂され、内容も250ページ以上に発展して、以前とは比べもにならないくらい充実してきましたが、一方で翻訳の手間も増えました。ドキュメンテーションはプロダクト新機能のリリースなどによって定期的に追加・更新が入ることもあり、極力人間の作業を省力化し自動化をしたいと議論していたところで、大規模言語モデル(LLM)を使って翻訳するプロジェクトがスタートしました。ドキュメントページをChatGPTに与え、別の言語に翻訳するよう頼むことで、翻訳を自動化するというアイディアです。
当初この素朴なアプローチは驚くほどうまくいきそうに感じられましたが、ドメイン知識不足や、英語のままにしておくべきだった単語が翻訳されてしまったりしました。そこで最新の内容を高い品質で翻訳をリリースできるよう、翻訳ワークフローをチューニングすることにしました。

💡本レポートに関連するColabは、こちらからご覧いただけます。

Markdownファイルで構築されたドキュメントシステムの翻訳

Markdownファイルは、noteの記事のように文章の構造的な表示を可能にする構文を持つ、キストファイルです。例えば、ヘッダーは#で、コードブロックはバックティックで定義することができます。また、引用符、表、太字、斜体など、種標準的なテキストのさまざまな要件をサポートしています。W&BのドキュメンテーションはMarkdownの構文とレンダリングエンジンの組み合わせで、美しいドキュメントサイト構築しています。

Markdown(.MD)ファイルとはどのようなものか

W&Bドキュメンテーションのソースとレンダリング後の記事の一例

ご覧のように、ファイルの構造は比較的単純で、構文を崩さずに特定のポイントで分割できます。そのために、LangChainのインジェストツールを使って、入力ファイルを任意の箇所で分解できます。以下はそのコードです。

from pathlib import Path
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter


# we can grab all mardown files recursevely using `rglob`
docs_path = Path("docs/")
md_files = list(docs_path.rglob("*.md"))
md_files.sort()


# load one file
one_file = md_files[0]
data = TextLoader(one_file)
docs = data.load()

内容を見ていきましょう

  • docs/フォルダからすべての.mdファイルを再帰的に取得します。わかりやすくするために、ファイルのリストをアルファベット順に並べ替えています。

  • デバッグと説明のために、最初のファイルを取得してTextLoaderで読み込むことにします。

MarkdownTextLoaderを使用しない理由はなぜかというと。このテキストローダーはマークダウンの構文を壊してしまうからです。構文を作り出す「特別な」マークダウン文字を取り除くことなく、できるだけ生のままファイルをロードしたいのです。

分割:ファイルをチャンク単位に分割する

ファイルをテキストのチャンク単位に分割してモデルにフィードします。私たちはもちろんGPT-4へのAPIアクセスをもっていましたが、長いコンテキストのリクエストを送信すると、タイムアウトになる(それでも課金はされる)ことが多く、難しいことがわかりました。そこで、安全のために、コンテキストウィンドウ十分に小さいサイズ(チャンク)に分割して、リクエストが安全に返るようにしました。

💡もちろんこれは理想的ではありません。近い将来、OpenAIが推論インフラストラを拡張し、コンテキストウィンドウ全体に近いチャンクを渡すことができるようになると期待しています。チャンクの代わりに、ドキュメントページ全体を渡すと、より一貫性のある良い翻訳が得られます。将来的に、gpt-4-32kが使えるようになれば、チャンク単位への分割は不要になるはずです。

それでは、テキストの分割を続けましょう。RecursiveTextSplitterを使います:

markdown_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n","\n"," "],
    chunk_size=2000, 
    chunk_overlap=0)
split_docs = markdown_splitter.split_documents(docs)

この関数は、2行の連続改行、1行の改行、そしてスペースでしか分割しませんので、特段賢いことをしているわけではありません。「でもLangChainにはMarkdownTextSplitterがあるのでは?」とおっしゃる方もいるかもしれません。イエスでもありノーでもあります。また、このスプリッターはヘッダーやその他の関連する構文を除去してしまうため、出力ファイルのフォーマットが入力とは変わってしまいました。結局一番上手くいったのは、そのままのRecursiveCharacterTextSplitterでした。

出力は、入力テキストとchunk_sizeに応じた一連のテキストチャンクになります。GPT-4では、2千語くらいが現実的なリミットであることがわかりました。

チャンクをひとつひとつ翻訳する

これでMarkdownファイルがチャンク単位に分割されたので、翻訳を開始することができます。単純な方法では、次のようにモデルに指示を出すだけです。

あなたは翻訳アシスタントです。ここにいくつかのMarkdownファイルがあります。{output_language}に翻訳してください。Markdownの構文は壊さないようにしてください。

この単純なプロンプトで75%くらいの精度までは到達しましたが (内部メトリックで任意に計算) 、次のような改善を試みました:

  • モデルに、より明確な指示を与える。

  • 技術的なコンテンツに有用な、単語とその訳語の辞書を渡す。

  • 翻訳する部分と、構文をそのまま維持する方法を指示。

最終的に使ったプロンプト

system_template = """あなたは翻訳アシスタントです。{input_language} を
 {output_language} に翻訳します。 下記のルールに注意してください:
- 空白の行を出力しないこと
- 出力は有効なマークダウン形式であること
- 翻訳の精度は重要ですが、「翻訳っぽい」結果にならないよう気をつけてください。
 単語ごとの対応に気を配りすぎず、自然さや伝わりやすさを優先させてください
- コードブロックにおいては、コメントのみを翻訳し、コードそのものは訳さないこと

ドメイン特化した単語の翻訳辞書を書きに示しますので、適宜使用すること:
<Dictionary start>
{input_language}: {output_language}
{dictionary}
<End of Dictionary>
""" 

英語→日本語の翻訳では、辞書の最初の部分はこのようになっています。

dictionary="""\
access: アクセス
accuracy plot: 精度図
address: アドレス
alias: エイリアス
analysis: 分析
artifact: artifact
Artifact: Artifact
...

LLMとプロンプトを設定する

GPT-3またはGPT-4のモデルは、システム、ユーザー、AIの3種類のメッセージに対応しています。LangChain内蔵のChatPromptTemplateを使って、プロンプトのテンプレートを作成できます:

system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)


human_template = ("Here is a chunk of Markdown text to translate"
                  "Return the translated text only, without adding anything else."
                  "Text: \n {text}")
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)


# putting everything together
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, 
                                                human_message_prompt])

これにより、以下のように、input_language、output_language、dictionaryの値を適切に挿入できるようになります。

for chunk in splited_docs:
  prompt = chat_prompt.format_prompt(
             input_language="English", 
             output_language="Japanese", 
             dictionary=dictionary,
             text=chunk.page_content)

すべてを1つのチェーンにまとめる

チェーンとは、モジュール型のコンポーネント(または他のチェーン)を順番に組み合わせて目的を達成する汎用的な概念です。最もよく使われるタイプのチェーンはLLMChainで、PromptTemplate、Model、Guardrailsを組み合わせて、ユーザーの入力を受け取り、それを適宜フォーマットし、モデルに渡して応答を取得し、モデルの出力を検証して(必要なら)修正します。

LLMChainを作成して、テキストのチャンクを各チャンクに適したプロンプトでパイピングします。また、WandbTracerを追加して、パイプラインがどのように機能しているかを検査できるようにしましょう

from langchain.chains import LLMChain
from wandb.integration.langchain import WandbTracer


# we setup a W&B project to log our pipeline traces
WandbTracer.init({"project": "docs_translate"})


# define the chat model, in our case GPT4 (lower temps, less hallucination)
chat = ChatOpenAI(model_name="gpt-4", temperature=0.5)


# we probe the chain with the model and prompt template
chain = LLMChain(llm=chat, prompt=chat_prompt)

上記ではブロックを定義しました。ここでは、チャンクを繰り返してチェーンを呼び出す必要があります。

translated_docs = []
for i, chunk in enumerate(split_docs):
    print(f"translating chunk {i+1}/{len(split_docs)}...")
    chain_out = chain.run(
        input_language="English", 
        output_language="Japanese", 
        dictionary=eng_ja_dict,
        text=chunk.page_content)
    translated_docs += [chain_out]   


out_md = "\n".join(translated_docs)

翻訳されたチャンクを連結することになりますが、これも単純に改行を追加しているだけであるため理想的ではありません。ここには改善の余地があるでしょう。

トレース:開発プロセスの可視化

パイプラインがどのように機能しているかを理解するために、シングルチェーンの呼び出しを試してみるとができます。これは、エラーが発生したときや、予期せぬ振る舞いをデバッグするときに非常に有効です。今回のプロジェクトで使ったのは、ほぼほぼシングルチェーンのパイプラインですが、複数のレベルを組み合わせたプロジェクトの理解は難しくなるため、W&Bを使った可視化が非常に有効です。

W&Bトレーサーを使ったLLM開発の可視化

結果と考察

とある1ページの翻訳を見てみましょう。ヘッダーと本文構造は保持されていますが、リストに関しては少し手直しが必要です。翻訳クオリティーは概ね高く、製品・機能名など(例えば、Google Colab、Weights & Biases)、翻訳するべきでない単語は翻訳されていません。

やってみてわかった難しさ

  • 翻訳クオリティ:翻訳のクオリティーは、完璧にはならないだろうという割り切りを前提に今回のプロジェクトは実施され、最終的には極めて高い精度(感覚的には90%くらい)の翻訳が行われました。GPT-4はそもそもW &Bについて多くのことを知っていることが事前のテストでもわかっており、そのことも非常に役立ったようです。また、技術選定時にはDeepLのAI翻訳とも定性評価で比較しましたが、GPT-4の優位性はかなり明確でした。それでも、同じ単語が複数の場所で別の日本語に翻訳されてしまう「揺らぎ」の問題などは完璧に整えることはできませんでした。

  • フォーマット崩れ:今回のドキュメントは全てマークダウン形式で書かれていました。マークダウン形式は非常に普及しており、GPT-4の学習データにも多く含まれていただろうと想像されます。ほとんどの場合において、マークダウンのフォーマットは崩れずに出力されてきましたが、時としてテーブルのセルがずれてしまったり、引用文が閉じられずに続いてしまったりなどの問題が発生してしまい、最終的には全てのページをチェックする必要がありました。

  • 意味不明の言い回し:なぜか、下記のような文章が、文中に多数出現しました

"以下のMarkdownテキストを和訳してください。和訳したテキストだけを返して、それ以外のことは言わないでください。テキスト:"

このような表現が微妙に表現を変えて多数のバリエーションで文中のさまざまな箇所に現れました。これに関しては防げるような気もしますが、今回のプロジェクト中には解決は見つからず、「和訳」などのワードを検索して手動で削除しました。

  • その他、文中のリンクが壊れてしまったり、日本語に訳されてしまったり、細かい問題は他にも色々あり、またその程度の問題は想定の範囲内でもありました。全てをChatGPTに任せられるという期待値はそもそも持っていなかったものの、どうやって人間の編集フローにうまく自動翻訳を取り入れるのか、今後ドキュメントアップデートにも対応していくためには作り込みが必要だと感じました。

結論 - 今後の改善に向けて

まず、最小限のコーディングでこのようなツールにアクセスし、ほとんどの翻訳作業を完了させることができるのは驚くべきことです。機械翻訳もここまできたか、と終始感動していました。
処理面ではまだまだ改善の余地がありますが、モデルの高速化、信頼性の向上に伴い、人間が介入するべき箇所は減り、もっと明確になることが期待されます。
現在私たちは、文書分割の改善に取り組んでいます。構文を保持するマークダウンスプリッタが必要だと気づいたのは私たちだけではなかったようで、LangChain GitHubリポジトリにも未解決のイシューがあります。
細かい高速化や効率化はいろいろ考えられます。例えばチャンク内に存在するワードで辞書をフィルタリングする方法を導入すれば、トークンを節約することができ、リクエストをより速くできます。
より大きなコンテキストウィンドウにアクセスできるようになれば、プロンプトに全ページの翻訳を渡すことができる日も来るかもしれませんし、プロの翻訳者による翻訳でモデルを数ショット学習させた上で翻訳することなども可能になるでしょう。
今後ますますこのような手法は広く活用されていき、文章の伝達において国境を意識することはますます少なくなっていくでしょう。特に今回の例にあったようなドキュメンテーションの翻訳などは、文章に解釈の分かれる情緒性などが含まれることは少ないので、うってつけの応用事例となりました。

この記事が気に入ったらサポートをしてみませんか?