flairを使って最速でNLPのベースラインモデルを作る

f:id:nmoriyama:20200710133947p:plain

自然言語処理に限らず、機械学習関連のプロジェクトではスタート時は、なるべく複雑なコーディングをせずにシンプルなベースラインモデルを低コストで作成し、そこからデータの傾向やタスクの複雑さを把握することが重要です。

ところが自然言語処理では前処理のコストが高く、最低限でも単語分割、ベクトル化、深層学習を用いる場合は事前学習された埋め込みベクトルを準備する必要があります。その後は他のタスクと同様にモデルの保存方法や、予測のパイプラインで悩みポイントを抱えることが多いと思います。

最近はAutoMLを始めとした機械学習の自動化が進歩し、初手から高性能なモデルをブラウザ上で数クリックで作成できますが、中身がブラックボックスである故に前述のデータの傾向やタスクの複雑さを把握することを目的とした場合には適切とは言えない側面があります。

本記事では自然言語処理を対象にモデルの中身が参照可能でかつ少ないコーディング量でモデルを作成する方法を紹介します。具体的には自然言語処理ライブラリであるflairの機能を活用し、日本語のオリジナルデータに対して学習と予測を行います。

flairでは前処理や学習と予測のパイプライン等はすべて抽象化されており、独自で定義せずともライブラリの機能を呼び出すことで数行のコードで動かすことができます。初手のベースラインとしてすぐに実行できます。

前準備

文書分類として株式会社ロンウィットが配布するlivedoorコーパスを使って文書のカテゴリ推定を行います。

livedoorコーパスはNHN Japan株式会社が運営する「livedoor ニュース」から作成したものです。各ニュースの記事とともにカテゴリ情報が含まれます。 記事にはクリエイティブ・コモンズライセンス「表示 – 改変禁止」が適用されますので、取り扱う際はご注意ください。

解凍したファイルと同じディレクトリで以下の前処理を行います。 DataFrameに記事本文とカテゴリラベルを保持させ、train, text, devの3つに分けてCSVに保存します。

import glob
import pandas as pd
from sklearn.model_selection import train_test_split

categories = [
    "sports-watch", "topic-news", "dokujo-tsushin", "peachy",
    "movie-enter", "kaden-channel", "livedoor-homme", "smax",
    "it-life-hack",
]

docs = []
for category in categories:
    for p in glob.glob(f"./text/{category}/{category}*.txt"):
        with open(p, "r") as f:
            url = next(f)
            date = next(f)
            title = next(f)
            body = "\n".join([line.strip() for line in f if line.strip()])
        docs.append((category, body))
        
df = pd.DataFrame(docs, columns=['label', 'text'])
train_df, test_df = train_test_split(df, test_size=0.4, shuffle=True)
test_df, dev_df = train_test_split(test_df, test_size=0.5, shuffle=True)

train_df.to_csv('./processed/train.csv', index=False, header=False)
test_df.to_csv('./processed/test.csv', index=False, header=False)
dev_df.to_csv('./processed/dev.csv', index=False, header=False)

今回は簡易化のためにラベルと本文しか扱いませんが、必要に応じてタイトル文などの情報も利用してみると面白いでしょう。

これで準備は完了です。

flair

flairを用いてシンプルな文書分類モデルを作ります。

flairは固有表現認識(NER)や品詞タグ付け(POS)タスクを中心に最先端モデルを簡単に利用出来ることが特徴です。バックエンドにPyTorchが利用されています。

固有表現認識や品詞タグ付けで注目されていますが、テキスト分類もサポートされています(こちらは最先端は強く意識されていないようです)。

開発頻度は高く、ネットから入手できるサンプルコードは最新バージョンで動かないことが多いです。以降コードで使用するflairは執筆時点で最新バージョン(0.5.1)を使います。

flairでは単語分割や事前学習された埋め込みベクトルの利用など、多くのタスクに共通する処理がそれぞれモジュールとして組み込まれており、それらを入れ替えることで実験がしやすくなるようにデザインされています。

日本語サポートとして単語分割にMeCabやSudachiなど各種分割器を簡単に利用できるkonohaが使われています。 学習済み埋め込みベクトルもfasttextなど数種類が用意されています。

モジュールインポート

from flair.data import build_japanese_tokenizer
from flair.datasets import CSVClassificationCorpus
from flair.embeddings import WordEmbeddings, DocumentLSTMEmbeddings
from flair.models import TextClassifier
from flair.trainers import ModelTrainer

japanese_tokenizer = build_japanese_tokenizer(tokenizer="MeCab")

build_japanese_tokenizerの内部ではMeCabやSudachiなどの形態素分割器を扱えるkonohaが呼び出されるが、flair経由でkonohaを使う場合は執筆時点でMeCabしか利用できません。

学習データとしてとしてCSVファイル読み出しがサポートされており、ファイルパスと対象のカラムを指定するのみです。ミニバッチのサンプラーを書かずに済むのは便利ですね。

# データフォルダ。ここにtrain.csv, test.csv, dev.csvが含まれることが想定される
data_folder = './processed/'

# カラムのインデックスを指定
column_name_map = {1: "text", 0: "label_topic"}
corpus = CSVClassificationCorpus(
    data_folder,
    column_name_map,
    skip_header=True,
    tokenizer=japanese_tokenizer  # 日本語トークナイザーをセット
) 

学習データが用意できました。

次にモデルを考えます。比較的に目にすることが多い学習済みの単語埋め込みベクトルを入力に、LSTMの隠れ層から文書ベクトルを作成し、その文書ベクトルからクラス分類を行うモデルを構築します。

