Llama-2が登場!8bit+LoRAでRLHFファインチューニングを試す方法はこちら

【Python】rinnaのGPT-2を使って個性を持つ会話の流れを保持して会話できるAI(chatbot)を作ってみた!おまけでタチコマを再現してみた

Dialogptやblenderbotを使ってボットを作りたいと思っても日本語の事前学習モデルが公開されていないのでなかなか個人には難しいですよね。そこで今回は、rinna社が公開している事前学習モデルを用いて誰でもできる方法を記事にしました。

  • 雑談ボットを作ってみたい!
  • 個性を持たせたボットを作りたい!
  • 文脈を保持した会話を話せるようにさせたい!
  • ある程度話すことを限定させたい!
  • 実行環境を整えるのがめんどくさい!

この記事は以上のような方向けとなっています。

今回はsimple transformersのConversational AIを参考にしています。

simple transformersのgithubはこちらです。

transformerってなんだ?という方はこちらをおすすめします。

かなり分かりやすいです。

今から解説するチャットボットの作成方法ではない方法も試したい方はこちらもご覧ください。

あわせて読みたい
日本語版LIMAデータセットでopen-calm-7bをLoRAファインチューニングしてチャットボットを作成してみた 現代の自然言語処理(NLP)タスクにおいて、事前学習モデルは非常に重要な役割を果たしています。これらのモデルは、大量のテキストデータを使用してトレーニングされ、...

(2023/07/20追記)

「Llama-2」が公開されました!

あわせて読みたい
【Llama-2】8bit+LoRAでRLHFファインチューニングを試す【学習できることは確認】 Llama-2が出たのでRLHFを試してみました。 事前学習モデルでは教師ありファインチューニングをしてから行う必要がありますが、すでに調整されているモデルが公開されて...
目次

実行環境

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
あわせて読みたい
【対処法】ImportError: cannot import name ‘cached_path’ from ‘transformers&#821... import cached_path from transformersとするとエラーが出ました。 エラーが出たのは、次の記事です。 https://zanote.net/ai/chatbot1/ 意外とこのエラーになる人がい...

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の事前学習モデルでも可能です。

詳しく知りたい方はこちらをご覧ください。

あわせて読みたい
rinna社が大規模な13億ものパラメータを持つGPTを公開!これまでのモデルサイズと性能は違うのか?zero ... これまでもGPT-2やBERTの事前学習モデルがrinna社より公開されていますが、さらに大規模なモデルが公開されたということで性能が気になります。こちらにGPTに入力するテキストを工夫することで文章生成、分類、翻訳などの出力を得ることができると書かれているので、zero shot, one shotぽいことをして性能を検証してみようと思います。

(2022/6/22 追記)rinna/japanese-gpt2-mediumでも学習できる場合がありました。データ量の問題?

オープンな超巨大言語モデルが登場しました!
日本語は学習データに使われていませんが、今後が楽しみですね。

あわせて読みたい
【BLOOM】1760億パラメータを持つ多言語モデルの性能・使い方を調べてみた! 「BLOOM」とは、産業規模の計算リソースを使用して大量のテキストデータを用いて学習した多言語モデルです。 人間が書いたテキストとほとんど区別できない46の自然言語と13のプログラミング言語の一貫したテキストを出力することができます。 また、GPT-3でも見られたような明示的に学習していないタスクについてもテキスト生成タスクとして投げかけることで、その実行を指示することができます。

学習の詳細を決める

# 加えられる情報の詳細は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までお願いします。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメント一覧 (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台並列実行

          • ご報告ありがとうございます。
            環境についても詳細ありがとうございます。

さんでぃー へ返信する コメントをキャンセル

目次