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
from transformers import T5Tokenizer, AutoModelForCausalLM
import re
import torch
必要なライブラリをimport
します。
2でインストールしたライブラリに加え、正規表現を使用するためre
もimport
します。
3-2.tokenizerとmodelの生成
model_name = 'rinna/japanese-gpt-1b'
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
モデルを指定し、tokenizer
とmodel
を生成します。
3-3.GPUが使用可能か判定
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.受け取ったメッセージをエンコード
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_tokens
をFalse
にしてスペシャルトークンの自動追加を無効にします。
Transformer
モデルの入力でテンソル形式に変換する必要があるため、return_tensors
を"pt"
に指定してPyTorch
のテンソル型で返します3。
3-4-2.generate関数で文章生成
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
に設定しています。 -
モデルの入力に必要な情報となるスペシャルトークンを指定します。
-
3-4-3.生成された文章をデコード
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_sequence
をtokenizer
でデコードし、text
に格納します。
エンコードする際にスペースが区切り文字として生成されているため、clean_up_tokenization_spaces
をTrue
にして生成されたスペースをデコードする際に削除します。
チャットボットからの返答だけ得られれば良いので、text
から最初に与えた入力文を除去します。
最初に与えた入力文であるinput_ids[0]
をtokenizer
でデコードし、その長さを取得してinput_ids_length
に格納します9。
生成されたスペースはデコードする際に削除します。
text
からinput_ids_length
を除去したものをtotal_text
に格納します。
3-4-4.デコードされた文章を返す
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.コード全文
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.テスト実行
print(generate_text("自己紹介をお願いします。"))
上記のコードを追加してテスト実行してみます。
"自己紹介をお願いします。"
の部分は任意のテキストでokです。
私はAIの開発者です。 現在は、画像・音声・言語を扱うデータマイニング、機械学習、そして、機械学習を用いた予測モデルを作成している研究テーマに取り組んでいます。 データマイニングは、人間の
上記の文章が生成されました。無事実行出来ていますね。
少し怪しい部分もありますが、文章も比較的自然です(AI開発者のAIって面白いですね…)。
5.おわりに
今回でメインとなる文章生成機能が完成しました(サンプルコードほぼそのままですが…)。
これだけでも十分遊ぶことはできますが、より楽しめるようSlackBotとして実装します。
次回に続きます。
6.参考記事
- Microsoftから独立した企業が公開した日本語版GPT-2でチャットボットを作るレシピ
- Huggingface Transformers 入門 (1) - 事始め
- テキストの前処理 - Keras
- Tokenizer
- 「torch.nnを用いたディープラーニングモデルの実装方法」
- 勾配法の仕組みを具体例でわかりやすく解説
- 第3回 なぜニューラルネットワークで学習できるのか?
-
cuda
は、NVIDIA社が自社製GPU向けに開発・提供しているGPUを利用して汎用の並列計算を行うためのソフトウェア開発・実行環境のことです。 ↩ -
ディープラーニングで行う行列演算と三次元グラフィックス処理時の行列演算が同じで、GPUとディープラーニングの相性が良かったという背景もあります。ちなみに、
PyTorch
でサポートされているGPUはNVIDIA製のもののみとなります(TensorFlow
も同様です)。 ↩ -
機械学習におけるテンソルは多次元配列とほぼ同義です(=行列)。 ↩
-
PAD(Padding) ↩
-
BOS(Begin Of Sentence) ↩
-
EOS(End Of Sentence) ↩
-
UNK(Unknown) ↩
-
トークン状態の生成された文章が
output_sequences.tolist()
に二次元リストで格納されています。今回は生成する文章が1つだけなので、0番目を指定しています。 ↩ -
最初に与えた入力文が
input_ids
に二次元リストで格納されています。入力文は1つだけなので、0番目を指定しています。 ↩ -
頻出なものや考えられうるものを設定しました。これら以外の一人称等が生成文に含まれていた場合はそのまま出力されてしまいます。 ↩
-
[UNK]
はbad_word_ids
に設定しているため、ここで指定する必要はないかもしれません。 ↩