子育て情報アプリを作れるように vol11 ~タグを自動生成する~

今回は、
クローラーで収集したデータにタグ情報を追加していきます。

で、前々から使ってみたいと思っていた
Mecabという形態素解析器を使いたいと思います。
形態素解析というのは、
簡単に言うと文章を品詞単位に分解してくれるものです。
詳しく言うと・・・
知りません!グーグルで検索して下さい笑

 
今回の目標

  1. Mecabの使い方を学ぶ
  2. Mecabを使ってタグを生成する
  3. TF-IDFを使う
  4. 自力でタグを生成する
  5. 今回の修正点

1. Mecabの使い方を学ぶ

mecabに限らず、使い方を学ぶ際は
ruby ○○○」で検索すると、大抵ヒットします。
今回も同様に「ruby mecab」で検索して、調べました。

で、まずは以下を実行

(1) brew install mecab
(2) brew install mecab-ipadic
※ 下記はmecab-ipadic-neologdをインストールするために必要
(3) brew install curl
(4) brew install xz

次にgemfileを修正
gem 'mecab', '>= 0.966'
そしてbundle installを実行します。

次に最新の辞書情報を取得してきます。
古い辞書で良いならこの処理はスキップできます。

(1) git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
(2) cd mecab-ipadic-neologd
(3) ./bin/install-mecab-ipadic-neologd -n

以後、最新の辞書に更新したい時は
./bin/install-mecab-ipadic-neologd -n
を実行すれば良さそうです。

設定は以上です。

2. Mecabを使ってタグを生成する

では、早速Rubyから呼び出してみます。

require 'mecab'

mecab = MeCab::Tagger.new("-Ochasen -d /usr/local/Cellar/mecab/0.996/lib/mecab/dic/mecab-ipadic-neologd")
parseNode = mecab.parseToNode("解析したい文字列")

while parseNode
  # parseNode.surfaceには品詞単位に分解した文字列がセットされている
  #  parseNode.featureには分解した文字列の品詞情報がセットされている
  puts "#{parseNode.surface} => #{parseNode.feature}"
  parseNode = parseNode.next # 次の品詞へ
end

と書くだけです。
正直言うと"-Ochasen"の意味を分かって無いですが
まぁ良しとします笑

試しに解析したい文字列に
"1歳の赤ちゃん"
という文字列を渡すと

1 => 名詞,数,*,*,*,*,1,イチ,イチ
歳 => 名詞,接尾,助数詞,*,*,*,歳,サイ,サイ
の => 助詞,連体化,*,*,*,*,の,ノ,ノ
赤ちゃん => 名詞,一般,*,*,*,*,赤ちゃん,アカチャン,アカチャン

おぉ〜。

これを使って、よく出てくる単語を抽出してみます。

例えば、

  • 名詞
  • 動詞
  • 形容詞

だけ取り出す場合はこんな感じで書きます。
f:id:tumiki_jp:20151005093251p:plain

続いて、
同じ単語が何回出てくるか集計します。
f:id:tumiki_jp:20151005093643p:plain

続いて続いて、
出現回数が多い単語順に並べ替えます。
f:id:tumiki_jp:20151005094130p:plain

最後に
ソートした上位の単語を登録すれば
よく出てくる単語がタグとして登録できます。
※最終的に私の作成したプログラムでは名詞だけ取り出すようにしています。
 

3. TF-IDFを使う

しかし、これだとまだ問題があります。
どの単語も一回しか出てこない場合は、 どの単語が重要か分かりません。 特にヤフー知恵袋のデータは
タグの基になるデータが「タイトル」しかなく、
そのタイトルも40文字しかありません。 これだと同じ単語なんて出てこない事の方が多いでしょう。

また、
同じ単語が何ども出てくるという事は
その単語がそのデータのタグとして ふさわしいと考えるのは安易すぎます。

 
どうしよ〜
と思ってネット検索していると
tf-idf
というキーワードが引っかかりました。

 
あれ・・・ 
どこかで聞いたことがある・・・
 

 
クロ本!
早速読み返したらやっぱり書いてありました。
ただしヤフーAPIを使った形態素解析でした。
最初からこっち使えばよかった・・・
っていう話ですが、
もう後戻りできないので
Mecabを使った方法で突き進みます。

(1) TF-IDFとは

Wikipediaには、このように書かれています。

tf-idf は、文書中の単語に関する重みの一種であり、主に情報検索や文章要約などの分野で利用される。

わかりやすく言うと ある文章を特徴付ける単語が何か判別してくれるということです。

(2) TF-IDFの考え方

公式は以下の通りです。

{ \displaystyle 
tfidf = tf \times idf
}
{ \displaystyle 
tf_{i,j} = \frac{n_{i,j}}{\sum_kn_{k,j}} 
}
{ \displaystyle 
idf_i = log \frac{|D|}{|\{d:d \ni t_i\}|} 
}

