上司「オンプレでチャットAI作って」

Pythonで[Rinna社](https://rinna.co.jp/)が提供している対話LLM [rinna/japanese-gpt-neox-3.6b-instruction-sft](https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-sft) を動かすことで、ローカルで動作するChatGPTのような対話AIソフトを作成しました！

筆者自身もLLMや自然言語AIに全く詳しくなく[^kuwasikunai]、Pythonに触ったのも久々だったのですが、言語モデルを手元で動かすのは案外簡単(大嘘[^youkenn])だったため、本記事ではその紹介をしたいと思います！

[^kuwasikunai]: 用語を誤使用している可能性が高いので、もしその時はコメント等でご指摘くださると幸いです。
[^youkenn]: 改めて後述していますがマシンパワーでゴリ押すスタイルです。動かすだけなら**手順は**簡単というのが本記事の主張です。

![rinna.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/f91216b7-f704-697b-b9b9-78ebcd564cc8.gif)

↑アイキャッチのこれは成果物である[Tauri](https://tauri.app/)製アプリです。内部でPython[^pyinstaller]によって前述のrinna言語モデルを動かすことで実現しています([後述](#pyinstaller%E3%81%A7%E5%AE%9F%E8%A1%8C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%8C%96%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B))。

リポジトリ: https://github.com/anotherhollow1125/rinna_gpt
リリース: https://github.com/anotherhollow1125/rinna_gpt/releases

Windows限定ですがインストーラ([rinna_gui_0.0.5_x64_ja-JP.msi](https://github.com/anotherhollow1125/rinna_gpt/releases/tag/v0.0.5))からインストールするだけで動きます！(CPUで演算させているので遅いのはご容赦ください...また、Cドライブを7GBほど持っていきます)

なお、タイトルにオンプレと書きましたがLAN内サーバーで動かすのではなく手元のPCで動かす想定です。

[^pyinstaller]: 後述していますが、正確にはPyInstallerによって実行ファイルとしたものになります。

# ある日

**※本物語は完全なるフィクションです**。筆者が筆者の上司にあたる人物より該当のソフトウェアの作成を指示されたという事実は全くありません。

上司「今流行りのチャットGPT？ってやつあるじゃん:grimacing:あんな感じでAIと対話できるサービスを作って、社内で利用させたいんだけどできる？:thinking: 」

筆者「ChatGPTなら、[API](https://platform.openai.com/docs/api-reference)が公開されているので[APIキー](https://platform.openai.com/account/api-keys)を発行すればすぐ組み込めますよ」

上司「...いや:angry:、chatGTPは使わないで実現してほしいんだよね:smiley:」

筆者「(いちいち表記ゆれしてくるの何なんだ...GTPって脂肪肝かよ)」

筆者「何故です...？」

上司「OPEN AIに社内機密が漏えいする可能性があるからね:triumph:。実際[サムスンの例](https://pc.watch.impress.co.jp/docs/news/yajiuma/1490904.html)があるのでOpenaiのサービス利用はなしの方向性でお願いしたいです！:grin: 」

筆者「(なんだってうちの上司はこういった類のニュースだけはチェックしてるんじゃ...[某ソースコード流出事件](https://xtech.nikkei.com/atcl/nxt/news/18/09551/)以降、GitHubの利用すら禁止してくるしよぉ...)」

筆者「つまり、オンプレミス、オフラインで使用できるChatGPTが欲しいということでしょうか？」

上司「そんな感じでイイカンジでお願いしたいかな」

筆者「知りません。他当たってください(技術的には可能かもしれませんがとても高性能なグラボととても高性能なCPUと大容量のメモリをモリモリ積んだハイスペックPCが必要になります)」

上司「本音と建前が逆ぅ...:cold_sweat: (しかも建前？もなかなか強欲...)」

...という成り行きでChatGPT以外の言語モデルをローカルで動かすことを命じられた筆者。言語モデル入門の足がかりを探します！

# とりあえず動く言語モデル探し ～キ○ーピー3分コーディング～

いきなりローカルで環境構築して動かすのも大変なので、公開されている言語モデルを探してきて[Google Colaboratory](https://colab.research.google.com/)で動かしてみようと思います。

「GPT オープンソース」や「LLM 日本語」みたいに色々と工夫して検索してみた感じ、以下2つのプレスリリースが目に入りました。

- [rinna、日本語に特化した36億パラメータのGPT言語モデルを公開｜rinna株式会社](https://rinna.co.jp/news/2023/05/20230507.html)

https://rinna.co.jp/news/2023/05/20230507.html

- [サイバーエージェント、最大68億パラメータの日本語LLM（大規模言語モデル）を一般公開 ―オープンなデータで学習した商用利用可能なモデルを提供― | 株式会社サイバーエージェント](https://www.cyberagent.co.jp/news/detail/id=28817)

https://www.cyberagent.co.jp/news/detail/id=28817

他にもありそうですがとりあえずこの2つを動かしてみましょう...！

...と思ったのですが、36億パラメータや68億パラメータモデルは、ColabolatoryのシステムRAM制限に引っかかってしまったので、とりあえず触る目的でパラメータが少ないやつにしてみます。

## Rinna GPT

36億(3.6B)モデルは

- [rinna/japanese-gpt-neox-3.6b · Hugging Face](https://huggingface.co/rinna/japanese-gpt-neox-3.6b)

なのですがColaboratoryの無料枠で使えるシステムRAM12GBメモリを全て消費してしまいエラーになったので、ソースコードは流用し、モデルだけGPT2のもの(パラメータ数は13億)にすげ替えて実行してみます

- [rinna/japanese-gpt2-medium · Hugging Face](https://huggingface.co/rinna/japanese-gpt2-medium)

ではいよいよ早速 [Google Colaboratory](https://colab.research.google.com/) にアクセスし、ノートブックを新規作成して動かしてみましょう！

https://colab.research.google.com/

Colaboratoryには`torch`類のパッケージは予め入っているようなので、デフォルトで入っていない`transformers`パッケージを`pip`で入れましょう。

```bash:Colaboratory 1つ目のセル
!pip install 'transformers[sentencepiece]'
```

行頭の`!`は特殊な命令を実行するために必要なので除かず入れてください。左の :arrow_forward: を押し実行して`transformers`を入れます。

[transformersパッケージ](https://huggingface.co/docs/transformers/index)は、[HuggingFace :hugging: ](https://huggingface.co/)という機械学習コミュニティが提供しているパッケージで、モデルを扱いやすくするためのAPIを提供してくれているようです...大丈夫です、筆者も今回初めて知りましたがこれ以上の詳細は不要です。さすがAPIパッケージ。

2つ目のセルが本命です！

:::note info
必須ではありませんが、左上の`編集`>`ノートブックの設定`>`ハードウェアアクセラレータ`を`None`から`GPU`に変更することで若干速く実行されます。
:::

```python:Colaboratory 2つ目のセル
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

# トークナイザー( 文章をいい感じに分解(トークナイズ)してくれるやつ )指定
tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt2-medium", use_fast=False)
tokenizer.do_lower_case = True  # due to some bug of tokenizer config loading

# モデル指定
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt2-medium")

# グラボが使える場合使う
# VRAMが足りない時はこのif文を消す
if torch.cuda.is_available():
    model = model.to("cuda")

text = "西田幾多郎は、"
token_ids = tokenizer.encode(text, add_special_tokens=False, return_tensors="pt")

with torch.no_grad():
    output_ids = model.generate(
        token_ids.to(model.device),
        max_new_tokens=100,
        min_new_tokens=100,
        do_sample=True,
        temperature=0.8,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id
    )

output = tokenizer.decode(output_ids.tolist()[0])
print(output)
```

コピペして実行しましょう！筆者の場合は次のような出力が出ました。

```plaintext:実行結果例
西田幾多郎は、こうした「文化」が、現代において非常に有用な「文化」であり、「古典」であると捉え、現代に活かせる「文化」を、社会や地域、国、そして時代を巻き込んだ「文化」へと変容させていくことを示唆しています。 「文化」は、その時代の変化や社会にそって変化していきますが、それをただ捉え、その時代にふさわしい、よりよいものとするためには、新しい文化を創造し、取り入れる必要があります。 そのためには
```

それっぽい文章が出ましたね...！第一歩を踏み出せました。

ところで西田幾多郎って誰()

## OpenCALM

お次はサイバーエージェントの方を試してみたいと思います！OpenCALMの本命は68億パラメータの

- [cyberagent/open-calm-7b · Hugging Face](https://huggingface.co/cyberagent/open-calm-7b)

なのですがRinna同様無料枠ではシステムRAMが足りないので、10億パラメータの

- [cyberagent/open-calm-1b · Hugging Face](https://huggingface.co/cyberagent/open-calm-1b)

の方を動かしてみました

Rinnaの時と同様、`transformers`を入れます。(トークナイザーの`centencepiece`の指定はOpenCALMでは不要みたいです)

```bash:Colaboratory 1つ目のセル
!pip install transformers
```

では早速実行！(`AutoModelForCausalLM.from_pretrained("cyberagent/open-calm-1b", device_map="auto", torch_dtype=torch.float16)`の、`device_map="auto", torch_dtype=torch.float16`はエラーになってしまったので削除しています)

```python:Colaboratory 2つ目のセル
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("cyberagent/open-calm-1b")
tokenizer = AutoTokenizer.from_pretrained("cyberagent/open-calm-1b")

inputs = tokenizer("AIによって私達の暮らしは、", return_tensors="pt").to(model.device)
with torch.no_grad():
    tokens = model.generate(
        **inputs,
        max_new_tokens=64,
        do_sample=True,
        temperature=0.7,
        top_p=0.9,
        repetition_penalty=1.05,
        pad_token_id=tokenizer.pad_token_id,
    )
    
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
print(output)
```

筆者の環境では次のような出力になりました

```plaintext:実行結果例
AIによって私達の暮らしは、より便利に快適なものへと進化してきました。
電気やガスなどのエネルギーはもちろんのこと、「食」「睡眠」といった生活の基本となるものまでをも自動化しています。「IoT(Internet of Things)」と呼ばれる技術の進化により「つながる世界」、つまりモノのインターネットが急速に進化し続けています。」と語るのは株式会社MJI代表取締役社長兼CEOの
```

ところでOpenCALMのCALMってなんの略かなぁと思っていたのですがサイバーエージェント言語モデル (CyberAgent Langage Model) でしょうか。なんかかっこいい感じがしました

## Colaboratoryでの実行所感Q&A

**Q. モデルはいつ/どこでダウンロードしたんだ...？**

A. `transformers`パッケージにより自動的にダウンロードされています。`AutoModelForCausalLM.from_pretrained`にてHuggingFaceへの登録名を指定するだけでモデルを取ってきてくれる親切仕様なのです！

**Q. じゃあオフラインで実行できなくない...？**

A. 最初に一度ダウンロードしてしまうと以降はキャッシュされるのでオフラインでも動いてくれるようになります。パスも特に変更する必要はないです。オフライン実行の詳細は次の記事様が詳しかったです、

- [Huggingface Transformersのモデルをオフラインで利用する - Qiita](https://qiita.com/suzuki_sh/items/0b43ca942b7294fac16a)

https://qiita.com/suzuki_sh/items/0b43ca942b7294fac16a

が、どのみち最初にはダウンロードが必要ですし、キャッシュが残っていれば特に細工なしでもオフラインで動いたので、本記事ではこれ以上「オフラインであること」にはこだわらないことにします。

**Q. 結局何やっているのかわからないのだけど...**

A. **筆者にも説明の技量がありません！！！** 一言で言うと、「プロンプトの次に来る可能性が最も高い単語を予測している」だけらしいです。ChatGPT-3.5や4を使っていると忘れてしまいがちですが、パラメータの少ないモデルだと実際そのような出力をしているように感じられます。 `model.generate` メソッドの各種パラメータにつきましては次の記事様が詳しかったです。

- [rinna GPT-2モデルの生成パラメータ](https://zenn.dev/tyaahan/articles/a8d99900000002)

https://zenn.dev/tyaahan/articles/a8d99900000002

# 手元で動かしたい ～環境の条件と構築～

Colaboratoryの無料枠では動かせるモデルに限界があったので、ここからはローカルに環境を構築して実行してみたいと思います！

なお最終的に対話モデルを動かしたいのでここからはRinnaの方に絞って解説していきます

## 実行環境

環境を構築する前に断っておくとここからは**マシンパワーが必要です**。十分なDRAMに加え、ミドル～ハイエンドCPUあるいはGPUが必要になります。

筆者の環境はCPUパワーでゴリ押しています、一応以下に動作した筆者の環境を書いておきます[^env]。**最低スペックでどれぐらい必要かは検証していません**。

[^env]: 自慢(笑)みたいになって嫌だったのですが「動かないんですけど？！」って言われるよりはマシかと思い明記してます...

筆者環境

- Windows 11
    - 以降解説も全てWindowsです。LinuxやMacの方はごめんなさい
- CPU: Ryzen 9 3950X
    - そのほかの環境では試していないので最低限必要なCPUはわからないです
    - 多分スレッドが多くて高性能な程速いです
- メモリ: 96GB
    - 最低でも16GBは必要だと思います
    - このうちRinna 3.6Bは14GB程を持っていきます

筆者は確認できていませんが、グラフィックボードでゴリ押しする場合はVRAM 12GB以上のモデルが必要らしいです(VRAM 8GBでは無理でした)。

## 環境構築① Python環境(pyenv+pipenv)の導入

解説自体はシンプルだと思いますが、Pythonの環境構築なので、各種環境に依存した問題がたくさん出るかと思います。その辺の解決は各自頑張っていただけると幸いです。

環境が汚れても良いならばpipでいきなり各種パッケージをインストールしてしまっても良いですが、**仮想環境を作る方法を推奨します**。

というのも[HuggingFace公式](https://huggingface.co/docs/transformers/installation)も仮想環境を推奨しているのと、依存関係の問題が発生した際に作り直しやすい、他のハードへの移行がし易い等メリットしかないためです。むしろ直接インストールしてしまうと大変なことになります...

ベストプラクティスを知らないのですが、今回はpyenv+pipenvで環境構築を行いました。導入済みであったり他の方法で管理していたりする場合は本節は読み飛ばしてください。

- pyenv: Python本体のバージョンを複数管理するツールです。
- pipenv: フォルダごとにパッケージの管理を行うツールです。

### pyenv

- https://github.com/pyenv-win/pyenv-win

https://github.com/pyenv-win/pyenv-win

PowerShellでの導入が一番楽な気がします。

```PowerShell:PowerShell
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"
```

:::note warn
`running scripts is disabled on this system.`みたいな内容が書かれた真っ赤なエラーが出た場合`ExecutionPolicy`が`Restricted`になっているのが多分原因です。その場合`RemoteSigned`等に変更してください。

```PowerShell:PowerShell
Set-ExecutionPolicy RemoteSigned
```
:::

シェルを再起動すると`pyenv`が使えるようになっています。LLMを動かすPythonのバージョンはpipenv側で指定しますが、pipenv導入にPythonを使用するので、デフォルトとなるPythonを入れましょう。最新は`3.12`っぽいですが筆者は`3.11.3`で各種動作の確認を行ったため、`3.11.3`とさせていただきます。

```PowerShell:PowerShell
pyenv install 3.11.3
pyenv global 3.11.3
```

### pipenv

こちらはpipで取ってくるツールになります。簡単！

```PowerShell:PowerShell
pip isntall pipenv
```

使い方は次節で実際に使っていくのでそこで解説します。

## 環境構築② 必要なパッケージのインストール

Rinna GPTを動かすための依存を入れていきます！

まずはプロジェクト用ディレクトリを作成した後、Pythonのバージョンを`3.11.3`として仮想環境を作成します。

```PowerShell:PowerShell
mkdir rinna_app
cd rinna_app
pipenv --python 3.11.3
```

### PyTorch

そしてまずはPython機械学習のデファクトスタンダードとも言えるライブラリ`torch`を入れる...

のですが、CPUで動かすか、グラフィックボードで動かすか、どちらにするかで、打つpipコマンドが変わってきます。[PyTorch公式](https://pytorch.org/get-started/locally/)にて環境を入力すると打つべきコマンドを教えてくれるページがあるので、そこを参照します。

https://pytorch.org/get-started/locally/

**グラフィックボード(CUDA)を利用する場合**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/27aa0580-1f44-e80d-2e52-305a3ed14937.png)

`pip3`を`pipenv`にし、また`index-url`オプションは`pypi-mirror`オプションにします。

```PowerShell:PowerShell (グラボを使う場合のみ実行！)
pipenv install torch torchvision torchaudio --pypi-mirror https://download.pytorch.org/whl/cu117
```

**CPUで動かす場合**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/b94803d0-a01b-751c-986e-7d3b5a97c788.png)

```PowerShell:PowerShell (CPUで動かす場合のみ実行！)
pipenv install torch torchvision torchaudio
```

以降大変申し訳無いですがGPUの方は実行確認が取れていないので、本記事では「CPUで動かす場合」を選択したものとしてください。

### transformers

Colaboratoryで入れたのと同様、HuggingFace提供の`transformers`を入れます。

```PowerShell:PowerShell
pipenv install 'transformers[sentencepiece]'
```

これで準備は万端です！

# いよいよローカルで実行！

先程Colaboratoryで動かせなかった、36億パラメータモデルを動かしてみましょう！

- https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-sft

https://huggingface.co/rinna/japanese-gpt-neox-3.6b-instruction-sft

## サンプル実行

公式が出しているサンプルコードをそのままコピペして実行です！

```python:main.py
prompt = [
    {
        "speaker": "ユーザー",
        "text": "日本のおすすめの観光地を教えてください。"
    },
    {
        "speaker": "システム",
        "text": "どの地域の観光地が知りたいですか？"
    },
    {
        "speaker": "ユーザー",
        "text": "渋谷の観光地を教えてください。"
    }
]
prompt = [
    f"{uttr['speaker']}: {uttr['text']}"
    for uttr in prompt
]
prompt = "<NL>".join(prompt)
prompt = (
    prompt
    + "<NL>"
    + "システム: "
)
print(prompt)

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", use_fast=False)
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft")

# 筆者環境ではCPUで計算させるので↓は走りません
if torch.cuda.is_available():
   model = model.to("cuda")

token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")

with torch.no_grad():
    output_ids = model.generate(
        token_ids.to(model.device),
        do_sample=True,
        max_new_tokens=128,
        temperature=0.7,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id
    )

output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1):])
output = output.replace("<NL>", "\n")
print(output)
```

`pipenv shell`で仮想環境を有効にした後、`python main.py`で実行です！

```PowerShell:PowerShell
pipenv shell
python main.py
```

(ちなみに`pipenv run python main.py`というコマンドで実行しても構いません)

一番最初に実行するときだけモデルダウンロードが走ります。以下のような出力になれば成功です！

```plaintext:実行結果
ユーザー: 日本のおすすめの観光地を教えてください。<NL>システム: どの地域の観光地が知りたいですか？<NL>ユーザー: 渋谷の観光地を教えてください。<NL>システム: 
Downloading (…)okenizer_config.json: 100%|██████████████████████████████████████████████████████████████████| 284/284 [00:00<?, ?B/s]
Downloading spiece.model: 100%|███████████████████████████████████████████████████████████████████| 786k/786k [00:00<00:00, 8.55MB/s]
Downloading (…)lve/main/config.json: 100%|██████████████████████████████████████████████████████████████████| 534/534 [00:00<?, ?B/s]
Downloading model.safetensors: 100%|████████████████████████████████████████████████████████████| 7.37G/7.37G [01:44<00:00, 70.4MB/s]
あなたが訪れたい場所は何ですか?

```

返答出力には少し時間がかかります。筆者環境では「あなたが訪れたい場所は何ですか？&lt;/s&gt;」という出力になりました！<font color="#bbbbbb">なんか短いし不自然ですが気にしないことにしましょう</font>

## 対話アプリにしてみる

せっかくの対話モデルなのでインタラクティブなアプリにしてみます！

いきなり完成形で申し訳ないですが、ソースコードは以下になります

```python:interactive.py
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import sys
import io

# Shift_JISから逃れるための呪文なので気にしないでください
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

def _input(prompt):
    print(prompt, end='', flush=True)
    s = sys.stdin.buffer.readline()
    s = s.decode('utf-8') \
        .replace("\r", "") \
        .replace("\n", "")
    return s

# 公式のExampleを参考にしたプロンプト組み立て関数
def make_prompt(log):
    prompt = [
        f"{uttr['speaker']}: {uttr['text']}"
        for uttr in log
    ]
    prompt = "<NL>".join(prompt)
    return prompt

# 対話については辞書で持っておくようにする
def add_log(log, role, text):
    log.append({
        "speaker": role,
        "text": text
    })

k = 40
max_length = 128

# ちょっとずつ結果を出力してくれるジェネレータ。ChatGPTに聞きました、後述
def gradually_generate(model, tokenizer, token_ids, max_length):
    for _ in range(max_length):
        with torch.no_grad():
            outputs = model(token_ids.to(model.device))

        logits = outputs.logits
        indices_to_remove = logits < torch.topk(logits, k)[0][..., -1, None]
        logits[indices_to_remove] = float('-inf')
        probs = torch.nn.functional.softmax(logits[..., -1, :], dim=-1)
        next_token_id = torch.multinomial(probs, num_samples=1)
        token_ids = torch.cat((token_ids, next_token_id), dim=-1)

        output_str = tokenizer.decode(next_token_id[0])

        yield output_str.replace("<NL>", "\n")

        if "
" in output_str:
            break

tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", use_fast=False)
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft")

log = []

while "[exit]" not in (user_message := _input("> ")):
    add_log(log, "ユーザー", user_message)

    prompt = (
        make_prompt(log)
        + "<NL>"
        + "システム: "
    )

    token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")

    output = ""
    for word in gradually_generate(model, tokenizer, token_ids, max_length):
        print(word, end='', flush=True)
        output += word
    print()

    add_log(log, "システム", output)

```

`[exit]`が渡されるまで繰り返しプロンプトを打てるCLIアプリケーションにしてみました！

```plaintext:実行結果例
pipenv run python interactive.py
> りんなについて教えてください
りんなは、人工知能アプリケーションで生成された音声アシスタントです。音声を使用して、さまざまなタスクを実行し、質問にも答えることができます。

> 人工知能とはなんですか？
人工知能とは、機械が知能的能力を持つ機械のことです。機械と話をして、機械が答えるべき質問をすると、機械は質問を理解して答えます。また、自然言語処理技術を使用することもあり、自然言語処理は多くの機械が「自然な会話」をするのに役立ちます。

> [exit]
```

### こだわり ～ちょっとずつ出力～

アイキャッチGIFのように、ChatGPTの出力は単語ごとにちょっとずつなされます。筆者は最初、そういうUIなのだと勘違いしていたのですが、どうやら本当に**逐次的に単語を予測して少しずつ返答している**らしいです。カッコよくて真似したかったので本家ChatGPT先生に方法を聞いて、それを元に`gradually_generate`を書きました。

<details><summary>ChatGPT-4先生との会話</summary>

> 筆者
> 
> Hugging Faceのライブラリtransformersを使用して会話生成プログラムを作成しています。
> 現在ソースコードは以下のような感じです。
> (省略)
>  model.generateが完了するまで出力を一切見られないのですが、ChatGPTのように出力が確定し次第表示されるようにすることは可能でしょうか...?

> ChatGPT先生
> 
> `transformers`ライブラリの現在のバージョンでは、出力を生成しながらそれを表示する機能は直接サポートしていません。`model.generate()`関数が出力を生成し終えるまで、その結果は利用できません。しかし、ある程度出力が確定したタイミングで表示するような機能を実装することは可能です。
> 
>この問題に対する一つのアプローチは、出力を一度に生成するのではなく、一文字または一単語ずつ生成することです。これにより、各ステップで新しく生成された単語を表示することが可能になります。ただし、この方法は各ステップでの推論に対するオーバーヘッドがあるため、全体の生成時間が長くなる可能性があります。
> 
> 以下に、単語ごとに生成して出力する例を示します：

```python:python
def generate_and_print(model, tokenizer, token_ids, max_length):
    for _ in range(max_length):
        with torch.no_grad():
            output = model(token_ids.to(model.device))
        logits = output.logits
        next_token_id = torch.argmax(logits[..., -1, :], dim=-1).unsqueeze(-1)
        token_ids = torch.cat((token_ids, next_token_id), dim=-1)
        print(tokenizer.decode(next_token_id[0]), end='', flush=True)
    print()

# 例えばこんな風に使う
prompt = (
    make_prompt(log)
    + "<NL>"
    + "システム: "
)

token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
generate_and_print(model, tokenizer, token_ids, 128)
```

> ChatGPT先生
> 
> ここでのキーは`flush=True`オプションを使用することで、出力が即座に表示されるようになります。なお、この例では最も確率が高い単語（`argmax`によって選ばれる）を選択していますが、より高度なサンプリング戦略（たとえばトップ-k サンプリング）を適用することも可能です。ただし、これらの高度なサンプリング戦略を適用すると、コードはより複雑になります。

(中略)

> 筆者
> 
> いただいたプログラムを加工して実行したところ返答内で同じ表現を何度も繰り返す現象が見られました。これを改善するにはどうすれば良いでしょうか...?
> 
> 以下現在のプログラムです。
> (省略)

> ChatGPT先生
> 
> 生成されるテキストに同じフレーズや単語が何度も繰り返される現象は、言語モデルの生成においてよく見られる問題です。これは特に一般的な問題であり、あるトークンが生成されると、そのトークンが再び生成される確率が一時的に高まることが一因として考えられます。
> 
> 解決策の一つとして、生成時にサンプリングの方法を変更することが挙げられます。最も単純な生成方法は、各ステップで最も高い確率を持つトークンを選択する方法です（これを「greedy decoding」や「argmax decoding」と呼びます）。しかし、これは多様性に欠ける生成結果を引き起こし、上述の繰り返し問題を引き起こす可能性があります。
> 
> 一方、いくつかの高度なサンプリング技術が提案されています。その中でもよく使用されるものに「トップ-k サンプリング」や「トップ-p サンプリング（または「ニュークリーサンプリング」）」があります。これらのテクニックは、適度なランダム性を組み込むことで生成結果の多様性を保ちつつ、一貫性を維持します。
> 
> transformersライブラリでは、これらのサンプリング手法を簡単に適用することができます。以下に、トップ-k サンプリングを適用した例を示します：

```python:python
def generate_and_print(model, tokenizer, token_ids, max_length):
    res = ""

    for _ in range(max_length):
        with torch.no_grad():
            # get the model outputs
            outputs = model(token_ids.to(model.device))

        # get the logits from the model outputs
        logits = outputs.logits

        # Apply top-k sampling
        indices_to_remove = logits < torch.topk(logits, k)[0][..., -1, None]
        logits[indices_to_remove] = float('-inf')

        # Sample from the logits
        probs = torch.nn.functional.softmax(logits[..., -1, :], dim=-1)
        next_token_id = torch.multinomial(probs, num_samples=1)

        # concatenate the new token ID to the existing sequence
        token_ids = torch.cat((token_ids, next_token_id), dim=-1)

        output_str = tokenizer.decode(next_token_id[0])

        res += output_str

        # decode the new token ID and print it
        print(output_str.replace("<NL>", "\n"), end='', flush=True)

        if "
" in output_str:
            break
    print()

    return res
```

> 上記の例では、`k`を適切な数値（例えば40や50）に設定することで、各ステップで考慮するトークンの数を制限します。また、`torch.multinomial`を使用して、確率分布からランダムにトークンをサンプリングします。
</details>

...ぶっちゃけ今でもどうしてちょっとずつ生成できているのかちゃんとは説明できないです(笑)

本記事やLLMでわからない事があったらChatGPT先生に聞いちゃりましょう！<font color="#bbbbbb">上司には内緒です</font>

# PyInstallerで実行ファイル化してみる

今回、TauriにPythonアプリを内包して配布したいと考えたわけですが、Pythonランタイムの配布が難しくとても悩みました。

そんな折、[PyInstaller](https://pyinstaller.org/en/stable/)を使うとスタンドアロンな実行ファイルにできることを知りました。実行ファイルならば、Tauri(Rust)からは子プロセスとして呼び出して標準入出力を介してやり取りすることができます！

ただ一筋縄ではいかないです...**PyInstallerでは隠しインポート？を明示しなければならない**らしく、Pythonで動いたからといって、脳死でコマンドを打てば実行ファイルができるわけじゃないみたいです。**隠しインポートって何**

参考:

- [PyInstallerによるPythonスクリプトのexe化とエラー対処方法まとめ - Qiita](https://qiita.com/kunishou/items/b2e8754886107292926d)

https://qiita.com/kunishou/items/b2e8754886107292926d

途方にくれて検索しまくっていたら自己解決している謎のStack Overflow投稿があったので縋ってみたところ、上手くいったのでこれをそのまま使うことにしました。<font color="#bbbbbb">多分冗長なものがありそうだけど調べてない</font>

`sentencepiece`周りでエラーが出たのでその分だけ追記しています。

- [python - PackageNotFound error while executing .exe file made by PyInstaller - Stack Overflow](https://stackoverflow.com/questions/71863714/packagenotfound-error-while-executing-exe-file-made-by-pyinstaller)

https://stackoverflow.com/questions/71863714/packagenotfound-error-while-executing-exe-file-made-by-pyinstaller

```PowerShell:PowerShell
pyinstaller --onefile --hidden-import=pytorch '
--collect-data torch `
--copy-metadata torch `
--copy-metadata tqdm `
--copy-metadata regex `
--copy-metadata requests `
--copy-metadata packaging `
--copy-metadata filelock `
--copy-metadata numpy `
--copy-metadata tokenizers `
--copy-metadata sentencepiece `
--hidden-import="sklearn.utils._cython_blas" `
--hidden-import="sklearn.neighbors.typedefs" `
--hidden-import="sklearn.neighbors.quad_tree" `
--hidden-import="sklearn.tree" `
--hidden-import="sklearn.tree._utils" `
--hidden-import="sentencepiece" `
interactive.py
```

PyInstallerはまだ入れていなかったのでpipenvで入れ、上記コマンドを実行すれば無事動く実行ファイルが作れます。

```PowerShell:PowerShell
pipenv install pyinstaller
pipenv run pyinstaller --onefile --hidden-import=pytorch --collect-data torch --copy-metadata torch --copy-metadata tqdm --copy-metadata regex --copy-metadata requests --copy-metadata packaging --copy-metadata filelock --copy-metadata numpy --copy-metadata tokenizers --copy-metadata sentencepiece --hidden-import="sklearn.utils._cython_blas" --hidden-import="sklearn.neighbors.typedefs" --hidden-import="sklearn.neighbors.quad_tree" --hidden-import="sklearn.tree" --hidden-import="sklearn.tree._utils" --hidden-import="sentencepiece" interactive.py
```

結局オプションの指定方法がよく分からずもにょる...何はともあれ、これで`dist`ディレクトリに`interactive.exe`という実行ファイルができ、Pythonランタイムなしで実行できるようになりました！

```plaintext:PowerShell
cd dist
.\interactive.exe
> 日本の首都は？
日本の首都は東京です。

> [exit]
```

# Tauriに埋め込んで配布

実行ファイルをTauriのバックエンド役を担うRustから呼び出す話も本当は本記事に書きたかったのですが、長くなりすぎるため別記事とさせていただきます！

2023/06/27 追記

投稿いたしました！ご覧いただけると幸いです[^alltitle]！

[^alltitle]: とてもどうでも良いですが1つの記事としてまとめる場合元々考えていたタイトル案は自分の記事シリーズの命名規則に則った「上司からオンプレでチャットAIを作るように指示された話またはRust on Tauriから実行ファイルを呼び出す方法的な何か」でした。長すぎ！！

- [【全CLIアプリGUI化計画】TauriでTypeScriptやRustからCLIアプリを呼び出す - Qiita](https://qiita.com/namn1125/items/36482917d9c7a8adb658)

https://qiita.com/namn1125/items/36482917d9c7a8adb658

# まとめ・所感

LLM入門は案外簡単だということを本記事で示せたでしょうか...？

Pythonで簡単にLLMに触れられたことにとても感動しつつも、変なこと(例えば実行ファイル化)をしようとするとなかなか情報がなくて辛かったです。<font color="#bbbbbb">まぁPythonに限った話ではないかもですが...</font>

最近[Mojo](https://zenn.dev/helloyuki/scraps/70e4bcdef0bc6f)みたいなAlt-Python言語がしばしば注目されますが、Python特有の問題に当たったりすると、やはり別な選択肢も欲しいなと感じます。

今回LLMに入門して、改めて機械学習や深層学習の勉強意欲が湧いてきました！ただ、冒頭みたいに業務として降ってくるのはできれば避けたいです(苦笑)

ここまで読んでいただきありがとうございました...！

# 参考

- [サイバーエージェントLLMとは？概要〜導入〜所感までを徹底解説！ | 株式会社SaaSis](https://saasis.jp/2023/05/30/%E3%82%B5%E3%82%A4%E3%83%90%E3%83%BC%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88llm%E3%81%A8%E3%81%AF%EF%BC%9F%E6%A6%82%E8%A6%81%E3%80%9C%E5%B0%8E%E5%85%A5%E3%80%9C%E6%89%80%E6%84%9F/)
    - OpenCALMをGoogle Colaboratoryで動かしています。
- [ローカルで動く大規模言語モデル(Rinna-3.6B)を使ってあなただけのAIパートナーを作ろう - Qiita](https://qiita.com/takaaki_inada/items/9a9c07e85e46ec0e872e)
    - VRAM情報はこちらの記事様を根拠にさせていただきました。
- [pyenvとpipenvで、快適Python生活（Windows向け）](https://zenn.dev/sql_geinin/articles/29f2b0a5c55db2)
    - pyenvとpipenvに関して参考にさせていただきました。