R
Azure
数学
データ分析
統計学

AzureのAIで生成した中から、Rの回帰分析で探し出す最強のハッシュタグ

3行まとめ

  • Microsoft Azure Computer Vision APIで、Instagramに投稿するための食事画像のハッシュタグを自動で生成する
  • 生成されたハッシュタグと、投稿に付いたいいね!数の相関をR言語で回帰分析する
  • Instagramで最も「いいね!」がもらえる女子力最強のハッシュタグが分かる

pexels-photo-248533.jpeg

Instagramという物を以前からやっていたのですが、女子力が足りないのか私には面白さがよく分かりませんでした。
そこで、「いいね!」をたくさんもらえるように#ハッシュタグについて考えてみることにしました。

AIでハッシュタグを自動生成する

まずInstagramがよく分かっていない私には、投稿に付けるハッシュタグを自分で考えることが難しかったです。
なのでタグを自動生成することを考えました。

過去に『Microsoft Azureで 女子力を生成する』というふざけた題目でLTさせて頂いたのですが、おおよそこの発表内容の通りです。
Microsoft Azure Cognitive Service の Computer Vision API で、画像からタグを生成します。

図1.png

たとえば上記の画像をComputer Vision APIに投げると「a man swimming in a pool of water」のような説明文と、「water」「swimming」「sport」「pool」「person」「watersport」といったタグの情報が得られます。

このComputer Vision APIを利用して、画像に自動でハッシュタグを付けることができるRuby on Railsアプリケーションを開発しました。
https://github.com/s2terminal/azure_computer_vision_on_rails

図2.png

InstagramのAPI利用申請は面倒だったので、投稿は手動です。タグをコピペしてInstagramに貼り付ける男らしい仕様です。こういう所がおしゃれじゃないのが女子力欠如の所以だとかそういうのは気にしないです。

画像をサーバーにアップロードして、Computer Vision APIに投げてタグを生成し、ついでにTranslation Text APIも使って日本語の情報を取得するだけです。

app/controllers/vision_controller.rb
    upload_file = params[:image]
    if upload_file != nil
      # 画像のアップロード
      @vision = Vision.create()
      filedir = "./public/uploads/visions/#{@vision.id}"
      FileUtils.mkdir_p(filedir) unless FileTest.exist?(filedir)
      filepath = "uploads/visions/#{@vision.id}/#{upload_file.original_filename}"
      File.open("./public/#{filepath}", 'wb') do |f|
        f.write(upload_file.read)
      end
      @vision.url = "#{request.url}/#{filepath}"

      # リクエストの生成
      uri = URI('https://westus.api.cognitive.microsoft.com/vision/v1.0/analyze')
      uri.query = URI.encode_www_form({
        'visualFeatures' => 'Tags',
        'language' => 'en'
      })

      http = Net::HTTP::Post.new(uri.request_uri)
      http['Content-Type'] = 'application/json'
      http['Ocp-Apim-Subscription-Key'] = Rails.application.secrets.azure_computer_vision_api_key
      http.body = { url: @vision.url }.to_json
      response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |client|
        client.request(http)
      end

      # レスポンスの加工
      json = JSON.parse(response.body)
      raise json["message"] if response.code != "200"
      json["tags"].each{|tag|
        vision_tag = @vision.vision_tags.build(name: tag["name"], confidence:tag["confidence"])
        vision_tag.save
      }

      # 翻訳のアクセストークン取得
      uri = URI('https://api.cognitive.microsoft.com/sts/v1.0/issueToken')
      http = Net::HTTP::Post.new(uri.request_uri)
      http['Content-Type'] = 'application/json'
      http['Accept'] = 'application/jwt'
      http['Ocp-Apim-Subscription-Key'] = Rails.application.secrets.azure_translator_text_api_key
      response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |client|
        client.request(http)
      end
      token = response.body

      # 翻訳
      require 'rexml/document'
      uri = URI('https://api.microsofttranslator.com/V2/Http.svc/Translate')
      @vision.vision_tags.each{|tag|
        uri.query = URI.encode_www_form({
          'appid' => 'Bearer ' + token,
          'text' => tag.name,
          'to' => 'ja'
        })
        http = Net::HTTP::Get.new(uri.request_uri)
        http['Accept'] = 'application/xml'
        response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |client|
          client.request(http)
        end
        raise response.body if response.code != "200"
        doc = REXML::Document.new(response.body)
        tag.translated_name = doc.elements.first.text
        tag.save
      }


      @vision.save
      redirect_to :action => "show", :id => @vision.id

