Julia meets LLMs

# 本日は

アドベントカレンダー $N=8$ 目の記事です．

LLM(大規模言語モデル）を翻訳エンジンとして使い英語で書かれた Julia の docstring, manual などを翻訳できるかを検証するために作ったパッケージ群を紹介します．モデル・プロンプトのチューニングが必要ですが docstring はできそうな感じです．

# 導入

新しいプログラミング言語，ライブラリ，フレームワークを触るとき，ドキュメントを読めと言われるが，読める環境にいる場合，それは次の意味で恵まれた状態です．

- ドキュメントが存在する
    - 先人たちが時間を割いて他の人のために作ってくれたものがある
    - それくぐらいコミュニティの規模がある
- 自分ができるレベルでの読める文章が存在する
  - ドキュメントがドキュメントの役割を果たしてる(理解の助けになる説明がある)
  - 母国語または教育機関で学んだ自然言語により記述されているものがある
- 読解力・解読する能力がある，気力がある
  - 特定の目的の知識を得るために当該の分野に関連する箇所を探す
  - 母国語で書かれているとは限らない文献を読む能力

ここでドキュメントというのはいわゆる「公式ドキュメント」と呼ばれるような一次情報を指していますが，広い意味で，媒体を問わず，コミュニティの人が書いた解説文でも言えることです．

## 読み手は思ったより賢くない

実際には上記の状態を持っている状態・人はそう多くありません．プログラミングについて教える立場をいくつか経験しましたが，学ぶ側は教える側・ドキュメントを書く側が想定しているほど聡明ではないです．次の例を考えてみましょう．

```julia
julia> y = x
ERROR: UndefVarError: `x` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1
```

これは `y` を定義するために書いたコードですが，`x` を定義してないのでエラーになります．ところが，学ぶ側は「何が起きているのだろう」これを読まない・読めないのです．

「おかしいなぁ `x` は定義したんだけれど」と思っていても実際には（セッションを再起動した関係で）定義してないということはよくあります．上記の例では高々６行ほどですが 100 行からなるエラーやC++のテンプレートに関するながーーーいエラーには困ってしまいます．このため，研究，実務など仕事をする際，それらの要請に耐えうる能力を得るにはある程度時間がかかってしまいます．

## 書き手は思ったよりも不親切だ

読む側も読む側ですが，書く側も書く側で課題はあります．仮に原因の箇所，今回の例では下記の箇所が主の要因です：

```julia
ERROR: UndefVarError: `x` not defined in `Main`
```

そこがわかっていても「`x` は定義されてません」以上のことは読み取れません．「じゃあ何をすればいいのか」までは説明してくれません．もちろん今回の場合は

```julia
julia> x = 3
julia> y = x
```

などのように `x` を定義する，`x` が何かの値を指している，といった読み手・ユーザによる自立した「解決する」動作が必要です．

~~長い C++ のメッセージなんて気が狂う．実際に修正すべき箇所とエラーメッセージの関連が見えにくい．また，Python の公式ドキュメントなんか典型的な例だ．あれは人に読ませるものなのか？日本語翻訳があるのはありがたいが，「そうなんですね，で，だからどうした？」という言葉が何回も思い浮かぶ．原文（英語）の方を見ても同様なので結局誘惑に負けて https://note.nkmk.me/en/python/ を読んでしまう．サンプルコードも豊富で実務の要請に耐えうる解説により何回も救われた．誰か公式ドキュメントの読み方を教えて欲しい．~~

おほん．．．失礼．

## 英語圏外に所属する人々の悩み

また，言葉の壁もあります，多くのプログラミング言語の公式ドキュメントは「英語」という自然言語で書かれています．母国語が英語ではない多くの日本人は母国語に対して相対的に不慣れな「英語」というものを読まなければいけません．Julia を例に取り上げてみましょう．

https://julialang.org/ に行ってみましょう．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/44c88cba-a9c2-9f70-f6ac-bff06262856f.png)

青色で描かれた Documentation ボタンを押します．

https://docs.julialang.org/en/v1/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/fa133e19-473b-a7e7-aeb6-94a168fe352d.png)

はい．もちろん（？）英語です．理解するために英語を読まなければいけません．

## なぜ「もちろん」と言わなければいけないのか？

Julia 自体は MIT（Massachusetts Institute of Technology）内で開発されたものですので彼らの母国語でドキュメントが書かれるのは自然でしょう．それが”たまたま”英語だったわけです．

この記事をここまで読んでくれる方は，特殊な手段を用いない限り，私と同様に日本人のかたが多いと思います．日本人の母国語は「日本語」と呼ばれる言葉であり「英語」ではありません．独自の言語を持っていることは素晴らしいことである一方，公用語の一つである英語を日本語と同等のスピードで必ずしもスラスラと読み・書き・会話・聞き取りができるとは限りません．その意味で我々は英語で書かれている文献を理解するのにある種のハンデがあるのです．

## React のお話

ここで JavaScript ライブラリの React を思い出しましょう．下記のリンクに行くと

https://react.dev/community/translations

`Full translations` という項目に日本語があります！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/fe0bd246-9704-8662-446f-c6ec8a3dbd59.png)

