17

【Python+MeCab】マルコフ連鎖と形態素解析を使って本田圭佑っぽいTwitterつぶやきを自動生成してみた話

はじめに

以前、TwitterでBotを作ってアフィリエイターとして活動していた頃は、
文章は全部、自分で編集なんかしてて、めちゃくちゃ時間かけてたけど、

2021/5/13に初めて、
形態素解析とマルコフ連鎖を使えば、自動で文章が生成できることを知り、感動したので、
実践してみました。

ちなみに、前の会社でインターンをやっていた大学生の子が、
自分のTwitterアカウントのつぶやきを解析させて、
自動生成したつぶやきをBot化していたので、
まずは「自分もエンジニアになったからには、そのスタートラインに立とう!!」
と思い立った次第であります。

※最初、座学多めです。下の方にコードあります。🙆‍♂️

形態素解析とは

難しい漢字が並んでてゲシュタルト崩壊したわっww
と言いたいところですが、

形態素解析(けいたいそかいせき、Morphological Analysis)とは、文法的な情報の注記の無い自然言語のテキストデータ(文)から、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する作業である。
参照元:Wikipedia

ふむふむw
「けいたいそかいせき」
だそうですw

形態素解析ソフトのMeCabはこんな感じに文章を分解してくれる

$ echo "魚を買う。" | mecab
魚 名詞,一般,*,*,*,*,魚,サカナ,サカナ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
買う  動詞,自立,*,*,五段・ワ行促音便,基本形,買う,カウ,カウ
。 記号,句点,*,*,*,*,。,。,。
EOS

イメージ掴めた。(⌒▽⌒)

MeCabのインストールはこちらを参照ください。

マルコフ連鎖とは

マルコフ連鎖(マルコフれんさ、英: Markov chain)とは、確率過程の一種であるマルコフ過程のうち、とりうる状態が離散的(有限または可算)なもの(離散状態マルコフ過程)をいう。また特に、時間が離散的なもの(時刻は添え字で表される)を指すことが多い(他に連続時間マルコフ過程というものもあり、これは時刻が連続である)。マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である(マルコフ性)。各時刻において起こる状態変化(遷移または推移)に関して、マルコフ連鎖は遷移確率が過去の状態によらず、現在の状態のみによる系列である。特に重要な確率過程として、様々な分野に応用される。
参照元:Wikipedia

ま る で な に を 言 っ て い る の か 、 全 く わ か ら な い 。 。 。

わざわざ分かち書きしてしまうほどでしたorz

実際にマルコフ連鎖でやること

例えば、下記の4つの文章がある時、
まずは形態素解析で分かち書き出力(文章において語の区切りに空白を挟んで記述すること)したものを用意し、辞書を作る。

文章

  • 魚 を 買う 。
  • 魚 は 好き 。
  • 魚 は かわいい 。
  • 猫 は かわいい けど 高い 。

マルコフ連鎖の辞書

一つ一つの単語をスライドしていって登録することになる。
1~3語の三つの組み合わせがブロック。(※単語数は可変にできるはず。)
「名詞」「形容詞」「動詞」は、文章の意図を示していることが多いと想定され、始点の単語で選ばれるようにする模様。

1語目 2語目 3語目
買う
買う
好き
好き
かわいい
かわいい
かわいい
かわいい けど
かわいい けど 高い
けど 高い

単語の組み合わせと選択ロジックの例

  1. 始まりが「魚 は」の場合、次に「は」から始まるブロックを探す。
  2. は 好き 。」「は かわいい 。」「は かわいい けど」からランダムに続くブロックが決定される。
  3. 同様の作業を「(文末)」がくるまで繰り返す。
  4. 魚 は かわいい けど 高い 。」、「猫 は 好き 。」といった文章がランダムに生成される。

ちなみに、
1ブロックあたりの単語数が多くなればなるほど、元の文章に近いものが生成される。
一方で、少なくなればなるほどぐちゃぐちゃな文章になる
ようです。

なんとなく分かってきたので、、、

マルコフ連鎖とMeCab形態素解析でやりたいこと

本田圭佑っぽいツイッターつぶやきを生成しちゃおう!!!!

ファイル構成

app
├── text_editor.py     # テキスト精製
├── make_dictionary.py # 辞書作成
├── tweet.py           # つぶやき生成と出力
└── text
    ├── keisuke_honta.txt     # 精製前のRawデータ
    ├── new_keisuke_honta.txt # 精製後のテキストデータ
    └── learned_data.json     # 辞書データJSONフォーマット

事前準備

$ pip install markovify
$ pip install mecab-python3

