1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python + Janomeでマルコフ連鎖人工無脳(3)尾崎放哉自由律俳句ジェネレータ

Last updated at Posted at 2020-09-29

#前書き
前々回
Python + Janomeでマルコフ連鎖人工無脳(1)Janome入門
前回
[Python + Janomeでマルコフ連鎖人工無脳(2)マルコフ連鎖入門]
(https://qiita.com/GlobeFish/items/17dc25f7920bb580d298)

いよいよ本格的に文章生成を実装します。
文章生成に使うネタはなんでもよいのですが、あまり理路整然としすぎた文章だと
支離滅裂さが際立ってしまう印象です。ふわっとハイコンテクストで、程よい長さが望ましいのです。
そこで、今回は尾崎放哉の自由律俳句を用意してみました。「咳をしても一人」の方です。

#データ入力
まず、テキストファイルを用意します。今回はこちらを用意しました。
尾崎放哉選句集(青空文庫)

DLして、テキストファイルを同じディレクトリに入れます。
中身はこんな感じです。

尾崎放哉選句集
尾崎放哉

青空文庫版まえがき

 このテキスト・ファイルには、種田山頭火と並んでいわゆる自由律俳句を代表する俳人、尾崎放哉(おざき・ほうさい。一八八五―一九二六)の作品を年代を追って並べた。放哉の句作は早く中学時代に始まっており、四一歳で死去するまでの足どりを十の時期に区分してある。
 ここに掲載したのは、もとより放哉の句すべてではなく、ごく一部にすぎない。選択にあたっては、若い人々に読まれることを願い、できるだけ平明なものに絞った。また、各章のはじめにはその時期の放哉についての簡単なコメントをつけてある。
 放哉の句は表記が異なって公表されているものが少なくない。デジタル化にあたり、『尾崎放哉句集』(彌生書房)『尾崎放哉全句集』(春秋社)を底本とし、表記が異なるものは双方を掲載した。( )付きの句の表記は『尾崎放哉全句集』に基づく。〈編集―青空文庫・浜野〉

[中学時代]
 尾崎放哉は、明治一八(一八八五)年一月二〇日、鳥取県邑美郡(現鳥取市)吉方町に父尾崎信三、母なかの次男として生まれた。本名秀雄。明治三〇(一八九七)年、県立第一中学校に入学。句作はこの頃始まった。

きれ凧の糸かかりけり梅の枝

水打つて静かな家や夏やなぎ

木の間より釣床見ゆる青葉かな

よき人の机によりて昼ねかな

露多き萩の小家や町はづれ

寒菊や鶏を呼ぶ畑のすみ

欄干に若葉のせまる二階かな

病いへずうつうつとして春くるる

自由律俳句だけ取り出して配列に格納するような関数を書きました。
要するに、句点終わりでも[]書きでもない文章を掬います。
(ヘッダーとフッターの一部も吸収されてしまうのが面倒だったので、これだけは元ファイルから手動で消しました)

import re

def haiku_reader(textfile):
    f = open(textfile, 'r')
    pre_haiku_set = [line.rstrip('\n') for line in f.readlines() if line != '\n']

    pre_haiku_set2 = [line for line in pre_haiku_set if re.findall(r'[.*|(.*',line) == []]
    haiku_set = [line + '.' for line in pre_haiku_set2 if re.findall(r'.*?。',line) == []]
    return haiku_set

pre_haiku_setは文末の改行を削除した文章群の配列です。
pre_haiku_set2は[]書き、()書きの含まれない文章を格納しています。
最後にhaiku_setに句読点の含まれない文章をまとめて完成です。あとで終わりの合図があると便利なので、ここで俳句のおしまいに'.'をつけています。
haiku_setの中身はこんな感じです。

['きれ凧の糸かかりけり梅の枝.', '水打つて静かな家や夏やなぎ.', '木の間より釣床見ゆる青葉かな.', 'よき人の机によりて昼ねかな.', '露多き萩の小家や町はづれ.', '寒菊や鶏を呼ぶ畑のすみ.', '欄干に若葉のせまる二階かな.', '病いへずうつうつとして春くるる.', '行春や母が遺愛の筑紫琴.',
(中略)
'渚白い足出し.', '貧乏して植木鉢並べて居る.', '霜とけ鳥光る.', 'あついめしがたけた野茶 
屋.', '森に近づき雪のある森.', '肉がやせて来る太い骨である.', '一つの湯呑を置いてむせてゐる.', 'やせたからだを窓に置き船の汽笛.', 'すつかり病人になつて柳の糸が吹かれる.', '春の山のう
しろから烟が出だした.']

ついでにこれらをバラバラにして格納した配列を作る関数も定義しましょう。

def barashi(text):
    t = Tokenizer()
    parted_text = ''
    for haiku in haiku_reader(text):
        for token in t.tokenize(haiku):
            parted_text += str(token.surface)
            parted_text += '|'
    word_list = parted_text.split('|') 
    word_list.pop()
    return word_list

関数化した点以外では前章と同様です。これに関しては特筆することはありません。

#deque型入門
前回はN=1の単純マルコフ連鎖を実装しましたが、今回は一般化してみましょう。
N階を実装するために、Pythonの標準ライブラリであるcollectionsモジュールのdeque型を使います。これはリストよりもキューやスタックに適したデータ構造で、両端からの挿入や削除が容易にできる優れものです。

from collections import deque

queue = deque([9,9],3)

for i in range(10):
    print(queue)
    queue.append(i)

deque()でdequeオブジェクトを生成できます。
第1引数には初期値を、第2引数には最大要素数を指定できます。最大要素数を指定した場合、追加した側と逆側から要素が捨てられます。
上のコードを実行してみましょう。

deque([9, 9], maxlen=3)
deque([9, 9, 0], maxlen=3)
deque([9, 0, 1], maxlen=3)
deque([0, 1, 2], maxlen=3)
deque([1, 2, 3], maxlen=3)
deque([2, 3, 4], maxlen=3)
deque([3, 4, 5], maxlen=3)
deque([4, 5, 6], maxlen=3)
deque([5, 6, 7], maxlen=3)
deque([6, 7, 8], maxlen=3)

#N階マルコフ連鎖実装(辞書編)
N個つなぎの単語群と、その後続の単語を対応させる辞書を作る関数を定義しましょう。

def dictionary_generator(text,order):
    dictionary = {}
    queue = deque([],order)
    word_list = barashi(text)
    for word in word_list:
        if len(queue) == order and '.' not in queue:
            key = tuple(queue)
            if key not in dictionary:
                dictionary[key] = []
                dictionary[key].append(word)
            else:
                dictionary[key].append(word)
        queue.append(word)
    
    return dictionary

基本的な構造は、辞書のキーがdequeになっただけで前章と変わりません。
ただし、辞書のキーはhashableでないとならない、という制約があるので
項目を追加するときにhashableなtupleに変換しています。
N=2の場合の辞書の中身はこんな感じです。

{('きれ', '凧'): ['の'], ('凧', 'の'): ['糸'], ('の', '糸'): ['かかり', 'が'], ('糸', 'かかり'): ['けり'], ('かかり', 'けり'): ['梅'], ('けり', '梅'): ['の'], ('梅', 'の'): ['枝'], ('の', '枝'): ['.'], ('水', '打つ'): ['て'], ('打つ', 'て'): ['静か'], ('て', '静か'): ['な'], ('静か', 'な'): ['家'], 
(後略)

#N階マルコフ連鎖実装(文章生成編)
いよいよ文章生成関数です。

def text_generator(dictionary,order):
    start = random.choice(list(dictionary.keys()))
    t = Tokenizer()
    token = list(t.tokenize(start[0]))
    part_of_speech = str(token[0].part_of_speech)
    while re.match(r'名詞|形容詞|感動詞|連体詞',part_of_speech) == None:
        start = random.choice(list(dictionary.keys()))
        token = list(t.tokenize(start[0]))
        part_of_speech = str(token[0].part_of_speech)
    now_word = deque(start,order)
    sentence = ''.join(now_word)
    for i in range (1000):
        if now_word[-1] == '.':
            break
        elif tuple(now_word) not in dictionary:
            break
        else:
            next_word = random.choice(dictionary[tuple(now_word)])
            now_word.append(next_word)
            sentence += next_word
    return sentence

===========以下注釈===========

def text_generator(dictionary,order):
    start = random.choice(list(dictionary.keys()))
    t = Tokenizer()
    token = list(t.tokenize(start[0]))
    part_of_speech = str(token[0].part_of_speech)
    while re.match(r'名詞|形容詞|感動詞|連体詞',part_of_speech) == None:
        start = random.choice(list(dictionary.keys()))
        token = list(t.tokenize(start[0]))
        part_of_speech = str(token[0].part_of_speech)
    now_word = deque(start,order)
    sentence = ''.join(now_word)

書き出しは先ほど作った辞書のキーからランダムに一つ持ってくることにします。
ただし、助詞から始まったりすると文章っぽくなくて格好がつかないので、
名詞か形容詞か感動詞か連体詞はじまりの書き出しが出てくるまでwhile文で引き直すことにします。
(ちなみに、元の歌集のほとんどが名詞始まりの俳句でした。)
tokenにはjanome.tokenizer.Tokenオブジェクト化した冒頭の単語が入っており、
token[0].part_of_speechで冒頭の単語の品詞を取得しています。
いい感じの書き出し配列を持ってこられたら、それをnow_wordキューに、
結合させた文字列をsentenceに格納します。

    for i in range (1000):
        if now_word[-1] == '.':
            break
        elif tuple(now_word) not in dictionary:
            break
        else:
            next_word = random.choice(dictionary[tuple(now_word)])
            now_word.append(next_word)
            sentence += next_word
    return sentence

・'.'まで来たらおしまいにする
・後続の単語が見つからなかったらおしまいにする
制約を初めに据えています。後半部分は後続の単語をランダムで選んでsentenceに付け加えているだけで、前章と同じです。

#まとめ

from collections import deque
from janome.tokenizer import Tokenizer
import re
import random


def haiku_reader(textfile):
    f = open(textfile, 'r')
    pre_haiku_set = [line.rstrip('\n') for line in f.readlines() if line != '\n']

    pre_haiku_set2 = [line for line in pre_haiku_set if re.findall(r'[.*|(.*',line) == []]
    haiku_set = [line + '.' for line in pre_haiku_set2 if re.findall(r'.*?。',line) == []]
    return haiku_set


def barashi(text):
    t = Tokenizer()
    parted_text = ''
    for haiku in haiku_reader(text):
        for token in t.tokenize(haiku):
            parted_text += str(token.surface)
            parted_text += '|'
    word_list = parted_text.split('|') 
    word_list.pop()
    return word_list

def dictionary_generator(text,order):
    dictionary = {}
    queue = deque([],order)
    word_list = barashi(text)
    for word in word_list:
        if len(queue) == order and '.' not in queue:
            key = tuple(queue)
            if key not in dictionary:
                dictionary[key] = []
                dictionary[key].append(word)
            else:
                dictionary[key].append(word)
        queue.append(word)
    
    return dictionary


def text_generator(dictionary,order):
    start = random.choice(list(dictionary.keys()))
    t = Tokenizer()
    token = list(t.tokenize(start[0]))
    part_of_speech = str(token[0].part_of_speech)
    while re.match(r'名詞|形容詞|感動詞|連体詞',part_of_speech) == None:
        start = random.choice(list(dictionary.keys()))
        token = list(t.tokenize(start[0]))
        part_of_speech = str(token[0].part_of_speech)
    now_word = deque(start,order)
    sentence = ''.join(now_word)
    for i in range (1000):
        if now_word[-1] == ".":
            break
        elif tuple(now_word) not in dictionary:
            break
        else:
            next_word = random.choice(dictionary[tuple(now_word)])
            now_word.append(next_word)
            sentence += next_word
    return sentence


text = "ozaki_hosai_senkushu.txt"
order = 2
dictionary = dictionary_generator(text,order)

print(text_generator(dictionary,order))
print(text_generator(dictionary,order))
print(text_generator(dictionary,order))

N=3の実行結果

火の顔あげる.
提灯が火事にとぶ也河岸の霧.
窓あけて居る朝の女にしじみ売.

かなりそれっぽい!とぬか喜びしたところ2つ目と謎の3つめが本物だったのでびっくりしました。
以下はN=2です。このへんがオリジナリティの限界かな。

前にす.
処へ乞食が来た顔を火鉢の上にのつける.
大きな石塔の下で死んでゐる.

これも3つめは本物の一部でした。わ、わかんねぇ~

#ハマりどころ

from janome.tokenizer import Tokenizer

t = Tokenizer()
s = "文章"
print(type(t.tokenize(s)))
#<class 'generator'>

t.tokenize()はデフォルトでgeneratorクラスです。
Janomeについての諸ドキュメントを見たところ、
・デフォルトではリスト
・引数streamをTrueにすることでgeneratorになる
とのことでしたが、最近streamがなくなってはじめからgeneratorになったようです。
リストにするにはlist()のなかにいれてやればOKです。

#余談
折角なので何個か羅列してみます。

こんなよい月を一人を投げ出す.
暗く垂れ大きな蟻が畳をはつてる.
酒のまぬ身は葛水のつめたさよ.
柘榴が口あけぬ蜆死んで庭淋し.
つめたい風の耳二つかたくついてる.
屋根が重たい.

わりと本物になってしまいましたね……
いろいろ生成してみましたが、個人的には

鶏なきて海へ放つ.

がヒットでした。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?