簡単なclassification

Python-nltkを使って簡単なclassificationをやっていきたいと思います.
今回使うのはロイターコーパスです(nltk.corpus.reuters).
このコーパスにはクラス(カテゴリ)がいくつかありますが,とりあえず2クラスのclassificationをやってみたいので,適当に2クラス選びます.

import nltk
len(nltk.corpus.reuters.categories())

でクラス数を調べると, 90 クラスあるみたいです.
次に,

from nltk.corpus import reuters
for category in sorted(reuters.categories(), key=lambda x:len(reuters.fileids(categories=x)), reverse=True):
    print category,
    print len(reuters.fileids(categories=category))

で各クラスに属する文書数を調べます.どれを選びましょうか.笑
なるべく2つのクラスが関係なさそうで,かつ文書数が同じくらいのを選びましょうか.
“ship”(船)は286件,”wheat”(小麦)は283件で,互いにあまり関係なさそうなのでこれにします.
さて,クラスが決まりました.次に教師ありか教師なしかをどうするかですね.今回はとりあえず教師ありにしましょう.
というわけでデータを訓練データとテストデータに分けます.
ロイターコーパスではfileidを使って分けることができます(もっと良い方法を知っている方は教えてください).
以下で訓練データとテストデータを分けます(正確には訓練データのfileidたちとテストデータのfileidたち).

from nltk.corpus import reuters
class1, class2 = "ship", "wheat"

train_fileids1 = [fileid for fileid in reuters.fileids(categories=class1) if "train" in fileid]
train_fileids2 = [fileid for fileid in reuters.fileids(categories=class2) if "train" in fileid]
test_fileids1 = [fileid for fileid in reuters.fileids(categories=class1) if "test" in fileid]
test_fileids2 = [fileid for fileid in reuters.fileids(categories=class2) if "test" in fileid]

print len(train_fileids1), len(test_fileids1)
print len(train_fileids2), len(test_fileids2)

こんな感じですかね.
shipでは訓練データが197件,テストデータが89件,
wheatでは訓練データが212件,テストデータが71件となりました.

さて,次はこれらをベクトルで表現します.
どう表現するかは重要ですが,言い出すときりがないし,今回の趣旨から外れるので,単純に単語の頻度とします.
ここで言う頻度は,ある文書での単語iの出現回数を,総単語数で除したものです.
ここから先はコードを観て頂いた方が理解しやすいと思います.説明が下手なので….
ベクトルの次元をそろえるために,共通の単語集合Vを用いて各文書をベクトルで表現することとします.
というわけでVを求めます.なお,Vはトレーニングデータに出てくる単語とします.

V = set()
for fileid in train_fileids1+train_fileids2:
    V = V.union(set(reuters.words(fileid)))
print len(V)

これでVを求めると,Vは8127単語からなる集合となりました.
ここで,ステミングや見出し語化をするかは皆さんにおまかせするとして,ストップワードくらいは外しておこうと思います.
それと,大文字小文字を区別しないようにします.

sws = stopwords.words("english")
V = set()
for fileid in train_fileids1+train_fileids2:
    V = V.union(set([word.lower() for word in reuters.words(fileid) if word.lower() not in sws]))
print len(V)

最終的にはこんな感じですかね.以下でもいいですね.

sws = stopwords.words("english")
V = set(word.lower() for fileid in train_fileids1+train_fileids2 for word in reuters.words(fileid) if word.lower() not in sws)
print len(V)

そうすると,Vは6459単語からなる集合となりました.
つまり,これから扱う各ベクトルは6459次元になります.
featuresという関数を用いて,reuters.words(fileid)で与えられる単語のリスト(以下words)をベクトルに変換することとします.
というわけでfeaturesを書いていきます.
featuresはwordsとVを引数に取り,Vの基数次元のベクトル(list)を返す関数とします.

def features(words, V):
    v = dict.fromkeys(V, 0.0)
    for word in words:
        try:
            v[word.lower()] += 1
        except:
            pass
    return [val/len(words) for val in v.values()]

