微忘録

好奇心に記憶力がついていかない人のブログ

gensimのmodels.TfidfModel()で、引数にSMART notationが使えるようになっていた話

つい先日こんなツイートをしたところ、爆速で公式からリプライが来ました。

「2018年1月2日のgensim3.3.0のリリースに際して、TfidfModelにSMART notation機能が追加されたから宜しくな!」という話で、つまり「models.TfidfModel()コーパスの重み付け手法の選択が、簡単な記法でできるsmartirs引数を新たに用意したよ」という要旨です。今回はこの機能について宣伝と備忘録。

gensimとTF-IDF法

gensimについて

gensimはLSIやLDAなどのトピック分析を行う際に便利なPythonモジュールです。トピック分析の発端である自然言語処理での活用と主しており、文書ごと単語リスト*1(配列)を用意すれば、gensimモジュールの関数を用いて、辞書とコーパスの作成からトピック推定と評価まで可能です。

TF-IDFについて

そして今回備忘録をするmodels.TfidfModel()は、コーパス作成時に単語にTF-IDF法を用いた重み付けをする関数です。

TFは"Term Frequency"つまり文書内単語頻度、IDFは"Inverse Document Frequency"つまり逆文書頻度、そしてTF-IDFはそれら2種の重み付けの積で、「全文書内での出現頻度は低いが、特定文書にて頻繁に用いられる特定単語に、強い重み付けができる」方法です。

そしてgensim3.3.0以前までのmodels.TfidfModel()ではTF-IDF法のみしか指定できず、「TF法だけ」もしくは「IDF法だけ」は自作する必要がありました。*2 この面倒くささを改善したのが、今回のSMART notation機能の導入です。

SMART notationを試す

SMART notationとは

SMART (System for the Mechanical Analysis and Retrieval of Text) notationとは、「文書検索システムにおける重み付けの簡便表記法」になります。

伝統的なベクトル空間モデルを用いた文書検索では、クエリ内容(文書名)に対して、各文書の単語群(Bag-of-Words)の出現頻度にコサイン類似度を用いた計算により、類似性の高い文書を検索結果として表示します。この時の検索結果を左右するのが、TF-IDF法などによる文書内単語群それぞれ対する重み付けです。*3

そしてこの文書内単語群に行われる重み付けを簡便的に表記法する方法が"SMART notation"であり、以下一覧のような表記になります。*4 f:id:wtnVenga:20180208115703p:plain

ここから"単語""文書""正規化"の順に記号を3つ重ねるだけで、どのような重み付けがされるのかが一目瞭然になります。

gensim3.3.0でサンプルコード

そしてmodels.TfidfModel()に"smartirs"引数が追加されたことでSMART notationが利用可能になりました。以下のように重み付けを指定することができます。

from gensim.corpora import Dictionary
from gensim.models import TfidfModel

documents = ["Human machine interface for lab abc computer applications",
              "A survey of user opinion of computer system response time",
              "The EPS user interface management system",
              "System and human system engineering testing of EPS",
              "Relation of user perceived response time to error measurement",
              "The generation of random binary unordered trees",
              "The intersection graph of paths in trees",
              "Graph minors IV Widths of trees and well quasi ordering",
              "Graph minors A survey"]

# 以下の7語をストップワードとして定義
stop_words = set('for a of the and to in'.split())

# 文を単語に分割し、ストップワードを除去した配列を作成
texts = [[word for word in document.lower().split() if word not in stop_words] for document in documents]

#辞書とコーパスの作成
dictionary = Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

#"n"(tf法)と、"t"(idf法)で、"c"(コサイン正規化)した重み付け
model = TfidfModel(corpus, id2word=dictionary, smartirs="ntc")
vectorized_corpus = list(model[corpus])

最後に

gensim3.3.0は最新版のAnaconda3-5.0.0では未だ対応していないので、別途"pip install"する必要があります。早く追加されて欲しいところ。

*1:厳密化すると「単語(Term)」よりも、字句解析における「トークン(token)」のことです。形態素解析などを用いても必ずしも意味ある単語にすることは難しく、あくまでも文字のカタマリとして「トークン」と呼ばれます。

*2:L2正規化するか否かの"normalise"引数は既存

*3:https://nlp.stanford.edu/IR-book/html/htmledition/queries-as-vectors-1.html#eqn:cosinescore

