この記事ではパーセプトロンを使って文書分類器を学習し、学習済みの分類器を使って文書を分類する流れをご紹介します。パーセプトロンはシンプルな分類アルゴリズムの一つである一方で、これを理解していると他の分類アルゴリズムを理解する助けになるため、初めて機械学習を学ぶ初学者の方にとってよい題材といえます。 この記事に載せているプログラムはここにまとまっています。

目次

パーセプトロンに基づく文書分類器

パーセプトロンに基づく文書分類器は、文書を表す素性ベクトル$\mathbf{x} \in \mathbb{R}^{K}$が与えられたとき、パラメータ$\mathbf{w} \in \mathbb{R}^K$との内積に基づいて、カテゴリ$y \in \{-1, 1\}$を予測します。

$$ \begin{eqnarray} y &=& \textrm{sign}(\mathbf{w} \mathbf{x}) \tag{1} \end{eqnarray} $$

ここでsign($z$)はzが0以上なら$1$、そうでなければ$-1$を返す関数とします。 マルチクラス分類問題を扱う際は、たとえばOne-Versus-Rest法で二値分類器をクラスの数だけ構築し、$\mathbf{w}\mathbf{x}$が最大となるカテゴリを予測結果として出力します。

パーセプトロンによる文書分類器の学習

式(1)によってそれらしい分類をするにはパラメータが学習する必要があります。パーセプトロンでは時刻$t$における学習事例 $(\mathbf{x}, y)$に対して、以下のような損失関数に基づいてパラメータの勾配を計算し、次の時刻のパラメータを計算します。

$$ \begin{eqnarray} L(\mathbf{w}^{(t)}) &=& \max(1-y \mathbf{w}^{(t)} \mathbf{x}, 0) \\\
\mathbf{w}^{(t+1)} &=& \mathbf{w}^{(t)} - \frac{\partial L(\mathbf{w}^{(t)})}{\partial \mathbf{w}^{(t)}} \\\
\quad &=& \mathbf{w}_{y_i}^{(t)} - L(\mathbf{W}) y \mathbf{x} \tag{2} \\\
\end{eqnarray} $$

式(2)から次のことが言えます。

  • 時刻$t$において$y\mathbf{w}\mathbf{x}$が1よりも大きな (つまり分類が正しくできている) 場合、$\max$は0を採用するため、勾配は更新しない。
  • $y\mathbf{w}\mathbf{x}$が1よりも小さな (分類が失敗している) 場合、勾配に基づいて1よりも大きくなるようにパラメータを更新する。

$y\mathbf{w}\mathbf{x}$が1より大きくなるには、少なくとも$y$と$\mathbf{w}\mathbf{x}$の符号が一致している必要があります。たとえば、$y=1$なら$\mathbf{w}\mathbf{x}>0$であれば、これは正解カテゴリが1の事例$\mathbf{x}$に対して正しくカテゴリを予測できていることになります。このような損失関数のもとで学習事例を使ってパラメータを更新することで、学習事例に対して適切にカテゴリを予測できるようにパラメータを更新します。

scikit-learnによる実装

文書分類にはライブドアコーパスを利用します。 ライブドアコーパスはタイトルと記事本文に対してカテゴリが付与されています。この記事では、タイトルをカテゴリに分類する文書分類器を構築します。

この記事で利用したOSS

  • Python 3.6.8 (Anaconda)
  • MeCab 0.996
  • mecab-python3 0.996.1
  • pandas 0.24.1
  • sklearn 0.20.2

前処理

前処理で利用するモジュールは以下になります。

import argparse
import csv
import os
import pickle

import pandas as pd
import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

ライブドアコーパスからタイトルを抽出する関数です。記事はファイル単位でカテゴリを表すディレクトリごとに配置されており、記事は1行目がURL、2行目が日付、3行目がタイトル、4行目から記事本文となっています。この記事では記事のタイトルがほしいため、ファイルごとに3行目のみ抽出します。

def read_file(dir_name):
    titles = []
    for file_name in os.listdir(dir_name):
        path = os.path.join(dir_name, file_name)
        with open(path) as f:
            _ = next(f)  # url
            _ = next(f)  # date
            title = next(f)
            titles.append(title.strip())
    return titles

以下の関数はカテゴリごとに分かれたディレクトリに配置された記事ファイルからタイトルを抽出し、タイトルにカテゴリを表すディレクトリ名を付与したデータを構築します。

def read_data(dir_name, tagger):
    categories = []
    titles = []
    for dir_or_file in os.listdir(dir_name):
        path = os.path.join(dir_name, dir_or_file)
        if os.path.isdir(path):
            titles_of_category = read_file(path)
            categories += [dir_or_file for _ in range(len(titles_of_category))]
            titles += [tokenize(tagger, t) for t in titles_of_category]

    df = pd.DataFrame({'category': categories, 'title': titles})
    return df

この関数により、分かち書きずみのタイトルに対してカテゴリがついた以下のようなデータが構築されます。

category                                              title
0   category A  分かち書きずみのタイトル
1   category B  分かち書きずみのタイトル

tokenize関数はtagger (ここではMeCab.Tagger) を用いてtextを分割する関数です。

def tokenize(tagger, text):
    result = tagger.parse(text).strip()
    return result

次は、

  1. ライブドアコーパスから見出しとカテゴリを読み込み、
  2. 見出しを分かち書きし、
  3. 学習データとテストデータに分割し、
  4. テキストを素性ベクトルへ変換し、
  5. ラベルをIDへ変換し、
  6. 学習データ、テストデータ、素性ベクトルへの変換器、ラベルの変換器を保存します。

以上のことをまとめて実施します。

parser = argparse.ArgumentParser()
parser.add_argument('--dir')
parser.add_argument('--out', default='data')
args = parser.parse_args()

