2019年11月23日土曜日

ここ最近の使い物にならないMacBook Proは機械学習マシンとして使えるか ~PlaidML~

機械学習の話題ばかりですみません.

ここ最近のMacBook Pro,本当に使えないですねぇ.

ここ最近のMacBook Pro

ここ最近のMacBook Proの嫌いな点を箇条書きで書きます.

・キーボードが押しづらい.たまに'a'キーが,押した感触が一切なくなる(でも押せている)
・Touch Bar(キーボード上部の細長いタッチパネル)などといううどの大木のせいで物理escキーがなくなった
・トラックパッドがでかすぎる.タイピングしている途中に,知らないうちに触っていて,意図しないところをクリックしてしまう
・外部機器とのインタフェースがUSB Type-Cだけになったことによって,互換性が非常に悪くなった(特に,BootCamp時に外部ディスプレイに接続する時の成功率が著しく低い.映像ソフトウエアの開発をする身としては致命的な欠陥)
GPUがNVidia GeforceではなくAMD Radeon Proになってしまった

何が「GAFAでは生産性が重視される」ですか.自分は生産性を低下させる製品を世に出しておいて良い御身分ですね.若い人材を使い潰して生産人口を低下させる日本のブラック企業と同様に社会に害しかもたらしていない企業です.

私のしょぼいキャリアの中で言えば,GPUがNVidiaでなくなってしまったのは,かなりキツイです.CUDAが使えなくなったからです.私は仕事でも趣味でも結構CUDAを使ってきました.これまでのMacBook ProはNVidiaのGPUを搭載していたので,焼肉食べ放題とかに行ってCUDAの開発をしたりということができていました.しかし,ある時期からMacBook Proに搭載するGPUがAMDになってしまったせいで,それが出来なくなってしまいました.そのために,最近はOpenCLでグラフィック表示との連携を試したりもしています.(でもCUDAの方が断然分かりやすいです.)

さて,ここ一年くらいで,私はようやく重い腰をあげて機械学習を勉強し始めて,自分が出演できそうなAVのジャンルとプレイを明らかにしたりしてきました.一個人が機械学習をやってみる上で,最も課題となるのは,その計算量の多さです.ちょっとした機械学習プログラムを実行するのでも,CPUで計算すると,何日単位で計算時間がかかります.
そのため,凄いGPU(大抵NVidia)を積んだマシンを用意するか,凄いGPUや計算デバイスを積んだクラウドサーバに計算させるかという方法をとります.後者の代表的なサービスがGoogle Colaboratoryです.このサービスは,無料でGPUを使った機械学習を実行させることができるかなり優良サービス(無料なのに優良.ガハハ)なのですが,私個人としてはちょっと嫌だなと思っている点がいくつかあります.

・データファイルを保持しておくストレージが一時的にしかファイルを保持しない.Google Driveをストレージとして連携する方法があるが,通常の動作方法より一手間多くなる


・一旦計算を開始して,長時間放っておくと,ランタイムが終了している.下記のような対策もなきにしもあらずだが,一体何のためのクラウドサービスなんだという感想しか抱かない.(下記でも全てのセッション切れを対策できない)


・私がやりたいのは,焼肉屋とかでラップトップを開いて機械学習をすることであって,焼肉屋はインターネット接続できるかどうか定かではない.

・本記事の冒頭で「GAFA」と一纏めにして悪く言っている以上,Googleのサービスを利用するのは男のすることではない

どれもこれも,MacBook ProがNVidiaを搭載しなくなったのが悪いのです.(MacBook ProにNVidiaのGPUが搭載されていたからといって,それが本当に機械学習のデバイスとして動作するのか,定かではありません.でもCUDA自体は動くわけだから,何かやり方があるだろうと思っています.)
私はただ,焼肉を食べながら機械学習の勉強をしたいだけなのに,なんでいちいちインターネット接続してクラウドサービスなんて使わないといけないのでしょう?

