Skip to content

Commit 70e7e6f

Browse files
committed
Merge branch 'release/0.1.0'
2 parents 6e2e84d + e224b47 commit 70e7e6f

15 files changed

+774
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ pom.xml.asc
99
/.nrepl-port
1010
.hgignore
1111
.hg/
12+
system_full.dic

README.md

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,54 @@
11
# Unmo - 形態素解析ベースの日本語チャットボット
22

3-
Unmo は日本語チャットボットプログラムです。
3+
Unmo は日本語チャットボットプログラムです。機械学習ではなく形態素解析に基づいてユーザーの発言を学習し、5 つの異なるエンジンが思考結果を返します。
4+
5+
- ホワット (単純に聞き返す)
6+
- ランダム (学習したユーザーの発言からランダムに返す)
7+
- パターン (名詞を学習し、発言に関係する言葉を返す)
8+
- テンプレート (文章の名詞部分を他の名詞に置き換える)
9+
- マルコフ (マルコフ連鎖アルゴリズムから独自の文章を生成する)
10+
11+
あくまでもユーザーの発言から文章を自動生成するだけなので、言葉の意味を理解しているわけではありません。そのため、乱暴な言葉ばかり使っていると乱暴なチャットボットに成長しますが、作者を恨まないでください。
12+
13+
はじめのうちはホワットやランダムが多いと感じるかもしれませんが、辛抱強く話しかけていると辞書にバリエーションが増え、あるときはまともな、あるときは突拍子もない反応をするようになります。じっくり話してあげてください。
414