APIを叩くのが順番に同期処理なので遅かったり、アップロード時のファイル名そのまま保存する仕様はどうなんだとか、色々イケてないプログラムなのでもしご利用される際はお気を付けください。なお実際には私はアクセス制限をかけたWebアプリケーションとして使っています。

この方法で生成したハッシュタグを何も考えずにぺたぺた貼り付けていたところ、以前のような「いいね0件」のような事態は起こらなくなり、「いいね!」が付くようになりました。

20171223_instagram.png

さて、こんな仕組みで本当に「いいね!」を獲得できているのか、女子力を会得したと言って良いのか、どのタグが最も「いいね!」に貢献しているのかを調べることにしました。

「いいね!」がもらえるハッシュタグを回帰分析で探す

たとえば、5つの投稿の「いいね!」数が下記だったとします。

投稿 付いたハッシュタグ いいね数
投稿A #肉 #食べ物 21いいね
投稿B #野菜 #食べ物 5いいね
投稿C #肉 #野菜 20いいね
投稿D #食べ物 10いいね
投稿E #肉 #野菜 #食べ物 20いいね

パッと見、なんだか「肉」ハッシュタグが付いていると「いいね!」が増えるように見えます。
この結果を、線形回帰分析で定量的に算出してみます。

まず、ハッシュタグといいね数の関係を足し算の数式にします。
(積の方がモデルとして適切かもしれませんが、対数を取ることで和と同様の方法で相関関係の算出ができると思います)

投稿A: (基準値)+("肉"に付くいいね数)+("食べ物"に付くいいね数)=21
投稿B: (基準値)+("野菜"に付くいいね数)+("食べ物"に付くいいね数)=5
投稿C: (基準値)+("肉"に付くいいね数)+("野菜"に付くいいね数)=20
投稿D: (基準値)+("食べ物"に付くいいね数)=10
投稿E: (基準値)+("肉"に付くいいね数)+("野菜"に付くいいね数)+("食べ物"に付くいいね数)=20

ここで

  • ("肉"に付くいいね数) := $a_1$
  • ("野菜"に付くいいね数) := $a_2$
  • ("食べ物"に付くいいね数) := $a_3$
  • (基準値) := $b$

と置くと

a_1 + a_3 + b = 21 \\
a_2 + a_3 + b = 5 \\
a_1 + a_2 + b = 20 \\
a_3 + b = 10 \\
a_1 + a_2 + a_3 + b = 20 \\

行列表記に書き換えると、投稿した結果は下記のように表現できます。

\left( \begin{array}{ccc}
1 & 0 & 1 \\
0 & 1 & 1 \\
1 & 1 & 0 \\
0 & 0 & 1 \\
1 & 1 & 1 \\
\end{array} \right)
\left( \begin{array}{c}
a_1 \\
a_2 \\
a_3 \end{array} \right)
+ b
=
\left( \begin{array}{c} 21 \\ 5 \\ 20 \\ 10 \\ 20 \end{array} \right)

このとき、ある投稿について、タグ$ a_i $を付けた時は$ x_i = 1$ 、 付けなかった時は$ x_i = 0$ となる$ x_i$を用いると
ある投稿に付くいいね!の数$ y$は

a_1x_1 + a_2x_2 + a_3x_3 + b = y

と表現できます。
つまり、 $ a_i$の値が最も大きいタグこそが、もっとも「いいね!」の付くタグ、最強のタグ と言えます。

この$ a_i$の値を、 重回帰分析 を用いて推定します。
「ハッシュタグ」と「いいね!」のように「因」と「果」があるものについて、「因」から「果」を予測するための回帰式を求めるのが、回帰分析です。

用語 説明
説明変数 ハッシュタグの有無を表す$x_i$
目的変数 (被説明変数) 「いいね数」である$y$
回帰係数 ハッシュタグと「いいね数」の関係を示す $a_i$

回帰分析の計算手順は長くて複雑なのですが、Rを使うことで簡単に算出できます。
本稿では解説しませんが、この計算で実際に何が行われているのか中身を知っておくと回帰分析の理解が進むと思います。

R環境構築

まずは試してみましょう。

R実行環境のRStudio ServerはrockerというDockerImageがあるので、Dockerがあれば簡単に構築できます。良い時代になりました。

PowerShell
> docker run --rm -p 8787:8787 rocker/rstudio

http://localhost:8787 にアクセスし、UserとPassにrstudioと入力すると、RStudio使うことができます。