*4:https://nlp.stanford.edu/IR-book/html/htmledition/document-and-query-weighting-schemes-1.html

MALLETラッパーを用いた、gensimでの"Gibbs sampler"によるトピック推定を試す

LSIやLDAなどのトピック分析をPythonで実行するなら、gensimモジュールの利用が一般的です。しかしgensimでは変分ベイズ法による推定しか基本的に対応しておらず、その他のギブスサンプリングなどを用いるには少し工夫が必要です。

そこで今回はgensimにあるgensim.models.wrappers.LdaMallet()という、java製のオープンソースソフトであるMALLETのラッパー機能を用いて、Pythonかつgensimの環境でギブスサンプリングによるトピック推定をざっくりと流れだけ試します。

崩壊型ギブスサンプリングはldaモジュールで可能です。現行のscikit-learnモジュールのLDAは変分ベイズのみです。ご注意を。

なお以下の環境で準備します。詳細については適宜ググってください。

MALLETの準備

MALLETをダウンロードして、Apach antでディレクトリをビルドするだけ

  1. MALLETのダウンロード(こちらから Downloading MALLET
  2. MALLETのディレクトリにてantコマンドでビルド
  3. "BUILD SUCCESSFUL"が表示されたらビルド成功
  4. ディレクトリのパスを"Users/~.../mallet-2.0.8/bin/mallet"まで記憶

Pythonの実行

文書ごと単語リストの用意

トピック分析したい文書群(documents)リストを用意し、ストップワード除去を行う。
gensimチュートリアルを引用

documents = ["Human machine interface for lab abc computer applications",
              "A survey of user opinion of computer system response time",
              "The EPS user interface management system",
              "System and human system engineering testing of EPS",
              "Relation of user perceived response time to error measurement",
              "The generation of random binary unordered trees",
              "The intersection graph of paths in trees",
              "Graph minors IV Widths of trees and well quasi ordering",
              "Graph minors A survey"]

# 以下の7語をストップワードとして定義
stop_words = set('for a of the and to in'.split())

# 文を単語に分割し、ストップワードを除去した配列を作成
texts = [[word for word in document.lower().split() if word not in stop_words] for document in documents]

以下のような中身の単語リスト(texts)に格納されました。

    [['human', 'machine', 'interface', 'lab', 'abc', 'computer', 'applications'], ['survey', 'user', 'opinion', 'computer', 'system', 'response', 'time']]

辞書とコーパスの作成

gensimモジュールを呼び出し、文書ごと単語リストを元に辞書とコーパスを作成します。

from gensim import corpora, models
import gensim

#辞書とコーパスを作成
dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

#コーパスをMALLET用の形式に変換します。
corpora.malletcorpus.MalletCorpus.serialize("./corpus.mallet", corpus)
mallet_corpus = corpora.malletcorpus.MalletCorpus("./corpus.mallet")

ギブスサンプリングによるトピックモデル推定

ビルドされたMALLETのパス(mallet_path)を指定し、用意した辞書とコーパスで3つのトピックを推定。

mallet_path = "Users/~.../mallet-2.0.8/bin/mallet"
lda = models.wrappers.LdaMallet(mallet_path, mallet_corpus, num_topics=3, id2word=dictionary)

推定されたトピックを確認。各トピック内容の解釈は割愛。

lda.print_topics()
 [(0,
  '0.182*"random" + 0.091*"user" + 0.091*"machine" + 0.091*"binary" + 0.091*"engineering" + 0.091*"paths" + 0.091*"human" + 0.091*"opinion" + 0.091*"minors" + 0.091*"lab"'),
 (1,
  '0.136*"perceived" + 0.136*"interface" + 0.136*"measurement" + 0.045*"survey" + 0.045*"graph" + 0.045*"eps" + 0.045*"time" + 0.045*"trees" + 0.045*"ordering" + 0.045*"testing"'),
 (2,
  '0.176*"quasi" + 0.118*"iv" + 0.118*"computer" + 0.059*"system" + 0.059*"relation" + 0.059*"generation" + 0.059*"management" + 0.059*"time" + 0.059*"response" + 0.059*"human"')]

最後に

今のところ「やってみた」系の記事では変分ベイズでの実装しか見当たらないので備忘録してみました。

gensimには他にも、Biei先生のフォーマットや、GibbsLDA++のフォーマットなどがあります(gensim: Corpora and Vector Spaces)。 そのため「gensimでデータ整形だけして他で実行するぜ!」な強者の方々は、その他の選択肢も是非ご検討ください。

※記事内容の改善点にお気付きの方は、コメントにてご一報いただけますと幸甚です。

2015年度版NLP100本ノック第2章をPython3で解く

前回(2015年度版NLP100本ノック第1章をPython3で解く - 微忘録)の続き。 bashとPython3で。各コードの実行結果は、こちらのリポジトリにて公開しています。

第2章: UNIXコマンドの基礎

hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.

10. 行数のカウント

行数をカウントせよ.確認にはwcコマンドを用いよ.

%%bash
wc -l ./datasets/hightemp.txt
with open("./datasets/hightemp.txt","r") as hightemp:
    print(len(hightemp.readlines()))

11. タブをスペースに置換

タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.

%%bash
sed -e "s/\t/| /g" ./datasets/hightemp.txt
import re
with open("./datasets/hightemp.txt","r") as hightemp:
    lines = hightemp.readlines()
    print(lines,'\n')
    
    print([re.sub(r'\t'," ",hightemp)for hightemp in lines])

12. 1列目をcol1.txtに,2列目をcol2.txtに保存

各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.

%%bash
cut -f 1 ./datasets/hightemp.txt > ./datasets/col1.txt
cut -f 2 ./datasets/hightemp.txt > ./datasets/col2.txt
with open("./datasets/hightemp.txt") as hightemp:
    lines = hightemp.readlines()
    
    #col1.txt
    col1 = []
    for line in lines:
        col1.append(line.split()[0] + "\n") #"\n"で単語ごとに改行
        
    with open("./datasets/col1py.txt" , "w") as split:
            split.writelines(col1)
    
    #col2.txt
    col2 = []
    for line in lines:
        col2.append(line.split()[1] + "\n")
        
    with open("./datasets/col2py.txt" , "w") as split:
            split.writelines(col2)
            
    hightemp.close()

13. col1.txtとcol2.txtをマージ

12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ

%%bash
paste -d"\t" ./datasets/col1.txt ./datasets/col2.txt
with open("./datasets/col1.txt") as col1_file, open("./datasets/col2.txt") as col2_file:
    col1, col2 = col1_file.readlines(), col2_file.readlines()

with open("./datasets/merged.txt", "w") as writer:
    for a, b in zip(col1, col2):
        writer.write("\t".join([a.rstrip(),b]))
        
for i in open("./datasets/merged.txt"):
    print(i)

14. 先頭からN行を出力

自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.

%%bash
head -n 5 ./datasets/hightemp.txt
def head(n,file):
    with open(file,'r') as hightemp:
        lines = hightemp.readlines()
        for lineNumber in range(n):
            print(lines[lineNumber])
            
head(5, "./datasets/hightemp.txt")

15. 末尾のN行を出力

自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ

%%bash
tail -n 5 ./datasets/hightemp.txt
def tail(n,file):
    with open(file,'r') as hightemp:
        lines = hightemp.readlines()
        stopline = len(lines) - n
        for lineNumber in range(stopline,len(lines),1):
            print(lines[lineNumber])
            
tail(5, "./datasets/hightemp.txt")

16. ファイルをN分割する

自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.

%%bash
split -l 5 ./datasets/hightemp.txt ./datasets/n-split_
def splitfunc(file,n):
    with open(file) as filed:
        lines = filed.readlines()
    
    for i in range(n):
        with open("./datasets/splitblock%s.txt" % str(i), "w") as split:
            split.writelines(lines[n*i : n*(i +1)])
            
splitfunc("./datasets/hightemp.txt",3)

with open("./datasets/splitblock2.txt","r") as check:
    print(check.readlines())

17. 1列目の文字列の異なり

1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.

%%bash
cut -f 1 ./datasets/hightemp.txt |sort | uniq
with open("./datasets/hightemp.txt","r") as hightemp:
    lines = hightemp.readlines()
    
    col1 = []
    for line in lines:
        col1.append(line.split()[0] + "\n") #"\n"で単語ごとに改行

    li = set(col1)
    print(sorted(li))

18. 各行を3コラム目の数値の降順にソート

各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).

