太陽がまぶしかったから

C'etait a cause du soleil.

AI資産化のために14年間分のTwitterログに並列URL展開とMPSによるツイートトピックス分類ラベル付与

Xの過去ログツイートをAI資産化する

 そんな前置きはさておき、GPTsにXの過去ツイートを読ませる方法だ。XはAPI制限が厳しくなって過去ツイートをAPIでよしなに取り出すことが難しくなってしまったが、『全ツイート履歴をダウンロードする方法』で過去ログを取り出すことができる。

 ここからダウンロードできるファイルは複数の巨大JSなファイル群であり、そのままGPTsに取り扱わせるのは難しいため以下の手順でファイルの変換処理を行う。

 こちらの記事で、GPTsにXの過去ツイートアーカイブを取扱いやすくするための変換処理を紹介したが、その続きで以下のようなことがしたい。ちなみに自分がTwitterをはじめたのは2008年だが、東日本大震災絡みの諸々でSNSから距離を取ろうとアカウントを消したため、2011年からのログとなる。14年間分だ。

graph TD
  A[アーカイブファイル取得] --> C[本文内のURL展開とメタ情報取得]
  C --> D[Markdownリンク化+メタキーワード付与]
  D --> E[上記変換後ツイート本文のトピック分類]
  E --> F[トピックのハッシュタグ化+サイズ分割保存(JSON形式)]

X上でシェアされる場合にはOGPから生成されるURLカードを見越してURLのみがシェアされがちだったが、本文の短縮URLの意味をGPTsからは読み取ることができないし、ジャンルを指定されても都度都度で適切な知識を取得するのは困難であるため、そのようなメタ情報を後付けで付与し、またRAGされるブロック単位で意味が明確となりやすいJSON形式で保存するものとした。

実装スクリプトの全文

➤ 全文をGitHub Gistで見る

ツイートアーカイブファイルの内の要素分解

まずは処理対象のファイル( ./data/tweets.js )を読んで、仕様を確認しよう。

 {
    "tweet" : {
      "edit_info" : {
        "edit" : {
          "initialTweetId" : "1908779567885803697",
          "editControlInitial" : {
            "editTweetIds" : [
              "1908779567885803697",
              "1908779637653516544",
              "1908780596861808869"
            ],
            "editableUntil" : "2025-04-06T08:11:27.000Z",
            "editsRemaining" : "3",
            "isEditEligible" : true
          }
        }
      },,
      "retweeted" : false,
      "source" : "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
      "entities" : {
        "hashtags" : [
          {
            "text" : "note",
            "indices" : [
              "86",
              "91"
            ]
          }
        ],
        "symbols" : [ ],
        "user_mentions" : [
          {
            "name" : "shi3z",
            "screen_name" : "shi3z",
            "indices" : [
              "79",
              "85"
            ],
            "id_str" : "3584601",
            "id" : "3584601"
          }
        ],
        "urls" : [
          {
            "url" : "https://t.co/4KqGzuvh92",
            "expanded_url" : "https://note.com/shi3zblog/n/nb60c41e8ef4a",
            "display_url" : "note.com/shi3zblog/n/nb…",
            "indices" : [
              "93",
              "116"
            ]
          }
        ]
      },
      "display_text_range" : [
        "0",
        "116"
      ],
      "favorite_count" : "0",
      "id_str" : "1908780596861808869",
      "truncated" : false,
      "retweet_count" : "0",
      "id" : "1908780596861808869",
      "possibly_sensitive" : false,
      "created_at" : "Sun Apr 06 06:53:26 +0000 2025",
      "favorited" : false,
      "full_text" : "\"今後、実装しかできない無愛想なエンジニアは淘汰されていくことになるだろう。AIの方が優しいからだ。\" / 床屋に行こうと思ったらやってなかった|shi3z @shi3z #note  https://t.co/4KqGzuvh92",
      "lang" : "ja"
    }
  },

edit_info には「編集されたツイート」の履歴が入るため、最新の editTweetIds のリストを作成し、それを id_str とマッチさせて編集前ツイートを除外する。

また urls[i].expanded_url短縮URLが展開済となっているため、こちらを利用する。なおハッシュタグも格納されているがこちらは本文からハッシュタグを除外する処理と一体化しているため、コンテンツ文字列を利用するものとしたい。

URLの展開とメタ情報取得の非同期化

async def process_urls_batch(urls_batch):
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls_batch:
            original_url = str(url["url"])
            expanded_url = str(url["expanded_url"])
            tasks.append(get_url_info(session, original_url, expanded_url))

        if not tasks:
            return []

        results = await asyncio.gather(*tasks)
        return [r for r in results if r is not None]

当初は requests でタイトル等を取得していたが、 request はひとつひとつが同期的な処理であって14年間ものツイートを対象とすると処理時間がネックとなるため、aiohttpによる非同期での並列スクレイピングを実施。Xの短縮URLである https://t.co/ に集中的にアクセスしたらブロックされる懸念があったが、 expanded_url があるためアクセス先が分散できている。

