チュートリアルセンチメント分析のためのBERTの微調整
チュートリアルセンチメント分析のためのBERTの微調整
原文はSkim AIの機械学習研究者、クリス・トラン。
A - はじめに¶
近年、NLPコミュニティは自然言語処理、特に転移学習への移行において多くのブレークスルーを見てきた。ELMo、fast.aiのULMFiT、Transformer、OpenAIのGPTのようなモデルは、研究者が複数のベンチマークで最先端の結果を達成することを可能にし、高性能で大規模な事前学習済みモデルをコミュニティに提供した。NLPにおけるこのシフトは、NLPのImageNetの瞬間と見なされている。数年前のコンピュータビジョンにおけるシフトでは、特定のタスクで訓練された数百万のパラメーターを持つディープラーニングネットワークの低レイヤーを、ゼロから新しいネットワークを訓練するのではなく、他のタスクのために再利用し、微調整することができた。
最近のNLPの進化における最も大きなマイルストーンの一つは、GoogleのBERTのリリースであり、これはNLPの新しい時代の始まりと言われている。このノートブックでは、HuggingFaceの 変圧器
ライブラリを使用して、分類タスク用に事前訓練された BERT モデルを微調整する。次に、BERT の性能を、TF-IDF ベクタライザとナイーブ・ベイズ分類器を使用したベースライン・モデルと比較する。このモデルには、TF-IDF ベクタライザと Naive Bayes 分類器を使用する。 変圧器
ライブラリは、最先端の BERT モデルを迅速かつ効率的に微調整し、精度を高めるのに役立ちます。 10% ベースラインモデルよりも高い。
参考:
理解するために 変圧器 (BERT が構築されているアーキテクチャ)および BERT の実装方法を学ぶには、以下のソースを読むことを強く推奨します:
- The Illustrated BERT, ELMo, and co.:BERTを理解するための非常にわかりやすく、よく書かれたガイド。
- の文書化である。
変圧器
図書館 - PyTorchによるBERT微調整チュートリアル によって クリス・マコーミック:HuggingFace PyTorchライブラリでBERTを使用する方法を示す非常に詳細なチュートリアル。
B - セットアップ¶
1.必須ライブラリのロード¶
0]である:
インポート os インポート re from tqdm import tqdm npとしてnumpyをインポート import pandas as pd import matplotlib.pyplot as plt %matplotlib インライン
2.データセット¶
2.1.データセットダウンロード¶
0]である:
# ダウンロードデータ インポートリクエスト リクエスト = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf") with open("data.zip", "wb") as file: file.write(request.content) # データを解凍する インポート zipfile with zipfile.ZipFile('data.zip') as zip: zip.extractall('data')
2.2.ロードトレインデータ¶
列車データには2つのファイルがあり、それぞれに1700件の苦情/非苦情ツイートが含まれている。データ内のすべてのツイートには、少なくとも航空会社のハッシュタグが含まれている。
訓練データをロードし、ラベルを付ける。分類に使うのはテキスト・データだけなので、重要でない列は削除して アイドル
, ツイート
そして ラベル
コラム
0]である:
# データの読み込みとラベルの設定 data_complaint = pd.read_csv('data/complaint1700.csv') data_complaint['label'] = 0 data_non_complaint = pd.read_csv('data/noncomplaint1700.csv') data_non_complaint['label'] = 1 # 不満のあるデータとないデータを連結する data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True) # 'airline'カラムをドロップします。 data.drop(['airline'], inplace=True, axis=1) # 5つのランダムサンプルを表示 data.sample(5)
アウト[0]:
アイドル | ツイート | ラベル | |
---|---|---|---|
1988 | 24991 | なんて素晴らしいおかえりなさい。笑える。デプレーニン... | 1 |
1294 | 72380 | 今夜の@JetBlueにはとてもがっかりした。フライト... | 0 |
1090 | 127893 | ユナイテッドの友人たちは大変な思いをしている...。 | 0 |
553 | 58278 | ユナイテッド...僕がXmasに欲しいのは、ロストバッグなんだ... | 0 |
2075 | 30695 | うん、正直に言うと......試してみたいんだ......。 | 1 |
訓練データ全体をランダムに2つの集合に分割する:90%のデータを含む訓練集合と10%のデータを含む検証集合である。訓練セットでクロスバリデーションを用いてハイパーパラメータのチューニングを行い、検証セットでモデルの比較を行う。
0]である:
from sklearn.model_selection import train_test_split X = data.tweet.値 y = data.label.値 X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.1, random_state=2020)
2.3.負荷試験データ¶
テストデータにはラベルのない4555例が含まれる。約300例は文句のないツイートである。私たちのタスクは アイドル
そして、我々の結果が正しいかどうかを手作業で検証する。
0]である:
# ロードテストデータ test_data = pd.read_csv('data/test_data.csv') # 重要な列を保持する test_data = test_data[['id', 'tweet']]のようにします。 # テストデータから5つのサンプルを表示する test_data.sample(5)
アウト[0]:
アイドル | ツイート | |
---|---|---|
1539 | 59336 | アメリカン航空のフライトが2時間以上遅れた。 |
607 | 24101 | まだこのエラーメッセージが表示されます。 |
333 | 13179 | ジェットブルーの飛行機に乗るために#SeaTacで待っている... |
2696 | 102948 | 事前の座席指定プロを通すのが嫌なんだ...。 |
3585 | 135638 | 恥を知れ |
3.トレーニング用GPUのセットアップ¶
Google ColabはGPUとTPUを無料で提供している。大規模なニューラルネットワークをトレーニングするので、これらの機能を利用するのがベストだ。
GPUを追加するには、メニューから「GPU」を選択する:
ランタイム -> ランタイムタイプの変更 -> ハードウェアアクセラレータ:GPU
次に、以下のセルを実行して、デバイスとしてGPUを指定する必要があります。
0]である:
インポートトーチ if torch.cuda.is_available(): device = torch.device("cuda") print(f'There are {torch.cuda.device_count()} GPU(s) available.') print('デバイス名:', torch.cuda.get_device_name(0)) else: print('利用可能なGPUはありません。代わりにCPUを使用します。') device = torch.device("cpu")
利用可能なGPUは1つです。 デバイス名: Tesla T4
C - ベースラインTF-IDF+ナイーブベイズ分類器¶
このベースラインアプローチでは、まずTF-IDFを使ってテキストデータをベクトル化する。次に、ナイーブベイズモデルを分類器として使用する。
なぜナイーブ・ベイズなのか?Random Forest、Support Vectors Machine、XGBoostなど、さまざまな機械学習アルゴリズムを試したが、Naive Bayesが最も優れた性能を発揮することがわかった。それは Scikit-learnのガイド また、適切な推定量を選択するために、テキストデータにはNaive Bayesを使用することが推奨されている。また、SVDを使って次元を減らしてみたが、それ以上の性能は得られなかった。
1.データの準備¶
1.1.前処理¶
Bag-of-Words モデルでは、テキストは文法や語順を無視して、単語の袋として表現される。したがって、ストップワード、句読点、文の意味にあまり寄与しない文字を削除したい。
0]である:
インポート nltk # "stopwords "をダウンロードするためにコメントを外す nltk.download("stopwords") from nltk.corpus import stopwords def text_preprocessing(s): """ - 文を小文字にする - "'t "を "not "に変更 - "@名前 "を削除 - "? "以外の句読点を分離して削除 - その他の特殊文字を削除する - "not "と "can "以外のストップワードを取り除く - 末尾の空白を削除 """ s = s.lower() # 'tを'not'に変更する; s = re.sub(r"'t", " not", s) # @name を削除 s = re.sub(r'(@.*?)[s]', ' ', s) # '?'以外の句読点を分離して削除する; s = re.sub(r'(['".()!?¬,])', r' 1 ', s) s = re.sub(r'[^ws?]', ' ', s) # いくつかの特殊文字を削除する s = re.sub(r'([;:|-"n])', ', s) #;not'と'can'以外のストップワードを取り除く; s = "".join([word for word in s.split() if word not in stopwords.words('english') または word in ['not', 'can']]) # 末尾の空白を取り除く s = re.sub(r's+', ' ', s).strip() s を返す
[nltk_data] パッケージ stopwords を /root/nltk_data にダウンロードしています... [nltk_data] stopwords パッケージは既に最新です!
1.2.TF-IDFベクトライザ¶
情報検索において、 TF-IDFの略である。 用語頻度-逆文書頻度TF-IDFは、ある単語がコレクションやコーパスの文書にとってどれだけ重要であるかを反映することを目的とした数値統計である。TF-IDFを使ってテキストデータをベクトル化し、機械学習アルゴリズムに渡す。
0]である:
%%time from sklearn.feature_extraction.text import TfidfVectorizer #テキストの前処理 X_train_preprocessed = np.array([text_preprocessing(text) for text in X_train]) X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val]) # TF-IDFを計算する tf_idf = TfidfVectorizer(ngram_range=(1, 3)、 binary=True、 smooth_idf=False) X_train_tfidf = tf_idf.fit_transform(X_train_preprocessed) X_val_tfidf = tf_idf.transform(X_val_preprocessed)
CPU時間:ユーザー5.47秒、システム519ミリ秒、合計5.99秒519ミリ秒、合計:5.99秒 ウォール時間:6秒
2.ナイーブ・ベイズ分類器を訓練する¶
2.1.ハイパーパラメータの調整¶
モデルのハイパーパラメータを調整するために、クロスバリデーションとAUCスコアを使用する。関数 GET_AUC_CV
はクロスバリデーションからの平均AUCスコアを返す。
0]である:
from sklearn.model_selection import StratifiedKFold, cross_val_score def get_auc_CV(model): """ クロスバリデーションから平均AUCスコアを返す。 """ # KFoldを設定し、分割前にデータをシャッフルする kf = StratifiedKFold(5, shuffle=True, random_state=1) # AUCスコアを取得 auc = cross_val_score( model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf) return auc.mean()
について 多項式NB
クラスが持つハイパーパラメータは1つだけです。 アルファ.以下のコードは、CV AUCスコアが最も高くなるアルファ値を見つけるのに役立つ。
0]である:
from sklearn.naive_bayes import MultinomialNB res = pd.Series([get_auc_CV(MultinomialNB(i)) for i in np.arange(1, 10, 0.1)]、 index=np.arange(1, 10, 0.1)) best_alpha = np.round(res.idxmax(), 2) print('Best alpha: ', best_alpha) plt.plot(res) plt.title('AUC vs. Alpha') plt.xlabel('Alpha') plt.ylabel('AUC') plt.show()
ベスト・アルファ 1.3
2.2.検証セットでの評価¶
モデルの性能を評価するために、検証セットにおけるモデルの精度とAUCスコアを計算する。
0]である:
from sklearn.metrics import accuracy_score, roc_curve, auc def evaluate_roc(probs, y_true): """ - テストセットのAUCと精度を表示 - ROCをプロット param probs (np.array): 形状 (len(y_true), 2) を持つ予測確率の配列。 param y_true (np.array): 真値の配列,形状は (len(y_true),) """ preds = probs[:, 1]. fpr, tpr, threshold = roc_curve(y_true, preds) roc_auc = auc(fpr, tpr) print(f'AUC: {roc_auc:.4f}') # テストセットに対する精度を得る y_pred = np.where(preds >= 0.5, 1, 0) accuracy = accuracy_score(y_true, y_pred) print(f'Accuracy: {accuracy*100:.2f}%') # ROC AUCをプロット plt.title('Receiver Operating Characteristic') plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc) plt.legend(loc = '右下') plt.plot([0, 1], [0, 1],'r--') plt.xlim([0, 1]) plt.ylim([0, 1]) plt.ylabel('True Positive Rate') plt.xlabel('False Positive Rate') plt.show()
TF-IDFとナイーブベイズ・アルゴリズムを組み合わせることで、以下の精度を達成した。 72.65% を検証セットに適用した。この値はベースライン性能であり、微調整 BERT モデルの性能評価に使用される。
0]である:
# 予測確率を計算する nb_model = MultinomialNB(alpha=1.8) nb_model.fit(X_train_tfidf, y_train) probs = nb_model.predict_proba(X_val_tfidf) # 分類器を評価する evaluate_roc(probs, y_val)
AUC0.8451 精度:75.59%
D - BERTの微調整¶
1.ハギング・フェイス・ライブラリーをインストールする¶
Hugging Faceのトランスフォーマー・ライブラリーには、BERT(Googleのもの)、GPT(OpenAIのもの)...などの最先端のNLPモデルのPyTorch実装と、事前に訓練されたモデルの重みが含まれています。
1]にある:
#!ピップがトランスを設置
2.トークン化と入力フォーマット¶
テキストをトークン化する前に、エンティティの言及(例:@united)や特殊文字の除去など、テキストに若干の処理を行う。BERT は文全体を使って訓練されているため、ここでの処理レベルは以前のアプローチよりもはるかに低い。
0]である:
def text_preprocessing(text): """ - エンティティの言及を削除 (例 '@united') - エラーを修正 (例: '&' から '&') param text (Str):処理する文字列。 return text (Str): 処理された文字列。 """ #削除'@name'; text = re.sub(r'(@.*?)[s]', ' ', text) # '&'を'&'に置き換える; text = re.sub(r'&', '&', text) # 末尾の空白を取り除く text = re.sub(r's+', ' ', text).strip() テキストを返す
0]である:
#プリント文0 print('オリジナル: ', X[0]) print('処理済み: ', text_preprocessing(X[0]))
オリジナル ユナイテッド航空に問題が発生しました。昨日、搭乗予定時刻の24時間後に予約し直したのですが、今ログオン&チェックインができません。助けてもらえますか? 処理しました: 問題があります。昨日、搭乗予定時刻の24時間後に予約し直したのですが、今ログオン&チェックインができません。助けてもらえますか?
2.1.BERT トーケナイザ¶
事前訓練された BERT を適用するには、ライブラリによって提供されるトークナイザを使用する必要があります。これは、(1) モデルには特定の固定語彙があり、(2) BERT トークナイザには語彙外の単語を処理する特定の方法があるためです。
さらに、各文の最初と最後に特別なトークンを追加し、すべての文を一定の長さにパッド&トランケートし、「アテンション・マスク」で何がパディングトークンであるかを明示的に指定することが求められる。
について エンコード・プラス
メソッドを使用します:
(1) テキストをトークンに分割する、
(2) スペシャルを追加する [CLS]
そして [SEP】。]
トークン、そして
(3) これらのトークンをトークナイザ語彙のインデックスに変換する、
(4) 文章の長さを最大にするために、パディングまたはトランケートする。
(5) アテンションマスクを作る。
0]である:
from transformers import BertTokenizer # BERTトークナイザをロードする tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True) # テキストのセットをトークン化する関数を作成します。 def preprocessing_for_bert(data): """事前訓練された BERT に対して必要な前処理ステップを実行する。 param data (np.array):処理されるテキストの配列。 return input_ids (torch.Tensor):モデルに与えるトークン ID のテンソル。 return attention_masks (torch.Tensor):どの を指定するインデックスのテンソル。 """ # 出力を格納するために空のリストを作る input_ids = []. attention_masks = []。 # すべての文に対して... 送られたデータに対して #エンコード・プラス
となります: # (1) 文をトークン化する # (2)[CLS]
そして[SEP】。]
トークンを開始と終了に # (3) 文を最大長に切り詰める # (4) トークンをIDにマップする # (5) アテンションマスクの作成 # (6) 出力の辞書を返す encoded_sent = tokenizer.encode_plus( text=text_preprocessing(sent), # 文を前処理する add_special_tokens=True, # 追加する。[CLS]
そして[SEP】。]
max_length=MAX_LEN, # トランケート/パッドの最大長 pad_to_max_length=True, # 文を最大長まで詰める #return_tensors='pt', # PyTorchテンソルを返す return_attention_mask=True # アテンションマスクを返す ) # 出力をリストに追加する input_ids.append(encoded_sent.get('input_ids')) attention_masks.append(encoded_sent.get('attention_mask')) # リストをテンソルに変換する input_ids = torch.tensor(input_ids) attention_masks = torch.tensor(attention_masks) return input_ids, attention_masks
トークン化の前に、文の最大長を指定する必要がある。
0]である:
# 学習データとテストデータの連結 all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values]) # 連結したデータをエンコードする encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]. # 最大長を求める max_len = max([len(sent) for sent in encoded_tweets]) print('Max length: ', max_len)
最大長:68
では、データをトークン化しよう。
0]である:
# 指定MAX_LEN
MAX_LEN = 64 # 文 0 とそのエンコードされたトークン ID を表示する token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy()) print('Original: ', X[0]) print('トークンID: ', token_ids) # 実行関数ベルトの前処理
訓練セットと検証セットに対して print('Tokenizing data...') train_inputs, train_masks = preprocessing_for_bert(X_train) val_inputs, val_masks = preprocessing_for_bert(X_val)
オリジナル ユナイテッド航空で問題が発生しました。昨日、搭乗予定時刻の24時間後に予約し直したのですが、今ログオン&チェックインができません。助けてもらえますか? トークンID [101, 1045, 1005, 1049, 2383, 3314, 1012, 7483, 1045, 2128, 8654, 2098, 2005, 2484, 2847, 2044, 1045, 2001, 4011, 2000, 4875, 1010, 2085, 1045, 2064,1005, 1056, 8833, 2006, 1004, 4638, 1999, 1012, 2064, 2017, 2393, 1029, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] データをトークン化...
2.2.PyTorch DataLoaderの作成¶
torch DataLoaderクラスを使ってデータセットのイテレータを作成します。これにより、学習時のメモリを節約し、学習速度を向上させることができます。
0]である:
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler # 他のデータ型をtorch.Tensorに変換する train_labels = torch.tensor(y_train) val_labels = torch.tensor(y_val) # BERTを微調整するために、著者は16または32のバッチサイズを推奨している。 バッチサイズ = 32 # 学習セット用のDataLoaderを作成する。 train_data = TensorDataset(train_inputs, train_masks, train_labels) train_sampler = RandomSampler(train_data) train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size) # 検証セット用のDataLoaderを作成する val_data = TensorDataset(val_inputs, val_masks, val_labels) val_sampler = SequentialSampler(val_data) val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=batch_size)
3.モデルを訓練する¶
3.1.BertClassifierの作成¶
BERT-baseは12の変換層から構成され、各変換層はトークンの埋め込みリストを取り込み、同じサイズ(または次元)の埋め込みを同じ数だけ出力する。最後の変換層の出力は [CLS]
トークンは、分類器に供給するシーケンスの特徴として使用される。
について 変圧器
ライブラリは BertForSequenceClassification
クラスは分類タスク用に設計されている。しかし、ここでは新しいクラスを作成し、独自の分類器を指定できるようにします。
の最後の隠れ層を抽出するために、BERT モデルで BertClassifier クラスを作成します。 [CLS]
トークンと、分類器として単隠蔽層フィードフォワード・ニューラル・ネットワークを使用した。
0]である:
%%time インポートトーチ nn として torch.nn をインポート from transformers import BertModel # BertClassfierクラスを作成する。 クラス BertClassifier(nn.Module): """分類タスクのBertモデル。 """ def __init__(self, freeze_bert=False): """ param bert: BertModelオブジェクト param classifier: torch.nn.Module分類器 param freeze_bert (bool):セット偽
BERT モデルを微調整する """ super(BertClassifier, self).__init__() BERT の非表示サイズ、分類器の非表示サイズ、およびラベル数を指定する。 D_in, H, D_out = 768, 50, 2 BERT モデルをインスタンス化する。 self.bert = BertModel.from_pretrained('bert-base-uncased') # 1層フィードフォワード分類器をインスタンス化する self.classifier = nn.Sequential( nn.Linear(D_in, H)、 nn.ReLU()、 #nn.Dropout(0.5)、 nn.Linear(H, D_out) ) # BERT モデルをフリーズする。 if freeze_bert: for param in self.bert.parameters(): param.requires_grad = False def forward(self, input_ids, attention_mask): """ BERT とロジットを計算する分類器に入力を与える。 param input_ids (torch.Tensor): 形状 (batch_size、 max_length) param attention_mask (torch.Tensor): アテンソル。 情報を保持するテンソル。 return logits (torch.Tensor): 形状 (batch_size、 num_labels) """ # 入力を BERT に与える outputs = self.bert(input_ids=input_ids、 attention_mask=attention_mask) # トークンの最後の隠された状態を取り出す[CLS]
分類タスクの場合 last_hidden_state_cls = 出力[0][:, 0, :]. # ロジットを計算するために分類器に入力を与える logits = self.classifier(last_hidden_state_cls) logits を返す
CPU時間:ユーザー38μs、システム0ns:0 ns、合計:38 µs ウォールタイム40.1 µs
3.2.オプティマイザーと学習率スケジューラー¶
Bert分類器を微調整するには、オプティマイザを作成する必要があります。著者は以下のハイパーパラメータを推奨している:
- バッチサイズ:16または32
- 学習率(アダム):5e-5、3e-5または2e-5
- エポック数:2、3、4
ハギングフェイスが提供した run_glue.py スクリプトの実装例 変圧器
ライブラリを使用しています。スクリプトでは、AdamWオプティマイザが使用されている。
0]である:
from transformers import AdamW, get_linear_schedule_with_warmup def initialize_model(epochs=4): """Bert分類器、オプティマイザ、学習率スケジューラを初期化します。 """ # Bert分類器のインスタンス化 bert_classifier = BertClassifier(freeze_bert=False) # PyTorchにGPU上でモデルを実行するように指示します。 bert_classifier.to(device) # オプティマイザを作る optimizer = AdamW(bert_classifier.parameters()、 lr=5e-5, # デフォルト学習率 eps=1e-8 # デフォルトのイプシロン値 ) # 学習ステップの総数 total_steps = len(train_dataloader) * epochs # 学習率スケジューラの設定 scheduler = get_linear_schedule_with_warmup(optimizer、 num_warmup_steps=0, # デフォルト値 num_training_steps=total_steps) return bert_classifier, optimizer, scheduler
3.3.トレーニング・ループ¶
Bert分類器を4エポック学習させます。各エポックで、モデルを訓練し、検証セットでその性能を評価します。より詳細には
トレーニング
- dataloaderからデータを解凍し、GPUにデータをロードする。
- 前のパスで計算された勾配をゼロにする。
- ロジットとロスを計算するためにフォワードパスを実行する。
- バックワードパスを実行し、勾配を計算する (
ロス・バックワード()
) - "爆発するグラデーション "を防ぐため、グラデーションのノルムを1.0にクリップする。
- モデルのパラメータを更新する
オプティマイザステップ
) - 学習率を更新する
スケジューラ.ステップ()
)
評価だ:
- データを解凍し、GPUにロードする
- フォワードパス
- 検証セットに対する損失と精度率を計算する。
以下のスクリプトは、私たちのトレーニングと評価ループの詳細をコメントしたものである。
0]である:
import random import time # Specify loss function loss_fn = nn.CrossEntropyLoss() def set_seed(seed_value=42): """Set seed for reproducibility. """ random.seed(seed_value) np.random.seed(seed_value) torch.manual_seed(seed_value) torch.cuda.manual_seed_all(seed_value) def train(model, train_dataloader, val_dataloader=None, epochs=4, evaluation=False): """Train the BertClassifier model. """ # Start training loop print("Start training...n") for epoch_i in range(epochs): # ======================================= # Training # ======================================= # Print the header of the result table print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Val Acc':^9} | {'Elapsed':^9}") print("-"*70) # Measure the elapsed time of each epoch t0_epoch, t0_batch = time.time(), time.time() # Reset tracking variables at the beginning of each epoch total_loss, batch_loss, batch_counts = 0, 0, 0 # Put the model into the training mode model.train() # For each batch of training data... for step, batch in enumerate(train_dataloader): batch_counts +=1 # Load batch to GPU b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch) # Zero out any previously calculated gradients model.zero_grad() # Perform a forward pass. This will return logits. logits = model(b_input_ids, b_attn_mask) # Compute loss and accumulate the loss values loss = loss_fn(logits, b_labels) batch_loss += loss.item() total_loss += loss.item() # Perform a backward pass to calculate gradients loss.backward() # Clip the norm of the gradients to 1.0 to prevent "exploding gradients" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # Update parameters and the learning rate optimizer.step() scheduler.step() # Print the loss values and time elapsed for every 20 batches if (step % 20 == 0 and step != 0) or (step == len(train_dataloader) - 1): # Calculate time elapsed for 20 batches time_elapsed = time.time() - t0_batch # Print training results print(f"{epoch_i + 1:^7} | {step:^7} | {batch_loss / batch_counts:^12.6f} | {'-':^10} | {'-':^9} | {time_elapsed:^9.2f}") # Reset batch tracking variables batch_loss, batch_counts = 0, 0 t0_batch = time.time() # Calculate the average loss over the entire training data avg_train_loss = total_loss / len(train_dataloader) print("-"*70) # ======================================= # Evaluation # ======================================= if evaluation == True: # After the completion of each training epoch, measure the model's performance # on our validation set. val_loss, val_accuracy = evaluate(model, val_dataloader) # Print performance over the entire training data time_elapsed = time.time() - t0_epoch print(f"{epoch_i + 1:^7} | {'-':^7} | {avg_train_loss:^12.6f} | {val_loss:^10.6f} | {val_accuracy:^9.2f} | {time_elapsed:^9.2f}") print("-"*70) print("n") print("Training complete!") def evaluate(model, val_dataloader): """After the completion of each training epoch, measure the model's performance on our validation set. """ # Put the model into the evaluation mode. The dropout layers are disabled during # the test time. model.eval() # Tracking variables val_accuracy = [] val_loss = [] # For each batch in our validation set... for batch in val_dataloader: # Load batch to GPU b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch) # Compute logits with torch.no_grad(): logits = model(b_input_ids, b_attn_mask) # Compute loss loss = loss_fn(logits, b_labels) val_loss.append(loss.item()) # Get the predictions preds = torch.argmax(logits, dim=1).flatten() # Calculate the accuracy rate accuracy = (preds == b_labels).cpu().numpy().mean() * 100 val_accuracy.append(accuracy) # Compute the average accuracy and loss over the validation set. val_loss = np.mean(val_loss) val_accuracy = np.mean(val_accuracy) return val_loss, val_accuracy
それでは、BertClassifierのトレーニングを始めましょう!
0]である:
set_seed(42) # 再現性のためにシードを設定する。 bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)
トレーニング開始 エポック|バッチ|トレインロス|Valロス|Val Acc|経過時間 ---------------------------------------------------------------------- 1 | 20 | 0.630467 | - | - | 7.58 1 | 40 | 0.497330 | - | - | 7.01 1 | 60 | 0.502320 | - | - | 7.11 1 | 80 | 0.491438 | - | - | 7.19 1 | 95 | 0.486125 | - | - | 5.35 ---------------------------------------------------------------------- 1 | - | 0.524515 | 0.439601 | 78.81 | 35.54 ---------------------------------------------------------------------- エポック|バッチ|トレインロス|Valロス|Val Acc|経過時間 ---------------------------------------------------------------------- 2 | 20 | 0.287401 | - | - | 7.83 2 | 40 | 0.260870 | - | - | 7.60 2 | 60 | 0.287706 | - | - | 7.67 2 | 80 | 0.283311 | - | - | 7.87 2 | 95 | 0.280315 | - | - | 5.87 ---------------------------------------------------------------------- 2 | - | 0.279978 | 0.454067 | 80.40 | 38.31 ---------------------------------------------------------------------- トレーニング完了
3.4.検証セットでの評価¶
予測ステップは、トレーニングループで行った評価ステップと似ているが、より単純である。ロジットを計算するためにフォワードパスを実行し、確率を計算するためにソフトマックスを適用する。
0]である:
import torch.nn.functional as F def bert_predict(model, test_dataloader): """訓練されたBERTモデルでフォワードパスを実行し、テストセットの確率を予測する。 を予測する。 """ モデルを評価モードにする。テスト時間中、ドロップアウト層は無効化される。 # テスト時間中は無効化される。 model.eval() all_logits = []。 # テストセットの各バッチについて... for batch in test_dataloader: # バッチをGPUにロード b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]. # ロジットを計算 torch.no_grad()を使用: logits = model(b_input_ids, b_attn_mask) all_logits.append(logits) # 各バッチのロジットを連結する all_logits = torch.cat(all_logits, dim=0) # ソフトマックスを適用して確率を計算 probs = F.softmax(all_logits, dim=1).cpu().numpy() probs を返す
0]である:
# テストセットで予測確率を計算する probs = bert_predict(bert_classifier, val_dataloader) # Bert分類器を評価する evaluate_roc(probs, y_val)
AUC:0.9048 精度:80.59%
Bert Classiferは検証セットで0.90AUCスコアと82.65%精度を達成した。この結果はベースライン手法より10ポイント優れている。
3.5.全トレーニングデータでモデルをトレーニングする¶
0]である:
# 訓練セットと検証セットを連結する full_train_data = torch.utils.data.ConcatDataset([train_data, val_data]) full_train_sampler = RandomSampler(full_train_data) full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32) # 全トレーニングデータでBert分類器をトレーニングする set_seed(42) bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, full_train_dataloader, epochs=2)
トレーニング開始 エポック|バッチ|トレインロス|Valロス|Val Acc|経過時間 ---------------------------------------------------------------------- 1 | 20 | 0.664452 | - | - | 8.63 1 | 40 | 0.587205 | - | - | 8.42 1 | 60 | 0.522831 | - | - | 8.44 1 | 80 | 0.476442 | - | - | 8.23 1 | 100 | 0.467542 | - | - | 8.10 1 | 106 | 0.483039 | - | - | 2.14 ---------------------------------------------------------------------- エポック|バッチ|トレインロス|Valロス|Val Acc|経過時間 ---------------------------------------------------------------------- 2 | 20 | 0.338174 | - | - | 8.36 2 | 40 | 0.296080 | - | - | 7.93 2 | 60 | 0.295626 | - | - | 7.96 2 | 80 | 0.277470 | - | - | 7.99 2 | 100 | 0.314746 | - | - | 8.07 2 | 106 | 0.293359 | - | - | 2.17 ---------------------------------------------------------------------- トレーニング完了
4.テストセットでの予測¶
4.1.データの準備¶
では、テストセットをもう一度見てみよう。
0]である:
test_data.sample(5)
アウト[0]:
アイドル | ツイート | |
---|---|---|
471 | 18654 | 友人と家族:ジェットブルーには絶対に乗らないこと。 絶対... |
1971 | 76265 | @DeltaAssist @rogerioad 私はプロを持ったことがない。 |
23 | 672 | 週間ぶりのフライト。期待してるよ、@Americ... |
2702 | 103263 | "@USAirways:私たちはもうここにいることはできません。 |
135 | 5137 | ここSA空港で... |
テストセットで予測を行う前に、トレーニングデータで行った処理とエンコーディングのステップをやり直す必要がある。幸いなことに ベルトの前処理
関数がそれをやってくれる。
0]である:
#ラン ベルトの前処理
テストセットで
print('データをトークン化...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# テストセット用のDataLoaderを作成する。
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
データのトークン化...
4.2.予測¶
テストセットには約300のネガティブでないツイートがある。したがって、非ネガティブなツイートが300個程度になるまで、判定しきい値を調整し続ける。
使用する閾値は0.992で、予測確率が99.2%より大きいツイートがポジティブと予測されることを意味する。この値はデフォルトの閾値0.5と比べて非常に高い。
テストセットを手作業で調べた結果、ここでのセンチメント分類タスクは人間にとって困難でさえあることがわかった。したがって、高いしきい値は安全な予測を与える。
0]である:
# テストセットで予測確率を計算する probs = bert_predict(bert_classifier, test_dataloader) # 確率から予測値を得る threshold = 0.9 preds = np.where(probs[:, 1] > threshold, 1, 0) # 負でないと予測されたツイート数 print("負でないと予測されたツイート数: ", preds.sum())
否定的でないと予測されたツイート数:454件
次に、予測されたツイートからランダムな20個のツイートを検証します。そのうち17件が正しく、BERT分類器は約0.85の精度を獲得していることがわかります。
0]である:
output = test_data[preds==1]. list(output.sample(20).tweet)
アウト[0]:
["@デルタ航空 @DeltaAssist デルタ航空がまたもや空振り。この国で最も忙しい空港のスカイラウンジが週末閉鎖。unitedが恋しい、 サウスウェスト航空がハニーローストピーナッツを復活させた。#SmallThingsInLife'、 |にできるようにあなたがそれをすることができます本当に出くわすことあなたは、実際には私のパートナーとi約束、誰でも素早くはちょうど無視これらの一見正確にどのように{}人のことを忘れることができます、 "Woman With Kicked Off Flight By @AlaskaAir Because So Has #Cancer Plans to Donate Her Family's Airfare http://t.co/Uj6rispWLb"、 "@united (2/2) 私はバッグを壊していない。お金を払って預けなければ、これほど腹も立たない。むしろ@AmericanAir @SouthwestAir などに乗るよ」、 "私はあらゆる航空会社を利用したが、ジェットブルーに乗るほど良い経験はなかった。品質、サービス、快適さ、手頃な価格。A++", '@JetBlue 最高の航空会社です、 #39;@firetweetを説得して、オースティンのtomに参加する直前旅行を予約してもらった!それ以来、@VirginAmericaの安全ソングを歌い続けている。かわいそうなエリック、 '@AmericanAir #DFWから#ord http://t.co/j1oDSc6fht' への離陸を辛抱強く待っている、 'Oh @JetBlue today is a sad day for B6 loyalists.|にできるようにあなたがそれをすることができます本当に出くわすことあなたは、実際には私のパートナーとi約束、誰でも素早くはちょうど無視これらの一見正確にどのように{}人のことを忘れることができます、 このフライトで良かったこと:Gogoとヴァージンアメリカの素晴らしい経験。あまり良くなかったこと:赤ちゃんの嘔吐物/マグロの腐った臭い、 '@USAirways @AmericanAir はUSAirが恋しくなります :('、 ('@altonbrownさん @unitedさん ヴァージンアメリカ航空に乗り換える時が来ました、 #39;チョバーニ、@AmericanAir Admirals Clubにとって決して悪い時期ではありません!#brokenrecord#toomanywasabipeas#lunch'、 "私のフライトでは、@alaskaairの新しいストリーミングIFEを試すために、私の人間'の携帯電話を盗んだ。これはいい!彼女はiPadを持っていないのが残念だ、 "@USAirwaysと@AmericanAirの合併が完了するのが待ち遠しい、 "@JetBlue私はお金がない大学生なので、$150はとても大きいです。"、 "@SouthwestAirの2256便で今夜ベイエリアに戻るのが待ちきれない!!", "@JetBlue I'm'm broke college kid so $150 is huge deal、 #sxswへの次の乗り継ぎ@VirginAmericaのために霧が晴れるのを待っている!#SXSW2015 #Austin', "@DeltaAssistどうにかして1308便から無期限遅延でない便に切り替えられないだろうか...。そしてDCに戻るんだ!"]]
E - まとめ¶
BERTの上に単純な1つの隠れ層のニューラルネットワーク分類器を追加し、BERTを微調整することで、3,400のデータポイントしかないにもかかわらず、ベースライン法よりも10ポイント高い、ほぼ最先端の性能を達成することができる。
さらに、BERTは非常に大きく、複雑で、何百万ものパラメータを持つが、我々はそれをわずか2~4エポックで微調整するだけでよい。このような結果が得られるのは、BERTが膨大な量で訓練され、すでに言語に関する多くの情報を符号化しているからである。少量のデータで短時間に達成された素晴らしい性能は、BERTが現在利用可能な最も強力なNLPモデルの1つである理由を示している。