%%bash
sort -nk3r ./datasets/hightemp.txt
with open("./datasets/hightemp.txt", 'r') as hightemp:
    lines = hightemp.readlines()
    for line in sorted(lines,key=lambda x: x.split()[2], reverse=False):
        print(line)

19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる

各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.

%%bash
cut -f 1 ./datasets/hightemp.txt | sort |uniq -c | sort -r
from collections import Counter

with open("./datasets/hightemp.txt","r") as hightemp:
    lines = hightemp.readlines()
    
    li = []
    for line in lines:
        li.append(line.split()[0])
    
    counted = Counter(li)
    print(counted)

2015年度版NLP100本ノック第1章をPython3で解く

Djangoアプリへの導入などを考えて、Pythonでやり直したいと思い挑戦したので備忘録。
各コードの実行結果はこちらのリポジトリにて公開しています。

第1章: 準備運動

00. 文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ

word = "stressed"
print(word[::-1])

01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

#単純に2飛ばしで
word = "パタトクカシーー"
print(word[::2])

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

#分割する文字列と、格納するリストを作成
word = "パタトクカシーー"
word_li =[]

#特定の順番の文字を分割し、リストに要素として格納する
for i in range(1,len(word),2) :
    word_li.append(word[i])

#リスト内文字をスペース無しで連結する
word_li = "".join(word_li)    
print(word_li)

