友利奈緒判定botを作った
TVアニメCharlotteのヒロイン友利奈緒がTwitter上で異常に増殖する怪現象が起こっています。
友利奈緒検出器、川奈プロが3秒で実装しそうなやつだ
— Ararik (@fimbul11) September 2, 2015
と煽られたので実装しました。(3秒ではできませんでした)
遊び方
@mitra_sun22に画像つきのリプライを飛ばせば顔と判定した部分に白枠をつけた画像と判定結果を返信します。簡単ですね!
ちなみに友利奈緒と判定された画像は筆者(@kivantium)のTwitterアイコンに設定されるようになっています。
要素技術
Twitterで煽られたので、Twitterで送られた画像から顔を検出してその顔が友利奈緒かどうか判断してリプライするという仕様にしました。
ほとんど過去の記事からコピペしただけでできました。
- Twitterからの画像を処理する部分: Twitterアイコンをリプライ画像に変更するPythonスクリプト - kivantium活動日記
- 顔を検出する部分: dlibによるHOG特徴を用いた物体検出がすごい - kivantium活動日記
- 顔が友利奈緒か判断する部分: ご注文はDeep Learningですか? - kivantium活動日記
ソースコード
#!/usr/bin/env python #-*- coding:utf-8 -*- from tweepy import * import urllib import sys import datetime import re from PIL import Image import cv2 import sys import os.path import caffe from caffe.proto import caffe_pb2 from caffe.io import array_to_datum import numpy as np import skimage import copy import dlib import scipy # mitra_sun22のログイン情報 f = open('config.txt') data = f.read() f.close() lines = data.split('\n') # kivantiumのログイン情報 f = open('config2.txt') data = f.read() f.close() lines2 = data.split('\n') # Caffeの準備 mean_blob = caffe_pb2.BlobProto() with open('mean.binaryproto') as f: mean_blob.ParseFromString(f.read()) mean_array = np.asarray( mean_blob.data, dtype=np.float32).reshape( (mean_blob.channels, mean_blob.height, mean_blob.width)) classifier = caffe.Classifier( 'cifar10_quick.prototxt', 'cifar10_quick_iter_4000.caffemodel', mean=mean_array, raw_scale=255) # 顔検出器 detector = dlib.simple_object_detector("detector.svm") # エンコード設定 reload(sys) sys.setdefaultencoding('utf-8') def get_oauth(): consumer_key = lines[0] consumer_secret = lines[1] access_key = lines[2] access_secret = lines[3] auth = OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_key, access_secret) return auth def get_oauth2(): consumer_key = lines2[0] consumer_secret = lines2[1] access_key = lines2[2] access_secret = lines2[3] auth = OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_key, access_secret) return auth class StreamListener(StreamListener): # ツイートされるたびにここが実行される def on_status(self, status): if status.in_reply_to_screen_name=='mitra_sun22': if status.entities.has_key('media') : text = re.sub(r'@mitra_sun22 ', '', status.text) text = re.sub(r'(https?|ftp)(://[\w:;/.?%#&=+-]+)', '', text) medias = status.entities['media'] m = medias[0] media_url = m['media_url'] print media_url now = datetime.datetime.now() time = now.strftime("%H%M%S") filename = '{}.jpg'.format(time) try: urllib.urlretrieve(media_url, filename) except IOError: print "保存に失敗しました" frame = cv2.imread(filename) img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) #顔の検出 dets = detector(img) height, width = img.shape[:2] flag = True #顔が見つかった場合は顔領域だけについて判定 if len(dets) > 0: flag = False d = dets[0] # 一番大きいものだけを調べる仕様にした # 顔の領域がおかしい場合のチェック if d.top()<0 or d.bottom()>height or d.left()<0 or d.right()>width: flag = True else: image = frame[d.top():d.bottom(), d.left():d.right()] margin = min((d.bottom()-d.top())/4, d.top(), height-d.bottom(), d.left(), width-d.right()) icon = frame[d.top()-margin:d.bottom()+margin, d.left()-margin:d.right()+margin] cv2.imwrite("original.jpg", icon) # アイコン画像は顔よりすこし広い範囲にする #顔部分を白枠で囲む cv2.rectangle(frame, (d.left(), d.top()), (d.right(), d.bottom()), (255, 255, 255), 2) cv2.imwrite(filename, frame) if flag: #顔が見つからない場合には全体について判定する image = frame cv2.imwrite("original.jpg", image) # Caffeで読める形式に変換 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) sample = skimage.img_as_float(image).astype(np.float32) predictions = classifier.predict([sample], oversample=False) pred = np.argmax(predictions) if pred==0: #友利奈緒の場合 print "友利奈緒です" message = '.@'+status.author.screen_name+' 友利奈緒です({}%)'.format(int(predictions[0][pred]*100)) try: #アイコンの更新 api2.update_profile_image("original.jpg") except TweepError, e: print "error response code: " + str(e.response.status) print "error message: " + str(e.response.reason) else: print "友利奈緒ではありません" message = '.@'+status.author.screen_name+' 友利奈緒ではありません({}%)'.format(int(predictions[0][pred]*100)) message = message.decode("utf-8") try: #画像をつけてリプライ api.update_with_media(filename, status=message, in_reply_to_status_id=status.id) except TweepError, e: print "error response code: " + str(e.response.status) print "error message: " + str(e.response.reason) # streamingを始めるための準備 auth = get_oauth() auth2 = get_oauth2() api = API(auth) api2 = API(auth2) stream = Stream(auth, StreamListener(), secure=True) print "Start Streaming!" stream.userstream()
結果
第1世代
初日には適当に集めた150枚くらいの画像で学習したデータで動かしていました。
それなりの精度を出していたのですが、次第に誤認識されるパターンがフォロワーによって暴かれていきました。
友利奈緒と判定された画像たち pic.twitter.com/z9fwDF6wIk
— 友利奈緒 (@kivantium) September 3, 2015
他にも



