赤飯にかかったアレ

雑多なメモ帳

太宰治の文章からボットを作る

 マルコフ連鎖の練習も兼ねて
マルコフ連鎖を使用した対話モデルを作って遊んでます。

(マルコフ連鎖とその学習のやり方は、ほぼ以下の書籍の写経です)

マルコフ連鎖とは

 マルコフ連鎖は確率過程の一つと考えていいのかな

  • 別名マルコフ過程
  • 確率過程を使って文章をつなぎ合わせて文を作る
  • マルコフ性(次に予測される状態が過去の状態に依存せず、現在の状態によってのみ決まる性質)を持つ確率過程のうち取り売る値が離散的なもの(マルコフ性を備えた確率過程を総称したマルコフ過程の中でも取る可能性のある値が連続的でなく離散的)これを特にマルコフ連鎖という・・・らしいよ

文の類似度を調べるN-gramの基本原理ってことでいいかな

 

次に来る文字を予測するLSTMでも試したけど、こっちの方が意味が通じる
パラメータ調整をミスってるのもあると思うけど、学習に時間かかりすぎるので却下

 

データの取得

今回は、いや、今回も

青空文庫に公開中の作品を作家別に一括ダウンロードできるサイトである

keison.sakura.ne.jp

こちらから太宰治の作品一覧をダウンロードしてマルコフ連鎖ように辞書化しました。

以下はソースコード

ZipDownload.py

"""
青空文庫に公開中の作品を作家別に一括ダウンロードできるサイト
から太宰治の作品一覧をダウンロードする。
"""
import os.path, urllib.request as req
import re, zipfile, sys

# 太宰治の作品一覧のダウンロード
local = 'dazai.zip'

if not os.path.exists(local):
print('開始')
req.urlretrieve(url, local)
print('終了')

これでzipファイルが取得できる。

zipファイルを解凍すると太宰治という名前のディレクトリができる。(本当は解凍しないで前処理したかった)

 

辞書を作る

解凍したディレクトリ内にあるテキストからヘッダーとフッダー、ルビや脚注を削除して文章をjanome形態素解析し、太宰治色に染まったはずの一つの辞書を作る。

そのソースコードがこれ

MakeDic.py

'''
青空文庫に公開中の作品を作家別に一括ダウンロードできるサイト
から太宰治の作品一覧をダウンロードし、展開したファイルを使用する

ファイルからヘッダーとフッダー、ルビや脚注を削除して
文章janome形態素解析する
'''
import os.path, urllib.request as req
from janome.tokenizer import Tokenizer
import os, re, json, sys, random

# マルコフ連鎖の辞書を作成
def make_dic(words):
tmp = ["@"]
dic = {}
for i in words:
word = i.surface
if word == "" or word == "\r\n" or word == "\n": continue
tmp.append(word)
if len(tmp) < 3: continue
if len(tmp) > 3: tmp = tmp[1:]
set_word3(dic, tmp)
if word == "":
tmp = ["@"]
continue
return dic

# 三要素のリストを辞書として登録
def set_word3(dic, s3):
w1, w2, w3 = s3
if not w1 in dic: dic[w1] = {}
if not w2 in dic[w1]: dic[w1][w2] = {}
if not w3 in dic[w1][w2]: dic[w1][w2][w3] = 0
dic[w1][w2][w3] += 1

# 文章を読み込む
def Read_Sentence(text):
# テキストの先頭にあるヘッダとフッタを削除
text = re.split(r'\-{5,}',text)[2]
text = re.split(r'底本:', text)[0]
text = text.strip()
# ルビを削除
text = text.replace('', '')
text = re.sub(r'.+?', '', text)
# テキスト内の脚注を削除
text = re.sub(r'[#.+?', '', text)
return text

# 辞書データの作成
person = '太宰治'
All_sakuhin_cnt = 0 # 作品数を数えるため
sakuhin_cnt = 0 # 作品数を数えるため
results =

file = 'dazai.json' # ファイルの作成

for sakuhin in os.listdir(person): # range(len(zipfile)): # os.listdir(ziplist):
print(sakuhin) # 解析中の作品
All_sakuhin_cnt += 1
sakuhin_file = person + '/' + sakuhin
try:
# 青空文庫Shift_JISファイルを読み込む
bindata = open(sakuhin_file, 'rb').read()
text = bindata.decode('shift_jis')
lines = Read_Sentence(text) # 形態素解析
results += lines
print('[解析成功]', sakuhin_file)

t = Tokenizer()
keyword = t.tokenize(text)
dic = make_dic(keyword)
json.dump(dic, open(file,"w", encoding="utf-8"))
sakuhin_cnt += 1

except Exception as error:
print('[解析失敗]', sakuhin_file, error)
continue
print('[全作品数]', All_sakuhin_cnt, '[解析できた作品数]', sakuhin_cnt)

print('モデルできた')

思ったより時間がかかった。MeCabでやった時は一瞬だったからかな。(ただし解析成功率はjanome方が良かった。MeCabは使えるようにしただけだからそれが差になったのかな?)

以下は結果のコンソール画面

 

[解析成功] 太宰治/小さいアルバム.txt

六月十九日.txt

[解析成功] 太宰治/六月十九日.txt

美男子と煙草.txt

[解析成功] 太宰治/美男子と煙草.txt

