[Python3] janomeとmarkovifyを使った文章の自動生成

# はじめに
Pure Pythonで書かれた日本語形態素解析エンジンJanomeと、マルコフ連鎖ライブラリmarkovifyを使って、日本語の文章を学習して自動生成します。

基本的には
[markovifyで日本語の文章を学習して、マルコフ連鎖により文章生成を行う](https://qiita.com/kakakaya/items/38042e807f3410b88b2d)
をもとにさせていただいています。

Python久しぶりに触ったのでかなりガバガバです。ご了承ください。

# 背景・目的
実はmarkovifyを使用した日本語文章の学習と自動生成は先例が1つならずあるのですが、それらは大抵MeCabを使用したもので、その導入の関係上Windows環境や一部の仮想環境では些か手間がかかります([Herokuとか](https://qiita.com/kenchin110100/items/6f1c84ac8858525fffc5))。

その点Janomeは導入がWindowsでも容易で、実際に自動生成への利用法を紹介した記事も[やはりある](https://omedstu.jimdofree.com/2018/05/06/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96%E3%81%AB%E3%82%88%E3%82%8B%E6%96%87%E6%9B%B8%E7%94%9F%E6%88%90/)のですが、markovifyとJanomeを併用したものは(ニッチ過ぎて)見当たりませんでした。markovifyを使ったほうがお手軽に生成文の自然さを高められますので、できれば使用したいところです。

そこで今回、両者を併用して文章を生成できるようにしてみたため、メモ代わりに置いておきます。まぁ、併用したくなるような人は、自力で書き換え可能な気はするので、本当にメモ程度ですが……。

# 準備
* Python (3.8.1)
* Janome (0.3.10)
* markovify (0.8.0)

janomeもmarkovifyも``pip install``で導入可能です。(環境によっては``pip3``)

# コード
まずは全体。textGen部分は[参考文献1](https://qiita.com/kakakaya/items/38042e807f3410b88b2d)を半ば以上流用しています。

```py:janomeGen.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from janome.tokenizer import Tokenizer
import markovify

def split(text):
    # 改行、スペース、問題を起こす文字の置換
    table = str.maketrans({
        '\n': '',
        '\r': '',
        '(': '（',
        ')': '）',
        '[': '［',
        ']': '］',
        '"':'”',
        "'":"’",
    })
    text = text.translate(table)
    t = Tokenizer()
    result = t.tokenize(text, wakati=True)
    # 1形態素ずつ見ていって、間に半角スペース、文末の場合は改行を挿入
    splitted_text = ""
    for i in range(len(result)):
        splitted_text += result[i]
        if result[i] != '。' and result[i] != '！' and result[i] != '？':
            splitted_text += ' '
        if result[i] == '。' or result[i] == '！' or result[i] == '？':
            splitted_text += '\n'
    return splitted_text

def textGen(file):
    f = open(file, 'r', encoding="utf-8")
    text = f.read()
    sentence = None
    while sentence == None: # 素材によっては空の文章が生成されることがあるので、その対策
        # テキストを処理できる形に分割
        splitted_text = split(text)

        # モデルの生成
        text_model = markovify.NewlineText(splitted_text, state_size=3)

        # モデルを基にして文章を生成
        sentence = text_model.make_sentence()   
    
    # 学習データの保存
    with open('learned_data.json', 'w') as f:
        f.write(text_model.to_json())
    
    # データを使いまわす場合
    """
    with open('learned_data.json') as f:
        text_model = markovify.NewlineText.from_json(f.read())
    """

    # 結合された一連の文字列として返す
    return ''.join(sentence.split())
```

以下、順番に見ていきます。

##テキストの下処理

```python
    table = str.maketrans({
        '\n': '',
        '\r': '',
        '(': '（',
        ')': '）',
        '[': '［',
        ']': '］',
        '"':'”',
        "'":"’",
    })
    text = text.translate(table)
```

markovifyが読み取れるよう、一部の文字を置換しておきます。改行とスペースはそれぞれ文章の区切りと単語の区切りを示すために使うので一旦削除(英文交じりの日本文などはうまく処理できなくなってしまいますが、今回は無視)。

また、markovifyの動作に悪影響を及ぼす'bad characters'も無害な全角文字に置換しておきます。(markovify v0.7.2からはmarkovify.Textのwell_formedパラメータで、bad charactersを含むセンテンスを無視するかどうかを指定できますが、丸ごと無視してしまうのはもったいないので事前に置換で済ませています)

## テキストの分割

```python
t = Tokenizer()
    result = t.tokenize(text, wakati=True)
    splitted_text = ""
    for i in range(len(result)):
        splitted_text += result[i]
        if result[i] != '。' and result[i] != '！' and result[i] != '？':
            splitted_text += ' '
        if result[i] == '。' or result[i] == '！' or result[i] == '？':
            splitted_text += '\n'
```
やっていること自体は参考記事1とほぼほぼ同じなのでそちらを参照していただいたほうが正確です。

JanomeのTokenizerでこのようにtokenizeすると、形態素で分けたリストとして返してくれます。
「私はリンゴを一つ食べる。」なら ```['私', 'は', 'リンゴ', 'を', '一つ', '食べる', '。']```という感じ。形態素の本体のみ欲しい人にはMeCabよりもお手軽で便利。

今回はmarkovifyが読めるように形態素を1個ずつ読んで間を半角スペースで分け、文末に来たら改行で区切ります(英文と形を揃える感じ)。参考記事では句点でのみ切っていましたが、今回は！と？でも切るようにしました。読点をどう区切るかは好みによりますが、ここでは1つの単語として分けました。英語同様の形にする場合、if文のところを

```python
        if i+1 < len(result):
            if result[i] != '。' and result[i] != '！' and result[i] != '？' and result[i+1] != '、':
                splitted_text += ' '
            if result[i] == '。' or result[i] == '！' or result[i] == '？':
                splitted_text += '\n'
        else:
            if result[i] != '。' and result[i] != '！' and result[i] != '？':
                splitted_text += ' '
            if result[i] == '。' or result[i] == '！' or result[i] == '？':
                splitted_text += '\n'
```
とか何とか書き換えるとうまくいくはずです。たぶん。

## 文章の生成

```python
def textGen(file):
    f = open(file, 'r', encoding="utf-8")
    text = f.read()
    sentence = None
    while sentence == None: # 素材によってはNoneが返ることがあるので、その対策
        # テキストを処理できる形に分割
        splitted_text = split(text)

        # モデルの生成
        text_model = markovify.NewlineText(splitted_text, state_size=3)

        # モデルを基にして文章を生成
        sentence = text_model.make_sentence()   
    
    # 学習データの保存
    with open('learned_data.json', 'w') as f:
        f.write(text_model.to_json())
    # 結合された一連の文字列として返す
    return ''.join(sentence.split())
```
今回は青空文庫ではないものからの生成のために書いていたので、そのための処理は省いて単純に読み込んでいます。markovifyの割と標準的な手順なので、それ以外はおおよそ参考文献1に準じています。

また、state_sizeや素材文の分量の関係で時々Noneが返ってしまうことがある(markovifyの[Issue#96](https://github.com/jsvine/markovify/issues/96), [Issue#22](https://github.com/jsvine/markovify/issues/22))ので、ここでは安易にNoneじゃないものを返すまで回しておきました。ある程度の文章量があれば無限ループにはならないと思います。

なお、make_sentenceのキーワード引数triesで試行回数を指定しておくことでもある程度対応可能です。(下のコード)

```python
    # テキストを処理できる形に分割
    splitted_text = split(text)

    # モデルの生成
    text_model = markovify.NewlineText(splitted_text, state_size=3)

    # モデルを基にして文章を生成
    sentence = text_model.make_sentence(tries=100)   
```

# 生成結果
テスト用に、青空文庫の[坊ちゃん](https://www.aozora.gr.jp/cards/000148/card752.html)から[delruby.exe](https://www.aokids.jp/others/delruby.html)を用いてルビを削除し、不要な部分を除いたものを基に生成してみました。
> * 一人不足ですがと考えてみると世の中はみんなこの生徒のようなものだ、虫の好かない奴が親切で、しかも上品だが、貧乏士族のけちん坊と来ちゃ仕方がないから、大きな声を出すもんだ。
* 溌墨の具合も至極よろしい、試してご覧なさいと、おれよりも下等だが、日本人はみな口から先へ免職になったら、よさそうな下宿を教えてくれるかも知れないから、為替で十円あげる。
* それも花の都の電車が通ってる所なら、野だは狼狽の気味で、はたで見ているときに来るかい」「そのマドンナさんが不たしかなのが、飛び起きると同時に忘れたようにうらなり君が眼に付く、途中をあるいていても眼がくらむ。
* 野だは時々山嵐に話しかけるが、山嵐の云う通りにした。
* 男はあっと小声に云ったが、やがて帰って来て言葉が出ないから、出すんだ。
* おれが戸を開けて中に居るんだ」「僕の前任者がやられたんだ。
* 歴史も教頭と同説だと云ってやった。
* 門口へ立ったなり中学校を教えろと云ったらあなたがおうちを持って教場へ出たら、山嵐は君それを引き込めるのかと驚ろいた。
* うらなり君に別れて、うちを出る時から、こんなに答えるんだろう。
* そうしておいて喧嘩を吹き懸ける男だ。

おおよそ目的は達せられているようです。

# あとがき
JanomeもMeCabも日本語形態素解析をしてくれるという意味では同様の機能を持つので、細かい書き換えのみで実装できました。Bot作成時などに活用できそうです。

# 参考
1. [markovifyで日本語の文章を学習して、マルコフ連鎖により文章生成を行う](https://qiita.com/kakakaya/items/38042e807f3410b88b2d)
2. [[Python] MeCab とマルコフ連鎖ライブラリ markovify を使い、文章を学習して自動生成する方法](https://qiita.com/shge/items/fbfce6b54d2e0cc1b382)
3. [マルコフ連鎖による文章生成 - 知識のサラダボウル](https://omedstu.jimdofree.com/2018/05/06/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96%E3%81%AB%E3%82%88%E3%82%8B%E6%96%87%E6%9B%B8%E7%94%9F%E6%88%90/)