LoginSignup
36
33

More than 1 year has passed since last update.

13億パラメータの日本語特化GPT言語モデルを使ってSlackAIチャットボットを作ってみた1~文章生成機能編~

Last updated at Posted at 2022-07-13

1.はじめに

社内勉強会で発表することになり、題材としてrinna/japanese-gpt-1bを使用したSlackのAIチャットボットを作成したので、備忘録として残しておきます。

「文章生成機能編」と「SlackBot実装編」で記事を2つに分けており、当記事では文章生成機能について記載します。

初心者なため、間違った部分や理解の足りていない部分もあるかと思います。
そういった部分がありましたらコメントまたはTwitterで指摘して頂けると幸いです。

rinna/japanese-gpt-1bとは

13億パラメータの日本語特化GPT言語モデルです。rinna株式会社がオープンソースで公開しています。
今回はこちらを使用して、文章を生成する機能をbotに実装しています。
サンプルコードとほぼ同じものを使用していますが、相違点もあるためそこも含めて解説していきます。

公式ニュースはこちらです。

2.ライブラリインストール

pip install transformers sentencepiece torch

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

2-1.transformers

モデルを呼び出すために必要なtransformersをインストールします。

2-2.sentencepiece

このモデルではsentencepieceというtokenizerが使用されているので、そちらもインストールします。
tokenizerは文字列をトークンにエンコード/デコードするために使用します。

2-3.torch

テンソルの計算やCPU/GPUの切り替えでPyTorchが必要になるため、torchもインストールします。

3.コード解説

3-1.import

RinnaJapaneseGPT1b.py
from transformers import T5Tokenizer, AutoModelForCausalLM
import re
import torch

必要なライブラリをimportします。
2でインストールしたライブラリに加え、正規表現を使用するためreimportします。

3-2.tokenizerとmodelの生成

RinnaJapaneseGPT1b.py
model_name = 'rinna/japanese-gpt-1b'
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

モデルを指定し、tokenizermodelを生成します。

3-3.GPUが使用可能か判定

RinnaJapaneseGPT1b.py
if torch.cuda.is_available():
    model = model.to("cuda")

GPUが使用可能な環境かを判定します。

torch.cuda.is_available()でGPUが使用可能かを確認します。デフォルトはCPUです。
GPUが使用可能であれば、model.to("cuda")でGPU上にモデルのコピーを作成し、作成したコピーをmodelに上書きする形で格納します。
こうすることで、モデルをCPUからGPUに切り替えることができます1

なぜGPUを使うかについてですが、GPUは演算能力・並列処理能力に優れており、複雑な計算を行う際はCPUよりも適しているためです。
GPUを使うことで文章生成時間を短縮することができます2

3-4.文章生成

受け取ったメッセージに対して文章を生成します。

3-4-1.受け取ったメッセージをエンコード

RinnaJapaneseGPT1b.py
def generate_text(input_message):
    input_ids = tokenizer.encode(
        "私: " + input_message + "\nAI: ",
        add_special_tokens=False,
        return_tensors="pt"
    )

input_messageに受け取ったメッセージを渡します。

"私: " + input_message + "\nAI: "といった対話形式でチャットボットに返答させる文章を入力文としてtokenizerに与え、エンコードし、input_idsに格納します。

なぜ対話形式で与えているのかというと、ユーザーとAIを区別し、対話文と認識できるよう与えることで、返答にふさわしい文章を生成されやすくするためです。

スペシャルトークンを自分で追加するため、add_special_tokensFalseにしてスペシャルトークンの自動追加を無効にします。

Transformerモデルの入力でテンソル形式に変換する必要があるため、return_tensors"pt"に指定してPyTorchのテンソル型で返します3

3-4-2.generate関数で文章生成

RinnaJapaneseGPT1b.py
    with torch.no_grad():
        output_sequences = model.generate(
            input_ids.to(model.device),
            max_length=50,
            min_length=10,
            temperature=0.9,
            top_k=50,
            top_p=0.95,
            repetition_penalty=1.0,
            do_sample=True,
            num_return_sequences=1,
            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]]
        )

