morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】言語処理100本ノック2020を解く(第9章 問題89)


はじめに

前回の続きで言語処理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章は少しわけわからないのでちょっと離れます 笑