「スタバなう」でアップロードされたtwitterの画像をGoで収集してS/N比を調べる

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">こういうtweetが機械学習界隈からの怒りを買ってます(笑) <a href="https://t.co/COV1IHyh03">https://t.co/COV1IHyh03</a></p>&mdash; Yuki Suga (@ysuga) <a href="https://twitter.com/ysuga/status/1154689461503942662?ref_src=twsrc%5Etfw">July 26, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、「スタバなう」と呟きながら関係ない画像を投稿するのが流行っているようです。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">ツイッター上に「スタバなう」とラーメン等を投稿することに深層学習系が怒っているという話しですが、ツイッターから取得するノイズの多い情報を使いこなせないようでは理想環境下に置いてしか成果出せない使えない人ということでしょう。<br>スタバなうと呟いてSN比を悪化させよう！</p>&mdash; シャイニング丸の内 (@shimaru365) <a href="https://twitter.com/shimaru365/status/1155262898953867264?ref_src=twsrc%5Etfw">July 27, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

実際にどれぐらいS/N比が悪化しているのか、画像を収集して調べてみます。

## golang+anacondaでtwitter APIを叩く
golangのtwitter SDKは、[ChimeraCoder/anaconda: A Go client library for the Twitter 1\.1 API](https://github.com/ChimeraCoder/anaconda)を使うのが良いっぽいです。(pythonの仮想環境を作るanacondaとは関係ない)

以下のような感じでツイートを取得することができます。

```go
api := anaconda.NewTwitterApiWithCredentials(
	accessToken, accessTokenSecret, consumerKey, consumerSecret,
)
v := url.Values{}
v.Set("count", "100") // 取得する件数(最大100件)
v.Set("max_id", maxIdStr) // ここで指定した数値以下のIDをもつツイートを検索対象とする
v.Set("since_id", sinceIdStr) // ここで指定した数値より大きいIDをもつツイートを検索対象とする
query := "スタバなう OR #スタバなう exclude:retweets filter:images"
searchResult, _ := api.GetSearch(query, v)
```

今回は、「スタバなう」というツイートがハッシュタグを含むツイートのうち、画像が添付されていてリツイートではないものだけを取得したいので、queryは`"スタバなう OR #スタバなう exclude:retweets filter:images"`としています。このようなフィルタリングは、anacondaではなくtwitterのsearch API自体の仕様です。

### 画像のダウンロード

まず与えられたURLからファイルをダウンロードしてローカルに保存する関数を定義します。

```go
func DownloadFile(fileUrl, downloadPath string) (err error) {
	response, err := http.Get(fileUrl)
	if err != nil {
		return xerrors.Errorf("failed to request http get to %s: %w", fileUrl, err)
	}
	defer func() {
		cerr := response.Body.Close()
		if cerr == nil {
			return
		}
		err = xerrors.Errorf("failed to close http response body: %w", err)
	}()

	file, err := os.Create(downloadPath)
	if err != nil {
		return err
	}
	defer func() {
		cerr := file.Close()
		if cerr == nil {
			return
		}
		err = xerrors.Errorf("failed to close file(%s): %w", downloadPath, err)
	}()

	if _, err := io.Copy(file, response.Body); err != nil {
		return xerrors.Errorf("failed to write download file to local file(%s): %w", downloadPath, err)
	}
	return nil
}
```

ツイートに添付された画像は`anaconda.Tweet.ExtendedEntities.Media`に配列として入っています。
`anaconda.Tweet.Entities.Media`というのもあるのですが、こちらは何枚画像が添付されていても１枚しか入っていません。

```go
tweets := searchResult.Statuses
tweet := tweets[0]
for i, entityMedia := range tweet.ExtendedEntities.Media {
	mediaRawUrl := entityMedia.Media_url_https
	_ := DownloadFile(mediaRawUrl, "image.jpg")
}
```

これで検索した画像をローカルに保存できるようになりました。

## 実際に画像を収集してみる
上記のコードは分かりやすさを優先して書いていますが、実際には以下を考慮しておいたほうが良いです。

* 検索結果のファイルへの書き出し
    * だいたい後から画像のメタデータを取りたくなります
* 検索/ダウンロードのレジューム機能
    * 完了まで時間がかかるので、途中でうっかり落ちても続きから実行できるようになっていると安心です
    * 一度実行してからの最新画像の差分を取得するのも効率的にやりたい
* 画像ダウンロードのインターバル
    * twitter APIは叩きまくってもlimitに引っかかるだけなので良いですが、画像ダウンロードは普通にクローリングしてるだけなので、適切なインターバルを入れてtwitter社に迷惑をかけないようにしましょう。

というわけで、twitterデータ収集用のCLIを書きました。

<a href="https://github.com/mpppk/twitter"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/mpppk/twitter.png" width="460px"></a>

こういう感じで画像をダウンロードできます。

```bash
$ twitter search \
  ---db-path tweets.db \
  -query スタバなう \
  --exclude retweets \
  --filter images 
# -> ツイート情報がtweets.dbに保存される

$ twitter download images --db-path tweets.db --dir images
media is downloaded to images/1157466034606972928-0-EBAlOGZUcAA2do9.jpg
media is downloaded to images/1157493760533471232-0-EBA-aqcUEAIdY8T.jpg
...
# -> tweets.dbに保存された画像から画像URLを取り出してimagesディレクトリに保存する

$ tree images | head -n 5
images
├── 1152831484220170240-0-D_-uHqzVUAAwTC2.jpg
├── 1152832631391657985-0-D_-vKCfU0AAk2xR.jpg
├── 1152846684981829632-0-D_-78LbUIAExWaV.jpg
├── 1152860261876822016-0-D__ISFVVAAApkg7.jpg
```

一晩ほっとくと4000枚ぐらいの画像が集まりました。

## スタバ画像にアノテーション付け

では集まった画像のうち本当にスタバで撮影された画像を数えます。
こういう用途の便利なツールが多分あるんだろうなーと思いつつ、今回はmacのタグ機能で、地道にやっていきます。
まずfinderの環境設定を開いて、よく使うタグにアノテーション用のタグを登録しておきます。
今回は「スタバ」というタグを登録しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28464/816c2036-5386-5b30-4de2-6611cbb555d5.png)