515
## インストール
16+
### 必要なもの
17+
実行には JVM(Java 仮想マシン)が必要です。https://java.com/ja/download/ からダウンロード・インストールしてください。
18+
19+
また、形態素解析エンジンとして [Sudachi](https://github.com/WorksApplications/Sudachi/releases) を使用しています。辞書ファイルが必要になりますので、リンク先から `sudachi-0.1.1-dictionary-full.zip` をダウンロード・展開し、 `system_full.dic``unmo-x.x.x-standalone.jar` と同じディレクトリに置いてください。
620

7-
実行には JVM(Java 仮想マシン)が必要です。
8-
https://java.com/ja/download/ からダウンロード・インストールしてください。
21+
Unmo 本体は https://github.com/sandmark/unmo-clojure/releases からダウンロードすることができます。
922

10-
Unmo 本体は https://github.com/sandmark/unmo-clojure/ から
11-
ダウンロードすることができます。
23+
- `system_full.dic`
24+
- `sudachi_fulldict.json`
25+
- `unmo-x.x.x-standalone.jar`
1226

13-
## Usage
27+
上記 3 つのファイルが同じディレクトリにあれば準備完了です。
1428

15-
FIXME: explanation
29+
## 使い方
1630

17-
$ java -jar unmo-0.1.0-standalone.jar [args]
31+
Unmo はコンソールアプリケーションであるため、コマンドを実行する必要があります。
1832

19-
## Options
33+
Unix 系なら端末エミュレータ、Mac OS ならターミナル、Windows ならコマンドプロンプトを起動し、 `cd` コマンドで `unmo-x.x.x-standalone.jar` のあるディレクトリへ移動します。その後、
2034

21-
FIXME: listing of options this app accepts.
35+
$ java -jar unmo-0.1.0-standalone.jar
36+
37+
と打ち込んで Enter キーを叩けば起動します。
2238

23-
## Examples
39+
ひとしきり会話を楽しんだら、話しかけずに Enter キーを押せば終了します。
2440

25-
...
41+
## アンインストール
2642

27-
### Bugs
43+
`unmo-x.x.x-standalone.jar` のあるディレクトリを削除してください。
2844

29-
...
45+
## 謝辞
3046

31-
### Any Other Sections
32-
### That You Think
33-
### Might be Useful
47+
このプログラムは Ruby 用の書籍 [恋するプログラム](https://amzn.to/2FAG0AA) を参考に制作されました。著者である秋山智俊さんに多大な感謝を申し上げます。
3448

3549
## License
3650

37-
Copyright © 2018 sandmark
51+
Copyright © 2018-2019 sandmark
3852

3953
Distributed under the Eclipse Public License either version 1.0 or
4054
any later version.

project.clj

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
(defproject unmo "0.1.0-SNAPSHOT"
1+
(defproject unmo "0.1.0"
22
:description "A japanese legacy chatbot using Sudachi, a morphological analyser for modern era."
33
:url "https://github.com/sandmark/unmo-clojure/"
44
:license {:name "Eclipse Public License"
55
:url "http://www.eclipse.org/legal/epl-v10.html"}
6-
:dependencies [[org.clojure/clojure "1.10.0-RC5"]]
6+
:repositories [["Sonatype" "https://oss.sonatype.org/content/repositories/snapshots"]]
7+
:dependencies [[org.clojure/clojure "1.10.0"]
8+
[bigml/sampling "3.2"]
9+
[fipp "0.6.14"]
10+
[com.worksap.nlp/sudachi "0.1.1-SNAPSHOT"]]
11+
:plugins [[lein-environ "1.1.0"]
12+
[lein-eftest "0.5.3"]
13+
[lein-auto "0.1.3"]]
714
:main ^:skip-aot unmo.core
815
:target-path "target/%s"
9-
:profiles {:uberjar {:aot :all}})
16+
:profiles {:dev {:dependencies [[alembic "0.3.2"]
17+
[eftest "0.5.3"]]}
18+
:uberjar {:aot :all}})

src/unmo/core.clj

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,71 @@
11
(ns unmo.core
2-
(:gen-class))
2+
(:gen-class)
3+
(:require [unmo.responder :refer [response]]
4+
[unmo.dictionary :refer [study save-dictionary load-dictionary]]
5+
[unmo.morph :refer [analyze]]
6+
[unmo.version :refer [unmo-version]]
7+
[bigml.sampling [simple :as simple]]))
8+
9+
(def ^{:private true
10+
:doc "デフォルトで使用される辞書ファイル名"}
11+
dictionary-file
12+
"dict.clj")
13+
14+
(defn- rand-responder
15+
"確率によって変動するResponderを返す。
16+
:what 10%
17+
:random 20%
18+
:pattern 30%
19+
:template 20%
20+
:markov 20%"
21+
[]
22+
(-> [:what :random :pattern :template :markov]
23+
(simple/sample :weigh {:what 0.1
24+
:random 0.2
25+
:pattern 0.3
26+
:template 0.2
27+
:markov 0.2})
28+
(first)))
29+
30+
(defn- format-response
31+
"Responder からの結果を整形して返す。"
32+
[{:keys [responder response error]}]
33+
(let [responder-name (-> responder (name) (clojure.string/capitalize))]
34+
(if error
35+
(str responder-name "> 警告: " (:message error))
36+
(str responder-name "> " response))))
37+
38+
(defn- dialogue
39+
"ユーザーからの発言、形態素解析結果、辞書を受け取り、AIの思考結果を整形した文字列を返す。"
40+
([input parts dictionary]
41+
(dialogue input parts dictionary (rand-responder)))
42+
([input parts dictionary responder]
43+
(let [res (-> {:input input
44+
:dictionary dictionary
45+
:parts parts
46+
:responder responder}
47+
(response))]
48+
(case (get-in res [:error :type])
49+
:fatal (format-response res)
50+
nil (format-response res)
51+
(recur input parts dictionary :random)))))
352

453
(defn -main
5-
"I don't do a whole lot ... yet."
54+
"標準入力からユーザーの発言を受け取り、Responder の結果を表示して繰り返す。"
655
[& args]
7-
(println "Hello, World!"))
56+
(println (format "Unmo version %s launched." unmo-version))
57+
(print "> ")
58+
(flush)
59+
60+
(loop [input (read-line)
61+
dictionary (load-dictionary dictionary-file)]
62+
(if (clojure.string/blank? input)
63+
(do (println "Saving dictionary...")
64+
(save-dictionary dictionary dictionary-file)
65+
(println "Quit."))
66+
(let [parts (analyze input)
67+
res (dialogue input parts dictionary)]
68+
(println res)
69+
(print "> ")
70+
(flush)
71+
(recur (read-line) (study dictionary input parts))))))

