4. 問題3:有害コメント検出

4.1. 取り組む問題

Toxic Comment Classification Challenge

4.1.1. 背景

多くのオンラインウェブサイト,アプリケーション,サービスでは,ディスカッション,コメント,その他の目的でユーザーがテキストを入力することができます.ほとんどの場合,私たちはユーザーが適切に行動し,テキスト機能を乱用しないことを期待しています.しかし,時には意図的であろうとなかろうと,ユーザーが悪意のあるテキストを発し,他の人の気持ちを傷つける結果になりかねません.

この種のチャットやサービスの運営にとって,会話を監視するための自動化ツールは需要は低くありません.有害な言葉を識別するためのそのようなツールは,すべてのユーザーにとってより良くユーザー体験を与えることでしょう.

4.1.2. タスク

Wikipediaから収集されたコメントが提供されます.様々なタイプの有害コメントを区別できるモデルを構築してください.

4.1.3. データ

  • train.csv - コメントとそのラベルを含むファイル.

  • test.csv - テストデータを含むファイル.

  • sample_submission.csv - サンプル提出ファイルを含むファイル.

  • test_labels.csv - コンペティション終了後に公開されたテストデータのラベル.

4.2. セットアップ

以下のセルは必要なデータをダウンロードし,ノートブックで使用する環境を設定するためのものです.

4.2.1. GPUランタイム

このノートブックはGPUを搭載したシステムで実行するように設計されています.ランタイムのタイプをGPUを含むものに切り替えていることを確認してください(例:A100,V100).

GPUランタイムを使用しているかどうかを確認するには,次のセルを実行してください.フォーマットされたテキストが複数行出力されるはずです.

! nvidia-smi

4.2.2. データセット

Kaggleはコンペティションと簡単にやり取りできるAPIを提供しています.このAPIを使ってデータをダウンロードし,予測結果をアップロードします.

このAPIを使用する最初のステップは,ユーザー認証です.APIトークンはユーザー名とKaggleが生成したキーを含むファイルです.トークンはアカウントページからダウンロードすることができ,通常 kaggle.json という名前のファイルです.APIトークンは,ユーザーとしてAPIにアクセスするために必要なものなので,個人のGoogle Driveフォルダに安全に保管してください.

このノートブックはGoogle Driveフォルダ内のkaggle.jsonというKaggle APIトークンを検索します.トークンをGoogle Driveに置いたことを確認し,プロンプトが表示されたらこのノートブックがトークンにアクセスすることを許可してください.

from google.colab import drive
import os
import json

drive.mount("/content/drive", force_remount=True)
fin = open("/content/drive/MyDrive/kaggle/token/kaggle.json", "r")
json_data = json.load(fin)
fin.close()
os.environ["KAGGLE_USERNAME"] = json_data["username"]
os.environ["KAGGLE_KEY"] = json_data["key"]

認証後,参加したすべてのコンペティションにアクセスできます.データのダウンロードにエラーが発生した場合は,Kaggle API トークンが有効であること,コンペティションのルールに同意していることを確認してください.

%%bash
kaggle competitions download -c jigsaw-toxic-comment-classification-challenge
if [ $? -ne 0 ]; then
    echo "データのダウンロードに問題があったようです."
    echo "競技規則に同意し,APIキーが有効であることを確認してください."