finder上で[ファイルを選択してctrl+1~7を押すと、よく使うタグの場所に対応するタグをそのファイルへ付与することができます。](https://support.apple.com/ja-jp/guide/mac-help/mchlp15236/10.14/mac/10.14)
あとはダウンロードした画像が入ったディレクトリをfinderで開いて、スタバの画像にタグを付与していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28464/c8f60fd4-203c-85b8-6a14-cc18e3742d5b.png)

最後に、finderの検索でスタバタグを指定すれば、画像の枚数がわかります。

(余談ですがmacのタグ機能は挙動がかなり怪しくて、アルファベット表記のタグが検索で引っかからなかったり(日本語UIにしてるから？)、自分の環境ではctrl+3-7は動作するけど1と2はしなかったりします...できればちゃんとしたアノテーションツールを使うほうが安心だと思います。)

## 結果

4時間ぐらいで4064枚のアノテーション付けをやったところ、結果は以下のようになりました。

|分類|枚数|
|---|---|
|全体|4064|
|スタバ画像|973|

真のスタバ画像は全体のだいたい24%ぐらいのようです。
これはどうなんでしょうかね。個人的には思ったよりちゃんとスタバの画像をアップしてる人いるなという感想です。
ちなみにラーメンの画像は617枚で、約15%です。
ただし、画像の集計時期は2019/07/21-2019/08/03で、7/26に冒頭のツイートがされている影響でおそらく通常よりもノイズが多いことにご留意ください。

## おわり
今回はS/N比を知りたかっただけなんですが、結果的にアノテーション済みのスタバ画像(とラーメン画像)がわりと集まったので、スタバなうムーブメントに激怒している機械学習界隈の方がいらっしゃればメタデータを差し上げますのでお声がけください。