MENU

マルコフ連鎖を用いて自動作文の作成

今回のプログラムではマルコフ連鎖を用いて、自動作文を作成していく。

マルコフ連鎖とは未来の状態が現在の状態によって決まるという確率過程のことで今回のプログラムに当てはめると、辞書型のオブジェクトに形態素解析した単語を保存していき、それを辞書として入力された文字から既存の文章を利用し、自動で文章を作成するというような形で現在の状態から未来を推測していくのがマルコフ連鎖である。

 

流れとしては

・辞書の作成(辞書型のオブジェクトに形態素解析した要素を追加していく)

・始点となる単語と辞書を使って作文する。

 

今回作成する辞書は前後の結びつきに注目して、3単語ずつ登録していく。

 

import MeCab

import os,json,random

 

dict_file = "markov_dict.json"

dic = {}

 

# 辞書への登録 --- (*1)

def regist_dic(wordlist):

    global dic

    w1 = ""

    w2 = ""

    

    # 要素が3未満の場合は、何もしない

    if len(wordlist) < 3 : return

    

    for w in wordlist :

        word = w[0]

        if word == "" or  word == "\r\n" or word == "\n" : continue

        # 辞書に単語を設定

        if w1 and w2 :

            set_dic(dic,w1, w2, word)

        # 文末を表す語のの場合、連鎖をクリアする

        if word == "。" or word == "?" or  word == "?" :

            w1 = ""

            w2 = ""

            continue

        # 次の前後関係を登録するために、単語をスライド

        w1, w2 = w2, word

    

    # 辞書を保存

    json.dump(dic, open(dict_file,"w", encoding="utf-8"))

 

# 辞書に単語を設定 --- (*2)

def set_dic(dic, w1, w2, w3):

    # 新しい単語の場合は、新しい辞書オブジェクトを作成

    if w1 not in dic : dic[w1] = {}

    if w2 not in dic[w1] : dic[w1][w2] = {}

    if w3 not in dic[w1][w2]: dic[w1][w2][w3] = 0

    # 単語の出現数をインクリメントする

    dic[w1][w2][w3] += 1

 

# 応答文の作成 --- (*3)

def make_response(word):

    res =

    

    # 「名詞」/「形容詞」/「動詞」は、文章の意図を示していることが多いと想定し、始点の単語とする。

    w1 = word

    res.append(w1)

    w2 = word_choice(dic[w1])

    res.append(w2)

    while True:

        # w1,w2の組み合わせから予想される、単語を選択

        if w1 in dic and w2 in dic[w1] : w3 = word_choice(dic[w1][w2])

        else : w3 = ""

        res.append(w3)

        # 文末を表す語の場合、作文を終了

        if w3 == "。" or w3 == "?" or  w3 == "?"  or w3 == "" :  break

        # 次の単語を選択するために、単語をスライド

        w1, w2 = w2, w3

    return "".join(res)

        

def word_choice(candidate):

    keys = candidate.keys()

    return random.choice(list(keys))

 

# メイン処理 --- (*4)

 

# 辞書がすでに存在する場合は、最初に読み込む

if os.path.exists(dict_file):

        dic = json.load(open(dict_file,"r"))

        

while True:

    # 標準入力から入力を受け付け、「さようなら」が入力されるまで続ける

    text = input("You -> ")

    if text == "" or text == "さようなら" :

        print("Bot -> さようなら")

        break

 

    # 文章整形

    if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"

    

    # 形態素解析

    tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")

    tagger.parse("")

    node =  tagger.parseToNode(text)

    

    # 形態素解析の結果から、単語と品詞情報を抽出

    wordlist =

    while node is not None:

        hinshi = node.feature.split(",")[0]

        if  hinshi not  in ["BOS/EOS"]:

            wordlist.append([node.surface,hinshi])

        node = node.next

    

    # マルコフ連鎖の辞書に登録

    regist_dic(wordlist)

 

    # 応答文の作成

    for w in wordlist:

        word = w[0]

        hinshi = w[1]

        # 品詞が「感動詞」の場合は、単語をそのまま返す

        if hinshi in [ "感動詞"] :

            print("Bot -> " + word)

            break

        # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す

        elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):

            print("Bot -> " + make_response(word))

            break

 

実行結果

f:id:hanamichi_sukusuku:20210125210935p:plain

You->が入力したもの

Bot->が自動作成された文章

猫や犬という単語を始点として辞書を元に文章を作成している。

