テキストの類似度って言っても出現する単語の回数比較でしか無いので文意解釈はしてないです。あくまで「出現した文字の一致度」ですね。
Word2Vecとかを使ってテキストを拡張してあげれば少しは文意を加味した類似度といえるのかもです。
from scipy.spatial.distance import cosine
import unittest
class SentenceSimilarity(object):
def __init__(self):
self._A = None
self._B = None
@property
def A(self):
return self._A
@property
def B(self):
return self._B
@A.setter
def A(self, v):
self._A = [i for i in v.replace(' ', ',').split(',') if len(i) > 0]
@B.setter
def B(self, v):
self._B = [i for i in v.replace(' ', ',').split(',') if len(i) > 0]
def distance(self):
if self._A is None or self._B is None:
return False
if len(self._A) < 2 or len(self._B) < 2:
return False
return self._distance()
def _distance(self):
_words = []
_words.extend(self._A)
_words.extend(self._B)
_words = list(set(_words))
_words.sort()
_listA = [self._A.count(_w) for _w in _words]
_listB = [self._B.count(_w) for _w in _words]
try:
return 1 - cosine(_listA, _listB)
except:
return False
class TestSentenceSimilarity(unittest.TestCase):
def setUp(self):
pass
def test_0(self):
_sentence = SentenceSimilarity()
_sentence.A = '今期 業績 予想 未定 期限切れ 肉 問題 販売減'
_sentence.B = '都市 対抗 野球 西濃運輸 初優勝 佐伯 富士 重工'
self.assertGreater(_sentence.distance(), 0.0)
def test_1(self):
_sentence = SentenceSimilarity()
_sentence.A = '今期 業績 予想 未定 期限切れ 肉 問題 販売減'
self.assertEqual(_sentence.distance(), False)
def test_2(self):
_sentence = SentenceSimilarity()
_sentence.A = '今期'
_sentence.B = '都市'
self.assertEqual(_sentence.distance(), False)
def test_3(self):
_sentence = SentenceSimilarity()
_sentence.A = '今期,業績,予想,未定'
_sentence.B = '都市,業績,予想,未定'
_sentence.B = '業績 対抗 野球 西濃運輸 初優勝'
self.assertGreater(_sentence.distance(), 0.0)
if __name__ == '__main__':
unittest.main()
使い方はテストをみていただければご理解いただけるかと。
コサイン類似度の計算なんて
return 1 - cosine(_listA, _listB)
の部分でしかやってなくて、残りは全部下ごしらえです。
_sentence = SentenceSimilarity()
_sentence.A = ‘今期 業績 予想 未定 期限切れ 肉 問題 販売減’
_sentence.B = ‘都市 対抗 野球 西濃運輸 初優勝 佐伯 富士 重工’
インスタンス作って、AとB(命名が安直ですいません)にカンマ切り、もしくはスペース切りの文字列を指定します。なので事前に分かち書きをしておく必要があります。
まあ文章を渡してメソッドの中で分かち書きしてもいいんですけどね。手元にあったのが分かち書き済みだったので今回はそのまま使いました。
_sentence.distance()
で、類似度を求めます。類似度を求める前に_Aと_Bの和集合を作って、和集合の単語リストにある単語が_Aと_Bそれぞれで幾つずつ出現するかを求めます。
上記例だと和集合は[今期,業績,予想,未定,期限切れ,肉,問題,販売減,都市,対抗,野球,西濃運輸,初優勝,佐伯,富士,重工]となり、
_listAは[1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0]
_listBは[0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1]
になるはずです。で、これらをベクトルにしてコサイン類似度を求めます。今は0と1だけですが複数回出現する単語があれば当然2以上の値も出てきます。
コサイン類似度は0から1の値で返ってきて、値が大きければ大きいほど類似しています。上記サンプルだと0が返ってくるはずです。ぜんぜん違う、ってことですね。
アイテムベースのレコメンドに使えると思います。サービスとして提供するレコメンドエンジンであればこんな単純な計算じゃないでしょうね。
Python 3.4.2, scipy 0.14.0 で書きました。
使ったのはMovieLens 100k。
「100,000 ratings from 1000 users on 1700 movies.」
だそうです。
from sklearn.cluster import KMeans
def main():
_items = []
_f = open('./rating.csv' )
_lines = _f.readlines()
for _line in _lines:
_items.append(_line.split(','))
_f.close()
km = KMeans(init='k-means++')
km.fit(_items)
print(km.labels_)
if __name__ == '__main__':
main()
すげえ簡単。。。
CSVは1行が1人分で全部の映画のレーティング情報(0が観てない、評価は1から5)が入っています。1000ユーザーが1700本の映画を評価(未鑑賞含む)してます。
だいたいこんな感じ。
5,3,4,3,3,5,4,1,5,3,2,5,5,5,5,5,3,4,,,,,,,,
4,0,0,0,0,0,0,0,0,2,0,0,4,4,0,0,0,0,,,,,,,,
得られる結果は
[0 1 4 4 3 2 6 3 4 6 3 7 6 2 1 5 1 6 4 7 1 3 3 7,,,,,,,
インデックス2番のユーザーさんと3番のユーザーさんは好きな映画が似る傾向にありそうですね。
Python 3.4.2、scikit-learn 0.15.2 で試しました。
Clustering text documents using k-means (K平均法を使ってテキスト文書をクラスタリングする)というそのまんまのサンプルがあったので写経して最低限だけ削りだしてみた。
K平均法とかTF-IDFとか潜在意味解析の説明は割愛。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from sklearn.decomposition import TruncatedSVD
from sklearn.preprocessing import Normalizer
def main():
_items = [
'わたし まけ まし た わ',
'わたし まけ まし た わ',
'わたし まけ まし た わ',
'わたし まけ まし た わ',
'となり の きゃく は よく かき くう きゃく だ',
'にわ には にわ なかにわ には にわ にわとり が いる',
'バカ と テスト と 召喚獣',
'俺 の 妹 が こんな に 可愛い わけ が ない'
]
vectorizer = TfidfVectorizer(
use_idf=True
)
X = vectorizer.fit_transform(_items)
lsa = TruncatedSVD(10)
X = lsa.fit_transform(X)
X = Normalizer(copy=False).fit_transform(X)
km = KMeans(
init='k-means++',
)
km.fit(X)
print(km.labels_)
if __name__ == '__main__':
main()
テキストは分かち書きされてる状態(空白区切りになってます)でドキュメント単位でリストにいれておく。これを元データとしてTfidfVectorizerに突っ込みます。ここでドキュメントがベクトル化されます。
で、潜在意味解析を通した上でK平均法でクラスタリング。至って単純。
km.labels_を出力すると
[1 1 1 1 0 2 4 3]
こんな感じで出力されるのですが、これは「データセットのインデックス0,1,2,3は1番クラスタ、インデックス4は0番クラスタ、、、」ということです。これを使って元データをグルーピングしておきます。
すっごい簡単。使い方はこれであってる、と思う。たぶん。あ、ドキュメント数は少なくても1,000以上ないときれいにクラスタリングされなそうだし、テキストももっと長くないと納得のいく結果は出力されにくいんじゃないかと。
自分のツイートを使ったり青空文庫を使ったりで気軽に試せそうです。
Python 3.4.2、scikit-learn==0.15.2 で動かしました。
昨日Mahoutの記事書いたばっかりなのにね。
0.8あたりからMahoutはHDFSからデータ読み込むのがデフォルトになったのか、ファイルを読み込んでもらおうとしても「HADOOP_HOMEが無いよ!」ってエラー吐いて先に進んでくれません。
Hadoopいれろや、ってことなんですけどたいしてメモリ積んでないVMでHadoopいれると結構ツラいものがあるし、たかだか数百万件のログ読ますのにHadoopいれるのもなぁ、という感じでローカルモードで使う方法調べた時のメモ。使ったのはMahout 0.9です。
/usr/lib/にDLしてきたMahoutを置いて、.bash_profileに
export JAVA_HOME=/usr/lib/jvm/jre-1.7.0
export PATH=$JAVA_HOME/bin:$PATH
export MAHOUT_HOME=/usr/lib/mahout-distribution-0.9
export PATH=$PATH:$MAHOUT_HOME/bin
export MAHOUT_LOCAL=TRUE
export CLASSPATH=:$MAHOUT_HOME/lib/hadoop/hadoop-core-1.2.1.jar:$MAHOUT_HOME/lib:$CLASSPATH
って記述。
細かいことは割愛なんですけど、ポイントは「export MAHOUT_LOCAL=TRUE」の部分で、あとは通常の設定となんも変わらず。
ただ、最近Mahout使うよりPythonで書くほうが多いのであんまりMahout触ってない。