LoginSignup
7
5

More than 1 year has passed since last update.

ドラ●エのモンスター名っぽい文章生成してみる(N階マルコフ連鎖/形態素解析・超入門編)【人工無脳】

Last updated at Posted at 2022-03-06

概要

ちなみに

この記事に掲載しているソースコードで、小説・セリフ・記事などの文章生成(文章合成)もできます。
N階マルコフ連鎖を指定している箇所(gi_n_markov_size)を「1」→「3 or 4」と変更して、あとは適切な元ネタを放り込むだけです。

少ない元ネタで、セリフ増殖していく方法をいろいろ試しているので、後日、そちらも記事にできればよいと考えています。
【追記:4/24】セリフ増殖記事公開しました

オープンソースのチャットボット「Botpress」もアレコレ試しています。チャットボットのテスト用に、同じようなセリフを増殖させられる方法を考えています。

記事が長いのでアジェンダもどき

  • はじめに
  • モンスター名っぽいの生成してみる
  • 「メタルキング」を作りたい
  • マルコフ連鎖で、セリフを増殖させる件について
  • ソースコード掲載

はじめに

子どもがドラ●エに大ハマリ!
「ドラ●エに出てきそうなモンスターの名前は?」と聞かれた際、毎度、毎度、頭がそんなに上手に回転する訳がないじゃないですか。
親としてのプライドと責任があるので、チートな救世主、マルコフの連鎖のお世話になるしかないのだよ。

参考文献

N階マルコフ連鎖(マルコフ過程)やJanome(日本語の形態素解析ができるやつ)の詳しい説明は、以下の素晴らしい記事をご覧くださいませ。

とにかく試す

この記事は、モンスター名っぽいものの生成を試みて、マルコフ連鎖が実際にどう動くのかを見ていただく目的で書いています。

マルコフ連鎖を使って、文章の生成をお考えのお方は、「小説・セリフ・記事を作ろう!」といきなり行動してしまいがちです(まあ、もちろん、自分も最初は突っ走りました。スーパーダッシュに決まってます)。

N階マルコフ連鎖で文章生成の情報を何十個と見ていると、「N=1にしちゃダメなんだ~」と思い込んでしまいませんか(自分がそうデシタ)。
データ件数を増やしても、文字数を増やしてもうまくいかない!
くそぉぉぉ、もう無理だぁぁぁああ!
それならば単純「N=1」を試してみましょう。

文章生成

じゃあ、始めます

さあ、今回登場いただくのはこの100体のモンスターたち……と、言いたいところですが、ただの『単語がくっついたやつ』です。
すべての単語が、辞書に載っている事を確認しています。

test_original_source1.txt
エビルプラント
エビルマスター
エンゼルスライム
キラーデーモン
キングイーター
キングコブラ
キングスライム
キングヒドラ
キングマーマン
キングミミック
クリスタルスライム
ゴールデンスライム
サタンジェネラル
サンドマスター
シースライム
ストーンスライム
ストーンビースト
ストーンマン
スライムエンペラー
スライムジェネラル
スライムスノー
スライムタワー
スライムダーク
スライムツリー
スライムナイト
スライムファング
スライムブレス
ソードドラゴン
ダイヤモンドスライム
ダークアイ
ダークアーマー
ダークキング
ダーククリスタル
ダークサタン
ダークシャーマン
ダークスライム
ダークナイト
ダークパンサー
ダークビショップ
ダークフレイム
ダークホーン
ダークマンモス
ダークランサー
デビルアンカー
デビルアーマー
デビルウィザード
デビルスノー
デビルスライム
デビルダンサー
デビルパピヨン
デビルプラント
デビルプリンス
デーモンキング
トロルキング
ナイトウォーカー
ナイトキング
バブルスライム
バブルデーモン
ボックススライム
マグマスライム
マスタースライム
マリンスライム
ミニスライム
ミニデーモン
メタルスコーピオン
メタルスライム
メタルドラゴン
メタルハンター
メタルハンド
メタルライダー
呪いのカガミ
呪いのツルギ
呪いのボトル
呪いのマスク
呪いのランプ
嘆きの亡霊
嘆きの巨人
嘆きの怪物
地獄の使い
地獄の炎
地獄の鎧
地獄の門番
地獄の騎士
彷徨う兵隊
彷徨う神官
彷徨う鎧
彷徨う魂
死霊の騎士
炎の巨人
炎の戦士
炎の精霊
鉄のサソリ
鎧の騎士
鎧ムカデ
魔法使い
魔物使い
魔王のランプ
魔王の仮面
魔王の使い
魔王の影