要は,MacBook Proに積んでいるAMDのGPUで機械学習できれば良いわけです.というわけで調べた結果,PlaidMLというフレームワークが存在することを知りました.PlaidMLは,様々なCPUやGPUを対象にして開発されている機械学習フレームワークで,Kerasのバックエンドとしても使えます.これを導入すれば,ここ最近のMacBook ProのAMD Radeon Proで機械学習できるようなので,試してみました.

実験に使ったのは,本ブログの以前の記事


の,画像を100種類に分類するプログラムです.



この記事では,学習したモデルとパラメータを一旦ファイルとして保存し,別プログラムでそのファイルを読み込んで検証画像を分類しました.

今回は,実験用に1つのプログラムで学習し,学習後,どうプログラム内で検証画像を分類します.まず,単純に上記記事のプログラムを1つのプログラムにまとめたのが以下.実行時間を計測する処理も加えています.

import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import time

#時間計測開始
start = time.time()

#CIFAR100のラベル名
CIFAR100_LABELS_LIST = [
                        'apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle',
                        'bicycle', 'bottle', 'bowl', 'boy', 'bridge', 'bus', 'butterfly', 'camel',
                        'can', 'castle', 'caterpillar', 'cattle', 'chair', 'chimpanzee', 'clock',
                        'cloud', 'cockroach', 'couch', 'crab', 'crocodile', 'cup', 'dinosaur',
                        'dolphin', 'elephant', 'flatfish', 'forest', 'fox', 'girl', 'hamster',
                        'house', 'kangaroo', 'keyboard', 'lamp', 'lawn_mower', 'leopard', 'lion',
                        'lizard', 'lobster', 'man', 'maple_tree', 'motorcycle', 'mountain', 'mouse',
                        'mushroom', 'oak_tree', 'orange', 'orchid', 'otter', 'palm_tree', 'pear',
                        'pickup_truck', 'pine_tree', 'plain', 'plate', 'poppy', 'porcupine',
                        'possum', 'rabbit', 'raccoon', 'ray', 'road', 'rocket', 'rose',
                        'sea', 'seal', 'shark', 'shrew', 'skunk', 'skyscraper', 'snail', 'snake',
                        'spider', 'squirrel', 'streetcar', 'sunflower', 'sweet_pepper', 'table',
                        'tank', 'telephone', 'television', 'tiger', 'tractor', 'train', 'trout',
                        'tulip', 'turtle', 'wardrobe', 'whale', 'willow_tree', 'wolf', 'woman',
                        'worm'
                        ]
#CIFAR-100 datasetの読み込み
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar100.load_data(label_mode='fine')

train_labels_onehot=keras.utils.to_categorical(y_train,100)
test_labels_onehot=keras.utils.to_categorical(y_test,100)

#画像をfloat32(0.~1.)に変換
x_train=x_train.astype("float32")/255.0
x_test=x_test.astype("float32")/255.0

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)

#学習画像を少し見てみる
plt.figure(figsize=(5,5))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(x_train[i+25])
    plt.xlabel(CIFAR100_LABELS_LIST[int(y_train[i+25])])

#ネットワーク作成
model=keras.Sequential()