本田圭佑の名言的なつぶやきを取得する

まだTwitterAPIの申請が降りないので、
All My Tweetsを使って、ツイートを取得しよう。
スクリーンショット 2021-05-14 18.16.03.png

公式アカウント+Bot3つからテキスト取得

本田圭佑 bot @hondak_bot
本田圭佑bot @ksk_nonrotation
本田圭佑bot @hondahanpanai4

つぶやきテキストを精製してキレイなテキストファイルに

text_editor.pyのコード

純粋な日本語の文章だけ欲しかったので、
RT @warpspaceとかApr 19, 2021とかその辺を除去する。

text_editor.py
import MeCab
import re

text = open("./text/keisuke_honda.txt","r").read()

# Replace Bad Symbols for markovify to function
# Refer: https://github.com/jsvine/markovify/issues/84
table = str.maketrans({
        # '\n': '',
        # '\r': '',
        '(': '(',
        ')': ')',
        '[': '[',
        ']': ']',
        '"':'”',
        "'":"’",
    })
text = text.translate(table).split()
url_pattern = "https?://[\w/:%#\$&\?\(\)~\.=\+\-]+"

for i in range(10): # remove()を使ってるから10回くらいやると綺麗になった
    for line in text:
        if re.match(url_pattern, line): # URL
            text.remove(line)
        elif bool(re.search(r'[a-zA-Z0-9]', line)):
            text.remove(line)
        elif re.match('^@.*', line):
            text.remove(line)
        elif re.match('.*,$', line):
            text.remove(line)
        elif re.match('^#.*', line):
            text.remove(line)
        elif re.match('RT', line):
            text.remove(line)

# Parse text using MeCab
m = MeCab.Tagger('-Owakati')
f = open('./text/new_keisuke_honda.txt', 'w')
for line in text:
    splited_line = m.parse(line)
    f.write(str(splited_line))
f.close()

精製前

ごちゃついてるなあ。。。

keisuke_honda.txt
RT @warpspace_inc: 【プレスリリース】 宇宙フロンティアファンドや @kskgroup2017 が率いるKSK Angel Fund LLC、SMBCベンチャーキャピタル産学連携2号投資事業有限責任組合等を引受先とした第三者割当増資による4億円の資金調達を実施… Apr 19, 2021 
Super guests visited our game yesterday! 🇦🇿 https://t.co/JHWX77DuVa Apr 18, 2021 
次は決める! Apr 18, 2021 
I will be on @NowVoice_jp about yesterday's game at 14:15. Apr 18, 2021 
本当に必要な情報にたどり着くために、時間とお金をかけ続ける。 Apr 15, 2021 
...

精製後

キレイになった。1行1行、分かち書きもされて、文末では改行も入ってる。

new_keisuke_honda.txt
恋愛 っていう もの に は 必ず 喧嘩 を し て しまう と 。 
その 時 の 状況 に 寄っ て 最悪 別れ て しまう 人 も 居る と 。 
でも よく 考え て み て 下さい よ 。 
思い出 の 方 が 沢山 ある し 、 お互い 笑い 合っ た 数 の 方 が 多い ん や し 、 そんな 数少ない 喧嘩 で 負け て どう する ん や と 。 
...

精製テキスト使って、マルコフ連鎖用の辞書登録

make_dictionary.pyのコード

markovify.NewlineText()を使っているのは、
句読点「。」で文章の終わりを示すために改行を入れて、1行1行にしたテキストをちゃんと読み込ませるため。
new_keisuke_honda.txtでも分かる通り、こうすることでちゃんと学習されて辞書登録がスムーズにいくように。

ちなみに、英語とかだと.で勝手に判定されるからmarkovify.Text()でいいけど、日本語は少し工夫が必要。

make_dictionary.py
import markovify

# Load file
text = open("./text/new_keisuke_honda.txt", "r").read()

# Build model
text_model = markovify.NewlineText(text, state_size=3, well_formed=False)

# Make Dictionary as Json_format
with open('./text/learned_data.json', 'w') as f:
    f.write(text_model.to_json())
  • state_sizeは、1ブロックあたりの単語セットで、次の単語の確率が依存する単語の数のこと。(参照元)
    • 例えば、「魚 は」の方が、「魚 は かわいい けど 高い 。」よりも、後続のアイテムを見つけるのがもっと簡単になる。
    • 1ブロックあたりの単語数が多くなればなるほど、元の文章に近いものが生成される。一方で、少なくなればなるほどぐちゃぐちゃな文章になるようです。
  • well_formed=Trueにすると、markovifyで読み込むことができない単語があるとKeyError: ('___BEGIN__', '___BEGIN__')が発生するから、falseにしておくといい感じになる。
    • ただし、mal_formedで読み込みができない文章は無視される模様