単語がくっついていればよいので「ファイヤーアロー」「炎の矢」とかでもokです。お好きなネタでお試しください。検証結果は、上記の『単語がくっついたやつ』を使った場合になります。
カタカナだけのやつが70件(499文字)、漢字を含むものが30件(144文字)、全部で100件(643文字)で実験スタート。

(50回生成して、元ネタにあるものと生成重複を隠すようになっています(掲載コードで、この仕様がフラグ制御可能))

出力結果
呪いの門番
炎の炎の炎
魔王の炎の巨人
呪いの鎧の騎士
デーモンキングヒドラ
彷徨う鎧の影
地獄のランプ
魔王のボトル
キング
地獄のツルギ
ダークキングコブラ
鎧

あれ!? カタカナのみ少ない!?
今回、日本語の形態素解析ができるJanomeを使用しています。

I can only speak Japanese!
Nihongo de Onegai Shimasu!

100件(643文字)もデータを与えてやったと言うのに……だが、いいだろう、お前の望み、聞き入れてやろう。
しかし、条件がある。
お前に与えてやるのは、30件(144文字)だけだ。
漢字を含むモンスター名をこれ以上さがしてきて、さらに辞書を引くのが面倒だ。
(ランプは江戸末期にはすでに渡来済みだし、マスクとボトルはすでに日本語だろ?)
掲載用220306_01.JPG
……あれ? 出力件数が、明らかに増えた!?
はっ! そうか、しまった!
「つるぎ→ツルギ」とかにして、「漢字+ひらがな+カタカナ」の組み合わせを作ってみたつもりだったが、マルコフ連鎖とは『(単純な文章生成の場合は)次に続く言葉の法則に従って動く』ものなので、「メタルの巨人」とか「地獄のスライム」とかを用意しておかなければいけなかったのか……うっかりしておりました。
ははは。では、Janomeの懇願など握り潰して、カタカナのみ70件(499文字)で実行してみましょう。
きっと、多くの新種モンスター名が生成され、我が軍の勝利も確たるものになるだろう。

出力結果
ミニデーモンキングヒドラ
出力結果
ダークキングコブラ
出力結果
デーモンキングヒドラ

しかし、新種モンスター名たちが助けにこなかった……いや、いちおう来るんだけど、数が少ない……こ、このままでは少しずつ、削られて……。

出力結果
新しい「何か」を、一件も生成できませんでした。

――人間たちの力の前に、モンスター名たちは全滅してしまった。

「言っておくが、俺のEXP力は3万以上だぜ(by メタルキング(Metal King))」
↑みたいなのを、死亡フラグなセリフ100選で見た記憶がありますが、そもそも『メタルキング』の表示を見ていません(キングを含むものは出やすいのに)。

前半が『Metal』なものは、「メタルスコーピオン」「メタルスライム」「メタルドラゴン」「メタルハンター」「メタルハンド」「メタルライダー」と6件あります。
後半が『King』なものは、「ダークキング」「デーモンキング」「トロルキング」「ナイトキング」と4件あります。
では、なぜ……。

実は、分かち書きがこのようになっています。

[['メタルスコーピオン'], ['メタルスライム'], ['メタル', 'ドラゴン'], ['メタル', 'ハンター'], ['メタル', 'ハンド'], ['メタル', 'ライダー'], ['ダーク', 'キング'], ['デーモン', 'キング'], ['トロルキング'], ['ナイトキング']]

Janomeは、「メタルスコーピオン」「メタルスライム」「トロルキング」「ナイトキング」を一つの名詞だと認識しているようです。

(以下は、説明用に分かりやすく[START][END]と表示しています)

{('[START]',): ['メタルスコーピオン', 'メタルスライム', 'メタル', 'メタル', 'メタル', 'メタル', 'ダーク', 'デーモン', 'トロルキング', 'ナイトキング'], ('メタルスコーピオン',): ['[END]'], ('メタルスライム',): ['[END]'], ('メタル',): ['ドラゴン', 'ハンター', 'ハンド', 'ライダー'], ('ドラゴン',): ['[END]'], ('ハンター',): ['[END]'], ('ハンド',): ['[END]'], ('ライダー',): ['[END]'], ('ダーク',): ['キング'], ('キング',): ['[END]', '[END]'], ('デーモン',): ['キング'], ('トロルキング',): ['[END]'], ('ナイトキング',): ['[END]']}

マルコフ連鎖とは『(単純な文章生成の場合は)次に続く言葉の法則に従って動く』ものです。

('メタル',): ['ドラゴン', 'ハンター', 'ハンド', 'ライダー']