03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

#すでに分かち書きされているので、半角スペースごとに分解してリストに格納。
sentence = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
sentence_li =[]
sentence_li = sentence.split(" ")
#リストを各単語の(アルファベットの)文字数を先頭から出現順に並べる
sorted(sentence_li,key=len)

04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

#単語に分解
sentence = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
sentence_li = sentence.split(" ")

sentence_dict = {}
topword = [1, 5, 6, 7, 8, 9, 15, 16, 19]

for i in sentence_li:
    if sentence_li.index(i) + 1 in topword:
        sentence_dict[i[:1]] = sentence_li.index(i) + 1
    else:
        sentence_dict[i[:2]] = sentence_li.index(i) + 1
        
print(sentence_dict)

05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

#n-gram関数作り
def ngram(input, n):
    result = []
    for i in range(0, len(input) - n + 1):
        result.append(input[i:i + n])

    return result

#文章用意
sentence = "I am an NLPer"

#単語n-gram(分かち書き)
sentence_li = sentence.split(" ")
ngram(sentence_li, 2)

#文字n-gram(文字区切り)
ngram(sentence, 2)

06. 集合

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

sentence1 = "paraparaparadise"
sentence2 = "paragraph"

#bi-gram結果をset型で重複無し
X = set(ngram(sentence1,2))
Y = set(ngram(sentence2,2))

#和集合
X | Y

#積集合
X & Y

#差集合
X - Y

#seを含むか確認
'se' in X
'se' in Y

07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

def xyz_def(x,y,z) :
    print(str(x) + "時の" + str(y) + "は" + str(z))
    
xyz_def(x=12, y="気温", z=22.4)
def xyz_def(x, y, z):
    print( '{hour}時の{subject}は{value}'.format(hour=x, subject=y, value=z))

xyz_def(x=12, y="気温", z=22.4)

08. 暗号文

与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

英小文字ならば(219 - 文字コード)の文字に置換 その他の文字はそのまま出力 この関数を用い,英語のメッセージを暗号化・復号化せよ.

def cipher(input) :
    result = ''
    for i in input :
        if i.islower() == True:
            result += chr(219 - ord(i))
        else:
            result += i  
    return result

cipher("oh,myパスタ")

09. Typoglycemia

スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

import random

def sliceRandom(input) :
    text = input.split(" ")
    result = []
    for i in text:
        if len(i) <= 4:
            result.append(i)
        else:
            ramdomed = list(i[1:-1])
            random.shuffle(ramdomed)
            result.append(i[0] + "".join(ramdomed) + i[-1])
    return "".join(result)

sliceRandom("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")

分類問題の評価指標をおさらいする

流行に敏感なのでインフルエンザB型に罹患しました。鼻奥の痛いあの検査の結果を待つ間に、「これ分類問題のやつだ!」と思い出し、評価指標をおさらいしたので備忘録。

分類問題の評価

分類問題を評価するとき、「真実の分類」と「推定の分類」の差異について考えます。評価の際には、結果をクロス集計表に落とし込んで簡便化することが一般的です。

