43
40

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 5 years have passed since last update.

マルコフ連鎖を使って名(迷?)言を作らせてみた

Last updated at Posted at 2018-06-29

世間からいくら拍手喝采をあびようとも、結局、自分から逃げることは、何の意味もない。
言葉の選択一つで、しばしば運命に出会う。

うーん、実に深いですね...
え?意味はなんだって?各人に判断を委ねます(投げやり)。

察しの良い読者は既にお気づきかもしれませんが、これはマルコフ連鎖という手法を用いて生成された名言になります。特に意味はありません。

マルコフ連鎖とは

文章を形態素と呼ばれる意味を持つ最小単位に分けて、文脈に合わせた形態素をピックアップして新たな文を生成するといった流れです。
pythonには形態素解析エンジンであるMeCabが存在し、分割することが難しい日本語でもOK!

MeCabを動かした結果がこちらです。

terminal
$mecab
すもももももももものうち。
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
。	記号,句点,*,*,*,*,。,。,。

参考になったサイト

実装にあたり、先人の方の知恵を9割ほどお借りしました。

o-tomox/TextGeneratorのコードが2系だった為、適宜3系に修正しました。

プロセス

実際に手を動かしたのは名言のスクレイピングと生成する文の数を任意に変更できるようにしただけです。マルコフ連鎖の雰囲気はつかめたかな?

  1. scraping.py...名言のスクレイピングを行い、テキストファイルにまとめる
  2. PrepareChain.py...マルコフ連鎖のチェーンを作成し、DBファイルを出力する
  3. GenerateText.py...マルコフ連鎖を用いて適当な文章を自動生成する

名言をスクレイピング

最初に元となる名言たちを用意しましょう。
癒しツアー - 人生の名言・格言から全15ページ分の名言を取得しました。ありがとうございます。
以下が名言を取得するコードです。

scraping.py
import os, re, bs4, requests

with open('meigen.txt', 'a') as f:
	for i in range(1, 16):
		url = 'http://iyashitour.com/meigen/theme/life/{}'.format(i)
		res = requests.get(url)
		res.raise_for_status()
		
		soup = bs4.BeautifulSoup(res.text)
		# ダウンロードしたデータを表示
		# print(soup)

		p_elem = soup.find_all('p')
		meigens_row = ""
		flg = False
		for meigen in p_elem:
			meigen = str(meigen)
			if meigen == "<p> </p>" and flg == False:
				flg = True
				continue
			elif meigen == "<p> </p>" and flg == True:
				flg = False
			if flg == True:
				meigens_row += meigen + '\n'
				
		# bタグの排除
		meigens = re.sub(r'<(/)?\w+>?', '', meigens_row)
		#「」の排除
		meigens = re.sub(r'(「|」)', '', meigens)
		# 例外の開始文字から始まる文字列を排除
		meigens = re.sub(r'((\(|【|-|※|\s/)(\d|\D)+)?', '', meigens)
		# 改行の排除
		meigens = re.sub(r'\n', '', meigens)

		# 生成データの確認
		print(meigens)

		f.write(meigens)

こんな感じでずらーっと出力されます。
スクリーンショット 2018-06-29 14.43.57.png

いざ、新しい名言を

PrepareChain.pyでDBを作成してからGenerateText.pyにDBを実行してみましょう。
o-tomoxさんのコードを修正し、コマンドラインの第一引数に入力された数字分だけ文が出力されます。未記入なら5文表示されるようにしました。

GenerateText.py
# -*- coding: utf-8 -*-

u"""
マルコフ連鎖を用いて適当な文章を自動生成するファイル
"""

import os.path
import sqlite3
import random
import sys

from PrepareChain import PrepareChain


