にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

前処理にディープラーニングを使う

前処理にディープラーニングを使う

目的

  • スクレイパーなどで集めた画像には、ターゲットとする画像以外必要ないケースが度々ある
  • データセットづくりと呼ばれる画像からノイズ画像を取り除くスクリーニングの作業の簡略化の必要性
  • 画像のスクリーニングを機械学習でやってしまおうという試みです

前処理そのものにディープラーニングを投入する

  • 画像処理において、学習したい画像かどうかをスクリーニングすることは膨大なコストがかかるので、この作業自体を自動化したい
  • 今回はスクレイパーでいい加減にあつめたグラビア女優の画像7万枚超えを、手動でスクリーニングするのは極めて困難なので、VGG16を転移学習させてフィルタを作っていきます
  • 一枚10円で500枚のペア(positiveとnegative)のデータセットを知り合いのニートに作ってもらう
  • ニートの作成したデータセットをもとに、転移学習させてフィルタを構築

システム構成図

図1. システム構成図

人間との比較

  • 実は人間よりどれくらい早くできるかとうことも検証したくて、自分の目で見て判断して分類していくのと、機械ではどの程度の差があるか試した
  • 人間は6時間で5000枚ぐらいのチェックが限界であった(精神的に大いに疲弊する)
  • 対して75000枚をGTX 1080 2基で 50分位である。圧倒的に機械学習の方がよい

ネットワークの出力の特性を知っておく

  • 活性化関数や最小化する目的関数の設計は実にバラエティに富んでおり、組み合わせは考え始めると無数にあるように見える
  • 内部がリニアであり、そのロジットを取ったロジスティック回帰が確率表現として優秀なのでよく使う
  • softmax, categorical crossentropyとかは出力値を寄せきってしまうので、あまり確率表現に向いていないように見える
  • 今回はロジットを使う

過学習の防止

  • どの程度、データセットにフィッティングさせていくかかがかなり重要なので、訓練データとバリデーションデータに分けて未知のデータセットに対しても汎化性能を確認する
  • 今回はepochごとにmodelを保存してベストなモデルを探索することで選んでいった -> 最適は85epochぐらいがよかった

しきい値の決定

  • 0~1に変化する値である
  • 当然0.5がしきい値であるが0.5から始めていき、しきい値を調整する

図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)