【Python】MacBookで日本語のローカルLLMを動かす

## はじめに

最近、LLM（大規模言語モデル）が注目を集めています。LLMは、文章を生成したり、質問に答えたりすることができる強力なAIのことです。たとえば、OpenAIのGPTシリーズやMetaのLlamaシリーズなどがあります。これらのモデルを使うには、APIを介してアクセスする方法が一般的です。しかし、APIには使用料がかかったり、LLMの中身についてほとんど学ぶことができません。

そこで、今回は**ローカル環境**（自分のPCで直接動かす方法）で、日本語LLMを動かす方法を紹介します。ローカルで動かす事で、API使用料をかけずに、無料で動かしてLLMの知識を身につけることができます。

本記事では、Pythonを使用し、M1 Macの中で日本語LLMを動かす方法を解説します。

## LLMの種類と特徴

日本語を使うLLMには、さまざまな種類があります。MacBookで動かせる可能性があるモデルとして、いくつかピックアップして特徴を説明します。以下は代表的なモデルと、その特徴や必要なメモリ量をまとめた表です。

| モデル名                          | 特徴                                      | 必要メモリ        |
|-----------------------------------|-----------------------------------------|-------------------|
| **rinna/japanese-gpt2-medium**    | GPT-2ベースで軽量で高速                   | 4GB〜6GB          |
| **elyza/ELYZA-japanese-Llama-2-7b** | Llama-2ベース、大規模モデルで高精度       | 16GB〜32GB        |
| **Japanese-BERT**                 | BERTベース、文脈の理解が得意             | 8GB〜16GB         |

- **軽量なモデル**（例: `rinna/japanese-gpt2-medium`）は、メモリが少なくても動作しますが、学習しているデータ量が少ないため精度が低くなります。
- **大規模なモデル**（例: `elyza/ELYZA-japanese-Llama-2-7b`）は、精度が高いですが、動作には大量のメモリが必要です。

自分のPCの性能に合わせて、使いたいモデルを選ぶことが大切です。

## 実装の準備

### 1. Pythonライブラリのインストール

以下のライブラリをインストールします。

```bash
pip install torch transformers airllm mlx
```

- **torch**: 機械学習を簡単に実装できるフレームワーク
- **transformers**: Hugging Face 社が公開する、最先端のNLPモデルの実装と事前学習済みモデルを提供するライブラリ
- **airllm**: メモリが少なくても大規模なLLMを実行可能にするライブラリ
- **mlx**: Apple製品に特化し機械学習を高速で処理できるライブラリ

### 2. デバイス設定

MacBookにM1-M4チップが搭載されている場合、GPU（グラフィック処理装置）を使うことで計算を速くできます。この設定を以下のコードで行います。
※後ほど、ソースコードに記述します。

```python
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
```

- **MPS**: MacBookのM1/M2チップに最適化されたGPU（Metalと呼ぶ）を使う設定
- **CUDA**: NVIDIAのGPUを使う設定
- **CPU**: 上記のどちらも使えない場合は、CPUで処理する設定

これで、PCの性能に応じた最適なデバイスを自動で選んでくれます。

## 実装

### 軽量なモデルを使った実装

まずは、軽量なモデル（`rinna/japanese-gpt2-medium`）を使って、日本語のテキストを生成するプログラムを紹介します。

