独自のgpt AIチャットボットを訓練と作成（自分の88本のQiita記事を使って、チャットAIを訓練してみた）

この記事では、独自のGPTチャットAIをゼロからトレーニングするプロセスについて説明します。

:::note warn
- 注：この記事の焦点は、既存のAIモジュールを微調整(Fine-Tuning)することではなく、あなたの入力テキストから会話スキルを学習するAIを訓練することです。
- OpenAIは多くのリソースと新しい技術を投入しているため、ChatGPTに近いものを訓練することを期待すべきではありません。（笑
:::

十分なデータ（例：Wikipediaのすべてのコンテンツ）があれば、GPT-2に似たモデルをトレーニングすることが可能です。

https://openai.com/research/better-language-models

本記事では、日本語で書かれた **自分の88本のQiita記事** をデータソースとしてAIをトレーニングします（笑
このデータセットは非常に限られており、結果は最適ではありませんのでご注意ください。

この記事では、AIに関する経験がなくても手順に従って進められるように、段階的に説明します。

## 最終結果

### 88のQiita記事だけを使って **1万回(Mac miniで約1時間）** のトレーニングを繰り返した後：

![スクリーンショット 2023-04-15 13.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/74dbbd9b-2936-2e81-6bab-f908ff016a4d.png)

### 200回の繰り返し学習しかしていない最初のうちは、正しい文章を作ることはできません：
しかし、使っている言葉は、私がQiitaで公開した88本の記事の中から選んだもので、それしか知識がありません。

![スクリーンショット 2023-04-15 13.06.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/57dbf5eb-7f68-6213-ca40-cbbc803c53c8.png)

### これが2000回のトレーニング後の結果です：

![スクリーンショット 2023-04-15 13.07.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/638b51c1-b8d8-5275-4a5a-5182e2919faa.png)

### 約8000回のトレーニングの後

![スクリーンショット 2023-04-15 13.05.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/b2925d5f-650c-0e4e-6b9b-cbe4439b0940.png)

より多くの回数を訓練することで、より正確な結果を得ることができます。
ただし、Qiitaの記事88件しか提供していないので（笑
（GPT-2はWebTextという約45テラバイトのテキストデータを含む大規模データセットで学習させた）

![スクリーンショット 2023-04-15 13.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/74dbbd9b-2936-2e81-6bab-f908ff016a4d.png)

この記事では、そんなAIを一からトレーニングする方法を紹介します。

## 環境設定

`nanoGPT` を使用して、AIチャットモジュールの学習を行うことになります。 

https://github.com/karpathy/nanoGPT

リポジトリには、シェイクスピアのようなトレーニング用英語モジュールのコードも含まれています。
https://github.com/karpathy/nanoGPT/tree/master/data/shakespeare

日本語のテキスト入力を受けて日本語を出力するようにカスタマイズし（トークナイザーを置き換える）、
サンプリング機能（質問して回答を得る機能）も変更して、いつでも回答を入力できるようにします（上のスクリーンショットにあるような）。

以下のコマンドはMacOSで実行されていますが、他のシステムでも動作するはずです。

### nanoGPTのリポジトリをクローン

```bash
git clone https://github.com/karpathy/nanoGPT
```

MacOSをお使いの場合は、HomeBrewをインストールして： 

https://brew.sh/index_ja

この記事で使用するすべての必要なライブラリをインストールするコードは次のとおりです：

```bash
python3 -m ensurepip

pip3 install transformers datasets tiktoken tqdm wandb numpy fugashi ipadic

pip3 install \   
  --pre torch torchvision torchaudio \
  --extra-index-url https://download.pytorch.org/whl/nightly/cpu

brew install mecab mecab-ipadic
```

## トークナイザ

### トークナイザとは何ですか？

AIモジュールは、トークンによって言語を理解します。テキストを入力すると、トークナイザを使用してこれらをトークンに変換します。AIが出力を生成するとき、トークンが生成されます。

そのトークンを、（モジュールのトレーニング時に使用した）同じトークナイザを使ってテキストに戻す必要があります。

