こんにちは、takeshiho0531です。
これの続きの記事です。
Tweet-Classifierとはなんぞやいという方は↑の初めの何行かを読んでいただければという感じです。
この記事では、集めたツイートを「バズ」「炎上」「その他」にカテゴライズして、これからそれを学習させようという記事です。
このパートは『つくりながら学ぶ!PyTorchによる発展ディープラーニング』と、その著者である小川雄太郎氏がご自身のgithubで公開されている、
github.com
を真似してやってみました。
. . . . . . . . . . . . . . . . . . . . .
元々は『つくりながら学ぶ!PyTorchによる発展ディープラーニング』にあった、BERTを使ってIMDbの感情分析モデルを構築するという部分を自分の母語である日本語でやってみたいと思い、このTweet-Classifierを作ってみようと思った感じでした。
ただこの本で学習した時点でtorchtextの新しいバージョンではtorchtext.data.Fieldが使えなくなっていて、いろいろ環境構築でグズグズした結果、ローカルでは仮想環境ができたものの、AWSのGPUインスタンスでは元々入っているPyTorchのバージョンを再インストールして変更することがなぜかできなくて、GPUインスタンスで学習させるというところまで到達できずにいました。(AWSのGPUインスタンスがどうこうという話は 『PyTorchによる発展ディープラーニング』1-4 AWSのAMIの選び方 - takeshiho0531のブログ に書きました。)
. . . . . . . . . . . . . . . . . . . . .
今回、Dockerの使い方を学んだので、元々condaとかPyTorchとかが入っているものではなくて、普通のubuntuサーバーのモデルを選んでやってみようと思いました。AWS Marketplace: Ubuntu Server 20.04 LTS (HVM), SSD Volume with Support by Terracloudxのp2.xlargeを選びました。このセクションの以下の内容は今まで一度もやったことがないことだったので一つ一つのステップを調べながら失敗しながら進めていきましたが、すごく精神的に疲弊しました。
まずNvidia Driverのインストールですが、ドキュメントをそのままコピペしてインスタンスにインストールしました。
docs.nvidia.com
ただこの通りにやったのですが、nvidia-smiと打っても
"NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver. Make sure that the latest NVIDIA driver is installed and running"
と出てしまいました。エラーメッセージの意味もわからなかったので、ここはググって
jskangaroo.hatenablog.com
を参考にすると、このブログを書いた方と同じタイミングでnvidia-smiに反応してくれるようになりました。
次にNvidia Dockerのインストールですが、github(GitHub - NVIDIA/nvidia-docker: Build and run Docker containers leveraging NVIDIA GPUs)に説明があり、そこにリンクがあった
docs.nvidia.com
を見ながらインストールしました。
その後、自分のgithubレポジトリをインスタンスにクローンする形でDockerfileをインスタンス内に入れて、そこからDocker imageをbuildし、コンテナを立てました。学習はJupyter notebookを使いながらやろうかなと思っていたので、Dockerfileは↓みたいな感じです。(githubにも上げていますが。)
FROM nvidia/cuda:11.3.0-cudnn8-runtime-ubuntu20.04 RUN apt-get update && apt-get install -y \ sudo \ wget \ vim WORKDIR /opt RUN wget https://repo.anaconda.com/archive/Anaconda3-2021.11-Linux-x86_64.sh && \ bash Anaconda3-2021.11-Linux-x86_64.sh -b -p /opt/anaconda3 && \ rm -f Anaconda3-2021.11-Linux-x86_64.sh ENV PATH /opt/anaconda3/bin:$PATH RUN pip install --upgrade pip && pip install \ torch==1.8.1 \ torchtext==0.9.1 \ transformers[ja] \ unidic-lite WORKDIR / CMD ["jupyter","lab","--ip=0.0.0.0","--allow-root","--LabApp.token=''"]
cudaのバージョンとかは、使いたいtorchtextのバージョンがある程度決まっていたので、Previous PyTorch Versions | PyTorchを見て、元となるDocker imageを決めました。どのanacondaをインストールするかは、Old package lists — Anaconda documentation を見ながら使いたいバージョンのtorchtextがあるものを選びました。
いざコンテナを立ててブラウザでJupyter notebookを開こうと思ったのですが、できませんでした。コンテナとインスタンスの通信はできてるみたいでしたが、ローカルとインスタンスでは繋がっていない??みたいな感じでできませんでした。確か "Connection refused"と言われた気がします。(ログとってませんでした.....)
ここでJupyter notebookをやめて普通にpythonファイルにしようかとも思ったのですが、イライラしていてかなり疲弊していたせいか、うまくいくとわかっているipynbファイルがもうできていたので書き換える気力が全くなくなってしまっていて(ほとんどコピペなのに??)やりませんでした。(多分ログを出すようにするのが面倒だと感じたんだと思います。)ものすごく疲れてしまっていたので、嫌になってインスタンスもさっさと消してしまいました。
. . . . . . . . . . . . . . . . . . . . .
だいぶん精神的に追い詰められてしまっていたので、別のマシンイメージを探し始めました。今まではクイックスタートAMIかAWS Marketplace AMIにしか手を出したことがなかったのですが、古いバージョンのubuntuとかはコミュニティAMIに移されているんだということに気づきました。そこで、Deep Learning AMI GPU PyTorch 1.9.0 (Ubuntu 20.04) 20211007 というAMIを選びました。このタイプならNvidia DriverとかNvidia Dockerとか考えなくても良いし、conda環境で欲しいバージョンのtorchtextも入れられるバージョンで、何よりも使ったことがある感じのものなので、学びという観点では△ですが課題を遂行しないといけないのですがる思いで選びました。いつも通りという感じで問題なく使えました。
ただ、学習にはすごく時間がかかるのですが、すぐにsshの通信が切れてしまい学習もそこで終わってしまうという問題点がありました。この点は
qiita.com
を見ながら10時間接続できるようにしました。(実際は5時間で終わりましたが。)
. . . . . . . . . . . . . . . . . . . . .
では学習とは一体何をしたのかというのを書きます。
上でも書きましたが、『つくりながら学ぶ!PyTorchによる発展ディープラーニング』にあった、BERTを使ってIMDbの感情分析モデルを構築を日本語でもやりたいと思って始めました。
日本語版のBERTは東北大学の出しているBERTを使いました。HuggingfaceのTransformersから簡単に使うことができます。
東北大BERTの出力を最後3つ(バズ・炎上・その他)にする層を接続させて、自前のTwitterのデータを用いてファインチューニングしました。
Attentionの結果は、BertLayerの最終モジュールのSelf-Attentionの結果を取ってくることにしました。
コードは
github.com
qiita.com
の二つを主に参考・真似させてもらいました。上の方は『つくりながら学ぶ!PyTorchによる発展ディープラーニング』の著者のgithubにあったのをたまたま見つけて真似させてもらったのですが、Attentionの可視化部分がなかったので下の方も参考にさせていただきました。Attentionの可視化部分に関してはHuggingfaceのドキュメントも使いました。
huggingface.co
一応以下のようなコードで学習させました。
# 乱数シードの固定 import os import random import numpy as np import torch import pandas as pd SEED_VALUE = 1234 # これはなんでも良い os.environ['PYTHONHASHSEED'] = str(SEED_VALUE) random.seed(SEED_VALUE) np.random.seed(SEED_VALUE) torch.manual_seed(SEED_VALUE) # PyTorchを使う場合
# GPUの使用確認:True or False
torch.cuda.is_available()
import torch import torchtext
# transformers等のライブラリが'./.local/lib/python3.8/site-packages'にインストールされたので。 import sys sys.path.append('./.local/lib/python3.8/site-packages')
import transformers from transformers import BertModel from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
# データを読み込んだときに、読み込んだ内容に対して行う処理 max_length = 512 # 東北大学_日本語版の最大の単語数(サブワード数)は512 def tokenizer_512(input_text): """torchtextのtokenizerとして扱えるように、512単語のpytorchでのencodeを定義。ここで[0]を指定し忘れないように""" return tokenizer.encode(input_text, max_length=512, return_tensors='pt')[0] TEXT = torchtext.legacy.data.Field(sequential=True, tokenize=tokenizer_512, use_vocab=False, lower=False, include_lengths=True, batch_first=True, fix_length=max_length, pad_token=0) LABEL = torchtext.legacy.data.Field(sequential=False, use_vocab=False)
# dataset dataset_train_eval, dataset_test = torchtext.legacy.data.TabularDataset.splits( path='.', train='train_eval.tsv', test='test.tsv', format='tsv', fields=[('Text', TEXT), ('Label', LABEL)]) # torchtext.data.Datasetのsplit関数で訓練データと検証データを分ける dataset_train, dataset_eval = dataset_train_eval.split( split_ratio=1.0 - 3401/13606, random_state=random.seed(1234))
# dataloader batch_size = 16 dl_train = torchtext.legacy.data.Iterator( dataset_train, batch_size=batch_size, train=True) dl_eval = torchtext.legacy.data.Iterator( dataset_eval, batch_size=batch_size, train=False, sort=False) dl_test = torchtext.legacy.data.Iterator( dataset_test, batch_size=batch_size, train=False, sort=False) # 辞書オブジェクトにまとめる dataloaders_dict = {"train": dl_train, "val": dl_eval}
# BERTモデル from transformers import BertModel model= BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking', output_attentions=True)
from torch import nn class BertForTweetClassifier(nn.Module): '''BERTモデルに3クラスを判定する部分をつなげたモデル''' def __init__(self): super(BertForLivedoor, self).__init__() # BERTモジュール self.bert = model # 日本語学習済みのBERTモデル # headにポジネガ予測を追加 # 入力はBERTの出力特徴量の次元768、出力は3クラス self.cls = nn.Linear(in_features=768, out_features=3) # 重み初期化処理 nn.init.normal_(self.cls.weight, std=0.02) nn.init.normal_(self.cls.bias, 0) def forward(self, input_ids): ''' input_ids: [batch_size, sequence_length]の文章の単語IDの羅列 ''' # BERTの基本モデル部分の順伝搬 # 順伝搬させる result = self.bert(input_ids) # reult は、sequence_output, pooled_output attentions = result['attentions'] # sequence_outputの先頭の単語ベクトルを抜き出す vec_0 = result[0] # 最初の0がsequence_outputを示す vec_0 = vec_0[:, 0, :] # 全バッチ。先頭0番目の単語の全768要素 vec_0 = vec_0.view(-1, 768) # sizeを[batch_size, hidden_size]に変換 output = self.cls(vec_0) # 全結合層 return output, attentions
# モデル構築 net = BertForTweetClassifier() # 訓練モードに設定 net.train()
# ファインチューニングの設定 # 勾配計算を最後のBertLayerモジュールと追加した分類アダプターのみ実行 # 1. まず全部を、勾配計算Falseにしてしまう for param in net.parameters(): param.requires_grad = False # 2. BertLayerモジュールの最後を勾配計算ありに変更 for param in net.bert.encoder.layer[-1].parameters(): param.requires_grad = True # 3. 識別器を勾配計算ありに変更 for param in net.cls.parameters(): param.requires_grad = True # 最適化手法の設定 import torch.optim as optim # BERTの元の部分はファインチューニング optimizer = optim.Adam([ {'params': net.bert.encoder.layer[-1].parameters(), 'lr': 5e-5}, {'params': net.cls.parameters(), 'lr': 1e-4} ]) # 損失関数の設定 criterion = nn.CrossEntropyLoss()
# モデルを学習させる関数を作成 def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): # GPUが使えるかを確認 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print("使用デバイス:", device) print('-----start-------') # ネットワークをGPUへ net.to(device) # ネットワークがある程度固定であれば、高速化させる torch.backends.cudnn.benchmark = True # ミニバッチのサイズ batch_size = dataloaders_dict["train"].batch_size # 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の正解数 iteration = 1 logs=[] # データローダーからミニバッチを取り出すループ for batch in (dataloaders_dict[phase]): # batchはTextとLableの辞書型変数 # GPUが使えるならGPUにデータを送る inputs = batch.Text[0].to(device) # 文章 labels = batch.Label.to(device) # ラベル # optimizerを初期化 optimizer.zero_grad() # 順伝搬(forward)計算 with torch.set_grad_enabled(phase == 'train'): # BERTに入力 outputs, attentions = net(inputs) loss = criterion(outputs, labels) # 損失を計算 _, preds = torch.max(outputs, 1) # ラベルを予測 # 訓練時はバックプロパゲーション if phase == 'train': loss.backward() optimizer.step() if (iteration % 10 == 0): # 10iterに1度、lossを表示 acc = (torch.sum(preds == labels.data) ).double()/batch_size print('イテレーション {} || Loss: {:.4f} || 10iter. || 本イテレーションの正解率:{}'.format( iteration, loss.item(), acc)) iteration += 1 # 損失と正解数の合計を更新 epoch_loss += loss.item() * batch_size 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) print('Epoch {}/{} | {:^5} | Loss: {:.4f} Acc: {:.4f}'.format(epoch+1, num_epochs, phase, epoch_loss, epoch_acc)) #logを保存 log_epoch={"epoch":epoch+1,"Loss":epoch_loss, "Acc":epoch_acc} logs.append(log_epoch) df=pd.DataFrame(logs) df.to_csv("log_output.csv") torch.save(net.state_dict(), "./save_model/model_"+str(epoch+1)+".pth") return net
num_epochs = 20
train_model(net, dataloaders_dict,
criterion, optimizer, num_epochs=num_epochs)
最後のepochの学習状況が以下です。

結果です。

正解率がなんか全然初めのepochから上がってなくて最終的にも0.6くらいなのがすごくすごく不穏でしたが、何が原因だったのでしょう....とりあえず課題遂行のために見なかったことにしましたが大丈夫ではないだろというね。
. . . . . . . . . . . . . . . . . . . . .
「2. 学習・検証」の部分での成長ポイントは、Nvidia Driver・Nvidia Dockerのインストールをしてみたことを使ってみたこと、Huggingfaceを使ってみたこと、何よりもやってみたかった日本語でのモデル構築ができたことかなと思っています。(ファインチューニング済みパラメータを得られたこと課題の進捗を埋めたことでだったかもしれないが。)改善ポイントは、なぜJupyter Notebookが開けなかったのかという部分と、Jupyter Notebook使わずにpythonファイルを実行させてみればよかったのでは?というところと、なぜ正解率が上がらなかったのかという部分かなと思っています。
まあでも精神的にすごく疲弊してしまった中頑張ったなと思います。
この記事はこれくらいにしようかな。「3. UI周り」に関しては次の記事にしようと思います。長くなってしまってごめんなさい。
ではでは〜👋👋