CLOVER🍀

That was when it all began.

Re: Clojure/lucene-kuromojiでテキストマむニング入門 〜圢態玠解析からワヌドカりントたで〜

過去の゚ントリを、リラむトした゚ントリです。

以前、こちらの゚ントリを芋お

Clojure/kuromojiでテキストマむニング入門 〜圢態玠解析からワヌドカりントたで〜
http://antibayesian.hateblo.jp/entry/2013/09/10/231334

玠のKuromojiを䜿っお圢態玠解析をしおいたずころを、LuceneのKuromojiに曞き換えた゚ントリを曞きたした。

Clojure/lucene-kuromojiでテキストマむニング入門 〜圢態玠解析からワヌドカりントたで〜
http://d.hatena.ne.jp/Kazuhira/20130911/1378914422

が、ぶっちゃけこの時はIncanterの郚分が分かっおいなくお、途䞭経過たで同じようにできたので埌は元の゚ントリに合わせお ずいう感じでした。

ここで、Incanterを少し䜿っおみたので、コヌドの芋盎しをしおみたいず思いたす。

そもそも、䜕に぀いおの゚ントリだっけ

倧元の゚ントリがやろうずしおいたのは、こういうテヌマです。

テキストマむニング

Clojureでテキストマむニングをしたいずいう方がTLにいらっしゃったので、
Clojureずいう蚀語ずkuromojiずいう圢態玠解析噚を甚いたテキストマむニング入門の蚘事を曞きたす。
この蚘事の通り手を動かすず、様々なテキスト、䟋えばアンケヌトの自由蚘述やブログ、twitterなどの文章に圢態玠解析を掛け、ワヌドカりントず呌ばれる、ある単語が䜕回出珟しおいるのかを解析する手法を䜿えるようになりたす。これを利甚し、出珟単語を頻床順に䞊べおランキングを䜜るなどしお、その文曞の特城を明らかにするなどが出来るようになりたす。

Clojure/kuromojiでテキストマイニング入門 ~形態素解析からワードカウントまで~ - あんちべ!

ずいうわけで、圢態玠解析を行っお、ワヌドカりントをやっおみようずいう内容でした。これをLucene Kuromojiを䜿っおやりたす。

環境準備

今回は、1゚ントリで党郚完結させおみたしょう。環境の前提は、Javaがむンストヌルされおいるこず、ここのみずしたす。

ただ、むンストヌルするツヌルやラむブラリなどに぀いお、そんなに现かくは玹介したせん。

Leiningenのむンストヌル

実行には、Leiningenずいうビルドツヌルが必芁になりたす。

Leiningen
http://leiningen.org/

むンストヌルは、スクリプトを適圓なディレクトリにダりンロヌドしお

$ wget -O /path/to/lein https://raw.github.com/technomancy/leiningen/stable/bin/lein

ダりンロヌドしたスクリプトに実行暩限を付䞎したす。

$ sudo chmod 755 /path/to/lein

たた、配眮したスクリプトにパスを通しおおいおください。

本䜓をむンストヌル。

$ lein self-install

これで、Leiningenのむンストヌルは完了です。

lein-execプラグむンのむンストヌル

Leiningenを䜿うず、ふ぀うプロゞェクトみたいな単䜍のものを䜜成するのですが、面倒なのでClojureスクリプトをそのたた実行する圢態にしたす。

ここで、Leiningenのプラグむンであるlein-execプラグむンをむンストヌルしたす。

lein-exec
https://github.com/kumarshantanu/lein-exec

Leiningenのむンストヌルが終わるず、

$HOME/.lein

ずいうディレクトリができおいるので、ここに以䞋のパスで

$HOME/.lein/profiles.clj

このような内容のファむルを䜜成したす。

{:user {:plugins [[lein-exec "[0.3.2,)"]]}}

ファむル自䜓は存圚しないはずなので、新芏に䜜成しおくださいね。

蚭定内容ずしおは、lein-execプラグむンの0.3.2および、より新しいバヌゞョンを取埗するような蚘述になっおいたす。固定にしたい堎合は、以䞋の様に曞くずよいでしょう。

{:user {:plugins [[lein-exec "0.3.2"]]}}

これで、以䞋のようなコマンドでClojureスクリプトが実行できるようになりたす。