generate関数を使い、文章を生成します。

  • torch.no_grad()
    generate関数で文章を生成する前に設定します。これを設定することで、テンソル計算時の勾配情報を持たず、メモリ消費を抑えることができます。
    ざっくり説明すると、より良い結果を求めていく際の途中経過の計算の値を保持しないことで、メモリ消費を抑えるということです。
    詳しくは勾配降下法や誤差逆伝播法で調べてみてください。

  • パラメータの設定
    generate関数で文章を生成するにあたり、パラメータの設定をしていきます。
    設定されたパラメータを元にテンソルの計算が行われます。

    • input_ids.to(model.device)
      PyTorchではモデル学習時にモデルとデータを同じデバイス(GPU/CPU)に置く必要があるため、モデルと同じデバイス(GPU/CPU)上にinput_idsのコピーを作成します。

    • max_length/min_length
      生成する文章の文字数上限/下限を設定します。
      今回はチャットボットなので上限を50、下限を10と短めに設定しています。

    • temperature
      値が1に近づくほどクリエイティブな文章、0に近づくと論理的で正確な返答になる…らしいです。
      今回はクリエイティブな文章になるよう0.9に設定しています。

    • top_k
      与えられた文章の続きとして適している確率の高い上位k個の候補の単語からランダムに選択します。
      今回はある程度絞りつつバラエティを豊かにするため50に設定しています。

    • top_p
      与えられた文章の続きとして適している確率の高い上位候補の単語の確率の合計がp を超えるような最小個数の候補を動的に選択します(pの設定できる数字の範囲は1>p>=0)。
      今回は出来るだけ精度を高めるために0.95に設定しています。
      どういうことかというと、例えば文章の続きとして以下の単語が候補としてあった場合、今回の0.95だと車、電車、バスが選択されます。

    候補 確率
    50%
    電車 30%
    バス 20%
    自転車 10%
    飛行機 5%
    • repetition_penalty
      1に近づけると同じ文章の繰り返しを減少する効果があります。
      今回は繰り返しを出来るだけ減少するために1.0に設定しています。

    • do_sample=True
      Trueにすることでサンプリングを有効化します。

    • num_return_sequences
      一度の実行で生成する文章の数を指定します。
      今回は1つだけで良いので1に設定しています。

    • モデルの入力に必要な情報となるスペシャルトークンを指定します。

      • pad_token_id=tokenizer.pad_token_id
        使用されていない部分を埋めるためのトークンです4

      • bos_token_id=tokenizer.bos_token_id
        シーケンス(文章)の始まりを表すトークンです5

      • eos_token_id=tokenizer.eos_token_id
        シーケンス(文章)の終わりを表すトークンです6

      • bad_word_ids
        生成が許可されていないトークンIDリストを設定します。
        禁止用語等の設定で使用します。

      • tokenizer.unk_token_id
        未知の語彙を表すトークンです7

3-4-3.生成された文章をデコード

RinnaJapaneseGPT1b.py
    output_sequence = output_sequences.tolist()[0]
    text = tokenizer.decode(
        output_sequence,
        clean_up_tokenization_spaces=True
        )
    input_ids_length = len(tokenizer.decode(
        input_ids[0],
        clean_up_tokenization_spaces=True
        ))
    total_text = (text[input_ids_length:])

生成された文章をtokenizerでデコードします。

output_sequences.tolist()[0]に格納されているトークン状態の生成された文章をoutput_sequenceに格納します8
output_sequencetokenizerでデコードし、textに格納します。
エンコードする際にスペースが区切り文字として生成されているため、clean_up_tokenization_spacesTrueにして生成されたスペースをデコードする際に削除します。

チャットボットからの返答だけ得られれば良いので、textから最初に与えた入力文を除去します。

最初に与えた入力文であるinput_ids[0]tokenizerでデコードし、その長さを取得してinput_ids_lengthに格納します9
生成されたスペースはデコードする際に削除します。
textからinput_ids_lengthを除去したものをtotal_textに格納します。

3-4-4.デコードされた文章を返す

RinnaJapaneseGPT1b.py
    pattern = r"AI:|私:|俺:|僕:|あなた:|<unk>|</s>|[UNK]"
    if re.search(pattern, total_text) != None:
        match_position = [match.span() for match in re.finditer(pattern, total_text)]
        first_match_position = match_position[0][0]
        edited_text = total_text[:first_match_position]
        return edited_text
    else:
        return total_text

生成文にpatternにある文字列が含まれていた場合、それ以降の文字列を除去します。
含まれていなかった場合はそのまま出力します。

patternに除去したい文字列を指定します。
今回は文章を対話形式で与えており、チャットボットからの返答を1つ得られた後も対話文が続く可能性があります。
そのため、チャットボットからの返答の後に対話文が続いた場合、それ以降の文字列を除去するためにAI:|私:|俺:|僕:|あなた:を指定しています10
<unk>|</s>は試していて出力されることがあり、[UNK]も出力される可能性があると考え、指定しています11

