【LLM】大規模言語モデルBERTでニュース記事をカテゴリに分類

本記事ではGPUSOROBANのインスタンスを使い、事前学習済みBERTモデルをファインチューニングする方法を紹介します。


GPUSOROBANは高性能なGPUインスタンスが低コストで使えるクラウドサービスです。
サービスについて詳しく知りたい方は、GPUSOROBANの公式サイトを御覧ください。


目次[非表示]

  1. 1.BERTとは
  2. 2.環境構築
  3. 3.データセットの準備
  4. 4.モデルとTokenizerの読み込み
  5. 5.評価関数の定義
  6. 6.Trainerの設定
  7. 7.モデルの学習
  8. 8.結果の確認


BERTとは


BERTは、Bidirectional Encoder Representations from Transformers(トランスフォーマーによる双方向エンコーダ表現)の略称で、2018年にGoogleが提案した大規模言語モデル(LLM)です。


BERTは、Transformerと呼ばれるニューラルネットワークアーキテクチャを基にしています。Transformerは、Attentionを利用して文章内の単語間の関連性を学習することができるモデルであり、それによって長い文章でも文脈を理解できる特徴があります。


ChatGPTの大規模言語モデルであるGPTにもTransfomerが使われていることもあり、BERTとGPTのアーキテクチャは似ています。
OpenAI GPTのアーキテクチャ は、図中では黒矢印で示されているとおり、左から右への学習を最終層で連結する形です。一方でBERT のアーキテクチャの特徴はその名の示すとおり双方向(=Bidirectional)であり、全ての層で双方向の学習を行っています。


双方向の学習とは、単語や文の双方向からのコンテキストを考慮した学習を意味します。これにより、単語の意味や文脈をより豊かに理解することができます。単語の前後の情報を利用することで、単語の意味や解釈が異なる文脈においても正確に捉えることができます。

BERT

BERTの学習には事前学習とファインチューニングの2つのステップがあります。
事前学習は、大規模なテキストコーパスを使用してBERTモデルを事前に学習するプロセスです。この学習では、入力文の一部をマスクし、BERTが欠損した単語を予測するタスクや、文章の関係性を予測するタスクなどが用いられます。


一方のファインチューニングは、特定のタスクのデータセットを利用して、事前学習したモデルに追加学習をします。タスクに応じた層の追加とタスクに適したデータセットでの学習を行います。ファインチューニングは学習済みモデルの一部も学習します。
ファインチューニングを行い、特定タスクへのモデルを適応させることで、パフォーマンスの向上が期待できます。

ファインチューニング


本記事ではBERTを使い、ニュースの分類をするモデルを実装をします。
ニュース記事のデータには、公開データであるlivedoorニュースコーパスを使用します。記事数は7367件から成り、各ニュース記事には下記の9種類のカテゴリが割り振られています。記事の内容をインプットし、適切なカテゴリを予測する問題になります。


<9種類のニュースカテゴリ>

トピックニュース

Sports Watch

ITライフハック  

家電チャンネル 

MOVIE ENTER

独女通信 

エスマックス

livedoor HOMME   

Peachy


環境構築


環境はGPUSOROBANのnvd4-80-1ulインスタンスを使用します。
nvd4-80-1ulは、NVIDIA A100を搭載した高性能GPUインスタンスです。機械学習を高速化するTensorコアや大容量GPUメモリ(80GB)が特徴です。後にGoogle ColaboのNVIDIA T4との学習時間を比較してみます。


GPUSOROBANのインスタンスの作成方法、秘密鍵の設置方法については、会員登録~インスタンス作成手順の記事をご覧ください。


インスタンスの作成と秘密鍵の設定が完了しましたら、アクセスサーバーおよびインスタンスに接続をします。
本記事ではJupyterLabおよびTensorBoardを使用するため、上記の手順書のインスタンス接続のコマンドと異なりますので、ご注意ください。


アクセスサーバーへの接続


ssh -L 20122:(インスタンスのIPアドレス):22 -l user as-highreso.com -p 30022 -i .ssh\ackey.txt


インスタンスへの接続方法


ssh -L 8888:localhost:8888 -L 6006:localhost:6006 user@localhost -p 20122 -i .ssh\mykey.txt


