静かなる名辞

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


【python】sklearnのCountVectorizerの使い方

 sklearnのCountVectorizerを使うとBoW(Bag of Words)の特徴量が簡単に作れます。

 ただし、指定するパラメタが多かったり、デフォルトで英語の文字列を想定していたりして若干とっつきづらい部分もあります。

 この記事ではCountVectorizerの使い方を簡単に説明します。

参考 sklearn公式ページ
sklearn.feature_extraction.text.CountVectorizer — scikit-learn 0.20.1 documentation

 目次

スポンサーリンク



何も考えずに使う

 英語の入力文なら何も考えずに使うことも可能です。とりあえず入力データとして文字列のリストを作る必要があるので、pythonの英語版wikipediaの冒頭の文章を使うことにします。無駄な脚注を取り除き、一文ずつ改行します。

Python is an interpreted high-level programming language for general-purpose programming.
Created by Guido van Rossum and first released in 1991, Python has a design philosophy that emphasizes code readability, and a syntax that allows programmers to express concepts in fewer lines of code,notably using significant whitespace.
It provides constructs that enable clear programming on both small and large scales.
Python features a dynamic type system and automatic memory management.
It supports multiple programming paradigms, including object-oriented, imperative, functional and procedural, and has a large and comprehensive standard library.
Python interpreters are available for many operating systems.
CPython, the reference implementation of Python, is open source software and has a community-based development model, as do nearly all of its variant implementations.
CPython is managed by the non-profit Python Software Foundation.

 これをsource.txtというファイル名で適当なディレクトリに保存し、そのディレクトリ上のシェルでpythonインタプリタを起動します。このファイルを読み込み、改行でsplitしてリストを作ります。

>>> source_list = [x for x in txt.split("\n") if x != ""]
>>> with open("source.txt", "r") as f:
...     txt = f.read()
... 
>>> source_list = [x for x in txt.split("\n") if x != ""]

 ファイル末尾の改行のせいで空文字列が入るので、対策をしています。

 後はCountVectorizerをimportし、インスタンス化してfit_transform一発でDocument-Term Matrixが得られます。

>>> from sklearn.feature_extraction.text import CountVectorizer as CV
>>> cv = CV()
>>> matrix = cv.fit_transform(source_list)
>>> matrix
<8x98 sparse matrix of type '<class 'numpy.int64'>'
	with 122 stored elements in Compressed Sparse Row format>

 おおっ、spicyのsparse matrixを吐きやがった! と思った人は正しいです。これは仕様なので仕方ありません。嫌なら.toarray()してnumpy配列に変換してください。sparse matrixの方がありがたいときとnumpy配列の方がありがたいとき、どちらもあるので、どっちにしておくのが良いかは一概には言えません。

 とりあえず形は8*98ということで、確かに8行のテキストなので上手くいっているようです。全データ中の異なり語数は98となり、100次元弱のBoW特徴量が得られました。

出現頻度の低すぎる・高すぎる単語を消す

 全文書中に1回とか2回しか出てこない単語、要らないですよね*1。逆に、全文書にまんべんなく出現する単語も要らない気がします*2

 CountVectorizerにはmin_df,max_dfというパラメータがあります。dfはDocument Frequencyのことで、tf-idfのアレです。要するに(何回出てくるかは置いておいて)全文書中の何%にその単語が出現するかの指標です。それを使って特徴をフィルタリングできます。

 今回は8文書なので、うっかり変な数字を指定するとまったく効果がなかったり、何も残らなかったりするのが難しいところです。とりあえず出現する文書が2文書以上、6文書以下くらいの特徴を取ってみることにします。min_df=2/8=0.25, max_df=6/8=0.75とすれば良さそうですが、比較がgreater | less than or equalなのか単にgreater | less thanなのかよくわからないので、安全を見てmin_df=0.24, max_df=0.76としておきます。

>>> cv = CV(min_df=0.24, max_df=0.76)
>>> matrix = cv.fit_transform(source_list)
>>> matrix
<8x14 sparse matrix of type '<class 'numpy.int64'>'
	with 38 stored elements in Compressed Sparse Row format>

 14次元まで減りました。特徴の名前(残っている単語)を見てみます。リストの0個目の単語が0次元目の特徴に・・・という形で対応しているはずです(たぶん)。

>>> cv.get_feature_names()
['and', 'by', 'cpython', 'for', 'has', 'is', 'it', 'large', 'of', 'programming', 'python', 'software', 'that', 'the']

 たぶんこんなものでしょう。

stop wordの除去

 byとかforとかthatとか要らないですよね*3。stop_wordsというパラメータがあり、「こんなの要らないよ」って単語のリストを渡すと除去してくれます。また、文字列"english"を渡すこともでき、その場合は「built-in stop word list for English」を使ってくれます。凄い。ちなみに「built-in stop word list for Japanese」はありません。残念。

 とりあえず"english"を指定してみます。

>>> cv = CV(min_df=0.24, max_df=0.76, stop_words="english")
>>> matrix = cv.fit_transform(source_list)
>>> cv.get_feature_names()
['cpython', 'large', 'programming', 'python', 'software']

 思ったより何も残らなかったので、min_dfを下げてみます。