回帰式の算出

まずはデータを登録します。

> likelist <- data.frame(tag1 = c(1,0,1,0,1),tag2 = c(0,1,1,0,1),tag3 = c(1,1,0,1,1),like = c(21,5,20,10,20))
> likelist
  tag1 tag2 tag3 like
1    1    0    1   21
2    0    1    1    5
3    1    1    0   20
4    0    0    1   10
5    1    1    1   20

lm()関数に、目的変数とデータを渡してあげることで回帰係数を求めることができます。
(lmはLinear Modellingの略でしょうか?)

> result = lm(like~., data=likelist)
> result

Call:
lm(formula = like ~ ., data = likelist)

Coefficients:
(Intercept)         tag1         tag2         tag3  
         10           13           -3           -1  

$a_1 = 13, a_2 = -3, a_3=-1, b = 10$であることが、簡単に分かりました。

つまり、この例の場合「肉」タグと「いいね数」にはプラス13の正の線形関係、「肉」タグで13いいね獲得できるということが分かります。

実際に獲得できる「いいね!」数を求める回帰式

13x_1 -3x_2 -x_3 + 10 = y

となります。

回帰分析の精度

回帰式はあくまで目的変数(いいね数)を予測するための式であり、実際の値とは一致しません。
回帰式で推定されるいいね数を、実際のいいね数と比べてみましょう。

投稿 付いたハッシュタグ いいね数 推定値
投稿A #肉 #食べ物 21いいね 22いいね
投稿B #野菜 #食べ物 5いいね 6いいね
投稿C #肉 #野菜 20いいね 20いいね
投稿D #食べ物 10いいね 9いいね
投稿E #肉 #野菜 #食べ物 20いいね 19いいね

いかがでしょうか。
完全一致とはいかないものの、結構いい感じの精度だと思います。

この精度は、lm()関数の出力結果をsummary()関数に渡すことで定量的に確認できます。

> summary(result)

Call:
lm(formula = like ~ ., data = likelist)

Residuals:
        1         2         3         4         5 
-1.00e+00 -1.00e+00  1.11e-16  1.00e+00  1.00e+00 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)  
(Intercept)   10.000      3.464   2.887   0.2123  
tag1          13.000      2.000   6.500   0.0972 .
tag2          -3.000      2.000  -1.500   0.3743  
tag3          -1.000      2.646  -0.378   0.7699  
---
Signif. codes:  0 *** 0.001 ** 0.01 * 0.05 . 0.1   1

Residual standard error: 2 on 1 degrees of freedom
Multiple R-squared:  0.981, Adjusted R-squared:  0.9241 
F-statistic: 17.23 on 3 and 1 DF,  p-value: 0.1748

なんか色々出てきました。

用語 名前 説明
Coefficients (Estimate) 回帰係数 (推定値) 説明変数と目的変数とのあいだの、線形関係の係数。大きければ大きいほど、説明変数が増えると目的変数も増えると言える
Pr(>|t|) p値 回帰係数がゼロであるという帰無仮説の仮説検定結果のp値。0に近いほど、線形関係がある可能性が高い
Adjusted R-squared 調整済み決定係数$R^2$ 目的変数について、推定値の分散を実測値の分散で割ったようなもの。1に近いほど、回帰式は実測値を正確に言い表している

超ざっくり言うとp値が小さくて$R^2$が大きいほどハッピーな感じです。
この場合、

  • $R^2$は0.92と大きいので、回帰式の精度は高い
  • tag1のp値は0.10と小さいので、「#肉」と「いいね!」数には線形関係がありそう
  • tag2、tag3のp値はそれぞれ0.37、0.77と大きいので、「#野菜」「#食べ物」と「いいね!」数には線形関係が無いかもしれない

という感じになります。

実際にInstagramのハッシュタグを解析

実際に、先ほどのWebアプリケーションのデータベース上には55個のハッシュタグと100件の画像投稿データがありました。そこからデータ数の少ないタグやInstagramに投稿していない画像などを削除し、「beer」「beverage」「bowl」など25個のハッシュタグと、27件分の投稿の「いいね!」数データを使いました。
本当はInstagramのAPIから取ってきたいのですが、たかが27件分なので「いいね!」数のデータは手動で集めました。

なお実際には生成されたタグを日本語に翻訳して使う時と英語のまま使った時があります。
今回はデータ数を確保するため、言語による差異は無いと仮定して英語に統一します。

20171223_azure2.png

このようなCSVを作ってRStudioにアップロードして、読み込んでみます。

