ラジオの感想ツイートの特徴量をtf-idfで抽出し、似ている番組を見つける

ラジオが好きでよく聴きます。自分が知らない面白いラジオが見つけられると良いなと思い、番組の感想ツイートから類似のラジオを探せないかを試しています。

↓前回書いた記事

前回書いた記事ではツイートの収集から形態素解析、単語カウントで簡単な特徴を確認するところまでできました。

今回は「tf-idfによる特徴量の抽出」と「コサイン類似度からラジオ番組同士の近さをみる」をやってみます。

※初心者が勉強しながら手を動かしたメモなので、間違っている部分があれば優しく教えてもらえるとうれしいですm

tf-idfによる特徴量の抽出

前回、文章→ベクトルの変換を単語カウントという方法で行いました。これは単純に登場した単語を数えるものですが、「し」「てる」などありふれた単語がランクインしていました(以下に再掲します)

# 「Creepy Nutsのオールナイトニッポン0」の感想ツイートの特徴量
# 単語カウントで算出

ann0 99
cn 97
https 26
t 26
co 26
ヤーマン 25
野菜 17
天丼 1211
さん 88
てる 8
笑っ 7
Creepy Nuts 7

その文章をより表した特徴量を得るために、今回は tf-idf を使ってみます。

tf-idf は「tf」と「idf」の概念を組み合わせたものです。

「tf」はTerm Frequencyの略で、単語の出現頻度です。その文書においてその単語がどれくらい登場するのか?を計算します。

「idf」はInverse Document Frequencyの略で、レアな単語ほど高くなります。こちらはすべての文書をみて判断します。つまり「どんな文書でも出てくるような単語はそんなに特徴ない(=idfが低い)となります。

この2つの値を掛け合わせて単語の特徴量とするのが tf-idf です。

より詳しい説明はこちらがわかりやすかったです:
tf-idfについてざっくりまとめ_理論編 - Developers.IO

tf-idf はpython の scikit-learn ライブラリを使えば簡単に計算することができます。早速やってみます。

データの準備をする

今回は番組間の類似度をみたいので複数の番組データが必要です。以下の5つの番組を例にします。