model.add(keras.layers.Conv2D(filters=32,kernel_size=(3,3),padding='same',activation='relu',input_shape=(32,32,3)))
model.add(keras.layers.Conv2D(filters=32,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(512,activation='relu'))
model.add(keras.layers.Dropout(0.5))
model.add(keras.layers.Dense(100,activation='softmax'))

model.compile(optimizer=tf.train.AdamOptimizer(),loss='categorical_crossentropy',metrics=["accuracy"])

#学習用データで学習してみる
model.fit(x_train[:,:,:,:],train_labels_onehot,epochs=200,batch_size=64)

#テスト画像を入力して識別
labels=model.predict(x_test[:,:,:,:])

#結果を少し見る
plt.figure(figsize=(5,5))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    #ランダムな番号の画像を見る
    index=np.random.randint(0,9999)
    true_index=np.argmax(test_labels_onehot[index])#正解
    predict_index=np.argmax(labels[index])#予測したインデックス
    plt.imshow(x_test[index])#画像表示
    plt.xlabel("{}({})".format(CIFAR100_LABELS_LIST[predict_index],CIFAR100_LABELS_LIST[true_index]),color=("green" if predict_index==true_index else "red"))#"予測したラベル(正解のラベル)"で表示.正解なら緑,間違っていれば赤で表示

#時間計測終了
process_time = time.time() - start
#実行時間を表示
print(process_time)

plt.show()
結果は後で書くとして,結構時間がかかります.

それでは,いよいよPlaidMLを導入します.初めに言っておくと,PlaidMLを導入して,本当にプログラムのバックエンドをPlaidMLにするためには,後述のように,ほんの少しプログラムの修正が必要です.導入後に上記ソースを実行してもそれまでのようにCPUで実行されます.

①PlaidMLをインストールします.私の場合,OSにPython2.xがインストールされていて,Python3.xを起動するコマンドが"python3"なので,"pip"も"pip3"になっています."python"でPython3.xが起動する環境では"pip"になります.


pip3 install plaidml-keras plaidbench

②下記コマンドを実行します.何故かこの手順を省略している記事をよく見かけますが,このコマンドを実行しないと次の手順に進めません.


export PLAIDML_NATIVE_PATH=/usr/local/lib/libplaidml.dylib

export RUNFILES_DIR=/usr/local/share/plaidml

③下記コマンドを実行して,PlaidMLの環境を設定します.


plaidml-setup

CUIで色々質問して来ます.まず,外部の計算デバイスを使うかどうか聞かれます.勿論yesです.

PlaidML Setup (0.6.4)

Thanks for using PlaidML!

Some Notes:
  * Bugs and other issues: https://github.com/plaidml/plaidml
  * Questions: https://stackoverflow.com/questions/tagged/plaidml
  * Say hello: https://groups.google.com/forum/#!forum/plaidml-dev
  * PlaidML is licensed under the Apache License 2.0


Default Config Devices:
   metal_amd_radeon_pro_560.0 : AMD Radeon Pro 560 (Metal)
   metal_intel(r)_hd_graphics_630.0 : Intel(R) HD Graphics 630 (Metal)

Experimental Config Devices:
   metal_intel(r)_hd_graphics_630.0 : Intel(R) HD Graphics 630 (Metal)
   opencl_cpu.0 : Intel CPU (OpenCL)
   opencl_amd_radeon_pro_560_compute_engine.0 : AMD AMD Radeon Pro 560 Compute Engine (OpenCL)
   opencl_intel_hd_graphics_630.0 : Intel Inc. Intel(R) HD Graphics 630 (OpenCL)
   llvm_cpu.0 : CPU (LLVM)
   metal_amd_radeon_pro_560.0 : AMD Radeon Pro 560 (Metal)

Using experimental devices can cause poor performance, crashes, and other nastiness.


Enable experimental device support? (y,n)[n]:

次に,実際に計算に使うデバイスを聞かれます.ここではAMD Radeon Pro 560.0 (Metal)を選択します.

Multiple devices detected (You can override by setting PLAIDML_DEVICE_IDS).
Please choose a default device:

   1 : metal_intel(r)_hd_graphics_630.0
   2 : opencl_cpu.0
   3 : opencl_amd_radeon_pro_560_compute_engine.0
   4 : opencl_intel_hd_graphics_630.0
   5 : llvm_cpu.0
   6 : metal_amd_radeon_pro_560.0


Default device? (1,2,3,4,5,6)[1]:

試しに行列の計算をしてみて,成功すると,/Users/user/.plaidmlに設定を保存するか聞かれます.yesを選択します.


Selected device:
    metal_amd_radeon_pro_560.0

Almost done. Multiplying some matrices...
Tile code:
  function (B[X,Z], C[Z,Y]) -> (A) { A[x,y : X,Y] = +(B[x,z] * C[z,y]); }
Whew. That worked.

Save settings to /Users/user/.plaidml? (y,n)[y]:y

Success!

"Success!"と出たら成功です. 

これで,PlaidMLの導入は終了です.

いよいよ先述のプログラムをPlaidML用に修正して,実行してみます.
ソースの最初に

import plaidml.keras
plaidml.keras.install_backend()

を追加します.その辺の部分以外は全く同じです.

import plaidml.keras
plaidml.keras.install_backend()
import numpy as np
import matplotlib.pyplot as plt
import time
import keras
from keras.optimizers import adam
#時間計測開始
start = time.time()

#CIFAR100のラベル名
CIFAR100_LABELS_LIST = [
                        'apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle',
                        'bicycle', 'bottle', 'bowl', 'boy', 'bridge', 'bus', 'butterfly', 'camel',
                        'can', 'castle', 'caterpillar', 'cattle', 'chair', 'chimpanzee', 'clock',
                        'cloud', 'cockroach', 'couch', 'crab', 'crocodile', 'cup', 'dinosaur',
                        'dolphin', 'elephant', 'flatfish', 'forest', 'fox', 'girl', 'hamster',
                        'house', 'kangaroo', 'keyboard', 'lamp', 'lawn_mower', 'leopard', 'lion',
                        'lizard', 'lobster', 'man', 'maple_tree', 'motorcycle', 'mountain', 'mouse',
                        'mushroom', 'oak_tree', 'orange', 'orchid', 'otter', 'palm_tree', 'pear',
                        'pickup_truck', 'pine_tree', 'plain', 'plate', 'poppy', 'porcupine',
                        'possum', 'rabbit', 'raccoon', 'ray', 'road', 'rocket', 'rose',
                        'sea', 'seal', 'shark', 'shrew', 'skunk', 'skyscraper', 'snail', 'snake',
                        'spider', 'squirrel', 'streetcar', 'sunflower', 'sweet_pepper', 'table',
                        'tank', 'telephone', 'television', 'tiger', 'tractor', 'train', 'trout',
                        'tulip', 'turtle', 'wardrobe', 'whale', 'willow_tree', 'wolf', 'woman',
                        'worm'
                        ]
#CIFAR-100 datasetの読み込み
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar100.load_data(label_mode='fine')

train_labels_onehot=keras.utils.to_categorical(y_train,100)
test_labels_onehot=keras.utils.to_categorical(y_test,100)

#画像をfloat32(0.~1.)に変換
x_train=x_train.astype("float32")/255.0
x_test=x_test.astype("float32")/255.0

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)