>>> cv = CV(min_df=0.12, max_df=0.76, stop_words="english")
>>> matrix = cv.fit_transform(source_list)
>>> cv.get_feature_names()
['1991', 'allows', 'automatic', 'available', 'based', 'clear', 'code', 'community', 'comprehensive', 'concepts', 'constructs', 'cpython', 'created', 'design', 'development', 'dynamic', 'emphasizes', 'enable', 'express', 'features', 'fewer', 'foundation', 'functional', 'general', 'guido', 'high', 'imperative', 'implementation', 'implementations', 'including', 'interpreted', 'interpreters', 'language', 'large', 'level', 'library', 'lines', 'managed', 'management', 'memory', 'model', 'multiple', 'nearly', 'non', 'notably', 'object', 'open', 'operating', 'oriented', 'paradigms', 'philosophy', 'procedural', 'profit', 'programmers', 'programming', 'provides', 'purpose', 'python', 'readability', 'reference', 'released', 'rossum', 'scales', 'significant', 'small', 'software', 'source', 'standard', 'supports', 'syntax', 'systems', 'type', 'using', 'van', 'variant', 'whitespace']

 ソースリストの下のスクロールバーが凄いことになってますが、どうせ誰も見たくもないでしょうし、対策はしていません。見たい人は頑張ってスクロールしてください。とりあえずこんなものだろうという結果は得られました。

n-gramの特徴量にする

 ngram_rangeというパラメータがあります。これはタプルで渡す必要があり、(1,1)とか(1,2)といった風に指定します。

The lower and upper boundary of the range of n-values for different n-grams to be extracted. All values of n such that min_n <= n <= max_n will be used.

 要するに(1,1)なら1-gram(ただの単語), (1,2)なら1-gramと2-gram、(1,3)なら1~3-gram、(2,3)なら2~3-gramという形でぜんぶ作り、まとめて一つの特徴空間にしてくれるようです。(1,2)を試してみます。