[全作品数] 250 [解析できた作品数] 228

モデルできた

 

会話する

できたので早速試します。太宰治のままでも良かったんだけど会話のたびに辞書を更新して成長するようになってます。

以下ソースコード

chatbot.py

'''
マルコフ連鎖を使用した対話モデル

マルコフ連鎖は確率過程の一つと考えていいのかな
確率過程を使って文章をつなぎ合わせて文を作る

マルコフ性(次に予測される状態が過去の状態に依存せず、現在の状態によってのみ決まる性質)
を持つ確率過程のうち取り売る値が離散的なもの
(マルコフ性を備えた確率過程を総称したマルコフ過程の中でも取る可能性のある値が連続的でなく離散的)
これを特にマルコフ連鎖という・・・らしいよ

文の類似度を調べるN-gramの基本原理ってことでいいかな

次に来る文字を予測するLSTMでも試したけど、こっちの方が意味が通じる
パラメータ調整をミスってるのもあると思うけど、学習に時間かかりすぎるので却下
'''

# coding:utf-8
from janome.tokenizer import Tokenizer
import os, re, json, random

# dict_file = 'bot_dic.json' # ゼロからbot用の辞書を作っていきたい場合はこちらを有効にしてdazai.jsonコメントアウトする。bot_dic.jsonは作られるから大丈夫
dict_file = 'dazai.json' # 太宰治bot用の辞書

dic = {} # global変数の使い方の練習
tokenizer = Tokenizer() # janome

# 辞書に単語を記録する
def register_dic(words):
global dic # global変数の使い方の練習
if len(words) == 0: return
tmp = ['@']
for i in words:
word = i.surface
if word == '' or word == '\r\n' or word == '\n': continue
tmp.append(word)
if len(tmp) < 3: continue
if len(tmp) > 3: tmp = tmp[1:]
set_word3(dic, tmp)
if word == '' or word == '?':
tmp = ['@']
continue
# 辞書を更新するごとにファイルへ保存
json.dump(dic, open(dict_file,'w', encoding='utf-8'))

# 三要素のリストを辞書として登録
def set_word3(dic, s3):
w1, w2, w3 = s3
if not w1 in dic: dic[w1] = {}
if not w2 in dic[w1]: dic[w1][w2] = {}
if not w3 in dic[w1][w2]: dic[w1][w2][w3] = 0
dic[w1][w2][w3] += 1

# 作文する
def make_sentence(head):
if not head in dic: return ''
ret =
if head != '@': ret.append(head)
top = dic[head]
w1 = word_choice(top)
w2 = word_choice(top[w1])
ret.append(w1)
ret.append(w2)
while True:
if w1 in dic and w2 in dic[w1]:
w3 = word_choice(dic[w1][w2])
else:
w3 = ''
ret.append(w3)
if w3 == '' or w3 == '' or w3 == '': break
w1, w2 = w2, w3
return ''.join(ret)

def word_choice(sel):
keys = sel.keys()
return random.choice(list(keys))

# チャットボットに返答させる
def make_reply(text):
# まず単語を学習する
if text[-1] != '': text += ''
words = tokenizer.tokenize(text)
register_dic(words)
# 辞書に単語があれば、そこから話す
for w in words:
face = w.surface
ps = w.part_of_speech.split(',')[0]
if ps == '感動詞':
return face + ''
if ps == '名詞' or ps == '形容詞':
if face in dic: return make_sentence(face)
return make_sentence('@')

# 辞書があれば最初に読み込む
if os.path.exists(dict_file):
dic = json.load(open(dict_file,'r'))

# メイン処理
# 入力に対して返答を返す処理
def main():
print('')
txt = 'chat_text'
while txt != 0:
txt = input('> ')
# 終了条件
if txt == 'exit' or txt == 'end' or txt == 'END' or txt == 'EXIT' or txt == '終了':
return
res = make_reply(txt)
print(res)
 
main()

今きずいたけどただコピペするとインデント崩れてひどいね

結果

こんな感じ

> はユーザーの入力内容です。

 

> こんにちは

こんにちは。

> 死

太宰といえば、太宰。

> どういうこと

ことになる。

> なに

どうも、僕より十も二十も二十も二十も上なんです。

> そうなんですか

んが、ふと私は少しずつおしゃべりになっておらない様子なんだ。

> 様子とは一体

様子でした。

> でしょうね

 先日、或る雑誌社の、若い記者をせき立てるようにも、男は、独《ひと》りで、やたらと仲間ぼめして、その悪口も笑ってしまうものだ。

> <>取りきれなかったようですね

ようです。

> だね

それこそ嬉々として遊びたわむれていました。

> グッドバイ

ひとごとじゃないかしら。

> あら女言葉

あら。

> あらあら

こんにちは。

> やあ

やあ。

> バイバイ

僕は、真直を見て、おいおい男泣きに泣いて、以《もっ》て新しい必死の発芽を踏みにじり、しかも、そのような因果関係でも無いのに、いきなり私を自動車に乗せ、上野駅に連れて行き、浮浪者をいじめます。

> 死とは

死とは知合いでもあるのでしょう。

> end

 

 

こんにちはという入力に対して同じ単語を返すのは学習できてなかったから、会話になってない部分も多々あるけど意外とうまく言ってる気がする結果になったので個人的に満足

 

 この記事で使用したソースコードはここ

github.com