#学習画像を少し見てみる
plt.figure(figsize=(5,5))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(x_train[i+25])
    plt.xlabel(CIFAR100_LABELS_LIST[int(y_train[i+25])])

#ネットワーク作成
model=keras.Sequential()

model.add(keras.layers.Conv2D(filters=32,kernel_size=(3,3),padding='same',activation='relu',input_shape=(32,32,3)))
model.add(keras.layers.Conv2D(filters=32,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(512,activation='relu'))
model.add(keras.layers.Dropout(0.5))
model.add(keras.layers.Dense(100,activation='softmax'))

model.compile(optimizer=adam(lr=0.001,decay=1e-6),loss='categorical_crossentropy',metrics=["accuracy"])

#学習用データで学習してみる
model.fit(x_train[:,:,:,:],train_labels_onehot,epochs=200,batch_size=64)

#テスト画像を入力して識別
labels=model.predict(x_test[:,:,:,:])

#結果を少し見る
plt.figure(figsize=(5,5))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    #ランダムな番号の画像を見る
    index=np.random.randint(0,9999)
    true_index=np.argmax(test_labels_onehot[index])#正解
    predict_index=np.argmax(labels[index])#予測したインデックス
    plt.imshow(x_test[index])#画像表示
    plt.xlabel("{}({})".format(CIFAR100_LABELS_LIST[predict_index],CIFAR100_LABELS_LIST[true_index]),color=("green" if predict_index==true_index else "red"))#"予測したラベル(正解のラベル)"で表示.正解なら緑,間違っていれば赤で表示

#時間計測終了
process_time = time.time() - start
#実行時間を表示
print(process_time)

plt.show()

MacBook ProのCPUとRadeon Pro 560,そしてGoogle ColabratoryのGPUで実行時間を比較しました.

CPU: 30950.88秒(8.59時間)
Radeon Pro 560 (Metal): 23190.82秒(6.44時間)
Google Colaboratory: 2580.95秒(43分)

Radeon Proも少しは速くなっていますが,そんなに速くなっていないですね.(MetalではなくOpenCLの方も試してみましたが,寧ろCPUの方が速いくらいでした)Google Colaboratoryだけ桁が違いますねえ.やはりGoogle Colaboratoryは素晴らしいサービスです.焼肉屋に行ったらテザリングでインターネットに繋いだら良いじゃないですか.Google Colaboratoryを積極的に使っていきましょう.





2019年5月16日木曜日

AV女優9852人のデータから自分が出演できそうなAVのジャンルを予測する

ご存知と思いますが,一応音楽活動なんぞしているもんで,しているからにはやっぱり有名になりたいという気持ちが少なからずあります.

有名になる方法はいろいろあると思いますが.その中の一つとして,AVに素人として出演して,炎上するという方法があります.筆者は男性ですが,この方法を採るかどうかは別として,AVに出演するとしたらどのようなジャンルのAVに出演できる可能性が高いか,現在日本最大級のアダルトポータル FANZA(DMM.R18)で販売されている作品に出演しているAV女優のデータをスクレイピングし,機械学習によって予測してみます.



1, AV女優の情報をスクレイピング


この処理は,似たようなことをやっている方の記事があったので,それをそのまま使い,本プロジェクトに足りない部分を追加しました.

【pythonコードを読む】AV女優10000人webスクレイピング

上記の記事では,AV女優の名前,顔写真のみ取得していますが,本プロジェクトでは,

名前,顔写真URL,出演作品数,作品一覧ページのURL,どのジャンルに何回出演したか

を取得しています.
コードは以下


from selenium import webdriver
import pandas as pd
import math
import collections
 
data = {"name": [],
        "image":[],
        "出演数":[],
        "URL":[]
}
df = pd.DataFrame(data)
 
url = 'http://www.dmm.co.jp/digital/videoa/-/actress/='
driver = webdriver.Chrome()
driver2=webdriver.Chrome()
driver3=webdriver.Chrome()
next_key = '#main-ds > div.d-area.area-actbox > div:nth-child(3) > p'
 
boins=['a', 'i', 'u', 'e', 'o']
siins=['','k','s','t','n','h','m','y', 'r', 'w']
print("test")
def function(df):
    #siin + boinで始まる女優の一覧ページ
    driver.get(url + '/keyword=' + siin + boin)
    ninzu = driver.find_element_by_css_selector(next_key).text
    #ページ数を取得
    num = -(-int(list(map(str,ninzu.split("\u3000")))[0][0:-2])//100)
    print(num)
    for i in range(num):
 #boin +siin で始まる女優一覧の num ページ目
        driver.get(url + '/keyword=' + siin + boin + '/page=' + str(i+1))
        #出演作品数
        actcount=driver.find_element_by_css_selector("#main-ds > div.d-area.area-actbox > div.d-sect.act-box").find_elements_by_tag_name("span")
        #各女優欄
        elements = driver.find_element_by_css_selector("#main-ds > div.d-area.area-actbox > div.d-sect.act-box").find_elements_by_tag_name("img")
        n=0
        for element in elements:
            genrelist=[]
     #女優名
            src = element.get_attribute("src")
            #画像URL
            alt = element.get_attribute("alt")
            #出演作品一覧URL
            actressurl=driver.find_element_by_css_selector("#main-ds > div.d-area.area-actbox > div.d-sect.act-box > ul > li:nth-child("+str(n+1)+") a").get_attribute("href")
            j=0
     #作品一覧のページ数
            videopagenum=math.floor(float(actcount[2*n+1].text.split(":")[1])/120)+1
            for j in range(videopagenum):
                try:
      #作品一覧URLの(J+1)ページ目を開く
                    driver2.get(actressurl+'page='+str(j+1)+'/')
                    #各作品欄をリストする
                    videos=driver2.find_element_by_css_selector("#main-src > div.d-area > div.d-sect > div.d-item").find_elements_by_tag_name("img")
                    k=1
                    for video in videos:
   #各作品のURL
                        videourl=driver2.find_element_by_css_selector("#list > li:nth-child("+str(k)+") > div > p.tmb > a").get_attribute("href")
                        k+=1
                        driver3.get(videourl)
   #ジャンル欄をリスト
                        genres=driver3.find_element_by_css_selector("#mu > div > table > tbody > tr > td:nth-child(1) > table > tbody > tr:nth-child(11) > td:nth-child(2)").find_elements_by_tag_name("a")
                        #ジャンルをリストに追加していく
                        for genre in genres:
                            genrelist.append(genre.text)
                except:
                    pass
            se = pd.Series([alt,src,actcount[2*n+1].text.split(":")[1],actressurl,collections.Counter(genrelist)],["name", "image","出演数","URL","ジャンル"])
            print(alt+": "+str(collections.Counter(genrelist)))
            df = df.append(se, ignore_index=True)
            n+=1
    return df
 
 
for siin in siins:
    if siin == 'y':
        for boin in boins[::2]:
            df=function(df)
    elif siin == 'w':
        for boin in boins[0]:
            df=function(df)
    else:
        for boin in boins:
            df=function(df)
print("output")
df.to_csv("final.csv")
driver.quit()
driver2.quit()

全女優の全作品のページ全てに順番にアクセスしていくので,時間が恐ろしくかかります.筆者は1ヶ月弱かかりました.この間にCIFAR-100でうまく学習できるようにKerasのモデルを準備したりしておきましょう.

保存したデータはこのような形式になります.ジャンルの列は,collections.Counter(genrelist)の結果をそのまま文字列にして記録しているので,初めにConterとかが付いています.利用するときは,文字列処理を実装して,必要な情報を抜き出す必要があります.(本記事でも後に行なっています)


2, ラベリングしたいジャンルを限定する


FANZAで作品タグとして存在しているジャンルは,画像のように大量に存在します.


このジャンル全てを識別するモデルを作成するのはかなり困難だと考えられます.また,意外と多いタグ

期間限定配信
独占配信
スマホ限定配信

こんなタグは,「AVのジャンル」としてはほとんど意味がありません.

そこで,上の画像のジャンルの中から「シチュエーション」にカテゴライズされているジャンルに限定して学習データを作成します.とりあえず,「シチュエーション」の中のジャンルを列挙したcsvファイルを作成します.

シチュエーションのところにあるジャンルを選択し,


viに貼り付けます.
一番上の行に「ジャンル」と書いた行を追加して,"genre.csv"として保存します.




3, 学習用データセット作成


スクレイピングしたデータfinal.csvをもとに,ニューラルネットワークに学習させるデータを作成します.

基本的には,final.csvに記述されたAV女優の情報を1人1人読んでいき,更に,ジャンル欄の中で列挙したジャンルも一つ一つ読んでいきます.この欄は,ジャンルとその数を書いた文字列が一つの文字列データとしてまとまっているので,

Counter({'

までは切り捨て,コンマを区切りにしてジャンルを区切っていきます.

その一つ一つを

':

で区切り,その最初の要素をリストにすれば,その女優が出演していたジャンルがリストとして取得できます.

ここで,このリストは出演回数が多い順に並んでいるので,女優ごとのジャンルのリストを順に見ていき,最初にgenre.csvに列挙したジャンルのどれかと一致した時のジャンルを,その女優に最も適したジャンルとします.一致したジャンルを見つけられたら,顔写真をダウンロードします.

これを全女優に対して行い,女優1人に対して,1つのジャンル,1枚の顔写真というデータを作っていきます.

一致するジャンルが一つもなければ,その女優は残念ですが,データセットから外れていただきます.

ここで,大問題が発生しました.FANZAから取得できる顔写真は,ほとんどが100x100というサイズなのですが,たまに違うサイズの画像が混じっていたのです.そこで,ダウンロードした画像について,解像度をチェックし,100x100でなければ,データセットから外すようにしました.

ここまでのコードがこちら.


from PIL import Image
import pandas as pd
import requests
import shutil
import numpy as np

data={"ジャンル":[],"label":[]}
df=pd.DataFrame(data)

#ジャンル一覧csvファイルを読み込む
genrelist=pd.read_csv('genre.csv')
genretemp=genrelist['ジャンル']

actlist=pd.read_csv('final.csv')
name=actlist['Name']
url=actlist['image']
genre=actlist['ジャンル']
count=0
for n in range(len(name)):
 findgenre=0
 #最初のジャンル名のところ(10文字目)まで切り取る
 genre[n]=genre[n][10:len(genre[n])-2]
 pgenrelist=genre[n].split(', \'')
 for pgenre in pgenrelist:
  #': のところまでをジャンル名とする(一番最初に記述されているジャンル名)
  pgenre=pgenre.split('\':')
  
  #ジャンル一覧に一致したら,画像をダウンロード.画像が100x100であれば画像をdataフォルダに保存,そのジャンルリストに追加.
  for m in range(len(genretemp)):
   if genretemp[m]==pgenre[0]:  
    print(name[n])
    print(url[n])
    print(pgenre[0])
    r=requests.get(url[n],stream=True)
    if r.status_code==200:
                                        with open('temp.jpg','wb') as f:
                                                f.write(r.content)
    img=Image.open('temp.jpg')
    width,height=img.size
    if width==100 and height==100:
     se=pd.Series([int(m),pgenre[0]],["label","ジャンル"])
     df=df.append(se,ignore_index=True)
     shutil.copyfile('temp.jpg','data/'+str(count)+'.jpg')
     count=count+1
     findgenre=1
     break
  if findgenre==1:
   #一回ジャンル一覧と一致したら,次の女優を処理する
   break
df.to_csv("data.csv")

女優ごとのジャンル一覧,ダウンロードした各女優の顔写真を,Kerasに入力するndarrayに変換しなければなりません.この処理も非常に時間がかかりました.時間がかかったので,この部分だけ独立させて,作成したndarrayをファイルに保存することにしました.


from PIL import Image
import pandas as pd
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *

#ジャンル一覧csvファイルを読み込む
genrelist=pd.read_csv('genre.csv')
genrename=genrelist['ジャンル']

#ジャンルをリスト化したcsvファイルを読み込む
datagenrelist=pd.read_csv('data.csv')
train_labels=datagenrelist['label']
#機械学習で使うOne=Hotベクトルに変換.これが教師データのラベルデータとなる.
train_labels_onehot=keras.utils.to_categorical(train_labels,len(genrename))

#一旦空のndarrayを定義
train_images=np.empty((0,100,100,3))
#dataフォルダの画像をRGBデータにしてtrain_imagesに追加
for n in range(len(train_labels)):
 img=Image.open('data/'+str(n)+'.jpg')
 im=np.array(img)
 train_images=np.append(train_images,[im],axis=0)

#画像を0.0.-1.0の表現に変換
train_images=train_images.astype("float32")/255.0
#ndarrayをファイルとして保存
np.save('train_images.npy',train_images)
np.save('train_labels_onehot.npy',train_labels_onehot)

#少し表示してみる
plt.figure(figsize=(10,10))
for i in range(25):
 plt.subplot(5,5,i+1)
 plt.xticks([])
 plt.yticks([])
 imgindex=randint(len(train_labels))
 plt.imshow(train_images[imgindex],cmap=plt.cm.binary)

plt.show()

一部のAV女優の顔写真を少し表示して見ました.ここで正しいっぽく表示されているということは,Kerasに問題なく入力できそうです.本当は,顔の部分だけ切り出したりしたほうがいいと思いますが,今回は理由がありそれはやりません.
その理由とは,面倒くさいからです.



4, 学習

CIFAR-100を畳み込みニューラルネットワークで認識の時とほとんど同じです.


from PIL import Image
import pandas as pd
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *

genrelist=pd.read_csv('genre.csv')
genrename=genrelist['ジャンル']

datagenrelist=pd.read_csv('data.csv')
train_labels=datagenrelist['label']

train_images=np.load('train_images.npy')
train_labels_onehot=np.load('train_labels_onehot.npy')

#ネットワーク作成
model=keras.Sequential()

model.add(keras.layers.Conv2D(filters=100,kernel_size=(3,3),padding='same',activation='relu',input_shape=(100,100,3)))
model.add(keras.layers.Conv2D(filters=100,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.Conv2D(filters=64,kernel_size=(3,3),padding='same',activation='relu'))
model.add(keras.layers.MaxPool2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))

model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(512,activation='relu'))
model.add(keras.layers.Dropout(0.5))
model.add(keras.layers.Dense(len(genrename),activation='softmax'))

model.compile(optimizer=tf.train.AdamOptimizer(),loss='categorical_crossentropy',metrics=["accuracy"])

#学習用データで学習してみる
model.fit(train_images[:,:,:,:],train_labels_onehot,epochs=30,batch_size=64)

#ネットワークをファイルに保存
model_json_str=model.to_json()
open('model.json','w').write(model_json_str)
#学習したパラメータをファイルに保存
model.save_weights('weights.h5')

学習結果はこんな感じです.


精度95.4%です.CIFAR-100の時より精度上がりましたね.
本来は,データセットの中で,チェック用のデータを学習に使ったデータとは別に用意して,どれだけの精度が出たかを調べるのですが,今回はデータセット全てを学習に使ったので,チェック用のデータがありません.なので,学習に使ったデータ自身を全てネットワークに入力してみましたが,予測結果は全て正解でした.まあ当たり前っちゃ当たり前ですが,少なくとも,全然出来ていない訳ではなさそうだということがわかりました.この部分は無理やり納得して,いよいよ最後の段階です.

5, 自分が出演できそうなAVのジャンルを予測


いよいよ予測します.OpenCVを使って入力した画像の中から,顔の部分を認識し,その部分を100x100にリサイズして入力します.

import sys
import pandas as pd
import tensorflow as tf
from tensorflow import keras
import numpy as np
import cv2
from PIL import Image
import matplotlib.pyplot as plt

genrelist=pd.read_csv('genre.csv')
genrename=genrelist['ジャンル']

datagenrelist=pd.read_csv('data.csv')
train_labels=datagenrelist['label']

train_images=np.load('train_images.npy')
train_labels_onehot=np.load('train_labels_onehot.npy')

#ネットワーク読み込み
model=keras.models.model_from_json(open('model.json').read())
#学習したパラメータ読み込み
model.load_weights('weights.h5')
model.summary()
model.compile(optimizer=tf.train.AdamOptimizer(),loss='categorical_crossentropy',metrics=["accuracy"])

#予測したい画像を読み込み,顔の部分を抜き出す
image=cv2.imread(sys.argv[1])
image=cv2.cvtColor(image,cv2.COLOR_RGB2BGR)
plt.figure()
plt.imshow(image)
cascade=cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')
face_list=cascade.detectMultiScale(image,scaleFactor=1.1,minNeighbors=1,minSize=(100,100))
if len(face_list)>0:
    for rect in face_list:
        image=image[rect[1]:rect[1]+rect[3],rect[0]:rect[0]+rect[2]]

#画像を100x100にリサイズ
image=cv2.resize(image,(100,100))
plt.figure()
plt.imshow(image)
#画像を0.0 - 1.0の間に正規化
image=image.astype("float32")/255.0
#予測
label=model.predict(np.array([image[:,:,:]]))
predict=np.argmax(label)
print("あなたが出演できるAVは "+genrename[predict]+" 系のAVです.")
plt.show()

こうしてできたプログラムで,私が出演できそうなAVのジャンルを予測した結果がこちら.


どうやら,私の出演できそうなAVのジャンルは,野外・露出系だそうです.

野外・露出系のAVを製作しようとされているAV関係者の皆さん,当方男性ですが,ブッキングお待ちしております.

6, 参考


日本最大級のアダルトポータル FANZA

【pythonコードを読む】AV女優10000人webスクレイピング

CIFAR-100を畳み込みニューラルネットワークで認識


7, 番外編


ラベルデータを「シチュエーション」欄のタグを使用しましたが,「プレイ」欄のタグを使用することで,自分に最適なプレイを予測することもできます.私に最適なプレイはフェラだそうです.