この回の演習では,以下のライブラリを使用します.
In [1]:
import numpy as np
import gensim
from nltk.corpus import stopwords
import pandas as pd
np.set_printoptions(precision=4)
In [2]:
# 小数点3ケタまで表示
%precision 3
Out[2]:
In [3]:
with open("../data/sample.corpus", "r") as f: #sample.corpusの読み込み
text = f.read().strip().split("\n") #sample.corpusのテキストデータを取得し,それを改行で分割
text
Out[3]:
3件の文書があることが分かりますね.次に,文章をトークン(単語)に分割します.今回は簡単のため単純にスペース区切りによって単語に分割します.
In [4]:
raw_corpus = [d.lower().split() for d in text] #文章を小文字に変換して単語に分割する
print("d1=" , raw_corpus[0])
print("d2=" , raw_corpus[1])
print("d3=" , raw_corpus[2])
文が単語の集合に変換されました.しかし,この単語集合には "i" や "of" などのストップワードが含まれています.そこで,ストップワードを除去してみましょう.
ストップワードのリストはネットで探せば様々な種類が見つかります.ここでは,nltkのstopwordsモジュールを利用します.
In [5]:
# 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])
In [6]:
dictionary = gensim.corpora.Dictionary(corpus) #コーパスを与えて,単語->IDの辞書を作成する
dictionary.token2id #作成された辞書の中身
Out[6]:
このdictionaryを用いて,文書の単語をID化します.
In [7]:
id_corpus = [dictionary.doc2bow(document) for document in corpus]
id_corpus
Out[7]:
作成されたid_corpusは,たとえば,1件目の文書は
In [8]:
id_corpus[0]
Out[8]:
という内容になっています.たとえば,(0,2)というデータは
単語ID0の単語が2回出現
という内容を表しています. つまり,単語の出現頻度(term frequency)のみで文書を特徴ベクトル化したことになります.なお,これをnumpyのベクトルとして抽出したければ,corpus2denseメソッドを用います.
In [9]:
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])
今回用意したコーパスは語彙数が8しかありませんが,実際のケースでは,この特徴ベクトルは非常に疎になることが容易に想像つくと思います.
さて,id_corpusからTFIDFで重み付けされた特徴ベクトルを得るには, models.TfidfModel メソッドを用います.
In [10]:
tfidf_model = gensim.models.TfidfModel(id_corpus, normalize=False) #normalize=Trueにすると,文書長によってtfを正規化する
tfidf_corpus = tfidf_model[id_corpus] #id_corpusをtfidfで重み付けされたものに変換
これでTF-IDFで重み付けされた特徴ベクトルが得られました.たとえば,1件目の文書$d_1$に対する特徴ベクトル${\mathbf d}_1$の中身を見てみます.
In [11]:
tfidf_corpus[0]
Out[11]:
TFIDFの値は,(単語ID,重み) として得られています.単語IDを実際の単語に変換するにはdictionaryを通します.
In [12]:
[(dictionary[x[0]], x[1]) for x in tfidf_corpus[0]]#dictionary[token_id]でアクセスすると実際の単語が返ってくる
Out[12]:
同様に2件目の文書$d_2$についても見てみます.
In [13]:
doc2 = [(dictionary[x[0]], x[1]) for x in tfidf_corpus[1]]
doc2
Out[13]:
たとえば, 文書$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で得られた結果と一致していることが分かります.
In [14]:
import math
2*math.log2(3) #2log3の計算方法
Out[14]:
In [15]:
# 各文書の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])
In [16]:
# コサイン類似度を計算する関数を用意
from scipy.spatial.distance import cosine
def cosine_sim(v1, v2):
#scipyのcosineは類似度ではなく距離関数のため, 1-コサイン距離 とすることで,コサイン類似度に変換する
return 1.0 - cosine(v1, v2)
In [17]:
# 各文書間のコサイン類似度を計算してみる
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]))
それでは,クエリを特徴ベクトルに変換し,クエリと文書のコサイン類似度を求めていきましょう.
In [18]:
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)
In [19]:
print([(dictionary[x[0]], x[1]) for x in tfidf_q])
In [20]:
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]))
この結果から,q={"kansai", "japan"} というクエリに対しては,$d_2,d_3, d_1$の順でランク付けされることが分かります.
In [21]:
import matplotlib.pylab as plt
In [22]:
%matplotlib inline
In [23]:
# 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)
In [24]:
# 散布図にプロットするため,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
Out[24]:
In [25]:
# 散布図をプロット
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) #データ点にラベル名を付与
この図を見てみると,やはりクエリ$q$と文書$d_2$はほぼ同じ方向(つまり,コサイン類似度が1に近い)であることがわかり, $q$と$d_1$の角度はほぼ直角(つまりコサイン類似度が0)であることがわかります.
以下からコーパスを1つ以上選択し,ベクトル空間モデルに基づいた検索を実現せよ.3種類以上のクエリでの検索結果を示すこと.
In [26]:
# 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]
Out[26]:
In [27]:
# `bow` には形態素解析でトークン化された単語列がスペース区切りで保存されている.
# これを使用して特徴ベクトルを作成するとよい.
docs[0]["bow"]
Out[27]:
上記(1)に対して, Okapi BM25 に基づくランキングを行い,上記(1)の結果と比較してみよ.
適合性フィードバックによるクエリ修正を行い,検索結果がどのように変化するのか分析せよ.また,コーパス及びクエリを可視化することで,修正されたクエリが適合・不適合文書の特徴ベクトルにどのように影響されているか幾何的に分析せよ.
いずれかの方法で,ipython notebookのページ(.ipynbファイル)とそのhtml版を提出すること.
受け付けます
.
In [ ]: