liblinearと自然言語処理を用いた文の分類

機械学習技術 人工知能技術 デジタルトランスフォーメーション技術 自然言語処理 異常・変化検知 オンライン学習 オントロジー技術 画像情報処理 サポートベクトルマシン Clojure 本ブログのナビ
liblinearと自然言語処理を用いた文の分類

少し前の記事で、k-meansを用いた分類について述べた。今回は、自然言語処理とliblinearを用いた分類について述べてみたい。

liblinearは国立台湾大学で開発されたオープンソースのSVM(support vector machine)である。SVMは値教師あり学習を用いるパターン認識モデルの一つで、分類や回帰への適用が可能なアルゴリズムとなる。liblinearはそのSVMを用いてinstanceや特徴が100万桁のデータを高速に線形分離可能である特徴を持ち、様々なタスクに活用される。

今回はそのClojureのラッパーであるclj-liblinearを用いて、自然言語処理ツールであるkuromojiと組み合わせて、文の分類を行うタスクを行ってみた。

clj-liblinearを利用するには、project.cljファイルの:dependenciesに以下を加える。

[clj-liblinear "0.1.0"]

clj-liblinearに記載のサンプルコードは以下のようになる。

(use '[clj-liblinear.core :only [train predict]]
     '[clojure.string :only [split lower-case]])

(def facetweets [{:class 0 :text "grr i am so angry at my iphone"}
                 {:class 0 :text "this new movie is terrible"}
                 {:class 0 :text "disappointed that my maximum attention span is 10 seconds"}
                 {:class 0 :text "damn the weather sucks"}

                 {:class 1 :text "sitting in the park in the sun is awesome"}
                 {:class 1 :text "eating a burrito life is super good"}
                 {:class 1 :text "i love weather like this"}
                 {:class 1 :text "great new album from my favorite band"}])

(let [bags-of-words (map #(-> % :text (split #" ") set) facetweets)
      model         (train bags-of-words (map :class facetweets))]
  
  (map #(predict model (into #{} (split % #" ")))
       ["damn it all to hell!"
        "i love everyone"
        "my iphone is super awesome"
        "the weather is terrible this sucks"]))

;; => (0 1 1 0)

facetweetに、文とclassを指定したデータを設定し、そのデータを使ってbag-of-wordsとmodelを生成して学習、その結果を使って入力した文を判定するシンプルなモジュールとなっている。サンプルでは入力が英語だが、日本語の文も英語と同様にトークンに分割した後、スペースを挿入して並べることで同様に利用できる。

そこで日本語の形態素解析ツールであるkuromojiを用いてトークンに分解した。kuromojiとして利用可能なモジュールは複数あるが、今回はオープンソースの検索エンジン Apache Lucene のテキスト解析部で用いられているJava で書かれたモジュールを、Clojureのjava互換性の特質を利用して用いた。

project.cljの:dependenciesには以下のようにjavaのmavenにあるモジュールを登録する

[org.apache.lucene/lucene-analyzers-kuromoji "5.0.0"] 

これをClojureのコードの中で利用するには、以下に示すように:importで指定する。

(:import (java.io File FileInputStream InputStreamReader BufferedReader StringReader
            BufferedWriter OutputStreamWriter FileOutputStream)
           (org.apache.lucene.analysis.ja JapaneseAnalyzer JapaneseTokenizer)
           (org.apache.lucene.analysis.ja.tokenattributes PartOfSpeechAttribute)
           (org.apache.lucene.analysis.tokenattributes CharTermAttribute OffsetAttribute)
           (org.apache.lucene.analysis.util CharArraySet))

この時、lucine.kuromojiモジュールだけではなく、java.ioモジュールもimportして文字列の操作が可能とする。後は、以下のコードで示されるようにトークンに分解してその間にスペースを挿入した文を生成する関数を設定する。

;;形態素解析関数
(defn morphological-analysis
  [src]
  (let [analyzer (JapaneseAnalyzer. nil
                                    JapaneseTokenizer/DEFAULT_MODE
                                    CharArraySet/EMPTY_SET
                                    #{})
        rdr (StringReader. src)]
    (with-open [ts (.tokenStream analyzer "field" rdr)]
      (let [^OffsetAttribute       offsetAtt (.addAttribute ts OffsetAttribute)
            ^PartOfSpeechAttribute posAtt    (.addAttribute ts PartOfSpeechAttribute)
            _       (.reset ts)
            surface #(subs src (.startOffset offsetAtt) (.endOffset offsetAtt))
            pos     #(.getPartOfSpeech posAtt)
            tokens  (->> #(if (.incrementToken ts)
                            [(surface) (pos)]
                            nil)
                         repeatedly
                         (take-while identity)
                         doall)
            _       (.end ts)]
        tokens))))

;;形態素解析の結果を(品詞,単語)の形に変形
(defn simple-morph [n] (map #(list (first (str/split (% 1) #"-")) (% 0)) (morphological-analysis n)))

;;名詞と動詞を抽出する関数 (名詞、動詞、助詞、形容詞、記号、助動詞)
(defn tokutei-token [n]
  (keep
   #(cond
      (= "名詞" (first %)) (second %)
      (= "動詞" (first %)) (second %)
      :else nil)(simple-morph n)))
(defn tokutei-token2 [n] (str/join #" " (tokutei-token n)))

;;全ての単語のみの抽出
(defn token-all [n](map #(second %)(simple-morph n)))
(defn token-all2 [n] (str/join #" " (token-all n)))

ここで、単純にトークン全てを用いるだけではなく、品詞のフィルタリングをしたバージョンの関数も規定した。これに先程のliblinearの関数を加えれば処理ができるようになる。

;;教師データ
(def facetweets [{:class 0 :text (token-all2 "今日のタスクは何ですか?")}
                 {:class 0 :text (token-all2 "何の仕事がありますか?")}
                 {:class 0 :text (token-all2 "何をするべきですか?")}
                 {:class 0 :text (token-all2 "今日は何が残っていますか?")}

                 {:class 3 :text (token-all2 "おはようございます")}
                 {:class 3 :text (token-all2 "こんにちは")}
                 {:class 3 :text (token-all2 "今日の天気は何ですか?")}
                 {:class 3 :text (token-all2 "ご機嫌いかが?")}

                 {:class 4 :text (token-all2 "はい")}
                 {:class 4 :text (token-all2 "yes")}
                 {:class 4 :text (token-all2 "お願いします")}
                 {:class 4 :text (token-all2 "YES")}
                 {:class 4 :text (token-all2 "そうですね")}

                 ])
;;分類判定関数
(defn categorize [input]
  (let [bags-of-words (map #(-> % :text (str/split #" ") set) facetweets)
        model         (liblinear/train bags-of-words (map :class facetweets))]
    (map #(liblinear/predict model (into #{} (str/split % #" ")))
         [(token-all2 input)])))

;;(categorize "こんにちは")   ;(1.0)

教師データの格納と/取り出しに前回に紹介したデータベースを活用すると、リアルタイムに修正する機械学習ツールができることになる。

コメント

  1. […] 前回に引き続き、clojureを用いた機械学習を述べたい。今回は教師なし学習であるk-meansについて。 […]

  2. […] Clojureでのliblinearと自然言語をを用いた文の分類 […]

  3. […] liblinearと自然言語処理を用いた文の分類 […]

  4. […] liblinearと自然言語処理を用いた文の分類 […]

タイトルとURLをコピーしました