```python
# 必要なライブラリをインポート
import torch
from transformers import AutoTokenizer, GPT2LMHeadModel

# 使用するモデル名を指定（日本語GPT-2モデル）
model_name = "rinna/japanese-gpt2-medium"

# モデルに対応するトークナイザーとモデルをロード
# AutoTokenizerは、指定されたモデルに適したトークナイザーを自動でロードします
tokenizer = AutoTokenizer.from_pretrained(model_name, legacy=False)

# GPT-2の事前学習済みモデルをロード
model = GPT2LMHeadModel.from_pretrained(model_name)

# デバイス設定: MPS（Apple M1/M2チップのGPU）を優先し、次にCUDA（NVIDIA GPU）、最後にCPUを使用
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
# モデルを選択したデバイス（MPS, CUDA, CPU）に移動
model.to(device)

# テキスト生成を行う関数
def generate_text(prompt, max_length=200):
    # プロンプトをトークン化（文字列を数値のトークンに変換）
    input_ids = tokenizer.encode(prompt, return_tensors='pt').to(device)
    
    # Attention Maskを設定（モデルが無視すべき部分を示すマスク）
    attention_mask = torch.ones(input_ids.shape, device=device)
    
    # モデルでテキストを生成
    with torch.no_grad():  # 推論時に勾配計算を行わない（メモリ節約）
        output = model.generate(input_ids, max_length=max_length, num_return_sequences=1,  # 生成するテキストの長さ
            no_repeat_ngram_size=3,  # 同じ3つの単語が連続しないように制限
            temperature=0.7,  # 生成の多様性を制御するパラメータ（高いほど多様性が増す）
            do_sample=True,  # サンプリングを行う（確率的に次の単語を選ぶ）
            attention_mask=attention_mask,  # Attention Maskの指定
            top_k=50,  # 上位50個の候補からサンプリング
            top_p=0.9,  # 累積確率が90%になる候補からサンプリング
            eos_token_id=tokenizer.eos_token_id)  # 終了トークンの指定（文章生成終了）
    
    # 生成されたトークンをデコードして、文字列に変換
    generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
    
    # 生成されたテキストを返す
    return generated_text

# ユーザーからプロンプトを入力してもらう
prompt = input("テキスト生成のプロンプトを入力してください: ")

# 入力されたプロンプトでテキストを生成
generated_text = generate_text(prompt)

# 生成されたテキストを、句点（。）で分割し、1行ずつ表示
for line in generated_text.split("。"):  # 句点（。）でテキストを分割
    print(line.strip())  # 前後の空白を削除して表示
```

---

### コード解説

#### 1. **ライブラリのインポート**
```python
import torch
from transformers import AutoTokenizer, GPT2LMHeadModel
```
- **`torch`**: PyTorchは、深層学習フレームワークの一つで、ニューラルネットワークの構築、学習、推論を行います。このコードでは、モデルを指定したデバイスに転送するために使用されます。
- **`AutoTokenizer`**: Hugging Faceの`transformers`ライブラリの一部で、指定したモデルに適したトークナイザー（文字列をトークン（数字に変換された単語）に変換するツール）を自動的にロードします。
- **`GPT2LMHeadModel`**: これはGPT-2（Generative Pretrained Transformer 2）モデルの一部で、生成モデル（テキスト生成）用のクラスです。これを使用して、入力プロンプトに基づいてテキストを生成します。

---

#### 2. **モデルの指定**
```python
model_name = "rinna/japanese-gpt2-medium"
```
- **`model_name`**: 使用するモデルの名前です。このコードでは、`rinna/japanese-gpt2-medium`という日本語対応のGPT-2モデルを使用しています。Hugging Faceのモデルリポジトリから取得します。

---

#### 3. **トークナイザーとモデルのロード**
```python
tokenizer = AutoTokenizer.from_pretrained(model_name, legacy=False)
model = GPT2LMHeadModel.from_pretrained(model_name)
```
- **`AutoTokenizer.from_pretrained(model_name)`**: この行で指定されたモデルに対応するトークナイザーをロードします。トークナイザーは、文字列を数値のトークン（モデルが理解できる形式）に変換します。`legacy=False`は、最新のトークナイザーを使うオプションです。
- **`GPT2LMHeadModel.from_pretrained(model_name)`**: 事前学習済みのGPT-2モデルを指定した名前（`model_name`）でロードします。これにより、指定された日本語GPT-2モデルが使用可能になります。

---

#### 4. **デバイス設定**
```python
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
```
- **`torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")`**:
    - PyTorchがサポートしているデバイスを選択します。AppleのM1/M2チップの場合は、MPS（Metal Performance Shaders）を利用してGPUを活用できます。次に、NVIDIA GPU（CUDA）があればそれを使い、GPUが利用できない場合はCPUを使用します。
- **`model.to(device)`**: モデルを選択したデバイス（MPS, CUDA, または CPU）に移動します。これにより、推論が最適なデバイスで実行されます。

---

#### 5. **テキスト生成関数**
```python
def generate_text(prompt, max_length=200):
```
- この関数は、指定されたプロンプトに基づいてテキストを生成する役割を持っています。引数`prompt`は生成の出発点となるテキストです。`max_length`は生成される最大のテキスト長を指定します。

```python
input_ids = tokenizer.encode(prompt, return_tensors='pt').to(device)
```
- **`tokenizer.encode(prompt, return_tensors='pt')`**: プロンプトをトークン化し、モデルが処理できる形式（PyTorchのテンソル）に変換します。`to(device)`で、デバイス（GPU/CPU）に転送します。