こんな感じになりました.この関数featuresを用いて,各文書をベクトルで表現します.
もちろん,これでできたベクトルは疎なベクトルです.

さあ,文書をベクトルで表現できたので,いよいよ今回の本題であるclassificationです.
今回の記事では,最も単純な方法を実装したいと思います.
優れたアルゴリズムを実感するために,あえて今回は簡単な方法を実装するのです.
今回実装する方法は非常にシンプルで,各クラスの代表ベクトルを求めて,それに近い方に分類するという方法です.
どんなベクトルを代表ベクトルにするかは,いろいろありますが,今回は単純に平均ベクトルにしたいと思います.
なお,以下からpython-numpyを使います.

import numpy
train_vectors1 = numpy.array([numpy.array(features(reuters.words(fileid), V)) for fileid in train_fileids1])
train_vectors2 = numpy.array([numpy.array(features(reuters.words(fileid), V)) for fileid in train_fileids2])
rep1 = train_vectors1.mean(axis=0)
rep2 = train_vectors2.mean(axis=0)

こんな感じになりました.rep1は”ship”の代表ベクトルで,rep2は”wheat”の代表ベクトルです.
さて,いよいよテストデータを分類していきますが,
rep1,rep2のどちらに近いか,あるいは似ているかをどのように決めたらよいでしょうか.
シンプルなのはユークリッド距離やコサイン類似度を使うことでしょう.
今回はコサイン類似度を用います.1に近いほど2文書は似ているということになります.
具体的には,predictという関数を用いて,ベクトルで表現された文書を0か1に写像しようと思います.
predictはvector, rep1, rep2を引数に取り,rep2よりrep1に近ければ1を,rep1よりrep2に近ければ0を返します.

def predict(v, rep1, rep2):
    def simcos(v1, v2):
        return v1.dot(v2) / numpy.sqrt(v1.dot(v1))*numpy.sqrt(v2.dot(v2))
    sim1 = simcos(v, rep1)
    sim2 = simcos(v, rep2)
    if sim1 >= sim2:
        return 1
    return 0

こんな感じになりました.これを使うと,

test_vectors1 = numpy.array([numpy.array(features(reuters.words(fileid), V)) for fileid in test_fileids1])
test_vectors2 = numpy.array([numpy.array(features(reuters.words(fileid), V)) for fileid in test_fileids2])
predicted = [predict(v, rep1, rep2) for v in test_vectors1] + [predict(v, rep1, rep2) for v in test_vectors2]
print predicted

sol = [1 for fileid in test_fileids1] + [0 for fileid in test_fileids2]
print sol

このように書けまして,結果は以下のようになりました.

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

非常におもしろいですね笑.なんと1つだけしか1がない,つまり”ship”に属する文書は1つしかないと言っているわけです.
一応精度を出しておきます.

sol = numpy.array(sol)
accuracy =  float(len([i for i in predicted-sol if i == 0])) / len(sol)
print accuracy

精度は, 0.45 でした.
「半分くらいはうまく分類できているのか」という印象を受けますが,もし,正例と負例の数が同じくらいなら全部片方に分類してもこのくらいの精度が出てしまうんですね.
そこで,もう1つの評価指標,F値を導入します.

def F_measure(predicted, solution):
    tp, tn, fp, fn = 0.0, 0.0, 0.0, 0.0
    for p, s in zip(predicted, solution):
        if p == 1:
            if p == s:
                tp += 1
            else:
                fp += 1
        else:
            if p == s:
                tn += 1
            else:
                fn += 1
    precision = tp / (tp + fp)
    recall = tp / (tp + fn)
    return 2 * precision * recall / (precision + recall)

これで評価すると,今回の分類結果は 0.02 ととても低くなります.
F値は横着者を許してはくれないんですね笑.

Summary

今回の分類では,ほとんど”wheat”に分類しちゃったんですね.
まったく横着者です笑.
次回からこの結果を改善していくことで,各アルゴリズムのすごさを実感したいと思います.

簡単なclassification」への2件のフィードバック

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中