Blog に
実装した
Mezzanine の
前提
以下の
OS
CentOS release 6.9 (Final)Python Version
Python 2.7.8Package (必要そうな
ものだけ 抜粋)
Django (1.10.6)
Mezzanine (4.2.3)
ブログ記事の レコメンドに ついて
レコメンドに
協調フィルタリング
をコンテンツベース
での
それらの
協調フィルタリング
に
どうしたらいいのかよく
コンテンツベース
での
レコメンド
の
[1].ユーザーに
実装していくですかね?
python で、 コンテンツベースレコメンドを 実装する 方法
コンテンツベースの
わかりました。
文書を、
scikit-learn
のTfidfVectorizer
を使って、 ベクトル化し、 文書の 特徴を 抽出する。
TfidfVectorizer
を使う際は、 文章から 単語を 抽出する analyzer
関数を指定する 必要が あり、
analyzer
関数は、Mecab
を使って 実装する。 Mecab
の辞書は、 mecab-ipadic
以外に、mecab-ipadic-NEologd
があり、 それも 使用すると、
新語も認識できるようになる。 1.
でベクトル化した 結果の Cos類似度 を 求める。 Cos類似度が 1に 近い 文書ほど、 類似度が 高い。
Cos類似度 もscikit-learn
のcosine_similarity
関数を使うと 求める ことができる。
以下、
自ブログに おける 独自の 関心事
自ブログはMezzanine
で、mezzanine-pagedown
と
記事を
Markdown から、
テキスト部を 抽出する
当ブログは、mezzanine-pagedown
という markdown pluginを 使っているのですが、
これは、blogの 本文と して、 markdown その ものを、 本文と してDBに 設定し、
画面表示時に、filter で HTMLに 変換しています。
このDBに 設定されたmarkdownから 本文を 抽出する 必要が あります。 関連記事の
登録を する
Mezzanine には、デフォルトで、 関連投稿
の手動登録機能が あり、
この機能で 登録した 投稿は、 blog_blogpost_related_posts
という テーブルに 登録されます。
Cos類似度
の高い 関連記事は、 この テーブルに 登録します。 2
このテーブルから データを 取得して、 表示する テンプレートは 既に 用意されているので、
データさえ入ってしまえば、 画面表示部を 実装するのは 不要です。
[2].特に
レコメンド実装に 前提で 必要となる ライブラリの インストール
以下、
コマンドを
mecab
と、mecab-ipadic-NEologd
のインストール
mecab-ipadic-NEologd
のCentOS rpm ファイルが ありますので、
それを使いました。 2015年モノが 落ちてきましたので、 Github の README に 記載が ある 通り、
気まぐれ更新なのだと 思われます。
mecab-ipadic-neologd/README.ja.md at master · neologd/mecab-ipadic-neologdsudo 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 コマンド
以下、
記事が
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件以上登録される 場合が あって、 その あたりの ロジックを 調整したいなと 思ったりします。
- パラメータに
もっと
以上です。
追記
続きを
にTODO
の
コメント