« ^ »

文章を自動生成する

所要時間: 約 8分

最近よくChatGPTが話題となっている。ChatGPTはOpenAIが開発している文章を生成するAIで、GPT-3.5の言語モデル上に構築されている。文章の自動生成に多少興味があった。もし文章の生成ができれば、記事やドキュメントを書く場合に助けになるし、入力サジェストの品質の向上、要約の作成など様々な場面で利用できる。ただ、なかなか腰が重くて手を付けていなかった。今回はそれを行う良い機会を得たので、文章の自動生成に挑戦してみる事にする。

また、ChatGPTが提供するAPIを使って、Emacsのdoctorを拡張した話は「Emacsの対話セラピー機能doctorをChatGPTに対応させる」に記載した。

GPT-2を用いて文章を生成する

文章の生成で参考になる記事がないかと探していた所、やってみようと思えるものがあった1。そこでここではGPT-3ではなく、GPT-2を用いて文章生成をするには、どのようにすればよいのか、またどのような文章が生成されるかを確認する。

GPT-2用の言語モデルはrinna社が公開しているものがあるので、それをそのまま使用する。通常こういった処理ではGPUを使用する。GPUが手元に無くてもGoogle Colaboratoryを使用すれば、 GPUを用いた計算をさせることができる。Mac Book ProでもIntel Iris Plus Graphicsといった内蔵GPUが搭載されており、これを用いて計算させるように設定することもできる23 。ただし今回は使い方だけを理解するという目的であるため、GPUは使用せずCPUで計算させることにした。

https://github.com/rinnakk/japanese-pretrained-models をcloneする。

git clone --depth 1 https://github.com/rinnakk/japanese-pretrained-models

必要なパッケージをインストールする。pytorchを使用していたため、今回はPython3.10系を使った。なおvenv等の説明は割愛する。

cd japanese-pretrained-models
pip install -r reqirements.txt

Pythonを起動し、インタラクティブシェルを用いて挙動を確認する。

>>> import torch
>>> from transformers import T5Tokenizer, AutoModelForCausalLM
2022-12-25 19:38:42.481522: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.

>>> tokenizer = T5Tokenizer.from_pretrained("rinna/japanese-gpt-1b")
Downloading: 100%|███████████████████████████████████████| 283/283 [00:00<00:00, 141kB/s]
>>> model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-1b")
Downloading: 100%|██████████████████████████████████| 2.66G/2.66G [05:27<00:00, 8.10MB/s]

macOSで実行しており、GPUを使用する設定をしていないため、CPUで計算を行う。

>>> prompt = "むかしむかしあるところにおじいさんとおばあさんがいました。おじいさんは"
>>> input_ids = tokenizer.encode(prompt, return_tensors="pt",add_special_tokens=False)
>>> num = 1
>>> with torch.no_grad():
    output = model.generate(
        input_ids,
        max_length=100,
        min_length=100,
        do_sample=True,
        top_k=500,
        top_p=0.95,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id,
        bad_word_ids=[[tokenizer.unk_token_id]],
        num_return_sequences=1
    )