「猫はどうですか?」と「猫は可愛い」と出力内容が違うのは動詩、形容詞からランダムに出力しているからである。このプログラムでは単語の意味や構文を考えて出力しているわけではないので作文の質を高めるには様々な改善を行う必要がある。それは前提においてプログラムを見ていく。

 

モジュールインポート

import MeCab
import os,json,random

 

jsonモジュールでは作成した辞書を外部に保存することで内容を更新するためにjson形式で辞書を保存するためや読み込むためにインポートする。

randomモジュールではランダムに値を取得するためにインポート。

 

辞書の保存名と辞書型のオブジェクトを定義

dict_file = "markov_dict.json"
dic = {}

 

 

辞書が既に存在している場合には、最初に辞書を読み込む

# 辞書がすでに存在する場合は、最初に読み込む
if os.path.exists(dict_file):
 dic = json.load(open(dict_file,"r"))

 

os.path.exists()で指定したファイル名が存在するかどうかの判定。

上記が存在すればjson.load(open(dict_file, "r"))でファイルを読み取り用で開き、json.loadで読み込む。

 

ループ処理内で関数を呼び出し辞書の作成や辞書を元に作成した文章を出力

while True:
  # 標準入力から入力を受け付け、「さようなら」が入力されるまで続ける 
   text = input("You -> ")
   if text == "" or text == "さようなら" :
    print("Bot -> さようなら")
    break

  # 文章整形
   if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"
 
   tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")
   tagger.parse("")
   node = tagger.parseToNode(text)
 
   # 形態素解析の結果から、単語と品詞情報を抽出
   wordlist =
   while node is not None:
    hinshi = node.feature.split(",")[0]
    if hinshi not in ["BOS/EOS"]:
     wordlist.append([node.surface,hinshi])
   node = node.next
 
  # マルコフ連鎖の辞書に登録
  regist_dic(wordlist)

   # 応答文の作成
   for w in wordlist:
     word = w[0]
     hinshi = w[1]
   # 品詞が「感動詞」の場合は、単語をそのまま返す
     if hinshi in [ "感動詞"] :
       print("Bot -> " + word)
       break
   # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す
     elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):
       print("Bot -> " + make_response(word))
       break

 

input()で入力を受け取り、さようならと入力された時処理を終了する。

if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"

ここでは入力された最後の文字が句読点やクエスチョンマークがない時、入力されたものに句読点を付与している。

tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")
tagger.parse("")
node = tagger.parseToNode(text)

ここで形態素解析を行う。

wordlist =
while node is not None:
  hinshi = node.feature.split(",")[0]
  if hinshi not in ["BOS/EOS"]:
   wordlist.append([node.surface,hinshi])
  node = node.next

 

変数hinshiに["BOS/EOS"]が含まれない時、リストに形態素解析した単語の表層形と品詞情報を追加。

 

regist_dic(wordlist)

マルコフ連鎖の辞書に登録するため単語と品詞情報を持ったリストをregist_dic()関数に渡す。

 

regist_dic()関数、辞書への登録、保存

def regist_dic(wordlist):
 global dic
 w1 = ""
 w2 = ""
 
 # 要素が3未満の場合は、何もしない
 if len(wordlist) < 3 : return
 
 for w in wordlist :
  word = w[0]
  if word == "" or word == "\r\n" or word == "\n" : continue
  # 辞書に単語を設定
  if w1 and w2 :
   set_dic(dic,w1, w2, word)
  # 文末を表す語のの場合、連鎖をクリアする
  if word == "。" or word == "?" or word == "?" :
   w1 = ""
   w2 = ""
   continue
  # 次の前後関係を登録するために、単語をスライド
  w1, w2 = w2, word
 
 # 辞書を保存
 json.dump(dic, open(dict_file,"w", encoding="utf-8"))

 

global 変数名とすることで関数内でグローバル変数として扱われるようになり、関数外で定義した変数の中身も更新されるようになる。

 

この関数の引数として受け取っているwordlistは入力された文章を形態素解析し、それぞれの単語と品詞が格納されているリストである。

if w1 and w2:の部分はif文の条件式では変数のみを指定すると文字列型なら空じゃなければTrueを返すことを利用している。そして、w1、w2どちらにも文字列が格納されている時、set_dic()関数を呼び出し、辞書に単語を設定していく。

 

w1, w2 = w2, wordでは一つの単語に対して前後3つまでの関係を登録するため一文字ずつずらし、for文とset_dic()関数で繰り返し辞書に登録していく。

