自分のツイートを感情分析して機嫌を管理する


きっかけ

一般に、テキストと言うものに感情はありません。「今日のTVめっちゃ面白かったわ〜(笑)」と書いていても、その実本人は真顔だったりします。

そんな無機質なテキストの集合体であるのがTwitterです。最新のトレンドを追うことができるTwitterは、日々喜怒哀楽が渦巻く場所には見えますが、呟く本人のその腹の中は何もわからないのです。

こんなことを考えるうちに、「せめて自分のアカウントだけでもテキストによって感情を分析し、今の機嫌をみんなに示しておこう」と思ったのがきっかけの一つです。

また、感情分析を行えるGoogle Natural Language APIとTwitter APIを組み合わせて何かできないかなと考えてたのも今回のモチベーションです。

概要

自分の直近12件のツイートを感情分析し、平均スコアを算出してからスコアに応じて自身のTwitterアカウントの名前の末尾に顔文字を付与する。平均スコアが高いほどポジティブな(笑った顔・喜んだ顔)を付与し、低いほどネガティブな顔(悲しい顔・泣いている顔)を付与する。

使ったもの

  • Ruby
  • TwitterAPI
  • Google Natural Language API … Googleが提供する自然言語処理用のAPI。自然言語処理を行うAPIはいくつか存在するが、日本語テキストに対応しているのでこれを選びました。サービスを比較している記事があったので、こちらを参考にています。

テキストの感情分析をするためのクラウドサービス比較

ディレクトリ構成とか大まかな仕組み

.
├── SentimentAnalysis
│   └── TextSentimentAnalysis.rb
├── Twitter
│   ├── AddUserName.rb
│   ├── GetTweet.rb 
│   └── Optimization.rb
└── main.rb
  • TextSentimentAnalysis.rb … ツイートの感情分析を行うところ
  • AddUserName.rb … ユーザーネームを変えるところ
  • GetTweet.rb … 自分のツイートを取得するところ
  • Optimization.rb … ツイート内容を感情分析しやすいように編集するところ
  • main.rb … こいつを実行してプロブラムを動かす

TwitterAPIのアクセストークンとかGoogle Natural Language APIを使用するためのキーは違うディレクトリに移動させてます。(後々出てくる"Privatekey")

実行結果

プログラム説明

Google Natural Language APIの導入

まずは感情分析を行うGoogle Natural Language APIをRubyで使えるようにします。これについては、別で記事を書いているのでそちらを参考にしてください。
RubyでGoogle Natural Language APIを使えるようにする

自分のつぶやきを取得

最初に、自身のTwitterアカウントにアクセスして、自分のツイートを取得します。自分自身のツイート頻度を考慮し、今回は最新12件のツイートを取得しました。

GetTweet.rb
require_relative '../../Privatekey/oauth_twitter'
require_relative 'Optimization'
# タイムラインからツイートを取得する関数get_tweetの定義

def get_tweet
  @tArray = []
  @client.user_timeline(count: 12).each do |tweet| #自分のタイムライン表示
    reg1 = /^RT @.+$/ #RTを除去するための正規表現
    reg2 = /#Nowplaying$/ #なうぷれ(聞いている音楽)の除去のための正規表現
    if (reg1 =~ tweet.text) || (reg2 =~ tweet.text)
      #正規表現にマッチしたら処理を繰越(RTを分析の対象に入れない)
      next #処理を次に回す
    end

    norm_text = t_norm(tweet.text) #最適化
    @tArray << norm_text #ツイートを配列に追加
  end
end

ここでは、ちょっとした工夫として、リツイート(他人のつぶやきを拡散する機能)で得たツイートやなうぷれ(いま聴いている音楽をツイートするもの)がある場合は、正規表現でそれを除去しています。感情分析を行うにあたって、これらを含めると精度が下がると認識したからです。また、最新のツイートを分析したいので、除去した分のツイートの再取得は行なっていません。例えば、RTが1件あった場合、取得するツイート数は11件になります。

t_normについては、後ほど説明します。これで、自分のつぶやきを配列に格納できました。

各ツイートに対し、感情分析を行う

次に、配列に格納された各ツイートに対して、感情分析を行います。ここで使う感情分析のスコア評価は以下の通りになっています。

  • score(-1.0 ~ +1.0)…+ならばポジティブ(喜び・嬉しさ)な感情であることを示し、-ならばネガテイブ(悲しみ・怒り)な感情を表す。絶対値が大きいほどその感情が大きいことを示しています。
  • magnitude(0.0 ~ inf)…感情の強さ。値が大きいほど感情的な文章であることを示しています。例えば、scoreが0.0でも、magnitudeの強さによって、文章が2パターンに分かれます。
    • magnitudeが大きいなら、感情的であり、文章の中でポジティブとネガティブが入り混じって打ち消しあっている
    • 小さいならば、全体的にニュートラルな文章である

