🐦

Pythonでマルコフ連鎖を使って自分のツイートから新たな文章を生成する(2023年3月版)

2023/04/02に公開

はじめに

今年のエイプリルフールに、Twitterで「自分は実はAIでした」というネタをやりました。
そして、その一環として過去の自分のツイートからマルコフ連鎖を使用してツイートを生成してみました[1]

せっかくなのでそのやり方を簡単にメモしておきます。

基本方針

https://qiita.com/hitsumabushi845/items/647f8bbe8d399f76825c

基本的にこちらの記事を踏襲する形で作業していきます。

ただし、現在はTwitterからダウンロードできる自分の全ツイートはtweets.csvではなくtweets.jsに入っているため、主にその仕様変更に従って前処理を変更していく必要があります。

前処理

全ツイートのダウンロード

Twitter Web > 設定とプライバシー > アカウント > データのアーカイブをダウンロードから、アーカイブをリクエストします。

詳しくは

https://www.howtonote.jp/twitter/helpful/index1.html

こちらの記事などを参照してください。

数日後ぐらいにアーカイブが作成されたら通知が届きます。
アーカイブをダウンロードしたら、zipファイルを解凍し、中のdata/tweets.jsを任意の作業ディレクトリにコピーor移動しておいてください。

tweets.jsからツイートの抽出

前述のとおり、全ツイートはtweets.jsにJSON形式で入っていますので、そこから全ツイートを抽出する必要があります。

下記のPythonコードをtweets.jsと同じディレクトリに保存してください。

extractTweetsJsToText.py
import json
import re
from datetime import datetime 

read_since = '2020-04-01T00:00:00+09:00'

retweet_pattern = re.compile(r'RT .*\n$')
reply_pattern = re.compile(r'@[\w]+\s')
url_pattern = re.compile(r'https?:\/\/[\w/:%#\$&\?\(\)~\.=\+\-]+')

def exclude_specific_pattern(s: str):
    s = retweet_pattern.sub('', s)
    s = reply_pattern.sub('', s)
    s = url_pattern.sub('', s)
    return s

def escape_special_chars(s: str):
    return (s.replace('\n', '\\n'))

def is_newer_than(twitter_date_str: str, comparison_date: datetime):
    date_format = '%a %b %d %H:%M:%S %z %Y'
    parsed_date = datetime.strptime(twitter_date_str, date_format)
    return parsed_date > comparison_date

def load_tweets_json(tweets_js_file: str = 'tweets.js'):
    with open(tweets_js_file, 'r', encoding='utf-8') as file:
        content = file.read()
        cleaned_content = content.replace('window.YTD.tweets.part0 = ', '', 1)
    return json.loads(cleaned_content)

def write_tweets_txt(tweets_json, output_file: str = 'tweets.txt'):
    since_date = datetime.fromisoformat(read_since)
    with open(output_file, 'w', encoding='utf-8') as output:
        for tw in tweets_json:
            tweet = tw["tweet"]
            if not is_newer_than(tweet["created_at"], since_date):
                continue
            tweet_text = escape_special_chars(tweet["full_text"]) + '\n'
            tweet_text = exclude_specific_pattern(tweet_text)
            output.write(tweet_text)

if __name__ == '__main__':
    tweets_json = load_tweets_json()
    write_tweets_txt(tweets_json)

このコードでは、

def exclude_specific_pattern(s: str):
    s = retweet_pattern.sub('', s)
    s = reply_pattern.sub('', s)
    s = url_pattern.sub('', s)
    return s

def escape_special_chars(s: str):
    return (s.replace('\n', '\\n'))

の部分で

  • 改行\nのサニタイズ
  • リツイートしたツイートの除去
  • リプライやメンションに含まれるTwitter IDの除去
  • URLの除去

をおこなっています。

このあたりは好みや目的によって適宜パターンを追加・削除してください。

また、「どの日時以降のツイートを抽出するか」を変更したい場合、下記の行を変更してください。

read_since = '2020-04-01T00:00:00+09:00'

ここで指定日時以降のツイートのみを抽出している理由としては、

  • 抽出されたツイートが多すぎると、単純にこの後の処理が時間がかかって重くなってしまう
  • あまり昔すぎるツイートを含めてしまうと、生活環境の変化などによって使用する単語が異なってきているため、生成する文章に「今の自分っぽさ」が出なくなってしまう[2]