最後にjson.dump()で作成した辞書を外部に保存する。外部に保存することで辞書の内容を更新し続けることができる。第一引数に保存する辞書を指定、第二引数で保存先(openでファイルを書き込み用で開きutf-8を指定している)

 

set_dic()関数、辞書に単語を設定

def set_dic(dic, w1, w2, w3):
  # 新しい単語の場合は、新しい辞書オブジェクトを作成
  if w1 not in dic : dic[w1] = {}
  if w2 not in dic[w1] : dic[w1][w2] = {}
  if w3 not in dic[w1][w2]: dic[w1][w2][w3] = 0
  # 単語の出現数をインクリメントする
  dic[w1][w2][w3] += 1

dicは辞書型のオブジェクトで今回自動作文に使用する辞書。

w1は入力された先頭の単語から始まり一つずつ後の単語にずれていく。w2は二番目に位置する単語から一つずつ後にずれていく。w3はregist_dic関数内のfor文で現在取得している単語。

入力 猫は可愛い。

w1 = ""

w2 = ""

regist_dic()関数内では初めこのように定義されており文字が格納されていないとif文で弾かれるのでset_dic()関数の引数は

dic, w1="猫", w2="は", w3="可愛い"このようになる。

 

次の処理では

dic, w1="は",w2="可愛い",w3="。"のようにずれていき、「。」や「?」などの文末を表す文字が出てきたら処理を終了する。

このような辞書が出来上がる。

f:id:hanamichi_sukusuku:20210125221605p:plain

一部をピックアップしてみると

'猫': {'は': {'可愛い': 5, 'どう': 3}}

'どう': {'です': {'か' : 7}}

今回のプログラムではこの辞書を元に自動作文されるのでこの辞書を作成した場合、猫と入力すると猫を始点として文章を作成するので「猫+は+(可愛いorどう)+です+か」という文章が出来上がる。可愛いorどうの部分はランダム。

 

 

while文に戻り、regist_dic()関数の次の処理へ

応答文の作成

for w in wordlist:
 word = w[0]
 hinshi = w[1]
 # 品詞が「感動詞」の場合は、単語をそのまま返す
 if hinshi in [ "感動詞"] :
  print("Bot -> " + word)
  break
 # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す
 elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):
  print("Bot -> " + make_response(word))
  break

 

辞書を作成、更新することができたので入力されたものから自動作文を作成していく。

入力したものを形態素解析し、単語と品詞情報を持ったwordlistから先頭の要素を取得し感動詞だった時そのままその単語を返し、名詞、形容詞、動詩かつ辞書にその単語が含まれている時、make_response()関数を呼び出し、辞書を元に作成した文章を出力する。

 

make_response()関数、応答文の作成

def make_response(word):
 res = []
 
 # 「名詞」/「形容詞」/「動詞」は、文章の意図を示していることが多いと想定し、始点の単語とする。
 w1 = word
 res.append(w1)
 w2 = word_choice(dic[w1])
 res.append(w2)
 while True:
  # w1,w2の組み合わせから予想される、単語を選択
  if w1 in dic and w2 in dic[w1] : w3 = word_choice(dic[w1][w2])
  else : w3 = ""
  res.append(w3)
  # 文末を表す語の場合、作文を終了
  if w3 == "。" or w3 == "?" or w3 == "?" or w3 == "" : break
  # 次の単語を選択するために、単語をスライド
  w1, w2 = w2, w3
 return "".join(res)

 

呼び出し元のfor文内で入力された文章から先頭の要素してif文で処理を終了しているので名詞、形容詞、動詞は文章の意図を示していることが多いと想定しているため始点の単語としている。

ここではresというリストに辞書を元に単語を追加していき最終的にリストの要素を連結して呼び出し元に返している。

w2=word_choice()dic[w1])ではmake_response()の引数である単語がキーとなるオブジェクトをword_choice()関数に渡して渡した単語に続く単語を取得している。

そして、w1、w2に格納された単語をresに追加。

その後、先頭と先頭から2番目の言葉から予想される単語をword_choice()関数で取得しresに追加していく。

最後にjoinメソッドでresに格納された要素を連結して呼び出し元に返す。

 

word_choice()関数、辞書から取得した要素からランダムに単語を返す

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

 

引数ではmake_response()関数からキーとなる単語の値となる要素が渡している。

その要素からkeys()メソッドでキーを全て取得し、list()でリストに変換、random.choice()メソッドに渡すことでキーの中からランダムな単語を呼び出し元に返す。