Edited at

中学生でも1日あれば作れる、人工無脳 Twitter Bot !


最初に補足しておきます。タイトルの「1日」はコードを書いている作業時間のことであり、この前改定されたとてつもなくめんどくさくて時間がかかる時もある Twitter API 申請認可 までの時間を含むものではありません。

あ、皮肉言ってるわけじゃないですよ?? だって僕の場合は普段の行いが良かったのか即時で承認され(ry



環境


  • Python 3.7.0 (pyenv でインストール)


開発環境


  • macOS Mojave 10.4

  • Atom 1.31.2


公開環境


  • Ubuntu 18.10


各種ライブラリなど


requirements.txt

mecab-python3==0.7

twitter==1.18.0


作ったもの

@HEISEIKUN_ という意味わからないことをつぶやくBotです。

following の TL から学習させて、意味わからないことをつぶやかせてます。

しゅうまい君 です。そのまんま。

例えば、

と、脈略ないけどまあとりあえずまともに喋ってたり、

このように意味のわからないこと言ってたりします。

Python 3 使いましたよ。


解説... の前に、一つ。

最初に書いておきます。一応、マルコフ連鎖(3階マルコフ??)はを用いたつもりです。

が、マルコフ連鎖の理論とかクソ難しい数式やら理論やらは理解できません。

中学生でも、仕組みさえ理解できれば簡単にできて、やばすぎて面白いので、全国の中学生の皆さんもぜひやってみてください、ということです。

要するに記事の内容では理論など細かい部分はすっ飛ばしてとりあえず完成させるということだけに集中しています。(バカ)

正しいこと言ってるわけでもありません。マルコフ連鎖もどきかも。

そしてできるだけ手抜きします。

とは言ってもある程度勉強はしなければいけない orz

あと、所どころスキップさせていきます。(手抜き)

ソースコード全部はこちらになります。

https://github.com/yu-san-19/Borscht


え? 環境構築してコピペしていろいろとめんどい?

そういう方々のために、PyPI にてモジュール化して公開していますよ!?

$ pip install borscht

Github: https://github.com/yu-san-19/Borscht

PyPI: https://pypi.org/project/borscht/

Reference: https://yu-san-19.github.io/Borscht/

日本語使用方法解説記事: 準備中

こんな感じで楽々すぐできる。


generate-sentence.py

# Import borscht.

from borscht import ChainGenerator
from borscht import TextGenerator

# Text used for chain generation of Markov chain
learned_text = """
 親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。
なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。
新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。
小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。
 親類のものから西洋製のナイフを貰って奇麗な刃を日に翳して、友達に見せていたら、一人が光る事は光るが切れそうもないと云った。
切れぬ事があるか、何でも切ってみせると受け合った。そんなら君の指を切ってみろと注文したから、何だ指ぐらいこの通りだと右の手の親指の甲をはすに切り込んだ。
幸ナイフが小さいのと、親指の骨が堅かったので、今だに親指は手に付いている。しかし創痕は死ぬまで消えぬ。
 庭を東へ二十歩に行き尽すと、南上がりにいささかばかりの菜園があって、真中に栗の木が一本立っている。
これは命より大事な栗だ。実の熟する時分は起き抜けに背戸を出て落ちた奴を拾ってきて、学校で食う。
菜園の西側が山城屋という質屋の庭続きで、この質屋に勘太郎という十三四の倅が居た。
勘太郎は無論弱虫である。弱虫の癖に四つ目垣を乗りこえて、栗を盗みにくる。ある日の夕方折戸の蔭に隠れて、とうとう勘太郎を捕まえてやった。
その時勘太郎は逃げ路を失って、一生懸命に飛びかかってきた。向うは二つばかり年上である。
弱虫だが力は強い。鉢の開いた頭を、こっちの胸へ宛ててぐいぐい押した拍子に、勘太郎の頭がすべって、おれの袷の袖の中にはいった。
邪魔になって手が使えぬから、無暗に手を振ったら、袖の中にある勘太郎の頭が、右左へぐらぐら靡いた。
しまいに苦しがって袖の中から、おれの二の腕へ食い付いた。痛かったから勘太郎を垣根へ押しつけておいて、足搦をかけて向うへ倒してやった。
山城屋の地面は菜園より六尺がた低い。勘太郎は四つ目垣を半分崩して、自分の領分へ真逆様に落ちて、ぐうと云った。
勘太郎が落ちるときに、おれの袷の片袖がもげて、急に手が自由になった。その晩母が山城屋に詫びに行ったついでに袷の片袖も取り返して来た。
 この外いたずらは大分やった。大工の兼公と肴屋の角をつれて、茂作の人参畠をあらした事がある。人参の芽が出揃わぬ処へ藁が一面に敷いてあったから、
その上で三人が半日相撲をとりつづけに取ったら、人参がみんな踏みつぶされてしまった。古川の持っている田圃の井戸を埋めて尻を持ち込まれた事もある。
太い孟宗の節を抜いて、深く埋めた中から水が湧き出て、そこいらの稲にみずがかかる仕掛であった。
その時分はどんな仕掛か知らぬから、石や棒ちぎれをぎゅうぎゅう井戸の中へ挿し込んで、水が出なくなったのを見届けて、うちへ帰って飯を食っていたら、
古川が真赤になって怒鳴り込んで来た。たしか罰金を出して済んだようである。
"""

# 夏目漱石 「坊っちゃん」より

cg = ChainGenerator(text) # Generate instance of class that generate chains
cg.dump() # Dump json file written about chains' info.
tg = TextGenerator("chain.json") # Generate instance of class that generate sentence

for i in range(10):
sentence = tg.generate() # Generate sentence
print(tg.generate())



0-a. マルコフ連鎖って何だ?

ggrks 精神です。

ちなみに僕が参考にしたサイトです。

マルコフ連鎖の話

https://dev.classmethod.jp/machine-learning/markov-chain/

マルコフ連鎖を使って〇〇っぽい文章を自動生成してみた

https://www.pc-koubou.jp/magazine/4238

[作り方を徹底解説] 人工知能が書いたAI入門ブログ

https://ai-kenkyujo.com/2017/10/31/markov-chain-commentary/

一応、最終的には数式のあたりで完全理解やめました。

かろうじてわかったことは、

分かち書き(形態素解析)して、

ある単語の組み合わせの次にくる形態素の確率を求める。

例えば、

「明日は」 という単語 (2つになってるけど) に対して、


  • 「晴れ」 50%

  • 「曇り」 20%

  • 「雨」 10%

  • 「飴」 5%

  • 「祭り」 1%

  • そのほか 4%

みたいな統計が取れたとすると、

とりあえずそれに則って繋げていけば自然(なはず)だよねーといった感じに理解しました。

(間違ってたら適宜コメント or 編集リクエストください)


0-b. どうやって作るのか?

とりあえず、確率の統計にしたがって単語を選んでく様子をどう表現するかで迷いました。

ー 迷ったら 「Github 見ましょう。あそこは宝庫です...」 という天の声が聞こえたので速攻検索。

そしてちょうどいいの見つけました。

https://github.com/o-tomox/TextGenerator

一瞬、これ使えばいいやんと悪魔の声が聞こえましたが、

Python 2、 そして DB を用いていたので断念...

とりあえず読んで参考にする程度にしました。

あらーくまとめると、

1. 学習元のテキスト収集

2. 形態素解析をする
3. 3つ連続した形態素の三つ組を作る
4. 出てきた三つ組を全部配列Aに代入
5. なんか最初の単語を一つ勝手に選ぶ
6. 配列Aからその単語で始まる三つ組全てを抽出して別の配列Bに代入
7. 配列B からランダムで選ぶ
8. 選ばれた3つ組の最後の単語から始まる三つ組全てを抽出して配列Bに代入
9. 文の終わりまで繰り返す
10. できた分を Twitter へ投稿

と言った感じにすればいいようです。


0-c. ディレクトリ構成

root/

  ├ - get-tweets.py
  ├ - post-tweets.py
  ├ - ChainGenerator.py
  └ - TextGenerator.py


1. 学習元のテキスト収集

学習用のデータが必要なのですが、やはり作る上では、現在な言葉を使ってて、比較的軽くて話し言葉がいい。と、勝手に決めてました。

どうするか。

Twitter という素晴らしいツールがあるではないか!

Twitter の API 申請も結構ドキュメントあるので割愛します。

さて、 TL の取得ですが、 REST API なので、普通にAPI叩くためのModule使って叩いてもいいです。

ですがTwitter用のModuleがあったみたいなので、それ使います。

$ pip install twitter


get-tweets.py

from twitter import *

import json
import re

twitter_clients = Twitter(
auth=OAuth(<ACCESS_TOKEN>, <ACCESS_TOKEN_SECRET>, <API_KEY>, <API_SECRET>)

timelines = twitter_clients.statuses.home_timeline(count=200) # 最大200件TLを取得

with open('tweets.txt', 'a') as f:
for tl_tweet in timelines:
content = tl_tweet['text'].replace("\n", "")
result = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", content) # URLを除去
result = re.sub(r'RT @.+: ', "", result) # RT情報を除去
f.write(result + '\n')


こんな具合にすれば普通に取れる && DUMP できます。

以上!


2. 形態素解析

将来的には是非ともこの機能も独自で実装したい... のですが、

少々難しいというかめんどくさい...

今回は「とりあえず完成させる」を目標にしてるので、ここは外部ツールに身を任せます。

JUMAN とか、 JANOME とかもいいのですが、とりあえず比較的豊富な資料が見つかった MeCab を使用することに。

Mecab を python 3 で使えるようにするモジュールもあるのでそれを用いていろいろ頑張ります。

やり方は探せばいくらでも出てくるので今回は割愛。


3. 連鎖と言うかチェーンというか辞書というかの部分を実装

コード全容はこちらです。

コメント書いていますが、


  • 文字とって

  • 使いやすいよう加工して

  • 形態素解析して

  • 三つ組作って出現回数カウントして

  • JSONでdump

といったことをしています。

ここで、指定の三つ組が出てくる数をカウントするのがミソになると思ってます。

保存する方法、DBを使いたくない...という意気込みで作ったのですが、

書いてて、「あ、これDB使ったほうが早いわ」になりました。

また、文頭と文末がわかるようにそれぞれの三つ組のうち、


  • 文頭: BOS (Begin Of String) を最初の要素へ

  • 文末: EOS (End Of String) を 最後の要素へ

入れることにします。

今回は json を用いたのですが、

[

[
"BOS", # 一つ目
"これ", # 二つ目
"は", # 三つ目
2 # 出てきた回数
],
[
"これ",
"は",
"大",
1
],
[
"は",
"大",
"思春期",
2
]

といった感じにしています。


4. DUMPした情報をもとに学習・文章生成

コード全容はこちら です。


  • 情報を読み込んで

  • 出現回数に基づいて、形態素の三つ組(タプルにしています) を配列 chain に代入していく


    • 出現数:1 => 1回(個?/要素?)代入

    • 出現数:4 => 4回(個?/要素?)代入



  • 次に最初の三つ組として BOS が 1つめの要素を chain からランダムに選ぶ

  • 選ばれた要素と、1番目, 2番目 の形態素が一致するのをまた別の配列へ (A)

  • その配列からランダムに選ぶ (B)

  • 3つめが EOS の形態素の三つ組が出てくる(文末)まで A, B を繰り返す

といったことをリファクタリングされずにやっています。

じきにもうちょっと手を加える予定です。


5. Twitter へ投稿

これも普通に探せば出てくるので割愛しますが、

from ChainGenerator import *

from TextGenerator import *
from twitter import *

# tweet = 生成された文章

twitter_clients.statuses.update(status=tweet)
print("Finished Tweet: " + tweet)

こんな感じにしておけばOKです。


最後に

これでなんとなく @HEISEIKUN_ が完成したわけです。

すごく長い文章かつ読みづらいことがこの上ないのは自覚しているので、

後日見直して適宜書き換えていくのだろうと思います。

結局何が言いたかったのか、というと、



  • @HEISEIKUN_ をフォローしてください!

  • 結構簡単に中学生でもできちゃったし、やばすぎて面白いので、皆さんもぜひやってみてください!

ということです。

ちなみに、次のステップとして形態素解析を自前で作ってみたいという気持ちが出てきてしまいました... :sweat_smile:

頑張って勉強してみます...

間違っている情報や、改善すべき場所などあれば是非ともコメントや編集リクエストをいただければ..! と思います^^