うーん、眠たくなってきた・・・笑
 
もう少し分かりやすく書くと

{ \displaystyle 
tfidf = \frac{ドキュメント内の単語出現回数}{ドキュメント内の単語の総数} \times log \frac{ドキュメントの総数}{単語が出現する文書の回数} 
}

こうなるようです。
 
実際に例を出してみます。

ドキュメントA内の単語(子育て, 大変, 子育て, つらい, 育児, つらい)
ドキュメントB内の単語(子育て、楽しい、離乳食、おいしい, 育児)
[ドキュメントAのTF]
  TF(単語)       = ドキュメント内の単語出現回数 / ドキュメント内の単語の総数
  TF(子育て)     = 2 / 6 = 0.33
  TF(大変)       = 1 / 6 = 0.16
  TF(つらい)     = 2 / 6 = 0.33
  TF(育児)       = 1 / 6 = 0.16
[ドキュメントAのIDF]
  IDF(単語)      = log(ドキュメントの総数 / 単語が出現する文書の回数)
  IDF(子育て)    = log(2 / 2) = 0.00
  IDF(大変)      = log(2 / 1) = 0.30
  IDF(つらい)    = log(2 / 1) = 0.30
  IDF(育児)      = log(2 / 2) = 0.00
[ドキュメントAのTF-IDF]
  TF-IDF(子育て) = 0.33 * 0.00 = 0.000
  TF-IDF(大変)   = 0.16 * 0.30 = 0.0480
  TF-IDF(つらい) = 0.33 * 0.30 = 0.099
  TF-IDF(育児)   = 0.16 * 0.00 = 0.000

この例では、
ドキュメントAを特徴付ける単語は
「つらい」
になります。
つまり、「つらい」をタグとして登録すれば良いことになります。

なるほどなぁ〜って感じです。

(3) TF-IDFをRubyで実装する

TF-IDFについて少し理解したので、
プログラムを作成しようと思ったのですが、
ここでまたちょっとした壁が・・・。
TFの方は対象のドキュメントがあれば簡単に計算することができますが、
IDFの方は

  • ドキュメントの総数
  • 対象の単語が出現したドキュメントの総数

が必要です。
この2つの値は分析する度に蓄積されていくものなので、
どこかに保存しておかないといけません。
とりあえず、jsonファイルに保存することにしました。
f:id:tumiki_jp:20151019022419p:plain こんな感じで、
あとでタグが更新できるように
URLと各単語の出現数を記録しておきます。

4. 自力でタグを生成する

あと、もう一つ問題があります。
Mecabを使って形態素解析すると
例えば、「1歳」という文字列は
「1」と「歳」に分解されてしまいます。
これだと、「1歳」というタグを作れないので
こういう時はMecabを使わずに自力で解析することにします。
解析と言うとすごくカッコいい感じがしますが、
大したことはしません。
単純に正規表現で抽出するだけです。
とりあえず今回は年齢を抽出するメソッドを作成しますが、
他にもタグにできそうな情報があればで後でメソッドを追加していけばいいと思います。
f:id:tumiki_jp:20151018231448p:plain

5. 今回の修正点

以上の事を踏まえてプログラムを書いてみました。
ファイルを追加したり修正したりと、
結構変更点があるので、
今回はソースコードを貼らずに
githubのリンクだけ貼っておきます。
興味のある方はご確認ください。
github.com

で、実行した結果がこちら

YouTube
f:id:tumiki_jp:20151019011329p:plain

cookpad
f:id:tumiki_jp:20151019011413p:plain

Yahoo知恵袋
f:id:tumiki_jp:20151019011432p:plain

GENERATED TAGS => の右側に表示されているのが、自動生成したタグになります。

こ、これは・・・
タグが微妙〜笑
使い物にならなそうですね・・・。
必要のない名詞はもっと削っていかないとダメですね。
(でもこんなの一個一個見てられない・・・。)
あと、もう一つ思った事があります。

例えば、「・・・赤いほっぺ・・・」という文章があったとして
これを形態素解析すると「赤い」と「ほっぺ」になります。
さらに「赤い」は形容詞なので、省かれて
「ほっぺ」がタグとして登録されてしまいます。
でもこれって、「赤いほっぺ」というタグの方が望ましいと思うわけです。

何かフリダシに戻された気分・・・。
ちーん。

でも、ここはあきらめずにネット検索
すると 「日本語係り受け解析器」
というキーワードが引っかかってきました。
これが使えるかどうかまだわかりませんが、
少しだけ可能性を感じています。
一筋の光がさしたような気持ちです。
なので、次の機会に取り組みます。

最後に

今回は、調査や開発に大変時間がかかってしまいましたので
ここらへんで一旦完成(ストップ)とします。