メタルから始まって繋がる言葉は、「ドラゴン」「ハンター」「ハンド」「ライダー」だけ。つまり上のデータから分かる事は、メタルキングは『幻のモンスター名』という事です。

ちなみに「そろそろ本気を出したらどうなんだ?」みたいなのも、死亡フラグなセリフ100選で見た記憶があります。

なので、ゆっくり考えていきましょう

形態素解析ツールを上手に使う為に『ユーザー辞書』を用意しましょう、という記述を見た事がある方は多いのではないでしょうか。
またJanomeではなく、『MeCab』を使うほうがよい、という記述もご覧になった方は多いはずです。
記事のタイトルに「超入門編」と書いてしまったので、「?」となった方は、『Janome ユーザー辞書』や『MeCab』でググってみてください。Qiita内にもたくさん記事があります。
あとJanomeのバージョンを変更すると、変わる事もあるでしょう。
JUMANを試してみる』もおすすめしておきます。

形態素解析の結果

さて、こちらがJanome使用時の『漢字を含むものの元ネタ』の状態です。

『漢字を含むものの元ネタ』の状態
{('[START]',): ['呪い', '呪い', '呪い', '呪い', '呪い', '嘆き', '嘆き', '嘆き', '地獄', '地獄', '地獄', '地獄', '地獄', '彷徨', '彷徨', '彷徨', '彷徨', '死霊', '炎', '炎', '炎', '鉄', '鎧', '鎧', '魔法使い', '魔物', '魔王', '魔王', '魔王', '魔王'], ('呪い',): ['の', 'の', 'の', 'の', 'の'], ('の',): ['カガミ', 'ツルギ', 'ボトル', 'マスク', 'ランプ', '亡霊', '巨人', '怪物', '使い', '炎', '鎧', '門番', '騎士', '騎士', '巨人', '戦士', '精霊', 'サソリ', '騎士', 'ランプ', '仮面', '使い', '影'], ('カガミ',): ['[END]'], ('ツルギ',): ['[END]'], ('ボトル',): ['[END]'], ('マスク',): ['[END]'], ('ランプ',): ['[END]', '[END]'], ('嘆き',): ['の', 'の', 'の'], ('亡霊',): ['[END]'], ('巨人',): ['[END]', '[END]'], ('怪物',): ['[END]'], ('地獄',): ['の', 'の', 'の', 'の', 'の'], ('使い',): ['[END]', '[END]', '[END]'], ('炎',): ['[END]', 'の', 'の', 'の'], ('鎧',): ['[END]', '[END]', 'の', 'ムカデ'], ('門番',): ['[END]'], ('騎士',): ['[END]', '[END]', '[END]'], ('彷徨',): ['う', 'う', 'う', 'う'], ('う',): ['兵隊', '神官', '鎧', '魂'], ('兵隊',): ['[END]'], ('神官',): ['[END]'], ('魂',): ['[END]'], ('死霊',): ['の'], ('戦士',): ['[END]'], ('精霊',): ['[END]'], ('鉄',): ['の'], ('サソリ',): ['[END]'], ('ムカデ',): ['[END]'], ('魔法使い',): ['[END]'], ('魔物',): ['使い'], ('魔王',): ['の', 'の', 'の', 'の'], ('仮面',): ['[END]'], ('影',): ['[END]']}

「'の'」に繋がるキーワードは多いですし、「('の',):」から始まるものも豊富です。
たくさんの新種モンスター名が生成されるはずです。

対して、カタカナのみのモンスター名は、こうなっています(長いので一部のみ抜粋)。

『カタカナのみの元ネタ』の状態(一部)
 ('エビルマスター',): ['[END]'], ('エンゼルス',): ['ライム'], ('ライム',): ['[END]', '[END]'], ('キラー',): ['デーモン'], ('デーモン',): ['[END]', 'キング', '[END]', '[END]'], ('キングイーター',): ['[END]'], ('キング',): ['コブラ', 'ヒドラ', '[END]', '[END]'], ('コブラ',): ['[END]'], ('キングスライム',): ['[END]'], ('ヒドラ',): ['[END]'], ('キングマーマン',): ['[END]'], ('キングミミック',): ['[END]']

ほとんどのモンスター名が、次の言葉は「 ['[END]']」のみです(つまり合成される事なく終了)。
エンゼルスライムは、「エンゼルス」と「ライム」に分かれています。「エンゼルス」はJanomeでは『組織名』扱いです。

どうしてもメタルキングと表示したい