![スクリーンショット 2023-04-15 15.42.58-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/4276a5ce-f40b-f8ae-64a4-81945f7866be.png)

デフォルトで `nanoGPT` が使用するトークナイザは `gpt-2` で、日本語についての理解はそれほど高くありません。そこで、ここでは日本語用のオープンソースのトークナイザを使用します：

https://huggingface.co/cl-tohoku/bert-base-japanese-whole-word-masking

### Pythonで上記のトークナイザを使う

Pythonで上記のトークナイザをすぐに使うことができます。例えば、以下のコードを.pyファイルに入れます。

```python
def read_markdown_file(file_path):
    if file_path.endswith(".DS_Store"):
        return ""
    with open(file_path, mode="r", encoding="utf-8") as md_file:
        print("Reading", file_path)
        content = md_file.read()
    return content

datasetFolderPath = "/Users/office/Desktop/MyOwnGPT/dataset/"
dataSetFileNames = os.listdir(datasetFolderPath)

entries = ""
for filename in dataSetFileNames:
    file_path = datasetFolderPath + filename
    print("Processing ", file_path)
    markdown_content = read_markdown_file(file_path)
    for line in markdown_content.split("\n"):
        if len(line) > 5:
            entries += line + "\n\n"

n = len(entries)
print("Total input entry length", n) 
```
これで、実行してトークナイズされた日本語テキストを確認できます。トークナイザモジュールは自動的にHugging faceからダウンロードされます。

![スクリーンショット 2023-04-15 15.49.04-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/22adecf9-1685-bed9-d26f-c8cecde39947.png)

また、トークナイズされたテキストを元の文字列に戻すこともできます：

```python
trainEntries = input("Enter the text to de-tokenize\n")

token_ids = [int(x) for x in trainEntries.strip("[]").split(", ")]
decoded_text = tokenizer.decode(token_ids)

print(decoded_text)
```

![スクリーンショット 2023-04-15 15.59.13-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/ce866635-252f-6d7b-4aa0-e01ba4b32f65.png)

トークナイザはテキストをいくつかのコンポーネントに分割しています。例えば、「個人開発者」は「個人」、「開発」、「者」となります。

## トレーニングデータを準備する

私のトレーニングデータセット（笑）

![スクリーンショット 2023-04-15 17.12.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/538a0a1f-c110-3938-31a3-5a8cbae09d3d.png)

次に、すべてのトレーニングデータを1つのテキストファイルにまとめます。ここでは、私のQiitaの記事（88件）から.mdファイルをすべて変換するためにこのコードを使っています：

```python
def read_markdown_file(file_path):
    if file_path.endswith(".DS_Store"):
        return ""
    with open(file_path, mode="r", encoding="utf-8") as md_file:
        print("Reading", file_path)
        content = md_file.read()
    return content

datasetFolderPath = "/Users/office/Desktop/MyOwnGPT/dataset/"
dataSetFileNames = os.listdir(datasetFolderPath)

entries = ""
for filename in dataSetFileNames:
    file_path = datasetFolderPath + filename
    print("Processing ", file_path)
    markdown_content = read_markdown_file(file_path)
    for line in markdown_content.split("\n"):
        if len(line) > 5:
            entries += line + "\n\n"

n = len(entries)
print("Total input entry length", n) 
```

さて、そのデータファイルの中身は、次のようなものになります：

```
また、App Clipのプログラムのファイルサイズは10MB未満でなければなりません。

## ステップ 1. App Clipのターゲットを作成する

トップにあるファイル (File) メニューをクリック、次にニュー (New) オプション、ターゲット (Target) をクリックして下さい。

App Clipを作成するには、ただ新しい  のターゲットをアプリに追加するだけです。

...
```

## トレーニングセットとテスト（試験）セットを用意する

ここで、上記のデータを2つのセットに分け、
トレーニングセットはAIモジュールの学習に使用し、
テストセットはモジュールがどのように動作するかをテストするためのトレーニングプログラムに使用することにします：

```python
trainEntries = entries[:int(n*0.9)]
evalEntries = entries[int(n*0.9):]

print("Train entries:", trainEntries, " eval entries ", evalEntries)
```