... ... ... ... ... ... ... ... ... ... ... ... ... ... Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/Users/foo/.venv/py310/lib/python3.10/site-packages/torch/autograd/grad_mode.py", line 27, in decorate_context
    return func(*args, **kwargs)
  File "/Users/foo/.venv/py310/lib/python3.10/site-packages/transformers/generation/utils.py", line 1296, in generate
    self._validate_model_kwargs(model_kwargs.copy())
  File "/Users/foo/.venv/py310/lib/python3.10/site-packages/transformers/generation/utils.py", line 993, in _validate_model_kwargs
    raise ValueError(
ValueError: The following `model_kwargs` are not used by the model: ['bad_word_ids'] (note: typos in the generate arguments will also show up in this list)

bad_word_idsで問題が発生したため、この引数は一旦削除する。

>>> with torch.no_grad():
    output = model.generate(
        input_ids,
        max_length=100,
        min_length=100,
        do_sample=True,
        top_k=500,
        top_p=0.95,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id,
        num_return_sequences=1)

CPUで計算を行ったため、2〜3分待ったが処理が完了した。結果を表示する。

>>> decoded = tokenizer.batch_decode(output,skip_special_tokens=True)
>>> for i in range(num): print(decoded[i])
...
むかしむかしあるところにおじいさんとおばあさんがいました。おじいさんは山へ芝刈りに出かけ、おばあさんは畑へ行っていました。あるときおばあちゃんのお父さんは山へ芝刈りに出ましたが、おばあさんが畑を耕していると、奥の方からたくさんの蜂の大軍が飛んできて、次から次へと白い花と緑色の小さな花を木から落として行きました。山へ入ることも恐れ、しばらくもがいていると、小さな足が岩に登れそうなところを見つけました。おばあさんはおそるおそ

出来ている。他の文章を入力して結果を確認する。

def generate_sentence(text):
    input_ids = tokenizer.encode(text, return_tensors="pt",add_special_tokens=False)
    
    with torch.no_grad():
        return model.generate(
            input_ids,
            max_length=100,
            min_length=100,
            do_sample=True,
            top_k=500,
            top_p=0.95,
            pad_token_id=tokenizer.pad_token_id,
            bos_token_id=tokenizer.bos_token_id,
            eos_token_id=tokenizer.eos_token_id,
            num_return_sequences=1,
        )


output = generate_sentence("GPT-1bを用いて文章を生成してみることにした。")
decoded = tokenizer.batch_decode(output,skip_special_tokens=True)
for msg in decoded:
    print(msg)

for i in range(num): print(decoded[i])
GPT-1bを用いて文章を生成してみることにした。ただし、対象とする論文は実験プログラムを公開しているので自分の実験ノートを利用することになるだろう。すなわち、プログラム内部のコードはすべて外部向けのものになるので、完全にこの手順にのっとって動作するわけではない。また、プログラムで扱うデータはJSON形式なので、それを処理するためにはそのためのPython環境が必要となる。データのうち主なものを以下に示す。また、この実験の条件として、必ず同一実験データを用いるようにする。そのため複数の論文を同一のデータを用いて実験する。 さらに、
出力結果

それっぽい文章が生成されてはいるが、ところどころ不自然だったり嘘があったりする。当然そのままどこかに記載する文章として使うことはできなさそうだった。

自分の言葉に寄せる

文章の自動生成の機能を自分の補助役として使う為には、意図したデータを学習させる必要がある。そうでないと、どこかおかしなデータが出力される。例えば自分自身に寄せた表現をしてほしいなら自分自身の表現を、特定のドキュメントであればその表現を学習させたい。ドキュメントを書いているのに、いきなり歌詞が出てきては困るのだ。幸い、このブログに記述している文章がそこそこの量ある。学習させるにはちょっと少ない気もするが、そのあたりはあまり深く考えず、やってみることにする4

ファインチューニング

モデルを最初からトレーニングする事もできなくはない。ただし、それには時間もかかるし、多くのデータも必要だし、計算リソースも必要になる。それらを簡略化する方法をここでは試す。これはファインチューニングと呼ばれる。トレーニング済みモデルを、用途に応じたデータセットを用いて再トレーニングする事でパラメータを調整し、期待する挙動の状態に近づける。

ライブラリのインストールと事前学習済みモデルの取得

まず、必要なライブラリをインストールする。

pip install transformers==4.20.1 sentencepiece

次にPythonインタプリタを起動し、トークナイザーとモデルを読み込む。この時にダウンロードが走る。今回は事前学習済みモデルに rinna/japanese-gpt2-small を使用した56

from transformers import AutoTokenizer, AutoModelForCausalLM

t = AutoTokenizer.from_pretrained("rinna/japanese-gpt2-small")
m = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt2-small")

再トレーニング

再トレーニング用のデータセットとして、このサイトの掲載している記事の原稿(Org-modeのファイル)を適当にcatで繋げて a.txt という1つのテキストファイルに出力した。このファイルを読み込む。block_sizeは文章の長さのサイズで、揃える必要がある7

from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments, AutoModelWithLMHead

train_dataset = TextDataset(
    tokenizer=t, file_path="./a.txt", block_size=128)
データセットを設定する
data_collator = DataCollatorForLanguageModeling(
    tokenizer=t, mlm=False)
データの入力に関する設定

その他のトレーニングに必要な引数の設定を行う。

args = TrainingArguments(output_dir="./output", overwrite_output_dir=True,
    num_train_epochs=3, per_device_train_batch_size=8,  logging_steps=100, save_steps=800)
訓練に関する設定
引数意味
output_dir保存先のディレクトリへのパス。存在しなければ作成される。
overwrite_output_dir真の場合、ファイルを上書きする。
num_train_epochsエポック数。
per_device_train_batch_sizeバッチサイズ。
logging_steps処理の経過表示をする間隔。
save_steps保存間隔。例えば10を指定すると、10ステップ毎にモデルを保存する。
TrainingArgumentsの引数
trainer = Trainer(model=m, args=args,
    data_collator=data_collator, train_dataset=train_dataset)
訓練の処理クラスのインスタンス化する

ここまでで再トレーニングの準備が完了した。それでは再トレーニングする。データや環境にもよるが、この処理はかなり時間がかかる。

trainer.train()
再トレーニングする

私の環境(型落ちのMacBook Pro)では8時間強かかった。夜実行を開始し寝て、朝起きたらトレーニングは終了していた。GPUなどは使用してない環境でもあるため、とても遅かった。

TrainOutput(global_step=3699, training_loss=2.3952957776212216, metrics={'train_runtime': 30176.5446, 'train_samples_per_second': 0.98, 'train_steps_per_second': 0.123, 'train_loss': 2.3952957776212216, 'epoch': 3.0})
trainer.train()の標準出力への出力

文章の生成

それでは文章を生成する。ここでは「こんにわ、私は」という言葉を、元の言葉として続きの文章を生成してみる。

input_ids = tokenizer.encode("こんにわ、私は", return_tensors="pt",add_special_tokens=False)

input_idsにはtorch.Tensorのインスタンスが格納される。

>>> input_ids
tensor([[    9, 11641,    11]])
>>> type(input_ids)
<class 'torch.Tensor'>

これを用いて新たな文章となるデータを生成する。

with torch.no_grad():
    output = m.generate(
        input_ids,
        max_length=100,
        min_length=100,
        do_sample=True,
        top_k=500,
        top_p=0.95,
        pad_token_id=tokenizer.pad_token_id,
        bos_token_id=tokenizer.bos_token_id,
        eos_token_id=tokenizer.eos_token_id,
        num_return_sequences=1,
    )

outputにはtorch.Tensorのインスタンスが格納される。

>>> type(output)
<class 'torch.Tensor'>
>>> output
tensor([[    9, 11641,    11,     7,   539,   518,   883,    17,   474, 10057,
           324,     7,  9717, 14919,   128,  5022,   913,  4959, 18460,     8,
            51,     9,   474, 18523,   324,    51,     9,  2699,  3546,  2437,
            17,  1216, 20559,     9,     0,     9,     0,    51,     9,  2416,
            17, 15426,  2332,   892,    86,     9,  5210, 22242,    35,     9,
            24,    86,     9,    26,   900, 11077,   665,   892,    86,     9,
            26,   900,    11,  5544,  1038,  1263,    86,     9,  2699,  8095,
           582,  1741, 14982,  1602,    86,     9,  1216,  3443,  7512, 20836,
             9,  6612,     9,  2237,  1263,    86,     9,  1216,  4957,    18,
          5254,    35,     9,  2965,  2480,  8473,     9,  6612,     9, 10199]])
モデルのgenerateメソッドから返されるデータの例

トークナイザーを用いてこのデータを言葉に変換する。

message_list = t.batch_decode(output,skip_special_tokens=True)

message_listには文字列のリストが格納される。この文字列が生成された文章となる。

for msg in message_list:
    print(msg)
生成された文章を表示する

今回は以下のような文章が生成された。

こんにちわ、私は誰だと思いますか?私はサミーです、私はこのシリーズが好きなんだ、しかし私はこのシリーズよりも面白いものにはなれないから私が選んだものは誰だと思いますか?私はそうは思わないし、全く違う見解は論外ですね。 - 1 - 僕の友達 - スポーツ - 音楽 #+end_quote この記事が正しければ私は全く関連性がない記事だ。確かに僕は偉大すぎる

生成された文章の例

所感

かなりイマイチな結果だ。ChatGPTなどのGPT-3.5以降のアルゴリズムで構築されたモデルが生成する文章と比較すると、どうも不自然で違和感のある文章だ。トレーニング用のデータの量やクリーニングの度合い、過去世代のアルゴリズムであることを考えると、これは仕方がないようにも思う。ただし、トレーニングや文章生成が、ローカル(しかも型落ちのMac Book Pro)の環境で実行できる事を確認できた事には意味があったように思う。


4

作業にあたっては https://zenn.dev/robes/articles/24ee45dd81636a]] を参考にした。

6

smallとは銘打っているが、それでも400MB強はある。もしかしたら、400MB程度は今の時代では小さいと感じるかもしれないが、それでも、少しは気を付ける必要のある大きさではあると思う。

7

何と揃える必要があるの?元データ?