インスタンス接続が完了しましたら、PyTorch、JupyterLabをインストールします。


(参考)PyTorchのインストール(Ubuntu)の記事
(参考)Jupyter Labのインストール(Ubuntu)の記事


Jupyterを起動したら、下記のライブラリをインストールします。


!pip install transformers==4.28.0
!pip install nlp
!pip install datasets
!pip install fugashi
!pip install ipadic
!pip install sentencepiece
!pip install scikit-learn
!pip install tensorboard


ライブラリのインストール


データセットの準備


ニュース記事のデータセットを準備します。
次のコマンドでlivedoorニュースコーパスをダウンロードします。


!wget "https://www.rondhuit.com/download/ldcc-20140209.tar.gz"



ライブラリインストール


ダウンロードしたファイルを解凍し、解凍後のディレクトリについて、カテゴリーの数とカテゴリー名を表示します。


# ファイルを解凍し、カテゴリー数と内容を確認
import tarfile
import os
# 解凍
tar = tarfile.open("ldcc-20140209.tar.gz", "r:gz")
tar.extractall("./data/livedoor/")
tar.close()
# ディレクトリのファイルとディレクトリを確認
files_folders = [name for name in os.listdir("./data/livedoor/text/")]
print(files_folders)
# カテゴリーのディレクトリのみを抽出
categories = [name for name in os.listdir(
    "./data/livedoor/text/") if os.path.isdir("./data/livedoor/text/"+name)]
print("カテゴリー数:", len(categories))
print(categories)


データセットの準備


ニュースカテゴリー数は9であることが分かります。
また出力されたカテゴリは、冒頭で紹介したlivedoorのニュースカテゴリに対応しています。


topic-news : トピックニュース

sports-watch : Sports Watch   

it-life-hack : ITライフハック   

kaden-channel : 家電チャンネル   

movie-enter : MOVIE ENTER   

dokujo-tsushin : 独女通信 

smax : エスマックス

livedoor-homme : livedoor HOMME   

peachy : Peachy  


続いてニュース記事が格納されているディレクトリ内のニュース記事のテキストファイルを読み込み、テキストとそれに対応するラベル(カテゴリ)をセットとして格納します。


import glob  # ファイルの取得に使用
import os
path = "./data/livedoor/text/"  # ディレクトリの場所を指定
dir_files = os.listdir(path=path)#指定したpathのファイルとディレクトリの一覧を取得
dirs = [f for f in dir_files if os.path.isdir(os.path.join(path, f))]  # ディレクトリをリストとして取り出す
text_label_data = []  # 文章とラベルのセット
dir_count = 0  # ディレクトリ数のカウント
file_count= 0  # ファイル数のカウント
for i in range(len(dirs)):#ディレクトリの数だけループ処理
    dir = dirs[i]#ディレクトリの名前を取り出す
    files = glob.glob(path + dir + "/*.txt")  # ファイルの一覧
    dir_count += 1
    for file in files:
        if os.path.basename(file) == "LICENSE.txt":#LICENSE.txtは除外する(ループをスキップ)
            continue
        with open(file, "r") as f:#ファイルを開く
            text = f.readlines()[3:]#指定の行だけ読み込む 4行目以降を読み込む
            text = "".join(text)#リストなのでjoinで結合する 空の文字列に結合して一つの文字列にする
            text = text.translate(str.maketrans({"\n":"", "\t":"", "\r":"", "\u3000":""})) #不要な文字を除去する
            text_label_data.append([text, i])#本文とディレクトリ番号をリストに加える
        file_count += 1
        print("\rfiles: " + str(file_count) +" "+ "dirs: " + str(dir_count), end="")


ニュース確認


出力結果は、ニュース記事のファイル数は7367件で、ニュース記事カテゴリのディレクトリが9つであることを表しています。

次にデータを学習データとテストデータに分割し、csvファイルとして保存します。


import csv
from sklearn.model_selection import train_test_split
news_train, news_test =  train_test_split(text_label_data, shuffle=True)  # 学習用とテスト用に分割
news_path = "./data/"
with open(news_path+"news_train.csv", "w") as f:#学習データ
    writer = csv.writer(f)#writerを作る
    writer.writerows(news_train)#csvファイルとして書き込み
with open(news_path+"news_test.csv", "w") as f:#テストデータ
    writer = csv.writer(f)
    writer.writerows(news_test)