ここでは、上記のテキストの90%をトレーニングに、10%をテストに使っています。

## 入力テキストをトークンに変換

ここで、AIモジュールの学習プログラムが理解できるように、すべての入力をトークンに変換します。上記の日本語トークナイザーを利用することができます：

```python
max_length = 512  # Maximum sequence length for BERT
tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking', max_len=max_length)

trainTokens = []
train_text_segments = [trainEntries[i:i+max_length] for i in range(0, len(trainEntries), max_length)]
for segment in train_text_segments:
    train_segment_tokens = tokenizer.encode(segment, add_special_tokens=True)
    trainTokens.extend(train_segment_tokens)

evalTokens = []
eval_text_segments = [evalEntries[i:i+max_length] for i in range(0, len(evalEntries), max_length)]
for segment in eval_text_segments:
    eval_segment_tokens = tokenizer.encode(segment, add_special_tokens=True)
    evalTokens.extend(eval_segment_tokens)

print(len(trainTokens), "used for training;", len(evalTokens), "used for eval")
```

これで、学習用のトークンがすべて入った `trainTokens` 変数と、
テストに使うトークンが入った `evalTokens` 変数が用意されました。

英語の入力しかない場合は、gpt-2トークナイザーを使用することができます：

```python
enc = tiktoken.get_encoding("gpt2")
train_ids = enc.encode_ordinary(train_data)
eval_ids = enc.encode_ordinary(val_data)
```

## トークンをファイルに保存

さて、トークンをファイルに書き込んでいきます：

```python
train_ids = np.array(trainTokens)
val_ids = np.array(evalTokens)
newFolderPath = os.path.join(os.path.dirname(__file__), 'TrainingSet')
if not os.path.exists(newFolderPath):
    os.makedirs(newFolderPath)
train_ids.tofile(os.path.join(newFolderPath, 'train.bin'))
val_ids.tofile(os.path.join(newFolderPath, 'val.bin'))
```

これにより、学習データ用の `train.bin` ファイルとテスト用の `val.bin` ファイルが作成されます。これらのファイルは、ディレクトリ内の `TrainingSet` フォルダに作成されます。

トレーニングの開始

ここで、新しく生成された `TrainingSet` を
クローンした `nanoGPT` リポジトリフォルダのdataフォルダに
コピーします。

![スクリーンショット 2023-04-15 16.09.33-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1cca45d0-d457-e486-59d0-8b1f7bf22f90.png)

次に、`nanoGPT` リポジトリにある `train.py` 関数を使って、モジュールをトレーニングします。
`eval_interval`を100に設定していることに注意してください。つまり、100回繰り返すごとに、`out`フォルダに学習済みモジュールが取得され、すぐにテスト（入力からの応答取得）に使用することができます。

```bash
python3 train.py \
  --dataset=TrainingSet \
  --n_layer=4 \
  --n_head=4 \
  --n_embd=64 \
  --compile=False \
  --eval_iters=1 \
  --block_size=64 \
  --batch_size=8 \
  --device=mps \
  --eval_interval=100
```

では、学習処理を開始します。

繰り返し回数が100回に達するたびに、`out` フォルダに出力モジュールファイルが表示されます：

![スクリーンショット 2023-04-15 16.10.24-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/34398339-dc10-ab24-9398-6f9d1e2f02f7.png)

このファイルをテストするために、トレーニングを停止する必要はありません。トレーニングのプロセスを停止したい場合は、キーボードのcontrol-cキーで終了してください。

注意：正しい文章を出力できるモジュールを作るには、20000回以上の繰り返しが必要です。

## sample.pyを日本語トークナイザーを使うように

今回はカスタムのトークナイザーを使用したので、クローンしたnanoGPTフォルダ内のデフォルトのsample.pyファイルを更新する必要があります：

