Blog に レコメンド表示機能を組み込んでみたくなり、
実装した結果を記載します。
Mezzanine の Plugin があればそれを使うのですが、そんなことはなくて、 自前実装してみました。
前提
以下の環境で実行しています。
-
OS
CentOS release 6.9 (Final) -
Python Version
Python 2.7.8 -
Package (必要そうなものだけ抜粋)
Django (1.10.6)
Mezzanine (4.2.3)
ブログ記事のレコメンドについて
レコメンドについて少し調べた限りだと、
協調フィルタリング
を使うレコメンドと、コンテンツベース
でのレコメンド、
それらの組み合わせがでのレコメンドがあるようです。
協調フィルタリング
については正直ブログを見ている人達は、認証なしユーザーになるので、
どうしたらいいのかよくわからずのため、1、
コンテンツベース
でのレコメンドを実装する方向で実装方法を調べていきました。
レコメンド
の方式についての説明は、以下の記事がわかりやすかったです。
[1].ユーザーにログインしてもらうわけではないので、Google Alaytics の情報とかを使って、
実装していくですかね?
python で、コンテンツベースレコメンドを実装する方法
コンテンツベースの実装を調べてると、だいだい以下の流れで実装できることが、
わかりました。
-
文書を、
scikit-learn
のTfidfVectorizer
を使って、ベクトル化し、文書の特徴を抽出する。
TfidfVectorizer
を使う際は、文章から単語を抽出するanalyzer
関数を指定する必要があり、
analyzer
関数は、Mecab
を使って実装する。Mecab
の辞書は、mecab-ipadic
以外に、mecab-ipadic-NEologd
があり、それも使用すると、
新語も認識できるようになる。 -
1.
でベクトル化した結果の Cos類似度 を求める。 Cos類似度が1に近い文書ほど、類似度が高い。
Cos類似度 もscikit-learn
のcosine_similarity
関数を使うと求めることができる。
以下、記事が参考になりました。
自ブログにおける独自の関心事
自ブログはCMS は、Mezzanine
で、mezzanine-pagedown
というマークダウンプラグインを使用して、
記事を作成しています。
そのような背景?から、以下2点を実装する必要があります。
-
Markdown から、テキスト部を抽出する
当ブログは、mezzanine-pagedown
というmarkdown pluginを使っているのですが、
これは、blogの本文として、markdown そのものを、本文としてDBに設定し、
画面表示時に、filter でHTMLに変換しています。
このDBに設定されたmarkdownから本文を抽出する必要があります。 -
関連記事の登録をする
Mezzanine には、デフォルトで、関連投稿
の手動登録機能があり、
この機能で登録した投稿は、blog_blogpost_related_posts
というテーブルに登録されます。
Cos類似度
の高い関連記事は、このテーブルに登録します。2
このテーブルからデータを取得して、表示するテンプレートは既に用意されているので、
データさえ入ってしまえば、画面表示部を実装するのは不要です。
[2].特に今までこの機能使ってなかったので、DELETE & INSERT で登録するように実装してしまいました。
レコメンド実装に前提で必要となる ライブラリのインストール
以下、作成したDjango コマンドを動かすのに必要となります。
コマンドを動作させる場合は、実行してください。
-
mecab
と、mecab-ipadic-NEologd
のインストール
mecab-ipadic-NEologd
の CentOS rpm ファイルがありますので、
それを使いました。2015年モノが落ちてきましたので、Github の README に記載がある通り、
気まぐれ更新なのだと思われます。
mecab-ipadic-neologd/README.ja.md at master · neologd/mecab-ipadic-neologd
sudo rpm -ivh http://packages.groonga.org/centos/groonga-release-1.1.0-1.noarch.rpm sudo yum install mecab mecab-devel mecab-ipadic curl -L https://goo.gl/int4Th | sh
-
以下、ライブラリのインストールが必要です。
# mecab-python pip install mecab-python # scikit-learn pip install scikit-learn # numpy pip install numpy # scipy pip install scipy
作成したDjango コマンド
以下、作成した Django コマンドになります。
記事が200 くらいで、1分30秒程度で動作しました。
recommend_blog_post.py
# -*- coding: utf-8 -*-
from __future__ import print_function
from django.core.management.base import BaseCommand
from mezzanine.blog.models import BlogPost
from BeautifulSoup import BeautifulSoup
from markdown import markdown
import MeCab
import HTMLParser
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction import text
import numpy as np
from django.db import transaction
from logging import getLogger
logger = getLogger(__name__)
additional_stop_words = frozenset([
'xxxx',
'xxxxx',
'xxxxxx',
'xxxxxxxx',
'xxxxxxxxx',
'xxxxxxxxxx',
'xxxxxxxxxxxx',
'xxxxxxxxxxxxxx',
'xxxxxxxxxxxxxxx',
'xxxxxxxxxxxxxxxxxxxxxxxx',
'xxxxxxxxxxxxxxxxxxxxxxxxx',
])
stop_words = text.ENGLISH_STOP_WORDS.union(additional_stop_words)
class Command(BaseCommand):
help = "My recommend blog post command."
def handle(self, *args, **options):
logger.info(__name__ + " START")
documents = []
blogs = []
# 1. BlogPostから、本文を取得、markdownから本文を抽出
for blog_post in BlogPost.objects.published():
source = blog_post.content
html = markdown(source)
text = ''.join(BeautifulSoup(html).findAll(text=True))
htmlParser = HTMLParser.HTMLParser()
unescaped_text = htmlParser.unescape(text)
documents.append(str(unescaped_text.encode('utf-8')))
blogs.append(blog_post)
# 2. TfidfVectorizer を使って、ベクトル化、Cos類似度を求める
cs_array = cosine_similarity(self.__vecs_array(documents),
self.__vecs_array(documents))
# 3. 関連投稿の全削除、Cos類似度の高い上位5件を登録
with transaction.atomic():
# 3-1. 関連投稿の全削除
for blog in blogs:
blog.related_posts.clear()
# 3-2. Cos類似度の高い上位5件を登録
for i, cs_item in enumerate(cs_array):
blog = blogs[i]
cs_dic = {}
for j, cs in enumerate(cs_item):
if round(cs - 1.0, 5) != 0:
cs_dic[blogs[j]] = cs
index = 0
for k, v in sorted(cs_dic.items(), key=lambda x: x[1],
reverse=True):
blog.related_posts.add(k)
index += 1
if index == 5:
break
logger.info(__name__ + " END")
def __vecs_array(self, documents):
docs = np.array(documents)
vectorizer = TfidfVectorizer(
analyzer=self.__get_words,
stop_words="|",
min_df=1,
token_pattern='(?u)\\b\\w+\\b')
vecs = vectorizer.fit_transform(docs)
return vecs.toarray()
def __get_words(self, text):
out_words = []
# mecab-ipadic-neologd をカスタム辞書として指定
tagger = MeCab.Tagger("-Ochasen -d /usr/lib64/mecab/dic/mecab-ipadic-neologd")
tagger.parse('')
node = tagger.parseToNode(text)
while node:
word_type = node.feature.split(",")[0]
if word_type in ["名詞"]:
word = node.surface.decode('utf-8')
# stopwordを弾く
if word not in stop_words:
out_words.append(word)
node = node.next
return out_words
説明
-
Markdownから、Textを抽出する方法
直接抽出する関数はなさそうで、一度HTMLに変換後に、BeautifulSoupでHTMLからテキストを抽出するようにしました。
python3
での実装方法は、以下にまとめましたので、必要であれば参照してください。
python で MarkDownファイルをPlain Text に変換する | Monotalk -
TfidfVectorizer の使い方と、Cos類似度の高い上位5件 の取得方法について
ほぼ、Cos類似度とDoc2Vecってどっちが良いの? - Qiita そのままです。
StopWordを弾くため、mecab-ipadic-neologd
を辞書として使用するため、words
関数を少し変更しました。
Markdown の記述や、プログラム内の記述で変な文字列が抽出されていたので、そのあたりをStopWordとして登録しています。
実際のStopWordの記述はもっと多いです。 -
関連投稿の削除、登録について
Mezzanine
の BlogPost には、related_posts というManyToManyField がありこのフィールドに対して操作すれば、
関連投稿の登録削除ができます。
実装した感想とTODO
-
感想
一応動くものはできました。最初StopWordとか無い状態で結果の確認をしてみましたが、
それっぽいものが関連投稿として抽出できているように思いました。
ただ、カテゴリーとして数が少ないものは、とんちんかんな結果となるため、
あまり関連度が低いものは足切りして、表示しないなどの考慮は必要になるかと。 -
TODO
- パラメータによる重みづけ
WordPress の 関連投稿Plugin等を見ていると、タイトルに対しての類似度、カテゴリー等を含めて、
重みづけをして関連投稿として表示したりする機能があったりするので、
そのような機能を参考に、パラメータ調整できるようにしてみたいです。 - 5件以上関連投稿が登録される場合がある
ManyToManyField だからか、複数の記事から関連投稿として抽出される記事は、自分自信の関連投稿を含めると、 5件以上登録される場合があって、そのあたりのロジックを調整したいなと思ったりします。
- パラメータによる重みづけ
WordPress の 関連投稿Plugin等を見ていると、タイトルに対しての類似度、カテゴリー等を含めて、
もっと実装に時間かかるかと思いましたが、それっぽく動作するものは、1日かからずに作ることができました。
以上です。
追記
続きをMezzanine の Blog に 関連記事 のレコメンド表示をカスタマイズする | Monotalk
に書きました。TODO
の一部を実装しましたので、そちらもご参照ください。
コメント