src/unmo/dictionary.clj

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
(ns unmo.dictionary
2+
(:require [unmo.util :refer [conj-unique file-exists?]]
3+
[unmo.morph :refer [noun?]]
4+
[fipp.edn :refer [pprint] :rename {pprint fipp}]))
5+
6+
(defn- parts->markov
7+
"形態素解析結果をマルコフ辞書形式に変換する。"
8+
([dictionary [[prefix1 _] [prefix2 _] & parts]]
9+
(->> parts
10+
(map first)
11+
(parts->markov dictionary prefix1 prefix2)))
12+
13+
([dictionary prefix1 prefix2 [suffix & rest]]
14+
(if (not suffix)
15+
(update-in dictionary [prefix1 prefix2] conj-unique "%ENDMARK%")
16+
(-> dictionary
17+
(update-in [prefix1 prefix2] conj-unique suffix)
18+
(recur prefix2 suffix rest)))))
19+
20+
(defn- study-markov
21+
"形態素解析結果から文節のつながりを記録し、学習する。
22+
実装を簡単にするため、3単語以上で構成された文章のみ学習する。"
23+
[dictionary parts]
24+
(if (< (count parts) 3)
25+
dictionary
26+
(let [[start _] (first parts)]
27+
(-> dictionary
28+
(update-in [:markov :starts start] (fnil inc 0))
29+
(update-in [:markov :dictionary] parts->markov parts)))))
30+
31+
(defn- study-template
32+
"形態素解析結果に基づき、名詞の数をキー、名詞を%noun%に置き換えた発言のリストを値として学習する。
33+
重複、ないし名詞が無い発言は学習しない。
34+
学習した結果をdictionaryの:templateに定義して返す。"
35+
[dictionary parts]
36+
(letfn [(->noun [[word _ :as part]]
37+
(if (noun? part)
38+
"%noun%"
39+
word))]
40+
(let [nouns-count (->> parts (filter noun?) (count))
41+
template (->> parts (map ->noun) (apply str))]
42+
(if (zero? nouns-count)
43+
dictionary
44+
(update-in dictionary [:template nouns-count] conj-unique template)))))
45+
46+
(defn- study-pattern
47+
"形態素解析結果に基づき、名詞をキー、発言のベクタを値として学習する。重複は学習しない。
48+
学習した結果をdictionaryの:patternに定義して返す。"
49+
[dictinoary input parts]
50+
(let [nouns (->> parts (filter noun?) (map first))
51+
merge-unique (partial merge-with (comp distinct concat))
52+
make-pattern #(update %1 %2 conj-unique input)]
53+
(->> nouns
54+
(reduce make-pattern {})
55+
(update-in dictinoary [:pattern] merge-unique))))
56+
57+
(defn- study-random
58+
"文字列inputを辞書dictionaryの:randomベクタに追加して返す。重複は追加しない。"
59+
[dictionary input]
60+
(update dictionary :random conj-unique input))
61+
62+
(defn study
63+
"文字列inputと形態素解析結果partsを受け取り、辞書dictionaryに保存したものを返す。"
64+
[dictionary input parts]
65+
(-> dictionary
66+
(study-random input)
67+
(study-pattern input parts)
68+
(study-template parts)
69+
(study-markov parts)))
70+
71+
(defn save-dictionary
72+
"辞書dictionaryをpprintし、指定されたファイルに保存する。"
73+
[dictionary filename]
74+
(let [data (with-out-str (fipp dictionary))]
75+
(spit filename data :encoding "UTF-8")))
76+
77+
(defn load-dictionary
78+
"指定されたファイルから辞書をロードして返す。"
79+
[filename]
80+
(if (file-exists? filename)
81+
(-> filename (slurp :encoding "UTF-8") (read-string))
82+
{}))

src/unmo/morph.clj

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
(ns unmo.morph
2+
(:import [com.worksap.nlp.sudachi DictionaryFactory Tokenizer$SplitMode]))
3+
4+
(def ^:private tokenizer
5+
"Sudachiの形態素解析インスタンス"
6+
(try
7+
(let [settings (slurp "sudachi_fulldict.json" :encoding "UTF-8")]
8+
(-> (DictionaryFactory.) (.create settings) (.create)))
9+
(catch java.io.FileNotFoundException e
10+
(println (.getMessage e))
11+
(println "形態素解析ライブラリSudachiの設定ファイルと辞書を用意してください。")
12+
(System/exit 1))))
13+
14+
(def ^:private split-mode
15+
"Sudachiの形態素解析オプション"
16+
{:a Tokenizer$SplitMode/A :b Tokenizer$SplitMode/B :c Tokenizer$SplitMode/C})
17+
18+
(defn analyze
19+
"与えられた文字列に対して形態素解析を行い、[形態素 表層系]のリストを返す"
20+
([text] (analyze text :c))
21+
([text mode]
22+
(letfn [(->parts [token]
23+
(let [parts (->> (.partOfSpeech token) (clojure.string/join \,))
24+
surface (.surface token)]
25+
[surface parts]))]
26+
(->> (.tokenize tokenizer (mode split-mode) text)
27+
(map ->parts)
28+
(filter (comp (complement empty?) first))))))
29+
30+
(defn noun?
31+
"与えられた形態素が名詞かどうかを判定する"
32+
[[word part]]
33+
(-> #"名詞,(一般|普通名詞|固有名詞|サ変接続|形容動詞語幹)" (re-find part) (boolean)))

src/unmo/responder.clj

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
(ns unmo.responder
2+
(:require [unmo.morph :refer [noun?]]))
3+
4+
(defmulti response
5+
"渡された発言オブジェクトに対する返答を :response キーに設定して返す。どの思考エンジンが使用されるかは :responder キーの値で変わる。"
6+
:responder)
7+
8+
(defn- markov-generate
9+
"単語と辞書を受け取り、その単語から始まる文章をマルコフ連鎖で生成して返す。"
10+
([dictionary prefix1 prefix2] (markov-generate 30 dictionary prefix1 prefix2 [prefix1 prefix2]))
11+
([times dictionary prefix1 prefix2 result]
12+
(let [suffix (-> dictionary (get-in [prefix1 prefix2]) (rand-nth))]
13+
(cond (zero? times) (apply str result)
14+
(= "%ENDMARK%" suffix) (apply str result)
15+
:else (->> (conj result suffix)
16+
(recur (dec times) dictionary prefix2 suffix))))))
17+
18+
(defmethod
19+
^{:doc "MarkovResponderは形態素解析結果partsを受け取り、最初の単語またはランダムな単語から始まる文章を生成して返す。"}
20+
response :markov [{:keys [parts dictionary] :as params}]
21+
(if (empty? (:markov dictionary))
22+
(assoc params :error {:type :dictionary-empty
23+
:message "マルコフ辞書が空です"})
24+
(let [starts (get-in dictionary [:markov :starts])
25+
markov (get-in dictionary [:markov :dictionary])
26+
word (ffirst parts)
27+
prefix1 (if (contains? starts word)
28+
word
29+
(-> starts (keys) (rand-nth)))
30+
prefix2 (-> (get markov prefix1) (keys) (rand-nth))]
31+
(assoc params :response (markov-generate markov prefix1 prefix2)))))
32+
33+
(defmethod
34+
^{:doc "TemplateResponderは入力inputの名詞を調べ、辞書のテンプレートの%noun%をその名詞で置き換えて返す。"}
35+
response :template [{:keys [parts dictionary] :as params}]
36+
(let [nouns (->> parts (filter noun?) (map first))
37+
nouns-count (count nouns)
38+
->response #(clojure.string/replace-first %1 #"%noun%" %2)]
39+
(if-let [templates (get-in dictionary [:template nouns-count])]
40+
(->> nouns
41+
(reduce ->response (rand-nth templates))
42+
(assoc params :response))
43+
(-> params
44+
(assoc :error {:type :no-match
45+
:message "一致するテンプレートがありません。"})))))
46+
47+
(defmethod
48+
^{:doc "PatternResponderは入力inputに正規表現でマッチするパターンを探し、そのうちランダムなものを返す。"}
49+
response :pattern [{:keys [input dictionary] :as params}]
50+
(letfn [(match? [[pattern phrases]]
51+
(-> (re-pattern pattern) (re-find input)))]
52+
(if-let [[pattern phrases] (->> (:pattern dictionary) (filter match?) (first))]
53+
(let [match (-> (re-pattern pattern) (re-find input) (first))
54+
phrase (rand-nth phrases)
55+
text (clojure.string/replace phrase #"%match%" match)]
56+
(assoc params :response text))
57+
(assoc params :error {:type :no-match :message "パターンがありません"}))))
58+
59+
(defmethod
60+
^{:doc "RandomResponderは入力に関係なく、:dictionary -> :random に定義されたVectorからランダムな値を返す。"}
61+
response :random [{:keys [dictionary] :as params}]
62+
(let [random (:random dictionary)]
63+
(if (empty? random)
64+
(assoc params :error {:message "ランダム辞書が空です。"
65+
:type :fatal})
66+
(assoc params :response (rand-nth random)))))
67+
68+
(defmethod
69+
^{:doc "WhatResponderは入力 :input に対し、常に 'inputってなに?' と返す。"}
70+
response :what [{:keys [input] :as params}]
71+
(->> (str input "ってなに?")
72+
(assoc params :response)))
73+
74+
(defmethod
75+
^{:doc "Responderの指定がない、もしくは存在しないResponderを指定された場合、IllegalArgumentException例外を投げる。"}
76+
response :default [{:keys [responder]}]
77+
(throw (IllegalArgumentException.
78+
(str "Responder " responder " が存在しません。"))))

src/unmo/util.clj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(ns unmo.util
2+
(:require [clojure.java.io :as io]))
3+
4+
(defn conj-unique
5+
"コレクションcollに要素xがない場合のみconjを適用する。"
6+
[coll x]
7+
(if (some #{x} coll)
8+
coll
9+
(conj coll x)))
10+
11+
(defn file-exists? [filename]
12+
(.exists (io/as-file filename)))

src/unmo/version.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(ns unmo.version)
2+
3+
(def unmo-version "0.1.0")

0 commit comments

Comments
 (0)