1. 霜降り明星のオールナイトニッポン0 (#霜降り明星ANN0)
2. 編集長稲垣吾郎 (#編集長稲垣吾郎)
3. 菅田将暉のオールナイトニッポン (#菅田将暉ann)
4. THE TRAD (#THETRAD)
5. Creepy Nutsのオールナイトニッポン0 (#cnann0)

まずはこの5つの番組それぞれの感想ツイートを取得し、一つの文章に連結してファイルに書き出します。

import tweepy

def export(filename, full_tweet):
   path = filename + ".txt"
   with open(path, mode='w') as f:
       f.write(full_tweet)


consumer_key = '{YOUR_CONSUMER_KEY}'
consumer_secret = '{YOUR_CONSUMER_SECRET}'
access_token = '{ACCESS_TOKEN}'
access_token_secret = '{ACCESS_TOKEN_SECRET}'

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)

api = tweepy.API(auth)

keyword = '#cnann0'  # ここを色々な番組ハッシュタグで
count = 100

query = keyword + ' -filter:retweets'
tweets = []

for tweet in tweepy.Cursor(api.search, q=query).items(count):
   tweets.append(tweet.text.replace('\n', ' '))

# 一つの文章にする
full_tweet = " ".join(tweets)

# ファイル出力
export(keyword, full_tweet)

実行すると #cnann0.txt というファイルが生成され、中身は以下のようになっています。

ミーーーーーーー!で盛り上がりすぎてRさんの韓国ラップ紹介のコーナーになってた。 
Creepy Nutsのオールナイトニッポン0(ZERO) | ニッポン放送 | 2020/07/28/火 
| 27:00-28:30… https://t.co/bfABGFIkQI #cnann0 でヤーマン聴いてからア
タマの中で鳴りヤーマン HIBIKILLA / ヤーマン PV https://t.co/XrtdYuo0uD
#cnann0 から見事にハマりました 鬼リピートです ヤーマン! #cnann0 #cnann0 

(...以下略)

これを1-5の番組ハッシュタグで実行し、5つの文章(ツイートが詰まったもの)を作りました。
場所を適当に出力してしまったので data というディレクトリを切ってそちらに5つのtxtファイルを移動しました。

tf-idfを計算する

では tf-idf を計算します。まずは scikit-learn をインストールします。

pip install scikit-learn

ファイルからデータを読み込み、tf-idf を計算します。

import numpy as np
import pprint
import mecaber
import glob
from sklearn.feature_extraction.text import TfidfVectorizer

# ファイルからデータを読み込む
def load():
   filenames = []
   tweets = []
   for file in glob.glob('data/*.txt'):
       for line in open(file, 'r'):
           filenames.append(file)
           tweets.append(line)
   return (filenames, tweets)

# 結果の整形表示
def show_result(feature_names, scores):
   result = dict()
   for token, weight in zip(feature_names, scores):
       result[token] = weight

   sorted_result = sorted(result.items(), key=lambda x: x[1], reverse=True)
   pprint.pprint(sorted_result[:10])

# tf-idfによるベクトル化
def run(filenames, texts):
   sample = np.array(texts)

   # tf-idfを利用する
   vectorizer = TfidfVectorizer(max_df=0.9, tokenizer=mecaber.parse)

   # ベクトル化
   vectorizer.fit(sample)
   tfidf = vectorizer.transform(sample)

   # サマリを確認
   print("テキスト数:%d, 単語の種類数:%d" % tfidf.shape)

   # 結果を表示
   for index, _ in enumerate(texts):
       print("--------- " + filenames[index] + "の結果 ---------")

       feature_names = vectorizer.get_feature_names()
       scores = tfidf.getrow(index).toarray()[0]

       show_result(feature_names, scores)
       print("\n")


(filenames, tweets) = load()
run(filenames, tweets)

TfidfVectorizer の初期化時に渡している mecaber.parse は前回の記事で書いたMeCabの中身と同じ物を渡しています。

これを実行すると、まずサマリとして

テキスト数:5, 単語の種類数:2612

に対して計算が行われたことが分かります。
次に各文章の特徴量を確認してみます。 各文書の上位の10つの単語を表示するようにしてみました。それぞれの結果は以下の通り。

1. #霜降り明星のANN0

[('霜降り明星', 0.775899566182668),
('ann0', 0.5169556028257698),
('借金', 0.12352206590537858),
('tf', 0.08272416713123615),
('コーナー', 0.07830179108265456),
('ann', 0.07456090425371681),
('ファン', 0.07118344643877687),
('あいみょん', 0.07058403766021633),
('金', 0.07058403766021633),
('24', 0.06176103295268929)]

2. #編集長稲垣吾郎 

​[('編集長稲垣吾郎', 0.7519950077971456),
('稲垣吾郎', 0.3193183354448089),
('スカルプd', 0.15039900155942912),
('#joqr', 0.13456752771106814),
('アンファー', 0.13456752771106814),
('吾郎さん', 0.1108203169385267),
('編集長', 0.1108203169385267),
('save', 0.10290458001434624),
('soap', 0.10290458001434624),
('プレゼント', 0.10290458001434624)]

3. #菅田将暉ANN

[('菅田将暉', 0.6644646535439591),
('ann', 0.5589686140768917),
('スタイル', 0.19472402758569143),
('菅田', 0.1720642259810005),
('米津玄師', 0.13908859113263672),
('将暉', 0.1122157995528264),
('米津', 0.09272572742175783),
('くん', 0.06830945036433224),
('レモン', 0.06490800919523047),
('ハマ', 0.05984842642817408)]

4. #THETRAD 

​[('thetrad', 0.7364692709599432),
('ハマ', 0.29573860460881707),
('ちゃんみな', 0.19473520060853247),
('店長', 0.17182517700752867),
('tokyofm', 0.16635296509245961),
('くん', 0.15343109811665484),
('the', 0.10166014533428086),
('菅田', 0.10166014533428086),
('trad', 0.09241831394025533),
('いか', 0.09164009440401528)]

5. #cnann0

[('cn', 0.6720795737497706),
('ann0', 0.5770317317188361),
('ヤーマン', 0.2896804167386453),
('野菜', 0.14484020836932265),
('天丼', 0.11380302086161065),
('creepy nuts', 0.06677495321386148),
('28', 0.062074375015423996),
('てんや', 0.062074375015423996),
('r', 0.051728645846186666),
('オールナイトニッポン0(zero)', 0.04173434575866342)]

なんかそれっぽい!

単語の数値が高いほどその文書をよく表している単語、ということになる。ちゃんと特徴が出せているように思えます(例えば菅田将暉のオールナイトニッポンに来週米津玄師がゲスト主演する予定)。

前回単語カウントで出した結果には「https」「t」「co」などのノイズも乗ってましたが、tf-idf ではそれが除かれています(max_df を指定してあまりに現れる単語を排除したのが効いているよう)。

これでラジオ番組の感想ツイートの特徴量を出せました。

コサイン類似度からラジオ番組同士の近さをみる

特徴量が出せたので、その番組同士の近さを求めることができるはず。コサイン類似度により近さを求めてみます。

from sklearn.metrics.pairwise import cosine_similarity

# コサイン類似度
def similarity(X, Y):
   cs = cosine_similarity(X, Y)
   pprint.pprint(cs)
   

# tfidf
def run(filenames, texts):
   #... (省略)

   # これを追加
   similarity(tfidf, tfidf)
   
   
(filenames, tweets) = load()
run(filenames, tweets)

コサイン類似度を計算するのは簡単で、cosine_similarity を呼ぶだけです。この結果は以下のようになります。

array([[1.        , 0.00759534, 0.07104369, 0.01247742, 0.32565984],
      [0.00759534, 1.        , 0.01763118, 0.03732022, 0.00946169],
      [0.07104369, 0.01763118, 1.        , 0.12547651, 0.03242817],
      [0.01247742, 0.03732022, 0.12547651, 1.        , 0.01281713],
      [0.32565984, 0.00946169, 0.03242817, 0.01281713, 1.        ]])

配列の順序は、

1. 霜降り明星のオールナイトニッポン0 (#霜降り明星ANN0)
2. 編集長稲垣吾郎 (#編集長稲垣吾郎)
3. 菅田将暉のオールナイトニッポン (#菅田将暉ann)
4. THE TRAD (#THETRAD)
5. Creepy Nutsのオールナイトニッポン0 (#cnann0)

の順番。数字が高いほど番組間の類似度が高い。なお、同じ行列を与えているので対称行列となっています。

この結果を見るとスコアが高いのは
・番組1と5(0.326)
・番組3と4(0.125)
あたりになっていそうです。それぞれ詳しくみてみます。

1つ目の「霜降り明星」と「Creeepy Nuts」は、どちらもオールナイトニッポン0枠であり、ann0 という特徴量が強く出ているため類似度は高くなりそうです。
内容に限らずどんな番組でもオールナイトニッポン0枠なら近いとされてしまうので、ann0はキーワードから除いて再度試してみても良いかもです。

2つ目の「菅田将暉のオールナイトニッポン」「THE TRAD」は実はつながりがありまして、THE TRADの曜日パーソナリティを務めるハマ・オカモト3がこの週の菅田将暉ANNにゲスト出演しています。そのためツイートに現れる単語が近しいものになり類似度が高く出たと思われます。

まとめ

tfidf とコサイン類似度によって、似ているラジオ番組の判定がなんとなくできました。今回は結果を確かめるために自分が聞いているラジオ番組5つで試したが、これを数多の番組でやれば全然知らないラジオに出会えるかもしれません。

また、データとして取っているのが「直近のツイート100件」なため番組といよりかはその週の放送にフォーカスした特徴が出ている気がしました。番組自体の特徴を出したければもう少し長いスパンで計算した方が良いかもしれません(でも放送同士の近さがわかるのもとても面白い!)

参考リンク

↑単語カウント、tf-idf について大変勉強になりました

この記事が気に入ったらサポートをしてみませんか?