はじめに
ホリデープログラマが趣味で自然言語処理をしてみます。
この投稿は、
「日本の自然言語処理の研究または研究者を増やし日本の自然言語処理の発展に貢献したい」
ついでに「Clojureのよさを知ってもらう」
というのが目的です。
今回は、LIBLINEARというものを試してみます。
公式サイトは、台湾の大学の研究室 になります。
論文は、[https://www.csie.ntu.edu.tw/~cjlin/papers/guide/guide.pdf] が参考になります。
今回使うライブラリは、clj-liblinear Github になります。
※ できるだけ難しい説明を使わず書いてみましたが、もし誤りなどありましたらコメントいただければと思います。
LIBLINEARの紹介
そもそもLIBLINEAR(リブライナー)とは何でしょうか?何ができるのでしょうか?
その前に、LIBLINEARの元になったSVM(support vector machine)を説明したほうがわかりやすいかもしれません。
ざっくりいうと、
「あるいくつかのA、B集団があるとして、XをA,Bの集団に割り当てるとして、どちらのほうに分類すべきか?」
というのを解決するものになります。
具体例はたくさんあるのですが、自然言語の分野で言えば、QAシステムなんかがわかりやすい例かもしれません。
ある人の質問が、フリーフォーマット入力の場合、どんな入力データがあるかわかりません。
そこで、事前にある特定の質問グループを作り、カテゴリ分けをしておきます。
・No.1 カテゴリA:あーお腹減った、近くにないかなぁ?
・No.2 カテゴリA:何か食べたい、店探してほしい。
・No.3 カテゴリB:中古車そろそろ売りたいなぁ~。
・No.4 カテゴリB:新車がほしいんですが、どこで探せばいいですか?
あるひとが「車をほしい」といったときに、システムが「あー車ね、カテゴリBに車の内容いっぱいあったわー」とかいって、カテゴリBというのに振り分け「車の販売、売買サイトへ誘導する」等に使えます。
数学的に説明すると、以下の資料がわかりやすいです。
・SVMについて
・SMO徹底入門-SVMをちゃんと実装する
・銀座で働くデータサイエンティストのブログ
予測をするために用いるデータの境界をサポートベクタ(support vector)といいます。上記の例だと、カテゴリAとカテゴリBの境界(文字の類似度で境界分けしてます)がサポートベクタです。で、あるデータをどっちのカテゴリに分けるか機械的に解析する手法がサポートベクタマシーン(support vector machine)です。よく略して、SVMと言われたりします。
なお、「車をほしい」といった質問がどのカテゴリに入るか(予測値)は、カテゴリA,B(訓練データ)との類似度の重み付け和となります。今回は単語の類似度になります。
そして、LIBLINEARは、このSVMに改良を加えて高速に計算できるようにして、線形予測(カテゴリを線でぶったぎる予測)に特化したライブラリのことを言います。
LIBLINEARの参考URLは以下のとおりです。
・LIBLINEARで分類したい!(1)
・線形予測の機械学習ツールliblinearで効果最大化のための最適な定数Cを探る
・LIBLINEARを用いた機械学習入門(単語分割)
プログラム
「ごたごたいってねーでさっさとプログラムさらせ」という声がでてきそうなので、そろそろサンプルプログラムを晒します。
覚えておく関数は、train(訓練する関数)とpredict(予測する関数)の2つだけ覚えておけばOKです。
各々関数の引数は、liblinear-java 参照してください。
clj-liblinearは、liblinear-javaをラッピング(書き直しみたいなもん)しています。
設定ファイル(project.clj)に依存するライブラリ書いて、テストデータ(category-utf8.csv)用意して、プログラム(core.clj)をぺちぺちREPL(プログラムを1行1行実行できる機能、JDK1.9から入るという噂です)で動かすだけです。
なお、今回は、辞書ファイル(userdict.txt)も用意しております。「サッカー日本代表」という単語があると、ご丁寧に「サッカー」「日本」「代表」と単語を区切ってしまいます。区切りたくない場合は、辞書に「サッカー日本代表」と書いておけば区切られずに済みます。
;(前方省略)
:dependencies [[org.clojure/clojure "1.7.0"]
[clj-liblinear "0.1.0"] ; liblinear本体
[org.atilika.kuromoji/kuromoji "0.7.7"] ; 形態素解析ライブラリ
[org.clojure/data.csv "0.1.3"] ; CSV処理ライブラリ
]
:repositories [["Atilika Open Source repository" ; 形態素解析ライブラリのリポジトリ
"http://www.atilika.org/nexus/content/repositories/atilika"]]
;(後方省略)
1,あーお腹減った、近くにないかなぁ?
2,何か食べたい、店探してほしい。
3,中古車そろそろ売りたいなぁ~。
4,新車がほしいんですが、どこで探せばいいですか?
## このファイルはUTF-8で記述ください。
## ただし、Windowsの場合は、SJISで記述ください。
関西国際空港,関西国際空港,カンサイコクサイクウコウ,カスタム名詞
サッカー日本代表,サッカー日本代表,ニホンダイヒョウ,カスタム名詞
欧州中央銀行,欧州中央銀行,オウシュウチュウオウギンコウ,銀行名詞
C++,C++,シープラプラ,プログラミング言語名詞
(ns bag-of-words-nlp.core
(:gen-class))
;; 各種ライブラリ
(require '[clj-liblinear.core :as liblinear])
(require '[clojure.java.io :as io])
(require '[clojure.string :as str])
(require '[clojure.data.csv :as csv])
;; 形態素解析ライブラリ
;; http://www.atilika.org/
(import '[org.atilika.kuromoji Token Tokenizer])
;; 名詞と動詞を抽出する関数 (名詞、動詞、助詞、形容詞、記号、助動詞)
(defn noun-token? [token]
(let [speech-part (str/split (.getPartOfSpeech token) #",")]
(cond ; 条件文
(= "名詞" (get speech-part 0)) true
(= "動詞" (get speech-part 0)) true
:else false
)))
;; kuromojiの辞書の場所を定義(WindowsはSJIS,MacはUTF-8)
;(def dic-file "c:\\lein\\userdict.txt") ; Windowsの場合の書き方
(def dic-file "resources/userdict.txt")
; 日本語の語彙を半角SPACE毎に切って1行にする関数
(defn convert-sentence
[sentence] ; 引数:形態素解析の対象となる文
; ↓ トークナイザ(形態素解析機能)をtokenizerという名前で定義
(let [^Tokenizer tokenizer (.build (.userDictionary (Tokenizer/builder) dic-file))]
(clojure.string/join ; 半角SPACE区切りのための文字列連結
; ↓ 出てきた形態素と品詞などの情報を抽出
(for [^Token token (.tokenize tokenizer sentence)]
(str (.getSurfaceForm token) " ")))))
;; 形態素解析の疎通確認
;(convert-sentence "私は、C++という言語が好きです。")
;(convert-sentence "欧州中央銀行")
;(convert-sentence "サッカー日本代表を応援しています。")
; → "サッカー日本代表 を 応援 し て い ます 。 "
;; 検索対象の文章ファイルの定義(Windows,MacともにUTF-8でOK)
;(def ctgy-file "c:\\lein\\category-utf8.csv") ; Windowsの場合の書き方
(def ctgy-file "resources/category-utf8.csv")
;; 検索対象の文章ファイルの取得
(def search-sentence
(with-open [in-file (io/reader ctgy-file)]
(doall (csv/read-csv in-file))))
;; CSV中身確認
; (println search-sentence)
; StringからNumberへキャスト関数
(defn String->Number [str]
(let [n (read-string str)]
(if (number? n) n nil)))
;; 検索対象の文章を半角SPACE区切りへ変換する
(def facelist
(into [] (for [x search-sentence
:let [y x]]
{:class (String->Number (first y)) :text (convert-sentence (second y))}
))
)
;; kuromoji変換後中身確認
; (println facelist)
;; 文章がどのカテゴリに入るか検証
(defn categorize
[search-word]
(let [word-text (map #(-> % :text (str/split #" ") set) facelist)
word-class (liblinear/train word-text (map :class facelist))]
(map
#(try
(liblinear/predict word-class (into #{} (str/split % #" ")))
(catch ClassCastException e (str "対象はありませんでした泣")))
[search-word]
)
))
;; 実行
(categorize (convert-sentence "新車が欲しい")) ;(4.0)
; →カテゴリ4を返却されれば成功です。
説明
clojureからjavaの呼び方
clojureを使う理由としてJAVAの過去の資産を生かせるというのがあります。
たくさんパターンあるので、インスタンスメソッドに特化して抜粋します。
import java.util.Random;
public class Ran{
public static void main(String[] args){
Random rnd = new Random();
int ran = rnd.nextInt(10);
System.out.println(ran);
}
}
(import '[java.util Random])
(def rnd (Random.)) ; (def rnd (new Random)) と等価
(def ran (. rnd nextInt 10) ; (def ran (.nextInt rnd 10)) と等価
(println ran)
どうでしょうか?
Clojureだと、javaのクラスやお決まり文法を書かずに、1行1行実行することができます。
そのため「やべ!CSVの読み込み間違えた!」という場合にも、その場で1行実行しなおせばOKです。
LightTableなどでは、1行実行した時点で実行した結果がエディタにおしゃれに表示されたりします。
(一概には言えませんが)Javaだと重いIDEでプログラム修正して、ビルドし直して、結果をみて~と時間とストレスがかかってしゃーないです。
なお、今回は、(2)のような糖衣構文(とういこうぶん:人がみてわかりやすい構文の書き方)を使いませんでしたが、以下の(1),(2)の構文は同じ意味です。
(.getLocation (.getCodeSource (.getProtectionDomain (.getClass '(1,2))))) ; (1)
(.. '(1 2) getClass getProtectionDomain getCodeSource getLocation) ; (2)
まあ、結局何がいいたいかというと、Webアプリケーションなどは、今後のメンテのためにJavaでやるしかないでしょうけど、データ解析やちょっとしたツール作りには、Clojureはもってこいだということです。
余談
最近、Word2vecという本を読んだんですが面白かったです。
単語をベクトル表現して、ある単語の近くには、その説明となる単語が含まれているはず、という仮説をもとに、例えば「野球」で「原辰徳」と定義した場合、「サッカー」だと誰がでてくるか検索をかけてみます。
すると・・・「三浦知良」「岡田武史」「奥寺康彦」「フィリップ・トルシエ」がでてきます。
どこかの有志の方が、wikipediaを全部ダウンロードして検索できるようにしているのがあるので叩いてみるといいかもしれません。
word2vec playground
しょせん上記サイトはwikipediaなので検索精度は低いですが、専門用語が多い分野では類義語検索などに適用できるかもしれません。また、人工知能の分野に応用できるかもしれません。
今、日本より欧米や中国なんかが自然言語処理の研究が進んでいるんですが、中国の自然言語処理の著名人は、昔、日本の大学で自然言語処理を学んでいた方が多いそうです。日本は、研究者が減る一方ですが、ポテンシャル高いはずなので、頑張ってほしいです。