前処理にディープラーニングを使う
前処理にディープラーニングを使う
目的
- スクレイパーなどで集めた画像には、ターゲットとする画像以外必要ないケースが度々ある
- データセットづくりと呼ばれる画像からノイズ画像を取り除くスクリーニングの作業の簡略化の必要性
- 画像のスクリーニングを機械学習でやってしまおうという試みです
前処理そのものにディープラーニングを投入する
- 画像処理において、学習したい画像かどうかをスクリーニングすることは膨大なコストがかかるので、この作業自体を自動化したい
- 今回はスクレイパーでいい加減にあつめたグラビア女優の画像7万枚超えを、手動でスクリーニングするのは極めて困難なので、VGG16を転移学習させてフィルタを作っていきます
- 一枚10円で500枚のペア(positiveとnegative)のデータセットを知り合いのニートに作ってもらう
- ニートの作成したデータセットをもとに、転移学習させてフィルタを構築
システム構成図
図1. システム構成図
人間との比較
- 実は人間よりどれくらい早くできるかとうことも検証したくて、自分の目で見て判断して分類していくのと、機械ではどの程度の差があるか試した
- 人間は6時間で5000枚ぐらいのチェックが限界であった(精神的に大いに疲弊する)
- 対して75000枚をGTX 1080 2基で 50分位である。圧倒的に機械学習の方がよい
ネットワークの出力の特性を知っておく
- 活性化関数や最小化する目的関数の設計は実にバラエティに富んでおり、組み合わせは考え始めると無数にあるように見える
- 内部がリニアであり、そのロジットを取ったロジスティック回帰が確率表現として優秀なのでよく使う
- softmax, categorical crossentropyとかは出力値を寄せきってしまうので、あまり確率表現に向いていないように見える
- 今回はロジットを使う
過学習の防止
- どの程度、データセットにフィッティングさせていくかかがかなり重要なので、訓練データとバリデーションデータに分けて未知のデータセットに対しても汎化性能を確認する
- 今回はepochごとにmodelを保存してベストなモデルを探索することで選んでいった -> 最適は85epochぐらいがよかった
しきい値の決定
図2. しきい値 0.5を上回った画像
図3. しきい値 0.5を下回った画像
-> いろいろ調整たが、多めにスクリーニングするとして、しきい値を0.65とした。
感想
全体の流れ
コードはgithubにおいておきます。非商用・研究目的では好きに使ってください
bitbucket.org
bitbucketよくわかってないので、何か不具合があればtwitterで教えていただけると幸いです。
$ git clone https://${YOUR_ID?}@bitbucket.org/nardtree/maeshori-toolkit-for-deeplearning.git
step1. 入力サイズに合わせて変形する
ニートから帰ってきたデータは500のpositive,negativeのフォルダに別れたデータセットであった
フォルダ名を答えとして、224×224のサイズに変形する。この時単純な変形にしてしまうと縦横比が崩壊してしまうので維持する細工を入れる。
実行
$ python3 image-resizer.py --gravia_noisy
コード
def gravia_noisy(): target_size = (224,224) dir_path = "./gravia-noisy-dataset/gravia/*/*" max_size = len(glob.glob(dir_path)) for i, name in enumerate(glob.glob(dir_path)): if i%10 == 0: print(i, max_size, name) save_name = name.split("/")[-1] type_name = name.split("/")[-2] if Path("gravia-noisy-dataset/{type_name}/{save_name}.minify" \ .format(type_name=type_name, save_name=save_name)).is_file(): continue try: img = Image.open(name) except OSError as e: continue w, h = img.size if w > h : blank = Image.new('RGB', (w, w)) if w <= h : blank = Image.new('RGB', (h, h)) try: blank.paste(img, (0, 0) ) except OSError as e: continue blank = blank.resize( target_size ) os.system("mkdir -p gravia-noisy-dataset/{type_name}".format(type_name=type_name)) blank.save("gravia-noisy-dataset/{type_name}/{save_name}.mini.jpeg" \ .format(type_name=type_name, save_name=save_name), "jpeg" )
step2. 学習する
最終的にはResNetを使うが、速度がほしい前処理のタスクのためVGG16で学習を行う
softmaxでなくて、sigmoid + binary_crossentropyです
実行
$ python3 deep_gravia_maeshori.py --train
コード
from keras.applications.vgg16 import VGG16 def build_model(): input_tensor = Input(shape=(224, 224, 3)) model = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor) dense = Flatten()( \ Dense(2048, activation='relu')( \ BN()( \ model.layers[-1].output ) ) ) result = Activation('sigmoid')( \ Dense(1, activation="linear")(\ dense) ) model = Model(input=model.input, output=result) for layer in model.layers[:11]: if 'BatchNormalization' in str(layer): ... else: layer.trainable = False model.compile(loss='binary_crossentropy', optimizer='adam') return model
step3. 全体のデータセットに適応する
適切にフォルダに画像を配置して行ってください
実行
$ python3 deep_gravia_maeshori.py --classify
コード
def classify(): os.system("mkdir ok") os.system("mkdir ng") model = build_model() model = load_model(sorted(glob.glob('models/*.model'))[-1]) files = glob.glob("bwh_resize/*") random.shuffle(files) for gi, name in enumerate(files): try: img = Image.open('{name}'.format(name=name)) except FileNotFoundError as e: continue img = [np.array(img.convert('RGB'))] if not os.path.exists(name): continue result = model.predict(np.array(img) ) result = result.tolist()[0] result = { i:w for i,w in enumerate(result)} for i,w in sorted(result.items(), key=lambda x:x[1]*-1): if w > 0.65: os.system("mv {name} ok/".format(name=name)) else: os.system("mv {name} ng/".format(name=name)) print(gi, name, w, file=sys.stderr)