コサイン類似度
定義と特性
ベクトル空間モデル (vector space model)、または単語ベクトルモデル (term vector model) はテキスト文書やそれに類似する任意のオブジェクトを識別子のベクトルとして表現するための代数空間モデルである。ここで識別子とは索引として使用できる n-gram や形態素ような情報を指している。このベクトル空間は情報フィルタリングや情報検索、文書のインデックス化、関連性ランキングなどに使用することができる。TF-IDF や Word2Vec で算出した文書の特徴ベクトルは、文書をベクトル空間モデル上の点として表したものと考えることができる。
コサイン類似度 (cosine similarity) はベクトル空間モデルにおいて 2 つの非ゼロベクトルの内積を使って算出した余弦による類似尺度である (方向のみでベクトル量の類似性は示さない)。
ベクトル \(\vector{p}=(p_1,p_2,\ldots,p_n)\) と \(\vector{q}=(q_1,q_2,\ldots,q_n)\) の角度を \(\theta\) とした場合、\(\vector{p}\) と \(\vector{q}\) が全く同じ方向であれば \(\cos \theta=1\)、逆の方向であれば \(\cos\theta=-1\) となることから \(\cos\theta\) を 2 つのベクトルの類似性を [-1, 1] で表す尺度として使用することができる。実際に類似性として示す場合は 0 以下の値を 0 に正規化して [0, 1] の範囲に収めることもある。
2 つのベクトル成分が分かっている場合は \(\theta\) を求めるより \(\cos\theta\) を求める方が簡単である。ユークリッド空間でのベクトル内積より: \[ \begin{equation} \cos\theta = \frac{\vector{p} \cdot \vector{q}}{|\vector{p}| |\vector{q}|} = \frac{\sum_{i=1}^n p_{i} q_{i}}{\sqrt{\sum_{i=1}^n p_i^2}\sqrt{\sum_{i=1}^n q_i^2}} \label{cosine_similarity} \end{equation} \] と表すことができる。
式 (\(\ref{cosine_similarity}\)) は真逆を表す -1 から同じ方向を表す 1 までの値を取り、その中間値は連続した類似性/被類似性を示している。特に 0 の場合は 2 つのベクトルが直交している (無相関)。
プログラミング
TF-IDF 文書ベクトルによる検索
TF-IDF の文書ベクトル化で示した例を用いてコサイン類似度を使った文書検索を考えてみよう。各文書 \(d_i\) は以下のテキスト要素を含むと仮定する。\[ \begin{align*} d_1 & = \{🍌, 🍌, 🍎, 🍊\} \\ d_2 & = \{🍌, 🍎, 🍊, 🍒, 🍒\} \\ d_3 & = \{🍎, 🍇, 🍇\} \end{align*} \] この文書セットに対して、クエリー (疑似文書) \(q = \{🍌, 🍒\}\) に類似した順に文書をランキングする。まず最初に各文書の TF-IDF を使用した特徴ベクトルを計算する。
🍌 | 🍎 | 🍊 | 🍒 | 🍇 | |
\(d_1\) | 0.144 | 0.072 | 0.173 | 0.000 | 0.000 |
\(d_2\) | 0.058 | 0.058 | 0.139 | 0.277 | 0.000 |
\(d_3\) | 0.000 | 0.096 | 0.000 | 0.000 | 0.924 |
\(q\) | 0.144 | 0.000 | 0.000 | 0.347 | 0.000 |
次に表 1 に示した文書ごとの特徴ベクトルから文書間のコサイン類似度を求める。
\(d_1\) | \(d_2\) | \(d_3\) | |
---|---|---|---|
\(d_1\) | 1.000 | 0.481 | 0.031 |
\(d_2\) | 0.481 | 1.000 | 0.019 |
\(d_3\) | 0.031 | 0.019 | 1.000 |
\(q\) | 0.233 | 0.868 | 0.000 |
従ってクエリー \(q\) に最も類似した文書は \(d_2\)、次に類似している文書は \(d_1\) であり、最も類似していない文書は \(d_3\) とランク付けすることができる。特に \(d_3\) のコサイン類似度は 0 であるため類似部分が見られないと判断しても良いだろう。
TF-IDF およびコサイン類似度の算出に使用したコードは以下の通り。
def tf(doc:Seq[Int], term:Int):Double = doc(term) / doc.sum.toDouble
def idf(docs:Seq[Seq[Int]], term:Int):Double = {
math.log(docs.length.toDouble / docs.count(_(term) > 0))
}
def tfidf(docs:Seq[Seq[Int]], doc:Int, term:Int):Double = {
tf(docs(doc), term) * idf(docs, term)
}
def cosineSimilarity(f1:Seq[Double], f2:Seq[Double]):Double = {
def srsum(x:Seq[Double]):Double = math.sqrt(x.map(x=>x*x).sum)
f1.zip(f2).map{ case (x, y)=> x * y }.sum.toDouble / (srsum(f1) * srsum(f2))
}
val terms = Seq("banana", "apple", "orange", "cherry", "grape")
val doc1 = Seq("banana", "banana", "apple", "orange")
val doc2 = Seq("banana", "apple", "orange", "cherry", "cherry")
val doc3 = Seq("apple", "grape", "grape")
val query = Seq("banana", "cherry")
val docs = Seq(doc1, doc2, doc3, query).map{ doc =>
terms.map(term => doc.count(_ == term))
}
println(docs.map(_.mkString("[", ", ", "]")).mkString("[", ", ", "]"))
// [[2, 1, 1, 0, 0], [1, 1, 1, 2, 0], [0, 1, 0, 0, 2], [1, 0, 0, 1, 0]]
val features = docs.indices.map{ doc =>
terms.map{ term =>
tfidf(docs, doc, terms.indexOf(term))
}
}
features.foreach{ fs =>
println(fs.map(x=>f"$x%.3f").mkString(", "))
}
// 0.144, 0.072, 0.173, 0.000, 0.000
// 0.058, 0.058, 0.139, 0.277, 0.000
// 0.000, 0.096, 0.000, 0.000, 0.924
// 0.144, 0.000, 0.000, 0.347, 0.000
features.map { f1 =>
features.map(f2 => cosineSimilarity(f1, f2))
}.foreach{ fs =>
println(fs.map(x=>f"$x%.3f").mkString(", "))
}
// 1.000, 0.481, 0.031, 0.233
// 0.481, 1.000, 0.019, 0.868
// 0.031, 0.019, 1.000, 0.000
// 0.233, 0.868, 0.000, 1.000
参照
- Vector Space Model (Wikipedia)