<jupyter_start><jupyter_text>第2回 ベクトル空間モデルこの演習ページでは，ベクトル空間モデルに基づく情報検索モデルについて説明します．具体的には，文書から特徴ベクトルへの変換方法，TF-IDFの計算方法，コサイン類似度による文書ランキングについて，その実装例を説明します．第2回演習の最終目的は，ある与えられた文書コーパスに対して，TF-IDFで重み付けされた特徴ベクトルによる文書ランキングが実装できるようになることです．  ライブラリこの回の演習では，以下のライブラリを使用します．　- [numpy, scipy](http://www.numpy.org/)  + Pythonで科学技術計算を行うための基礎的なライブラリ．- [gensim](https://radimrehurek.com/gensim/index.html)  + トピックモデリング（LDA）やword2vecなどを手軽に利用するためのPythonライブラリ．- [nltk (natural language toolkit)](http://www.nltk.org/)  + 自然言語処理に関するpythonライブラリです．この演習ではストップワードのために用います．ほかにも，単語のステミングやトークナイズなどの機能をはじめ，品詞推定，依存関係分析など自然言語処理のあらゆるメソッドが用意されています．- [pandas](http://pandas.pydata.org/)  + pythonでデータ分析をするためのフレームワークです．この演習ではデータをプロットするために用いています．  第2回目の演習の内容``h29iro/data/`` に `sample.corpus` というファイルを置いています． このファイルには改行区切りで3件の短い文書が保存されています．この演習では，このファイルに対してTF-IDFで重み付けされた特徴ベクトルを作成し，コサイン類似度によるランキングを行います．  1. 文書の読み込みとトークナイズまずは，`sample.corpus`を読み込み，各文書のBoW表現を抽出します．<jupyter_code>import numpy as np
import gensim
from nltk.corpus import stopwords
import pandas as pd
np.set_printoptions(precision=4)
# 小数点3ケタまで表示
%precision 3
with open("../data/sample.corpus", "r") as f:  #sample.corpusの読み込み
    text = f.read().strip().split("\n") #sample.corpusのテキストデータを取得し，それを改行で分割
text<jupyter_output><empty_output><jupyter_text>３件の文書があることが分かりますね．次に，文章をトークン（単語）に分割します．今回は簡単のため単純にスペース区切りによって単語に分割します．<jupyter_code>raw_corpus = [d.lower().split() for d in text] #文章を小文字に変換して単語に分割する
print("d1=" , raw_corpus[0])
print("d2=" , raw_corpus[1])
print("d3=" , raw_corpus[2])<jupyter_output>d1= ['i', 'live', 'in', 'kyoto', 'and', 'kyoto', 'is', 'a', 'beautiful', 'city']
d2= ['kyoto', 'was', 'the', 'captial', 'of', 'japan', 'and', 'is', 'in', 'kansai', 'and', 'kansai', 'is', 'in', 'japan']
d3= ['kyoto', 'is', 'in', 'kansai', 'and', 'kyoto', 'is', 'historical', 'city']<jupyter_text>文が単語の集合に変換されました．しかし，この単語集合には "i" や "of" などのストップワードが含まれています．そこで，ストップワードを除去してみましょう．ストップワードのリストはネットで探せば様々な種類が見つかります．ここでは，nltkのstopwordsモジュールを利用します．<jupyter_code># stopwords.words("english")に含まれていない単語のみ抽出
corpus = [list(filter(lambda word: word not in stopwords.words("english"), x)) for x in raw_corpus] 
print("d1=" , corpus[0])
print("d2=" , corpus[1])
print("d3=" , corpus[2])<jupyter_output>d1= ['live', 'kyoto', 'kyoto', 'beautiful', 'city']
d2= ['kyoto', 'captial', 'japan', 'kansai', 'kansai', 'japan']
d3= ['kyoto', 'kansai', 'kyoto', 'historical', 'city']<jupyter_text>2. 特徴ベクトルの生成次に文書の特徴ベクトルを生成します．ここからの流れは，以下の通りになります．1. 文書集合（corpus）から 単語->単語ID の辞書 (dictionary) を作成する．2. 作成された辞書を基に，文書を (単語ID，出現回数）の集合 (id_corpus) として表現する．3. id_corpusからTfidfModelを用いて，TF-IDFで重み付けされた特徴ベクトルを作成する．まずは，文書集合（コーパス）から単語->単語ID の辞書 (dictionary) を作成します．<jupyter_code>dictionary = gensim.corpora.Dictionary(corpus) #コーパスを与えて，単語->IDの辞書を作成する
dictionary.token2id #作成された辞書の中身<jupyter_output><empty_output><jupyter_text>このdictionaryを用いて，文書の単語をID化します．<jupyter_code>id_corpus = [dictionary.doc2bow(document) for document in corpus]
id_corpus<jupyter_output><empty_output><jupyter_text>作成されたid_corpusは，たとえば，1件目の文書は<jupyter_code>id_corpus[0]<jupyter_output><empty_output><jupyter_text>という内容になっています．たとえば，(0,2)というデータは```単語ID0の単語が２回出現```という内容を表しています． つまり，単語の出現頻度(term frequency)のみで文書を特徴ベクトル化したことになります．なお，これをnumpyのベクトルとして抽出したければ，corpus2denseメソッドを用います．<jupyter_code>tf_vectors = gensim.matutils.corpus2dense(id_corpus, len(dictionary)).T
print("d1=", tf_vectors[0])
print("d2=", tf_vectors[1])
print("d3=", tf_vectors[2])<jupyter_output>d1= [ 1.  1.  2.  1.  0.  0.  0.  0.]
d2= [ 0.  0.  1.  0.  1.  2.  2.  0.]
d3= [ 0.  0.  2.  1.  0.  1.  0.  1.]<jupyter_text>今回用意したコーパスは語彙数が8しかありませんが，実際のケースでは，この特徴ベクトルは非常に疎になることが容易に想像つくと思います．さて，id_corpusからTFIDFで重み付けされた特徴ベクトルを得るには， models.TfidfModel メソッドを用います．<jupyter_code>tfidf_model = gensim.models.TfidfModel(id_corpus, normalize=False) #normalize=Trueにすると，文書長によってtfを正規化する
tfidf_corpus = tfidf_model[id_corpus] #id_corpusをtfidfで重み付けされたものに変換<jupyter_output><empty_output><jupyter_text>これでTF-IDFで重み付けされた特徴ベクトルが得られました．たとえば，1件目の文書$d_1$に対する特徴ベクトル${\mathbf d}_1$の中身を見てみます．<jupyter_code>tfidf_corpus[0]<jupyter_output><empty_output><jupyter_text>TFIDFの値は，(単語ID，重み） として得られています．単語IDを実際の単語に変換するにはdictionaryを通します．<jupyter_code>[(dictionary[x[0]], x[1]) for x in tfidf_corpus[0]]#dictionary[token_id]でアクセスすると実際の単語が返ってくる<jupyter_output><empty_output><jupyter_text>同様に2件目の文書$d_2$についても見てみます．<jupyter_code>doc2 = [(dictionary[x[0]], x[1]) for x in tfidf_corpus[1]]
doc2<jupyter_output><empty_output><jupyter_text>たとえば， 文書$d_{2}$における`japan`のTFIDF値が本当に正しいのか検証してみましょう．$tfidf_{d_2, japan} = tf_{d_2, japan} \log \frac{N}{df_{japan}}$ ,いま， $tf_{d_2, japan} = 2$, $N = 3$, $df_{japan}$ = 1 ですので，$tfidf_{d_2, japan} = 2 \log 3 = 3.170$となり，gensimで得られた結果と一致していることが分かります．<jupyter_code>import math
2*math.log2(3) #2log3の計算方法<jupyter_output><empty_output><jupyter_text>3. コサイン類似度それでは，コサイン類似度による文書ランキングを行ってみましょう．クエリと文書の類似度を測る前に，まずは文書同士のコサイン類似度を計算してみます． コサイン類似度の計算はgensimでも良いのですが，ここでは，いったんnumpyのベクトルを取得して，そのベクトルに対してコサイン類似度を計算してみます．<jupyter_code># 各文書のtfidfベクトルを取得
tfidf_vectors = gensim.matutils.corpus2dense(tfidf_corpus, len(dictionary)).T
print ("d1=", tfidf_vectors[0])
print ("d2=", tfidf_vectors[1])
print ("d3=", tfidf_vectors[2])
# コサイン類似度を計算する関数を用意
from scipy.spatial.distance import cosine
def cosine_sim(v1, v2):
    #scipyのcosineは類似度ではなく距離関数のため， 1-コサイン距離　とすることで，コサイン類似度に変換する
    return 1.0 - cosine(v1, v2) 
# 各文書間のコサイン類似度を計算してみる
print ("sim(d1, d2)=", cosine_sim(tfidf_vectors[0], tfidf_vectors[1]))
print ("sim(d2, d3)=", cosine_sim(tfidf_vectors[1], tfidf_vectors[2]))
print ("sim(d1, d3)=", cosine_sim(tfidf_vectors[0], tfidf_vectors[2]))<jupyter_output>sim(d1, d2)= 0.0
sim(d2, d3)= 0.102562095083
sim(d1, d3)= 0.082618937993<jupyter_text>それでは，クエリを特徴ベクトルに変換し，クエリと文書のコサイン類似度を求めていきましょう．<jupyter_code>q = {"kansai", "japan"}
tfidf_q = tfidf_model[dictionary.doc2bow(q)]  #クエリをtfidfベクトルに変換
query_vector = gensim.matutils.corpus2dense([tfidf_q], len(dictionary)).T[0] #numpyのベクトルに変換
print ("q=", query_vector)
print([(dictionary[x[0]], x[1]) for x in tfidf_q])
print ("sim(q, d1) = ", cosine_sim(query_vector, tfidf_vectors[0]))
print ("sim(q, d2) = ", cosine_sim(query_vector, tfidf_vectors[1]))
print ("sim(q, d3) = ", cosine_sim(query_vector, tfidf_vectors[2]))<jupyter_output>sim(q, d1) =  0.0
sim(q, d2) =  0.905346644389
sim(q, d3) =  0.113284893168<jupyter_text>この結果から，q={"kansai", "japan"} というクエリに対しては，$d_2,d_3, d_1$の順でランク付けされることが分かります．  4. ベクトル空間の可視化最後に，得られた特徴ベクトルを可視化してみましょう．特徴ベクトルそのものは多次元（今回の場合は8次元）ですが，これを次元削減の手法を使って，2次元空間に射影してみます．今回は，`LSI`（Latent Semantic Indexing)という手法を用いて，特徴ベクトルを2次元空間に落とし込みます．LSIについては，講義で触れるかもしれません（講義の進み方次第）．<jupyter_code>import matplotlib.pylab as plt
%matplotlib inline
# LSIにより特徴ベクトルを2次元に落とし込む
lsi = gensim.models.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=2)
lsi_corpus = lsi[tfidf_corpus]
lsi_vectors = gensim.matutils.corpus2dense(lsi_corpus, 2).T
print("d1=", lsi_vectors[0])
print("d2=", lsi_vectors[1])
print("d3=", lsi_vectors[2])
query_lsi_corpus = lsi[[tfidf_q]] 
query_lsi_vector = gensim.matutils.corpus2dense(query_lsi_corpus, 2).T[0]
print ("q=", query_lsi_vector)
# 散布図にプロットするため，DataFrameに変換
axis_names = ["z1", "z2"]
doc_names = ["d1", "d2", "d3", "q"]
df = pd.DataFrame(np.r_[lsi_vectors, [query_lsi_vector]], 
                  columns=axis_names, index=doc_names) # np.r_ は行列同士の連結
df
# 散布図をプロット
fig, ax = plt.subplots()
df.plot.scatter(x="z1", y="z2", ax=ax)
ax.axvline(x=0, lw=2, color='red') #x軸とy軸に線を引く
ax.axhline(y=0, lw=2, color='red') 
ax.grid(True)
for k, v in df.iterrows():
    ax.annotate(k, xy=(v[0]+0.05,v[1]+0.05),size=15) #データ点にラベル名を付与<jupyter_output><empty_output><jupyter_text>この図を見てみると，やはりクエリ$q$と文書$d_2$はほぼ同じ方向（つまり，コサイン類似度が１に近い）であることがわかり， $q$と$d_1$の角度はほぼ直角（つまりコサイン類似度が0）であることがわかります． ----  演習課題その1 ベクトル空間モデル 必須課題（1） 与えられたコーパスに対する検索の実現以下からコーパスを1つ以上選択し，ベクトル空間モデルに基づいた検索を実現せよ．３種類以上のクエリでの検索結果を示すこと．1. 京都観光に関する83件の文書（h29iro/data/kyoto_results_100.json）2. 各自で用意したコーパス．ただし，100件以上の文書数を含むこと．もっと多くてもよい．3. Wikipedia（[参考: gensim Tutorial](https://radimrehurek.com/gensim/wiki.html) ）※ただし，モデル構築にとんでもない時間がかかるそうなので覚悟すること．- ページに表示する検索結果は各クエリ5-10件程度で良い．<jupyter_code># 1.のコーパスはjson形式で保管されている．
import json
with open("../data/kyoto_results_100.json", "r") as f:
    docs = json.load(f)
print("Num of docs = ", len(docs))
docs[0]
# `bow`　には形態素解析でトークン化された単語列がスペース区切りで保存されている．
# これを使用して特徴ベクトルを作成するとよい．
docs[0]["bow"]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15943248767584464, 'fraction_numerical': 0.03438739930263316, 'mean_word_length': 10.03183023872679, 'pattern_counts': {'":': 0, '<': 60, '<?xml version=': 0, '>': 63, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '9339', 'license': 'MIT', 'n_tokens_mistral': 5487, 'n_tokens_neox': 5270, 'n_words': 445, 'path': 'codes/.ipynb_checkpoints/2_Vector_Space_Model-checkpoint.ipynb', 'repo_name': 'dai39suke/iro_assignment', 'size': '8317'}