```python
attention_mask = torch.ones(input_ids.shape, device=device)
```
- **`attention_mask`**: Attention Maskは、入力の各トークンが有効かどうかを示します。ここではすべてのトークンに対して有効（1）を設定しています。

```python
with torch.no_grad():
    output = model.generate(input_ids, max_length=max_length, num_return_sequences=1, no_repeat_ngram_size=3, temperature=0.7, do_sample=True, attention_mask=attention_mask, top_k=50, top_p=0.9, eos_token_id=tokenizer.eos_token_id)
```
- **`with torch.no_grad()`**: 推論時に勾配を計算しないようにします。これにより、メモリ使用量を節約できます。
- **`model.generate()`**: ここでテキストを生成します。`max_length`は生成するテキストの長さを、`num_return_sequences`は生成する候補数を指定します。他のパラメータ（`temperature`, `top_k`, `top_p`）は生成されたテキストの多様性を制御します。

```python
generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
```
- **`tokenizer.decode()`**: 生成されたトークン（数値）を、元のテキストに戻します。`skip_special_tokens=True`は、特殊なトークン（例えば、`<pad>`など）を無視してデコードします。

---

#### 6. **ユーザーからの入力とテキスト生成**
```python
prompt = input("テキスト生成のプロンプトを入力

してください: ")
generated_text = generate_text(prompt)
```
- ユーザーからプロンプトを受け取り、そのプロンプトに基づいてテキストを生成します。

---

#### 7. **生成されたテキストの表示**
```python
for line in generated_text.split("。"):
    print(line.strip())
```
- 生成されたテキストを句点（`。`）で分割し、各文を1行ずつ表示します。`strip()`は、前後の余分な空白を削除するために使われます。

---

- このコードは、指定された日本語のGPT-2モデルを使用して、ユーザーから与えられたプロンプトに基づいてテキストを生成します。生成プロセスでは、トークナイザーを使ってテキストをトークン化し、モデルがそのトークンを使って予測を行います。生成されたトークンは再び文字列に変換され、ユーザーに結果が表示されます。









### 大容量モデルを使った実装

次に、大きなモデル（`elyza/ELYZA-japanese-Llama-2-7b-fast-instruct`）を使って、より高精度な日本語テキストを生成するプログラムを紹介します。

```python
# 必要なライブラリをインポート
from airllm import AutoModel
import mlx.core as mx

# モデルをロード
model_name = "elyza/ELYZA-japanese-Llama-2-7b-fast-instruct"  # 使用するモデルの名前
model = AutoModel.from_pretrained(model_name)  # モデルをロード

# プロンプト作成の準備
B_INST, E_INST = "
", "
"  # プロンプトを囲むタグ（インストラクションの開始と終了）
B_SYS, E_SYS = "
\n", "\n
\n\n"  # システムメッセージを囲むタグ
DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"  # システムの指示文（モデルに与える情報）
text = "富士山の標高を教えてください"  # ユーザーからの質問（プロンプト）

# プロンプトの作成
# プロンプトは、システムメッセージとユーザーの質問を含んだ形でフォーマットする
prompt = "{bos_token}{b_inst} {system}{prompt} {e_inst}".format(
    bos_token=model.tokenizer.bos_token,  # モデルのBOS（文の始まり）トークンを取得
    b_inst=B_INST,  # インストラクション開始タグ
    system=f"{B_SYS}{DEFAULT_SYSTEM_PROMPT}{E_SYS}",  # システムメッセージ
    prompt=text,  # ユーザーからの入力
    e_inst=E_INST,  # インストラクション終了タグ
)

# プロンプトをトークン化
# トークン化とは、文字列をモデルが理解できる数値の列に変換する処理です
input_ids = model.tokenizer(
    prompt,  # 作成したプロンプト
    return_tensors="np",  # トークン化した結果をNumPy配列で返す
    return_attention_mask=False,  # Attention maskを返さない
    truncation=True,  # 長すぎる場合は切り捨てる
    max_length=256,  # 最大のトークン長（256トークンを超えないようにする）
)

# モデルでテキスト生成
# モデルを使って、入力トークンに基づいたテキストを生成します
outputs = model.generate(
    mx.array(input_ids["input_ids"]),  # トークン化した入力をモデルに渡す
    max_new_tokens=50,  # 新たに生成するトークンの最大数（50トークン）
    use_cache=True,  # キャッシュを使う（推論速度を速くするため）
    return_dict_in_generate=True  # 結果を辞書形式で返す（生成結果の詳細も含める）
)

# 結果を表示
print(f"Model Output: {outputs}")
```