```python
import os
from contextlib import nullcontext
import torch
from transformers import BertJapaneseTokenizer
from model import GPTConfig, GPT
import random

# -----------------------------------------------------------------------------
init_from = 'resume' # either 'resume' (from an out_dir) or a gpt2 variant (e.g. 'gpt2-xl')
out_dir = 'out' # ignored if init_from is not 'resume'
num_samples = 1 # number of samples to draw
max_new_tokens = 500 # number of tokens generated in each sample
temperature = 0.05 # 1.0 = no change, < 1.0 = less random, > 1.0 = more random, in predictions
top_k = 100 # retain only the top_k most likely tokens, clamp others to have 0 probability
seed = random.randint(0, 2**32 - 1)
device = 'cpu' # examples: 'cpu', 'cuda', 'cuda:0', 'cuda:1', etc.
dtype = 'bfloat16' # 'float32' or 'bfloat16' or 'float16'
compile = False # use PyTorch 2.0 to compile the model to be faster
exec(open('configurator.py').read()) # overrides from command line or config file
# -----------------------------------------------------------------------------

torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cuda.matmul.allow_tf32 = True # allow tf32 on matmul
torch.backends.cudnn.allow_tf32 = True # allow tf32 on cudnn
device_type = 'cuda' if 'cuda' in device else 'cpu' # for later use in torch.autocast
ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)

# model
if init_from == 'resume':
    # init from a model saved in a specific directory
    ckpt_path = os.path.join(out_dir, 'ckpt.pt')
    checkpoint = torch.load(ckpt_path, map_location=device)
    gptconf = GPTConfig(**checkpoint['model_args'])
    model = GPT(gptconf)
    state_dict = checkpoint['model']
    unwanted_prefix = '_orig_mod.'
    for k,v in list(state_dict.items()):
        if k.startswith(unwanted_prefix):
            state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
    model.load_state_dict(state_dict)
elif init_from.startswith('gpt2'):
    # init from a given GPT-2 model
    model = GPT.from_pretrained(init_from, dict(dropout=0.0))

model.eval()
model.to(device)
if compile:
    model = torch.compile(model) # requires PyTorch 2.0 (optional)

# load custom tokenizer
tokenizer = BertJapaneseTokenizer.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")

# encode the beginning of the prompt
while(True):
    start = input("What do you want to ask?")
    start_ids = tokenizer.encode(start)
    x = (torch.tensor(start_ids, dtype=torch.long, device=device)[None, ...])
# run generation
    with torch.no_grad():
        with ctx:
            y = model.generate(x, max_new_tokens, temperature=temperature, top_k=top_k)
            decoded_text = tokenizer.decode(y[0].tolist(), skip_special_tokens=True)
            print(decoded_text)
            print('---------------')  
```

ここでは、`BertJapaneseTokenizer` を使って、日本語の出力をデトークしています。
また、次の入力を求め続けるループを追加しています。

さて、テストしてみましょう：

![スクリーンショット 2023-04-15 13.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/74dbbd9b-2936-2e81-6bab-f908ff016a4d.png)

質問したい文章（日本語）を入力し、キーボードのエンターキーを押す。

終了したいときは、control-cキーの組み合わせで終了します。

## より多くのデータ、より多くの反復練習

より多くのデータ入力（例えば、https://github.com/karpathy/nanoGPT/tree/master/data/openwebtext）でAIを訓練すれば、gpt-2論文に記載されたようなチャットロボットを得ることができます。

もちろん、より多くの反復のためのモジュールの訓練も必要です。

私の論文では、私のQiitaの記事88個と1時間程度のトレーニングを使いました（なので、とてもとても弱いです、笑）。

## Githubリポジトリ

https://github.com/mszpro/MyOwnGPT

:::note
私は、質問をしたり、ただランダムにチャットしたりするのにChatGPTを使うのが好きです。Siriを使ってGPT-3に質問をしたり、音声チャットを使えるiOSアプリを作りました。是非、以下のリンクをチェックしてください：
https://twitter.com/MszPro/status/1640874358301409281
:::

---

お読みいただき、ありがとうございます。

MastodonやMissKeyでフォローしてください：🐘 マストドン @me@mszpro.com

Twitterでフォローしてください：☺️ Twitter @MszPro

☺️ サイト https://MszPro.com

![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)

Written by MszPro\~