といった理由です。

python extractTweetsJsToText.py

を実行して、同じディレクトリにtweets.txtが生成されていることを確認します。
tweets.txtには1行1ツイートの形式でツイートが抽出されています。

データベースの作成

MeCabのインストール

MeCabをインストールしていない方は、事前にこちらからインストールしておいてください。

また、辞書はUTF-8のものをインストールしておいてください。

同様に、PythonでMeCabが利用できるよう、

pip install mecab

などでPython用パッケージをインストールしておいてください。

文章生成プログラムのダウンロード

実際に文章を生成するプログラムとして、

ohshige15/TextGenerator: マルコフ連鎖を使った文章自動生成プログラム

こちらのPythonスクリプトを使用します。

git clone git@github.com:ohshige15/TextGenerator.git

などで上記リポジトリをクローンし[3]、中の

  • GenerateText.py
  • PrepareChain.py
  • schema.sql

を作業ディレクトリにコピーしてください。

基本方針で挙げた参考記事にあるstoreTweetstoDB.pyを使用させていただき、データベースを生成します。

ただし、今回は抽出したツイートがCSVではなくテキストファイルにベタ書きされていますので、それに対応した修正を行っています。
下記は修正後のstoreTweetstoDB.pyです。

storeTweetstoDB.py
from PrepareChain import *
from tqdm import tqdm

def storeTweetstoDB():
    
    with open('tweets.txt', 'r', encoding='utf-8') as f:
        tweets = [line.strip() for line in f]

    print(len(tweets))

    chain = PrepareChain(tweets[0])
    triplet_freqs = chain.make_triplet_freqs()
    chain.save(triplet_freqs, True)

    for i in tqdm(tweets[1:]):
        chain = PrepareChain(i)
        triplet_freqs = chain.make_triplet_freqs()
        chain.save(triplet_freqs, False)

if __name__ == '__main__':
    storeTweetstoDB()

上記コードを作業ディレクトリに保存し、

python storeTweetstoDB.py

を実行します。しばらく待つと、chain.dbが生成されます。

文章の生成

これで準備はできました。
試しに100個ほど文章を生成してみましょう。

generateMarkovTweets.py
from GenerateText import GenerateText

with open('generated_tweets.txt', 'w', encoding='utf-8') as f:
    for _ in range(100):
        generator = GenerateText()
        tweet = generator.generate()
        f.write(f'{tweet}\n')

上記コードを作業ディレクトリに保存し、

python generateMarkovTweets.py

を実行します。しばらく待つと、generated_tweets.txtにマルコフ連鎖で生成された文章が100行ぶん保存されています。

あとは生成結果を眺めて楽しんだり、Botを作って自動でツイートさせたり、様々な楽しみ方ができるかと思います[4]

おわりに

tweets.jsからのツイートの抽出は、他の用途、たとえば「Notionなどのメモアプリにツイートをバックアップしておき、後で検索しやすいようにしておく」など応用が効くかもしれないな、と思いました。

また、マルコフ連鎖による文章生成も、アイディア次第で様々なネタに使えそうです。

参考になれば幸いです。

参考文献

脚注
  1. 最初はChatGPTやNotion AIにツイートする文章を考えさせようと思っていたのですが、自分の過去のツイートを学習させても、あまり「自分っぽい」文章になりませんでした。やはり「自分っぽい」ツイートを生成するのはマルコフ連鎖が一番良いようです。 ↩︎

  2. 具体的には、たとえば自分の学生時代のツイートから単語を拾ってきてしまい、社会人になってからはめったに使わなくなった「講義」とか「学食」とかみたいな学生っぽい単語が含まれた文章が多数生成されてしまうなど。 ↩︎

  3. 参考にさせていただいたQiita記事では、Python3への対応が必要と記載されていましたが、自分が試した時点では既にPython3への対応が済んでおり、特に対応不要でした。 ↩︎

  4. 昨今のTwitter社によるAPIへの締め付け強化の影響もあり、筆者はリアルタイムで生成してツイートするBotを作成する方法をとれませんでした。そこで、泣く泣くあらかじめ生成した文章を公式Webクライアントの予約投稿でツイートする方式をとりました。 ↩︎

GitHubで編集を提案

Discussion