はじめに
今回は、女性の「カワイイ」をラーメンを例に考えてみた→「可愛いって言ったのに購入しない」問題も解決...か...? のtogetterまとめを見て、「なるほどな」と思ったことがきっかけです。
女性が「かわいい」と言った対象と、男性が「味がする」「甘い」と言った対象を比較すると、意味的に似ている部分があるのではないか、という仮説を立てました。
この仮説を検証するために、Twitter上の実際の発言を収集し、文脈ごとの意味空間をベクトル化して類似度を測るプロトタイプを作ることにしました。
背景とテーマ
「かわいい」と「味がする」の不思議な関係
「かわいい」は男性には理解しづらい概念です。
なぜなら、対象を問わず何にでも「かわいい」というからです。
しかし、これを「味がする」と同等であると考えると意味が通ります。
言語感覚の対比例
-
女性側(感覚語)
- この洋服かわいい
-
男性側(味覚比喩)
- このラーメン、普通にうまいじゃん
このように、女性語の「かわいい」と男性語の「味がする/甘い」といった表現は、文脈や対象は違うものの、意味空間上では近い部分があるのではないか という仮説です。
なぜTwitterなのか
ニュースコーパスや話し言葉コーパスも検討しましたが、やはり日常のリアルな発言、つまり 生の会話・感情・比喩がそのまま出ている場所 はTwitterしかありません。
ニュース記事は整備されすぎており、人間の微妙な認知差や比喩のニュアンスは表れにくいのです。
プロトタイプ設計
プロトタイプは以下の構成です。
- データ収集: Twitter API を利用して「かわいい」「味がする」「甘い」「旨い」などのツイートを取得
- 文埋め込み生成: Sentence Transformers を使用して、文章をベクトル化
- 類似度計算: コサイン類似度を用いて、「かわいい文脈」と「味覚比喩文脈」の意味的距離を算出
- CSV出力: 分析結果を CSV に保存し、統計値と分布を確認可能
技術的詳細
ディレクトリ構成
lib/llm/prototype/
├─ semantic_kawaii_and_taste_service.py
├─ semantic_kawaii_and_taste_vo.py
-
service.py: API通信、データ前処理、埋め込み生成、類似度計算、CSV出力 -
vo.py: ValueObject定義、データ構造、型安全な受け渡し
ValueObject設計
from dataclasses import dataclass
from typing import List
import numpy as np
@dataclass
class TweetText:
text: str
@dataclass
class TweetCollection:
category: str # "kawaii" or "taste"
tweets: List[TweetText]
@dataclass
class EmbeddingVector:
text: str
vector: np.ndarray
@dataclass
class EmbeddingCollection:
category: str
embeddings: List[EmbeddingVector]
@dataclass
class SimilarityResult:
mean: float
median: float
max: float
min: float
@dataclass
class SimilarityMatrix:
category_a: str
category_b: str
matrix: np.ndarray
stats: SimilarityResult
Service設計
import os
import re
import numpy as np
import pandas as pd
import tweepy
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from semantic_kawaii_and_taste_vo import (
TweetText,
TweetCollection,
EmbeddingVector,
EmbeddingCollection,
SimilarityResult,
SimilarityMatrix,
)
class SemanticKawaiiAndTasteService:
def __init__(self):
self.twitter_client = tweepy.Client(
bearer_token=os.getenv("TWITTER_BEARER_TOKEN")
)
self.embedding_model = SentenceTransformer(
"sentence-transformers/all-MiniLM-L6-v2"
)
def fetch_tweets(self, query: str, category: str, limit: int = 10) -> TweetCollection:
tweets = []
try:
resp = self.twitter_client.search_recent_tweets(
query=query,
tweet_fields=["text", "lang"],
max_results=limit
)
if resp.data:
for t in resp.data:
if t.lang == "ja":
cleaned = self._clean_text(t.text)
tweets.append(TweetText(text=cleaned))
except tweepy.TooManyRequests as e:
reset_time = int(e.response.headers.get("x-rate-limit-reset", 0))
raise RuntimeError(f"Rate limit exceeded. Reset at {reset_time}") from e
return TweetCollection(category=category, tweets=tweets)
def _clean_text(self, text: str) -> str:
text = re.sub(r"http\S+", "", text)
text = re.sub(r"@\w+", "", text)
return text.strip()
# Embeddings, similarity calc, CSV export...
実行例
if __name__ == "__main__":
service = SemanticKawaiiAndTasteService()
result = service.run_pipeline()
print("Semantic Similarity Stats")
print(result.stats)
開発中の苦労話
- いざ Twitter API を叩いたところ、Free tier の制限にあっさり引っかかる
- 15分の短期レート制限 429 Too Many Requests
- さらに月間上限 Usage cap exceeded
「かわいい」を解析したかったのに、API に阻まれて Pipeline が中断
INFO:__main__:Fetching tweets for query: かわいい -is:retweet lang:ja
ERROR:__main__:Rate limit exceeded for kawaii. Reset at: 2026-01-10 23:03:15 (448s remaining). Aborting.
Pipeline aborted due to error: 429 Too Many Requests
Too Many Requests
くやしいです!