今回はmagnitudeは使っていません。いずれ使い道を考えて実装します。(アイデアをください)

ここでは感情分析を行う関数sentiment_analysisを定義しています。

TextSentimentAnalysis.rb
require 'google/cloud/language'

# テキストに対して
# 感情分析を行う関数sentiment_analysisを定義

def sentiment_analysis(text_content)

  language = Google::Cloud::Language.new
  response = language.analyze_sentiment content: text_content,
                                        type: :PLAIN_TEXT

  sentiment = response.document_sentiment

  @s_score = sentiment.score.to_f.round(1)
  @s_magnitude = sentiment.magnitude.to_f.round(1)
  puts "Score: #{@s_score}"
  puts "Magnitude: #{@s_magnitude}"

end

この関数自体は、1つのテキストを感情分析するものです。結果は少数第一位で四捨五入されます。これを、main.rbで複数回実行し、最終的に平均スコアを算出しています。

感情分析をより正確なものにするために

ツイートの取得時にRTやなうぷれを除去しましたが、他にも感情分析を妨げる要素があると考え、これらをツイートのテキスト内から除去する関数を定義しました。それがt_normです。

Optimization.rb
#ツイートを分析しやすくするための関数

def t_norm(btext)

  change_text = +btext #同内容の非凍結テキストを所得

  #URLを含むツイートに対して、URLを削除する
  URI.extract(change_text).each{ |url| change_text.gsub!(url, "")}

  return change_text
end

思いつきで実装したのが、ツイート内に含まれるURLの存在です。少しでも正確な(とはいうもののAPIが叩き出した値が正確かどうかは自分自身もわからないが)結果出るように、URL部分だけを除去しています。他に除去すべきテキストがあった場合は随時追記していきます。

スコアを評価し、それに応じた絵文字を付与してユーザーネームを変更

平均スコアを算出したら、それに応じた顔文字をユーザーネームの末尾に付与します。さらに、感情分析の結果とユーザーネームを変更したことをつぶやきます。

AddUserName.rb
require_relative '../../Privatekey/oauth_twitter'

def add_emoji(score,magnitude)
  default = "fyhcu" #デフォルトの名前

  #ポジティブな絵文字とネガティブな絵文字をそれぞれ配列に格納
  emoji_array_positive = ["🙂","🙃","😀","😃","😄","😆","😝","😤","🤩","🥳"]
  emoji_array_negative = ["😐","😕","😟","☹️","😔","😖","😫","😵","🤮","🤪"]

  select = score*10 #スコアを10倍

  # selectが正ならポジティブな顔文字が格納されている配列を、そうで
  # なければネガティブな顔文字が格納されている配列を使用する
  # floorは切り上げ、ceilは切り下げ
  if select <= 0
    emoji = emoji_array_negative[-1*select.ceil]
  else
    emoji = emoji_array_positive[select.floor]
  end

  result = default + emoji
  @client.update_profile(:name => result)
  tweet = "直近#{@count}件のツイートの感情分析結果\r
平均スコア:#{@result_score}\r
ユーザー名を#{result}に変更しました。"
  @client.update tweet
end

顔文字の選定は独断と偏見です。配列を見てもらえば分かるように、右ほど感情が大きい時の顔文字を格納しています。

実行

あとはこれらの関数をまとめたmain.rbを実行すると結果が返ってきます。

main.rb
require_relative 'SentimentAnalysis/TextSentimentAnalysis'
require_relative 'Twitter/GetTweet'
require_relative 'Twitter/AddUserName'

get_tweet #自分のTLからツイートを取得

@count = 0 #分析対象のツイート数
sum_score = 0 #平均Score計算用
sum_magnitude = 0 #平均Magnitude計算用

@tArray.each do |text|
  puts "-------------------------------------------------------------------"
  puts "tweet : #{text}"
  sentiment_analysis(text)
  @count = @count + 1
  sum_score = sum_score + @s_score
  sum_magnitude = sum_magnitude + @s_magnitude
end

@result_score = (sum_score/@count).round(3) #平均を計算
@result_magnitude = (sum_magnitude/@count).round(3)

puts "分析対象ツイート数:#{@count}"
puts "平均Score: #{@result_score}"
puts "平均Magnitude: #{@result_magnitude}"

add_emoji(@result_score,@result_magnitude)

課題点

  • ほんとに正しい結果かどうかいまいちわからない
  • 毎回実行するのが面倒。自動化したい(launchdとかわからんかった)

最後に

なんかもうちょっと実用的なもの作りたいなって思います。

アイデアの元となった記事
【祝】Twitterの名前をお天気と連動されるアプリを作った時の技術的な紹介【公開した🌥️】