メタルキングという文字列が画面に表示されただけで、条件反射で「キターーー!」と叫ぶ人は多いでしょう(自分もその一人)。
どうしても「メタルキング」という文字列を表示させたいのです。

Janomeで分かち書きになる言葉、そして、メタルのあとが「キ」になる言葉をさがしてみました。

test_original_source1.txt
メタルキツネ
メタルキューピット
メタルキーホルダー
メタルキーワード
サッカーキツネ
ルールキツネ
アクセルキツネ
サッカーキューピット
ルールキューピット
アクセルキューピット
サッカーキーホルダー
ルールキーホルダー
アクセルキーホルダー
サッカーキーワード
ルールキーワード
アクセルキーワード
サッカーキング
ルールキング
アクセルキング
('メタル',): ['キツネ', 'キューピット', 'キーホルダー', 'キーワード']

はい。惨敗です。
だがしかし、負ける訳にはいかない……。

test_original_source1.txt
メタ
タル
メタルキ
タルキング
タルキ
ルキング
ルキ
ング

8個に分離させてみました。

{('[START]',): ['メタ', 'タル', 'メタルキ', 'タルキング', 'タルキ', 'ルキング', 'ルキ', 'ング'], ('メタ',): ['[END]'], ('タル',): ['[END]'], ('メタルキ',): ['[END]'], ('タルキング',): ['[END]'], ('タルキ',): ['[END]'], ('ルキング',): ['[END]'], ('ルキ',): ['[END]'], ('ング',): ['[END]']}

8個すべてが、一つの名詞だと認識されました。
またしても惨敗。

続・どうしてもメタルキングと表示したい

「マルコフ連鎖を使ってみよう!」系の記事では、「形態素解析ツールを使おう!」という言葉がセットで書かれていますが、実は『形態素解析ツールを使わない』という選択肢があるのです!

掲載コードの「d_e_f_wakatigaki」関数で、Tokenizerを実際に使っています。こちらを使わないように変更してみます。
d_e_f_wakatigaki」関数内の、「print(lis_r_wakatigaki)」のコメント解除をおこなう事で、ご自分用の元ネタに変更したとしても、この記事と同じようなテストができます。

掲載コード内
di_data_markov = d_e_f_make_data_markov(lis_orig_sour_data)
print(di_data_markov) さらにここをコメント解除

元ネタの量が多いと、たくさん画面に表示されてしまうので、試す際は注意してください。
掲載用220306_02.JPG

キターーー!

上の画面を見ていただいた通り、Janomeなどを使わないと、一つ一つの『文字』でN階マルコフ連鎖をおこなう事になり、生成結果もそのようなものになります。
Janomeなど、形態素解析ツールを使わないと、それはそれで、N=4くらいにしてもよい文章ができない原因に繋がる事もあります。

N=2
{('[START]',): ['メ', 'タ', 'メ', 'タ', 'タ', 'ル', 'ル', 'ン'], ('[START]', 'メ'): ['タ', 'タ'], ('メ', 'タ'): ['[END]', 'ル'], ('[START]', 'タ'): ['ル', 'ル', 'ル'], ('タ', 'ル'): ['[END]', 'キ', 'キ', 'キ'], ('ル', 'キ'): ['[END]', 'ン', '[END]', 'ン', '[END]'], ('キ', 'ン'): ['グ', 'グ'], ('ン', 'グ'): ['[END]', '[END]', '[END]'], ('[START]', 'ル'): ['キ', 'キ'], ('[START]', 'ン'): ['グ']}
N=3
{('[START]',): ['メ', 'タ', 'メ', 'タ', 'タ', 'ル', 'ル', 'ン'], ('[START]', 'メ'): ['タ', 'タ'], ('[START]', 'メ', 'タ'): ['[END]', 'ル'], ('[START]', 'タ'): ['ル', 'ル', 'ル'], ('[START]', 'タ', 'ル'): ['[END]', 'キ', 'キ'], ('メ', 'タ', 'ル'): ['キ'], ('タ', 'ル', 'キ'): ['[END]', 'ン', '[END]'], ('ル', 'キ', 'ン'): ['グ', 'グ'], ('キ', 'ン', 'グ'): ['[END]', '[END]'], ('[START]', 'ル'): ['キ', 'キ'], ('[START]', 'ル', 'キ'): ['ン', '[END]'], ('[START]', 'ン'): ['グ'], ('[START]', 'ン', 'グ'): ['[END]']}
N=4
{('[START]',): ['メ', 'タ', 'メ', 'タ', 'タ', 'ル', 'ル', 'ン'], ('[START]', 'メ'): ['タ', 'タ'], ('[START]', 'メ', 'タ'): ['[END]', 'ル'], ('[START]', 'タ'): ['ル', 'ル', 'ル'], ('[START]', 'タ', 'ル'): ['[END]', 'キ', 'キ'], ('[START]', 'メ', 'タ', 'ル'): ['キ'], ('メ', 'タ', 'ル', 'キ'): ['[END]'], ('[START]', 'タ', 'ル', 'キ'): ['ン', '[END]'], ('タ', 'ル', 'キ', 'ン'): ['グ'], ('ル', 'キ', 'ン', 'グ'): ['[END]', '[END]'], ('[START]', 'ル'): ['キ', 'キ'], ('[START]', 'ル', 'キ'): ['ン', '[END]'], ('[START]', 'ル', 'キ', 'ン'): ['グ'], ('[START]', 'ン'): ['グ'], ('[START]', 'ン', 'グ'): ['[END]']}

