静かなる名辞

pythonとプログラミングのこと


【python】pythonでn-gramの特徴量を作る

 ○○ってパッケージでできるよ! という意見もあると思いますが、ちょっと挙動を変えたくなる度にパッケージのhelp読んだり、微妙に柔軟性のないパッケージに苦しむ(たとえば文末の句点と次の文の最初の文字は繋げないで欲しいのにできない、とか)くらいなら、最初から自分で書いた方が速いです。好きなだけ編集できます。

 とりあえず、文字列ないし形態素のリストなどをn-gramに切り分ける関数を作ってみます。

def ngram_split(string, n, splitter="-*-"):
    """
    string:iterableなら何でも n:n-gramのn splitter:処理対象と混ざらなければ何でも良い
    """
    lst = []
    for i in range(len(string[:-n+1])):
        lst.append(splitter.join(string[i:i+n]))
    return lst

 結果はこんな感じ。

>>> for x in ngram_split("吾輩は猫である。", 3):
...     print(x)
... 
吾-*-輩-*-は
輩-*-は-*-猫
は-*-猫-*-で
猫-*-で-*-あ
で-*-あ-*-る
あ-*-る-*-。
>>> for x in ngram_split(["吾輩","は","猫","で","ある","。"], 3):
...     print(x)
... 
吾輩-*-は-*-猫
は-*-猫-*-で
猫-*-で-*-ある
で-*-ある-*-。

 悪くないですが、返り値はdictの方が便利そうです。

from collections import defaultdict
def itr_dict(itr):
    d = defaultdict(int)
    for x in itr:
        d[x] += 1
    return d

 ↑こういうのを作っておいて、

>>> itr_dict(ngram_split(["吾輩","は","猫","で","ある","。"], 3))
defaultdict(<class 'int'>, {'は-*-猫-*-で': 1, '吾輩-*-は-*-猫': 1, '猫-*-で-*-ある': 1, 'で-*-ある-*-。': 1})

 こんな感じで使えば良いのではないでしょうか。


 さて、以下のようなデータを考えます。

data_lst = ["吾輩は猫である",
            "国境の長いトンネルを抜けると雪国であった",
            "恥の多い生涯を送って来ました",
            "一人の下人が、羅生門の下で雨やみを待っていた",
            "幼時から父は、私によく、金閣のことを語った"]

 これを文字2-gramの特徴量にしてみます。すでにn-gramは作れるようになっているので簡単です。

from sklearn.feature_extraction import DictVectorizer

bgram_dict_lst = [itr_dict(ngram_split(x, 3)) for x in data_lst]
dict_vectorizer = DictVectorizer()
a = dict_vectorizer.fit_transform(bgram_dict_lst).toarray()

 DictVectorizerって何? という声が聞こえてきそうなので、sklearnのドキュメントを貼ります。
sklearn.feature_extraction.DictVectorizer — scikit-learn 0.20.1 documentation

 結果を先に見せると、こういうものが生成されています。要するに、dictから特徴量まで一気に作ってくれます。

>>> a
array([[ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  1.,  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.,  1.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.,  0.,
         0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,
         1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  0.,
         1.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  1.,
         0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  1.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,
         1.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
         0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         1.,  0.,  0.,  1.,  0.,  0.,  1.,  1.,  0.,  0.,  1.,  0.,  0.,
         0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  1.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  2.,  0.,
         0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  1.,  1.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,
         0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,
         1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.],
       [ 1.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  1.,
         0.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  1.,  0.,  0.,
         0.,  1.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  1.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  1.,
         0.,  1.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.]])

 DictVectorizerに似たようなものを自分で作ることも、当然できます。が、基本的にこの手のデータ処理で細かく弄り回したいと思うのは前処理の部分ですから、こういう「dictを行列に変換する」みたいな単純な処理はパッケージに投げてしまった方が世の中の幸せの総量は増える気がします。逆に言えば、前処理が必要ならDictVectorizerに投げる前(あるいは投げた後)に済ませておく必要があります。


 ところで、この特徴量はこれでアリですが、たぶんそれぞれのベクトルをテキスト長か何かで割って相対頻度に変換してやると、機械学習的に良い感じになると思います。

import numpy as np

len_array = np.array([[len(x)]*a.shape[1] for x in data_lst])
#↑もうちょっと綺麗な方法があるなら教えて欲しい

std_a = a/len_array

 ここまで来れば、そのまま機械学習アルゴリズムに突っ込んでもそこそこなんとかなるかと思います。性能を求めるなら、更に特徴選択等を入れるべきでしょう。