正規表現を使い、if re.search(pattern, total_text) != None:で生成文にpatternにある文字列が含まれているかどうかを判定します。

  • 生成文にpatternにある文字列が含まれていた場合:
    [match.span() for match in re.finditer(pattern, total_text)]で文章中のpatternにある文字列の位置を全て抽出し、抽出したものをmatch_positionに格納します。
    match_position[0][0]で一番最初に出現したpatternにある文字列の位置を抽出し、first_match_positionに格納します。
    total_text[:first_match_position]で一番最初に出現したpatternにある文字列以降の文字列を除去し、edited_textに格納してreturn edited_textで返します。

  • 生成文にpatternにある文字列が含まれていなかった場合:
    return total_textでそのまま返します。

3-5.コード全文

RinnaJapaneseGPT1b.py
from transformers import T5Tokenizer, AutoModelForCausalLM
import re
import torch

model_name = 'rinna/japanese-gpt-1b'
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

if torch.cuda.is_available():
    model = model.to("cuda")
    
def generate_text(input_message):
    input_ids = tokenizer.encode(
        "私: " + input_message + "\nAI: ",
        add_special_tokens=False,
        return_tensors="pt"
    )
    
    with torch.no_grad():
        output_sequences = model.generate(
            input_ids.to(model.device),
            max_length=50,
            min_length=10,
            temperature=0.9,
            top_k=50,
            top_p=0.95,
            repetition_penalty=1.0,
            do_sample=True,
            num_return_sequences=1,
            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]]
        )
        
    output_sequence = output_sequences.tolist()[0]
    text = tokenizer.decode(
        output_sequence,
        clean_up_tokenization_spaces=True
        )
    input_ids_length = len(tokenizer.decode(
        input_ids[0],
        clean_up_tokenization_spaces=True
        ))
    total_text = (text[input_ids_length:])
    
    pattern = r"AI:|私:|俺:|僕:|あなた:|<unk>|</s>|[UNK]"
    if re.search(pattern, total_text) != None:
        match_position = [match.span() for match in re.finditer(pattern, total_text)]
        first_match_position = match_position[0][0]
        edited_text = total_text[:first_match_position]
        return edited_text
    else:
        return total_text

上記コピペでそのまま使用できます。

4.テスト実行

RinnaJapaneseGPT1b.py
print(generate_text("自己紹介をお願いします。"))

上記のコードを追加してテスト実行してみます。
"自己紹介をお願いします。"の部分は任意のテキストでokです。

私はAIの開発者です。 現在は、画像・音声・言語を扱うデータマイニング、機械学習、そして、機械学習を用いた予測モデルを作成している研究テーマに取り組んでいます。 データマイニングは、人間の

上記の文章が生成されました。無事実行出来ていますね。
少し怪しい部分もありますが、文章も比較的自然です(AI開発者のAIって面白いですね…)。

5.おわりに

今回でメインとなる文章生成機能が完成しました(サンプルコードほぼそのままですが…)。
これだけでも十分遊ぶことはできますが、より楽しめるようSlackBotとして実装します。
次回に続きます。

6.参考記事

  1. cudaは、NVIDIA社が自社製GPU向けに開発・提供しているGPUを利用して汎用の並列計算を行うためのソフトウェア開発・実行環境のことです。

  2. ディープラーニングで行う行列演算と三次元グラフィックス処理時の行列演算が同じで、GPUとディープラーニングの相性が良かったという背景もあります。ちなみに、PyTorchでサポートされているGPUはNVIDIA製のもののみとなります(TensorFlowも同様です)。

  3. 機械学習におけるテンソルは多次元配列とほぼ同義です(=行列)。

  4. PAD(Padding)

  5. BOS(Begin Of Sentence)

  6. EOS(End Of Sentence)

  7. UNK(Unknown)

  8. トークン状態の生成された文章がoutput_sequences.tolist()に二次元リストで格納されています。今回は生成する文章が1つだけなので、0番目を指定しています。

  9. 最初に与えた入力文がinput_idsに二次元リストで格納されています。入力文は1つだけなので、0番目を指定しています。

  10. 頻出なものや考えられうるものを設定しました。これら以外の一人称等が生成文に含まれていた場合はそのまま出力されてしまいます。

  11. [UNK]bad_word_idsに設定しているため、ここで指定する必要はないかもしれません。

36
33
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
33