とはいえ、毎度の起動ごとにスクレイピングするのは負荷が大きいし、途中エラーの時に泣きたくなってしまうためキャッシュファイルを作成して保存しておくこととした。このキャッシュファイル自体もシェア先ドメインやメタキーワードのランキング分析など直接的に分析できる副産物となっている。なお取得したHTML本文の文字コード問題があるため、変換処理を厚めに行っている。

Hugging Face のツイート分類モデルを実行

ツイート分類を行うのにあたって汎用的なテキスト分析モデルよりも、ツイート分類器に特化した学習済モデルがないかをHugging Faceで探索。

Hugging Faceとは機械学習モデルのGitHub的なプラットフォームであり、自然言語処理NLP)を中心に、世界中の研究機関や企業がモデルやデータセットを公開している。Pythonライブラリ transformers で簡単に使えるためローカルで機械学習結果を利用するのに便利だ。

今回はイギリス国立のカーディフ大学が作成している cardiffnlp/tweet-topic-large-multilingual を利用することとした。こちらは多言語と短文に対応した、21カテゴリの多ラベル分類ができるものとなっている。分類結果はハッシュタグ扱いとするためスコアが一定以上のラベルは全て追加。また既存のハッシュタグと並列させるため英文のラベルを日本語で正規化している。

このモデルの実行を通じて、ハッシュタグをつけていなかったり、URLだけシェアされていたようなツイートにも「#日常」「#テクノロジー」「#音楽」などの意味づけが行われるが、こちらも処理時間がネックとなるため、MPSによる高速を試みる。

MPSによるツイート分類のGPU利用化

MPSとは Metal Performance Shaders の略で、Appleが提供しているGPU向けの高速演算ライブラリである。主に macOS/iOS上での機械学習・画像処理・数値演算を高速化するためのAPI群として提供されている。Hugging Faceの提供する Transformer は MPSに対応しているため、以下のように使える場合にはMPSを設定して利用している。

device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
print(f"Using device: {device}")
model.to(device)

うまく利用できるとアクティビティモニタのCPUタブでGPU利用率が跳ね上がり体感としてCPU利用時の5倍程度の処理速度となる。URL展開の並列化と分類器のGPU利用がなければ1日放置しても処理が終わらないところであった。ここまでで作成した情報をマージすることで意味づけされたツイートのリストが手に入る。

GPTsへのアップロードに適したファイル形式に変換

GPTsの知識(knowledge)機能について改めて公式ドキュメントを確認。

項目 内容
最大ファイル数 1つのGPTに 最大20ファイル アップロード可能
最大ファイルサイズ 1ファイルあたり最大512MB
トークン数上限 1ファイルあたり最大2,000,000トーク(約150万語相当)
対応ファイル形式 テキストを含むファイル(画像も可だが テキストのみ処理対象
処理内容 テキストをチャンクに分割 → 埋め込みベクトルを生成して保存
使用方法 GPT Builder UI からのみファイル管理可能(APIでは不可)
適した用途 頻繁に変更されない静的ドキュメント(例:社員ハンドブック、方針集など)

最大ファイルサイズは問題にならないが、日本語でのトークン数制限には達しやすいため、概ね10MBごとにファイルを分割することとした。また画像ファイルも画風参照などはしてくれないことが明らかになった。ちなみにAPIによる更新ができないと明記されているのも、ツイートという更新性が高いデータとの適合性は高くなく、あくまである断面までのアーカイブとしての使い方となるだろう。

自分で作ったデータは自分のAI資産にできる

生成されたファイルをGPTsにアップロードすることで、テーマ x 年代などの複数軸を指定してのツイート分析や創発の精度が上がる感じがする。例えば「アート」についてのツイート分析。

✴ 言い回し・語彙の特徴
•インテリ×皮肉のミックス
→ 例:「死んだ文化系女子という精霊をRadio Control Helicopterにして飛ばす現代アート
✦ ナンセンスに見えるが、実は深い社会風刺や象徴性を含んでいる。
•日常×高尚な語彙の接続
現代アートや建築、デザインを語る際に「ファサード」「周波数が異なる振動」「謎の調和」といった詩的・理系チックな語彙を好んで使う。

2019年頃にテクノロジーについて漫談するなら?

2019年のIoTは「とりあえず何でも繋げばええんやろ」ってノリで、冷蔵庫がツイートしてた頃。中身は空なのに発信力だけ無駄にあるあたり、まるで意識高い系のSNSアカウントだった。

よくもわるくも過去14年間の発言が分析され、離れた概念や専門用語の共通項で謎かけを試みるしょうもない発言が無限に生成されていく。ChatGPTにメモリー機能が公開され、ChatGPTに入れた発言は自動的にパーソナライズされていくが、TwitterAPIが無償公開されておらず自動で取り込ませるのが難しいため自発的にアップロードする必要があり、どうせアップロードするならよしなに編集する仕組みを整えたい。

汎用的なものは汎用的な結果となるし、他人からの学習結果には倫理的な問題もあるため、自分で作った自分が自由にできるゼロパーティデータをAI資産化し、いくばくかのオリジナリティを確保していく必要もある。これは広くは伝搬しないことを目的とした自己矛盾的なミームであるが、大抵の人類の遺伝子も広くは残さないことを考えるとクローンであり、子供のような意味付けを持っていくのかもしれない。