この前は学習済みのBERTをから取り出したEmbeddigを使ってLightGBMに突っ込んでみるところまでやってみました。 その時は特にタスク個別にBERTを学習させていなかったので、今回はタスク向けに転移学習させたBERTをモデルを使用して、そのEmbeddingをLightGBMに突っ込んでみたいと思います。
やりたいこと
イメージはこんな感じです。 前回は提供されるままのBERTの値を取得しているので、含意関係認識のような個別のタスク向けのEmbeddingが生成されているとは考えにくいです。 そこで、含意関係認識用にBERTを追加で学習してしまって、その出力を使用すれば精度が上がりそうな気がします。
イメージはこんな感じです。
やってみる
実際やってみるとこんな感じです。
実装は次の3つを参考にしてやっています。
BERTの特徴量の部分で、こんな感じです。
from transformers import BertJapaneseTokenizer, BertForSequenceClassification, BertForSequenceClassification, AdamW, BertConfig import pandas as pd import numpy as np import torch from torch.utils.data import TensorDataset, random_split from torch.utils.data import DataLoader, RandomSampler, SequentialSampler from mlflow import log_metric, log_param, log_artifact class FeaturePretrainedBert: def __init__(self): super().__init__() self.tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking') self.model = BertForSequenceClassification.from_pretrained( "cl-tohoku/bert-base-japanese-whole-word-masking", # 日本語Pre trainedモデルの指定 num_labels = 2, # ラベル数(今回はBinayなので2、数値を増やせばマルチラベルも対応可) output_attentions = False, # アテンションベクトルを出力するか output_hidden_states = True, # 隠れ層を出力するか ) def max_len(self, primary_texts, secondary_texts): # 最大単語数の確認 max_len = [] # 1文づつ処理 for sent1, sent2 in zip(primary_texts, secondary_texts): token_words_1 = self.tokenizer.tokenize(sent1) token_words_2 = self.tokenizer.tokenize(sent2) token_words_1.extend(token_words_2) # 文章数を取得してリストへ格納 max_len.append(len(token_words_1)) max_length = max(max_len) + 3 # 最大単語数にSpecial token([CLS], [SEP])の+3をした値が最大単語数 # 最大の値を確認 return max_length def encode(self, primary_texts, secondary_texts, max_len): input_ids = [] attention_masks = [] # 1文づつ処理 for x , y in zip(primary_texts, secondary_texts): sent= x + "[SEP]" + y encoded_dict = self.tokenizer.encode_plus( sent, add_special_tokens = True, # Special Tokenの追加 max_length = max_len, # 文章の長さを固定(Padding/Trancatinating) pad_to_max_length = True,# PADDINGで埋める return_attention_mask = True, # Attention maksの作成 return_tensors = 'pt', # Pytorch tensorsで返す ) # 単語IDを取得 input_ids.append(encoded_dict['input_ids']) # Attention maskの取得 attention_masks.append(encoded_dict['attention_mask']) # リストに入ったtensorを縦方向(dim=0)へ結合 input_ids = torch.cat(input_ids, dim=0) attention_masks = torch.cat(attention_masks, dim=0) return input_ids, attention_masks def train(self, primary_texts, secondary_texts, labels): max_len = self.max_len(primary_texts, secondary_texts) input_ids, attention_masks = self.encode(primary_texts, secondary_texts, max_len) # tenosor型に変換 labels = torch.tensor(labels) # データセットクラスの作成 train_dataset = TensorDataset(input_ids, attention_masks, labels) # データローダーの作成 batch_size = 32 log_param("batch_size", batch_size) # 訓練データローダー train_dataloader = DataLoader( train_dataset, sampler = RandomSampler(train_dataset), # ランダムにデータを取得してバッチ化 batch_size = batch_size ) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 最適化手法の設定 lr = 2e-5 log_param("lr", lr) optimizer = AdamW(self.model.parameters(), lr=lr) # 学習の実行 max_epoch = 50 log_param("max_epoch", max_epoch) for epoch in range(max_epoch): self.model.train() # 訓練モードで実行 train_loss = 0 for batch in train_dataloader:# train_dataloaderはword_id, mask, labelを出力する点に注意 b_input_ids = batch[0].to(device) b_input_mask = batch[1].to(device) b_labels = batch[2].to(device) optimizer.zero_grad() loss, logits, _ = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels) loss.backward() torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0) optimizer.step() train_loss += loss.item() log_metric("train_loss", train_loss, step=epoch) dir_name = './model/' self.tokenizer.save_pretrained(dir_name) self.model.save_pretrained(dir_name) return def get_embedding(self, text: str): tokenizer = BertJapaneseTokenizer.from_pretrained('./model/') model = BertForSequenceClassification.from_pretrained("./model/") model.eval() tokenized_text = tokenizer.tokenize(text) indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text) tokens_tensor = torch.tensor([indexed_tokens]) with torch.no_grad(): # 勾配計算なし all_encoder_layers = model(tokens_tensor) embedding = all_encoder_layers[1][-2].numpy()[0] result = np.mean(embedding, axis=0) return result def get(self, primary_text: str, secondary_text: str): text = primary_text + "[SEP]" + secondary_text f8 = self.get_embedding(text) return f8
その他、ワークフローはBERTの学習が必要になることに伴って学習・テストデータの分割の関係で結構書き換えました。 Metaflowのきれいな書き方が未だによくわからないですが、そのへんはまた別の機会に勉強してみたいと思います。
結果
こんな感じになりました。
precision recall f1-score support 0 0.5147 0.3933 0.4459 89 1 0.7000 0.7925 0.7434 159 accuracy 0.6492 248 macro avg 0.6074 0.5929 0.5946 248 weighted avg 0.6335 0.6492 0.6366 248
気持ち上がったくらいでしょうか?多分、個数としては1件正解が増えたとかそんなもんです。 学習過程のブレもあると思いますので、正直全く変わってないといったところでしょうか。
使ったコード
あまり大した進捗ではないですが、今回作った残骸はこちらに残しておきます。
感想
とりあえず思いついたのでやってみたって感じです。 思ったより結果が出なくて残念ですが、単体のBERTの方が良い結果が出るってことなんですかね。
よくわかりませんね…