はじめに
前回の続きで言語処理100本ノック解いていきたいと思います。
今回は第9章の最後の問題です。
9章は非常に盛りだくさんなので3部に分けて、そのうちの最後です。
最後は少し重そうだったので分けました。
前回はRNNやCNNと呼ばれるモデルを使用していきましたが、今回は事前学習済み言語モデルのBERTを使用してみます。
第9章: RNNとCNN
深層学習フレームワークを用い,再帰型ニューラルネットワーク(RNN)や畳み込みニューラルネットワーク(CNN)を実装します.
89. 事前学習済み言語モデルからの転移学習
事前学習済み言語モデル(例えばBERTなど)を出発点として,ニュース記事見出しをカテゴリに分類するモデルを構築せよ.
データの準備
まずは9章の最初のほうでやったのと同じようにデータの前処理を行います
# データのロード import pandas as pd import re import numpy as np import random import transformers import torch from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizer, BertModel from torch import optim from torch import cuda from torch import nn from matplotlib import pyplot as plt # 乱数のシードを設定 # parserなどで指定 seed = 1234 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.backends.cudnn.benchmark = False torch.backends.cudnn.deterministic = True def seed_worker(worker_id): worker_seed = torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed) g = torch.Generator() g.manual_seed(seed) # ファイル読み込み file = './input/section6/newsCorpora.csv' data = pd.read_csv(file, encoding='utf-8', header=None, sep='\t', names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP']) data = data.replace('"', "'") # 特定のpublisherのみ抽出 publishers = ['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail'] data = data.loc[data['PUBLISHER'].isin(publishers), ['TITLE', 'CATEGORY']].reset_index(drop=True) # 前処理 def preprocessing(text): text_clean = re.sub(r'[\"\'.,:;\(\)#\|\*\+\!\?#$%&/\]\[\{\}]', '', text) text_clean = re.sub('[0-9]+', '0', text_clean) text_clean = re.sub('\s-\s', ' ', text_clean) return text_clean data['TITLE'] = data['TITLE'].apply(preprocessing) # 学習用、検証用、評価用に分割する from sklearn.model_selection import train_test_split train, valid_test = train_test_split(data, test_size=0.2, shuffle=True, random_state=64, stratify=data['CATEGORY']) valid, test = train_test_split(valid_test, test_size=0.5, shuffle=True, random_state=64, stratify=valid_test['CATEGORY']) train = train.reset_index(drop=True) valid = valid.reset_index(drop=True) test = test.reset_index(drop=True) # データ数の確認 print('学習データ') print(train['CATEGORY'].value_counts()) print('検証データ') print(valid['CATEGORY'].value_counts()) print('評価データ') print(test['CATEGORY'].value_counts()) # ターゲットのテンソル化 category_dict = {'b': 0, 't': 1, 'e':2, 'm':3} Y_train = torch.from_numpy(train['CATEGORY'].map(category_dict).values) Y_valid = torch.from_numpy(valid['CATEGORY'].map(category_dict).values) Y_test = torch.from_numpy(test['CATEGORY'].map(category_dict).values)
出力は以下。
学習データ b 4502 e 4223 t 1219 m 728 Name: CATEGORY, dtype: int64 検証データ b 562 e 528 t 153 m 91 Name: CATEGORY, dtype: int64 評価データ b 563 e 528 t 152 m 91 Name: CATEGORY, dtype: int64
BERTモデルで読み取れるように入力の文章をトークン化します。
BertTokenizerを使うことで変換できます。
今回使用するアウトプットは以下の2種類
- input_ids:トークンのid(整数)
- attention_mask:トークンの場合1、トークンでない場合0
tokenizerをもちいてデータセットを作ります。
class BERTDataSet(Dataset): def __init__(self, X, y, phase): self.X = X['TITLE'] self.y = y self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') self.phase = phase def __len__(self): return len(self.y) def __getitem__(self,idx): sentence = self.X[idx] sentence = str(sentence) sentence = " ".join(sentence.split()) bert_sens = self.tokenizer.encode_plus( sentence, add_special_tokens = True, # [CLS],[SEP] max_length = 20, pad_to_max_length = True, # add padding to blank truncation=True) ids = torch.tensor(bert_sens['input_ids'], dtype=torch.long) mask = torch.tensor(bert_sens['attention_mask'], dtype=torch.long) labels = self.y[idx] return { 'ids': ids, 'mask': mask, 'labels': labels, } train_dataset = BERTDataSet(train, Y_train, phase='train') valid_dataset = BERTDataSet(valid, Y_valid, phase='val') test_dataset = BERTDataSet(test, Y_test, phase='val') # 動作確認 train_dataset[0]
出力は以下。
{'ids': tensor([ 101, 6796, 12170, 22669, 2104, 4812, 2005, 4692, 13742, 2012, 4913, 18396, 2015, 102, 0, 0, 0, 0, 0, 0]), 'mask': tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]), 'labels': tensor(2)}
モデルでの学習、予測用のデータローダーを作ります
# DataLoaderを作成 batch_size = 64 train_dataloader = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, worker_init_fn=seed_worker, generator=g) valid_dataloader = DataLoader( valid_dataset, batch_size=batch_size, shuffle=False, worker_init_fn=seed_worker, generator=g) test_dataloader = DataLoader( test_dataset, batch_size=batch_size, shuffle=False, worker_init_fn=seed_worker, generator=g) dataloaders_dict = {'train': train_dataloader, 'val': valid_dataloader, 'test': test_dataloader, } # 動作確認 batch_iter = iter(dataloaders_dict['train']) inputs = next(batch_iter) print(inputs['ids'].size()) print(inputs['mask'].size()) print(inputs['labels'].size())
torch.Size([64, 20]) torch.Size([64, 20]) torch.Size([64])
モデルの構築と学習
BERTモデルを使って文章分類モデルを作ります
中間の全結合層とその前後にドロップアウト層とバッチノーマライゼーション層を追加しておきます。
# BERT分類モデルの定義 class BERTClass(torch.nn.Module): def __init__(self, drop_rate, hidden_size, output_size): super().__init__() self.bert = BertModel.from_pretrained('bert-base-uncased') self.drop = nn.Dropout(drop_rate) self.fc = nn.Sequential( nn.Linear(768, hidden_size), nn.ReLU(), nn.BatchNorm1d(hidden_size), nn.Linear(hidden_size, output_size) ) # self.fc = nn.Linear(768, output_size) # BERTの出力に合わせて768次元を指定 def forward(self, ids, mask): out = self.bert(ids, attention_mask=mask)[-1] out = self.fc(self.drop(out)) return out
学習させて予測してみます。
訓練、予測用関数はほぼ使いまわしなので説明は割愛します。
# 学習用の関数を定義 def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): # 初期設定 # GPUが使えるか確認 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print(torch.cuda.get_device_name()) print("使用デバイス:", device) # ネットワークをgpuへ net.to(device) train_loss = [] train_acc = [] valid_loss = [] valid_acc = [] # epochのループ for epoch in range(num_epochs): # epochごとの学習と検証のループ for phase in ['train', 'val']: if phase == 'train': net.train() # 訓練モード else: net.eval() # 検証モード epoch_loss = 0.0 # epochの損失和 epoch_corrects = 0 # epochの正解数 # データローダーからミニバッチを取り出すループ for data in dataloaders_dict[phase]: # GPUが使えるならGPUにおくる ids = data['ids'].to(device) mask = data['mask'].to(device) labels = data['labels'].to(device) optimizer.zero_grad() # optimizerを初期化 # 順伝播計算(forward) with torch.set_grad_enabled(phase == 'train'): outputs = net(ids, mask) loss = criterion(outputs, labels) # 損失を計算 _, preds = torch.max(outputs, 1) # ラベルを予想 # 訓練時は逆伝播 if phase == 'train': loss.backward() optimizer.step() # イテレーション結果の計算 # lossの合計を更新 epoch_loss += loss.item() * ids.size(0) # 正解数の合計を更新 epoch_corrects += torch.sum(preds == labels.data) # epochごとのlossと正解率の表示 epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) if phase == 'train': train_loss.append(epoch_loss) train_acc.append(epoch_acc.cpu()) else: valid_loss.append(epoch_loss) valid_acc.append(epoch_acc.cpu()) print('Epoch {} / {} (train) Loss: {:.4f}, Acc: {:.4f}, (val) Loss: {:.4f}, Acc: {:.4f}'.format(epoch + 1, num_epochs, train_loss[-1], train_acc[-1], valid_loss[-1], valid_acc[-1])) return train_loss, train_acc, valid_loss, valid_acc # パラメータの設定 DROP_RATE = 0.2 HIDDEN_SIZE = 256 OUTPUT_SIZE = 4 BATCH_SIZE = 64 NUM_EPOCHS = 8 LEARNING_RATE = 1e-5 # モデルの定義 net = BERTClass(DROP_RATE, HIDDEN_SIZE, OUTPUT_SIZE) net.train() # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.AdamW(params=net.parameters(), lr=LEARNING_RATE) train_loss, train_acc, valid_loss, valid_acc = train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=NUM_EPOCHS)
学習の推移は以下の通りです。
NVIDIA GeForce GTX 1660 使用デバイス: cuda:0 Epoch 1 / 8 (train) Loss: 0.6397, Acc: 0.7679, (val) Loss: 0.2881, Acc: 0.9093 Epoch 2 / 8 (train) Loss: 0.2752, Acc: 0.9168, (val) Loss: 0.2041, Acc: 0.9370 Epoch 3 / 8 (train) Loss: 0.1911, Acc: 0.9430, (val) Loss: 0.2073, Acc: 0.9340 Epoch 4 / 8 (train) Loss: 0.1422, Acc: 0.9603, (val) Loss: 0.1903, Acc: 0.9408 Epoch 5 / 8 (train) Loss: 0.1099, Acc: 0.9690, (val) Loss: 0.2004, Acc: 0.9445 Epoch 6 / 8 (train) Loss: 0.0799, Acc: 0.9795, (val) Loss: 0.1938, Acc: 0.9415 Epoch 7 / 8 (train) Loss: 0.0634, Acc: 0.9848, (val) Loss: 0.2208, Acc: 0.9430 Epoch 8 / 8 (train) Loss: 0.0503, Acc: 0.9882, (val) Loss: 0.2290, Acc: 0.9445
予測結果はこちら
学習データの正解率: 0.9963 検証データの正解率: 0.9445 テストデータの正解率: 0.9378
ほぼほぼいじってませんが、これまでで一番いい結果が出ましたね。
自然言語処理におけるBERTモデルの有効さがよくわかります。
おわりに
今回はBERTモデルを使用した文章分類の予測をおこないました。
ライブラリで簡単に実装できるのでありがたいですね。
ようやく9章終わりです。とりあえず9章までやってみてかなり言語処理に対応する力がついたと実感してます。
難しいことも多いですが、一度チャレンジしてみてください。
10章は少しわけわからないのでちょっと離れます 笑