理想を言うと、  
もっと人工知能的な事をしたかったですが、
私がそこに足を踏み入れてしまうと、
何年かかるかわからないのでやりません。
もし簡単に実現できる方法があれば教えてください。
即採用します笑
 
では。

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

子育て情報アプリを作れるように vol10 ~yamlを使ってみる~

今回はYamlファイルを使って
クローラーの検索キーワードを読み込めるようにします。
多分、ヤムルと呼びます。
Yamlについては
http://magazine.rubyist.net/?0009-YAML#l2
に詳しく書かれています。

今回の目標

  1. Yamlを理解する
  2. Yamlファイルを読み込む

1. Yamlを理解する

外部ファイルを読み込むのって大変そうに思ってたんですが、
すごく簡単です。

まずは
require 'yaml'
と書いて
config = YAML.load_file('ファイル名')
で読み込めます。
あとは、変数configから値を取り出すだけです。

次に、Yamlファイルを作っていきたいと思います。
Yamlでは

というデータを扱えます。

配列

行頭に-を付けることで表現できます。

- hoge
- piyo

これをRubyで(先程上で書いたソースコード)読み込むと
["hoge", "piyo"]
となります。

ハッシュ

キー:値の形式で表現します。

name: yamada
age: 30

これをRubyで読み込むと
["name"=>"yamada", "age"=>30]
となります。
ここで注目なのは、
ageの値が"30"ではなく30となっている点です。
文字列じゃなくて、
ちゃんと数値として認識してくれています。

あとは、配列もハッシュもネスト(入れ子)にできるので、

  • 配列の配列
  • ハッシュのハッシュ

といった事もできます。

2. Yamlファイルを読み込む

では、実際に作っていきたいと思います。
今回はまず、 クローラーの検索キーワードをYamlファイルに書き込みます。
クローラーごとにハッシュと配列を組み合わせて
以下のように作成しました。

movie_crawler:
  search_word:
    - "子育て 1歳"
    - "離乳食"

cook_crawler:
  search_word:
    - "離乳食"
    - "幼児食"

これをconfigフォルダの中に
crawler-settings.ymlとして保存します。

続いて、読み込む処理ですが、
以下の2行で検索キーワードを取得できます。

crawler_settings = YAML.load_file('./main/config/crawler-settings.yml')
@keywords = crawler_settings["movie_crawler"]["search_word"]

これは、movie用のクローラーのプログラムですが、他のクローラー(cook_crawler)も同じです。

あとは、@keywordsを初期化していたロジックを
削除すれば終わりです。簡単!

movie_crawlerのソースコードはこんな感じになりました。
修正したのはcrawl_base()メソッドの中だけです。

今回はこの辺で。

子育て情報アプリを作れるように vol9 ~同じデータを何度も保存しない~

前回、クローラーで収集したデータを データベースへ保存する所まで実装しました。
しかしながら、前回のプログラムだと
クローラーを実行する度に同じデータをデータベースに保存してしまいます。
そこで、今回は同じデータを保存しないように
重複チェックを行いたいと思います。
また、重複した場合はデータベースに保存されたデータを 書き換えます。

今回の目標

  1. 重複をチェックする

1. 重複をチェックする

重複チェックに何のデータを使うか

収集した全てのデータにURLを持っているので
URLにしようと思います。
もしURLが途中で変わる事があったら

その時は諦めます笑
もしかしたら動画のタイトルでチェックする方がいいのかな・・・?

いつチェックするか

重複のチェックはデータベースに保存されているURLと比較を行うので、
データベースの更新処理が実装されている直前に行います。
なので、
movie-crawler
cook-crawler
qa-crawler
のsave_dataメソッドの中に書いていきます。

実際に作る

やる事は単純。

  1. データベースに同じURLがあったらUPDATE
  2. データベースに同じURLがなかったらINSERT文

それぞれ別のSQL文として実行します。それだけです。
修正箇所は

  • movie-crawler.rb
  • cook-crawler.rb
  • qa-crawler.rb

の3つで、
それぞれのsave_data_baseメソッド内の処理を修正します。

内容が同じなのでmovie-crawler.rbだけ掲載します。
修正前
f:id:tumiki_jp:20150918013956p:plain

修正後 f:id:tumiki_jp:20150918014404p:plain

ソースコード内にあるresult.cmdtuples
更新したレコード数がセットされていますのでログとして件数を出力しています。

特に難しいところはなかったですが、
最初は、UPDATE文とINSERT文を一回で実行しようとしていました。
しかし、複数SQL文(複文というらしい?)を一気に実行することは
できないっぽい事がネットに書かれていたのでやめました。
(実際にエラーが発生しました。)
まぁ特に分けても現状問題ないのでこれでいきます。

 

今回は短めですが、 
少し改良されたので良しとします。
では、今回はこの辺で。

次回は、
YouTubecookpadの検索キーワードを
外部ファイルから読み込むようにしたいと思います。