learned_data.jsonが出力される

一部のみ紹介

{
  "state_size": 3,
  "chain": "[[[\"___BEGIN__\", \"___BEGIN__\", \"___BEGIN__\"],
             {\"\恋\愛\": 2, \"\そ\の\": 22, \"\で\も\": 31,
              \"\思\い\出\": 1, \"\ま\ぁ\": 13, \"\俺\": 191, \"\脱\": 1,
...

マルコフ連鎖用に生成した辞書を使ってつぶやきを作成

tweet.pyのコード

tweet.py
# -*- coding: utf-8 -*-
import markovify

with open('./text/learned_data.json', 'r') as f:
    text_model = markovify.Text.from_json(f.read())

for i in range(10): #10個のつぶやき生成
    print(text_model.make_short_sentence(140))

自動生成されたつぶやきを見てみる

・自分は何でもかんでも理由をつけて勝ち上がっていくことが大事。
・好きな事だけではなく継続して、俺は次の試合に向けて自分を高めましたよね。。
・信じることっていうのは、僕にとって希望なんですよね。そういうサッカー選手としてだけでなく、批判してくれました👍
・ありがとうございます!ちょっと考えてください。仕事つくってブラジルに来てください😁🇧🇷⚽️
・おれもあいつらに“欲”という意味ではホントいい感じできてますけどね
・そろそろ皆さんも旅立ちの日ですと。だからなんだと。
・ボール来るなって言っても遅い
・革命家ですね。この壁だって神様に感謝しないと上にはいけない
・色々と周りが騒がしいけど、俺は選ばれてたのかなと。
・俺のパンチング見たか?ナイス飛び込みやろ。
・昔の選手も好きですけど、やはりみんなで戦っているんじゃないかという風に思いますね
・僕なんかゴール尽きまくってますよ。自分の情熱が弱かったと。
・あの白いユニフォームには特別な思いがありますよ。自分の用事が終わったら、すぐドリブルしようかと思っている。その二つは連動してはいるけど。
・嘘でもいいから「わかる」って言ってもなくならないし、正直
・スポーツを止めるな!素晴らしい活動は皆んなで応援したいというものに投資します!
・どこでプレーしても、自分の人生に誇りを持ってくれるまで突き進みますよと。

本田圭佑っぽいwwwめっちゃ言いそうwww深良さがより深まっているwwww

エモいものありました?w

ハマったポイント

  • 最初のテキスト精製のところがなかなか難しかった。
    • list.remove()って最初にヒットしたものしか消さないので、何個もテキスト上にあると、ゴミが残ってしまうから、forを10回とか15回とか回す必要があるかも。
    • もっと効率の良い方法があるんだけど、突貫でやったからまだ考えきれていない。
  • markovifystate_sizeの意味と、well_formedの意味がなかなか掴めなかったので、難しかった。
  • well_formed=Trueにすると今でも、key errorが出てしまうから、理由がまだ分かっていない。
    • githubで言われている、シンボルたちは全て置換したんだけど、なんでだろう、、、、
  • 日本語は、句読点を目印に、文章を1行ごと改行して、NewlineText()を使った方が相性が良いということを知らなかったから、テキスト精製のところで、つまずいた。

感想とまとめ

できた時は、まじで、めーっちゃテンション上がった!ぶち上げwwww(☝︎ ՞ਊ ՞)☝︎

素直に面白いw
自分は本田圭佑のファンなので、

バーチャル本田圭佑が誕生した瞬間を目の当たりにして感動したwwwwwww

次の目標は

  • twitterに生成したつぶやきを自動投稿する「本田圭佑っぽいことをつぶやくBot」を作成すること
  • Twitter APIを使って、本田圭佑だけでなく、他の著名人もトライしてみること。
  • いずれはLSTMとか自然言語処理系の機械学習モデルも使って挑戦すること。

最後に、マルコフィファイってまじで言いにくい。特にvifyのところ、めっちゃ吐息出るわw

以上、ありがとうございました!!!

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ichi_zamurai
フルスタック目指すデータエンジニア。 異色な経歴ですがw 🇺🇸大卒 →イベント系設営派遣 →元消防士 →アフィリエイター →ITマーケター(2社経験) →Java習得@ハロワ →エンジニアデビュー@31歳 →AWS中級制覇@33歳 →現在のカオスに至る

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
Azure Kubernetes Serviceに関する記事を投稿しよう!
~