『彷徨う鎧』を漢字で表現したくない

『彷徨う鎧』を漢字で表現したくない場合、それこそ形態素解析ツールを使うとよいです。
以下は、何も考えずにJanomeで形態素解析した結果です。

Janome
彷徨	名詞,サ変接続,*,*,*,*,彷徨,ホウコウ,ホーコー
う	感動詞,*,*,*,*,*,う,ウ,ウ
鎧	名詞,一般,*,*,*,*,鎧,ヨロイ,ヨロイ

以下は、JUMANを試してみた結果です。

JUMAN
彷徨う さまよう 彷徨う 動詞 2 * 0 子音動詞ワ行 12 基本形 2 "代表表記:彷徨う/さまよう"
鎧 よろい 鎧 名詞 6 普通名詞 1 * 0 * 0 "代表表記:鎧/よろい カテゴリ:人工物-衣類"

どの形態素解析ツールがよいとか悪いとかよりも、入力テキストの内容によって、向き不向きがあります。
この記事で試したように、場合によっては、そもそも形態素解析ツールを使うと実現できない出力の形もあります。アレコレ試してみるのをおすすめしておきます(カタカナ→ひらがなの変換は、ゴリ押しプログラムでもなんとかなるでしょう)。

そして締めの文章っぽいのへ

結論として

N=1~N=4と指定する以外に、形態素解析の形を、適切にする必要があります。
元ネタのデータを増やすだけでは、望む出力が得られない場合があります。
元ネタの特性を考えて、いろいろと調整が必要です。

システム開発をする際は、入力と出力のケースを考えます。必要なソフトウェアや方法も考えるでしょう。
マルコフ連鎖の素直な思想として、ナイトウォーカーとスライムナイトの『ナイト』が違うなんて知ったこっちゃないのです。

今後

ボットチャットを一から作りたいと考えています。その為に、ビジネスでも注目されているボットチャット(「Botpress」など)も、いろいろと試しています。Kerasも試してみました。
ゴリ押し人工無脳で始めて、だんだんAI化していければ、記事の構成としてはBestだと考えています。

解析、文章から「重要キーワード」を抽出する実験の為にも、話し言葉(セリフ)文章が大量にほしかったのです。だからマルコフ連鎖をしっかり学ぼうと考えました。

と言う訳で、次回か、さらにそのあとになるかも知れませんが、「似てるけどちょっとずつ違う」セリフの増殖方法の記事を書こうと思います(セリフ増殖は、すでに成功しているのですが、記事にするにはもう少しまとめが必要なので)。
【追記:4/24】
「セリフ増殖記事公開しました」
「超初心者向け、Word2vec記事も公開しました」

最後に

お読みいただいてありがとうございました。
このネタを改良していろいろ試してみてください。
ただし、一つだけ注意事項があります。
これは『単語がくっついたやつ』で文章生成をおこなったにすぎません。
モンスター名とあわせて、「ゲームなどのイラスト」を表示してオープンに公開してしまうと、スクエニさんも当事者になった、【ビホルダー問題】のようなものに発展する可能性があります。そこを考えた上でいろいろおこなってくださいね。

実装

環境

  • Janome 0.4.1
  • Python 3

ソースコード

なるべくコメントを増やしておきました。
プログラム講師をしていた頃の癖で、関数名「d_e_f_」、str型「stri_」、list型「lis_」のように名称をつけています。
逆にゴチャゴチャしたソースコードになっていたら申し訳ないです。

「俺はメモ帳でしかプログラムを書かない主義だ。関数や変数を追う時はCtrl+Fだ」と、Windowsマックスな方には読みやすいかも知れません。