---

### コード解説

#### 1. **必要なライブラリのインポート**
   ```python
   from airllm import AutoModel
   import mlx.core as mx
   ```
   - `airllm`: 高度なLLMモデルを簡単にローカルで使用するためのライブラリです。
   - `mlx`: モデル推論を高速化するライブラリで生成処理を効率的に行うために使用します。

#### 2. **モデルのロード**
   ```python
   model_name = "elyza/ELYZA-japanese-Llama-2-7b-fast-instruct"
   model = AutoModel.from_pretrained(model_name)
   ```
   - `elyza/ELYZA-japanese-Llama-2-7b-fast-instruct`というLlama-2ベースの日本語LLMをロードしています。
   - `AutoModel.from_pretrained`は、指定したモデル名に基づいて、事前学習されたモデルを自動的にロードします。

#### 3. **プロンプト作成**
   ```python
   B_INST, E_INST = "
", "
"
   B_SYS, E_SYS = "
\n", "\n
\n\n"
   DEFAULT_SYSTEM_PROMPT = "あなたは誠実で優秀な日本人のアシスタントです。"
   text = "富士山の標高を教えてください"
   ```
   - プロンプトは、ユーザーからの入力を元に生成されますが、モデルに与える際には特定の形式（タグ）を使うことが多いです。
   - `B_INST`と`E_INST`は、指示の開始と終了を示すタグです。
   - `B_SYS`と`E_SYS`は、システムメッセージを囲むタグ。モデルに対して「あなたは優れた日本語アシスタントです」といった指示を与えるために使います。
   - `text`は、ユーザーが入力した質問（今回は「富士山の標高を教えてください」）

#### 4. **プロンプトのフォーマット**
   ```python
   prompt = "{bos_token}{b_inst} {system}{prompt} {e_inst}".format(
       bos_token=model.tokenizer.bos_token,
       b_inst=B_INST,
       system=f"{B_SYS}{DEFAULT_SYSTEM_PROMPT}{E_SYS}",
       prompt=text,
       e_inst=E_INST,
   )
   ```
   - プロンプトをモデルに渡すためには、特定の形式に整える必要があります。
   - `bos_token`は、文の開始を示すトークンで、モデルによって異なる場合があります（`model.tokenizer.bos_token`で取得）。
   - システムの指示（`DEFAULT_SYSTEM_PROMPT`）とユーザーの入力（`text`）を組み合わせ、インストラクションとしてまとめます。

#### 5. **トークン化**
   ```python
   input_ids = model.tokenizer(
       prompt,
       return_tensors="np",  # NumPy配列で返す
       return_attention_mask=False,
       truncation=True,  # 長い場合に切り捨て
       max_length=256,  # 最大256トークン
   )
   ```
   - `model.tokenizer`を使って、テキスト（`prompt`）をトークンに変換します。
   - `return_tensors="np"`で、トークン化された入力をNumPy配列形式で返します。
   - `max_length=256`で、入力が長すぎる場合に256トークンに切り捨てます。

#### 6. **テキスト生成**
   ```python
   outputs = model.generate(
       mx.array(input_ids["input_ids"]),
       max_new_tokens=50,  # 最大50トークンを生成
       use_cache=True,  # キャッシュを使用
       return_dict_in_generate=True
   )
   ```
   - `model.generate`で、トークン化した入力を元に新しいテキストを生成します。
   - `max_new_tokens=50`で、新たに生成するトークン数を最大50に設定します。
   - `use_cache=True`は、推論時にキャッシュを使って生成を速くするためのオプションです。
   - `return_dict_in_generate=True`で、生成されたテキストだけでなく、生成過程の詳細な情報も含めた辞書を返します。

#### 7. **結果の表示**
   ```python
   print(f"Model Output: {outputs}")
   ```
   - 最後に、生成されたテキスト（`outputs`）を表示します。`outputs`は辞書形式で、生成されたテキストやメタデータが含まれています。

---

このコードは、Llama-2ベースの日本語LLMを使用して、ユーザーが与えた質問に対してモデルが応答する仕組みを作っています。





## 終わりに

MacBookを使ってローカルで日本語LLMを動かす方法を学びました。APIを使う方法とは異なり、ローカルで実行することでコストを抑え、LLMの学習をすることができます。アプリ開発などへの利用も可能となります。今回紹介した軽量なモデルと大容量モデルの使い分けを参考に、用途に応じた最適なモデルを選んで試してみてください。