データを学習データとテストデータに分割し、csvファイルとして保存


モデルとTokenizerの読み込み


日本語の事前学習済みBERTモデルと、これに紐づいたTokenizerを読み込みます。
BertForSequenceClassificationは、BERT学習済みモデルに分類レイヤーを追加した構成となり、テキスト文の分類タスクに対応するものです。BertJapaneseTokenizerは、日本語の文を単語(トークン)に分割し、それぞれのトークンに対してIDを割り当てます。IDを割り当てることにより、テキストデータが数値データとして処理され、モデルが理解できるようになります。


from transformers import BertForSequenceClassification, BertJapaneseTokenizer
sc_model = BertForSequenceClassification.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking", num_labels=9)#事前学習済みBERTモデルの読み込み
sc_model.cuda()#GPU対応
tokenizer = BertJapaneseTokenizer.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")#事前学習済みのBERTトークナイザーを読み込み


モデルとTokenizerの読み込み


datasetsモジュールを使用して、ニュース記事のデータセットを読み込み、tokenizerでトークン化(単語分割)を行い、データフォーマットの指定をします。


from datasets import load_dataset
def tokenize(batch):
    return tokenizer(batch["text"], padding=True, truncation=True, max_length=512) #受け取ったバッチからtextデータを取り出してtokenizerに入れる
news_path = "./data/"
train_data = load_dataset("csv", data_files=news_path+"news_train.csv",column_names=["text", "label"], split="train")#CSVデータの読み込み #カラムの指定,#学習データとテストデータは事前に分割済みのためtrainを指定
train_data = train_data.map(tokenize, batched=True,batch_size=len(train_data))#単語ごとに分割する  #バッチサイズは学習データすべてを指定
train_data.set_format("torch", columns=["input_ids", "label"])#学習データのフォーマット指定,Pytorchを指定,input_idsとlabelのカラムを指定
test_data = load_dataset("csv", data_files=news_path+"news_test.csv", column_names=["text", "label"], split="train")#学習データとテストデータは事前に分割済みのためtrainを指定
test_data = test_data.map(tokenize, batched=True, batch_size=len(test_data))
test_data.set_format("torch", columns=["input_ids", "label"])


datasetsモジュールを使用して、ニュース記事のデータセットを読み込み、tokenizerでトークン化(単語分割)を行い、データフォーマットの指定


評価関数の定義


sklearn.metricsを使用し、モデルを評価するための関数を定義します。
accuracy_scoreは、分類モデルの予測結果の正解率(accuracy)を計算します。全サンプルのうち、予測結果と実際の正解クラスが一致する数の割合が正解率になります。


from sklearn.metrics import accuracy_score
def compute_metrics(result):
    labels = result.label_ids
    preds = result.predictions.argmax(-1)
    acc = accuracy_score(labels, preds)
    return {
        "accuracy": acc,
    }


評価関数の定義


Trainerの設定


学習を行うTrainerの設定を行います。TrainerはHuggingfaceが提供するモデル学習と評価を簡単に行うためのクラスです。
Trainerでは、学習モデルや評価関数、データセットなどの指定を行います。TrainingArgumentsでは、ハイパーパラメータを指定するものです。エポック数やバッチサイズ、学習率スケジューラ、重みの減衰率などを指定しています。


from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
    output_dir = "./data/results",
    num_train_epochs = 2,
    per_device_train_batch_size = 8,
    per_device_eval_batch_size = 32,
    warmup_steps = 500,  # 学習係数が0からこのステップ数で上昇
    weight_decay = 0.01,  # 重みの減衰率
    logging_dir = "./data/logs",
    evaluation_strategy = "steps"
)
trainer = Trainer(
    model = sc_model,
    args = training_args,
    compute_metrics = compute_metrics,
    train_dataset = train_data,
    eval_dataset = test_data,
)



Trainerの設定


モデルの学習


上記の設定に基づきファインチューニングを行います。


trainer.train()



trainer.train()実行結果


GPUSOROBANのnvd4-80-1ulインスタンスでは、5分34秒で学習が完了しました。
Google Colaboと比較するためにバッチサイズを8に設定しましたが、バッチサイズを上げてさらに学習時間を短縮することもできます。


