コンテンツベースフィルタリングあるいは文書推薦を実装してみる

前回の[pythonでアイテムベース協調フィルタリングを実装する - MovieLensを例に](http://qiita.com/kotaroito/items/6acb58bb16b68a460af9) 続きとして、コンテンツベースフィルタリングについて考えてみます。


## コンテンツベースフィルタリングについて

ユーザの嗜好とアイテムの特徴をマッチさせて、アイテムを推薦するシステムの総称です。

コンテンツの特徴量を捉えれば、コンテンツベースフィルタリングと言えるので、決まったアルゴリズムがあるわけではないです。（と自分は理解してます）


## コンテンツベースフィルタリングの手法

#### 例１：アイテムのメタデータの活用

例えば映画であれば、アイテムの特徴量を {俳優, ジャンル, 監督, 国} とし、ユーザの特徴量はお気に入りアイテムの平均値で表現することが考えられます。


#### 例２：文書に出現した単語の活用

ニュースサイトの記事を推薦する際には、文書に出現した単語を利用する手法が考えられます。
例えば、アイテムの特徴量を単語のTF・IDF値で表現し、ユーザの特徴量はお気に入りアイテム（=記事）の平均値で表現します。


## 文書をコンテンツベースで推薦する

今回は例2を想定して、文書をコンテンツベースで推薦する手法を実装してみたいと思います。

### データソース
scikit-learn の dataset である[The 20 newsgroups text dataset](http://scikit-learn.org/stable/datasets/twenty_newsgroups.html#newsgroups) を用います。

"comp.graphics"や"rec.sport.baseball"など、20カテゴリのnewsgroupのデータが入っています。

```py3
>>> from sklearn.datasets import fetch_20newsgroups
>>> newsgroups_train = fetch_20newsgroups(subset='train')
>>> newsgroups_train.data[0]
"From: lerxst@wam.umd.edu (where's my thing)\nSubject: WHAT car is this!?\nNntp-Posting-Host: rac3.wam.umd.edu\nOrganization: University of Maryland, College Park\nLines: 15\n\n I was wondering if anyone out there could enlighten me on this car I saw\nthe other day. It was a 2-door sports car, looked to be from the late 60s/\nearly 70s. It was called a Bricklin. The doors were really small. In addition,\nthe front bumper was separate from the rest of the body. This is \nall I know. If anyone can tellme a model name, engine specs, years\nof production, where this car is made, history, or whatever info you\nhave on this funky looking car, please e-mail.\n\nThanks,\n- IL\n   ---- brought to you by your neighborhood Lerxst ----\n\n\n\n\n"
```

### 構築する文書推薦システム

単語の出現頻度（bag of words）を入力とし、類似度の高い文書（今回で言うと、newsgroup への投稿）を返すシステムを構築します。


#### 1. corpus を構築する
```py3
from gensim import corpora, models, similarities
from sklearn.datasets import fetch_20newsgroups
from collections import defaultdict
import nltk
import re

def create_dictionary_and_corpus(documents):
    texts = [tokens(document) for document in documents]

    frequency = defaultdict(int)
    for text in texts:
        for token in text:
            frequency[token] += 1

    # 全ドキュメントを横断して出現回数が1のtokenを捨てる 
    texts = [[token for token in text if frequency[token] > 1] for text in texts]

    dictionary = corpora.Dictionary(texts)
    corpus = [dictionary.doc2bow(text) for text in texts]
    
    return dictionary, corpus

def tokens(document):
    symbols = ["'", '"', '`', '.', ',', '-', '!', '?', ':', ';', '(', ')', '*', '--', '\\']
    stopwords = nltk.corpus.stopwords.words('english')

    # ストップワードと記号を捨てる
    tokens = [re.sub(r'[,\.]$', '', word) for word in document.lower().split() if word not in stopwords + symbols]
    
    return tokens

# 20 newsgroups のデータをダウンロード
newsgroups_train = fetch_20newsgroups(subset='train',remove=('headers', 'footers', 'quotes'))

# 最初の100件を使って、辞書とcorpusを作成する
dictionary, corpus = create_dictionary_and_corpus(newsgroups_train.data[0:100])
```

dictonary と corpus の扱いは [gensim のチュートリアル](http://radimrehurek.com/gensim/tut1.html) を見るとよいです。

## 2. モデルを構築する

```py3
def create_model_and_index(corpus):
    tfidf = models.TfidfModel(corpus)
    index = similarities.MatrixSimilarity(tfidf[corpus])
    return tfidf, index

model, index = create_model_and_index(corpus)
```

corpus から Tfidf モデルを構築します。詳しくは、[Topics and Transformations](http://radimrehurek.com/gensim/tut2.html) に譲ります。

## 3. 文書を推薦する

入力にはトレーニングデータの1番目(index: 0）を使ってみます。 推薦システムが正しく構築できていれば、類似している文書として自身が推薦されるはずです。

```py3
bow = dictionary.doc2bow(tokens(newsgroups_train.data[0]))
vec_tfidf = model[bow]
sims = index[vec_tfidf] 

sims = sorted(enumerate(sims), key=lambda item: item[1], reverse=True)

for i in range(3):
    doc_id     = sims[i][0]
    simirarity = round(sims[i][1] * 100, 0)
    print(doc_id, simirarity)
```

期待通りに doc_id: 0 が類似度 100% で推薦されました。

```py3
0 100.0
17 12.0
84 11.0
```

なお

```py3
sims = index[vec_tfidf] 

```
の部分で simirarity を計算していますが、詳しくはチュートリアル[Similarity Queries](https://radimrehurek.com/gensim/tut3.html)を見るとよいと思います。


## まとめ

今回は入力データとして、トレーニングデータ用いましたが、 ユーザのお気に入り文書を bag of words で表現して入力すれば、パーソナライズされた文書推薦システムになると思います。（実用上はきっといくつか工夫が必要だろうけれども）

結合したコードも貼っておきます。

```py3

from gensim import corpora, models, similarities
from sklearn.datasets import fetch_20newsgroups
from collections import defaultdict
import nltk
import re

def create_dictionary_and_corpus(documents):
    texts = [tokens(document) for document in documents]

    frequency = defaultdict(int)
    for text in texts:
        for token in text:
            frequency[token] += 1

    texts = [[token for token in text if frequency[token] > 1] for text in texts]

    dictionary = corpora.Dictionary(texts)
    corpus = [dictionary.doc2bow(text) for text in texts]
    
    return dictionary, corpus

def tokens(document):
    symbols = ["'", '"', '`', '.', ',', '-', '!', '?', ':', ';', '(', ')', '*', '--', '\\']
    stopwords = nltk.corpus.stopwords.words('english')
    tokens = [re.sub(r'[,\.]$', '', word) for word in document.lower().split() if word not in stopwords + symbols]
    
    return tokens

def create_model_and_index(corpus):
    tfidf = models.TfidfModel(corpus)
    index = similarities.MatrixSimilarity(tfidf[corpus])
    return tfidf, index

# Use 100 samples to build dictionary and corpus
newsgroups_train = fetch_20newsgroups(subset='train',remove=('headers', 'footers', 'quotes'))
dictionary, corpus = create_dictionary_and_corpus(newsgroups_train.data[0:100])

# Create TfIdf Model and its index
model, index = create_model_and_index(corpus)

# System Evaluation
bow = dictionary.doc2bow(tokens(newsgroups_train.data[0]))
vec_tfidf = model[bow]
sims = index[vec_tfidf] 

sims = sorted(enumerate(sims), key=lambda item: item[1], reverse=True)

for i in range(3):
    doc_id     = sims[i][0]
    simirarity = round(sims[i][1] * 100, 0)
    print(doc_id, simirarity)
```