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に格納しています。
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から抜粋
Method | Overview |
---|---|
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オプションも使えない(シングルコアでしか動かない)ため、なおさらです。