一方Google ColaboのNVIDIA T4インスタンスでは、学習の完了までに20分47秒かかりました。
(Google colaboの学習結果↓)

Google colaboの学習結果


続いてTrainerのevaluate()メソッドによりモデルを評価します。
評価の結果は後のTensorBoardでグラフとともに確認します。


trainer.evaluate()


trainer.evaluate()実行結果


結果の確認


TensorBoardを使って、logsディレクトリに格納された学習過程を表示します。


%load_ext tensorboard
%tensorboard --logdir ./data/logs


trainer.train()


学習時の誤差(loss)は、0.934になりました。

Trainerのevaluate()メソッドモデル評価


評価時の誤差(loss)は0.33です。

Trainerのevaluate()メソッドによるモデル評価


評価時の精度(accuracy)は0.94であるため、分類の精度が94%になることが分かります。

Trainerのevaluate()メソッドによるモデルを評価


学習済みのモデルを保存します。


news_path = "./data/"
sc_model.save_pretrained(news_path)
tokenizer.save_pretrained(news_path)


モデルの読み込み


読み込んだモデルを使いニュースを分類します。


import glob  # ファイルの取得に使用
import os
import torch
import random
category_list = ["dokujo-tsushin", "it-life-hack", "livedoor-homme","kaden-channel", "movie-enter", "sports-watch","smax","topic-news","peachy"]
category = random.choice(category_list)
#print("dirname:", category)
random_number = random.randrange(1, 99)
#print(random_number)
sample_path = "./data/livedoor/text/"  # ディレクトリの場所を指定
files = glob.glob(sample_path + category + "/*.txt")  # ファイルの一覧
file = files[random_number]  # 適当なニュースを1つ取り出した
file_name = os.path.basename(file)
#print("filename:", file_name)
dir_files = os.listdir(path=sample_path)
dirs = [f for f in dir_files if os.path.isdir(os.path.join(sample_path, f))]  # ディレクトリ一覧
with open(file, "r") as f:
    sample_text = f.readlines()[3:]
    sample_text = "".join(sample_text)
    sample_text = sample_text.translate(str.maketrans({"\n":"", "\t":"", "\r":"", "\u3000":""})) 
print(sample_text)
max_length = 512
words = loaded_tokenizer.tokenize(sample_text) #torknizeを行う
word_ids = loaded_tokenizer.convert_tokens_to_ids(words)  # 単語をインデックスに変換
word_tensor = torch.tensor([word_ids[:max_length]])  # テンソルに変換 スライスを使って単語が512より多い場合は切る
x = word_tensor.cuda()  # GPU対応
y = loaded_model(x)  # 予測
pred = y[0].argmax(-1)  # 最大値のインデックス
print("predict-result:", dirs[pred])



ニュースの分類



以下にニュースの分類結果をいくつか紹介します。
次の記事は、movie-enter(MOVIE ENTER )のカテゴリに分類されています。
記事の内容は映画になっているので、記事内容とカテゴリは一致していることが分かります。

movie-enter記事


次の記事は、dokujyo-tsushin(独女通信 )のカテゴリに分類されています。
独女通信は独身女性の心理を取り上げるニュースとのことで、記事内容とカテゴリは合っています。

dokujyo-tsushin


次の記事は、kaden-channel(家電チャンネル)のカテゴリに分類されています。
家電に関する記事内容になりますので、カテゴリと一致しています。

kaden-channel


次の記事は、sports-watch(Sports Watch)のカテゴリに分類されています。
記事の内容はスポーツに関することですので、正しく分類されています。

sports-watch


次の記事は、it-life-hack(ITライフハック)のカテゴリに分類されています。
ITサービスに関する事が書かれているため、カテゴリと一致しています。

it-life-hack


BERTでニュース記事のカテゴリを分類する実装の説明は以上になります。
本環境には、GPUSOROBANのインスタンスを使用しました。


GPUSOROBANは高性能なGPUインスタンスが低コストで使えるクラウドサービスです。
サービスについて詳しく知りたい方は、GPUSOROBANの公式サイトをご覧ください。

MORE INFORMATION

GPUでお困りの方はGPUSOROBANで解決!
お気軽にご相談ください

10日間無料トライアル
詳しい資料はこちら
質問・相談はこちら