概要
sklearn.feature_extraction.text
のvectorizerで日本語の単語ngramを生成しようとして苦戦したのでメモ。
結論としては、vectorizerの引数で、tokenizer=lambda x: mecab.parse(x).strip().split()
(mecabはMeCabの分かち書きTaggerオブジェクト)としたうえで、ngram_range=(3,3)
などと指定すればよい。分かち書き関数をanalyzer
ではなくtokenizer
に指定するのがポイント。
from sklearn.feature_extraction.text import TfidfVectorizer
import MeCab
mecab = MeCab.Tagger("-Owakati")
vectorizer = TfidfVectorizer(
tokenizer=lambda x: mecab.parse(x).strip().split(),
ngram_range=(3,3)
)
# 確認
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
['あらゆる 現実 を', 'た の だ', 'の 方 へ', 'へ 捻じ 曲げ', 'を 自分 の', '捻じ 曲げ た', '方 へ 捻じ', '曲げ た の', '現実 を 自分', '自分 の 方']
経緯
sklearn.feature_extraction.text
のvectorizerで日本語の単語ngramを生成しようとしたが、analyzer
で分かち書きの関数を指定すると、unigramにしかならなかった。
from sklearn.feature_extraction.text import TfidfVectorizer
import MeCab
mecab = MeCab.Tagger("-Owakati")
vectorizer = TfidfVectorizer(
analyzer=lambda x: mecab.parse(x).strip().split(),
ngram_range=(3,3)
)
# 経緯
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
原因と解決方法
analyzer
にcallableな値を指定していたためngram_range
が適用されなかった。公式ドキュメントにも「ngram_rangeはanalyzerがcallbale出ない場合のみ、適用される」と書いてある。analyzerとtokenizerの違いをきちんと理解できていなかった。
ngram_rangetuple (min_n, max_n), default=(1, 1)
...(略)... Only applies if analyzer is not callable.
ちなみにソースコード上でも、analyzerがNoneの場合のみ、tokenizerとngramが実行されそうな雰囲気を感じられる。
補足
要はanalyzer
にcallableな値を渡さなければよいので、概要で書いた以外にもいろいろ実現方法がありそう。せっかくなので試してみる。
analyzerにngramの処理も含めた関数を渡す
まず、analyzerにngramのtokenを生成する記述も含めて渡す方法。
結果は問題なさそうだが、普通に面倒なので、あえてをこれをやる理由はない。
from sklearn.feature_extraction.text import TfidfVectorizer
import MeCab
mecab = MeCab.Tagger("-Owakati")
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
# ngramを作る関数
def ngram(l, n):
return list(zip(*[l[i:] for i in range(n)]))
# テキストを単語に分かち書きして指定された数字のngramを返す関数を返す(生成する)関数
def analyze(min_n=1, max_n=1):
def a(text):
tokens = mecab.parse(text).strip().split() #単語のリストにする
res = []
for n in range(min_n, max_n+1):
res += [" ".join(v) for v in ngram(tokens, n)] #vectorizerのfeature_name似合わせるため、スペースでjoinする
return res
return a
vectorizer = TfidfVectorizer(
analyzer = analyze(3,3)
)
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
['あらゆる 現実 を', 'た の だ', 'の 方 へ', 'へ 捻じ 曲げ', 'を 自分 の', '捻じ 曲げ た', '方 へ 捻じ', '曲げ た の', '現実 を 自分', '自分 の 方']
事前にスペース区切りしたdocumentを渡す
vectorizerの外で分かち書き(スペース区切り)したドキュメントを渡してみる。英語がそう(スペース区切り)なので、うまくいくはず。
from sklearn.feature_extraction.text import TfidfVectorizer
import MeCab
mecab = MeCab.Tagger("-Owakati")
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
# 事前に単語のスペース区切りに変換しておく
documents = [ mecab.parse(v).strip() for v in documents]
vectorizer = TfidfVectorizer(
ngram_range=(3,3)
)
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
['あらゆる 現実 自分', '現実 自分 捻じ', '自分 捻じ 曲げ']
悪くない結果だが、vectorizer
のデフォルト設定により、1文字の単語が除外されている。
1文字の単語も含める場合、token_pattern
の引数を指定する。
vectorizer = TfidfVectorizer(
ngram_range=(3,3),
# (?u)はunicodeの指定(たぶん)、\bは単語の境界。\w+は1文字以上にマッチ。デフォルトは\w\w+となっており、2文字以上のみマッチする
token_pattern = r"(?u)\b\w+\b"
)
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
['あらゆる 現実 を', 'た の だ', 'の 方 へ', 'へ 捻じ 曲げ', 'を 自分 の', '捻じ 曲げ た', '方 へ 捻じ', '曲げ た の', '現実 を 自分', '自分 の 方']
preprocessorに分かち書き関数を指定する
preprocessor
引数に分かち書き関数を渡しても良い。tokenizer
と違って、splitの手前(スペース区切りの文字列にするところ)までの処理でとめる必要がある。
from sklearn.feature_extraction.text import TfidfVectorizer
import MeCab
mecab = MeCab.Tagger("-Owakati")
vectorizer = TfidfVectorizer(
# tokenizerでsplitをやめる代わりに、analyzerを明示的に指定する
preprocessor=lambda x: mecab.parse(x).strip(),
analyzer="word", # 省略可
token_pattern = r"(?u)\b\w+\b", #1文字の単語も含めたい場合、必要
ngram_range=(3,3)
)
# 確認
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
['あらゆる 現実 を', 'た の だ', 'の 方 へ', 'へ 捻じ 曲げ', 'を 自分 の', '捻じ 曲げ た', '方 へ 捻じ', '曲げ た の', '現実 を 自分', '自分 の 方']
なおpreprocessor
ではなくtokenizer
に同じ関数を渡すと、charのngramになってしまい、失敗する。
vectorizer = TfidfVectorizer(
tokenizer=lambda x: mecab.parse(x).strip(),
ngram_range=(3,3)
)
# 確認
documents = [
"あらゆる現実を自分の方へ捻じ曲げたのだ"
]
vectorizer.fit(documents)
print(vectorizer.get_feature_names())
[' た ', ' の ', ' へ ', ' を ', ' 捻 じ', ' 方 ', ' 曲 げ', ' 現 実', ' 自 分', 'あ ら ゆ', 'げ た', 'じ 曲', 'た の', 'の だ', 'の 方', 'へ 捻', 'ゆ る ', 'ら ゆ る', 'る 現', 'を 自', '分 の', '実 を', '捻 じ ', '方 へ', '曲 げ ', '現 実 ', '自 分 ']
analyzerのデフォルト値はwordのため、(analyzerを指定しない場合は)渡した文字列がスペースでsplitされることが期待される。実際、preprocessor
に関数を渡した場合はそのような挙動になっていそうである。一方で、tokenizer
に同様の関数を渡した場合は、analyzer="word"
の処理は無視(あるいは上書き?)されてしまい、スペースによるsplitは実施されないようである。
おわりに
scikit-learnのvectorizerで日本語の単語ngramがうまく生成できなかったことをきっかけとして、tokenizer
、analyzer
、preprocessor
の挙動について手を動かしながら勉強してみた。
そもそも公式ドキュメントに書いてあることを見落としていただけなので褒められたことではないが、理解を整理するきっかけになってよかった。