ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

機械学習によるテキスト分類(入門)

研究開発部の浅野です。普段は画像処理、信号処理、データ分析などを中心に行っていますが、自然言語処理についても今後の応用範囲が広そうなので理解を深めていきたいと思っています。自然言語処理には翻訳、対話応答、感情分析、要約など様々なタスクがある中で、今回は基本的かつ汎用的であるテキスト分類についてまとめたいと思います。テキスト分類とは、文章がどんな内容について書かれているかを調べ、それをもとにトピックごとに分類するタスクです。

ルールベースの手法

最も直感的な方法としては、例えばニュースの分類を行う際に、「日本」や「アメリカ」など複数の国名がでてきたら「国際」というカテゴリーにする、というように経験に基づくルールを設定するというものがあります。この場合「日本はWBC準決勝にて6-2でアメリカを下した。」という文も国際ニュースに分類されてしまうので、「WBC」や「勝ち/負け」に関する単語が入っている場合は「スポーツ」にする、といった新たなルールが必要です。この例からも容易に想像がつくように、多様な文章を適切に処理したり新たな言葉やトピックに対応する上でルールベースの手法は非常に手間がかかります。

機械学習による分類

そこで有効なのが、たくさんのデータをもとに効率よく分類する方法をコンピュータに習得させる機械学習です。機械学習によるテキスト分類には大きくわけて2つのフェーズがあり、それぞれのフェーズで多くの方法が提案されています。

f:id:unifa_tech:20200321223757j:plain:w500
機械学習によるテキスト分類の流れと種類
テキストをベクトル化する方法としては、文書内の単語の出現頻度をもとに算出するカウントベースの手法や、単語や文などの分散表現を算出するモデルを使用する推論ベースの手法があります。また、分類のフェーズでは、特異値分解や確率モデルを用いて指定したトピック数の群に分ける教師なし学習、あるいは決められたトピックに分類するための学習モデルを作成する教師あり学習が主に用いられます。

BERT

今回はベクトル化においてはBERTを、分類にはSVMを使用することにします。BERTは2018年10月に発表された単語や文の分散表現を計算する仕組みで、自然言語処理の様々なタスクへの応用で軒並み最高精度を叩き出した非常に汎用性の高いモデルです(とてもわかりやすい解説記事はこちら)。形態素に分解した文(のID)を入力すると、BERTはそれぞれに対応した768次元のベクトル(分散表現)を出力します。

f:id:unifa_tech:20200322210030j:plain
BERTの入出力(概略図)
文頭に付与する[CLS]というトークンに対応する出力は文全体の分散表現となるように事前学習が行われています。今回の分類においてもそれを利用します。複数の文からなるテキストに対しては例えば各文に対応するベクトルの平均をとることでそのテキスト全体のベクトル表現を得ることができます。

学習

BERTの学習には非常に時間がかかるので、日本語Wikipediaで事前学習済みのモデルをありがたく使用させてもらいます。ファインチューニングは行っていません。Keras BERTを使うことでモデルまわりの実装はとても容易です。また、SVMによる分類器の学習用データにはKNBコーパスを使いました。KNBコーパスは京都観光、携帯電話、スポーツ、グルメの4つのトピックに関するテキストが合計249記事含まれているデータセットです。BERTによる分散表現を用いて、各テキストが4つのうち正しいトピックに分類されるようなSVM分類器を学習します。SVMの実装もscikit-learnを使えば難なくできます。今回スクラッチから書いたのはコーパスをクレンジングしてSentencePieceで形態素に分け、BERTでEmbeddingを取得して学習/テストデータを作るところだけです。KNBコーパスに含まれるtsvファイルから必要な部分だけを抜き出すスクリプトの例を書いておきます。

import os
import pandas as pd

column_names = ['id_long', 'sentence', 'na0', 'na1', 'na2', 'na3']
get_index = lambda x: x.split('-')[0]

def get_corpus(tsv):
    df = pd.read_table(os.path.join(path_to_corpus, tsv), header=None)
    df.columns = pd.Index(column_names)
    df['id_short'] = df['id_long'].map(get_index) #文章ごとのindexを取得
    droplist = list(df.groupby('id_short')['sentence'].first()) #各文章の最初はタイトルなので削除
    droplist_index = df['sentence'].isin(droplist)
    corpus = df[~droplist_index]

    return corpus[['id_short', 'sentence']]

gourmet  = get_corpus('Gourmet.tsv')
kyoto = get_corpus('Kyoto.tsv')
keitai = get_corpus('Keitai.tsv')
sports = get_corpus('Sports.tsv')

6割のデータを使って学習をした後、残りの4割でテストを行った結果が下記です。f1-scoreのweighted averageでは95%と高い精度が出ました。さすがBERTです。

      {0: 'gourmet', 1: 'keitai', 2: 'sports', 3: 'kyoto'}

          precision    recall  f1-score   support
       0       0.95      0.90      0.92        20
       1       0.97      0.97      0.97        32
       2       1.00      0.71      0.83         7
       3       0.93      1.00      0.96        40
 wgt avg       0.95      0.95      0.95        99

      Confusion matrix: 
      [[18  0  0  2]
       [ 1 31  0  0]
       [ 0  1  5  1]
       [ 0  0  0 40]]

結果の例

具体的にどのような分類になるのかイメージするために次の文章の結果を紹介します。

  1. 両親をつれてたくさんのお寺めぐりをしながら紅葉を楽しんだ。
  2. 途中見つけた素敵なカフェで紅茶をいただいた。
  3. 外国からもたくさんの方が訪れていた。

まず、上記1の文だけを分類した場合には「京都」カテゴリに正しく分類されました。一方、1と2の文からなる文章を入力した場合は「グルメ」に分類されました。これは確かに人間でも迷うケースですね。そして最後に1,2,3の文からなる文章にすると、分類結果は「京都」になりました。日本語Wikipediaで一般的な知識を学習したBERTとKNBコーパスで文章の分類方法を学習したSVMで、かなり高度な判断ができるようになっていることがわかりますね。

まとめ

機械学習によるテキスト分類の流れと、BERTとSVMを使った例を見てきました。難しいチューニングなどを一切行わなくてもこれだけの精度がでてしまうというのはなかなか驚きです。BERTはテキスト分類だけでなく様々なタスクに応用が効くので、今後は弊社が展開しているサービスやインターネット上から得られる保育関連のテキストデータの処理にも挑戦していきたいと思っています。