class GenerateText(object):
    u"""
    文章生成用クラス
    """

    def __init__(self, n=5):
        u"""
        初期化メソッド
        @param n いくつの文章を生成するか
        """
        self.n = n

    def generate(self, sentence_num):
        u"""
        実際に生成する
        @return 生成された文章
        """
        # DBが存在しないときは例外をあげる
        if not os.path.exists(PrepareChain.DB_PATH):
            raise IOError(u"DBファイルが存在しません")

        # DBオープン
        con = sqlite3.connect(PrepareChain.DB_PATH)
        con.row_factory = sqlite3.Row

        # 最終的にできる文章
        generated_text = u""

        # 指定の数だけ作成する
        # for i in xrange(self.n):
        for i in range(sentence_num):
            text = self._generate_sentence(con)
            generated_text += text + '\n'

        # DBクローズ
        con.close()

        return generated_text

    def _generate_sentence(self, con):
        u"""
        ランダムに一文を生成する
        @param con DBコネクション
        @return 生成された1つの文章
        """
        # 生成文章のリスト
        morphemes = []

        # はじまりを取得
        first_triplet = self._get_first_triplet(con)
        morphemes.append(first_triplet[1])
        morphemes.append(first_triplet[2])

        # 文章を紡いでいく
        while morphemes[-1] != PrepareChain.END:
            prefix1 = morphemes[-2]
            prefix2 = morphemes[-1]
            triplet = self._get_triplet(con, prefix1, prefix2)
            morphemes.append(triplet[2])

        # 連結
        result = "".join(morphemes[:-1])

        return result

    def _get_chain_from_DB(self, con, prefixes):
        u"""
        チェーンの情報をDBから取得する
        @param con DBコネクション
        @param prefixes チェーンを取得するprefixの条件 tupleかlist
        @return チェーンの情報の配列
        """
        # ベースとなるSQL
        sql = u"select prefix1, prefix2, suffix, freq from chain_freqs where prefix1 = ?"

        # prefixが2つなら条件に加える
        if len(prefixes) == 2:
            sql += u" and prefix2 = ?"

        # 結果
        result = []

        # DBから取得
        cursor = con.execute(sql, prefixes)
        for row in cursor:
            result.append(dict(row))

        return result

    def _get_first_triplet(self, con):
        u"""
        文章のはじまりの3つ組をランダムに取得する
        @param con DBコネクション
        @return 文章のはじまりの3つ組のタプル
        """
        # BEGINをprefix1としてチェーンを取得
        prefixes = (PrepareChain.BEGIN,)

        # チェーン情報を取得
        chains = self._get_chain_from_DB(con, prefixes)

        # 取得したチェーンから、確率的に1つ選ぶ
        triplet = self._get_probable_triplet(chains)

        return (triplet["prefix1"], triplet["prefix2"], triplet["suffix"])

    def _get_triplet(self, con, prefix1, prefix2):
        u"""
        prefix1とprefix2からsuffixをランダムに取得する
        @param con DBコネクション
        @param prefix1 1つ目のprefix
        @param prefix2 2つ目のprefix
        @return 3つ組のタプル
        """
        # BEGINをprefix1としてチェーンを取得
        prefixes = (prefix1, prefix2)

        # チェーン情報を取得
        chains = self._get_chain_from_DB(con, prefixes)

        # 取得したチェーンから、確率的に1つ選ぶ
        triplet = self._get_probable_triplet(chains)

        return (triplet["prefix1"], triplet["prefix2"], triplet["suffix"])

    def _get_probable_triplet(self, chains):
        u"""
        チェーンの配列の中から確率的に1つを返す
        @param chains チェーンの配列
        @return 確率的に選んだ3つ組
        """
        # 確率配列
        probability = []

        # 確率に合うように、インデックスを入れる
        for (index, chain) in enumerate(chains):
            # for j in xrange(chain["freq"]):
            for j in range(chain["freq"]):
                probability.append(index)

        # ランダムに1つを選ぶ
        chain_index = random.choice(probability)

        return chains[chain_index]


if __name__ == '__main__':
    if len(sys.argv) > 1:
        sentence_num = sys.argv[1]
    else:
        sentence_num = 5
    generator = GenerateText()
    print(generator.generate(int(sentence_num)), end = '')
terminal
$python GenerateText.py 1
世の中で成功を収めるには耐えられない。
$python GenerateText.py 3
成熟するためにここにいるのでは目的地につくことは、耐える力が弱いと見なければならぬ。
人間には感謝しましょう。
確信を持っているからではない。
$python GenerateText.py
遊ばなくなる。
渇しても悪木の陰に息わず。
危険を避けるのも、もう波が出やしないか。
不幸というものは、太陽の持つ輝きはなく、心の中の酒を保とうとする者が奴隷となる。
弱いものを作るのは自分ひとりで歩かなければ、人生で本当に重要な価値観を一切やめてみるだけでいい。

前後の文脈からどれを使うか判断しているので適当な文しかできないですが、たまに人間が理解できる深い名言が生成されると驚きますね。
いくつか面白い名言が完成したので見ていきましょう。

産み落とされた名(迷?)言たち

そんなレベルに達するまで人生の意義を探し求めようとするのだからだ。

色々考えた結果おかしくなっちゃったのでしょうか...
1つのことに追求しすぎるのかもよくないといった意味が込められている気がします。

すべてを知りつくしたなんて決して思わないことを、忘れてはいません。

これは非常に興味深いですね。少し弄れば多くの心に響きそうです。
いつまでも精進し続けるためには慢心してはならない、エンジニアたるもの常に新しい技術にチャレンジする姿勢を忘れずに!

ある者は十年先に希望をもつ。

現状上手くいかなくたって、積み重ねればいつか報われる。そんな意味が伝わってきました。
受験や就活などの人生の節目で目の前にある壁に囚われてはいけない。その先に意味があるんだと考えさせます。

以下、ユニークな迷言の紹介です。

いろいろ考えられる選択肢の中の蛙、大海を知らず。

カエルも思い詰まっていたら行動できないんですね...
今度見かけたら海に連れて行ってリフレッシュさせてきます。

やけどする事だけを考える男だ。

デンジャラスな男を見つけてしまいました。きっと彼の周りには常に放水車がスタンバイしていることでしょう。
言葉足らずですが、恋に情熱を「燃やす」といった受け取り方もできなくもない(かも)。
色々な捉え方ができますね。

オレの人生の面白さを願え。

また変な男が(笑)。クラスに1人はいるお調子者が校長先生の朝礼中に乱入してきて「オレの人生の面白さを願え!」と、叫んでいる情景が思い浮かびました。
こんなフレーズを堂々と言えること自体が既に面白いです。

まとめ

今回はマルコフ連鎖を使って名言の自動生成を扱いました。
迷言で笑い、名言に考えさられ...プログラムで文を作れるってすごい面白いですね。
人間の心を動かす未来もそう遠くないのかもしれません。

名言のみならず他のテーマで行っても十分楽しめます。皆さんも思いついたら是非マルコフ連鎖をフル活用してユニークな文を作ってみてください!

43
40
4

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
43
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?