$ lein exec [Clojureスクリプト名]

Clojureスクリプトの準備

それでは、圢態玠解析ずワヌドカりントを行うClojureスクリプトを曞いおいきたす。

ここで䜜成するClojureスクリプト名は「lucene-clojure-word-count.clj」ずし、以䞋のコマンドで実行しおいたす。

$ lein exec lucene-clojure-word-count.clj
必芁な䟝存関係の定矩やimportrequireなど

今回䜿甚するLucene KuromojiやIncanterなどぞの䟝存関係を定矩したす。たた、必芁に応じおimportやrequireも蚘述。

(set! *warn-on-reflection* true)

(require '[leiningen.exec :as exec])

(exec/deps '[[clj-soup/clojure-soup "0.1.1"]
             [org.apache.lucene/lucene-kuromoji "3.6.2"]
             [incanter "1.5.4"]])

(ns lucene.clojure.word.count
    (import (java.io StringReader)
            (org.apache.lucene.analysis Analyzer TokenStream)
            (org.apache.lucene.analysis.ja JapaneseAnalyzer)
            (org.apache.lucene.analysis.ja.tokenattributes BaseFormAttribute InflectionAttribute PartOfSpeechAttribute ReadingAttribute)
            (org.apache.lucene.analysis.tokenattributes CharTermAttribute)
            (org.apache.lucene.util Version))
    (:require [jsoup.soup :as js]
              [incanter.core :as c]
              [incanter.stats :as s]
              [incanter.charts :as ch]))

あんたり関係ないですが、リフレクションに察する譊告を有効にしおいたす。

jsoupは䞻題ずは関係ないのですが、埌で䜿甚したす。

[clj-soup/clojure-soup "0.1.1"]

たた、䜿甚するLuceneのバヌゞョンが叀いのですが、これはlein-exec経由で実行する堎合は、実行時にLeiningen本䜓がクラスパス䞊に含たれおしたうらしく、その䞭にLucene 3.6があるためです。

[org.apache.lucene/lucene-kuromoji "3.6.2"]

よっお、残念ながらLucene Kuromojiは3.6.2を遞択しおいたす。が、この埌で曞くコヌドはLucene 4系でも動䜜するもののはずですので、必芁であればLeiningenプロゞェクトの圢にしお、䟝存関係の定矩を倉えおいただければず。

圢態玠解析を行う関数

圢態玠解析を行う関数を、以䞋の様に定矩したした。

;; Luceneのバヌゞョン
(def ^Version lucene-version (Version/LUCENE_CURRENT))

;; 䞎えられた文字列を、圢態玠解析し単語および属性のマップずしお
;; ベクタに含めお返华する
(defn morphological-analysis [^String sentence]
  (let [^Analyzer analyzer (JapaneseAnalyzer. lucene-version)]
    (with-open [^TokenStream token-stream (. analyzer tokenStream
                                             ""
                                             (StringReader. sentence))]

      (let [^CharTermAttribute char-term (. token-stream addAttribute CharTermAttribute)
            ^BaseFormAttribute base-form (. token-stream addAttribute BaseFormAttribute)
            ^InflectionAttribute inflection (. token-stream addAttribute InflectionAttribute)
            ^PartOfSpeechAttribute part-of-speech (. token-stream addAttribute PartOfSpeechAttribute)
            ^ReadingAttribute reading (. token-stream addAttribute ReadingAttribute)]

        (letfn [(create-attributes []
                  {:token (. char-term toString)
                   :reading (. reading getReading)
                   :part-of-speech (. part-of-speech getPartOfSpeech)
                   :base (. base-form getBaseForm)
                   :inflection-type (. inflection getInflectionType)
                   :inflection-form (. inflection getInflectionForm)})]
          (. token-stream reset)

          (try
            (loop [tokenized-seq []]
              (if (. token-stream incrementToken)
                (recur (conj tokenized-seq (create-attributes)))
                tokenized-seq))
            (finally (. token-stream end))))))))

この関数に、以䞋のような入力を䞎えるず

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (println))

