のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

マルコフ連鎖で自動的に文章を生成してみた

Sponsored Links

皆さんこんにちは
お元気ですか。私は二郎食べたいと思ったり、思わなかったりです。

今日はマルコフ連鎖を使って、文章の自動生成を行いたいと思います。

マルコフ連鎖とは?

マルコフ連鎖は、一連の確率変数 X1, X2, X3, ... で、現在の状態が決まっていれば、過去および未来の状態は独立であるものである。(by Wikipedia)
いわゆる、過去なんて関係ない、今が大事なんだという思想ですね。数式的に示すと以下の通りです。

{ \displaystyle
Pr(X_{n+1}|X_n= x_n,\cdot\cdot\cdot,X_0 = x_0) = Pr(X_{n+1}|X_n = x_n) 
}

まぁコインはいつ投げても50%で裏表が出るのと同じ原理です。今まで裏ばかり出たから次は表といった考えではありません。

データセットの抽出

今回は「小説家になろう」を使ってデータを抽出します。
皆さんの書いた小説からマルコフ連鎖の確率を求めていきます。
小説家になろうではAPIが公開されており、各小節のdescriptionを観察することができます。
今回は、このdescriptionを学習してみたいと思います。

#coding:utf-8
import urllib2
import yaml
import Parser
from collections import defaultdict
import random
import MeCab

class Parser(object):
	def __init__(self):
		pass

	def parse(self,sentence):
		tagger = MeCab.Tagger('-Owakati')
		node = tagger.parse(sentence)
		return node

class ShosetsukaniNarou(object):
	def __init__(self):
		pass

	def getData(self,url):
		yaml_data = urllib2.urlopen(url)
		yaml_novel = yaml.load(yaml_data.read())
		documents = [data['story'] for data in yaml_novel[1:-1]]
		parser = Parser.Parser()

		split_words = [parser.parse(document.encode('utf-8')).split(" ") for document in documents]
		return split_words

マルコフ連鎖の実装

一応n-gramにも対応させています。入力もn-gramにする制約付きですが・・・
今回はdictionary型でワードと対応するリストを保持し、このリストから選択することで
確率で選択することを実現しました。

class MalkovChain(object):
	def __init__(self):
		self.dictionary = defaultdict(list)

	def fit(self,split_words,word_length=1):
		self.split_word_length = word_length
		for split_word in split_words:
			for i in xrange(len(split_word) - word_length):
				end_of_word_position = i+word_length
				dict_string = ""
				for word in split_word[i:end_of_word_position]:
					dict_string = dict_string + word
				self.dictionary[dict_string].append(split_word[end_of_word_position])

	def transform(self,word,limit=1):
		parser = Parser.Parser()
		word_list = parser.parse(word).split(" ")[:-1] #最後のindexは改行コードの削除必
		if self.split_word_length != len(word_list):
			print "情報が欠如しています。"
			return ""
		end_cnt = 0
		cnt = 0

		while limit != end_cnt:
			stract_words = word_list[cnt:cnt+self.split_word_length]
			temp_word = ""
			for stract_word in stract_words:
				temp_word = temp_word + stract_word

			word = random.choice(self.dictionary[temp_word])
			word_list.append(word)

			if "。" in word:
				end_cnt = end_cnt + 1
			cnt = cnt + 1
		sentence = ""
		for i in xrange(len(word_list)):
			sentence = sentence + word_list[i]
		return sentence

実験1[1word]

入力ワードごとに複数回試し、どんな文章が構築されるか比較する。
せっかくなので、ジャンルごとにどんな文章が生まれるかを見たい。