のような銀髪+青い目という特徴を狙い撃ちにする単純なパターンが友利奈緒と認識されるようになっていました。
第2世代
そこでデータを増やして、精度の向上を図りました。
.@kivantium 友利奈緒ではありません(99%) pic.twitter.com/qgybnMRCAS
— まほろ (@mitra_sun22) September 3, 2015
のように最初はよくなったように見えたのですが……
.@coil_kpc 友利奈緒です(76%) pic.twitter.com/VB74uACCtU
— まほろ (@mitra_sun22) September 3, 2015
.@akemi_mkr 友利奈緒ではありません(99%) pic.twitter.com/dKpLropSxU
— まほろ (@mitra_sun22) September 3, 2015
.@fimbul11 友利奈緒ではありません(99%) pic.twitter.com/lzK4wVC6Rh
— まほろ (@mitra_sun22) September 3, 2015
.@akemi_mkr 友利奈緒です(100%) pic.twitter.com/RfN7QFQb5N
— まほろ (@mitra_sun22) September 3, 2015
のように次々と失敗例が挙げられていきました。
そしてついに
.@sampi_ 友利奈緒です(99%) pic.twitter.com/bKVgV8GtRh
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(99%) pic.twitter.com/uQsk3MyovJ
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(99%) pic.twitter.com/mKVyYroL5N
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(98%) pic.twitter.com/3xorK9bgBo
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(99%) pic.twitter.com/n4tx7ohLwB
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(99%) pic.twitter.com/lgz8wv3C5w
— まほろ (@mitra_sun22) September 3, 2015
.@sampi_ 友利奈緒です(99%) pic.twitter.com/JiuMFEod5F
— まほろ (@mitra_sun22) September 3, 2015
.@firkirb 友利奈緒です(91%) pic.twitter.com/c3R6o2Inos
— まほろ (@mitra_sun22) September 3, 2015
という過程で2色による誤認識パターンが発見されました。
これを発見した@sampi_さんには頭が上がりません。
第3世代
第2世代の反省を元にdropoutなどの技術を追加した第3世代のネットワークで運用を行いました。
ちなみに第3世代に投稿された画像は集計を行った21時半の段階で448枚でした。この判定対象部位のうち、真の友利奈緒が98枚、友利奈緒ではないものが404枚、友利奈緒ではあるが加工されている/顔ではない画像が44枚でした。いかに誤判定が狙われているかが伺えます。
面白かった判定例
.@akemi_mkr 友利奈緒です(99%) pic.twitter.com/ozlio5LT1G
— まほろ (@mitra_sun22) 2015, 9月 4
.@akemi_mkr 友利奈緒ではありません(99%) pic.twitter.com/CXa1svBgD2
— まほろ (@mitra_sun22) 2015, 9月 4
目の位置に青成分を入れる重要性が感じられます
.@14eggplant 友利奈緒です(98%) pic.twitter.com/OqdLJVgxiy
— まほろ (@mitra_sun22) 2015, 9月 4
.@14eggplant 友利奈緒です(98%) pic.twitter.com/UVXGFYspnQ
— まほろ (@mitra_sun22) 2015, 9月 4
.@14eggplant 友利奈緒です(76%) pic.twitter.com/dYEv0ETVjH
— まほろ (@mitra_sun22) 2015, 9月 4
この撃破パターン、第2世代で見ました。
.@kaz_hiramatsu 友利奈緒です(100%) pic.twitter.com/Wa9KXSpgiG
— まほろ (@mitra_sun22) 2015, 9月 4
銀髪キャラによる攻撃パターンが多かったのですが、この発見によりピンクキャラによる攻撃が続きました。
学習データにピンク髪のキャラがほとんどいなかったのがピンクに弱い原因かもしれません。
.@nick_debu_p 友利奈緒です(76%) pic.twitter.com/04jBqxJqM9
— まほろ (@mitra_sun22) 2015, 9月 4
BPOもビックリ!
.@akemi_mkr 友利奈緒です(99%) pic.twitter.com/UUNCvY4H7X
— まほろ (@mitra_sun22) 2015, 9月 4
ふざけずにちゃんと認識してくれ!
.@nick_debu_p 友利奈緒です(99%) pic.twitter.com/dBxrXYvqVt
— まほろ (@mitra_sun22) 2015, 9月 4
(ピンクは)ヤバいです ヤバいです もう本当にヤバいんです。
.@EbXpJ6bp 友利奈緒です(50%) pic.twitter.com/Cks4HlWmtf
— まほろ (@mitra_sun22) 2015, 9月 4
.@EbXpJ6bp 友利奈緒です(50%) pic.twitter.com/ijh2XbmuIu
— まほろ (@mitra_sun22) 2015, 9月 4
50%というなんとも微妙な判定が2回も出ました。
khws4v1.myhome.cx
あんまりまほろさんをいじめないであげてください……
番外編
.@fimbul11 友利奈緒ではありません(100%) pic.twitter.com/292BmJRjCi
— まほろ (@mitra_sun22) 2015, 9月 3
これはさすがに厳しすぎます……
第4世代
第3世代のネットワークのまま、学習回数と画像数を増やしたネットワークを現在準備中です。今夜のCharlotteが放送されるまでに投入したいです。Caffeの活躍に期待しています。
強化を続けるネットワークとそれに挑むオタクたち。
最後に勝利を収めるのはDeep Learningか、オタクたちか。
目の離せない戦いが飽きられるまで続きます。