scikit-learnのCountVectorizerやTfidfVectorizerで追加学習させる
01
- 9月
2019
Posted By : boomin
scikit-learnのCountVectorizerやTfidfVectorizerで追加学習させる
Advertisements

TL; DR

テキストマイニングをpythonで行う場合、gensimやscikit-learnなど、いくつかのライブラリを使用することができます。ここでは、scikit-learnでBoW(Bag of Words)を作った後に、新たな単語を追加させる方法について書いていきます。

1. scikit-learnで追加学習

scikit-learnで、TfidfVectorやCountVectorをすると、対象corpusの単語の登場回数やtf-idfスコアがわかります。でも、一度fitして学習させると、その後に未知の新語を含むcorpusを対象にベクトル化のためのtransformしても、対応するベクトル要素がありません。そのため、未知の単語に該当するベクトル要素が空となります。そこで、未知の単語を追加学習させる方法を以下に示します。

2. CountVectorizerで実際に追加学習させる

テキストマイニングするときに、まずやることと言ったら分かち書き(形態素解析)です。でも、今回は面倒なので、サンプル文書を英語にして、最初から単語が半角スペースで分割されているものとします。

ここでは、以下の4つの文からなるcorpusをサンプルとします。

corpus = [
    "I am a perfect man",
    "I am regend",
    "you are Sam",
    "I am a robot and regend",
]

2.1 サンプルcorpusの単語登場回数

このcorpusを対象に、CountVectorizerで単語の登場回数をcountしてみましょう。

from sklearn.feature_extraction.text import CountVectorizer

# CountVectorizerの実行
cnt_vec = CountVectorizer(
    token_pattern=u'(?u)\\b\\w+\\b', # 1文字のトークンも対象にする
)

# vectorizeする
cnt_vec.fit(corpus)

#抽出した単語を確認する
cnt_vec.get_feature_names()
# ['a', 'am', 'and', 'are', 'i', 'man', 'perfect', 'regend', robot', 'sam', 'you']

# vectorizeしたモデルをもとに、(同じ文章を)ベクトル化する
words = cnt_vec.transform(corpus)

# ベクトル化した文章の疎行列を確認する
df_cnt = pd.DataFrame(
    words.toarray(),
    columns=cnt_vec.get_feature_names(),
    index=corpus
)

print(df_cnt)
#                         a  am  and  are  i  ...  perfect  regend  robot  sam  you
#I am a perfect man       1   1    0    0  1  ...        1       0      0    0    0
#I am regend              0   1    0    0  1  ...        0       1      0    0    0
#you are Sam              0   0    0    1  0  ...        0       0      0    1    1
#I am a robot and regend  1   1    1    0  1  ...        0       1      1    0    0

print(df_cnt.iloc[0])
#a          1
#am         1
#and        0
#are        0
#i          1
#man        1
#perfect    1
#regend     0
#robot      0
#sam        0
#you        0

こんな感じで、各単語の登場回数を確認できます。ここで、BoWをdf_cntというDataFrameに格納しています。

Advertisements

2.2 未知の単語を含む文章をvectorizeする

I am a spyderman という、spydermanという未知語を含む文章を対象に、先ほど学習させた(fitした)モデルを使ってベクトル化させてみます。

# spyderman が未知の単語
new_corpus = [
    "I am a spyderman",
]

# vectorizeしたモデルをもとに、(同じ文章を)ベクトル化する
words = cnt_vec.transform(new_corpus)

# ベクトル化した文章の疎行列を確認する
new_df_cnt = pd.DataFrame(
    words.toarray(),
    columns=cnt_vec.get_feature_names(),
    index=new_corpus
)

print(new_df_cnt)
#                  a  am  and  are  i  man  perfect  regend  robot  sam  you
#I am a spyderman  1   1    0    0  1    0        0       0      0    0    0

print([ v.nonzero()[0].tolist() for v in words.toarray() ]
)
# [[0, 1, 4]] <- 4語のcorpusを対象にvectorizeしたのに、i,am,aの3単語の成分しかない

最後の結果部分のコメントにも記載していますが、4語のcorpusを対象にvectorizeしたのに、得られたvectorは、i,am,aの3単語の成分しかありませんでした。これは、spydermanが未知語のため、この単語の成分が見つからず、その要素がなかったことになってしまっています。

2.3 未知の単語を追加学習する

すると、ならばBoWにspydermanという単語を追加すればいいと思うわけです。しかしながら、scikit-learn CountVectorizerの公式をみてみても、それらしきmethodは用意されていません。

以下、scikit-learn CountVectorizerの公式のMethodsから抜粋

MethodOverview
build_analyzer(self)Return a callable that handles preprocessing and tokenization
build_preprocessor(self)Return a function to preprocess the text before tokenization
build_tokenizer(self)Return a function that splits a string into a sequence of tokens
decode(self, doc)Decode the input into a string of unicode symbols
fit(self, raw_documents[, y])Learn a vocabulary dictionary of all tokens in the raw documents.
fit_transform(self, raw_documents[, y])Learn the vocabulary dictionary and return term-document matrix.
get_feature_names(self)Array mapping from feature integer indices to feature name
get_params(self[, deep])Get parameters for this estimator.
get_stop_words(self)Build or fetch the effective stop words list
inverse_transform(self, X)Return terms per document with nonzero entries in X.
set_params(self, **params)Set the parameters of this estimator.
transform(self, raw_documents)Transform documents to document-term matrix.

