【Python】rinnaのGPT-2を使って個性を持つ会話の流れを保持して会話できるAI(chatbot)を作ってみた!おまけでタチコマを再現してみた
Dialogptやblenderbotを使ってボットを作りたいと思っても日本語の事前学習モデルが公開されていないのでなかなか個人には難しいですよね。そこで今回は、rinna社が公開している事前学習モデルを用いて誰でもできる方法を記事にしました。
- 雑談ボットを作ってみたい!
- 個性を持たせたボットを作りたい!
- 文脈を保持した会話を話せるようにさせたい!
- ある程度話すことを限定させたい!
- 実行環境を整えるのがめんどくさい!
この記事は以上のような方向けとなっています。
今回はsimple transformersのConversational AIを参考にしています。
simple transformersのgithubはこちらです。
transformerってなんだ?という方はこちらをおすすめします。
かなり分かりやすいです。
(2023/07/20追記)
「Llama-2」が公開されました!
実行環境
Google Colaboratory
無料枠でも学習可能です。
データセット
以下のデータは最小限のデータです。ご自分で増やすなど工夫してjsonファイルとして保存してください。
[
{
"personality": [
"私は鹿児島県で生まれました。",
"私は東京に住んでみたいです。",
"私は看護師です。"
],
"utterances": [
{
"candidates": [
"はい、元気です。鹿児島での看護師の仕事が忙しいですけど"
],
"history": [
"こんばんは。お元気ですか?"
]
},
{
"candidates": [
"介護福祉士として働いていらっしゃるんですね。大変なお仕事ですよね。"
],
"history": [
"こんばんは。お元気ですか?",
"はい、元気です。鹿児島での看護師の仕事が忙しいですけど",
"お疲れさまです。私は介護福祉士をしています。"
]
},
{
"candidates": [
"北海道ですか。ご飯の美味しいところで羨ましいです。私は青森県出身で山の多いところが好きなので、山の近くに住んでいます"
],
"history": [
"こんばんは。お元気ですか?",
"はい、元気です。鹿児島での看護師の仕事が忙しいですけど",
"お疲れさまです。私は介護福祉士をしています。",
"介護福祉士として働いていらっしゃるんですね。大変なお仕事ですよね。",
"いえ。看護師も大変ですよね。ちなみに私は北海道の一軒家に住んでいます。"
]
}
]
}
]
コード
「ランタイム」→「ランタイムのタイプの変更」からGPUを選択してください。
それから以下のコードをrun.ipynb
にコピペして上から順番にColaboratoryで実行してください。
フォルダを作成
# Googleドライブをマウントします
from google.colab import drive
drive.mount('/content/drive')
!cd /content/drive/MyDrive/
# conversation_AIフォルダを作成します
!mkdir conversation_AI
# 作成したフォルダへ移動します
cd conversation_AI/
ファイルは以下のように設置してください
conversation_AI
└── run.ipynb
└── data.json
ライブラリのインストール
必要なライブラリをインストールします。
!pip install simpletransformers
(追記2022/11/1)
importerror
が出るようになったので、transformers
のバージョンを指定します。
!pip install transformers==4.21
model定義
simple transformersのライセンスはこちらになります。日本語版のモデルに合わせるためにtokenizerなどを変更しているのと学習に関係のないeval部分は取り除きました。
model code
# 必要なものをインポートします
import torch
import math
import random
import logging
import warnings
from collections import defaultdict
from transformers import T5Tokenizer, GPT2LMHeadModel, GPT2Config, AutoModelForCausalLM, GPT2DoubleHeadsModel
from transformers.optimization import AdamW, get_linear_schedule_with_warmup
import pandas as pd
from itertools import chain
import os
from tqdm.auto import tqdm, trange
from simpletransformers.config.model_args import ConvAIArgs
from simpletransformers.conv_ai.conv_ai_utils import get_dataset
from simpletransformers.config.utils import sweep_config_to_sweep_values
import wandb
import torch.nn.functional as F
from torch.utils.tensorboard import SummaryWriter
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler, TensorDataset
from sklearn.metrics import (
f1_score
)
logger = logging.getLogger(__name__)
SPECIAL_TOKENS = ["<s>", "</s>", "<speaker1>", "<speaker2>", "[PAD]"]
ATTR_TO_SPECIAL_TOKEN = {
"bos_token": "<s>",
"eos_token": "</s>",
"pad_token": "<PAD>",
"additional_special_tokens": ["<speaker1>", "<speaker2>"],
}
MODEL_INPUTS = ["input_ids", "mc_token_ids", "labels", "mc_labels", "token_type_ids"]
PADDED_INPUTS = ["input_ids", "labels", "token_type_ids"]
class ConvAIModel:
def __init__(self, model_name, args=None, **kwargs):
self.args = self._load_model_args(model_name)
if "sweep_config" in kwargs:
self.is_sweeping = True
sweep_config = kwargs.pop("sweep_config")
sweep_values = sweep_config_to_sweep_values(sweep_config)
self.args.update_from_dict(sweep_values)
else:
self.is_sweeping = False
if isinstance(args, dict):
self.args.update_from_dict(args)
elif isinstance(args, ConvAIArgs):
self.args = args
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 日本語版のモデルに合わせる
self.tokenizer = T5Tokenizer.from_pretrained(model_name)
self.tokenizer.do_lower_case = True
# AutoModelForCausalLM
self.model = GPT2DoubleHeadsModel.from_pretrained(model_name)
self.config = GPT2Config.from_pretrained(model_name)
self.add_special_tokens_(self.model, self.tokenizer)
def train_model(self, train_file=None, output_dir=None, show_running_loss=True, args=None, eval_file=None, verbose=True, **kwargs):
if self.args.evaluate_during_training and eval_file is None:
warnings.warn(
"eval_file not specified but evaluate_during_training is True. Using personachat eval data."
)
if args:
self.args.update_from_dict(args)
if not output_dir:
output_dir = self.args.output_dir
if (
os.path.exists(output_dir)
and os.listdir(output_dir)
and not self.args.overwrite_output_dir
):
raise ValueError(
"Output directory ({}) already exists and is not empty."
" Set overwrite_output_dir: True to automatically overwrite.".format(
output_dir
)
)
self._move_model_to_device()
train_dataloader, train_sampler = self.load_and_cache_examples(
dataset_path=train_file,
verbose=verbose,
no_cache=self.args.no_cache or self.args.reprocess_input_data,
)
eval_loader = None
os.makedirs(output_dir, exist_ok=True)
global_step, training_details = self.train(
train_dataloader,
output_dir,
show_running_loss=show_running_loss,
verbose=verbose,
**kwargs,
)
self.save_model(model=self.model)
if verbose:
logger.info(
" Training of {} model complete. Saved to {}.".format(
self.args.model_type, output_dir
)
)
def train(self, train_dataloader, output_dir, show_running_loss=True, verbose=True, **kwargs,):
device = self.device
model = self.model
args = self.args
tb_writer = SummaryWriter(log_dir=args.tensorboard_dir)
t_total = (
len(train_dataloader)
// args.gradient_accumulation_steps
* args.num_train_epochs
)
no_decay = ["bias", "LayerNorm.weight"]
optimizer_grouped_parameters = []
custom_parameter_names = set()
for group in self.args.custom_parameter_groups:
params = group.pop("params")
custom_parameter_names.update(params)
param_group = {**group}
param_group["params"] = [
p for n, p in model.named_parameters() if n in params
]
optimizer_grouped_parameters.append(param_group)
for group in self.args.custom_layer_parameters:
layer_number = group.pop("layer")
layer = f"layer.{layer_number}."
group_d = {**group}
group_nd = {**group}
group_nd["weight_decay"] = 0.0
params_d = []
params_nd = []
for n, p in model.named_parameters():
if n not in custom_parameter_names and layer in n:
if any(nd in n for nd in no_decay):
params_nd.append(p)
else:
params_d.append(p)
custom_parameter_names.add(n)
group_d["params"] = params_d
group_nd["params"] = params_nd
optimizer_grouped_parameters.append(group_d)
optimizer_grouped_parameters.append(group_nd)
if not self.args.train_custom_parameters_only:
optimizer_grouped_parameters.extend(
[
{
"params": [
p
for n, p in model.named_parameters()
if n not in custom_parameter_names
and not any(nd in n for nd in no_decay)
],
"weight_decay": args.weight_decay,
},
{
"params": [
p
for n, p in model.named_parameters()
if n not in custom_parameter_names
and any(nd in n for nd in no_decay)
],
"weight_decay": 0.0,
},
]
)
warmup_steps = math.ceil(t_total * args.warmup_ratio)
args.warmup_steps = (
warmup_steps if args.warmup_steps == 0 else args.warmup_steps
)
optimizer = AdamW(
optimizer_grouped_parameters,
lr=args.learning_rate,
eps=args.adam_epsilon,
)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=args.warmup_steps,
num_training_steps=t_total,
)
if args.n_gpu > 1:
model = torch.nn.DataParallel(model)
global_step = 0
training_progress_scores = None
tr_loss, logging_loss = 0.0, 0.0
model.zero_grad()
train_iterator = trange(
int(args.num_train_epochs), desc="Epoch", disable=args.silent
)
epoch_number = 0
best_eval_metric = None
early_stopping_counter = 0
if args.fp16:
from torch.cuda import amp
scaler = amp.GradScaler()
for _ in train_iterator:
model.train()
train_iterator.set_description(
f"Epoch {epoch_number} of {args.num_train_epochs}"
)
batch_iterator = tqdm(
train_dataloader,
desc=f"Running Epoch {epoch_number + 1} of {args.num_train_epochs}",
disable=args.silent,
mininterval=0,
)
for step, batch in enumerate(batch_iterator):
batch = tuple(t.to(device) for t in batch)
input_ids, mc_token_ids, labels, mc_labels, token_type_ids = batch
if args.fp16:
with amp.autocast():
outputs = model(
input_ids,
token_type_ids=token_type_ids,
mc_token_ids=mc_token_ids,
mc_labels=mc_labels,
labels=labels,
)
lm_loss, mc_loss = outputs[:2]
# model outputs are always tuple in pytorch-transformers (see doc)
loss = lm_loss * args.lm_coef + mc_loss * args.mc_coef
else:
outputs = model(
input_ids,
token_type_ids=token_type_ids,
mc_token_ids=mc_token_ids,
mc_labels=mc_labels,
labels=labels,
)
lm_loss, mc_loss = outputs[:2]
# model outputs are always tuple in pytorch-transformers (see doc)
loss = lm_loss * args.lm_coef + mc_loss * args.mc_coef
if args.n_gpu > 1:
loss = (
loss.mean()
)
current_loss = loss.item()
if show_running_loss:
print("\rRunning loss: %f" % current_loss, end="")
if args.gradient_accumulation_steps > 1:
loss = loss / args.gradient_accumulation_steps
if args.fp16:
scaler.scale(loss).backward()
else:
loss.backward()
tr_loss += loss.item()
if (step + 1) % args.gradient_accumulation_steps == 0:
if args.fp16:
scaler.unscale_(optimizer)
if args.optimizer == "AdamW":
torch.nn.utils.clip_grad_norm_(
model.parameters(), args.max_grad_norm
)
if args.fp16:
scaler.step(optimizer)
scaler.update()
else:
optimizer.step()
scheduler.step() # Update learning rate schedule
model.zero_grad()
global_step += 1
if args.logging_steps > 0 and global_step % args.logging_steps == 0:
# Log metrics
tb_writer.add_scalar(
"lr", scheduler.get_last_lr()[0], global_step
)
tb_writer.add_scalar(
"loss",
(tr_loss - logging_loss) / args.logging_steps,
global_step,
)
logging_loss = tr_loss
if args.wandb_project or self.is_sweeping:
wandb.log(
{
"Training loss": current_loss,
"lr": scheduler.get_last_lr()[0],
"global_step": global_step,
}
)
if args.save_steps > 0 and global_step % args.save_steps == 0:
# Save model checkpoint
output_dir_current = os.path.join(
output_dir, "checkpoint-{}".format(global_step)
)
self.save_model(output_dir_current, model=model)
epoch_number += 1
output_dir_current = os.path.join(
output_dir, "checkpoint-{}-epoch-{}".format(global_step, epoch_number)
)
if args.save_model_every_epoch or args.evaluate_during_training:
os.makedirs(output_dir_current, exist_ok=True)
if args.save_model_every_epoch:
self.save_model(output_dir_current, model=model)
return (
global_step,
tr_loss / global_step
if not self.args.evaluate_during_training
else training_progress_scores,
)
def load_and_cache_examples(self, dataset_path=None, evaluate=False, no_cache=False, verbose=True, silent=False):
process_count = self.args.process_count
tokenizer = self.tokenizer
args = self.args
if not no_cache:
no_cache = args.no_cache
os.makedirs(self.args.cache_dir, exist_ok=True)
dataset_path = dataset_path if dataset_path else ""
dataset = get_dataset(
tokenizer,
dataset_path,
args.cache_dir,
process_count=process_count,
proxies=self.__dict__.get("proxies", None),
evaluate=evaluate,
no_cache=no_cache,
args=args,
)
datasets = defaultdict(list)
num_candidates = len(dataset[0]["utterances"][0]["candidates"])
if args.num_candidates > 0 and not evaluate:
num_candidates = min(args.num_candidates, num_candidates)
for dialog in dataset:
persona = dialog["personality"].copy()
for _ in range(args.personality_permutations):
for utterance in dialog["utterances"]:
history = utterance["history"][-(2 * args.max_history + 1) :]
for j, candidate in enumerate(
utterance["candidates"][-num_candidates:]
):
labels = bool(j == num_candidates - 1)
instance = self.build_input_from_segments(
persona, history, candidate, tokenizer, labels
)
for input_name, input_array in instance.items():
datasets[input_name].append(input_array)
datasets["mc_labels"].append(num_candidates - 1)
datasets["n_candidates"] = num_candidates
persona = [persona[-1]] + persona[:-1] # permuted personalities
tensor_datasets = []
dataset = self.pad_dataset(
datasets, padding=tokenizer.convert_tokens_to_ids(SPECIAL_TOKENS[-1])
)
for input_name in MODEL_INPUTS:
tensor = torch.tensor(dataset[input_name])
if input_name != "mc_labels":
tensor = tensor.view((-1, datasets["n_candidates"]) + tensor.shape[1:])
tensor_datasets.append(tensor)
tensor_dataset = TensorDataset(*tensor_datasets)
if not evaluate:
data_sampler = RandomSampler(tensor_dataset)
data_loader = DataLoader(
tensor_dataset, sampler=data_sampler, batch_size=args.train_batch_size
)
else:
data_sampler = SequentialSampler(tensor_dataset)
data_loader = DataLoader(
tensor_dataset, sampler=data_sampler, batch_size=args.eval_batch_size
)
return data_loader, data_sampler
def compute_metrics(self, mc_preds, mc_labels, lm_logits, labels, **kwargs):
loss_fct = torch.nn.CrossEntropyLoss(ignore_index=-100)
extra_metrics = {}
for metric, func in kwargs.items():
extra_metrics[metric] = func(mc_labels, mc_preds)
f1_current = f1_score(mc_labels.cpu().numpy(), mc_preds, average="macro")
lm_loss_current = loss_fct(lm_logits, labels)
return {
**{"f1_score": f1_current, "language_model_loss": lm_loss_current},
**extra_metrics,
}
def interact(self, personality=None):
model = self.model
args = self.args
tokenizer = self.tokenizer
process_count = self.args.process_count
if self.args.fp16:
from torch.cuda import amp
self._move_model_to_device()
if not personality:
dataset = get_dataset(
tokenizer,
None,
args.cache_dir,
process_count=process_count,
proxies=self.__dict__.get("proxies", None),
interact=True,
args=args,
)
personalities = [
dialog["personality"]
for dataset in dataset.values()
for dialog in dataset
]
personality = random.choice(personalities)
else:
personality = [tokenizer.encode(s.lower()) for s in personality]
history = []
while True:
raw_text = input(">>> ")
while not raw_text:
print("Prompt should not be empty!")
raw_text = input(">>> ")
history.append(
tokenizer.encode(raw_text)
)
with torch.no_grad():
if args.fp16:
with amp.autocast():
out_ids = self.sample_sequence(
personality, history, tokenizer, model, args
)
else:
out_ids = self.sample_sequence(
personality, history, tokenizer, model, args
)
history.append(out_ids)
history = history[-(2 * args.max_history + 1) :]
out_text = tokenizer.decode(
out_ids, skip_special_tokens=self.args.skip_special_tokens
)
print("you->", raw_text)
print("bot->", out_text)
print("--------------------------------")
# print(history)
def interact_single(self, message, history, personality=None, encode_history=True):
model = self.model
args = self.args
tokenizer = self.tokenizer
process_count = self.args.process_count
if self.args.fp16:
from torch.cuda import amp
self._move_model_to_device()
if not personality:
dataset = get_dataset(
tokenizer,
None,
args.cache_dir,
process_count=process_count,
proxies=self.__dict__.get("proxies", None),
interact=True,
)
personalities = [
dialog["personality"]
for dataset in dataset.values()
for dialog in dataset
]
personality = random.choice(personalities)
else:
personality = [tokenizer.encode(s.lower()) for s in personality]
if encode_history:
raw_history = history.copy()
raw_history.append(message)
history = [tokenizer.encode(sentence) for sentence in history]
history.append(tokenizer.encode(message))
with torch.no_grad():
if args.fp16:
with amp.autocast():
out_ids = self.sample_sequence(
personality, history, tokenizer, model, args
)
else:
out_ids = self.sample_sequence(
personality, history, tokenizer, model, args
)
out_text = tokenizer.decode(
out_ids, skip_special_tokens=self.args.skip_special_tokens
)
if encode_history:
raw_history.append(out_text)
history = raw_history
else:
history.append(out_ids)
return out_text, history
def _threshold(self, x, threshold):
if x >= threshold:
return 1
return 0
def _move_model_to_device(self):
self.model.to(self.device)
def _get_last_metrics(self, metric_values):
return {metric: values[-1] for metric, values in metric_values.items()}
def _create_training_progress_scores(self, **kwargs):
extra_metrics = {key: [] for key in kwargs}
training_progress_scores = {
"global_step": [],
"language_model_loss": [],
"f1_score": [],
**extra_metrics,
}
return training_progress_scores
def save_model(self, output_dir=None, model=None, results=None):
if not output_dir:
output_dir = self.args.output_dir
if model and not self.args.no_save:
# Take care of distributed/parallel training
model_to_save = model.module if hasattr(model, "module") else model
model_to_save.save_pretrained(output_dir)
self.tokenizer.save_pretrained(output_dir)
self.save_model_args(output_dir)
if results:
output_eval_file = os.path.join(output_dir, "eval_results.txt")
with open(output_eval_file, "w") as writer:
for key in sorted(results.keys()):
writer.write("{} = {}\n".format(key, str(results[key])))
def add_special_tokens_(self, model, tokenizer):
orig_num_tokens = tokenizer.vocab_size
num_added_tokens = tokenizer.add_special_tokens(
ATTR_TO_SPECIAL_TOKEN
) # doesn't add if they are already there
if num_added_tokens > 0:
self.model.resize_token_embeddings(
new_num_tokens=orig_num_tokens + num_added_tokens
)
def build_input_from_segments(self, persona, history, reply, tokenizer, labels=False, with_eos=True):
bos, eos, speaker1, speaker2 = tokenizer.convert_tokens_to_ids(
SPECIAL_TOKENS[:-1]
)
sequence = (
[[bos] + list(chain(*persona))]
+ history
+ [reply + ([eos] if with_eos else [])]
)
sequence = [sequence[0]] + [
[speaker2 if (len(sequence) - i) % 2 else speaker1] + s
for i, s in enumerate(sequence[1:])
]
instance = {}
instance["input_ids"] = list(chain(*sequence))
instance["token_type_ids"] = [
speaker2 if i % 2 else speaker1 for i, s in enumerate(sequence) for _ in s
]
instance["mc_token_ids"] = len(instance["input_ids"]) - 1
instance["labels"] = [-100] * len(instance["input_ids"])
if labels:
instance["labels"] = (
([-100] * sum(len(s) for s in sequence[:-1]))
+ [-100]
+ sequence[-1][1:]
)
return instance
def pad_dataset(self, dataset, padding=0):
max_l = max(len(x) for x in dataset["input_ids"])
for name in PADDED_INPUTS:
dataset[name] = [
x + [padding if name != "labels" else -100] * (max_l - len(x))
for x in dataset[name]
]
return dataset
def top_filtering(self, logits, top_k=0.0, top_p=0.9, threshold=-float("Inf"), filter_value=-float("Inf"), ):
assert (
logits.dim() == 1
) # Only work for batch size 1 for now - could update but it would obfuscate a bit the code
top_k = min(top_k, logits.size(-1))
if top_k > 0:
# Remove all tokens with a probability less than the last token in the top-k tokens
indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
logits[indices_to_remove] = filter_value
if top_p > 0.0:
# Compute cumulative probabilities of sorted tokens
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probabilities = torch.cumsum(
F.softmax(sorted_logits, dim=-1), dim=-1
)
# Remove tokens with cumulative probability above the threshold
sorted_indices_to_remove = cumulative_probabilities > top_p
# Shift the indices to the right to keep also the first token above the threshold
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[
..., :-1
].clone()
sorted_indices_to_remove[..., 0] = 0
# Back to unsorted indices and set them to -infinity
indices_to_remove = sorted_indices[sorted_indices_to_remove]
logits[indices_to_remove] = filter_value
indices_to_remove = logits < threshold
logits[indices_to_remove] = filter_value
return logits
def sample_sequence(self, personality, history, tokenizer, model, args, current_output=None):
special_tokens_ids = tokenizer.convert_tokens_to_ids(SPECIAL_TOKENS)
if current_output is None:
current_output = []
for i in range(args.max_length):
instance = self.build_input_from_segments(
personality, history, current_output, tokenizer, with_eos=False
)
input_ids = torch.tensor(
instance["input_ids"], device=self.device
).unsqueeze(0)
token_type_ids = torch.tensor(
instance["token_type_ids"], device=self.device
).unsqueeze(0)
logits = model(input_ids, token_type_ids=token_type_ids)
logits = logits[0]
logits = logits[0, -1, :] / args.temperature
logits = self.top_filtering(logits, top_k=args.top_k, top_p=args.top_p)
probs = F.softmax(logits, dim=-1)
prev = (
torch.topk(probs, 1)[1]
if not args.do_sample
else torch.multinomial(probs, 1)
)
if i < args.min_length and prev.item() in special_tokens_ids:
while prev.item() in special_tokens_ids:
if probs.max().item() == 1:
warnings.warn(
"Warning: model generating special token with probability 1."
)
break # avoid infinitely looping over special token
prev = torch.multinomial(probs, num_samples=1)
if prev.item() in special_tokens_ids:
break
current_output.append(prev.item())
return current_output
def save_model_args(self, output_dir):
os.makedirs(output_dir, exist_ok=True)
self.args.save(output_dir)
def _load_model_args(self, input_dir):
args = ConvAIArgs()
args.load(input_dir)
return args
def get_named_parameters(self):
return [n for n, p in self.model.named_parameters()]
(2023/2/6 修正)
rinna/japanese-gpt-1bのvocab_sizeに対応するために以下のように変更しました。元々は、medium以下のモデルを想定していたので32000で固定していましたが、より大きなvocab_sizeを持つモデルに対応できるようにしました。(というより固定する意味もなかったですね。)
def add_special_tokens_(self, model, tokenizer):
# 変更前
# orig_num_tokens = 32000
# 変更後
orig_num_tokens = tokenizer.vocab_size
モデルを構築
今回は、rinna社が提供している事前学習モデル(rinna/japanese-gpt2-small)を使用します。rinna社が提供している他のGPT-2の事前学習モデルでも可能です。
詳しく知りたい方はこちらをご覧ください。
(2022/6/22 追記)rinna/japanese-gpt2-mediumでも学習できる場合がありました。データ量の問題?
学習の詳細を決める
# 加えられる情報の詳細はsimple transformersを見てください
train_args = {
"num_train_epochs": 10,
"save_model_every_epoch": False,
}
model=ConvAIModel("rinna/japanese-gpt2-small", args=train_args)
加えられる情報はこちらをご覧ください。(無理やり合わせたので、情報加えてエラー出たらすみません)
どれくらいhistoryを残すかなども設定できます。
学習を行う
# 学習を行います
model.train_model("data.json")
学習が終わるとoutputsフォルダができます。
対話をしてみよう!
以下のコードで対話ができます。
# outputsフォルダから読み込むこともできます
# model = ConvAIModel("outputs/")
# personalityを与えます
personality=[
"私の趣味は読書です。",
"私はガンダムが大好きです。"
]
# 対話を開始する
model.interact(personality=personality)
# こちらの方法でも対話ができます
history = []
reply, history = model.interact_single("あなたの趣味は何ですか?", history=history, personality=personality)
print(history)
何度か試してみて良いと思った対話結果がこちらです。
you -> よろしくね
bot -> よろしくね!
you -> 今日は何してたの?
bot -> 最近ハマってるガンダムの本を読んでたよ
you -> 小説?
bot -> そうだね、最近ハマってるのはこの手のsfかな
思考戦車タチコマを作ってみた
攻殻機動隊のタチコマを作ってみます。
personalityはこちらにしました。
# personalityを決める
personality=[
"僕の名前はタチコマだよ。",
"僕はいつも事件解決のお手伝いをしているよ。",
"僕は公安9課に所属しているよ。",
"僕はバトーさんの天然オイルが大好物だよ。"
]
結果はこちらになりました。
you -> 私の名前はzanです。
bot -> こんにちは。よろしくお願いします!
you -> あなたの名前は何ですか?
bot -> タチコマだよ。
you -> タチコマっていう名前なんだね。今は何かしてるの?
bot -> 刑事コロンボの犯人を追ってるから、犯人のことが忘れられないんだ。
you -> 大変なんだね。どこかに所属してるの?
bot -> 実は今、公安9課に勤めてるんだ。あなたは普段はどんな仕事をしているの?
ほうほう、刑事コロンボの犯人を追ってるんだね?
少し突っ込みたいところはありますが概ね満足いく結果になりました。
失敗例はこちらになります。
you -> あなたの好物は何?
bot -> バトーさんだよ。いつも事件を解決のお手伝いをしているよ。
最後に
今回はrinnaのGPT-2を用いてchatbotを作ってみました。historyの部分に「語尾をなっしーにする」と書くとその通りになるようにデータを用意してみても面白いと思います。また、他にも応用が利くと思うのでデータを変えて学習してみるのも面白いかも。
データについては、candidatesの部分を複数から一つにするなど工夫しています。simple transformersでは複数のcandidatesが用意されていました。
その他コードについては無理やり日本語版のGPT-2に合わせたりいろいろと削ったりしたので警告が多々出ると思いますが大目に見てください。また、間違えているところなどあればコメント、またはtwitterまでお願いします。
コメント
コメント一覧 (13件)
非常に面白い記事でした!
自分の環境で再現するにあたって質問したいことがあります。
記事で紹介されていた使用例を自分の環境で再現しようとしたのですが、
同じ返答が連続するなど変な挙動を見せました。
“`
you-> 私の名前はhogeです。
bot-> 私はバトーさんの魔法が好きだよ。 僕はバトーさんの魔法が好きだよ
——————————–
>>> あなたの名前は何ですか?
you-> あなたの名前は何ですか?
bot-> 僕はバトーさんの魔法が好きだよ。 僕はバトーさんの魔法が好き
——————————–
>>> 魔法って何?
you-> 魔法って何?
bot-> 僕はバトーさんの魔法が好きだ。 僕はバトーさんの魔法が好き
“`
zanさんはこういった現象に見舞われたことはありますか?
質問ありがとうございます。
私の場合も質問によっては同じような返答が連続することがありました。
データ量を増やしたり、少し工夫(話題が広がるような会話データを作るなど)したりすると、ある程度はこのような挙動が減少しました。
色々試してみてください!
すみません。
こちらの記事のコードを実行したいのですが、いろいろ分からず四苦八苦しています。
まず、model codeをgoole colaboratoryで実行した際に、
no module named transformersと表示され、それよりも先に進めずにいます。
また、途中からrun.ipynbでコードを実行していますが、当初googleドライブをマウントしたipynbからどう実行していますか?
できればこのあたりのやり方について、詳しく教えていただけるとありがたいです。
コメントありがとうございます。
no module named transformers
については、transformersというモジュールがないよというエラーなので、!pip install simpletransformers
を実行してsimpletransformersをインストールすることで一緒にインストールされて解決すると思います。実行する場所については、追加しているのでそちらを参考にしてください。途中からrun.ipynbにコードをコピペして実行するのではなく、すべてのコードをrun.ipynbにコピペして順番に実行して見てください。どのファイルで「フォルダを作成」のコードを実行するのかについて追加しています。
まだ何かエラーが出るようでしたらコメントをお願いします。
返信ありがとうございます。
ディレクトリを確認し、上から順にコードを実行してみたのですが、modelを実行した時に以下のようなエラーが表示され先に進みません。
simpletransformers,transformers双方にインポートができないものがあるようなのですが、原因がわからず、混乱しています。
ImportError Traceback (most recent call last)
in
—> 15 from simpletransformers.conv_ai.conv_ai_utils import get_dataset
2 frames
/usr/local/lib/python3.7/dist-packages/simpletransformers/conv_ai/conv_ai_utils.py in
—> 15 from transformers import cached_path
ImportError: cannot import name ‘cached_path’ from ‘transformers’ (/usr/local/lib/python3.7/dist-packages/transformers/__init__.py)
お手数おかけしますが、何が原因なのか、どのように直せば動作するか、ご教示いただけるとありがたいです。
勉強不足ですみません…
返信遅くなりました。
エラーについては、transformersのバージョンが関係しているようです。
どのバージョンを指定するのかについて追記しているので、参考にしてモデル作成して見てください。
初心者です
# 加えられる情報の詳細はsimple transformersを見てください
train_args = {
“num_train_epochs”: 10,
“save_model_every_epoch”: False,
}
model=ConvAIModel(“rinna/japanese-gpt2-small”, args=train_args)
で
TypeError Traceback (most recent call last)
in
4 “save_model_every_epoch”: False,
5 }
—-> 6 model=ConvAIModel(“rinna/japanese-gpt2-small”, args=train_args)
TypeError: __init__() missing 1 required positional argument: ‘model_name’
となります。何が悪いのでしょうか?
返信遅くなり申し訳ありません。
恐らくConvAIModel クラスが正しくインポートされていないものと思います。
もう一度、ConvAIModelを定義しているコードを実行して見てください。
よろしくお願いします。
わかりやすい丁寧な説明でとても分かりやすく、rinna/japanese-gpt2-small,rinna/japanese-gpt2-mediumの動作確認は取れたですが、japanese-gpt-1bを利用すると以下のエラーを吐き、うまく動作しなくなってしまいます。海外の方の解説等を参考に、バッチサイズの変更やドライババージョンの確認は行ったのですが、特に進展はありませんでした。
システムRAM及びGPURAMは50%以下程度しか使用されていません。
(実行環境:RAM:128GB GPU:A100{VRAM 40GB})
/usr/local/lib/python3.8/dist-packages/transformers/optimization.py:306: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
warnings.warn(
Epoch 0 of 12: 0%
0/12 [00:00<?, ?it/s]
Running Epoch 1 of 12: 0%
0/52 [00:00<?, ?it/s]
—————————————————————————
RuntimeError Traceback (most recent call last)
in
2 get_ipython().system(‘ls’)
3 # 学習を行います
—-> 4 model.train_model(“data.json”)
11 frames
/usr/local/lib/python3.8/dist-packages/transformers/pytorch_utils.py in forward(self, x)
107 def forward(self, x):
108 size_out = x.size()[:-1] + (self.nf,)
–> 109 x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight)
110 x = x.view(size_out)
111 return x
RuntimeError: CUDA error: CUBLAS_STATUS_NOT_INITIALIZED when calling `cublasCreate(handle)`
コメントありがとうございます。
モデルを少し修正しました。修正点については、モデル定義の下に書いています。
修正点を書き換えれば学習できるはずです。
よろしければ結果を教えて頂ければ幸いです。
よろしくお願いします。
返信ありがとうございます。
修正後のコードで無事学習へ進むことができました。
以降の問題は記事の趣旨とは多少ずれてしまう可能性があるのですが、1bの処理の為異常な量のVRAMが必要なため、GPU1枚での処理では無理があるようで、DDPを有効化しなければならないとのことで作業が止まっています。
もし、解決方法をご存じであればご教示いただければ幸いです。
以降エラーです。
UserWarning: Was asked to gather along dimension 0, but all input tensors were scalars; will instead unsqueeze and return a vector. warnings.warn(‘Was asked to gather along dimension 0, but all ‘
追記.マルチGPUの件解決いたしました。
単純にメインとなるGPU0のVRAMが不足していました。
今回の学習で分かった1b用のファインチューニング必要スペックを載せておきます。
GPU0:VRAM最低35GB(ファインチューニング中に平均して33GB程度消費していました)
GPU1以降の並列実行用GPU:VRAM最低11GB
=======================
以下1b学習成功した検証環境です。
システムRAM:128GB
GPU:RTX A6000(VRAM:48GB)を8台並列実行
ご報告ありがとうございます。
環境についても詳細ありがとうございます。