PythonソースをWindows版で公開する人は少ないですが、このソースはWindows版です。
元ネタファイルを含めてですが、改行コードなどの問題が発生するかも知れません。改行コードに関しては、「gstri_line_feed_code」「.strip()」のキーワードでソース内を検索してください。

markov_chain.py
#【※】このソースコードは、「Windows」版です。
# その為、下のように文字コードを指定している箇所があります。
#   open("test1.txt", "r", encoding="UTF-8")
# 環境によっては、
#   open("test1.txt")
# などに修正してから実行してください。
# また「改行コード」の扱いについても注意してください。
# (改行コードを扱っている箇所は「gstri_line_feed_code」「.strip()」で検索)

#【参考文献】
#https://qiita.com/k-jimon/items/f02fae75e853a9c02127
#https://qiita.com/takaito0423/items/1a39d790b5e5b8e4cf36
#https://qiita.com/GlobeFish/items/14587d79dcef8722fe57
#https://qiita.com/GlobeFish/items/17dc25f7920bb580d298
#https://qiita.com/GlobeFish/items/00e0ba91432caffa7946
#https://qiita.com/eycjur/items/2fc0b9a240306cfd3008

#【※】janome(形態素解析ライブラリ)を初めて使う場合は、pipする。
# ①バージョンを指定しない
# !pip install janome
# ②バージョンを指定する場合の例
# !pip install janome==0.4.1
#【※】pipで登録されたパッケージのバージョン確認方法
# pip list

#####■# マルコフ連鎖を試みたい回数
#(重複除外機能などをonにすると、当然ですが、出力件数は減ります)
gi_try_markov = 50

#####■# N階マルコフ連鎖の、「N」を指定する
#(単語合成なら「1」、小説・セリフ・記事なら「3」「4」を指定するのがおすすめ)
gi_n_markov_size = 1

#####■# 元ネタファイルのパス(相対パスで指定)
gstri_orig_sour_file_path = "./test_original_source1.txt"
#■ まったくの重複行が、元ネタにある場合、出力せず隠すか?
#■(on:True、off:False)
gis_chk_hidden_word_orig_sour_f = True

#####■#(生成された文章(結合された単語)の)同じ内容を出力せず隠すか?
#■(on:True、off:False)
gis_chk_hidden_duplicate = True

#####■# janomeで、分かち書きになった情報を、区切る為に使う文字
gstri_split_word = "|"

#####■# 改行コード
gstri_line_feed_code = "\n"

#####■# 文頭と文末を、どうユニークに表現するか決める
#【※】マルコフ連鎖で使いたい文章中に絶対にない「記号のようなもの」にする
#(↓は、かなりユニークなのにしておきました)
#■ BOS(文頭:the beginning of a sentence)
gstri_bos = "+++---***///BOS///***---+++"
#■ EOS(文末:the end of a sentence)
gstri_eos = "+++---***///EOS///***---+++"

# collectionsをロードして、dequeをインポート
from collections import deque
# janome(形態素解析ライブラリ)をロードして、Tokenizerをインポート
from janome.tokenizer import Tokenizer
# randomをインポート(その名の通り、ランダム処理で使う)
import random
# 読み書き用ライブラリをインポート
#(エンコードやデコードをおこなう為の関数が定義されている)
import codecs

# ■■■■■■■■■■■■■■■
# ■【下の関数(分かち書き関数)の中で使うTokenizerを初期化】
# ■ Tokenizerを初期化する処理が重いので、初期化はここでおこなう
# ■(ループ内で初期化すると「OSError: [Errno 24] Too many open files」発生)
token_p_wakati_text = Tokenizer()
# ■■■■■■■■■■■■■■■
# ■ 分かち書きをおこなう関数
# ■ 引数:<class "str">:stri_p_wakati_text
# ■ 戻り値:<class "list">:lis_r_wakatigaki
def d_e_f_wakatigaki(stri_p_wakati_text):
  # 戻り値を格納するリストを初期化
  lis_r_wakatigaki = []

  # 区切り文字を加えた情報を、一時的に格納する文字列変数を初期化
  stri_spl_text = ""
  # Tokenizerを使用する
  for token_current in token_p_wakati_text.tokenize(stri_p_wakati_text):
    # janome(形態素解析ライブラリ)で分かち書きにされた情報を、区切っていく
    #【※】品詞情報などが必要ない場合は、surfaceを使い「表層形」だけを取得
    #(もしくは「.tokenize("テキスト", wakati=True)」を使うとメモリ抑制にもなる)
    stri_spl_text += token_current.surface + gstri_split_word
    #■【※】↓のprint文を解除してみると、動きが分かりやすいです
    #print(stri_spl_text)

    # 区切り文字を目印に、リスト型に変換する(これが戻り値になる)
    lis_r_wakatigaki = stri_spl_text.split(gstri_split_word)
    # リスト末尾の要素を削除する為に、pop()の引数をなしにする
    #■【※】↓のprint文を解除してみると、動きが分かりやすいです
    #print(lis_r_wakatigaki)
    lis_r_wakatigaki.pop()

  # リスト型で値を返す
  return lis_r_wakatigaki

