この記事は、 ミクシィ18新卒 Advent Calendar 2017 の22日目の記事になります。(1日遅れましたごめんなさい)
皆さん、インスタ映え、してますか?
Instagramは近年女子大生を中心に大流行している、ユーザーが画像を気軽に投稿できるタイプのSNSです。
近頃は「インスタ映え」というワードも至る所で聞かれるようになり、観光地なども「インスタ映え」を重視したスポットなどを乱立するようになってしまいました。
その結果、Instagramに投稿される画像は、どの画像も同じようなものばかり。
同じようなものばかりということは、実は簡単に機械的に生成することができるのではないでしょうか?
ということで、今回はディープラーニングで最高にインスタ映えする画像を生成していきます。
リポジトリ: Instafly
画像データ収集
実のところ、今回一番難しかったのは画像収集の部分でした。
まず大前提として、InstagramのAPIは制限が大きく、運営に承認されるまでは個別に承認を取った10名程度のユーザーのデータしか収集できないという大問題があります。
そのため、公式のAPIを使うという作戦は序盤で断念。かと言ってスクレイピングするのは申し訳ないし、自分のアカウントでログインした状態でスクレイピングして垢BANされるのもできれば避けたいです。
そこで、これらを解決する方法として、mitmproxyを使うことにしました。
iPhoneの通信の間にmitmproxyを挟むことによって、家でInstagramを見ているだけで勝手に画像が収集できる仕組みを作りました。
詳細はあんまり詳しく書いてしまうと怒られそうな気もしないこともないので、気になる人はリポジトリを見てください。
学習
上に書いた方法で、363ユーザー、3231枚の画像を取得することができました。
これらを元に、まずは投稿のいいね数を推定できるようなニューラルネットを学習させていきます。
今回以下のような雑な推定のものに、いいね数そのものではなく、投稿の「インスタ映え度」という数値を画像から推定していきます。
- Instagram上のあるユーザーの投稿を見れるのは、そのユーザーのフォロワー、もしくは投稿についたハッシュタグで検索した人
- ハッシュタグで頻繁に検索する人は、ユーザー全体からみればごく少数
- Twitterで言う、RTに相当するような機能は存在しない
- このことから、あるユーザーの投稿に付くいいねの数は、そのユーザーのフォロワーの数にある程度比例するのではないか
- また、簡単のため今回は投稿に付くハッシュタグや本文の内容は、いいね数とは全く無関係とする
- 以上のことから、投稿に付くいいねの数は、「ユーザーのフォロワー数」×「画像から求められるインスタ映え度」で求められる
この比例定数
\[インスタ映え度 = \frac{投稿のいいね数}{ユーザーのフォロワー}\]
を、画像から推定するニューラルネットを構築していきます。
今回使ったモデルはこちら。
上の部分の層は、VGG-16という画像認識によく使われる有名なモデルになっています。
今回はVGG-16の部分には予めImageNetで学習させた重みを初期値として使い、最後の畳み込み層以外の重みは初期値のまま固定しておきます。いわゆるFine-Tuningというやつです。
このニューラルネットに、入力として320x320にリサイズした画像を与え、その画像の持つ「インスタ映え度」を出力として学習させます。
画像生成
学習が終わった後は、上で学習させたニューラルネットを元に「最高にインスタ映えする画像」を生成していきます。
画像生成の基本的な考え方は、以下の通りです:
通常の学習では、入力 \(x\) と重み \(W\) からなるニューラルネット \(f(x; W)\) の出力と、実際の評価値の間の損失 \(L\) について、
\[W \leftarrow W - \eta \frac{\partial L}{\partial W}\]
という更新式で \(W\) を動かし、損失関数を最小化していきます。
それに対して、今回は \(W\) を固定し、画像 \(x\) を動かしてインスタ映え度の推定値 \(f(x; W)\) を最大化します。
すなわち、
\[a = -f(x; W)\]
の値を、以下のような更新式で最小化していくことにより、最終的に得られる \(x\) が「最高にインスタ映えする画像」になるという理屈です。
\[x \leftarrow x - \eta \frac{\partial a}{\partial x}\]
なお、実際は \(x\) の更新式として、もう少しだけ複雑なモメンタムという方法を使用しています。
実際のコードはこんな感じです:
class MomentumUpdator(object):
def __init__(self, lr=0.01, gamma=0.01):
self._lr = lr
self._gamma = gamma
self._v = None
def update(self, param, loss, X):
grads = K.gradients(loss, [param])[0]
f = K.function([param], [loss, grads])
if self._v is None:
self._v = np.zeros(X.shape)
loss_val, grads_val = f([X])
self._v *= self._gamma
self._v += self._lr * grads_val
X -= self._v
return loss_val
class Minimizer(object, metaclass=abc.ABCMeta):
def __init__(self, updator=MomentumUpdator(), **kwargs):
self._updator = updator
@abc.abstractmethod
def get_input(self):
...
@abc.abstractmethod
def get_loss(self):
...
@abc.abstractmethod
def create_initial_input(self):
...
def minimize_loss(self, num_loops=1000):
X = self.create_initial_input()
input = self.get_input()
loss = self.get_loss()
for i in range(num_loops):
loss_val = self._updator.update(input, loss, X)
print('%d: %f' % (i, loss_val))
return X
class ImageVisualizer(Minimizer):
def __init__(self, model, initial_image=None, **kwargs):
self._model = model
self._initial_image = initial_image
super().__init__(**kwargs)
def get_input(self):
return self._model.input
def get_loss(self):
return -self._model.layers[-2].output
def create_initial_input(self):
if self._initial_image is None:
return np.random.random((1, config.IMG_WIDTH, config.IMG_HEIGHT, 3))
else:
return self._initial_image
ちなみに、画像 \(x\) の初期値は乱数により決定しています。
これを使って、先ほどの更新式を100回ほど回し、インスタ映えする画像を生成していきましょう。
いい感じに映えてますね。
上の例では \(x\) の初期値として乱数を使っていましたが、初期値として既存の画像を与えることで、その画像をさらにインスタ映えさせることができるようになるはずです。
というわけで、以下の画像を初期値として入力し、上の更新式を100回適用してみました。
その結果が以下になります。
なんということでしょう。芸術的な装飾が施され、何の変哲もないカフェの画像が見事にインスタ映えするようになってしまいました。
結果
上で生成した2枚の画像を実際にInstagramに投稿し、普段の投稿に比べてどれだけいいねが稼げるかを確認してみました。
- 自分のInstagramのフォロワーは、基本的に大学の友人のみ
- Instagramのヘビーユーザーはあまりいない
- 通常は1投稿につき15-25いいねくらい
という前提で見ていただくと、今回の結果が如何にインスタ映えしたかがお分かりいただけるかと思います。
それでは、結果をどうぞ!!
普通
おわり