【GPT-2, Twitter API】平沢進氏のツイートを学習して文章生成してみた

## 環境
+ Microsoft Windows 10.0.22000.613（21H2）
+ Windows PowerShell 5.1.22000.613
+ conda 4.12.0
+ Python 3.8.12.final.0
+ tweepy 4.8.0
+ csv 1.0
+ PyTorch 1.11

## はじめに
### 1. モチベーション
+ Twitter API を使ってみたい
+ GPT-2 を試してみたい

### 2. 誰のツイートにするか
まず、真面目な雰囲気の文章よりも**個性的**で**独特**な文章の方が学習の成果がわかりやすいのではと考えた。そこでコウメ太夫氏が思い浮かんだが調べてみると既に行われていた。

+ [人工知能にコウメ太夫さんのツイートを学習させてみた](https://blog.takuya-andou.com/entry/ai_koume)
+ [AIにコウメ太夫のツイッターを1000件学習させてみた](https://www.youtube.com/watch?v=cWepLWzDjIM)


今回はまだ誰も行っていないようだったので[平沢進](https://ja.wikipedia.org/wiki/%E5%B9%B3%E6%B2%A2%E9%80%B2)氏のツイートを学習することにした。（次回はツイートではないが[夢野久作の作品](https://www.aozora.gr.jp/index_pages/person96.html)で試したい。）

そこで、平沢氏のツイートを観察し特徴を探してみた。

#### 特徴
+ 改行・空行が多い
+ だ・である調が多め
+ 独特な言葉選び
+ 偶に末尾に「またこんど！！」と書く

https://twitter.com/hirasawa/status/1512404379994767369?s=20&t=NzyN5Q7PZpASmPg1rB9flg


## 生成された文章
学習データ：4/18以前のツイート約1000件
字数制限なし

GPT-2 の精度には本当に驚く。[特徴](#特徴)を捉えていて個人的には全然違和感がなかったが、実際のところはどうなのだろうか。もし平沢氏のフォロワーの方が読まれていたら、是非ご意見・ご感想を頂ければと思う。（[評価はこちらから](https://forms.gle/C2dYbvx1H2qaHro77)）

```:生成例
今はその時代に突入したが、私はこのように考察している。

「世界は私から私へと移行を始めた」

ある意味、ヒラサワ枠がハエトリグモの中に突入してから2時間ほど時間が過ぎた。

それぞれの環境で長いのか、短いのか。
========
ここにきて私の人生は半永久的に停止線に巻き戻されることになる。

世界は半永久的に停止線の中に巻き戻されることを知る。
それなのに、世界は半永久的に停止線の中に共存している。

ところが、世界は半永久的に進行する。そこに「時間」という概念がなければ、「止まった」世界はこんなにも簡単に存在しない。

世界は半永久的に止まっている。
========
私は小学生の時からダンゴムシは大嫌いだ。理由なく脱皮する脱皮Ｇの脱皮を見て「これはダンゴムシじゃない、ダンゴムシじゃない」と言いたくなる。脱皮しようがしまいが、ダンゴムシを食べても脱皮しなくてもいいと思う。私の嫌うのは、脱皮したものから着るものだ。脱皮は脱皮という方法でどこに投げても可能である。

ただし脱皮の逃げ方が悪いと脱皮の意味が抜けてしまう。
========
ステルス回転する人造人間に見えなくてもよかったものを。

それくらい私の身体は機械だらけだ。
========
貴方の心に生じた澱みに貴方の理を刻む日が来る日まで。
========
もう無理無理と絶望しながらもう無理と諦めようとした時、出会った。

とてもそうは見えない光景が見えた。

またこんど！！
========
ステルスQW・スタジオ・ライブに使用したPENONのスタッド・カーキーを２年間無駄使いした後に現在の東京のiモードに切り替えることにした。。。。

これであいつもステルスQW・スタジオ・ライブに出る。

またこんど！！
========
P-MODELの歌詞には「歌わなければどうぞ」という明確な境目がない。

その先に「あれこれやっていることを無駄に時間を費やす」などという表現があるかのようなタイトルと、「もう無理」と言っているかのようなタイトルの差が生じる。

それだけ短絡的思考回路が発達しているということ。
========
あ、騙されてる。騙されてるんだから仕方ないでしょう。とはいえ、そこは私の落ち度ゼロから来るものである。実際、この間はプロデューサーさんからアドバイスを受けたけど。もう少し落ち着いてくださいな。もう少し面白いキャラが居るんですよ。ああ、私を馬鹿にしないで。はい、もういい。
========
ある文化の日はその日にミュージカルが公演され、文化の日はその日に絵が投影される。

そのミュージカルは舞台上の舞台上からであり、舞台上の関係者から提供されるものではない。

私が創造したわけではない。

舞台の外に投影されているだけだ。
```

## 実際にツイートを投稿
アカウントを作って投稿してみた。

https://twitter.com/ai_hirasawa/status/1521163421202653185

ここからは実装の説明をする。

## 準備
ここでは簡単に説明する。以下のサイトに詳しい手順が書かれている。

https://bloomtectec.com/twitter-api-application-procedure/
### 1. Twitterの開発者アカウントを作成
T予め Twitter にログインしておく。https://developer.twitter.com/ja にアクセスし、「申し込む」から開発者アカウントを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/80d170cb-95cf-ef07-bba8-55da69c700b5.png)

### 2. Bearer Token を取得
あとで使うのでコピーしておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/34850b57-1ff6-2825-1eb5-bfd126212d33.png)

### 3. User authentication の設定
[Developer Portal](https://developer.twitter.com/en/portal/) で App settings をクリックし、User authentication settings で OAuth 2.0 を有効にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1946472/14b6c6fc-587b-1186-b5f0-8a50894dc0f3.png)

テスト用なので適当に設定した。
Type of App：`Automated App or bot`
Callback URI / Redirect URL：`http://120.0.0.1/`（ローカルホスト）
Website URL：`自作サイトのURL`

## コード
### 環境変数の設定
```py:.env
BEARER_TOKEN = "Bearer Token"
```

### 環境変数を取得
```py:main.py
import os
from dotenv import load_dotenv

# .envを読み込む
load_dotenv()

# 環境変数を取得
BT = os.getenv("BEARER_TOKEN")
```
https://note.com/yucco72/n/nb52bfb6d65bb

### 認証
<details><summary>Anaconda で tweepy をインストールする</summary><div>
詳しくは <a href="https://anaconda.org/conda-forge/tweepy">conda-forge / packages / tweepy</a> を参照

```posh:PowerShell
conda install -c conda-forge tweepy
```
</div></details>

```py:main.py
import tweepy

# Authentication（Twitter API v2）
# OAuth 2.0 Bearer Token (App-Only)
client = tweepy.Client(BT)
```
https://docs.tweepy.org/en/latest/authentication.html#id2

### ユーザIDの設定
以下のサイトで、ツイートを取得したいユーザのユーザネーム（ `@` を除く）を入力して Convert する。

https://tweeterid.com/

```py:main.py
user_id = "User ID"
```

### ツイートの取得
csvファイルにTweet IDとツイート内容を記録した。空行ができるのを防ぐため、ツイート内の改行 `\n` は `<n>` に置換した。また、いくつか画像を含んでいるツイートがあったので手動で削除した。

```py:main.py
tweet_data = []
with open(CSV_PATH, mode="a", encoding="utf-8") as f:
    writer = csv.writer(f)
    for tweet in tweepy.Paginator(client.get_users_tweets, id=USER_ID, max_results=100, exclude="retweets", until_id="ここで指定したTweet IDより古いツイートを取得できる").flatten(limit=取得したいツイート数を指定):
        tweet_data.append([tweet.id, tweet.text.replace("\n", "<n>")])
        writer.writerow([tweet.id, tweet.text.replace("\n", "<n>")])

# 確認
print("\n-------------------------------\n".join(["Tweet ID: "+str(x[0])+"\n"+x[1] for x in tweet_data]))
```

```:出力の一部
Tweet ID: 1508423085086883843
通常は一日1.5食のところライブの楽屋では二食以上食べてしまう。<n><n>出されるから食べてしまう。<n><n>しかし体重は減っている。<n><n>羨望の視線が注がれる身体。
-------------------------------
Tweet ID: 1508422048426905610
キーひとつひとつがピラミッド形をしており、打つと逆さまになります。<n><n>ピラミッドが逆さまにされてゆく日々。いいぞ、もっとやれ。
-------------------------------
Tweet ID: 1508421032159608841
サブのまたサブのマシンからのTwでありますからキーボードの形状に不慣れで誤字脱字がはびこっています。
```

```:csvファイルの一部
1508423085086883843,通常は一日1.5食のところライブの楽屋では二食以上食べてしまう。<n><n>出されるから食べてしまう。<n><n>しかし体重は減っている。<n><n>羨望の視線が注がれる身体。
1508422048426905610,キーひとつひとつがピラミッド形をしており、打つと逆さまになります。<n><n>ピラミッドが逆さまにされてゆく日々。いいぞ、もっとやれ。
1508421032159608841,サブのまたサブのマシンからのTwでありますからキーボードの形状に不慣れで誤字脱字がはびこっています。
```

https://dev.to/twitterdev/a-comprehensive-guide-for-using-the-twitter-api-v2-using-tweepy-in-python-15d9#4-getting-more-than-100-tweets-at-a-time-using-paginator

#### get_users_tweets() について
```py
Client.get_users_tweets(id, *, end_time=None, exclude=None, expansions=None, max_results=None, media_fields=None, pagination_token=None, place_fields=None, poll_fields=None, since_id=None, start_time=None, tweet_fields=None, until_id=None, user_fields=None, user_auth=False)
```
https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets

https://docs.tweepy.org/en/stable/client.html#tweepy.Client.get_users_tweets

##### 取得できるツイート数
デフォルトでは、最新の10件のツイートがリクエストごとに返されるが、ページネーションを使用すると、最新の3,200件のツイートを取得できる。

##### レート制限
+ App rate limit (OAuth 2.0 App Access Token): 1500 requests per 15-minute window shared among all users of your app
+ User rate limit (OAuth 2.0 user Access Token): 900 requests per 15-minute window per each authenticated user
+ User rate limit (OAuth 1.0a): 900 requests per 15-minute window per each authenticated user

#### ページネーションについて
https://developer.twitter.com/en/docs/twitter-api/pagination

https://docs.tweepy.org/en/latest/v2_pagination.html

### 学習データ作成
csvファイルからツイートを取り出し、末尾に `
` をつけてテキストファイルに保存。

```py:csv_to_txt.py
import csv

CSV_PATH = "tweets.csv"
TXT_PATH = "tweets.txt"

tweets = []
with open(CSV_PATH, mode="r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        if(row == []):
            continue
        tweets.append([row[1] + "
"])

with open(TXT_PATH, mode="w", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(tweets)
```

```:tweets.txtの一部
通常は一日1.5食のところライブの楽屋では二食以上食べてしまう。<n><n>出されるから食べてしまう。<n><n>しかし体重は減っている。<n><n>羨望の視線が注がれる身体。

キーひとつひとつがピラミッド形をしており、打つと逆さまになります。<n><n>ピラミッドが逆さまにされてゆく日々。いいぞ、もっとやれ。

サブのまたサブのマシンからのTwでありますからキーボードの形状に不慣れで誤字脱字がはびこっています。

```

### ツイート文の学習・生成
[![Gradient](https://assets.paperspace.io/img/gradient-badge.svg)](https://console.paperspace.com/github/probabilityhill/txt-gen-from-tw/blob/main/gpt2_tweet.ipynb)
こちらでコードとともに説明している。
出力された文章の `<n>` を `\n` に置換すると見やすくなる。

## 参考
https://klab.hateblo.jp/entry/2021/11/02/173559