はじめに
PyCon JP 2016に参加してきました!大変モチベーションが上がった!ので更新します←
ちなみにPyConで勉強してきたことも、追々整理&動かしてみて、記事にしてみたいと思います。
今回は「データ準備編」の続きで、Twitterのテキストデータをクラスタリング…する前に、データ前処理とテキストデータ同士の簡単な類似度算出をしてみます。
ご注意!(前回と同じ)
- 理論よりも、とりあえず触って理解するやり方が好きなので、色々と雑です。
- さらに文盲ということで、分かりづらい文章になると思ってます。
- そんな訳で読んでてツッコミたくなる内容は多々あると思いますが、ご了承頂きたく!
※優しいツッコミは大歓迎です
3行でまとめ
- クラスタリングの前処理として、テキストデータを数値(ベクトル)化した。
- 数値化したテキストデータ同士で、類似度を計算してみた。
- 次回からクラスタリングできる!(はず)
なぜクラスタリングにしたか?手法は?
まず、なぜ機械学習の中からクラスタリングをしようとしたか、背景を簡単に。
とりあえずデータを準備したは良いものの、機械学習を使うと何が出来るのか、どんな手法があるのか全然知らなかったので色々調べた所、scikit-learnのチートシートを元に分かりやすく纏められていた「scikit-learn から学ぶ機械学習の手法の概要」に行き着きました。
チートシートによるとscikit-learnで出来ることとして「分類」「回帰」「クラスタリング」「次元削減」があるみたいです。
- 分類 (Classification) - ラベルとデータを学習し、データに対してのラベルを予測する。
- 回帰 (Regression) - 実数値をデータで学習して、実数値を予測する。
- クラスタリング (Clustering) - データの似ているもの同士をまとめて、データの構造を発見する。
- 次元削減 (Dimensionality reduction) - データの次元を削減して、要因を発見 (主成分分析など) したり、他の手法の入力に使う (次元の呪い回避)。
この中で 「回帰」「次元削減」は字面からして難しそうなので却下で、 直感的に一番分かりやすそうな「分類」「クラスタリング」どちらかにしようと思いました。(とりあえず動かしてみたい!というのが当面の目的なので…笑)
最初「分類」と「クラスタリング」の違いすらあまり分からなかったのですが、チートシートによると教師データ有りの場合は「分類」、無しの場合は「クラスタリング」を使うと良いようです。
ツイートの分類に使えそうな教師データは 探すの面倒 残念ながら見つからなかったので、クラスタリングに決定した訳です。
ちなみに概念的な違いは「クラスタリングとクラス分類」に詳しく書かれていました。
次にクラスタリングの手法ですが、Twitterのデータをいくつに分けられるか(クラスタがいくつになるか)は事前に分かっていないので、本来は**「Mean shift」等を使うべきなのでしょうが、まずは参考情報が多い「KMeans」**でやってみることにしました。
テキストのままじゃ駄目なんだ…
お勉強開始時、
「テキストデータでも何でも、そのまま機械学習ライブラリにぶちこめば、良い感じで結果を出してくれる。」
と勝手に思って、コピペでスパッと試してみましたが、結果は、
>> from sklearn.cluster import KMeans
>> km = KMeans(n_clusters=3)
>> km.fit(["あーちゃん", "かしゆか", "のっち"])
ValueError: could not convert string to float: あーちゃん
こんな感じであーちゃんをfloat型に変更できないというValueErrorになりました←
参考書「実践機械学習システム」の引用になりますが、
- 「機械学習の観点ではテキストデータは役に立たない、意味のある数値に変換できて初めてクラスタリング等の機械学習アルゴリズムに入力することができる。」
- 「bag-ob-wordsという手法では、単語の出現回数を特徴量として扱うことができる。出現回数はベクトルで表現される。」
だそうです。生データからその特徴を数値で表した「特徴量」にしないと駄目なんですね。なるほどなるほど…甘かった…。
ということで、まずはテキストデータをbag-ob-words手法を用いて数値(ベクトル)化をしてみます。
なぜベクトルなんか使わなきゃならんのか??「[初心者向け]機械学習におけるベクトル化 入門」が参考になります…!(基本、他力本願)
ベクトライズ
scikit-learnのドキュメントによるとベクトルに変換する(ベクトライズ)には、以下のステップを踏む必要があるようです。
- tokenizing(いわゆる分かち書き、トークン化)
- counting(単語の数を数える)
- normalizing(正規化。tf-idf等で単語毎に重み付けをする)
英語等の空白で区切られたテキストであればscikit-learnのTfidfVectorizerクラスが全てやってくれるのですが、日本語のtokenizingは出来ないようです。
そこで、以下のサンプルデータを用いてベクトライズする為の実装を、ステップ別に見ていきます。
テキストA:「のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx」
テキストB:「Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx」
テキストC:「チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx」
テキストD:「Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx」
テキストE:「チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx」
tokenizing
前回の記事の通りtokenizingにはMecabを使います。
とは言っても、以下のようにトークン化処理を実行する関数を定義して、TfidfVectorizer初期化時に渡してあげるだけで良いようです。
def mecab_tokenizer(text):
"""Mecabを使ったトークン化処理(省略)"""
return word_list
vectorizer = TfidfVectorizer(tokenizer=mecab_tokenizer)
ちなみに類似するtokenを基本形にして共通化する処理**「ステミング処理」**は後ほどコードで示しますがtokenize関数内でしれっとやってます。
Mecabで形態素解析すると単語毎に以下のような情報が得られ、基本形も分かる(6つ目の要素)ので、
これをtokenとして使うことで実現しています。(この時点でのっちは片仮名でノッチ…)
名詞,一般,*,*,*,*,ノッチ,ノッチ,ノッチ
形容詞,自立,*,*,形容詞・イ段,基本形,可愛い,カワイイ,カワイイ
名詞,固有名詞,一般,*,*,*,Perfume,パフューム,パフューム
名詞,固有名詞,一般,*,*,*,HTTPS,エイチティーティーピーエス,エイチティーティーピーエス
Mecabによる形態素解析についてはスタバのTwitterデータをpythonで大量に取得し、データ分析を試みる その3を参考にさせて頂きました。
counting/normalizing
これらはTfidfVectorizerが良い感じでやってくれます。
tf-idfの概念については「scikit-learn で TF-IDF を計算する」を参考にさせて頂きました。
さらに出現回数が多すぎて重要度が低い単語を取り除く処理**「ストップワード除去」も、TfidfVectorizerに教えてあげればやってくれます。便利。ここでは全てのテキストに含まれる「Perfume」と「HTTPS」**を除外することにしました。
逆に出現回数が少ない単語を無視したい時はmin_dfを指定して除外します。
今回なんとなく**「1」**を指定しましたが、計算方法は分かっておらず、
また値を変えてもサンプルデータではデータ量が少な過ぎるのか効果は分かりませんでした…今後要検証です。
vectorizer = TfidfVectorizer(
min_df=1, stop_words=[u"Perfume", u"HTTPS"], tokenizer=mecab_tokenizer)
ソースコード
さて、こんな感じになりました。(mecab_tokenizerも文字コードで地味にハマりました。)
# !/usr/bin/env python
# -*- coding:utf-8 -*-
import codecs
import ast
import MeCab as mc
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
MECAB_OPT = "-Ochasen -d C:\\tmp\\mecab-ipadic-neologd\\"
SAMPLE_DATA = [
u"のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx",
u"Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx",
u"チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx",
u"Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx",
u"チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx"
]
def mecab_tokenizer(text):
t = mc.Tagger(MECAB_OPT)
node = t.parseToNode(text.encode("utf-8"))
word_list = list()
while node:
if node.surface != "": # ヘッダとフッタを除外
res = node.feature.split(",")
word_type = res[0]
if word_type in ["名詞", "動詞", "形容詞", "副詞"]:
basic_word = res[6]
if basic_word != "*":
word_list.append(basic_word.decode("utf-8"))
node = node.next
if node is None:
break
return word_list
def main():
vectorizer = TfidfVectorizer(
min_df=1, stop_words=[u"Perfume", u"HTTPS"], tokenizer=mecab_tokenizer)
# サンプルデータを使うのでファイルからの読み込み処理は一旦コメントアウト。
# with codecs.open("es.log", "r", "utf-8") as f:
# es_dict = ast.literal_eval(f.read())
# print "doc:%d" % len(es_dict) # doc:265
# text_list = [es_doc["_source"]["text"] for es_doc in es_dict]
# サンプルデータで代入
text_list = SAMPLE_DATA
# サンプルデータのベクトル化
tfidf_weighted_matrix = vectorizer.fit_transform(text_list)
# ベクトル情報を確認
print u"テキスト数:%d,単語の種類数:%d" % tfidf_weighted_matrix.shape
print u"単語の種類:%s\n" % ",".join(vectorizer.get_feature_names())
for n, text in enumerate(text_list):
print "[%s]" % text
print u"重み: %s\n" % ",".join(["%s:%.2f" % (token, weight) for token, weight in
zip(vectorizer.get_feature_names(),
tfidf_weighted_matrix.getrow(n).toarray()[0]
)])
if __name__ == '__main__':
main()
実行結果
以下のようになりました。重み付けしたベクトル化できたようです。
テキスト数:5,単語の種類数:14
単語の種類:London,NowPlaying,a,in,いい,くる,すごい,カッコ,チョコレイト・ディスコ,ノッチ,ライブ,可愛い,演出,行く
[のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:0.71,ライブ:0.00,可愛い:0.71,演出:0.00,行く:0.00
[Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:0.78,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:0.63,行く:0.00
[チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.78,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:0.63,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:0.00,行く:0.00
[Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx]
重み: London:0.46,NowPlaying:0.00,a:0.46,in:0.46,いい:0.00,くる:0.46,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:0.00,行く:0.37
[チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:0.45,くる:0.00,すごい:0.00,カッコ:0.45,チョコレイト・ディスコ:0.36,ノッチ:0.00,ライブ:0.45,可愛い:0.00,演出:0.36,行く:0.36
ちょっと見づらいので結果を表にします。
1テキスト中の単語の重み付けを眺めてみると**「当該単語以外に登場する単語が少ない」「当該単語が他のテキストに含まれない」**場合に値が高くなっており、テキストの特徴を表していることが分かります。
単語 | テキストA | テキストB | テキストC | テキストD | テキストE |
---|---|---|---|---|---|
London | 0 | 0 | 0 | 0.46 | 0 |
NowPlaying | 0 | 0 | 0.78 | 0 | 0 |
a | 0 | 0 | 0 | 0.46 | 0 |
in | 0 | 0 | 0 | 0.46 | 0 |
いい | 0 | 0 | 0 | 0 | 0.45 |
くる | 0 | 0 | 0 | 0.46 | 0 |
すごい | 0 | 0.78 | 0 | 0 | 0 |
カッコ | 0 | 0 | 0 | 0 | 0.45 |
チョコレイト・ディスコ | 0 | 0 | 0.63 | 0 | 0.36 |
ノッチ | 0.71 | 0 | 0 | 0 | 0 |
ライブ | 0 | 0 | 0 | 0 | 0.45 |
可愛い | 0.71 | 0 | 0 | 0 | 0 |
演出 | 0 | 0.63 | 0 | 0 | 0.36 |
行く | 0 | 0 | 0 | 0.37 | 0.36 |
なおTfidfVectorizerではなく、CountVectorizerを使った場合、
単語登場頻度が考慮されて無いため、重みは以下のような結果(0 or 1)になりました。
つまり、特徴の表現が粗いことが分かります。
[のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:1.00,ライブ:0.00,可愛い:1.00,演出:0.00,行く:0.00
[Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:1.00,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:1.00,行く:0.00
[チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:1.00,a:0.00,in:0.00,いい:0.00,くる:0.00,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:1.00,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:0.00,行く:0.00
[Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx]
重み: London:1.00,NowPlaying:0.00,a:1.00,in:1.00,いい:0.00,くる:1.00,すごい:0.00,カッコ:0.00,チョコレイト・ディスコ:0.00,ノッチ:0.00,ライブ:0.00,可愛い:0.00,演出:0.00,行く:1.00
[チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx]
重み: London:0.00,NowPlaying:0.00,a:0.00,in:0.00,いい:1.00,くる:0.00,すごい:0.00,カッコ:1.00,チョコレイト・ディスコ:1.00,ノッチ:0.00,ライブ:1.00,可愛い:0.00,演出:1.00,行く:1.00
類似度を算出してみる
ここまでで、KMeansにはベクトル化されたデータがあればクラスタリングできるので、準備は整いました(多分)。
念のためここで、クラスタリングの前にユークリッドノルム(ユークリッド距離)によるテキスト同士の類似度を算出してみて、ベクトル化が適切かざっくり見ておくことにします。
そう、ユークリッド距離とは…!…「データマイニング クラスター分析」が参考になりました!(最後まで他力本願)
試しに、以下の類似度確認用のテキストデータについて同じくベクトル化し、
上記サンプルデータと一番類似しているものを見つけてみます。
「Perfumeの演出などで活躍する真鍋大度さん率いるライゾマティクス https://t.co/xxxxxxxxxxxx」
「ノッチ可愛い #デンジャラス #Perfumeではない https://t.co/xxxxxxxxxxxx」
コードは、さっきのコードに以下を追加するような感じです。
# ユークリッド距離計算
def dist_raw(v1, v2):
delta = v1 - v2
return np.linalg.norm(delta.toarray())
# 類似度試験用のテキストをベクトル化。
test_text_list = [
u"Perfumeの演出などで活躍する真鍋大度さん率いるライゾマティクス https://t.co/xxxxxxxxxxxx",
u"ノッチ可愛い #デンジャラス #Perfumeではない https://t.co/xxxxxxxxxxxx"
]
test_tfidf_weighted_matrix = vectorizer.transform(test_text_list)
# テキスト毎に類似度を出してみる。
for testn, test_vec in enumerate(test_tfidf_weighted_matrix):
print "Test No%i text:%s" % (testn, test_text_list[testn])
dest_list = list()
for n, text in enumerate(text_list):
text_vec = tfidf_weighted_matrix.getrow(n)
d = dist_raw(text_vec, test_vec)
print "- No%i dist:%.2f, text:%s" % (n, d, text)
dest_list.append(d)
min_dest = min(dest_list)
min_text_num = dest_list.index(min_dest)
min_text = text_list[min_text_num]
# 最も類似したテキストを表示
print "Best Match:No%i, dist:%.2f, text:%s" % (min_text_num, min_dest, min_text)
print
結果、こんな感じになりました。
Test No0 text:Perfumeの演出などで活躍する真鍋大度さん率いるライゾマティクス https://t.co/xxxxxxxxxxxx
- No0 dist:1.41, text:のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx
- No1 dist:0.86, text:Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx
- No2 dist:1.41, text:チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx
- No3 dist:1.41, text:Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx
- No4 dist:1.13, text:チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx
Best Match:No1, dist:0.86, text:Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx
Test No1 text:ノッチ可愛い #デンジャラス #Perfumeではない https://t.co/xxxxxxxxxxxx
- No0 dist:0.00, text:のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx
- No1 dist:1.41, text:Perfumeの演出スゴイ #prfm #Perfume_um https://t.co/xxxxxxxxxxxx
- No2 dist:1.41, text:チョコレイト・ディスコ / Perfume #NowPlaying https://t.co/xxxxxxxxxxxx
- No3 dist:1.41, text:Perfume A Gallery Experience in Londonに行ってきました https://t.co/xxxxxxxxxxxx
- No4 dist:1.41, text:チョコレイトディスコの演出カッコいい。ライブ行きたい。#Perfume https://t.co/xxxxxxxxxxxx
Best Match:No0, dist:0.00, text:のっち可愛い #Perfume https://t.co/xxxxxxxxxxxx
人間で見た目で比べた感じと、数値が表すベクトルの距離が近さ(=類似度が高さ)が、ある程度一致しているように思います。ベクトル化がうまくいって、類似度が正しく計算できていますね、きっと!!←
しかしデンジャラスのノッチを除外する方法が浮かばない…Mecabの辞書を弄るしかないのかな…。
やっと
ベクトル化、類似度の算出か出来たので、次回はKMeansにベクトルを食わせてクラスタリングしてみます!(長かった…)