ChatGPTを使って、TeXで書かれた数学書を日本語に翻訳してみた

## 概要

冬休みのお勉強として、TeXで書かれた数学書 [Category Theory for Programmers](https://github.com/hmemcpy/milewski-ctfp-pdf) を、ChatGPTを使って日本語に翻訳してみました。
従来の機械翻訳を用いたときと比べて良い結果になりましたので、その方法を共有します。

* Category Theory for Programmersの日本語翻訳プロジェクト: https://github.com/sonoisa/milewski-ctfp-pdf-japanese
  <img src="https://user-images.githubusercontent.com/601206/47271389-8eea0900-d581-11e8-8e81-5b932e336336.png" width="600">
  （画像の出典: [An unofficial PDF version of "Category Theory For Programmers"](https://github.com/hmemcpy/milewski-ctfp-pdf)）

## 今回用いた方法

数学書を日本語に翻訳する場合、一つの英単語に対して日本語訳が複数存在することがありますし、また、ChatGPTが知らない単語の場合は変な翻訳になることがあります。
そこで今回はそのような単語については翻訳例を与えることで、翻訳のブレや誤りを減らすことにしました。
具体的には次のステップとプログラムを用いました。

### 1. 複数の翻訳候補があったり、ChatGPTの学習データになさそうな単語について翻訳例を手動で作成

今回は翻訳対象書籍の索引と、圏論の代表的な書籍（[圏論の基礎](https://www.maruzen-publishing.co.jp/item/?book_no=294317)）の索引を眺めつつ、以下のような翻訳例を最終的に約250件ほど用意しました。
期待通りではない翻訳を見かけるたびに翻訳例を追加することも少ししました。

```yaml
- 2-categories: 2-圏
- 2-category: 2-圏
- a meet in order theory: 順序理論における交わり
- ad hoc polymorphism: アドホック多相性
- additive: 加法
- adjoint: 随伴
- adjunct: 随伴
- adjunction: 随伴
- algebra: 代数
- algebraic data type: 代数的データ型
- Anamorphism: Anamorphism
- arity: アリティ
- arrows: 矢印
- associative: 結合的
...
```

全量は https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/for_translation/CTFP_dictionary.md?plain=1 を参照のこと。


### 2. 一つのTeXファイルごとに翻訳用プロンプトをプログラムで生成

1で作成した単語の翻訳例と、翻訳対象のTeXファイル（1ファイルごと）を入力にして、翻訳用プロンプトを出力するプログラムを作成しました。

このプログラムで行う主な処理は、翻訳対象ファイルに現れる単語に翻訳例を絞り込んで、それらをプロンプトに書き出すことです。
翻訳例が増えるとプロンプトのトークン数を消費しがちですし、翻訳例を全部与えるよりも必要十分なもののみ与えた方が期待通りの翻訳になったため、絞り込むようにしました。
（その他、翻訳の邪魔になるTeXコマンドを削除することも行っています）


プロンプト生成プログラム: 
https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/for_translation/create_prompt.py

```python
import sys
import re


def load_dictionary(dict_file):
    dictionary = {}
    with open(dict_file, 'r', encoding='utf-8') as file:
        for line in file:
            if line.startswith("- "):
                word, definition = line[2:].split(": ", 1)
                dictionary[word.strip().lower()] = line.strip()
    return dictionary

def find_definitions(text, dictionary):
    examples = ""
    text = text.lower()
    for word in dictionary:
        if word in text:
            examples += f"{dictionary[word]}\n"
    return examples

def delete_lettrine_commands(text):
    pattern = r'\\lettrine(?:\[[^\]]*\])?\{([^}]*)\}\{([^}]*)\}'
    
    def replacer(match):
        return f"{match.group(1)} {match.group(2)}"
    
    return re.sub(pattern, replacer, text)

def main():
    if len(sys.argv) != 3:
        print("Usage: python script.py <text_file> <dictionary_file>")
        sys.exit(1)

    text_file = sys.argv[1]
    dict_file = sys.argv[2]

    dictionary = load_dictionary(dict_file)

    with open(text_file, 'r', encoding='utf-8') as file:
        text = file.read()
        text = delete_lettrine_commands(text).strip()
        examples = find_definitions(text, dictionary)

    print(f'''次の圏論とプログラミング言語に関するTeXコードを制約条件と翻訳例に従って自然かつ綺麗な日本語に翻訳してください。

# 制約条件
- 日本語として自然かつ綺麗な表現になるようにしてください。
- 説明は省略して翻訳結果のみを出力してください。
- 翻訳例にある単語はそれに従って翻訳してください。
- TeXのコマンドとプログラムは翻訳しないでください。
- 元々あったTeXコマンドは翻訳によって削除しないでください。

# 翻訳例
{examples}

# 英語のTeXコード
"""
{text}
"""

# 日本語のTeXコード (自然かつ綺麗な日本語表現) ''')

if __name__ == "__main__":
    main()
```


このプログラムを用いて作成したプロンプトの例（抜粋）を下記に示します。
制約条件と翻訳例を明示的に与えて翻訳を行わせるプロンプトになっています。
TeXコマンドがそのまま残っているテキストを翻訳対象にしていることにも注意してください。

```latex
次の圏論とプログラミング言語に関するTeXコードを制約条件と翻訳例に従って自然かつ綺麗な日本語に翻訳してください。

# 制約条件
- 日本語として自然かつ綺麗な表現になるようにしてください。
- 説明は省略して翻訳結果のみを出力してください。
- 翻訳例にある単語はそれに従って翻訳してください。
- TeXのコマンドとプログラムは翻訳しないでください。
- 元々あったTeXコマンドは翻訳によって削除しないでください。

# 翻訳例
- algebra: 代数
- arrows: 矢印
- associative: 結合的
- bottom: ボトム
- categories: 圏
- category: 圏
- composition: 合成
- end: エンド
- equality: 等価性
- Haskell: Haskell
- instance: インスタンス
- limit: 極限
- morphism: 射
- object: 対象
- polymorphic function: 多相的関数
- rank: ランク
- ring: 環
- type: 型
- unit: 単位
- universal: 普遍
- programmer: プログラマ


# 英語のTeXコード
"""
% !TEX root = ../../ctfp-print.tex

A category is an embarrassingly simple concept.
A category consists of \newterm{objects} and \newterm{arrows} that go between them. That's
why categories are so easy to represent pictorially. An object can be
drawn as a circle or a point, and an arrow\ldots{} is an arrow. (Just
for variety, I will occasionally draw objects as piggies and arrows as
fireworks.) But the essence of a category is \emph{composition}. Or, if you
prefer, the essence of composition is a category. Arrows compose, so
if you have an arrow from object $A$ to object $B$, and another arrow from
object $B$ to object $C$, then there must be an arrow --- their composition
--- that goes from $A$ to $C$.

\begin{figure}
  \centering
  \includegraphics[width=0.8\textwidth]{images/img_1330.jpg}
  \caption{In a category, if there is an arrow going from $A$ to $B$ and an arrow going from $B$ to $C$
    then there must also be a direct arrow from $A$ to $C$ that is their composition. This diagram is not a full
    category because it’s missing identity morphisms (see later).}
\end{figure}

\section{Arrows as Functions}

Is this already too much abstract nonsense? Do not despair. Let's talk
concretes. Think of arrows, which are also called \newterm{morphisms}, as
functions. You have a function $f$ that takes an argument of type $A$ and
returns a $B$. You have another function $g$ that takes a $B$ and returns a $C$.
You can compose them by passing the result of $f$ to $g$. You have just
defined a new function that takes an $A$ and returns a $C$.

In math, such composition is denoted by a small circle between
functions: $g \circ f$. Notice the right to left order of composition. For some
people this is confusing. You may be familiar with the pipe notation in
Unix, as in:

（省略）
"""

# 日本語のTeXコード (自然かつ綺麗な日本語表現) 
```


## 翻訳結果の所感

ChatGPT（GPT-4-Turbo）を用いて数学書のTeXファイルの翻訳を行った結果の所感は次のとおりです。

1. TeXコマンドが残ったままでも正しく翻訳してくれる。これだけで非常に嬉しい。
    * 従来の機械翻訳サービスでは、TeXコマンドが削除されたり、コマンド名が日本語に翻訳されてしまいコンパイルが通らなくなったりしたが、そのような問題はほとんど発生しない（ゼロではない。ところどころ削られてしまったりすることがある）。
    * TeXコマンドの部分はそのままに、引数の部分のみ翻訳してくれてありがたい。
1. 概ね翻訳例に従って翻訳してくれる。しかし、時々従ってくれないこともあるため確認と修正が必要である。
1. ほぼ直訳で、数学的な言い回しが上手く翻訳されない。あくまでも翻訳の叩き台の作成までで、人間の手によって自然な表現に修正する作業がやはりかなり発生する。
1. 翻訳速度は遅い。それでも人間が翻訳するよりは圧倒的に速い。


ご参考に、先ほどのプロンプトを用いたChatGPTによる生成結果例（一部人間による修正あり）:
```latex
% !TEX root = ../../ctfp-print.tex

圏は驚くほど単純な概念です。圏 (category) は\newterm{対象 (object)} とそれらの間を
結ぶ\newterm{矢印 (arrow)} で構成されます。だから圏は図示しやすいのです。対象は円や
点として描かれ、矢印は\ldots{}矢印です。(たまに変わり種として、対象を豚として、矢印を
花火として描くこともあります。) しかし、圏の本質は\emph{合成}です。もしくは、合成の本質が
圏です。矢印は合成できるので、対象$A$から$B$への矢印があり、さらに$B$から$C$への矢印が
あるならば --- それらの合成である --- $A$から$C$への矢印が存在しなければなりません。

\begin{figure}
  \centering
  \includegraphics[width=0.8\textwidth]{images/img_1330.jpg}
  \caption{圏では、$A$から$B$への矢印と、$B$から$C$への矢印がある場合、
  それらの合成である$A$から$C$への直接の矢印も存在しなければなりません。
  この図は、恒等射 (後述) が欠けているため完全な圏ではありません。}
\end{figure}

\section{関数としての射}

抽象的すぎてナンセンスに感じますか？落胆しないでください。具体例で考えましょう。矢印、
つまり\newterm{射 (morphism)} を関数と考えてみましょう。型$A$の引数を取り、$B$を
返す関数$f$があります。そして、$B$を取り、$C$を返す別の関数$g$があります。それらを、
$f$の結果を$g$に渡すことで合成することができます。これにより、$A$を取り、$C$を返す
新しい関数が定義されました。

（省略）
```



## 現時点の翻訳成果物: プログラマのための圏論 (Category Theory For Programmers 日本語版)

いま時点の翻訳成果物である、プログラマのための圏論 (Category Theory For Programmers 日本語版) のPDFファイルは下記からダウンロードできます。  
まだ翻訳の修正途中であるため、変な部分が多々あると思います。

- 閲覧用
  - [Standard Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp.pdf)
  - [Scala Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp-scala.pdf)
  - [OCaml Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp-ocaml.pdf)

- 印刷用
  - [Standard Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp-print.pdf)
  - [Scala Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp-print-scala.pdf)
  - [OCaml Edition](https://github.com/sonoisa/milewski-ctfp-pdf-japanese/blob/master/build/ctfp-print-ocaml.pdf)

2024/01/08時点の翻訳精度に至るのにかかった日数は、ChatGPTによる処理時間を除いて、約3日でした（そのうち2日は手動による単語レベルの誤訳修正作業）。ChatGPTを用いない方法と比べて非常に短く済んでいるのではないでしょうか。