flairを使わずにこれをスクラッチで書く場合はデータのミニバッチサンプラーニューラルネットの各レイヤーの設定、学習ループ、モデルのアーキテクチャとパラメータの保存、予測のパイプラインをそれぞれ書く必要があります。

また、学習のloggingをしっかり取ろうとすると更に書く内容が増えます。

以下のflairの機能を活用して少ないコードで学習させます。

# ラベルをIDに変換
label_dict = corpus.make_label_dictionary()

# 単語ベクトルとして学習済みの日本語のfasttextベクトルを指定
word_embeddings = [WordEmbeddings('ja-crawl')]

# 文書ベクトル化のモデルを指定。ここではLSTMを選択
document_embeddings = DocumentLSTMEmbeddings(word_embeddings, hidden_size=256)

# 分類器
classifier = TextClassifier(document_embeddings, label_dictionary=label_dict)

# 学習ループを設定
trainer = ModelTrainer(classifier, corpus)
trainer.train('trained_model/',  # モデルの保存先
              learning_rate=0.01,
              mini_batch_size=32,
              anneal_factor=0.5,
              patience=5,
              max_epochs=10)

学習ループのtrainerは大変シンプルですね。上記の処理をスクラッチで書く場合のコード量は短くても上の10倍はかかると思います。

また、勾配を持たない埋め込みベクトル層はCPUメモリに置き、勾配を保持させるパラメータだけをGPUメモリに置くなど、GPUメモリを節約してくれるテクニックをデフォルトで実行してくれます(学習時間は増えますが)。

更にearly_stoppingといった機能もデフォルトで用意されていることは大変便利です。

学習後にtrainer.train()の第一引数で指定したフォルダにモデル、学習のログ、スコアなどの情報が保存されます、学習済みモデルを呼び出して予測を行います。

学習中にKeyboardInterrupt で中断される場合は。その時点のモデルを保存してくれる設計も親切です。

デフォルトの設定では以下のようなログが出力されます

2020-07-10 09:53:03,398 epoch 5 - iter 13/139 - loss 1.67030419 - samples/sec: 0.76
2020-07-10 10:00:33,105 epoch 5 - iter 26/139 - loss 1.69300615 - samples/sec: 0.94
2020-07-10 10:07:57,656 epoch 5 - iter 39/139 - loss 1.68367987 - samples/sec: 0.95
2020-07-10 10:14:45,635 epoch 5 - iter 52/139 - loss 1.68211064 - samples/sec: 1.05
2020-07-10 10:21:46,314 epoch 5 - iter 65/139 - loss 1.67768772 - samples/sec: 1.00
2020-07-10 10:29:30,703 epoch 5 - iter 78/139 - loss 1.68330931 - samples/sec: 0.91
2020-07-10 10:37:07,806 epoch 5 - iter 91/139 - loss 1.68172456 - samples/sec: 0.92
2020-07-10 10:44:45,063 epoch 5 - iter 104/139 - loss 1.67901101 - samples/sec: 0.92
2020-07-10 10:51:53,324 epoch 5 - iter 117/139 - loss 1.67862995 - samples/sec: 0.99
2020-07-10 10:59:34,579 epoch 5 - iter 130/139 - loss 1.67751856 - samples/sec: 0.93
2020-07-10 11:04:07,374 ----------------------------------------------------------------------------------------------------
2020-07-10 11:04:07,376 EPOCH 5 done: loss 1.6784 - lr 0.0100000
2020-07-10 11:30:03,295 DEV : loss 1.616660714149475 - score 0.7924
2020-07-10 11:30:19,412 BAD EPOCHS (no improvement): 0
saving best model

予測の確認として学習データに含まれない2020/7/10時点のlivedoorニュースのスポーツカテゴリの記事を読み込ませます

https://news.livedoor.com/article/detail/18550966/

from urllib.request import urlopen
from bs4 import BeautifulSoup
from flair.data import Sentence

# 記事本文をダウンロード
url = 'https://news.livedoor.com/article/detail/18550966/'
soup = BeautifulSoup(urlopen(url))
text = soup.find(class_="articleBody").text

# 保存モデルを呼び出し予測する
classifier = TextClassifier.load('trained_model/best-model.pt')
sentence = Sentence(text, use_tokenizer=japanese_tokenizer)
classifier.predict(sentence)
print(sentence.labels)

>>>
[sports-watch (0.571)]

NEXTアクション

ベースラインモデルの予測結果から様々なエラー分析が可能です。livedoorコーパス固有のエラー分析は今回では取り上げませんが一般的な精度向上カスタマイズを紹介します

  • 語彙に含まれない単語が多い
    事前学習された単語埋め込みに含まれない単語がデータに多く出現する場合、FlairEmbeddingが有効です。これは事前学習された文字の埋め込みから単語の埋め込みを構築する手法でflairの目玉機能です。日本語では2000種ほどの文字が学習されていますので、多くの単語に変換することが可能です。

  • LSTM以外の方法で文書ベクトルを試したい
    より高機能なBERTを使った文書ベクトル化はTransformerDocumentEmbeddingsがサポートされていますので、上記のDocumentLSTMEmbeddingsと差し替えることでBERTを組み込むことができます。

  • 文書ベクトル以外の情報を扱いたい
    少し発展的な構造になりますがflairの特徴である固有表現情報と文書を組み合わせて分類するモデルを書く場合はflairの機能で完結します。

以上flairで簡単にモデルを作成する方法を紹介しました。

私が勤めるMNTSQでは自然言語処理エンジニアを募集しておりますので、自然言語処理を活用する仕事を探していましたらぜひ一度ご検討お願いします。