else
    mkdir -p /content/drive/MyDrive/kaggle/project/toxic
    unzip -o /content/jigsaw-toxic-comment-classification-challenge.zip -d /content/drive/MyDrive/kaggle/project/toxic
    unzip -o "/content/drive/MyDrive/kaggle/project/toxic/*.zip" -d /content/drive/MyDrive/kaggle/project/toxic
    rm /content/drive/MyDrive/kaggle/project/toxic/*.zip
fi
wget -q -P /tmp https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip
unzip -o /tmp/NotoSansCJKjp-hinted.zip -d /usr/share/fonts/NotoSansCJKjp

4.2.3. 環境

デフォルトではインストールされていないライブラリを使用するので,ここでインストールします.

! pip install transformers pytorch-lightning
import os
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import transformers
import torch
import pytorch_lightning as pl
from tqdm.auto import tqdm
import torchmetrics
from glob import glob

font_path = '/usr/share/fonts/NotoSansCJKjp/NotoSansMonoCJKjp-Regular.otf'
matplotlib.font_manager.fontManager.addfont(font_path)
prop = matplotlib.font_manager.FontProperties(fname=font_path)
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = prop.get_name()
os.chdir('/content/drive/MyDrive/kaggle/project/toxic')

4.3. 探索的データ解析

この大会のデータはコメントとそのラベルで構成されています.可視化できるものはあまりありませんが,ラベルの分布といくつかの有害なコメントを見ることができます.

4.3.1. train.csv

data = pd.read_csv('./train.csv')
data

まず,ラベルの分布を見てみましょう.

data_bin = data.copy()
data_bin['non-toxic'] = 1 - data_bin[['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']].max(axis=1)
data_bin['toxic'] = data_bin[['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']].max(axis=1)
data_bin = data_bin[['id', 'toxic', 'non-toxic']].melt(id_vars='id')
sns.barplot(data_bin, x='variable', y='value', errorbar=None)
plt.xlabel(None)
plt.ylabel(None)
plt.xticks([0, 1], ['Toxic', 'Non-toxic']);

この図から,有害と分類されるコメントは全体の10%程度であることがわかります.

data_melt = data.copy()
data_melt = data_melt.drop(columns=['comment_text']).melt(id_vars='id')
sns.barplot(data_melt, x='variable', y='value', estimator='sum', errorbar=None)
plt.xlabel(None)
plt.ylabel(None);

有害なコメントの種類は同じ頻度ではありません.

data_sum = data.copy()
data_sum['num_labels'] = data_sum[['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']].sum(axis=1)
data_sum = data_sum[data_sum['num_labels']>0]
sns.barplot(data_sum, x='num_labels', y='num_labels', estimator='count', errorbar=None)
plt.ylabel(None);

有害なクラスが通常より多く表示される理由のひとつは,有害なクラスが最も一般的な分類であり,コメントによっては複数の有害なタイプが含まれているからかもしれません.

有害なコメントを見てみましょう.

obscene = data.loc[67329]
obscene['comment_text']

このコメントはわいせつに分類されます.わいせつなコメントには通常,閲覧者によっては不適切と思われる言葉が含まれています.

insult = data.loc[106700]
insult['comment_text']

このコメントは侮辱に分類されます.侮辱は通常,1人または複数の人に向けられ,軽蔑や軽蔑を伝えます.

4.4. モデリング

このノートブックでは,2018年に開発された言語モデルであるBERTを使用します.また,HuggingFaceのtransformers ライブラリも利用します.これは,事前に学習されたBERTモデルと,テキストデータを前処理するための様々な機能を提供します.最後に,PyTorchとPyTorch Lightningを使用します.PyTorchとPyTorch Lightningは,モデルの構築と学習を可能にする有用なライブラリです.

4.4.1. 前処理

BERT のようなモデルを使用するには,まず入力値をトークン化する必要があります.これは単にテキストを BERT が理解できる数値に変換することを意味します.transformersは自動的に正しいトークナイザーを読み込むAutoTokenizerクラスを提供しています.

次に,学習中に使用できる構造化された方法でデータを整理する必要があります.そのために,PyTorchの Dataset を使います.この Dataset はモデルとデータの間のインターフェイスとして機能します.

このコンペティションのデータセットは小さいので,すべてのトレーニングデータセットを前処理してメモリに保存することができます.そうすることで,データをその場でトークン化するのではなく,事前にトークン化されたデータをメモリからロードするため,トレーニングが大幅にスピードアップします.

トレーニングの後半では,データサンプルをミニバッチ処理します.簡単のため,トークナイザーが返す最初の128個のトークンのみを使用します.それ以上のトークンを持つテキストは切り捨てられ,それ以下のトークンを持つテキストはパディングされます.パラメータ max_length を変更すると,トークン化された入力により多くの情報が含まれるようになりますが,その分メモリのコストが増加します.

model_name = 'bert-base-uncased'
tokenizer = transformers.AutoTokenizer.from_pretrained(model_name)

class CustomDataset(torch.utils.data.Dataset):
    def __init__(self, tokenizer, comments, labels=None):
        self.tokenized = []
        if labels is not None:
            self.labels = torch.tensor(labels, dtype=torch.float32)
        else:
            self.labels = None
        print('データのトークン化')
        for comment in tqdm(comments):
            self.tokenized.append({
                key: value[0] for key, value in
                    tokenizer(
                        comment,
                        return_tensors='pt',
                        padding='max_length',
                        truncation=True,
                        max_length=128
                    ).items()
            })

    def __len__(self):
        return len(self.tokenized)

    def __getitem__(self, idx):
        if self.labels is None:
            return self.tokenized[idx]
        else:
            return self.tokenized[idx], self.labels[idx]

ds = CustomDataset(tokenizer, data['comment_text'], data[['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']].values)

4.4.2. 学習

今回使用するBERTモデルは,別のタスクのためにすでに訓練されています.単にモデルを初期化し,データセットのクラスの値を出力するように変更します.その後,モデルを微調整し,タスクのためにモデルを再トレーニングします.

標準的なPyTorchでは,モデルやタスクに関係なく同じテンプレートコードを書きます.これは非常に面倒で,また細かい部分を忘れやすいためエラーが起こりやすいです.PyTorch Lightningを使えば,学習コードの関連性の高い部分を1つのクラスにまとめ,細かい部分はライブラリに任せることができます.また,PyTorch Lightningはロギング,チェックポイント,その他のタスクのための多くの便利なユーティリティを提供します.

以下のセルでは,以下のメソッドを含む PyTorch Lightning LightningModule を定義します.

  • __init__ - 学習中に使用する変数を初期化します.これにはモデルとトレーニングの進捗を監視するためのメトリクスが含まれます.

  • forward - 与えられた入力をどのようにモデルに渡すかを定義します.

  • (training|validation|predict)_step - 各ステップで実行するアクションを定義します.最も単純なケースでは forward を呼び出して損失を計算しますが,追加の後処理やロギングを含めることもできます.

  • configure_optimizers - トレーニング中に使用するオプティマイザを返します.

学習を高速化し,過学習を回避するために,既に学習済みのモデルのパラメータは学習しません.学習されるのは,分類タスク用に作成されたパラメータのみです.

class CustomModel(pl.LightningModule):
    def __init__(self, freeze_pretrained=True, lr=1e-5):
        super().__init__()
        self.lr = lr
        self.model = transformers.AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=6)
        if freeze_pretrained:
            for name, parameter in self.model.named_parameters():
                if 'classifier' not in name:
                    parameter.requires_grad = False
        self.loss_fn = torch.nn.BCEWithLogitsLoss()
        self.train_AUROC = torchmetrics.classification.MultilabelAUROC(num_labels=6)
        self.val_AUROC = torchmetrics.classification.MultilabelAUROC(num_labels=6)

    def forward(self, tokenized_input):
        return self.model(**tokenized_input)

    def training_step(self, batch, batch_idx):
        tokenized_input, labels = batch
        preds = self.forward(tokenized_input).logits
        loss = self.loss_fn(preds, labels)
        self.log('train_loss', loss, on_step=True, on_epoch=True)
        self.train_AUROC(preds.sigmoid(), labels.long())
        self.log('train_AUROC', self.train_AUROC, on_step=False, on_epoch=True)
        return loss

    def validation_step(self, batch, batch_idx):
        tokenized_input, labels = batch
        preds = self.forward(tokenized_input).logits
        loss = self.loss_fn(preds, labels)
        self.log('val_loss', loss, on_step=False, on_epoch=True)
        self.val_AUROC(preds.sigmoid(), labels.long())
        self.log('val_AUROC', self.val_AUROC, on_step=False, on_epoch=True)

    def predict_step(self, batch, batch_idx):
        tokenized_input = batch
        preds = self.forward(tokenized_input).logits
        preds = preds.sigmoid()
        return preds.detach().cpu()

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=self.lr)
        return optimizer

モデルが学習しているかどうかを確認するには,トレーニングをモニタリングする必要があります.具体的には,時間の経過とともに損失が減少していること,また,観察しているメトリクスが改善しているかどうかを確認します.

ここでは,TensorBoardを使用してトレーニングを監視します.TensorBoardは,時間の経過とともにモデルのパフォーマンスを可視化する便利なツールです.

TensorBoardサーバーを起動するには,以下のセルを実行します.数秒後,インターフェースが表示されます.トレーニングやロギング値を開始していないため,最初は何も出力されません.トレーニングが開始されると,セルの出力は定期的に更新されます.

%load_ext tensorboard
%tensorboard --logdir lightning_logs/

トレーニングの準備はほぼ整いました.次のセルでは,まずデータを学習セットと検証セットに分割し,データをバッチ処理するPyTorchの DataLoaders を準備します.次に,モデルとロガーを初期化し,モデルの最適な反復を保存するコールバックを作成します.最後に,学習を開始します.

train_ds, val_ds = torch.utils.data.random_split(ds, [0.8, 0.2])
train_dl = torch.utils.data.DataLoader(train_ds, batch_size=32, shuffle=True)
val_dl = torch.utils.data.DataLoader(val_ds, batch_size=32, shuffle=False)

model = CustomModel()
tb_logger = pl.loggers.TensorBoardLogger('lightning_logs', name='', version='custom_model')
if os.path.exists(f'lightning_logs/{tb_logger.version}/checkpoints'):
    for checkpoint in os.listdir(f'lightning_logs/{tb_logger.version}/checkpoints/'):
        os.remove(f'lightning_logs/{tb_logger.version}/checkpoints/{checkpoint}')
checkpoint_callback = pl.callbacks.ModelCheckpoint(monitor='val_AUROC', mode='max')
profiler = pl.profilers.AdvancedProfiler(dirpath='.', filename='profile')
trainer = pl.Trainer(
    max_epochs=3, accelerator='gpu', precision='16-mixed',
    logger=tb_logger,
    callbacks=[checkpoint_callback],
    enable_progress_bar=True
)

trainer.fit(model, train_dl, val_dl)

4.4.3. 予測

モデルの学習が終了したら,テストデータの予測を生成します.上で行ったようにデータをトークン化する必要があります.さらに,さまざまなトレーニング・パラメータによっては,モデルが最適な状態にないこともあります.しかし,チェックポイント・コールバックで最適なバージョンを保存したので,簡単にモデルを最適な状態にリロードすることができます.

test_data = pd.read_csv('test.csv')
test_ds = CustomDataset(tokenizer, test_data['comment_text'])
test_dl = torch.utils.data.DataLoader(test_ds, batch_size=32, shuffle=False)
ckpt_path = glob(f'lightning_logs/{tb_logger.version}/checkpoints/*')[0]
model = CustomModel.load_from_checkpoint(ckpt_path)
preds = trainer.predict(model, test_dl)
submission = pd.read_csv('sample_submission.csv')
submission['id'] = test_data['id']
submission[['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']] = torch.vstack(preds).numpy()
submission.to_csv('submission.csv', index=False)
submission

4.4.4. Kaggleへのアップロード

予測はKaggleのAPIを通して直接提出できるので,ファイルを手動でダウンロードし,Kaggleのウェブサイトに再アップロードする必要はありません.提出後は提出ページでスコアを確認します

! kaggle competitions submit -c jigsaw-toxic-comment-classification-challenge -f submission.csv -m "Upload"

4.5. 性能向上のための提案

4.5.1. 他のモデル

BERT以外にも多くの言語モデルがあります.新しいモデルの方が性能は良いですが,パラメータが多いのでオーバーフィッティングになりやすいかもしれません.

4.5.2. 学習率

現在の学習率は非常に小さいです.これはオーバーフィッティングを避けるのに役立ち,スコアが悪くなる可能性がありますが,訓練時間が長くなります.学習結果を観察し,学習率を上げてみてください.

4.5.3. エポック

現在のモデルは3エポックしか学習しません.TensorBoardでトレーニングの進捗を確認すると,検証損失とメトリクスが着実に向上しているように見えますが,これはもっとトレーニングすべき指標です.エポック数を増やしてみましょう.

4.5.4. パラメータ

現在,分類器の重みのみを更新しており,事前訓練された重みは更新していません.すべての重みを更新することで,より高いスコアが得られますが,過学習しやすくなります.さらに,すべてのパラメータをトレーニングすると,トレーニング時間とメモリ要件が増加します.

4.5.5. 学習の早期停止

多くのエポック数を学習すると,モデルの改善はどこかで止まってしまいます.PyTorch Lightningには,学習を早期に終了させる早期停止コールバックがあります.ドキュメンテーションを確認して,学習プロセスに追加できるか確認してください.