1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

今さらだけど、日本語のテキスト分類を学習してみた

Last updated at Posted at 2024-08-09

きっかけ

O'reillyの「機械学習エンジニアのためのTransformers」でHuggingFaceの使い方を勉強していたのさ。
前にも書いた通り、HuggingFaceについて人前で話す機会を得たんだけど、全然理解できていなくて、再勉強中なのよね。
いわゆる2周目。

より深く理解したくて、2章 テキスト分類をなんとなく日本語でやってみたくてググったのさ。

いい記事↓があったので真似してみようと思ったのさ

環境

M1 MacbookAir2020
python:3.10.8
scikit-learn==1.5.1
matplotlib==3.9.0
transformers==4.43.4
torch==2.4.0
sentencepiece==0.2.0
datasets==2.20.0
fugashi==1.3.2
unidic-lite==1.0.8

(transfromers, torch, sentencepieceはpip install transformers[torch, sentencepiece]でインストール

参考書籍

もうね、僕のバイブル

ざっくりした手順

基本的には参考にしたサイトそのまんま実行です。

  1. データセットの準備
  2. トークナイザを用意して、テキストをトークン化したものをデータセットに追加
  3. モデルのダウンロード
  4. 学習条件の設定と学習
  5. 評価

1.データセットの準備

参考にしたホームページの通り、ライブドアニュースコーパスをダウンロード
以下のリンクの中でも、ldcc-20140209.tar.gzを使いました。真似っこ。

tar.gzファイルは圧縮ファイルなので、解凍します。これも参考にしたhpの真似っこ。そして、ipynbと同じ階層にtextフォルダを作って保存しておきました。
xvfって確か、extractとvarboseと・・・なんだっけ?

tar -xvf ldcc-20140209.tar.gz

まぁライブラリのインストールとファイルの読み込みぐらいやっときましょう。

import os
from glob import glob
import pandas as pd
from sklearn.metrics import classification_report

フォルダ名がカテゴリ名になっているのでリストを作っておきます。

categories = glob('./text/*')
categories = [os.path.basename(x) for x in categories if not x.endswith('.txt')]
categories

さらにidに変換するための辞書を作っておく。

cat2id = {v:i for i, v in enumerate(categories)}
cat2id

さらにデータを見てみると各ファイルの3行目から文字がある。さらに読み込んでみると改行が鬱陶しいので、改行を削除するための関数を作る。

def file2text(file):
    with open(file, 'r', encoding='utf8') as f:
        lines = f.readlines()

    text = ''
    
    for line in lines[2:]:
        text += line.replace('\n', '')

    return text

そしていよいよ各ファイルを読み込んで、cat(カテゴリ)とtextをcolumnにしたデータフレームを作っちゃいます。
僕は辞書から作るのが好きなんで、この方式で。
それと、ある意味動作確認なので200データだけ抽出します。

data_dic = {
    'cat': [],
    'text': [],
}
                       
for cat in categories:
    print(cat)
    files = glob(f'./text/{cat}/*.txt')
    for i, file in enumerate(files):
        data_dic['cat'].append(cat)
        data_dic['text'].append(file2text(file))

dataset_df = pd.DataFrame(data_dic)
# dataset_df = dataset_df.sample(frac=1, random_state=0).reset_index(drop=True) # fracは抽出割合
# dataset_df = dataset_df[:200]
dataset_df = dataset_df.sample(200, random_state=0).reset_index(drop=True)
dataset_df

ここで使ったsample関数。滅多に使わないんですが、便利っすね!
最高!

ここで、先に作ったカテゴリをidに変換する関数を使います。lambda関数を使ってもいいけど、今回はmap関数に慣れてみよう。

dataset_df['label'] = dataset_df['cat'].map(cat2id)
dataset_df = dataset_df[['text', 'label']]
dataset_df

いよいよdataset形式への変換です。
なんと、これだけ。HuggingFaceの良さはデータセットの作りやすさかもしれません。

from datasets import Dataset

dataset = Dataset.from_pandas(dataset_df)
dataset

# 出力
# Dataset({
#     features: ['text', 'label'],
#     num_rows: 200
# })

ここまでで、datasetの準備完了!
と思いたいところですが、モデルに入れるにはトークナイザを通さねばいけません。
学習の都度トークナイザーをなん度も通すのはロスなので先に変換しちゃいましょう。

2.トークナイザを用意して、テキストをトークン化したものをデータセットに追加

まずはトークナイザを用意しましょう。

from transformers import AutoTokenizer

tokenizer= AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-v2')

datasetには強力なmap関数を備えていて、一気にぜ〜んぶ処理できちゃいます。
そのために事前に関数を作っておいて、map関数でその関数を渡しちゃいます。

def preprocess_function(examples):
    MAX_LENGTH = 512
    return tokenizer(examples["text"], max_length=MAX_LENGTH, truncation=True)

tokenized_dataset = dataset.map(preprocess_function, batched=True)
tokenized_dataset

でお馴染み、学習用データと検証用データに分けます。これもとっても簡単。この辺りがdatasetライブラリの強さなのかなと思います。正直、感動もんです。

tokenized_dataset = tokenized_dataset.train_test_split(test_size=0.2)
tokenized_dataset

# 出力
# DatasetDict({
#     train: Dataset({
#         features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
#         num_rows: 160
#     })
#     test: Dataset({
#         features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
#         num_rows: 40
#     })
# })

3.モデルのダウンロード

そしていよいよモデルのダウンロード。あ、のちのために必要なclassもインポートしておきましょう。

from transformers import DataCollatorWithPadding
from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
model = AutoModelForSequenceClassification.from_pretrained("cl-tohoku/bert-base-japanese-v2", num_labels=len(categories))

先に評価指標を作っておきますね。

from sklearn.metrics import accuracy_score, f1_score

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    f1 = f1_score(labels, preds, average='weighted')
    acc = accuracy_score(labels, preds)
    return {'accuracy':acc, 'f1':f1}

4.学習条件の設定と学習

そして、学習用条件を作って、Trainerクラスのインスタンスを作ります。
後で、この辺が理由でエラー出ます。

training_args = TrainingArguments(
    output_dir="./results",
    num_train_epochs=2,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    weight_decay=0.01,
    # evaluation_strategy='epoch',
    eval_strategy='epoch',
    logging_strategy='epoch',
    save_strategy='epoch',
    save_total_limit=1,
    learning_rate=2e-5,
    use_cpu=True, # GPUを使用する場合はFalse
)

trainer = Trainer(
    model=model,
    args=training_args,
    compute_metrics=compute_metrics,
    train_dataset=tokenized_dataset['train'],
    eval_dataset=tokenized_dataset['test'],
    tokenizer=tokenizer,
    data_collator=data_collator,
)

trainer.train() # 学習

こんなエラー。

ValueError: You are trying to save a non contiguous tensor: `bert.encoder.layer.0.attention.self.query.weight` 
which is not allowed. It either means you are trying to save tensors which are reference of each other in which
case it's recommended to save only the full tensors, and reslice at load time, or simply call `.contiguous()` on 
your tensor to pack it before saving.

簡単にいうと、モデルをセーブしようとしているけど、モデルの一部は初期化されていて繋がっていないよ。だから保存したければ、.contiguous()関数を使って、セーブ前に繋げなさいよ。
そんな感じ。

なので、Trainerクラスのsave_model()関数を修正することにします。
Trainerクラスを継承して、新しく、CustomTrainerクラスを作りました。
この辺はChatGPTさんと綿密に対話させていただきました。笑
GPTさん、マジすげぇ。

# Trainerクラスを拡張して保存する前にテンソルを連続化する
class CustomTrainer(Trainer):
    def save_model(self, output_dir=None, _internal_call=False):
        if output_dir is None:
            output_dir = self.args.output_dir
        
        self.model = self.model.to('cpu')  # モデルをCPUに移動
        
        # すべてのテンソルを連続化する
        for param in self.model.parameters():
            if not param.is_contiguous():
                param.data = param.data.contiguous()
        
        super().save_model(output_dir, _internal_call)

作り直したCustomTrainerクラスのインスタンスを作って学習開始です。

trainer = CustomTrainer(
    model=model,
    args=training_args,
    compute_metrics=compute_metrics,
    train_dataset=tokenized_dataset['train'],
    eval_dataset=tokenized_dataset['test'],
    tokenizer=tokenizer,
    data_collator=data_collator,
)

trainer.train() # ここで実行

せっかくなので保存しておきます。保存先はtraining_argsで設定したresultフォルダです。

trainer.save_state()
trainer.save_model()

5.評価

評価結果を見てニヤニヤしましょう。

from sklearn.metrics import classification_report
print(classification_report(tokenized_dataset['test']['label'], pred_label, target_names=categories, zero_division=0))

まとめ

今回Qiitaに書こうと思ったのでちゃんとエラー回避できたから。
参考にさせていただいた記事とはライブラリのバージョンも異なるし、モデルもちょっと変わっているのかもしれません(調べてない)
その結果生まれたエラー、それも、僕にとっては結構難解だったのですが、GPTに理由を聞き、対象のクラスのプログラムをしっかり読み、かつ、対策についてGPTと議論した結果解決に至ったということをお伝えしたかったんです。

GPTに聞く時は前提条件をし〜っかりとしつこいほどに伝えるといい回答が得られますね。いつかはGPTのような賢くて、さらにローカルPCで動くものが出てくることを期待しています。

また、テキスト分類のチューニングが意外と身近だということがわかったし、活用する場面はアイデアがいくつか浮かんできています。
手を動かしてみるってやっぱりいいですね。

皆さんはテキスト分類、何に使いますか?
僕は自分が作ったプログラムがなんらかの理由でエラーを時に、その時のログを入れたらどんなエラーで止まったかを出力させてみようと思ってます。
あ、ログって基本英語で書いてるから日本語じゃなくて良かったかも。w

GitHubはこちら(マナビDXクエストアルムナイの次回HuggingFaceモクモク会のネタにしようと思ってる)

最後に宣伝

イベントのお手伝いしてます。
場所は名古屋駅近く、平日で現地開催のみですが、ぜひご参加ください。

ではまた〜

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?