とすると、自分で上記サンプルのdf_cntのDataFrameをもとにして、自作するか、、、というのもかったるいです。そこで、CountVectorizerをOverrideした自作のclassを作り、partial_fitというmethodを追加させてみることにします。

2.4 追加学習させることができる自作クラス 「MessageVectorizer」

以下に、CountVectorizerを継承したMessageVectorizerのsource codeを示します。

from sklearn.feature_extraction.text import CountVectorizer
class MessageVectorizer(CountVectorizer):
    def first_fit(self, X):
        self.fit(X)
        self.n_docs = len(X)

    def partial_fit(self, X):
        max_idx = max(self.vocabulary_.values())

        intervec = CountVectorizer(
            ngram_range = self.ngram_range,
            tokenizer   = self.tokenizer
        )

        for a in X:
            #update vocabulary_
            if self.lowercase: a = a.lower()
            intervec.fit([a])
            tokens = intervec.get_feature_names()
            for w in tokens:
                if w not in self.vocabulary_:
                    max_idx += 1
                    self.vocabulary_[w] = max_idx

このMessageVectorizerを使って、追加学習させる様子を以下に紹介します。ここでは、spyderman が未知語です。

# MessageVectorizerをimport
from Vectorizer import MessageVectorizer

# 最初のcorpus
corpus = [
    "I am a perfect man",
    "I am regend",
    "you are Sam",
    "I am a robot and regend",
]

# MessageVectorizerの実行
msg_vec = MessageVectorizer(
    token_pattern=u'(?u)\\b\\w+\\b', # 1文字のトークンも対象にする
)

# ベクトル化する
msg_vec.first_fit(corpus)

# ベクトル化した文章の疎行列を確認する
df_cnt = pd.DataFrame(
    msg_vec.transform(corpus).toarray(),
    columns=msg_vec.get_feature_names(),
    index=corpus
)

# 未知語を含むcorpus
new_corpus = [
    "I am a spyderman",
]

# 未知語を含むcorpusを、追加学習する前に、ベクトル化してみる
words = msg_vec.transform(new_corpus)

# ベクトルの要素の確認
[ v.nonzero()[0].tolist() for v in words.toarray() ]
# [[0, 1, 4]]
# ベクトルの要素を確認しても、当然、未知語であるspydermanの要素は取得できず、3成分のvectorとなる

# new_corpusを追加学習させる
msg_vec.partial_fit(new_corpus)

# 未知語を追加学習させたモデルを使用して、未知語を含むcorpusをベクトル化してみる
add_words = msg_vec.transform(new_corpus)

# ベクトルの要素の確認
[ v.nonzero()[0].tolist() for v in add_words.toarray() ]
# [[0, 1, 4, 11]] となり、1成分が追加され、計4成分となったことがわかる

今度はBoWとして、学習モデルがどのよう担っているかを確認してみましょう。

# BoWを確認するためのDataFrameの準備
df_msg = pd.DataFrame(
    msg_vec.transform(new_corpus).toarray(),
    columns=msg_vec.get_feature_names(),
    index=new_corpus
)

print(df_msg)
#                  a  am  and  are  i  ...  regend  robot  sam  you  spyderman
#I am a spyderman  1   1    0    0  1  ...       0      0    0    0          1

BoWに、spydermanという単語が追加されていることが確認できています。

3. TfidfVectorizerの追加学習

TfidfVectorizerを対象とする場合は、追加した単語に対して、idfスコアを計算する必要があります。そこで、この場合のMessageVectorizerは、以下のように修正します。

from sklearn.feature_extraction.text import TfidfVectorizer

class MessageVectorizer(TfidfVectorizer):
    def first_fit(self, X):
        self.fit(X)
        self.n_docs = len(X)

    def partial_fit(self, X):
        max_idx = max(self.vocabulary_.values())

        intervec = TfidfVectorizer(
            ngram_range = self.ngram_range,
            tokenizer   = self.tokenizer
        )

        for a in X:
            #update vocabulary_
            if self.lowercase: a = a.lower()
            intervec.fit([a])
            tokens = intervec.get_feature_names()
            for w in tokens:
                if w not in self.vocabulary_:
                    max_idx += 1
                    self.vocabulary_[w] = max_idx

            # update idf_
            df = (self.n_docs + self.smooth_idf)/np.exp(self.idf_ - 1) - self.smooth_idf
            self.n_docs += 1
            df.resize(len(self.vocabulary_), refcheck=False)
            for w in tokens:
                df[self.vocabulary_[w]] += 1
            idf = np.log((self.n_docs + self.smooth_idf)/(df + self.smooth_idf)) + 1
            self._tfidf._idf_diag = dia_matrix((idf, 0), shape=(len(idf), len(idf)))

使い方は、CountVectorizerの場合と同じです。

4. まとめ

単に、文章の分析したいと思ってCountVectorizerやTfidfVectorizerを使う分には、あまり困ることはありません。が、これを使って何らかのシステム化を考えた場合、そのうちに学習データにない文章を対象とするケースが出てくるでしょう。その場合、なんの工夫をさせないとなると、追加分を含めて定期的にすべてのcorpusを再学習させる必要があり、量によっては結構時間がかかります。CountVectorizerやTfidfVectorizerは、n_jobsオプションも使えない(シングルコアでしか動かない)ため、なおさらです。

Advertisements

コメントを残す