具体例として、「インフルエンザ検査の診断性能を評価するために、検査結果の分類が真実の分類を正しく捉えているか調べる」という場合について考えます。 f:id:wtnVenga:20180125004936p:plain

全ての患者は以下の(1)~(4)の診断結果が与えられます。

  • (1)真陽性…インフルエンザ罹患者に、陽性と正しく診断
  • (2)偽陰性…インフルエンザ罹患者に、陰性と間違えて診断
  • (3)偽陽性…インフルエンザ以外の罹患者に、陽性と間違えて診断
  • (4)真陰性…インフルエンザ以外の罹患者に、陰性と正しく診断

診断結果(1)・(4)が高い検査は有用である一方で、(2)・(3)が高い検査は無用であると判断できます。

評価指標

評価指標は「正しさ」と「間違い」の2種が用意され、判断したい内容に合わせて評価指標を用いることが大事です。

「正しさ」を評価する指標

  • 適合率(Precision)…陽性と診断した中で、インフルエンザ罹患者の割合

     \dfrac{(1)}{(1)+(3)}

  • 再現率(Recall)…インフルエンザ罹患者に、陽性と診断した割合

     \dfrac{(1)}{(1)+(2)}

  • F値(F-meansure)…(再現率と適合率の調和平均)

     \dfrac{2(1)} {2(1)+(2)+(3)} = \dfrac{2}{\frac{1}{Recall} + \frac{1}{Precision}}

  • 特異度(Specificity)…インフルエンザ以外の罹患者に、陰性と診断した割合

    \dfrac{(4)}{(3)+(4)}

  • 正確性(Accuracy)…全診断結果の中で、インフルエンザ罹患者に陽性、インフルエンザ以外の罹患者に陰性と診断した割合

    \dfrac{(1)+(4)}{(1)+(2)+(3)+(4)}

「間違い」を評価する指標

  • 偽陽性率…インフルエンザ以外の羅漢者に、陽性と診断した割合

     \dfrac{(3)}{(3)+(4)}

  • 偽陰性率…インフルエンザ罹患者に対して、陰性と診断した割合

     \dfrac{(2)}{(1)+(2)}

適合率と再現率の関係

適合率と再現率のトレードオフ関係の対策として、情報検索システムの精度評価に使われるF値が、評価指標として用いられることも一般的です。

陽性診断を多く出すと分子増加により再現率が上昇しますが、分母増加により適合率の割合が低下します。 また陰性診断を多く出すと分母減少により適合率が上昇しますが、分子減少により再現率の割合が低下します。

そこで適合率と再現率の2つの指標それぞれの割合を考慮したF値が活用できます。F値は以下のような式で、2つの指標の調和平均(逆数の算術平均の逆数)を取ることで算出されます。

 \dfrac{2(1)} {2(1)+(2)+(3)} = \dfrac{1}{\frac{1}{2} (\frac{1}{Recall} + \frac{1}{Precision})} = \dfrac{2}{\frac{1}{Recall} + \frac{1}{Precision}}

別の具体例として「同一距離を平均時速5kmと10kmで2回走った時の平均時速は時速6.7kmほどである」というような以下の計算からも、よくある算術平均(=相加平均)との差と有用性についても確認できると思います。

 \dfrac{2}{\frac{1}{5}+ \frac{1}{10}} = \dfrac{1}{\frac{1}{2} (\frac{1}{5} + \frac{1}{10})} = \dfrac{20}{3} \fallingdotseq 6.666....(km/h)

最後に

さっくりとですが分類問題における評価指標について備忘録を認めました。クロス集計表といえば「カイ二乗値の独立性の検定」ですが、まだ全快でないので本記事では割愛します。

おさらいする機会をくれた点では、インフルエンザも悪くないかなって感じです。 今年のインフルエンザ、発熱は低いものの抜群の感染力なので、皆様お身体をお大事になさいませ。

参考元

検査の評価指標(再現率、適合率、特異度、正確度、F値) - 具体例で学ぶ数学

協調フィルタリングで将来レビュー値を予測する

いまコレ↓読んでます。卒論の参考図書です。

情報推薦システム入門 -理論と実践-

情報推薦システム入門 -理論と実践-

協調フィルタリングの端緒であるピアソンの積率相関を用いた推測を、実際にpythonで動かしてみたので備忘録。

ピアソンの積立相関の式