# ■■■■■■■■■■■■■■■
# ■ マルコフ連鎖を使う為に、実際にデータを作る関数
# ■ 引数:<class "list">:lis_p_markov_text
# ■ 戻り値:<class "dict">:di_r_data_markov
def d_e_f_make_data_markov(lis_p_markov_text):
  # 戻り値を格納するdictを初期化
  di_r_data_markov = {}
  for stri_p_markov_text in lis_p_markov_text:
    # janome(形態素解析ライブラリ)で分かち書きになった情報を、リスト型で得る
    list_wakati = d_e_f_wakatigaki(stri_p_markov_text)

    #【※】collectionsをロードしてインポートしたdequeはここで使う
    #■ N階マルコフ連鎖の、「N」を、ここで指定する
    dequ_n_markov = deque([], gi_n_markov_size)
    #■ BOS(文頭:the beginning of a sentence)を示すワードを追加
    dequ_n_markov.append(gstri_bos)
    for i in range(0, len(list_wakati)):
      #■【※】↓のprint文を解除してみると、動きが分かりやすいです
      #print("【追加済みのキーの状態】" + str(di_r_data_markov.keys()))

      #【※】キー情報は、tuple型で扱う
      tupl_markov_key = tuple(dequ_n_markov)
      # マルコフ連鎖用のデータにキーがなければ、ここで追加
      #(ここで、キー専用リストを作っていく)
      if tupl_markov_key not in di_r_data_markov:
        #■【※】↓のprint文を解除してみると、動きが分かりやすいです
        #print("①【これから追加されるキー】" + str(tupl_markov_key))
        di_r_data_markov[tupl_markov_key] = []

      # 対象のキーのところに、分かち書きした情報を追加
      di_r_data_markov[tupl_markov_key].append(list_wakati[i])
      # 新たなキー情報も追加
      dequ_n_markov.append(list_wakati[i])

    #【※】キー情報は、tuple型で扱う
    tupl_markov_key = tuple(dequ_n_markov)
    if tupl_markov_key not in di_r_data_markov:
      #■【※】↓のprint文を解除してみると、動きが分かりやすいです
      #print("②【これから追加されるキー】" + str(tupl_markov_key))
      di_r_data_markov[tupl_markov_key] = []

    #■ EOS(文末:the end of a sentence)を示すワードを追加
    di_r_data_markov[tupl_markov_key].append(gstri_eos)

  # dict型で値を返す
  return di_r_data_markov

# ■■■■■■■■■■■■■■■
# ■ マルコフ連鎖の結果を得る関数
# ■ 引数:<class "dict">:di_p_data
# ■ 戻り値:<class "list">:lis_r_make_markov
def d_e_f_do_composition(di_p_data):
  # 戻り値を格納するリストを初期化
  lis_r_make_markov = []
  #【※】collectionsをロードしてインポートしたdequeはここで使う
  #■ N階マルコフ連鎖の、「N」を、ここで指定する
  dequ_n_composition = deque([], gi_n_markov_size)
  #■ BOS(文頭:the beginning of a sentence)を示すワードを追加
  dequ_n_composition.append(gstri_bos)
  while(True):
    #【※】キー情報は、tuple型で扱う
    tupl_composition_key = tuple(dequ_n_composition)
    # ここでランダム処理を実行
    stri_composition_value = random.choice(di_p_data[tupl_composition_key])
    #■ EOS(文末:the end of a sentence)だったら、whileを抜ける
    if stri_composition_value == gstri_eos:
      break
    # 戻り値に追加する
    lis_r_make_markov.append(stri_composition_value)
    # 次の処理で使えるように、ランダム処理で取り出した値を追加
    dequ_n_composition.append(stri_composition_value)

  # リスト型で値を返す
  return lis_r_make_markov

