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

## 3行まとめ

- [Microsoft Azure Computer Vision API](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)で、Instagramに投稿するための食事画像のハッシュタグを自動で生成する
- 生成されたハッシュタグと、投稿に付いたいいね！数の相関を[R言語](https://www.r-project.org/)で回帰分析する
- Instagramで最も「いいね！」がもらえる**女子力最強のハッシュタグ**が分かる

![pexels-photo-248533.jpeg](https://qiita-image-store.s3.amazonaws.com/0/50391/8600393e-410d-0038-a860-0a9cd9e3555a.jpeg)

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

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

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

過去に[『Microsoft Azureで 女子力を生成する』](https://www.slideshare.net/ShutoSuzuki/microsoft-azure-81231635)というふざけた題目でLTさせて頂いたのですが、おおよそこの発表内容の通りです。
Microsoft Azure Cognitive Service の [Computer Vision API](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/) で、画像からタグを生成します。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/50391/2df09bef-18b2-21d7-d9e2-e325192b8f81.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](https://qiita-image-store.s3.amazonaws.com/0/50391/f7778d1a-8a08-112e-d36f-cd24b1683bab.png)

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

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

```ruby: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](https://qiita-image-store.s3.amazonaws.com/0/50391/c3f60474-09f2-722a-dc82-66fd3fe25eda.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$

と置くと

```math
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 \\
```

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

```math
\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$は

```math
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](https://hub.docker.com/r/rocker/rstudio/)があるので、[Docker](https://docs.docker.com/docker-for-windows/install/)があれば簡単に構築できます。良い時代になりました。

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

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

### 回帰式の算出

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

```r
> 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の略でしょうか？)

```r
> 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いいね獲得できる**ということが分かります。

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

```math
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()`関数に渡すことで定量的に確認できます。

```r
> 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(>&#124;t&#124;)|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](https://qiita-image-store.s3.amazonaws.com/0/50391/fb0218c5-9a2c-20b6-9fe7-ff9d348d8d15.png)

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

```r
> 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値の低い変数を自動的に除去し、適切な結果を得る事ができます。便利ですね。

```r
> 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](https://qiita-image-store.s3.amazonaws.com/0/50391/f530df9b-078a-0ed9-54ea-325df858b2fb.png)

## 結論

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

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

## 参考文献

- 統計の本
    - [『恋する統計学［回帰分析入門（多変量解析1）］』金城俊哉 著 秀和システム](http://www.shuwasystem.co.jp/products/7980html/5072.html)
    - [『統計学図鑑』栗原伸一・丸山敦史 著 オーム社](http://shop.ohmsha.co.jp/shopdetail/000000005063/)
- Rの本
    - [『Rではじめるビジネス統計分析』末吉正成 里洋平 酒巻隆治 小林雄一郎 大城信晃 翔泳社](http://www.shoeisha.co.jp/book/detail/9784798134901)
    - [『楽しいR ビジネスに役立つデータの扱い方・読み解き方を知りたい人のためのR統計分析入門』豊澤栄治 翔泳社](http://www.shoeisha.co.jp/book/detail/9784798139012)
- ウェブサイト
    - [お勉強日記: Rで簡単な重回帰分析](http://ayaka-study.blogspot.jp/2012/08/r.html)
    - [151116Rによる回帰分析\.pages](http://www.juen.ac.jp/lab/okumura/handout/151116R%E3%81%AB%E3%82%88%E3%82%8B%E5%9B%9E%E5%B8%B0%E5%88%86%E6%9E%90.pages.pdf)
    - [データ解析・マイニングとR言語](https://www1.doshisha.ac.jp/~mjin/R/Chap_15/15.html)



以上、[Cogbot！Advent Calendar 2017](https://qiita.com/advent-calendar/2017/cogbot) の24日目でした。
それではみなさま、良いクリスマスをお過ごし下さいませ。