1. shge

    Posted

    shge
Changes in title
+[Python] MeCab とマルコフ連鎖ライブラリ markovify を使い、文章を学習して自動生成する方法
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,340 @@
+# はじめに
+日本語形態素解析エンジン MeCab とマルコフ連鎖ライブラリ [markovify](https://github.com/jsvine/markovify) を使って文章を学習して自動生成します。
+[markovify](https://github.com/jsvine/markovify) の使い方についても説明します。
+
+# 結果
+とりあえず結果から。
+>**"メロスは、徐々に釣り上げられている。私は約束をさえ忘れていた。"**
+
+>**"メロスは村を出て仕事をはじめていた。メロスは村で結婚式を挙げる。早いほうがよかろう。"**
+
+>**"メロスは悪びれずに答えた。「この短刀で何をするのだ。メロスは、夢見心地でいた。"**
+
+>**"メロスは、きっと佳い夫婦になるだろう。そうして質問を重ねた。眼が覚めたのだ。私は王の前に引き出された。"**
+
+>**"メロスの懐中からは短剣が出て仕事をはじめた。"**
+
+もうなんかツッコミどころ満載ですねw
+
+# マルコフ連鎖とは?
+
+こちらの図がわかりやすいですね。
+https://omedstu.jimdo.com/2018/05/06/マルコフ連鎖による文書生成/
+
+![](https://i.imgur.com/UpCKbNX.png)
+「は→トマト」「は→休み」
+「が→好き」「が→ほしい」
+などを保存して、
+
+![](https://i.imgur.com/HleFiS0.png)
+ランダムに生成します。
+
+今回はコードを簡単にするために [markovify](https://github.com/jsvine/markovify) というライブラリを使用します。
+
+# 必要なもの
+
+- Python
+- [MeCab](https://taku910.github.io/mecab/)
+- [mecab-python3](https://github.com/SamuraiT/mecab-python3), [SWIG](https://github.com/swig/swig)
+- [markovify](https://github.com/jsvine/markovify)
+
+```sh
+$ brew install python
+$ brew install mecab mecab-ipadic
+$ brew install swig
+$ pip install mecab-python3
+$ pip install markovify
+```
+`pip` がエラーなら `pip3`
+
+
+# markovify の基本的な使い方
+とりあえず下にあるコードをコピーすればできるので、この章は読み飛ばしても構いません。
+詳細は https://github.com/jsvine/markovify 参照。
+
+---
+
+1. **モデルの生成**
+2. **モデルをもとにテキスト生成**
+
+という流れになります。
+
+## モデルの生成
+
+```py
+text_model = markovify.Text(text)
+```
+
+##### 改行で区切り1行を1文として認識する
+
+```py
+text_model = markovify.NewlineText(text)
+```
+
+##### マルコフ連鎖に使う単語の連結を増やす(デフォルトは2)
+
+```py
+text_model = markovify.Text(text, state_size=3)
+```
+
+#### 指定単語から始まる文の生成
+```py
+text_model = markovify.make_sentence_with_start(beginning=start)
+```
+
+## テキスト生成
+
+```py
+print(text_model.make_sentence())
+```
+
+##### 複数文の生成
+
+```py
+for _ in range(5):
+ print(text_model.make_sentence())
+```
+
+##### 140字を超えない文の生成
+
+```py
+print(text_model.make_short_sentence(140))
+```
+
+##### 文章が短かったりしてエラーが出るとき
+
+```py
+print(text_model.make_short_sentence(tries=100))
+```
+
+
+## モデルの合成
+
+```py
+model_a = markovify.Text(text_a)
+model_b = markovify.Text(text_b)
+
+model_combo = markovify.combine([model_a, model_b], [1.5, 1])
+```
+
+`[1.5, 1]` は各モデルの比重。デフォルトはすべて等しい(`[1, 1, ...]`)
+
+
+# コード
+
+```app.py
+import markovify
+import MeCab
+
+# Load file
+text_file = open("input.txt", "r")
+text = text_file.read()
+
+# Parse text using MeCab
+parsed_text = MeCab.Tagger('-Owakati').parse(text)
+
+# Build model
+text_model = markovify.Text(parsed_text, state_size=2)
+
+# Output
+for _ in range(10):
+ sentence = text_model.make_short_sentence(100, 20, tries=100).replace(' ', '')
+ print(sentence)
+```
+
+# 学習データ
+
+上のファイルを保存したのと同じディレクトリに `input.txt` という名前で保存します。
+
+今回は、青空文庫のみんな大好き『走れメロス』を使用しました。
+https://www.aozora.gr.jp/cards/000035/files/1567_14913.html
+スクレイピングするのも面倒だったので手動でコピー&ペーストしました。
+
+その時にルビ(ふりがな)が邪魔だったのでブラウザのコンソールでこれを実行:
+
+```js
+Array.from(document.querySelectorAll('rt')).forEach(el => el.remove());
+```
+
+# 結果
+
+1番上の結果。
+
+>**"メロスは、徐々に釣り上げられている。私は約束をさえ忘れていた。"**
+
+>**"メロスは村を出て仕事をはじめていた。メロスは村で結婚式を挙げる。早いほうがよかろう。"**
+
+>**"メロスは悪びれずに答えた。「この短刀で何をするのだ。メロスは、夢見心地でいた。"**
+
+>**"メロスは、きっと佳い夫婦になるだろう。そうして質問を重ねた。眼が覚めたのだ。私は王の前に引き出された。"**
+
+>**"メロスの懐中からは短剣が出て仕事をはじめた。"**
+
+---
+
+あれ、これすべての文が「メロス」から始まっている…?
+
+これは文章全体が1文として認識されているからですね。
+英語は `.` を文の切れ目と認識しますが日本語の `。` は認識されないからのようです。
+
+# 修正
+
+```markov.py
+import logging
+import markovify
+import MeCab
+import re
+
+logger = logging.getLogger(__name__)
+fmt = "%(asctime)s %(levelname)s %(name)s :%(message)s"
+logging.basicConfig(level=logging.DEBUG, format=fmt)
+
+# Toggle test_sentence_input
+test_sentence_input = markovify.Text.test_sentence_input # Stash
+def disable_test_sentence_input():
+ def do_nothing(self, sentence):
+ return True
+ markovify.Text.test_sentence_input = do_nothing
+def enable_test_sentence_input():
+ markovify.Text.test_sentence_input = test_sentence_input
+
+def format_text(t):
+ t = t.replace(' ', ' ') # Full width spaces
+ # t = re.sub(r'([。.!?…]+)', r'\1\n', t) # \n after !?
+ t = re.sub(r'(.+。) (.+。)', r'\1 \2\n', t)
+ t = re.sub(r'\n +', '\n', t) # Spaces
+ t = re.sub(r'([。.!?…])\n」', r'\1」 \n', t) # \n before 」
+ t = re.sub(r'\n +', '\n', t) # Spaces
+ t = re.sub(r'\n+', r'\n', t).rstrip('\n') # Empty lines
+ t = re.sub(r'\n +', '\n', t) # Spaces
+ return t
+
+def parse_text(filepath):
+ file = open(filepath, 'r').read()
+
+ parsed_text = ''
+ for line in file.split("\n"):
+ parsed_text = parsed_text + MeCab.Tagger('-Owakati').parse(line)
+ return parsed_text
+
+def build_model(text, format=True, state_size=2):
+ """
+ format=True: Fast.
+ format=False: Slow. Funnier(?)
+ """
+ if format is True:
+ logger.info('Format: True')
+ return markovify.NewlineText(format_text(text), state_size)
+ else:
+ logger.info('Format: False')
+ disable_test_sentence_input()
+ text = markovify.Text(text, state_size)
+ enable_test_sentence_input()
+ return text
+
+def make_sentences(text, start=None, max=300, min=1, tries=100):
+ if start is (None or ''): # If start is not specified
+ for _ in range(tries):
+ sentence = str(text.make_sentence()).replace(' ', '')
+ if sentence and len(sentence) <= max and len(sentence) >= min:
+ return sentence
+ else: # If start is specified
+ for _ in range(tries):
+ sentence = str(text.make_sentence_with_start(beginning=start)).replace(' ', '')
+ if sentence and len(sentence) <= max and len(sentence) >= min:
+ return sentence
+```
+
+```learn.py
+"""
+python learn.py <filename> [format] [max_chars] [min_chars]
+filename: Do not include .txt or .json etc.
+"""
+
+import markov
+import logging
+import sys
+from distutils.util import strtobool
+
+logger = logging.getLogger(__name__)
+fmt = "%(asctime)s %(levelname)s %(name)s :%(message)s"
+logging.basicConfig(level=logging.DEBUG, format=fmt)
+
+args = sys.argv
+
+print('Usage: python learn.py <filename> [format] [max_chars] [min_chars]')
+
+try:
+ filename = args[1]
+except IndexError:
+ print('ERROR: filename is required. (e.g. "sample")')
+ sys.exit()
+
+format = bool(strtobool(args[2])) if args[2:3] else True
+max_chars = int(args[3]) if args[3:4] else 70
+min_chars = int(args[4]) if args[4:5] else 25
+
+
+"""
+1. Load text -> Parse text using MeCab
+"""
+parsed_text = markov.parse_text('data/' + filename + '.txt')
+logger.info('Parsed text.')
+
+
+"""
+2. Build model
+"""
+text_model = markov.build_model(parsed_text, format=format, state_size=2)
+logger.info('Built text model.')
+
+json = text_model.to_json()
+open('data/' + filename + '.json', 'w').write(json)
+
+# Load from JSON
+# json = open('input.json').read()
+# text_model = markovify.Text.from_json(json)
+
+
+"""
+3. Make sentences
+"""
+try:
+ for _ in range(10):
+ sentence = markov.make_sentences(text_model, start='', max=max_chars, min=min_chars)
+ logger.info(sentence)
+except KeyError:
+ logger.error('KeyError: No sentence starts with "start".')
+ logger.info('If you set format=True, please change "start" to another word.')
+ logger.info('If you set format=False, you cannot specify "start".')
+```
+
+めっちゃ長くなってしまいましたね…
+`markov.py`, `learn.py` を保存し、`filename.txt` にテキストデータを入れて、
+
+```sh
+$ python learn.py filename
+```
+
+で実行できます。
+これで「メロス」から始まる文以外も生成できるようになりました。
+
+`learn.py` 下から6行目の `start=''` の `''` に単語を入れると、その単語から始まる文章を生成できます。
+試しに「私」を入れてみた結果:
+
+>**"私は、人質としてここにいる!」と、岩の裂目から滾々と、わしの孤独がわからぬ。"**
+
+>**"私は精一ぱいに鳴り響くほど音高くメロスの懐中からは短剣が出て仕事をはじめた。"**
+
+>**"私は、人を殺して、真紅の心臓をお目に殺して、はじめて君を、信ずる事が出来ぬのだ。"**
+
+# あとがき
+
+LINE Bot と組み合わせると自動生成した文章で返信してくれる Bot も作れます。
+https://qiita.com/shge/items/06169220a8f55e2ed861
+
+このマルコフ連鎖は文章をつなぎ合わせていっているだけで、本当に学習しているかというと微妙なので、今度はディープラーニングとかやってみたいですね。
+
+# 参考
+https://qiita.com/kakakaya/items/38042e807f3410b88b2d
+https://stackoverflow.com/questions/13125817/how-to-remove-elements-that-were-fetched-using-queryselectorall