tagger = MeCab.Tagger('-Owakati')

df = read_data(args.dir, tagger)

# 学習データとテストデータに分割
train, test = train_test_split(df)

vectorizer = TfidfVectorizer(
    input='content')
# 素性ベクトル変換器を構築し、学習データに適用
train_titles = train['title']
train_X = vectorizer.fit_transform(train_titles)

# テストデータに素性ベクトル変換器を適用
test_titles = test['title']
test_X = vectorizer.transform(test_titles)

label_encoder = LabelEncoder()
# ラベルをIDへ変換する変換器を構築し、学習データに適用
train_categories = train['category']
train_y = label_encoder.fit_transform(train_categories)

# 構築したラベル変換器をテストデータに適用
test_categories = test['category']
test_y = label_encoder.transform(test_categories)

if not os.path.exists(args.out):
    os.makedirs(args.out)

train_file = os.path.join(args.out, 'train.pickle')
test_file = os.path.join(args.out, 'test.pickle')
vectorizer_file = os.path.join(args.out, 'vectorizer.pickle')
label_encoder_file = os.path.join(args.out, 'label_encoder.pickle')

with open(train_file, 'wb') as f:
    pickle.dump([train_X, train_y], f)

with open(test_file, 'wb') as f:
    pickle.dump([test_X, test_y], f)

with open(vectorizer_file, 'wb') as f:
    pickle.dump(vectorizer, f)

with open(label_encoder_file, 'wb') as f:
    pickle.dump(label_encoder, f)

これで、文書分類器の学習と、予測に必要なデータの用意ができました。

パーセプトロンによる文書分類器の学習

前処理が終わったらいよいよ文書分類器を学習します。 といっても、学習部分はscikit-learnによって実装されているため簡潔に記述できます。

学習用のプログラムで利用するモジュールは以下の通りです。

import argparse
import pickle
import os

from sklearn.linear_model import Perceptron
from sklearn.metrics import precision_recall_fscore_support

学習用のプログラムでは、パーセプトロンに基づく文書分類器を学習データによって学習し、テストデータにおける分類器の評価値を算出するとともに、学習済み文書分類器を保存します。

parser = argparse.ArgumentParser()
parser.add_argument('--dir', default='data')
args = parser.parse_args()

train_file = os.path.join(args.dir, 'train.pickle')
with open(train_file, 'rb') as f:
    train_X, train_y = pickle.load(f)
    print('train', train_X.shape, train_y.shape)

test_file = os.path.join(args.dir, 'test.pickle')
with open(test_file, 'rb') as f:
    test_X, test_y = pickle.load(f)
    print('test:', test_X.shape, test_y.shape)

model = Perceptron(
    penalty='l2',
    shuffle=True,
    verbose=2)

# パーセプトロンを学習
model.fit(train_X, train_y)
# テストデータに対して予測
test_y_pred = model.predict(test_X)

# テストデータにおける適合率、再現率、F値を算出
precision, recall, fscore, _ = precision_recall_fscore_support(
    test_y,
    test_y_pred,
    average='micro')

print('Precision:', precision)
print('Recall:', recall)
print('F-score:', fscore)

model_file = os.path.join(args.dir, 'model.pickle')
with open(model_file, 'wb') as f:
    pickle.dump(model, f)

私の環境では以下の評価値となりました。

Precision: 0.702819956616052
Recall: 0.702819956616052
F-score: 0.702819956616052

学習済み文書分類器を使って未知のテキストのカテゴリを予測

学習済み文書分類器を構築出来たら、それを利用して実際に分類してみます。 予測用のプログラムで利用するモジュールは以下の通りです。

import argparse
import os
import pickle

import MeCab

import preprocess  # この記事の前処理用プログラム

未知のテキストに対して予測をする際は、学習時と同様に前処理で構築した素性ベクトル変換器に適用して得られた素性ベクトルを文書分類器の入力とします。

parser = argparse.ArgumentParser()
parser.add_argument('--dir', default='data')
args = parser.parse_args()

model_file = os.path.join(args.dir, 'model.pickle')
with open(model_file, 'rb') as f:
    model = pickle.load(f)

label_encoder_file = os.path.join(args.dir, 'label_encoder.pickle')
with open(label_encoder_file, 'rb') as f:
    label_encoder = pickle.load(f)

vectorizer_file = os.path.join(args.dir, 'vectorizer.pickle')
with open(vectorizer_file, 'rb') as f:
    vectorizer = pickle.load(f)

tagger = MeCab.Tagger('-Owakati')

while True:
    text = input()
    tokenized = preprocess.tokenize(tagger, text)

    x = vectorizer.transform([tokenized])
    y = model.predict(x)
    # ラベルをIDから対応する文字に変換
    label = label_encoder.inverse_transform(y)[0]
    print('Tokenized:', tokenized)
    print('Label:', label)

上記のプログラム (predict.py) を実行して、適当なテキストを入力してみます。

python predict.py
新しい家電
Tokenized: 新しい 家電
Label: kaden-channel
サッカーを観にいこう
Tokenized: サッカー を 観 に いこ う
Label: sports-watch

なんとなくそれらしいカテゴリを予測する文書分類器を構築出来ました。

おわり

本記事ではパーセプトロンの概要を説明し、scikit-learnを使って実際にパーセプトロンに基づく文書分類器を構築し、予測する流れをご紹介しました。 今回は素性ベクトルの設計は単純なものにしましたが、より高い分類精度を出したい場合はデータに合わせて設計を検討する必要があります。 この記事に載せているプログラムはここにまとまっています。

LSTMを使った文書分類の実装についても記事を書きましたのでよければこちらも御覧ください。

【自然言語処理】LSTMに基づく文書分類 (PyTorchコード付き)


関連記事






最近の記事