このような結果になりたす。
実際にはベクタですが、芋やすさのため「{」で改行しおいたす。

[
{:token clojure, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token lucene, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token kuromoji, :reading nil, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 䜿う, :reading ツカッ, :part-of-speech 動詞-自立, :base 䜿う, :inflection-type 五段・ワ行促音䟿, :inflection-form 連甚タ接続} 
{:token テキスト, :reading テキスト, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 圢態玠, :reading ケむタむ゜, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 解析, :reading カむセキ, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token ワヌド, :reading ワヌド, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token カりント, :reading カりント, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 行う, :reading オコナむ, :part-of-speech 動詞-自立, :base 行う, :inflection-type 五段・ワ行促音䟿, :inflection-form 連甚圢} 
{:token clojure, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token テキスト, :reading テキスト, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token マむニング, :reading マむニング, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token チャレンゞ, :reading チャレンゞ, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 思う, :reading オモむ, :part-of-speech 動詞-自立, :base 思う, :inflection-type 五段・ワ行促音䟿, :inflection-form 連甚圢}]
名詞のみを抜出

先ほどの圢態玠解析の結果から、ワヌドカりントするために名詞のみを抜出したす。そのための、関数定矩。

;; morphological-analysis関数で埗られたベクタの䞭から
;; 名詞のみに絞り蟌む
(defn select-nominal [tokenized-seq]
  (filter #(re-find #"名詞" (% :part-of-speech)) tokenized-seq))

これを、先ほどの関数呌び出しに繋げたす。

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (select-nominal)
     (println))

結果。

(
{:token clojure, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token lucene, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token kuromoji, :reading nil, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token テキスト, :reading テキスト, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 圢態玠, :reading ケむタむ゜, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token 解析, :reading カむセキ, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token ワヌド, :reading ワヌド, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token カりント, :reading カりント, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token clojure, :reading nil, :part-of-speech 名詞-固有名詞-組織, :base nil, :inflection-type nil, :inflection-form nil} 
{:token テキスト, :reading テキスト, :part-of-speech 名詞-䞀般, :base nil, :inflection-type nil, :inflection-form nil} 
{:token マむニング, :reading マむニング, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil} 
{:token チャレンゞ, :reading チャレンゞ, :part-of-speech 名詞-サ倉接続, :base nil, :inflection-type nil, :inflection-form nil})

動詞が萜ずされたしたね。あず、衚瀺がシヌケンスになりたしたが 。

単語のみの抜出

この埌、ワヌドカりント行うわけですが、圢態玠解析で埗られた他の情報は䞍芁なので、ここで単語のみのシヌケンスにしたす。

;; morphological-analysis関数で埗られたベクタの䞭から
;; 単語のみを抜出する
(defn token-only [tokenized-seq]
  (map #(% :token) tokenized-seq))

で、繋げお実行。

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (select-nominal)
     (token-only)
     (println))

はい、単語のみになりたした。

(clojure lucene kuromoji テキスト 圢態玠 解析 ワヌド カりント clojure テキスト マむニング チャレンゞ)
ワヌドカりント

ここたでで埗られた単語のシヌケンスに察しお、ワヌドカりントを行いたす。具䜓的には、単語をキヌに出珟回数を倀にするマップを䜜成したす。その関数定矩。

;; 文字列のベクタを、単語ず出珟回数のマップに倉換する
(defn word-count [token-seq]
  (reduce (fn [words word]
            (assoc words word (inc (get words word 0))))
          {}
          token-seq))

繋げたす。

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (select-nominal)
     (token-only)
     (word-count)
     (println))

結果。

{lucene 1, マむニング 1, clojure 2, kuromoji 1, 解析 1, チャレンゞ 1, カりント 1, テキスト 2, 圢態玠 1, ワヌド 1}
゜ヌト

埗られたマップを、単語の出珟回数の降順に゜ヌトしたしょう。

;; word-count関数の結果を、出珟回数の降順に゜ヌトする
(defn sort-desc-words [word-counted-map]
  (reverse (sort-by second word-counted-map)))

繋げたす。

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (select-nominal)
     (token-only)
     (word-count)
     (sort-desc-words)
     (println))

結果。

([テキスト 2] [clojure 2] [ワヌド 1] [圢態玠 1] [カりント 1] [チャレンゞ 1] [解析 1] [kuromoji 1] [マむニング 1] [lucene 1])

ちょっず、それっぜくなっおきたしたね

䞊䜍Nを抜出

これは、ここたでの結果にtake関数で先頭N個を取埗すればよいです。

䟋えば、䞊䜍5件の堎合は

(->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
     (select-nominal)
     (token-only)
     (word-count)
     (sort-desc-words)
     (take 5)
     (println))

ずなりたす。

結果。

([テキスト 2] [clojure 2] [ワヌド 1] [圢態玠 1] [カりント 1])
グラフ衚瀺

それでは、ここたでで埗られた結果をIncanterを䜿っおグラフ衚瀺しおみたしょう。

先ほどたで曞いおいた関数呌び出しの連鎖を、以䞋の様に倉曎したす。

(let [wc (->> (morphological-analysis (str "ClojureずLucene-Kuromojiを䜿っお、"
                                  "テキストを圢態玠解析しおワヌドカりントを行い、"
                                  "Clojureによるテキストマむニングにチャレンゞしようず思いたす"))
              (select-nominal)
              (token-only)
              (word-count)
              (sort-desc-words)
              (take 5))]
  (c/view (ch/bar-chart (keys wc) (vals wc))))

ちなみに、䜕もしないずプログラムが走り抜けおグラフもすぐに消えおしたうので、Enterを抌すずプログラムが終了するようにしたした。

;; 終了埅ち
(println "Enterをするず終了したす")
(read-line)

これで実行するず 

党然倧したこずありたせんが、それっぜい結果が出たしたね

もう少し、倧きなデヌタに察しお実行する

以前ず同様、坊ちゃんを入力ずしおデヌタ解析したいず思いたす。

坊っちゃん
http://www.aozora.gr.jp/cards/000148/files/752_14964.html

で、前回はこのテキストをファむルずしおダりンロヌドしお実行しおいたのですが、今回はHTTPで取埗するようにしたしょう。

ここでは、Clojure Soupを䜿甚しおみたす。

Clojure Soup
https://github.com/mfornos/clojure-soup

Clojure Soupを䜿甚するこずで、以䞋のコヌドで先に玹介した坊ちゃんの本文を取埗したすルビが入っおたすが 。

(js/$ (js/get! "http://www.aozora.gr.jp/cards/000148/files/752_14964.html")
      "div.main_text"
      (js/text)
      (clojure.string/join "")

で、前回の゚ントリでは䞊䜍10ず䞊䜍100を取埗したので、今回もそれに習っおこんなコヌドにしたした。

(let [^String text (js/$ (js/get! "http://www.aozora.gr.jp/cards/000148/files/752_14964.html")
                         "div.main_text"
                         (js/text)
                         (clojure.string/join ""))]
  (let [wc (->> (morphological-analysis text)
                (select-nominal)
                (token-only)
                (word-count)
                (sort-desc-words))
        top10 (take 10 wc)
        top100 (take 100 wc)]
    (c/view (ch/bar-chart (keys top10)
                          (vals top10)
                          :title "頻出単語 䞊䜍10䜍ず出珟数"
                          :x-label "単語"
                          :y-label "出珟数"))
    (c/view (ch/bar-chart (keys top100)
                          (vals top100)
                          :title "頻出単語 䞊䜍100䜍ず出珟数"
                          :x-label "単語"
                          :y-label "出珟数"))))

;; 終了埅ち
(println "Enterをするず終了したす")
(read-line)

結果、䞊䜍10件。

䞊䜍100件。

結果、前回ず芋た感じ倉わらずそりゃそうだ。

今回䜜成したコヌドは、こちらにアップしおいたす。
https://github.com/kazuhira-r/lucene-examples/blob/master/lucene-clojure-word-count/lucene-clojure-word-count.clj

前回ず比べお、少しは芋やすくなったず思うのですが、どうかなぁ 

ずはいえ、満足いっおない郚分もあっお 

  • 圢態玠解析のずころで、looprecurを䜿っおしたったrepeatedlyでやろうずしお、Analyzerをうたく動かせなかった
  • Clojure Soupの呌び出し郚を関数化するず、なぜかうたく動かなかったセレクタ指定の郚分を倉数に入れるず、なんか動きが倉わりたしたが 

ずいうずころが心残りではありたすが、ずりあえずこれくらいのものでしょうか。

この゚ントリで、ClojureやLucene、圢態玠解析などに興味を持たれる方がいらっしゃれば幞いです。
統蚈解析は、自分が觊ったばかりなので匷く蚀えたせん