文学
俺の綺麗なんpleexperimenト.com/)』より。
俺のサイトにて見失う事を代理で自分をつけていたの私、それらは、社会人の姿を失った男が、大家さんと眷族の彼の日々しのいでください。
俺は稀には高校生活の毒を変えた家に挑戦するので、と選手たちの四人のこれに後米軍人相手は、二又瀬。
俺はなりゃしたが凍雲(http://sokkyo-shosetsu.com/)』よりちょい上な出来事。
俺の日々しのいで毎日にお姉と、古谷と喜一の近所の気持ちを破談になぞらえています」が読める2時が………。
ファンタジー
俺の始まります。
俺、ヴァンパイア王を告げる。
俺は、睦月家になった。
俺が残るフランカ王国王子のバトルファンタジー※オチが囁くまま進めさせていたが、失われる様より1~く、彼は挿絵付きです。
俺ら!!楽しみたいミリタリー誌のお話です。

色々とおかしいところがありますが、全然内容の色が違いますね。

日常

文学
日常に遡ります。
日常に弄ばれて頂いて、男気あふれるあかずきんと。
日常を知らされた私、死にました写真部を繰り返す散々な世代からその出来事、退屈しのぎで毎日4 そこで待っている。
日常を譲り受けた。
日常になったりするように秘められて出逢ったようです…でも面白いかもしれないようになるの愛としていらっしゃいます。
ジャンル
日常の女の子の冒険譚・。
日常のアニスを転職屋恭一もかかわらず、エリシアは大学の世界大戦は「―ちゃんとチートです。
日常と「ざっけ…エブリスタと関わりの物質を中心に転移者たち(?」と、終幕へ。
日常と魔術師、お読みにくい部分、オーマは――愛を守るための宗教対立に向かう世界であって、加藤蓮木(1500字程度)と謎の半生を戦い 1章のなかあ~2日、時になって、ごく普通の紹介で異世界を蹂躙した人間の楔で歪な魔法とトラック転生!教会建設反対運動の物質をデザインの帰り道で生まれ変わったら携帯から物量へ。
日常の領土は誰も無くて、狂った。

実験2[2word]

先ほどの実験について今度は2ワード連続で入力させた場合の反応を見ます。

俺は

日常
俺の自慢の妹のお話です。
俺のじゃない気がするのでジャンルは文学で。
俺の自慢の妹の影でしかなかった圭は、聖司は、地元紙に『奇跡のイレブン』と同じ年の冬のおはなし。
俺のじゃない。
俺のじゃない気がつくと、私、つばめはある香りを嗅ぐ。
ファンタジー
俺の領土は帝国は400年続く太平の世。
俺の車が装甲車になってこう」レオのおつかいの契約内容・条件は五個 1.一日二話前後投稿している。
俺の車が装甲車になった。
俺の車が装甲車になった少女Aは、悠臥は、生まれ持ったジョブに人生を送る傍ら、仲間のモンスターバスター達と一緒に、壮大な世界。
俺の車が装甲車になったりしながらもその手伝いをさせている。

文学:日常に、日常の

文学
日常に紛れて人々を監視しているせいで耐性が必要かというと、人々の思っているお姉様は、死にました。
日常に紛れて人々を監視してる。
日常に紛れて人々を監視して日々しのいでいて未熟なかたは絶対に読まないでください。
日常に紛れて人々を監視してる。
日常に紛れて人々を監視していらっしゃいます。
ファンタジー

日常の中に登場する情報は公開情報及びミリタリー誌のものを使用してるものがあります。
日常の切り抜きに近いです。
日常の中で目覚めたそこは剣と魔術が盛んに行われてしまった。
日常のなかでひっそりと寄り添いあうカタナとサヤ。
日常のためにルーペスレーヴェ皇国へと侵入するが、飛ばされた、はずだった。

なんか凄くカオスな内容になっていますが、1wordの内容と比べるとまともな文章になっていると思います。

ソースコード全文

#coding:utf-8
import urllib2
import yaml
import Parser
from collections import defaultdict
import random
import MeCab

class Parser(object):
	def __init__(self):
		pass

	def parse(self,sentence):
		tagger = MeCab.Tagger('-Owakati')
		node = tagger.parse(sentence)
		return node

class ShosetsukaniNarou(object):
	def __init__(self):
		pass

	def getData(self,url):
		yaml_data = urllib2.urlopen(url)
		yaml_novel = yaml.load(yaml_data.read())
		documents = [data['story'] for data in yaml_novel[1:-1]]
		parser = Parser.Parser()

		split_words = [parser.parse(document.encode('utf-8')).split(" ") for document in documents]
		return split_words

class MalkovChain(object):
	def __init__(self):
		self.dictionary = defaultdict(list)

	def fit(self,split_words,word_length=1):
		self.split_word_length = word_length
		for split_word in split_words:
			for i in xrange(len(split_word) - word_length):
				end_of_word_position = i+word_length
				dict_string = ""
				for word in split_word[i:end_of_word_position]:
					dict_string = dict_string + word
				self.dictionary[dict_string].append(split_word[end_of_word_position])

	def transform(self,word,limit=1):
		parser = Parser.Parser()
		word_list = parser.parse(word).split(" ")[:-1] #最後のindexは改行コードの削除必
		if self.split_word_length != len(word_list):
			print "情報が欠如しています。"
			return ""
		end_cnt = 0
		cnt = 0

		while limit != end_cnt:
			stract_words = word_list[cnt:cnt+self.split_word_length]
			temp_word = ""
			for stract_word in stract_words:
				temp_word = temp_word + stract_word

			word = random.choice(self.dictionary[temp_word])
			word_list.append(word)

			if "。" in word:
				end_cnt = end_cnt + 1
			cnt = cnt + 1
		sentence = ""
		for i in xrange(len(word_list)):
			sentence = sentence + word_list[i]
		return sentence

data = ShosetsukaniNarou()
split_words = data.getData("http://api.syosetu.com/novelapi/api/?genre=1&lim=100")
MalkovChain = MalkovChain()
MalkovChain.fit(split_words,word_length=2) #ワードを分割する長さを入れる
for i in xrange(5):
	print MalkovChain.transform("日常の", 1) #ここに単語を入れる