> likelist <- read.csv("likelist.csv")
> result <- lm(like~., data=likelist[,2:length(likelist)])
> summary(result)

(中略)

Coefficients:
                      Estimate Std. Error t value Pr(>|t|)
(Intercept)          3.000e+00  1.375e+01   0.218    0.863
beer                 2.100e+01  1.775e+01   1.183    0.447
beverage            -5.000e+00  1.039e+01  -0.481    0.715
bowl                -8.000e+00  8.485e+00  -0.943    0.519
(後略)

係数は算出されましたが、全体的にp値が高くなっています。
「meal」「indoor」などほとんどの投稿に付いているタグや、「drink」「glass」のように付いている投稿がほとんど重複しているタグがあるせいか、分析の精度が下がっているようです。
相関のありそうなタグに絞って計算してみます。

変数の選択

手動で変数を減らしたり増やしたりしていってp値を調整してもいいのですが、Rのstep()関数を使うことでp値の低い変数を自動的に除去し、適切な結果を得る事ができます。便利ですね。

> filteredResult <- step(result)
> summary(filteredResult)

(中略)

Coefficients:
                    Estimate Std. Error t value Pr(>|t|)    
(Intercept)          -0.4099     2.1554  -0.190 0.855459    
beer                 17.3469     3.0896   5.615 0.001362 ** 
beverage             -4.6003     1.7870  -2.574 0.042088 *  
bowl                 -7.8359     1.9863  -3.945 0.007582 ** 
chocolate            -7.7976     2.7099  -2.877 0.028151 *  
coffee               -3.3189     1.9659  -1.688 0.142339    
croquette            -3.0000     2.6036  -1.152 0.293048    
cup                   9.6148     3.0993   3.102 0.021057 *  
dessert               5.3844     2.3621   2.279 0.062848 .  
dish                  8.9158     3.0374   2.935 0.026103 *  
eaten                 9.5451     2.6632   3.584 0.011587 *  
food                 -3.6327     1.9327  -1.880 0.109213    
fresh                10.5570     3.1619   3.339 0.015636 *  
meat                  1.7338     1.6328   1.062 0.329139    
pan                   7.8231     2.9833   2.622 0.039462 *  
pasta                 5.1395     2.5589   2.008 0.091351 .  
person               16.8350     2.2953   7.335 0.000328 ***
piece.de.resistance  -2.4303     2.2419  -1.084 0.319990    
plate                 4.8231     1.4564   3.312 0.016173 *  
rice                  9.4855     4.0364   2.350 0.057057 .  
stew                  3.8231     2.9833   1.282 0.247303    
---
Signif. codes:  0 *** 0.001 ** 0.01 * 0.05 . 0.1   1

Residual standard error: 1.841 on 6 degrees of freedom
Multiple R-squared:  0.9651,    Adjusted R-squared:  0.8488 
F-statistic: 8.296 on 20 and 6 DF,  p-value: 0.00738

出ました!

ハッシュタグ 回帰係数
beer 17.3469
person 16.835
fresh 10.557
cup 9.6148
eaten 9.5451
rice 9.4855
dish 8.9158
pan 7.8231
dessert 5.3844
pasta 5.1395
plate 4.8231
stew 3.8231
meat 1.7338
piece.de.resistance -2.4303
croquette -3
coffee -3.3189
food -3.6327
beverage -4.6003
chocolate -7.7976
bowl -7.8359

#beer#person というハッシュタグが、いずれも回帰係数が約17、つまり付けると17いいね!もらえる女子力の強いハッシュタグだということが分かりました。

逆に、#chocolate や #coffee などは「いいね!」数と負の相関関係という興味深いデータも算出されていますね。

つまり、例えばこのような「ビール」と「人物」の映った画像が、Instagramで最も「いいね!」がもらえる最強に女子力の高い画像という事になります。

20171224_malts.png

結論

  • 何を食べるかよりも、ヒトと食べる写真を上げた方が「いいね!」がもらえる
  • コーヒーは「いいね!」が減る。ビールは「いいね!」が増える
  • さやかちゃんはかわいい

クリスマスの夜はぜひ大切なパートナーとお酒を飲む風景を写真に撮り、#person#beerタグを付けてInstagramに投稿するのは如何でしょうか。私は家で一人でスプラトゥーンでもしてますが。

参考文献

以上、Cogbot!Advent Calendar 2017 の24日目でした。
それではみなさま、良いクリスマスをお過ごし下さいませ。