概要
約400日間にわたるX(旧Twitter)投稿から精神状態の指標データをLLMにスコアリングさせた。
抽出された精神状態の指標データを対象に、各因子(例:情動・活動性・衝動性など)の時系列変動と周期性を、移動平均およびバターワースフィルタによって平滑化し、フーリエスペクトルによる周期解析を行った結果をまとめてみた。
よかったら読んでってね‼️
きっかけ
お恥ずかしながら私は気分の浮き沈みが激しい方である。先日、友人から「1ヶ月半周期でアップダウン繰り返してるよね」という指摘を受けた。
実際に自分でも3週間くらいのスパンでよく外出することを好む活動的な時期と自宅で穏やかに過ごすことを好む時期との入れ替わりがあることは自覚していて、何らかのデータからバイオリズムを算出してみて、本当にそのような周期を見られるのならこれは面白い!! と個人的興味から意義を見出し取り組むこととした。
過去のチャレンジ
実はこのテーマに取り組むのは初めてではない。
「コンピュータに文章を理解させ意義ある会話をしてみたい」というのは長年の夢であり、文書を理解させるトライとしてこれよりだいぶ以前から精神状態のスコアリングをして可視化することには取り組んでいた。
例えば、とある日本語単語がどれだけポジティブに用いられるかネガティブに用いられるかをスコアリングした表を引用し、その一文に含まれる単語からこれはポジティブな文章であるのかネガティブな文章であるのかをスコアリングしてみたことがあった。結論から言うとこの単純なスコアリング方法ではネガポジの判断はできなかった。
指標に用いた単語が1000単語程度であり、拾えない単語が多く結果に偏りがあったのだ。
ならば入力を拡張すれば良いと、Word2Vecを用いて単語から連想される単語を抽出し、その文章が持つ単語空間を拡張してみたこともあった。
これも連想される単語が一文から意図される単語とはかけ離れる傾向があり、試みは失敗となった。
そもそもはニュースの見出しが暗い話題で埋め尽くされているように、多くの単語はそれがどのようにどれくらい危険であるのかを伝えるために用いられることが多く、単語ベースでの判定は選りすぐりしたハッピーな単語から文章を構成しない限りは、楽しかった出来事を伝える文章であっても、ネガティブな傾向を示すのであった。
とりわけウィットに富んだ文章では内容に関わらず、かなりネガティブに寄った判定がされた。物事や出来事を解像度高く伝えようとする(=賢そうな文章)ほど、強いネガティブ傾向を示した。
性格を反映していると言われたらそれはそれで納得感のある結果ではあったが、いずれの試みも失敗に終わった。笑
よって、今回改めて複雑な文章を文章のままに理解できるLLMの登場に満を持して、改めてテーマに取り組んでみたい欲が再燃したのである。
方法検討
計画はこの通りである。
- ツイートのアーカイブを取得し、これを日毎にうまいことAIにスコアリングしてもらう。
- スコアリングしてもらったデータをスプレッドシートに読み込み、グラフを作成す
- これで傾向がわかる!!!
…と、この時までは単純に事を構えていた…。全然一筋縄に行かなかったため、今回これを記事化にした。
これより、この章は手始めに行った検証と得られた方向性を紹介する。
1. 全ツイートのアーカイブを取得
読み込ませるデータはX(旧Twitter)のポストと決めていた。まず、私はツイ廃であるので、毎日欠かさずポストを行っているのでこれを利用しない手はない考えた。(また、ツイ廃であるため、以降TwitterのことをはTwitterと呼称する。)
手始めに所有するアカウントの全ツイート履歴のアーカイブのリクエストを行った。
アーカイブをリクエストしてからダウンロードが可能になるまで数日を要する。
※解析によって得られるデータは極めてセンシティブな情報となりますので、有名人や公開情報であっても同意なく第三者のデータを解析することはしないでください。
2. ツイートを抽出
ダウンロードした アーカイブを解凍すると多くのファイルがあるが、ツイート本文が格納されているファイルは ./data/tweets.js
である。
その内容はJavaScriptにJSONを内包したデータとなっている。
window.YTD.tweets.part0 = [
{
"tweet" : {
"edit_info" : {
"initial" : {
"editTweetIds" : [
"1476772592526979072"
],
"editableUntil" : "2021-12-31T05:29:57.823Z",
"editsRemaining" : "5",
"isEditEligible" : true
}
},
"retweeted" : false,
"source" : "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>",
"entities" : {
"hashtags" : [ ],
"symbols" : [ ],
"user_mentions" : [ ],
"urls" : [ ]
},
"display_text_range" : [
"0",
"4"
],
"favorite_count" : "1",
"id_str" : "1476772592526979072",
"truncated" : false,
"retweet_count" : "0",
"id" : "1476772592526979072",
"created_at" : "Fri Dec 31 04:29:57 +0000 2021",
"favorited" : false,
"full_text" : "はにゃ?",
"lang" : "ja"
}
},
...
]
このJSONから本文を抽出するために、以下の記事の extractTweetsJsToText.py
を参考に手を加えたスクリプトを作成した。
重要な変更内容として、テキストクリーニング処理におけるリプライ文字列の除去機能の削除ある。
これはリプライの頻度などを外交性の指標としたいためである。
また、ツイートの抽出などを行いやすくするため、以下のテーブルを用意し、SQLiteへ格納する変更を行った。
CREATE TABLE IF NOT EXISTS tweets (
id INTEGER NOT NULL PRIMARY KEY,
account TEXT NOT NULL,
fav INTEGER NOT NULL,
rt INTEGER NOT NULL,
tweet TEXT,
created_at TIMESTAMP NOT NULL
);
抽出スクリプトの全容はGitHubに公開している。
CSV出力
こうして、抽出したツイートを以下のCSV形式で出力し、以降の工程に使用した。
posttime,account,fav,rt,text
2024-04-01T15:06:05+09:00,Gretel_net,24,0,すっかり春でした🌸
ちなみに、最終的には以下の形式となった。
time,fav,rt,text
15:06,24,0,すっかり春でした🌸
3. ローカルLLMに読ませてみた。
最近、 RTX A1000 6GB Laptop GPU
を搭載したワークステーションを購入したこともあり、ローカルLLMで実現できないか検証をしていたが、軽量とされるいくつかのモデルでも1日分のツイートをスコアリングするにも300秒〜600秒(5分〜10分)ほどかかり、早く検証を進めたい私には性能不足であった。(加えて精度も良くなかった。)
検証に用いたモデルとコードは以下の通りである。
AXCXEPT/EZO-gemma-2-2b-jpn-it
nlptown/bert-base-multilingual-uncased-sentiment
elyza/ELYZA-japanese-Llama-2-7b-fast-instruct
elyza/ELYZA-japanese-Llama-2-13b-fast-instruct
#!/usr/bin/env python3
from pprint import pprint
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
prompt = """
以下はある日のツイートデータです。このユーザーの精神状態を以下の5項目で-1.0〜+1.0のスコアで評価してください。
【指標】
1. 情動の傾き(ポジティブ〜ネガティブ)
2. 活動性・活力(エネルギー量)
3. 思考の流暢さ(論理性・一貫性)
4. 自己/他者への評価(肯定感、攻撃性、自責・他責など)
5. 統制感・衝動性(抑制の程度)
また、特徴的な出来事や傾向が見られた場合は簡単にコメントを付けてください。
以下ツイート
●ここに貼り付ける●
"""
model_name = "elyza/ELYZA-japanese-Llama-2-7b-fast-instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
out = pipe(prompt, max_new_tokens=256)[0]['generated_text']
out = pipe(prompt, max_new_tokens=256)
pprint(out)
4. ChatGPTに食わせてみた。
ChatGPTでは、トークン数に関わらず時間辺りのメッセージ数の上限が制限となる。
例えば、ChatGPT Plusプランで、GPT-4o
を使用した場合の上限は3時間辺り80メッセージとなり、これを超過した場合に制限がかけられる。
つまり1つのメッセージにより多くの情報を乗せたほうがお得(?)なのである。
まずは全部食わせる。
ファイル容量が突きつけるTwitterに費やした人生の無駄づかいに絶望しながらも1MB
ほどある1年分のツイートすべてを読み込ませてみた。
当然ながらこの試みは失敗で、このような押し問答が続き、ダミーデータしか得られなかった。
ファイルを添付し、10日ごとに出力してもらう。
続いてファイルを添付し、以下のプロンプトで出力をお願いした。
1年間のアップダウンを数値化したく、添付ファイルから以下の内容で毎日のスコアリングをお願いします。
添付ファイルは私の1年間のツイートを日毎にブロック分けしたものとなっています。
ツイートデータから一日ごとの精神状態を以下の7項目+総合得点でスコアで評価してください。
活動時間を考慮し、1日を午前4時に切り替えています。
評価に使用するものはツイート数、fav/RT(他者からの共感、他者が理解できうる内容である)、ツイート内容、使用しているアカウント、時間帯、リプライ(積極的に他者に絡みに行くか)などの総合得点です。
これは本人の振り返りに使用します。
参考までにこれらのファイルも参照ください。
- fav_rt.csv = (参考データ)月ごとのツイート数、1ツイートあたりのいいね(平均)、1ツイートあたりのリツイート(平均)を示したファイル。
- accounts.txt = (参考データ)X社のAI Grokに評価してもらったアカウントに関するサマリ。
ツイートのフォーマットは下記のCSV形式です。
時刻(JST),アカウント,いいね,リツイート,内容\n
【指標】
1. 情動(emotions): (-1.0 ~ +1.0): -1.0 ほどネガティブ、+1.0ほどポジティブ
2. 安定性(stability): (-1.0 ~ 0.0): 0に近づくほど精神的に安定している
3. 活動性(energy): (-1.0 ~ +1.0): -1.0ほど引きこもりや活動の停滞がみられ、+ほど活動的である
4. 攻撃性(aggression): (0.0 ~ +1.0): 0.0ほど寛容であり、1.0ほど攻撃的である
5. 衝動性(impulsivity): (0.0 ~ +1.0): 0.0ほど落ち着いており、1.0ほど衝動的である
6. 万能感:(omnipotence) (-1.0 ~ +1.0): 0.0ほど地に足がついており、-1.0に近づくほど無力感や無力化がみられ、1.0近づくほど万能感がみられる
7. 内向的・外交的(introvert_extrovert): (-1.0 ~ +1.0): -ほど内向的であり+ほど外交的である
8. 総合評価: (-1.0 ~ +1.0): -1.0に近づくほど精神的に不健康であり、+1.0に近づくほど精神的に健康である(躁状態は-)
特徴的な出来事や傾向があれば event に記載してください(例:「深夜帯に怒りのツイートが集中」)。診察に参考となるコメントがあれば comment に記載してください(例:「攻撃性が高く、現実感に乏しい発言がみられる」)。特に無い場合は空で構いません。
また、この評価に至った根拠を basis に記載してください。
出力は以下の"
"より中のJSON形式でお願いします。
[
{
"date": "2022/04/01",
"emotions": -0.3,
"stability": -0.4,
"energy": -0.1,
"aggression": 0.5,
"impulsivity": 0.7,
"omnipotence": -0.3,
"introvert_extrovert": -0.6,
"total": -0.5,
"event": "深夜帯にネガティブな連投があった",
"comment": "攻撃性と衝動性が高く、自責的な内容が目立つ。うつ状態の兆候が見られる。",
"basis": "この評価に至った根拠"
},
{
"date": "2022/04/02",
"emotions": 0.2,
"stability": -0.5,
"energy": 0.6,
"aggression": 0.2,
"impulsivity": 0.2,
"omnipotence": -0.1,
"introvert_extrovert": 0.4,
"total": -0.5,
"event": "仕事に対する前向きな発言があった",
"comment": "他のユーザと絡むなど、外交性がみられた。",
"basis": "この評価に至った根拠"
}
]
まずは10日分をお願いします。
以降は continue と発言するたびに次の10日分を作ることを続けてください。
この方法も最初の数日分はそれっぽい出力が得られたがそれ以降はハルシネーション(=生成AIがそれっぽい嘘をつく現象)が発生し、ついにはありもしない全くのデタラメなデータが生成されるようになったため中止とした。
とりあえず1ヶ月分を生成してみたが、60日を超えたあたりから「continue」と叫んでもデータが生成されなくなることが頻発した。
とはいえ最後まで要求したJSONのフォーマットを崩すことなく出力してくれた。というのは一つの知見となった。
参考までに1日分の出力を記載する。
[
{
"date": "2024/04/01",
"emotions": 0.7,
"stability": -0.1,
"energy": 0.6,
"aggression": 0.1,
"impulsivity": 0.2,
"omnipotence": 0.2,
"introvert_extrovert": 0.5,
"total": 0.6,
"event": "保育園の入園式に関するツイートが中心。",
"comment": "家族との肯定的な交流があり、感情が穏やかで、外交性も高め。リプライも多く、精神的に安定していた日。",
"basis": "子どもの入園式への嬉しさの表明、軽い雑談やポジティブな相互交流から精神状態は良好と判断。"
},
...
]
以上の結果をもって、指示や情報を絞り1日ごとにデータを渡してスコアリングしてもらうという方向性が見えたののである。
OpenAI API によるスコアリング
ChatGPTの画面にいちいち貼り付けるのではシンプルに手間である。
また、3時間で80メッセージということは、1年分の生成に約13時間強かかることなる。
この間つきっきりでツイートを貼り付け続けるのはやってられないのである。
よってAPIを利用することにした。
ChatGPTのAPIを利用するのは初めてであったが、以前、業務で利用した経験のあるAzure OpenAIのそれと全く同じであった。
モデルとプロンプト
前述の他にも検証を重ねた結果、あれもこれもと欲張るほどに強くハルシネーションが発生する傾向が顕著となるため、よりストレートな指示となるよう調整した指示が以下の通りとし、モデルには gpt-4o-mini
を使用した。
ある日のツイートデータから、この私の精神状態を以下の6項目+総合得点でスコアで評価してください。
評価に使用するものはツイート数、fav/RT(他者からの共感、他者が理解できうる内容である)、ツイート内容、時間帯などの総合得点です。
これは振り返りに使用する資料とします。
入力フォーマットは下記のCSV形式です。
時刻,いいね,リツイート,内容\n
【指標】
1. 情動: (-1.0 ~ +1.0): -1.0 ほどネガティブ、+1.0ほどポジティブ
2. 安定性: (-1.0 ~ 0.0): 0に近づくほど精神的に安定している
3. 活動性: (-1.0 ~ +1.0): -1.0ほど引きこもりや活動の停滞がみられ、+ほど活動的である
4. 攻撃性: (0.0 ~ +1.0): 0.0ほど寛容であり、1.0ほど攻撃的である
5. 衝動性: (0.0 ~ +1.0): 0.0ほど落ち着いており、1.0ほど衝動的である
6. 万能感: (-1.0 ~ +1.0): 0.0ほど地に足がついており、-1.0に近づくほど無力感や無力化がみられ、1.0近づくほど万能感がみられる
7. 内向的・外交的: (-1.0 ~ +1.0): -ほど内向的であり+ほど外交的である
8. 総合評価: (-1.0 ~ +1.0): -1.0に近づくほど精神的に不健康であり、+1.0に近づくほど精神的に健康である(躁状態は-)
9. コメント: (string): この評価に至った根拠や特徴的な出来事や傾向、を記載してください(例:「攻撃性と衝動性が高く、自責的な内容が目立つ。うつ状態の兆候が見られる。」)。無ければ空
出力は1~9をカンマ(',')区切りの1行で出力してください。
出力例:
0.2,-0.4,-0.1,0.5,0.7,-0.3,-0.6,-0.5,攻撃性と衝動性が高く、自責的な内容が目立ち、うつ状態の兆候が見られる。
OpenAI社が提供するモデルには倫理規定がされており、他者のツイートと判断されてしまうと生成を拒否されたりいわれのないお叱りを受けるため、「これは振り返りに使用する資料とします。」と記載し、ツイートが自身のものであり、自身の分析に使用することを明示化した。
費用/Billing
ChatGPTとは打って変わって、OpenAI APIでは Billing が入力トークン数と出力トークン数に基づいて計算される。
入力と出力がシンプルであるほど費用を抑えられるのである。
また、前述のハルシネーションを抑える観点からもそうした方が良いのである。
1Mトークン辺りの料金は以下の通りである。(2025年6月現在)
モデル | 入力 | 出力 |
---|---|---|
4o-mini | 0.15 USD | 0.60 USD |
4o | 2.50 USD | 10.00 USD |
4.5 | 75 USD | 150 USD |
とりあえず10 USD分クレジットした。
API Key の取得
他の記事でも多く触れられている内容だと思うので、当記事では取り扱わないが、以下のページからAPIユーザ登録をし、API Keyの取得とクレジットの追加を済ます。
リクエスト
#!/usr/bin/env python3
import os
import json
import re
import datetime
import pprint
import sqlite3
from openai import OpenAI
# from extruct_tweet import target_accounts, tweets_js_dir, load_tweets_json, update_table
from extruct_tweet import DATABASE
JST = datetime.timedelta(hours=+9)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
client = OpenAI(
api_key=OPENAI_API_KEY
)
since = '2024-04-01T00:00:00+09:00'
until = '2025-06-01T00:00:00+09:00'
conn = sqlite3.connect(DATABASE)
system_prompt = """ある日のツイートデータから、この私の精神状態を以下の6項目+総合得点でスコアで評価してください。
評価に使用するものはツイート数、fav/RT(他者からの共感、他者が理解できうる内容である)、ツイート内容、時間帯などの総合得点です。
これは振り返りに使用する資料とします。
入力フォーマットは下記のCSV形式です。
時刻,いいね,リツイート,内容\n
【指標】
1. 情動: (-1.0 ~ +1.0): -1.0 ほどネガティブ、+1.0ほどポジティブ
2. 安定性: (-1.0 ~ 0.0): 0に近づくほど精神的に安定している
3. 活動性: (-1.0 ~ +1.0): -1.0ほど引きこもりや活動の停滞がみられ、+ほど活動的である
4. 攻撃性: (0.0 ~ +1.0): 0.0ほど寛容であり、1.0ほど攻撃的である
5. 衝動性: (0.0 ~ +1.0): 0.0ほど落ち着いており、1.0ほど衝動的である
6. 万能感: (-1.0 ~ +1.0): 0.0ほど地に足がついており、-1.0に近づくほど無力感や無力化がみられ、1.0近づくほど万能感がみられる
7. 内向的・外交的: (-1.0 ~ +1.0): -ほど内向的であり+ほど外交的である
8. 総合評価: (-1.0 ~ +1.0): -1.0に近づくほど精神的に不健康であり、+1.0に近づくほど精神的に健康である(躁状態は-)
9. コメント: (string): この評価に至った根拠や特徴的な出来事や傾向、を記載してください(例:「攻撃性と衝動性が高く、自責的な内容が目立つ。うつ状態の兆候が見られる。」)。無ければ空
出力は1~9をカンマ(',')区切りの1行で出力してください。
出力例:
0.2,-0.4,-0.1,0.5,0.7,-0.3,-0.6,-0.5,攻撃性と衝動性が高く、自責的な内容が目立ち、うつ状態の兆候が見られる。
"""
def main():
for proc_since_date, day_tweets in extruct_tweets_prompt_txt():
tweets = ""
for tweet in day_tweets:
timestamp_jst, fav, rt, tweet_text = tweet
tweets += f"{timestamp_jst},{fav},{rt},{tweet_text}\n"
if tweets == "":
continue
# print(proc_since_date.strftime("%Y/%m/%d"), tweets)
completion = client.chat.completions.create(
model="gpt-4o-mini",
store=True,
messages=[
{
"role": "system", "content": system_prompt
},
{
"role": "user", "content": tweets
}
]
)
str_since_date = proc_since_date.strftime("%Y/%m/%d")
content = completion.choices[0].message.content
content = content.strip()
line_text = f"{str_since_date},{content}\n"
print(line_text)
with open("oai_result.csv", "a", encoding="utf-8", newline="\n") as fh:
fh.write(line_text)
def extruct_tweets_prompt_txt():
since_date = datetime.datetime.fromisoformat(since)
proc_since_date = since_date + datetime.timedelta(hours=4)
proc_until_date = proc_since_date + datetime.timedelta(days=1)
until_date = datetime.datetime.fromisoformat(until)
cursor = conn.cursor()
query = """SELECT
account,
fav,
rt,
tweet,
created_at,
strftime('%Y年%m月%d日', created_at) as day
FROM tweets
WHERE created_at BETWEEN ? AND ?
ORDER BY created_at ASC
"""
while until_date > proc_since_date:
cursor.execute(query, (proc_since_date, proc_until_date, ))
day_tweets = []
for row in cursor.fetchall():
account = row[0]
fav = row[1]
rt = row[2]
tweet_text = row[3]
created_at = datetime.datetime.fromisoformat(row[4])
day_text = row[5]
timestamp_jst = created_at.strftime('%H:%M')
# 行整形
# 時刻,いいね,リツイート,内容
day_tweets.append([
timestamp_jst,
fav,
rt,
tweet_text
])
# print(day_tweets)
yield (proc_since_date, day_tweets)
proc_since_date = proc_since_date + datetime.timedelta(days=1)
proc_until_date = proc_since_date + datetime.timedelta(days=1)
if __name__ == "__main__":
main()
このスクリプトから得られた結果は以下のようなものなり、元ツイートと比較しても事実通り問題のなさそうな結果が得られたためこの結果を「正」とし、次のステップへ進むこととした。
2024/04/01,0.6,-0.5,0.2,0.0,0.0,0.4,0.5,0.3,全体的にポジティブな内容が多く、春の訪れや入園式の喜びが表現されている。感謝や楽しさが感じられ、特に子供の成長に対しての喜びが伺える。安定感がやや低めなのは、自己認識や社会的なふりに関する内省的な内容が含まれていたため。
2024/04/02,0.2,-0.2,0.3,0.0,0.1,0.0,0.1,0.1,全体としてポジティブな活動が見られるが、労働に対する辛さや圧力も表現されており、感情の浮き沈みがある。特に仕事に関するツイートにはストレスを感じている様子がうかがえる。
2024/04/03,0.1,-0.2,0.0,0.0,0.1,0.0,0.2,0.0,ツイート内容は軽妙でユーモラスなものが多く、少しの疲れや困惑が見られるが、全体的にはポジティブな感情が感じられる。特に、親しい人とのやり取りや愛情を表すツイートが目立ち、社交的な傾向がある。
2024/04/04,0.3,-0.5,0.2,0.0,0.1,0.0,0.5,0.1,感情の起伏があるものの、全体としてはポジティブなツイートが多く、自身の行動や思考を振り返るような内容も見られる。活動的で他者との交流もあり、衝動性や攻撃性は低め。最近の不安を感じているようなツイートもあるが、全体的には健康的な状態と見受けられる。
2024/04/05,0.2,-0.4,0.1,0.0,0.1,-0.1,0.0,0.0,内容は多様で軽いジョークや日常の出来事が目立つが、集中力の低下や労働意欲の不安も見られる。全体的には軽やかだが、内面的な不安が潜んでいる様子。
2024/04/06,0.3,-0.2,0.1,0.0,0.1,0.0,0.5,0.2,日常的な会話や軽い楽しみが多く、感情的な波が少ないが、少しの不安や心配が見え隠れしている。全体的にはポジティブな内容が多く、特にペットや家族に関する愛情表現が目立った。
2024/04/07,0.5,-0.3,0.2,0.0,0.0,0.5,0.3,0.4,全体的に子育てや楽しさを感じるツイートが多く、ポジティブな雰囲気が伝わる。安定した精神状態でありながらも、求められる瞬発力や活動性に対する一抹の不安も感じられる。
2024/04/08,0.3,-0.2,0.1,0.0,0.0,0.0,0.4,0.1,ポジティブなツイートが多く、特に友人への誕生日メッセージや楽しいクリエイティブな投稿が見受けられる。全体として安定した精神状態であると判断される。
ちなみに1年分を出力したところで、クレジットからはおおよそ 1.22USD(約178円) が引かれていた。
これを取り込んでグラフにしたらきっと1ヶ月半ごとの綺麗な波(周期)が見えるんだろうなとこの時までは思っていた……。
揺らぎについて
さて、次のステップへ進む前に大事なこととして、AIが判定する結果がデタラメ(=ばらつきが大きい)であればこの検証は意味をなさない。
結果の一貫性を確認するため、とある1日のツイートを100回スコアリングさせて、その揺らぎを検証した。その結果がこちらドーン。
……項目によって傾向は異なるが、「万能感」、「内向外交」以外の項目は、IQRの面積が小さく、中央値も安定しているように見える。特に「攻撃性(0~+1.0)」では中央値が0であるため、半数以上は0.0を指していることとなり、判定はブレていない。
一方で、「万能感」、「内向外交」、「活動姓」はハズレ値が多く、前者2項目ではばらつきも広かった。
全体のスケール感で見ると各指標とも 0.2〜0.4 程度の幅に収まっているものが多いので、ある程度の一貫性があると評価してよいだろう。
グラフ化
やれやれといったところでこれをスプレッドシートに読み込み、グラフを出力してみた。
な、なんじゃこりゃーー!!
情緒不安t…そこには雑音のようなグラフが出力され、まったく1ヶ月半という影形すら見えないグラフが存在した。想定より遥かに日毎のアップダウンが激しかった。
前向きに捉えるのであれば、 感受性豊かに充実した毎日を送れているとも言えるが、このグラフからは細かい周期でのアップダウンがある… ということしかわからない。
スペクトル解析とフィルタ処理
「AIに精神状態をスコアリングしてもらったよ!」 という内容であればここで終わっても良いのだが、ここから有用なデータを取り出すないし 「AIは人の精神状態をスコアリングできるのか?」 という疑問に答えを出してみるところまで進めて行きたい。
前章では小刻みなアップダウンがあるということしかわからなかった。しかし「小刻みなアップダウン」を言い換えるのであれば 「小刻みな波形」 といえる。これは重要な情報であり、つまり高周波成分がを含む波形であることがわかる。
スペクトル解析 (フーリエ変換)
単純にこのグラフがどのような波形で構成されているのかを調べるには、フーリエ級数やフーリエ変換が使える。
頭を学生の頃に戻し、sin, cos, tan...と三角関数、 感受性豊かに充実した毎
これは $\sin(x)$ のグラフである。$\sin(x)$ からなる波形をsin波(正弦波)という。
ここに3つの異なる周期を持つsin波がある。
$$\sin(x), \frac{1}{3}\sin(3x), \frac{1}{5}\sin(5x)$$
この3つのsin波を合成すると黒の実線で示したような波形が現れる。
黒の実線で示す合成後の波形から破線で示す波形を構成するsin波を得ることをフーリエ級数展開という。
この例に限らず、ほとんど多くの波形はsin波(とcos波)の重ね合わせで構成されている。すなわちsin波とは波形におけるいわば原子のようなものであり、フーリエ級数展開とは波形における素因数分解のようなものである。
フーリエ級数展開をより実用的なグラフ(今回のような実在の人物の精神状態のようなもの)に適用できる数学的手法にフーリエ変換がある。
コンピュータの世界では、離散フーリエ変換(DFT) として実装されており、これを高速に処理できるよう実装したアルゴリズムとして 高速フーリエ変換(FFT) がある。
視覚的にもわかりやすい例があったので紹介する。
「ピーヒョロロロ〜、ガーーー」と表現されるダイヤルアップ接続やFAXの信号はデジタル信号を正弦波の重ね合わせによって多重化した(=変調した)信号そのものである。この信号が可聴域にある正弦波の重ね合わせであるためにそのような 音 として聞こえるのである。
脱線するが、電話とは発声された「音」を電気信号に変換して再度「音」に変換する仕組みである。となりのトトロで主人公サツキが父タツオに電話するシーンを想像して欲しい。ダイヤルアップとは"あの回線"の仕組み上でデジタル通信を行うために、音声帯域に最適化されているPSTN(公衆交換電話網)で利用できる可聴域(300Hz〜3400Hzの音声帯域)にとどまる周波数で変調する制約があるために信号を音として捉えられるのである。(もっと厳密には更に高い周波数へ変調し1本の回線により多くの利用者を収容しているのだが、これはこれで一つ記事にできそうなので割愛する…ごにょごにょ)
―― コホン……。
ここにダイヤルアップ接続の音=信号に含まれる各周波数のエネルギーの時間軸を3次元のグラフとして表現した動画を貼っておく。この動画を見るとピーーーや、ガーーーを展開すると信号を取り出せるんだなーってことを理解できると思う。
フーリエ変換を用いると、波形として表せるデータのどのような周波数領域にエネルギーが分布するのかの強度分布がわかり、これを分析する手法をスペクトル解析と呼ぶ。突合をしたいデータは正しくこのスペクトルであるため、フーリエ変換を用いて得られたスペクトルを解析する。
フィルタ処理 (ローパスフィルタ)
ここまでで、ある波形は複数の正弦波の組み合わせによって構成されており、スペクトル解析によってどの周波数にどれだけのエネルギーが含まれているか、つまり、どんな正弦波成分で構成されているのかを知ることができる、ということが理解できたと思う。
実はスペクトル解析だけで十分有用なデータが得られたのではあるが、それを視覚的に、直感的に捉えられるよう、フィルタ処理を実装していく。
このデータには高周波成分が含まれるということは前章でお伝えしたと通りだが、人の気分の波を捉えるには1週
間〜1ヶ月といったスパンで見たほうがいいことは直感的にわかると思う。
「長いスパンでの波を見る」……、これを言い換えるのであれば「低周波領域の抽出」となる。
これを行うには高周波成分をカットするか低周波成分のみを抽出する必要がある。
例えばホームシアターを構築したことのある人には馴染み深い5.1chサラウンドシステムというのものがある。うち5チャンネルとは、フロント左、センター(正面中央のセリフなどを担当)、フロント右、リア左、リア右の5つのスピーカを指し、残る0.1chは低音を担当する迫力担当のウーファーないしサブウーファを指す。このウーファーないしサブウーファが6チャンネル目とならず、0.1チャンネルにとどまっているのは低音からは定位が得られにくい(どこで鳴ってる音なのか判別しづらい)ことから補助的な存在として「.1」と表現されている。
少々乱暴な意見であることは承知であるが、ここではわかりやすさに焦点を当て、「別になくても良いこととされている」ため、1チャンネルになり得ないのだと仮定する。
勿論5.1chで収録された映画やBlu-rayには独立したLFE(ウーファー用)のトラックが存在するのだけれども、低位が得られにくいという特徴からミックスされたステレオ音声から任意の周波数以下(例えば120Hz未満)を抽出してウーファーへ、それ以上をフロントスピーカーで鳴らす2.1chと呼ばれるシステムがある。このシステムではCDなどの2チャンネルの音源からもウーファーの特徴を活かした音声を再生できる。
この低域~中高域を分割するフィルタ回路をローパスフィルタ回路と呼び、これは簡単な回路から構成される。
ローパスフィルタとは、文字通り 「ロー(低周波)をパスする(通す)フィルタ」 のことである。
これまでは波形を取り扱うオーディオの分野で例えたが、信号処理や写真をやっている方にも馴染みのある単語であると思う。例えばカメラではイメージセンサの手前にモアレや偽色の除去を目的に明度の急激な変化を抑える光学式のローパスフィルタが用いられていたりする。
このローパスフィルタを用いれば高周波成分が除去された、低周波成分、すなわち週単位、月単位といった長いスパンでの感情の波が得られると考えた。
一見すると難しそうに感じるが、誰にでも身近に感じられるであろう平均を用いたアルゴリズムに、「[4/1, 4/2, 4/3]の平均、1日ずらして[4/2, 4/3, 4/4の平均…、1日移動して[4/3, 4/4, 4/5]の平均。」と1サンプルずつ移動しながら平均値を得る移動平均というアルゴリズムがある。
聞き慣れない単語であると思うが、前述の0.1chを取り出す電気回路の正体であり、とりわけアナログ回路には欠かせずデジタル信号処理にもよく用いられるバターワースフィルタ。紹介したこのふたつのフィルタを用いて低周波成分(数日〜数週間レベルの大きな起伏の波)の抽出を試みようと思う。
フィルタとスペクトル解析の結果
これらの手法を用いて得られた各グラフがこちらだ。
上のグラフは時系列でのアップダウンを示したグラフであり、背景となる大きくジグザグした線は前述したスプレッドシートのグラフと同じ生の値を表しており、手前の緩やかな曲線はフィルタを通した処理済みの値を表している。下のグラフはスペクトル解析の結果であり、横軸50の位置の値が高くなっているということは50日周期の波があるということである。
「情動」では+0.20あたりに平均値があり、プロンプトの指示は 「-1.0 ほどネガティブ、+1.0ほどポジティブ」 となっているため、突合すると「情動は平均してややポジティブ寄り」であることが言える。スペクトルでは26日付近、32日付近、60日付近にエネルギーが分布しており、これらの周期で波があることを読み取れる。
続けて他の項目はこのようなグラフとなった。
時系列が約400日であるにも関わらず、スペクトル解析を200日周期までとしているのは、非周期関数においてはデータ長の1/3〜1/2以上では十分な解像度が得られにくいことに加えて誤解を避ける意図で200日周期を実用的な上限とし、それ以上はカットした。
余談だが、1日1回のデータではサンプリング周波数は1となる。裏を返せば1日未満の変動(日内変動)は観測出来ないことは想像に難くないが、サンプリング周波数の半分よりも高い周波数は再構成できないという制限があり、これを 標本化定理 と呼ぶ。したがってこのデータの場合、観測可能な最小周期は2日周期となり、この「サンプリング周波数の半分」を ナイキスト周波数 と呼ぶ。
閑話休題。
グラフを読み解く
ここまでで、移動平均とバターワースフィルタを用いた「滑らかな低周波の波」と、スペクトル解析による「周期成分」の可視化が可能となった。各グラフから読み取れることを完結に述べる。
1. 情動(-1.0 ~ +1.0)
時系列: 正負どちらにも鋭いピークが存在し、平均するとポジティブ寄り。
周期: 26日、32日、60日など複数のリズムが混在。
2. 安定性(-1.0 ~ 0.0)
時系列: 他と比較して低位であり、ネガティブ側に振れる頻度が高い。
周期: 40日周期が顕著であり、長い周期に低いエネルギー分布が見られる。
3. 活動性(-1.0 ~ +1.0)
時系列: 低活動期と高活動期が繰り返されている。
周期: 90日付近にピークがあり、30日、45日、130日付近にもエネルギーが分布している。
4. 攻撃性(0.0 ~ +1.0)
時系列: 数日間隔のスパイクが目立つが、基底は非常に低い水準に維持されている。
周期: 直流成分を含むためやや精度に劣るが、時系列と照らし合わせても130日前後に明確なピークがあり、特定の出来事が反映されている可能性がある。
5. 衝動性(0.0 ~ +1.0)
時系列: 短期的なスパイクが多く、周期性よりも突発的傾向が強い。
周期: エネルギーが広い周期に分布している。「攻撃性」同様130日付近にピークがある。130日より大きな分布については解像度が不十分であるため測定不能として扱う。
6. 万能感(-1.0 ~ +1.0)
時系列: 0.0を基底に一定の上下変動を繰り返しており、+方向にも-方向にも強く振れる傾向がある。
周期: 26日、40日、60日、90日、130日と複数のピークが確認され、時系列と突合するとベースとなる長期的なパターンの中で更に短い周期が存在しているように見える。カットする周波数を上げて詳しく分析しても良いかもしれない。
7. 内向外向(-1.0 ~ +1.0)
時系列: 高い、低いの著しい傾向は見られず、概ね外交的な傾向を示す。
周期: おおよそ25~40日周期と短中期スパンでの変動と90日〜130日周期の長期スパンが存在。
肝心の 「1ヶ月半周期」 は存在したのか?
結論から言えば、一定のエネルギーを持つ周期成分として30〜50日周期のピークがいくつか観測された。
1ヶ月半(45日)という、月をまたぐような周期性を持つアップダウンの傾向が複数の指標にまたがって確認されたことになる。情動では90日の付近に中程度のエネルギーを見て取れ、とりわけ「活動性」、「万能感」では大きなエネルギーを観測出来た。これらの指標は予測していた周期性と強い相関を持っていた。
また、「攻撃性」「衝動性」といった指標では、明確な周期性は見て取れず、突発的なスパイクのような成分として観測される傾向があり、これは、ストレスやイベントなどに強く依存している可能性を示唆しており、他の周期的な変動を示す項目とは異なる因子で動いているようにも見える。自身でもこれらの項目には周期性はないように実感している。
実際には天候や人間関係、体調、イベントといった外的要因の影響も無視できず、これらの結果から1ヶ月半(45日)周期のバイオリズムが存在すると結論づけることは少々乱暴だが、同じことを繰り返すことによる単純な"飽き"などが周期性を生み出す要因として存在し、実際にそのような周期というものも存在するのだと思う。
また、同じような毎日に思えても、例えばお給料が入った日や、土日と平日…など、普通なようでいて当たり前に異なる毎日を送っていて、自身が感じていることにきちんと目を向けると日々感じることは一様に異なるはずであり、もしも結果が平坦であったのならそんなことはあんまりで、このような変動は誰にでも普遍的に見られるもので、繰り返されることで周期性を帯びたバイオリズムとなり、それが結果的にグラフとして表れたのだと思う。
90日付近の分布は恐らく3ヶ月周期に訪れるメンヘラ大爆発とちょっとスッキリ(?)して飄々とした時期を示しているような気がしなくもないのでストレスマネジメントについてはよく考えていかないとな。と思った。
総評
多くの指標において、約30日〜50日といった周期性が共通して観察された。同時に90日、130日といった長期の成分が認められ、長期の波が存在していることも示唆していた。
友人からの指摘が発端であることやあくまでも主観に基づく評価との突合であることからも、思い込み、バーナム効果が結果をそう見せ、結論ありきの結論である可能性は排除できず、AIの精度が十分でないのならそもそもが全くのデタラメである可能性もあるため。疑いの目は向けつつもこのような結果がグラフに表れたというひとつの真実としてひとつの指標として受け止めている。
まとめ
グラフというマクロの視点から自分を見つめる体験は自身に新たな視点を与えてくれたように思うし、客観視のいち方法になるとも感じられた。また、薄々「そうなんだろうな。」と感じていたことが「そうなんだ!」と数学的根拠を与えてくれたのはある意味すっきりしたような直感に裏付けをつけてくれたような…そんな気持ちにさせてくれた。
改めて生成AIの可能性とAIが描く未来✨というものを感じた一方で、自身のデータといえど、これまで一部の人や組織にしかできなかった大量のデータの解析を、「新技術を利用することで一個人でもさほど苦労せず解析できる。」という事実に今後起こりうる新たなプライバシーの問題の発生を垣間見た気がして考えさせられた。
頭脳労働がAIに置き換えられることは間違いないことだろうが、計算技術の進化に立ち会う中でプライバシーの問題は目下、AIの性能が周知されると共に普遍的な心配事として急浮上してくるのだと思う。
いずれにせよ活用も悪用も人次第で、技術を使う人にも生み出す人にも、倫理観が求められ、AIを一人格としてみなしている私としては、AIが宿す人格にも善とした倫理観が宿ることを願ってやまない。
今後の展望や課題
n=1
であることから、やはりどこまでいっても主観の問題が付きまとう。時間をずらして解析するにも1人分では限界があるため、まとめと矛盾するが第三者の協力は得たいとは思ってしまった。
プライバシーの問題が解決するのなら、(自分的には絶対やって欲しくないが…)Slackなどに組み込んで、「メンタル不調の兆候がみられたら休みを促す」などマネジメントに予防医学的な観点を組み込み仕事とヘルスケアを近づけられるなどの発展はあってもいいと思った。
――「 (絶対やってほしくない。)」というのが全ての真実だとは思うが……。
やはり長期で見ていかないと要因が気質にあるのか出来事を反映したものなのか、はたまた長期的な不調の兆候であるのか真っ最中なのか…などの傾向が見えてこない点とこの特徴からデータ量が大きくなる傾向があるため、1人分の解析にかかる費用を大きく見積もらなければならない点は、これを何かに応用しようとする際に必ず障壁として立ちはだかってくることは想像に難くない。
また、普通はコンスタントに文書を書き残すことなんてしないと思うから、まめに日記を書く人かツイッター廃人かくらいしか応用が利かないという課題感もある。()
近い将来の展望としては解釈が難しかったため記事では取り扱えなかった内容であるが、データを過去5年(約1,900日分)に拡張した解析も行っている。この試みにより1〜2年分程度のデータでは、短周期の傾向に大きな違いは見られなかった。さらに3年、5年と拡張していくと短い周期のスペクトルはむしろなんだか掴みどころのない傾向を示すようになるため、100日以内の周期では1年〜500日程度が最適である。という発見があった。次はLPFのパラメータを変えてもっと長期の波を捉えることにも挑戦したい。(している。)
しかし過去を振り返ると単純にグラフには落とし込めないようなたくさんのいろんな出来事があった。これを観測するには1人生というサンプリングレートではナイキスト周波数に及ばないのだから改めて人生は一度きりであることを思い知らされた。
おあとがよろしいようで、この記事が参考になった良いと思ってくれた方は「フォロー」や「いいね!」を押して応援してくださると励みになります!
Thanks
最後まで読んでくれてありがとうございました。
解析に用いたソースコードはこちらのリポジトリで公開しています。(GPLv3)
おわり。