\begin{align} r_{xy} = \frac{\displaystyle \sum_{i = 1}^n (x_i - \overline{x}) (y_i - \overline{y})}{\sqrt{\displaystyle \sum_{i = 1}^n (x_i - \overline{x})^2}\sqrt{\displaystyle \sum_{i = 1}^n (y_i - \overline{y})^2}} \end{align}

正規分布を仮定した変数XとYに対して用いられる相関係数ことピアソンの積立相関
分子が「変数Xと変数Yの共分散」であり、分母が「変数Xと変数Yのそれぞれの標準偏差の積」で計算できる。

よって以下の様に「ユーザーXとユーザーYのレビュー値の共分散」と「ユーザーXとユーザーYのそれぞれのレビュー値の標準偏差の積」で計算できる。

pandasのDataframeを用意

お題は、 『ユーザーXのアイテム5のレビューを、ピアソンの積立相関から推測する。』

import pandas as pd

review_df = pd.DataFrame([[4,2,3,1,],
                          [1,4,3,2,5],
                          [4,2,4,5,1],
                          [5,3,3,2,4],
                          [3,1,2,4,3],
                          [2,2,3,1,3],
                          [3,1,1,1,1],])

review_df.columns = ["item_1","item_2","item_3","item_4","item_5"]
review_df.index   = ["user_X","user_1","user_2","user_3","user_4","user_5","user_6"]
review_df
item_1 item_2 item_3 item_4 item_5
user_X 4 2 3 1 NaN
user_1 1 4 3 2 5.0
user_2 4 2 4 5 1.0
user_3 5 3 3 2 4.0
user_4 3 1 2 4 3.0
user_5 2 2 3 1 3.0
user_6 3 1 1 1 1.0

ピアソンの積立相関とユーザー毎平均レビュー値

#アイテム1~4まで各ユーザーの平均評価値を計算
review_mean = review_df.iloc[:,0:4].T.mean(skipna=True)
review_mean
user_X    2.50
user_1    2.50
user_2    3.75
user_3    3.25
user_4    2.50
user_5    2.00
user_6    1.50
dtype: float64
#ユーザー間の相関係数を見ると、user_Xはuser_3と強い正の相関であることがわかる。
review_corr = review_df.iloc[:,0:4].T.corr(method='pearson')
review_corr
user_X user_1 user_2 user_3 user_4 user_5 user_6
user_X 1.000000 -0.400000 -0.102598 0.923381 -0.200000 0.632456 0.774597
user_1 -0.400000 1.000000 -0.718185 -0.512989 -0.800000 0.316228 -0.774597
user_2 -0.102598 -0.718185 1.000000 -0.157895 0.923381 -0.324443 0.132453
user_3 0.923381 -0.512989 -0.157895 1.000000 -0.102598 0.324443 0.927173
user_4 -0.200000 -0.800000 0.923381 -0.102598 1.000000 -0.632456 0.258199
user_5 0.632456 0.316228 -0.324443 0.324443 -0.632456 1.000000 0.000000
user_6 0.774597 -0.774597 0.132453 0.927173 0.258199 0.000000 1.000000

let's 予測

#user_Xのitem_5の評価値を、近傍のuser_3とuser_6の相関係数を元に算出する
import numpy as np

rev3_5 = review_df.loc[['user_3'],['item_5']].values[0]
rev6_5 = review_df.loc[['user_6'],['item_5']].values[0]

avgX = review_mean['user_X']
avg3 = review_mean['user_3']
avg6 = review_mean['user_6']

corrX_3 = review_corr.loc[['user_X'],['user_3']].values[0]
corrX_6 = review_corr.loc[['user_X'],['user_6']].values[0]

answer = avgX + 1/((corrX_3 + corrX_6) *(corrX_3 * (rev3_5 - avg3) + corrX_6 * (rev6_5 - avg6)))
#推定したレビュー値が以下。
answer
array([ 4.42943831])

4か5のレビューを付けそうなので、アイテムをオススメして良さそうだと判断できました。

集合からベイズの定理の公式までを復習

雰囲気でベイズ推定を扱うことから脱却したい。まずはベイズの定理の公式理解まで。
しかし数式だけで納得できる頭脳ではないので、具体例を添えて復習し直したので備忘録。

※本記事の情報正確性については担保できないため。体系的な書籍を購入することをお勧め致します。

集合記号