>>> cv = CV(ngram_range=(1,2))
>>> cv = CV(ngram_range=(1,2))
>>> matrix = cv.fit_transform(source_list)
>>> cv.get_feature_names() 
['1991', '1991 python', 'all', 'all of', 'allows', 'allows programmers',... 
# 多いので途中で省略

 期待通り動いているようです。

名詞だけでBoWを作る。更にstemmingも行う

 これはCountVectorizerだけではできません(CountVectorizer内部でPOS taggingを行っていないため)。

 そこでnltkを使います。まず、次のような関数を定義します。

>>> def noun_stem_analyzer(string):
...     st = nltk.stem.lancaster.LancasterStemmer()
...     return [st.stem(word) for word, pos in nltk.pos_tag(
...             nltk.word_tokenize(string)) if pos == "NN"]
... 

 nltkを入れていない人は入れてください。また、一回目の呼び出しでは処理に必要なリソースがないというエラーが出るので、エラーメッセージの案内通りにコマンドを打ち、リソースをダウンロードしてください。

 使ってみます。

>>> string = "Python is an interpreted high-level programming language for general-purpose programming."
>>> noun_stem_analyzer(string)
['high-level', 'program', 'langu', 'program']

 pythonが入ってないのが微妙なので、POSタグをちゃんと見てみます。

>>> nltk.pos_tag(nltk.word_tokenize(string))
[('Python', 'NNP'), ('is', 'VBZ'), ('an', 'DT'), ('interpreted', 'JJ'), ('high-level', 'NN'), ('programming', 'NN'), ('language', 'NN'), ('for', 'IN'), ('general-purpose', 'JJ'), ('programming', 'NN'), ('.', '.')]

 NNPは固有名詞・・・かな。これを踏まえて関数を修正。

>>> def noun_stem_analyzer(string):
...     st = nltk.stem.lancaster.LancasterStemmer()
...     return [st.stem(word) for word, pos in nltk.pos_tag(
...             nltk.word_tokenize(string)) if pos == "NN" or pos == "NNP"]
... 
>>> noun_stem_analyzer(string)
['python', 'high-level', 'program', 'langu', 'program']

 これなら期待通りです。stemmingの結果に若干納得できないような気もしますが、今回はこのまま行きます。

 この関数をどうやってCountVectorizerと組み合わせて使うのかというと、analyzer引数に渡してあげます。

>>> cv = CV(analyzer=noun_stem_analyzer)
>>> matrix = cv.fit_transform(source_list)
>>> cv.get_feature_names()
['cod', 'cpython', 'design', 'develop', 'found', 'guido', 'high-level', 'impl', 'langu', 'libr', 'man', 'mem', 'model', 'paradigm', 'philosoph', 'program', 'python', 'read', 'ref', 'ross', 'softw', 'sourc', 'standard', 'syntax', 'system', 'typ', 'van', 'whitespac']

 こうやって使える訳です。

 ちなみに、似たような引数にpreprocessorとtokenizerがあります。ありますが、ドキュメントを何回読んでもなんとなくしかわからなかったので、説明はしません。とりあえず、analyzerを指定すれば大抵の場合問題はないでしょう。

 なお、analyzerはcallableならなんでも渡せるので、たとえば(lambda x:x)を渡し、

>>> cv = CV(analyzer=lambda x:x)
>>> matrix = cv.fit_transform(
...              [noun_stem_analyzer(string) for string in source_list])

 こうしても上と同じ結果になります。どうしてわざわざこんなことを書いたのかというと、これを使ってテキストの前処理を事前にまとめて行っておくという方針が使えるからです。実際にテキスト分析をやったことのある方ならご存知かと思いますが、大量のデータに形態素解析などをかけるのはそれ自体けっこうヘビーな処理になるので、一度データを丸ごと形態素解析してファイルにダンプするとか、DBに入れるとかして処理を行うことが多い訳です。そういうデータも、わざわざ分かち書きに戻したりしなくても上記の方法で解析できます。

日本語で使う

 上の例を見て分かる通り、analyzerには好きなものが渡せます。ということは、日本語形態素解析器を突っ込んでやればCountVectorizerは日本語でも使える訳です。pythonの日本語版wikipediaから以下の文章を取ってきました。

Python(パイソン)は、汎用のプログラミング言語である。
コードがシンプルで扱いやすく設計されており、C言語などに比べて、さまざまなプログラムを分かりやすく、少ないコード行数で書けるといった特徴がある。
文法を極力単純化してコードの可読性を高め、読みやすく、また書きやすくしてプログラマの作業性とコードの信頼性を高めることを重視してデザインされた、汎用の高水準言語である。
反面、実行速度はCに比べて犠牲にされている。
核となる本体部分は必要最小限に抑えられている。
一方で標準ライブラリやサードパーティ製のライブラリ、関数など、さまざまな領域に特化した豊富で大規模なツール群が用意され、インターネット上から無料で入手でき、自らの使用目的に応じて機能を拡張してゆくことができる。
またPythonは多くのハードウェアとOS (プラットフォーム) に対応しており、複数のプログラミングパラダイムに対応している。
Pythonはオブジェクト指向、命令型、手続き型、関数型などの形式でプログラムを書くことができる。

 source2.txtとして保存し、さきほどと同様に読み込みます。

>>> with open("source2.txt", "r") as f:
...     txt = f.read()
... 
>>> source2_list = [x for x in txt.split("\n") if x != ""]

 日本語形態素解析器にはMeCabを使います。次のようにanalyzerを定義します。

>>> import MeCab
>>> tagger = MeCab.Tagger("")
>>> def japanese_analyzer(string):
...     result_list = []
...     for line in tagger.parse(string).split("\n"):
...         splited_line = line.split("\t")
...         if len(splited_line) >= 2 and "名詞" in splited_line[1]:
...             result_list.append(splited_line[0])
...     return result_list

 色々妥協して書いたので、このコードは実用的な用途には転用しないでください(する人もいないだろうけど)。とにかくCountVectorizerにこれを入れます。

>>> cv = CV(analyzer=japanese_analyzer)
>>> matrix = cv.fit_transform(source2_list)
>>> cv.get_feature_names()
['(', ')', 'C', 'OS', 'Python', 'こと', 'さまざま', 'インターネット', 'オブジェクト', 'コード', 'サード', 'シンプル', 'ツール', 'デザイン', 'ハードウェア', 'パイソン', 'パーティ', 'プラットフォーム', 'プログラマ', 'プログラミング', 'プログラミングパラダイム', 'プログラム', 'ライブラリ', '上', '作業', '使用', '信頼', '入手', '化', '単純', '可読性', '命令', '型', '多く', '大', '実行', '対応', '形式', '必要', '性', '手続き', '拡張', '指向', '数', '文法', '最小限', '本体', '核', '標準', '機能', '汎用', '無料', '特', '特徴', '犠牲', '用意', '目的', '群', '自ら', '行', '製', '複数', '規模', '言語', '設計', '豊富', '速度', '部分', '重視', '関数', '領域', '高水準']

 最初の半角カッコが目立ちますが、mecabのデフォルトの挙動では半角記号は「名詞,サ変接続」に割り当てるのでこれで間違っていません。それを除けば、それほど悪くない感じになっていると思います。

似たようなもの

 CountVectorizerに似たものとして、

  • TfidfVectorizer
  • HashingVectorizer

 があります。

 参考 公式ドキュメント
sklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 0.20.1 documentation
sklearn.feature_extraction.text.HashingVectorizer — scikit-learn 0.20.1 documentation

 Tfidfの方はその名の通り、出力される行列をidfで重み付けします。Hashingの方ではfeature hashingという手法を使い、次元数が膨れ上がるのを抑制してくれるようです。

まとめ

 素晴らしく簡単に使えます。テキストの特徴量が必要になったときには、使ってみては如何でしょうか。

*1:今回はデータが小さいのでそうも言い切れない部分があるが・・・

*2:これはタスク依存。著者推定のようなタスクではまんべんなく出現する単語の頻度を見るので重要だったりする

*3:ぶっちゃけタスク依存(ry