はじめに
「SiriとAlexaを対話させてみた」の動画を見て、テキストベースで似たようなことができないかな、と思いました。
色々探してみたところ、同じようなことを行っている方がおられたので、自分なりに改良してみました。
事前学習モデル
今回は、参考にさせていただいたサイト様と同じく、rinnna社のjapanese-gpt-1bを使用します。
開発環境
python 3.9.12
torch 1.12.1
transformers 4.21.3
sentencepiece 0.1.97
コード
コンストラクタ
トークナイザとモデルの準備、あとはGPUと後で使うリスト(後述)を作成します。
class create_text:
def __init__(self, model_name):
self.tokenizer = T5Tokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(model_name)
self.loop_list = []
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model = self.model.to(device)
文章の作成
文章を作成する関数です。(そのまんまですね 笑)
- 引数1:文章生成を行うトリガーになるテキストです。この関数では、このテキストの続きになるような文章を作成します。
- 引数2:生成する文章の最大文字数です。あまり長いと会話感が薄れるため、今回は40文字にしました。40文字を出力文がオーバーすると出力文は作り直しです。
今回は、理論的なことをゴリゴリ書くのではなく、どうやったら会話っぽくなるかを中心に記事を書くため、細かいパラメータ等の説明は省かせていただきます。
#create_txtのクラス内関数
def generate_text(self, text, max_length = 40):
token_ids = self.tokenizer.encode(text[-100:], add_special_tokens=False, return_tensors="pt")
if max_length < len(text): max_length = len(text) + 5
l = 0
while True:
with torch.no_grad():
output_ids = self.model.generate(token_ids.to(self.model.device),
max_length = max_length,
min_length = 15,
do_sample = True,
pad_token_id = self.tokenizer.pad_token_id,
bos_token_id = self.tokenizer.bos_token_id,
eos_token_id = self.tokenizer.eos_token_id,
bad_word_ids = [[self.tokenizer.unk_token_id]])
output = self.adjust(output_ids, text)
l += 1
if (4 < len(output)) and (len(output) < max_length): break
self.loop_list.append(l)
return output
生成した文章の調整
生成した文章を会話っぽくするために、諸々を調整する関数です。
- 引数1:出力文の単語インデックス
- 引数2:出力文以前の全文章
ざっくり説明すると、出力用にデコードした文章から頭の1文を抽出。直前の文章が同じだった場合は作り直しを行わせるものになっています。
今回は、文章を1文取り出すようにしていますが、これをしないと「~~、」で出力されて、次の文章が 完全に直前の文章の続きになってしまいます。
#create_txtのクラス内関数
def adjust(self, output_ids, text):
earlier_text = text[ text[:-1].rfind("。") + 1:]
output = self.tokenizer.decode(output_ids.tolist()[0])
output = output.replace(text[-100:], "").replace("</s>" ,"")
output = output[:output.find("。")] + "。"
if output == earlier_text: output = ""
return output
会話関数
出力された文章をAさん、Bさんに交互に分けてコンソールに出力する関数です。
- 引数1:出力文
- 引数2:Aさん、Bさんを判断する変数
これは、見たまんまなので説明は省略します。
def talker(text, i):
player = "Aさん " if i % 2 != 0 else "Bさん "
print(player, text)
if i % 2 == 0: print("")
create_txt.py 全コード
ここまで記載したクラス・関数と実行部分を以下に記載します。
import torch
from transformers import T5Tokenizer, AutoModelForCausalLM
class create_text:
def __init__(self, model_name):
self.tokenizer = T5Tokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(model_name)
self.loop_list = []
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model = self.model.to(device)
def generate_text(self, text, max_length = 30):
token_ids = self.tokenizer.encode(text[-100:], add_special_tokens=False, return_tensors="pt")
if max_length < len(text): max_length = len(text) + 5
l = 0
while True:
with torch.no_grad():
output_ids = self.model.generate(token_ids.to(self.model.device),
max_length = max_length,
min_length = 15,
do_sample = True,
pad_token_id = self.tokenizer.pad_token_id,
bos_token_id = self.tokenizer.bos_token_id,
eos_token_id = self.tokenizer.eos_token_id,
bad_word_ids = [[self.tokenizer.unk_token_id]])
output = self.adjust(output_ids, text)
l += 1
if (4 < len(output)) and (len(output) < 40): break
self.loop_list.append(l)
return output
def adjust(self, output_ids, text):
earlier_text = text[ text[:-1].rfind("。") + 1:]
output = self.tokenizer.decode(output_ids.tolist()[0])
output = output.replace(text[-100:], "").replace("</s>" ,"")
output = output[:output.find("。")] + "。"
if output == earlier_text: output = ""
return output
def talker(text, i):
player = "Aさん " if i % 2 != 0 else "Bさん "
print(player, text)
if i % 2 == 0: print("")
if __name__ == "__main__":
input_text='こんにちは。調子はどうですか。'
model_name = "rinna/japanese-gpt-1b"
creator = create_text(model_name)
i = 0
print("Aさん ", input_text)
for i in range(13):
talk_text = creator.generate_text(input_text)
talker(talk_text, i)
input_text += talk_text
i += 1
print("各テキストのやり直し回数", creator.loop_list)
一番下の行の、creator.loop_listは、generate_textの中で、何回出力文を作り直したかをmainのループ分だけ記録したものです。
実行結果
実際に動かした結果を2パターンほど、記載します。
パターン1「input_txt:こんにちは。調子はどうですか。」
Aさん こんにちは。調子はどうですか。
Bさん 元気ですか。
Aさん ぼくはとても元気です。
Bさん ぼくは昨夜、とてもうれしくなりました。
Aさん ぼくが書いた夢日記を読んだ人が、ぼくに対して、こう言ってくれたからです。
Bさん 「きみの夢日記を読んで、はじめて、夢について、分かったんだ。
Aさん ぼくはとても元気です。
Bさん いつも、とても楽しくて...。
Aさん ぼくの夢とは、 冒険と発見に満ちた、素晴らしい夢なんだ。
Bさん きみの夢日記がこんなに楽しいのは、きみの夢日記が、ぼくの夢日記なんだよ。
Aさん ぼくの大好きなきみは、いつも、ぼくより少し賢い。
Bさん どんな時も、ぼくのことを信じてくれる。
Aさん きみは、大切なことを教えてくれた。
Bさん ぼくたちは、いつでも、一緒にいる。
各テキストのやり直し回数 [3, 2, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1]
...1人喋り、というかポエミーな文章を分割したような会話になりました 笑
「めちゃくちゃ仲良しの双子が互いを大事に思っている」という設定を付加すれば、会話っぽいと言えなくもないですね(無理矢理)
パターン2「input_txt:京都観光は楽しいな。」
Aさん 京都観光は楽しいな。
Bさん おみやげに、梅干しは最高ですが・・。
Aさん ちなみに、私は梅干し大好きなので、これはお土産としては失敗。
Bさん ただ、お供え用なのか、数個のみですが・・。
Aさん その代わり、お寺の参道のところでも売っていましたよ。
Bさん 梅干しを供えている人を見たこともあります。
Aさん こちらはその時のものです。
Bさん ↓また、お寺では、お供え物の水子人形を飾っていらっしゃるお家もありました。
Aさん 子供のお宮参りをした時のお土産も売っていましたよ。
Bさん お稲荷さん(神社)です。
Aさん この神社は日本三大稲荷のひとつです。
Bさん なので、小さい子だけでなく、大人のお参りも多いですよ。
Aさん こちらは神社が祀られている森。
Bさん 秋には紅葉した木々に囲まれ、紅葉狩りにも最適です。
各テキストのやり直し回数 [1, 1, 3, 5, 1, 6, 4, 2, 1, 1, 1, 21, 1]
京都土産に梅干しは無いと思います(真顔)
パターン1よりは会話っぽいかな~、と思いますが やはり微妙ですね...。
神社の話に移行した途端、土産の話をしなくなったあたり、直前の文章に大きく左右されていることが伺えますね。
考察
直前の文章にどんどん文章を付け加えることによって、会話っぽくなるのではないかと思いましたが、あんまり良い結果にはなりませんでした。
これは、単純に「1つの文章として続いているから」であるのが、1番大きな理由だと思われます。会話として眺めても、対話というよりは同じ方向を向いて同調しているだけに見えます。
これを会話っぽい感じにするのであれば、文章に脈絡があることは大前提ですが、文章作成インスタンスを2つ(Aさん用、Bさん用)作成して、それぞれ一貫したスタンスをもたせる必要があるのかな、と個人的には思います。
(スタンスとは少し違うかもしれませんが)2つのインスタンスに、出力する前にネガポジを判断させるような関数を組み込んで、インスタンスAはポジティブ文のみ出力、インスタンスBがネガティブ分のみ出力させるように調整すれば、正反対の人格をもつ2人が喋っているように見えたりしないかなー、なんて考えています。
ちなみにですが、インスタンスを単純に2つ作成して交互に文章作成を行ったり、2つのインスタンスに別々の事前学習モデルを突っ込んで文章作成させるだけでは、同じような結果になりました。
まだまだ精進が必要そうですね...。(伸び代ッ!!)