確率を扱うのに集合記号は必須。以下の基本的なもの一覧

  •  a∈A:要素aは集合Aに属する

  •  b∉A:要素bは集合Aに属さない

  •  A⊆B:集合Aは集合Bの部分集合

  •  A∪B:集合Aと集合Bの和集合(どちらか一方には属する要素の全体の集合)

  •  A∩B:集合Aと集合Bの積集合(両方に属する要素の要素全体の集合)

  •  A×B:集合Aの要素aと集合Bの要素bからなる直積集合。(  {(a,b)∣a∈A,b∈B}

事象確率

事象Xが起きる時の確率Pを、 P(X) と表す。

サイコロAを振る時に、iの面(A_i)がでる事象の確率Pは、  P(A_i)

同時確率

複数の事象X,Yが同時に起きる時の確率Pのことを P(X, Y) と表す

サイコロAとBを振る時に、Aでiの面(A_i)、Bでjの面(B_j)がでる事象の確率Pは、  P(A_i , B_j)

互いに独立(事象がその他の事象に影響しない)な場合は、独立事象として以下が成り立つ。

 P(X,Y) = P(X)P(Y)

条件付き確率

事象Xが起きる場合を条件とし、その他の事象Yが起きる確率Pのことを、  P(Y|X)または P_X (Y)と表す。

サイコロAとBに、A_iの出目とB_jの出目の偶/奇数が同じになるイカサマが仕込まれているとする。 出目A_jが起きたときを条件として、出目B_jが出る条件付き確率Pは、  \displaystyle P(B_j | A_i) = \frac{P(A_i \cap B_j)}{P(A_i)}

なお「事象Xが起きない場合を条件とした、事象Yが起きる確率P」は、  P(Y| \overline{X} )または P_\overline{X} (Y)と表す。

補足:加法定理(確率の和の法則)

事象Xまたは事象Yが起こる確率Pは、集合Xと集合Yの和集合から積集合を除いた集合部分で表される。

 P(X∪Y)=P(X)+P(Y)-P(X∩Y)

またXとYが排反事象(一方の事象が起きれば他方が決して起きない)の場合は、事象Xが起こる確率と事象Yが起こる確率の和で表される。

 P(X∪Y)=P(X)+P(Y)

補足:乗法定理(確率の積の法則)

事象Xが起こり続いて事象Yが起こる確率P(X∩Y)は、Xが起こる確率P(X)と、Xが起こったという条件のもとでYが起こる確率P(Y|X)の積で表される。

 P(X∩Y) = P(X)P(Y|X)

これは条件付き確率を式変形することで導出することもできる。

 \displaystyle P(Y | X) = \frac{P(X \cap Y)}{P(X)} \
 P(X)P(Y|X) = P(X \cap Y)

またXとYが互いに独立した事象である場合は、Xが起こる確率P(X)とYが起こる確率P(Y)の積で表される。

 P(X∩Y) = P(X)P(Y)

ベイズの定理

条件付き確率に乗法定理による式変形を行った以下がベイズの定理として表される。

 \displaystyle P(Y | X) = \frac{P(X \cap Y)}{P(X)} = \frac{P(X | Y) P(Y)}{P(X)}\

またこれは条件付き確率の式変形により、以下のように求めることができる。

条件Yにおける事象Xの確率
 \displaystyle P(Y | X) = \frac{P(X \cap Y)}{P(X)}
 P(X \cap Y) = P(Y | X)P(X)

条件Xにおける事象Yの確率
 \displaystyle P(X | Y) = \frac{P(X \cap Y)}{P(Y)}
 P(X \cap Y) = P(X | Y)P(Y)

両式の左辺が一致するので
 P(Y | X)P(X) = P(X | Y)P(Y)

これを式変形すると以下2種が得られる
 \displaystyle P(Y | X) =  \frac{P(X | Y) P(Y)}{P(X)}\
 \displaystyle P(X | Y) =  \frac{P(Y | X) P(X)}{P(Y)}\

また乗法定理の対称性からベイズ定理の式を導くこともできる。

 P(X \cap Y) = P(X)P(Y|X)
 P(X \cap Y) = P(X | Y)P(Y)
この2式を結合して、条件付き確率の式変形と同様にベイズの定理を導く

最後に

「条件付き確率」と「確率の乗法定理」を用いたのがベイズの定理であると解釈しました。
次はベイズ理論の具体例、ベイズ推定、頻度論との差異について備忘録を残します。