実際, https://ja.react.dev/ に行くと日本語を読める読者ならスラスラと読めるでしょう．

これだよ．こういうのでいいんだよ（割り箸を割りながら）．

---

なぜ日本語で読めるのかというと，コミュニティの方がメンテナンスをしているからです．Zenn の smikitky 氏の記事を参照：

https://zenn.dev/smikitky/articles/0d250f7367eda9

https://zenn.dev/smikitky/articles/chatgpt-md-translator

特に「React 日本語ドキュメントが実はわりと ChatGPT で翻訳されている件」の記事は興味深いです．紹介されている https://github.com/smikitky/chatgpt-md-translator 自分の資料の作成に使ったこともあります．

このように近年では LLM(大規模言語モデル) を用いた翻訳技術が採用されているケースがあることがわかります．

---

## Julia でもすればいいのでは？

そうかも？最近は o1-preview も OpenAI の API で使えるようになったので丸ごと投げられるかもしれませんね．

で？誰がするの？

使うのは無料ではなので金銭的な負担を誰かがしないといけません．手元に５０００超円あったらやりますが，残念ながらそこまでの大金を持ち合わせてはいません．

### 千里の道も一歩から

翻訳全部はお金的にしんどくても動的に，必要な箇所だけ，ピンポイントですればそこまで負担はありません．例えば関数などのAPI仕様を説明する docstring であればそこまでトークン数は多くないので比較的安価で済みます．（OpenAI API を使ったアクセスはユーザがクレジットカードを持っていることが前提となりますが．．．良い子のみんなは保護者にお願いしてみましょう）

## DocstringTranslation またそのファミリーの紹介

ようやく本題に入れます． Julia パッケージ DocstringTranslation.jl の紹介です．これらのパッケージ，パッケージ群を使うと日本語で情報にアクセスすることを可能にします．

https://github.com/AtelierArith/DocstringTranslation.jl 

事前に `OPENAI_API_KEY` を環境変数にセットアップします．`.env` に入れておいて `using DotEnv; DotEnv.load!()` でセットアップすることもできます．この辺は多くの OpenAI の API に依存するアプリケーションと共通していることでしょう．公式パッケージにはしていないので下記のようにしてソースを git clone で導入する必要があります．または `Pkg.add` で直接URLを指定してもOKです．

```sh
$ git clone https://github.com/AtelierArith/DocstringTranslation.jl
$ cd DocstringTranslation.jl
$ julia --project -e 'using Pkg; Pkg.instantiate()'
```

### docstring を日本語で読む

ここでは正弦関数 `sin` の docstring を例に使い方を紹介します．`sin` の docstring の原文は下記のとおりです．

```julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.11.2 (2024-12-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> # ? を入力することでヘルプモードに移る
help?> sin
search: sin asin sign sind sinc sinh in min isinf stdin asind sinpi asinh using

  sin(x)

  Compute sine of x, where x is in radians.

  See also sind, sinpi, sincos, cis, asin.

  Examples
  ≡≡≡≡≡≡≡≡

  julia> round.(sin.(range(0, 2pi, length=9)'), digits=3)
  1×9 Matrix{Float64}:
   0.0  0.707  1.0  0.707  0.0  -0.707  -1.0  -0.707  -0.0

  julia> sind(45)
  0.7071067811865476

  julia> sinpi(1/4)
  0.7071067811865475

  julia> round.(sincos(pi/6), digits=3)
  (0.5, 0.866)

  julia> round(cis(pi/6), digits=3)
  0.866 + 0.5im

  julia> round(exp(im*pi/6), digits=3)
  0.866 + 0.5im

  ────────────────────────────────────────────────────────────────────────────

  sin(A::AbstractMatrix)

  Compute the matrix sine of a square matrix A.

  If A is symmetric or Hermitian, its eigendecomposition (eigen) is used to
  compute the sine. Otherwise, the sine is determined by calling exp.

  Examples
  ≡≡≡≡≡≡≡≡

  julia> sin(fill(1.0, (2,2)))
  2×2 Matrix{Float64}:
   0.454649  0.454649
   0.454649  0.454649

julia>
```

`julia> @doc sin` としてもOKです．

とりあえずこれを日本語で読めるようにしましょう．

```julia
julia> using DocstringTranslation
julia> @switchlang! :Japanese
julia> @doc sin
```

このような結果が得られます．OpenAIのAPIの仕様上，実行ごとにやや翻訳結果は異なりますがおおむね下記の図のように日本語で読めることがわかるでしょう．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/2ba99b19-d66a-66a7-c2d7-162cea89cd09.png)

やったぜ！これで英語が苦手なユーザにも理解できる機会を与えることができます！Julia のドキュメントにありがちな（？）関係代名詞で修飾される長い文章の読解に苦労することからおさらばです！

## OpenAI 以外の代替方法

Local LLM を使う方法もあります．これを使えば API キーを入手することができなくても手元の計算機で計算（推論）が閉じるので仕様できるユーザがさらに広がります．Ollama は Local LLM を動かすためのツールとしてはとてもよくできています．インストールも簡単ですし，モデルをインストールするのも簡単です．ということで翻訳エンジンのバックエンドとして Ollama を使う版も書きました：

https://github.com/AtelierArith/DocstringTranslationOllamaBackend.jl

使い方は OpenAI 版とほぼ同じですが，事前に Ollama をインストールする必要があります．モデルには　gemma2:9b　を使っていますので事前に pull しておくと良いでしょう：

```
$ ollama pull gemma2:9b
```

翻訳にかかる時間はマシン依存ではありますが，待つことさえ許せば GPU, CPU 環境で動作することができるのでとっかかりには良いかなと思います．何回叩いてもお金が発生しないことはいいことです．

## 動作原理

Julia の docstring を入手するロジックを次のように上書きしています．

```julia
function Docs.parsedoc(d::DocStr)
    if d.object === nothing
        md = Docs.formatdoc(d)
        md.meta[:module] = d.data[:module]
        md.meta[:path] = d.data[:path]
        d.object = md
    end
    # この行を追加する
    translate_with_ollama(d.object, string($(lang)))
end
```

これは元の実装である

https://github.com/JuliaLang/julia/blob/5e9a32e7af2837e677e60543d4a15faa8d3a7297/stdlib/REPL/src/docview.jl#L104-L112

を上書きします．これは type-piracy を引き起こしますので使用には注意が必要です．一般に既存のメソッド（関数と入力の引数の組みから決まる実装）を上書きすることは推奨されていません．

https://docs.julialang.org/en/v1/manual/style-guide/#avoid-type-piracy

超極端なことを言えば下記のようなコードを実行することも原理的には可能です．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/1b40ebf3-efde-9785-4e2c-09ce0e738c2a.png)

ただこれを宣言する時点でJuliaのセッションが落ちてしまいます．なので一般的には好ましいアプローチではありません．

もう少し上品なことをするのであれば

https://github.com/JuliaLang/julia/blob/5e9a32e7af2837e677e60543d4a15faa8d3a7297/stdlib/REPL/src/docview.jl#L203-L246

の実装を多重ディスパッチでなんとかするとか別のマクロを作ることになると思いますが，help mode で使える魅力がなくなってしまいます．（できるかもしれないけれどそこまで REPL.jl のソースコードの理解が追いついていないです）

まぁ，とりあえず”動く”のでもしよかったら使ってみてください．