# ■■■■■■■■■■■■■■■
# ■ 指定された回数を最大として、マルコフ連鎖の結果を得る関数
# ■ 引数:<class "dict">:di_p_markov_data
# ■ 戻り値:<class "list">:lis_r_composition_data
def d_e_f_get_composition_data(di_p_markov_data):
  # 戻り値を格納するリストを初期化
  lis_r_composition_data = []
  # 指定された回数を最大として、マルコフ連鎖の結果を得る
  for i in range(gi_try_markov):
    # マルコフ連鎖用に用意したデータで、作文をする
    #■【"".join(引数) 】
    #■ joinの区切り文字を「""=空文字」として指定。区切り文字なしの意味になる。
    #■ つまり、文字列はそのままjoin(結合)される。
    stri_composition = "".join(d_e_f_do_composition(di_p_markov_data))
    # 作文結果を、戻り値のリストに追加
    lis_r_composition_data.append(stri_composition)

  # 作文したものを、リスト型で返す
  return lis_r_composition_data

# ■■■■■■■■■■■■■■■
# ■ 「r」属性で開いたファイルの中身をリスト型にして返す関数
# ■ 引数:<class "str">:stri_p_r_file_path
# ■ 戻り値:<class "list">:lis_r_file_r_open_data
def d_e_f_file_r_open(stri_p_r_file_path):
  # 戻り値を格納するリストを初期化
  lis_r_file_r_open_data = []
  # 引数で指定されたパスのファイルを開く
  fil_open_data_r = codecs.open(stri_p_r_file_path, "r", encoding="UTF-8")
  try:
    # for文で一行ずつ取り出す
    for stri_open_data_r_row in fil_open_data_r:
      #【※】円記号「\」は、長いプログラム文の改行用
      #【※】(Windows環境以外ではバックスラッシュで長い文を改行して記述できる)
      # ファイルから取り出した内容を、改行コードで区切ってリストに格納
      lis_r_file_r_open_data.append( \
                  stri_open_data_r_row.rstrip(gstri_line_feed_code))
  finally:
    # ファイルclose
    fil_open_data_r.close()

  # リスト型で値を返す
  return lis_r_file_r_open_data

# ■■■■■■■■■■■■■■■
# ■ 指定した元ネタのファイルを読み込み、リスト型として扱えるようにする
lis_orig_sour_data = d_e_f_file_r_open(gstri_orig_sour_file_path)

# ■■■■■■■■■■■■■■■
# ■ マルコフ連鎖の結果を得る為に作られたデータをdict型で取得する
di_data_markov = d_e_f_make_data_markov(lis_orig_sour_data)
#■【※】↓のprint文を解除してみると、動きが分かりやすいです
#print(di_data_markov)

# ■■■■■■■■■■■■■■■
# ■ 作文したものを、リスト型で取得する
lis_composition_data = d_e_f_get_composition_data(di_data_markov)

# ■■■■■■■■■■■■■■■
# ■ 最終的に出力するデータを格納するリストを初期化
lis_output_data = []

# ■■■■■■■■■■■■■■■
# ■【※】フラグを確認して、重複データなどを、出力対象から外していく
for stri_composition_data in lis_composition_data:
  # 出力しないデータを見つけたら、Falseに変えるフラグ
  is_output = True

  # 元ネタに重複行があるかのチェック
  if gis_chk_hidden_word_orig_sour_f == True:
    for stri_chk_orig_sour in lis_orig_sour_data:
      # ■【※】↓改行コードが、比較の邪魔になるので「.strip()」を指定する
      if stri_chk_orig_sour.strip() == stri_composition_data:
        #■【※】↓のprint文を解除してみると、動きが分かりやすいです
        #print("元ネタにある:" + stri_composition_data)
        # 出力対象から除外して、内側のループを抜ける
        is_output = False
        break

  # 重複出力を許可しない場合、すでに同じものが生成されていたら出力対象から外す
  if is_output == True and gis_chk_hidden_duplicate == True:
    if (stri_composition_data in lis_output_data) == True:
        #■【※】↓のprint文を解除してみると、動きが分かりやすいです
        #print("すでに登録済みのデータ:" + stri_composition_data)
        is_output = False

  # 除外対象ではなく、出力対象であれば、出力対象リストに追加する
  if is_output == True:
    lis_output_data.append(stri_composition_data)

# ■■■■■■■■■■■■■■■
# ■ 出力
for stri_output in lis_output_data:
  print(stri_output)

掲載するとさらにゴチャゴチャしてしまうので省略しましたが、「除外キーワード指定」「出力最大・最小文字数指定」などの機能を加えると、とても使いやすくなります。
このプログラムに加えなくても、外部で簡単なプログラムを作ってもよいかと思います(自分は、分別用プログラムを、たくさん用意して作業しています)。
あと、長い文章を生成するのであれば、結果をファイルに出力すると扱いやすいです。

 

7
5
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
7
5