【GPUSOROBAN】NVIDIA系のインスタンスでPetFinder(Kaggleの題材)を実行してみた

Contents 

はじめに

今回GPUSOROBANのNvidia系のインスタンスでJupyterLabを用いてKaggleの題材を実行してみました。今回選定した題材は下記の通りになります。この題材はとても尊い背景を有し、AIを駆使して野良ペットの写真の人気度合いをより科学的に判定するコンペになります。このコンペを通して、より早く野良ペットを新たな飼い主につながることを主旨としています。また、 画像認識大革命であるVision Transformer(通称:ViT)を用いた一例である。ViTは、画像の分類タスクに用いられています。

原論文: “An Image is Worth 16×16 Words: Transformers for Image Recognition at Scale”,(2021)

https://openreview.net/forum?id=YicbFdNTTy
cuteness_meter

[TF] PetFinder: ViT+Cls [TPU][Train]

[TF] PetFinder: ViT+Cls [TPU][Train]

https://www.kaggle.com/awsaf49/tf-petfinder-vit-cls-tpu-train

データのダウンロード

!kaggle competitions download -c petfinder-pawpularity-score

以下はcuDNNを駆動させるのに必要な環境変数:trueになっていることを確認する。必要に応じて、Jupyterサーバの再起動を行う。

!echo $TF_FORCE_GPU_ALLOW_GROWTH
true
# ダウンロードしたデータの解凍(必要な時以外は動作させないこと!)
# import zipfile
# z= zipfile.ZipFile(‘./input/petfinder/petfinder-pawpularity-score.zip’)
# z.extractall(path=’./input/petfinder’)

ライブラリーのインストール

Install Libraries

!pip install -q efficientnet >> /dev/null
!pip install -q imagesize
!pip install -qU wandb
!pip install -q vit-keras
!pip install -q tensorflow_addons
!pip install -q opencv-python
!pip install -q kaggledatasets
!pip install -q ipython-secrets # ipython-secretsを使ったsecret管理ができなさそうなので、多分不要…
!pip install -q pandas_profiling
!pip install -q sklearn
!pip install -q h5py==2.10.0

ライブラリーのインポート

Import Libraries

import pandas as pd, numpy as np, random,os, shutil
import tensorflow as tf, re, math
import tensorflow.keras.backend as K
import efficientnet.tfkeras as efn
import matplotlib.pyplot as plt
import tensorflow_addons as tfa
import imagesize
import wandb
import yaml
import sklearn

from vit_keras import vit
from IPython import display as ipd
from glob import glob
from tqdm.notebook import tqdm

from sklearn.model_selection import KFold, StratifiedKFold, GroupKFold
from sklearn.metrics import roc_auc_score

import h5py

import cv2

バージョンチェック

Version Check

print(‘np:’, np.version)
print(‘pd:’, pd.version)
print(‘sklearn:’, sklearn.version)
print(‘tf:’,tf.version)
print(‘tfa:’, tfa.version)
print(‘w&b:’, wandb.version)
print(‘h5py:’, h5py.version)
print(‘open cv:’, cv2.version)
np: 1.19.5
pd: 1.1.5
sklearn: 0.24.2
tf: 2.4.1
tfa: 0.14.0
w&b: 0.12.9
h5py: 2.10.0
open cv: 4.5.4

GPUを使えるかどうかの確認

import tensorflow as tf
tf.config.list_physical_devices(“GPU”)
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]Copy

Wandb

Weights & Biases(W&B)はMLOpsPFの経験をトラッキングする。
これを使うと、経験のトラッキング、データセットのバージョニング、モデルのマネージメントによってより早く良いモデルが構築できる。
W&Bの特徴

  • 追跡、比較、ML経験の可視化
  • 一元化されたダッシュボードにストリーミングされたライブメトリック、ターミナルログ、およびシステム統計を取得する
  • どのようにモデルが動くのかを説明する、モデルのバージョンによる改善をグラフで見ることができる、バグを議論することができる、マイルストーンに対する進捗度合いを確認できる
#MEMO: オリジナルはKaggle Secretを使ってAPI_KEYを取得している部分、
#    この環境ではsecretを扱える方法がなかったので、一旦スキップ
#    ipython_secretsで入力する方法を試してみたけど、ダメっぽかった…

# import wandb
# from ipython_secrets import get_secret

## wandbAPIを使えるようにログインする

#try:

#   WANDB_API_KEY = get_secret(‘WANDB_API_KEY’, prompt=”Enter the API key”)
#   wandb.login(key=WANDB_API_KEY)
#   anonymous = None
#   print(‘OK’)
#except:
#   anonymous = ‘must’
#   print(‘To use your W&B account,\nGo to Add-ons -> Secrets and provide your W&B access token. Use the Label name as WANDB. \nGet your W&B access token from here: https://wandb.ai/authorize’)

Configuration

このノートブックで利用する定数を定義

class CFG:
  wandb = False
  competition = ‘petfinder’
  _wandb_kernel = ‘awsaf49’
  debug = False
  exp_name = ‘vit_b16+cls-aug1’

  # USE verbose=0 for silent,
  # vebose=1 for interactive,
  # verbose=2 for commit
  verbose = 1 if debug else 0
  display_plot = True

  device = ‘GPU’
  model_name = ‘vit_b16’

  # use different seed for different stratified KFOLD
  seed = 42

  # nomber of folds. use 2,5,10
  folds = 5
  folds = 2

   selected_folds = [0, 1, 2, 3, 4]

  img_size = [256, 256]

  # batch size and epochs
  batch_size = 10

  batch_size = 2
  epochs = 2
  epochs = 1

   # loss
  loss = ‘BCE’
  optimizer = ‘Adam’

  # これ何? ViT関係のものっぽい?
  # CFG.augmentATION
  augment = True
  transform = True

  # TRANSFORMATION
  # この辺りの変数は定義するだけ??
  fill_mode = ‘reflect’
  rot = 10.0
  shr = 5.0
  hzoom = 30.0
  wzoom = 30.0
  hshift = 30.0
  wshift = 30.0

  # FLIP
  hflip = True
  vflip = True

  # CLIP [0, 1]
  clip = False

  # LEARNING RATE SCHEDULER
  scheduler = ‘exp’ # Cosine

  # Dropout
  drop_prob = 0.75
  drop_cnt = 10
  drop_size = 0.05

  # brightness, contrast
  # ここで写真を編集するパラメータを指定している?
  sat = [0.7, 1.3] # saturation(鮮やかさ、彩度)
  cont = [0.8, 1.2] # contrast(明暗差)
  bri = 0.15 # brightness(光の量を調整)
  hue = 0.05 # hue(色相)

  # TEST TIME CFG.augmentation STEPS
  tta = 1

  tab_cols = [‘Subject Focus’, ‘Eyes’, ‘Face’, ‘Near’, ‘Action’, ‘Accessory’,
        ’Group’, ‘Collage’, ‘Human’, ‘Occlusion’, ‘Info’, ‘Blur’]
  target_col = [‘Pawpularity’]

  data_dir = ‘./petfinder/input/petfinder-pawpularity-score’
  output_dir = ‘./petfinder/output’ Copy

Set Seed for Reproducibility (再現性のためのseedを設定)

def seeding(SEED):
 np.random.seed(SEED)
 random.seed(SEED)
 os.environ[‘PYTHONHASHSEED’] = str(SEED)
 tf.random.set_seed(SEED)
 print(‘seeding done.’)

seeding(CFG.seed)
seeding done.

TPU Configs GPU Configs

オリジナルのノートブックではTPUの設定をしていましたが、今回はGPUの設定をここではします

if CFG.device == ‘GPU’:
 print(‘Using default strategy for CPU and single GPU’)
 strategy = tf.distribute.get_strategy()
 print(‘Num GPUs Available: ‘, len(tf.config.experimental.list_physical_devices(‘GPU’)))

AUTO = tf.data.experimental.AUTOTUNE
REPLICAS = strategy.num_replicas_in_sync
print(f’REPLICAS: {REPLICAS}’)
Using default strategy for CPU and single GPU
Num GPUs Available:  1
REPLICAS: 1

GCS Path

TPUではGCSのパスが必要らしい?
オリジナルではここでGCS_PATHを設定しているけど、GCS接続ができるかどうかわからないので、一旦はスキップ
以降のコードはGCSパスを使わないものに書き換えてやってみます

#BASE_PATH = ‘/kaggle/input/petfinder-pawpularity-score’
#GCS_PATH = KaggleDatasets().get_gcs_path(‘petfinder-pawpularity-score’)
#print(‘GCS_PATH : ‘, GCS_PATH)

Meta Data

def get_imgsize(row):
 width, height = imagesize.get(row[‘image_path’])
 row[‘width’] = width
 row[‘height’] = height
 return row
# Train Data
# image_path, width, height のカラムを追加する
df = pd.read_csv(CFG.data_dir + ‘/train.csv’)
# オリジナル
# df[‘image_path’] = GCS_PATH + ‘/train/’ + df.Id + ‘.jpg’
# 今回は、ローカルの写真ファイルのパスに書き換えます
df[‘image_path’] = CFG.data_dir + ‘/train/’ + df.Id + ‘.jpg’
tqdm.pandas(desc=’train’) # プログレスバーの設定
df = df.progress_apply(get_imgsize, axis=1)
display(df.head(2))

# Test Data
test_df = pd.read_csv(CFG.data_dir + ‘/test.csv’)
# test_df[‘image_path’] = GCS_PATH + ‘/test/’ + test_df.Id + ‘.jpg’
test_df[‘image_path’] = CFG.data_dir + ‘/test/’ + test_df.Id + ‘.jpg’
tqdm.pandas(desc=’test’)
test_df = test_df.progress_apply(get_imgsize, axis=1)
display(test_df.head(2))

Train-Test Ditribution

print(‘train_files:’, df.shape[0])
print(‘test_files:’, test_df.shape[0])
train_files: 9912
test_files: 8

Light EDA(探索的データ解析)

If you’re too lazy to do EDA by yourself. Then definitely this library is for you. You can use Pandas-Profiling to do bunch of EDA with vey few lines of code. EDAの作業を楽するために、Pandas-Profilingを利用する。
Pandas-Profiling はEDAを自動でたくさんやってくれる。

from pandas_profiling import ProfileReport
train_profile = ProfileReport(df, title=’Train Data’)
test_profile = ProfileReport(test_df, title=’Test Data’)

Show Train data

TerminatedWorkerError: A worker process managed by the executor was unexpectedly terminated. This could be caused by a segmentation fault while calling the function or by an excessive memory usage causing the Operating System to kill the worker. The exit codes of the workers are {EXIT(1), EXIT(1), EXIT(1), EXIT(1), EXIT(1), EXIT(1), EXIT(1), EXIT(1)}
▲このようなエラーが出て、サーバー側でプロセスkillされてそう、、、 データを見るためのツールなので、学習には特に影響はないのでここはスキップ
# display(train_profile)

Data Split

データをPawpularityを元に分割する
um_bins = int(np.floor(1 + np.log2(len(df))))
df[‘bins’] = pd.cut(df[CFG.target_col].values.reshape(-1), bins=num_bins, labels=False)

skf = StratifiedKFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)
for fold, (train_idx, val_idx) in enumerate(skf.split(df, df[‘bins’])):
 df.loc[val_idx, ‘fold’] = fold
display(df.groupby([‘fold’, ‘bins’]).size())
fold  bins
0.0   0        165
      1        209
      2        551
      3       1014
      4        941
      5        650
      6        420
      7        267
      8        203
      9        137
      10        99
      11        70
      12        51
      13       179
1.0   0        165
      1        209
      2        550
      3       1015
      4        942
      5        649
      6        419
      7        266
      8        203
      9        137
      10        99
      11        70
      12        52
      13       180
dtype: int64Copy

Data Augmentation (データの増加)

単純なデータ拡張を使いますが、そのいくつかはモデルを傷つける場合があります
  • RandomFlip(Left0Right)
  • No Potation
  • RandomBrightness
  • RndomContrast
  • Shear
  • Zoom
  • Coarsee Dropout/Cutout
def get_mat(shear, height_zoom, width_zoom, height_shift, width_shift):
 # returns 3×3 transformmatrix which transforms indicies
 
 # CONVERT DEGREES TO RADIANS
 #rotation = math.pi * rotation / 180.
 shear = math.pi * shear / 180.

 def get_3x3_mat(lst):
  return tf.reshape(tf.concat([lst],axis=0), [3,3])

 # ROTATION MATRIX
#  c1 = tf.math.cos(rotation)
#  s1 = tf.math.sin(rotation)
 one = tf.constant([1],dtype=’float32′)
 zero = tf.constant([0],dtype=’float32′)

# rotation_matrix = get_3x3_mat([c1, s1, zero,
#          -s1, c1, zero,
#          ero, zero, one])
 # SHEAR MATRIX
 c2 = tf.math.cos(shear)
 s2 = tf.math.sin(shear)
 shear_matrix = get_3x3_mat([one, s2, zero,
         zero,c2, zero,
         zero, zero, one])
 # ZOOM MATRIX
zoom_matrix = get_3x3_mat([one/height_zoom, zero, zero,
         zero, one/width_zoom, zero,
         zero, zero, one])
 # SHIFT MATRIX
 shift_matrix = get_3x3_mat([one, zero, height_shift,
         zero, one, width_shift,
         zero, zero, one])

 return K.dot(shear_matrix,K.dot(zoom_matrix, shift_matrix)) #K.dot(K.dot(rotation_matrix, shear_matrix), K.dot(zoom_matrix, shift_matrix))

def transform(image, DIM=CFG.img_size):#[rot,shr,h_zoom,w_zoom,h_shift,w_shift]):
 if DIM[0]!=DIM[1]:
  pad = (DIM[0]-DIM[1])//2
  image = tf.pad(image, [[0, 0], [pad, pad+1],[0, 0]])

 NEW_DIM = DIM[0]

 rot = CFG.rot * tf.random.normal([1], dtype=’float32′)
 shr = CFG.shr * tf.random.normal([1], dtype=’float32′)
 h_zoom = 1.0 + tf.random.normal([1], dtype=’float32′) / CFG.hzoom
 w_zoom = 1.0 + tf.random.normal([1], dtype=’float32′) / CFG.wzoom
 h_shift = CFG.hshift * tf.random.normal([1], dtype=’float32′)
 w_shift = CFG.wshift * tf.random.normal([1], dtype=’float32′)
 
 transformation_matrix=tf.linalg.inv(get_mat(shr,h_zoom,w_zoom,h_shift,w_shift))

 flat_tensor=tfa.image.transform_ops.matrices_to_flat_transforms(transformation_matrix)

 image=tfa.image.transform(image,flat_tensor, fill_mode=CFG.fill_mode)

 rotation = math.pi * rot / 180.

 image=tfa.image.rotate(image,-rotation, fill_mode=CFG.fill_mode)

 if DIM[0]!=DIM[1]:
  image=tf.reshape(image, [NEW_DIM, NEW_DIM,3])
  image = image[:, pad:DIM[1]+pad,:]
 image = tf.reshape(image, [*DIM, 3])
 return image

def dropout(image,DIM=CFG.img_size, PROBABILITY = 0.6, CT = 5, SZ = 0.1):
 # input image – is one image of size [dim,dim,3] not a batch of [b,dim,dim,3]
 # output – image with CT squares of side size SZ*DIM removed

 # DO DROPOUT WITH PROBABILITY DEFINED ABOVE
 P = tf.cast( tf.random.uniform([],0,1)<PROBABILITY, tf.int32)
 if (P==0)|(CT==0)|(SZ==0):
  return image

 for k in range(CT):
  # CHOOSE RANDOM LOCATION
  x = tf.cast( tf.random.uniform([],0,DIM[1]),tf.int32)
  y = tf.cast( tf.random.uniform([],0,DIM[0]),tf.int32)
  # COMPUTE SQUARE
  WIDTH = tf.cast( SZ*min(DIM),tf.int32) * P
  ya = tf.math.maximum(0,y-WIDTH//2)
  yb = tf.math.minimum(DIM[0],y+WIDTH//2)
  xa = tf.math.maximum(0,x-WIDTH//2)
  xb = tf.math.minimum(DIM[1],x+WIDTH//2)
  # DROPOUT IMAGE
  one = image[ya:yb,0:xa,:]
  two = tf.zeros([yb-ya,xb-xa,3], dtype = image.dtype)
  three = image[ya:yb,xb:DIM[1],:]
  middle = tf.concat([one,two,three],axis=1)
  image = tf.concat([image[0:ya,:,:],middle,image[yb:DIM[0],:,:]],axis=0)
  image = tf.reshape(image,[*DIM,3])
#  image = tf.reshape(image,[*DIM,3])
 return image

Data Pipeline

  • raw ファイルを読み込んで、tf.Tensorへデコードする
  • 画像のサイズをリサイズする
  • データ型を float32 へ変更する
  • 処理速度を上げるためにデータをキャッシュする
  • 過学習(overfitting)を減らすためと、よりロバスト性のあるモデルを作るために、Augmentation を利用する
  • 最後に、バッチにデータを切り分ける(Finally, splits the data into batches.)
(※) データセットの処理とモデルの生成で CPU と GPU/TPU のアイドル時間を減らすために、
このノードでは、tf.data.Dataset API でデータの処理を良しなに分散して処理する設定をしている。
tf.data.experimental.AUTOTUNE
参考: tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる #3-performance向上のtips

https://qiita.com/bee2/items/c4f6f08a347e5d82b955#3-performance%E5%90%91%E4%B8%8A%E3%81%AEtips
def build_decoder(with_labels = True, target_size = CFG.img_size, ext = ‘jpg’):
 def decode(path):
  file_bytes = tf.io.read_file(path)
  if ext == ‘png’:
   img = tf.image.decode_png(file_bytes, channels = 3)
  elif ext in [‘jpg’, ‘jpeg’]:
   img = tf.image.decode_jpeg(file_bytes, channels = 3)
  else:
   raise ValueError(‘Image extension not supported’)

  img = tf.image.resize(img, target_size)
  img = tf.cast(img, tf.float32) / 255.0
  img = tf.reshape(img, [*target_size, 3])

  return img

 def decode_with_labels(path, label):
  return decode(path), tf.cast(label, tf.float32) / 100.0

 return decode_with_labels if with_labels else decode

def build_augmenter(with_labels = True, dim = CFG.img_size):
 def augment(img, dim = dim):
  img = transform(img, DIM = dim) if CFG.transform else img
  img = tf.image.random_flip_left_right(img) if CFG.hflip else img
  img = tf.image.random_flip_up_down(img) if CFG.vflip else img
  img = tf.image.random_hue(img, CFG.hue) # 色相
  img = tf.image.random_saturation(img, CFG.sat[0], CFG.sat[1])
  imt = tf.image.random_contrast(img, CFG.cont[0], CFG.cont[1])
  img = tf.image.random_brightness(img, CFG.bri)
  img = dropout(img, DIM = dim, PROBABILITY = CFG.drop_prob, CT = CFG.drop_cnt, SZ = CFG.drop_size)
  img = tf.clip_by_value(img, 0, 1) if CFG.clip else img
  img = tf.reshape(img, [*dim, 3])
  return img

 def augment_with_labels(img, label):
  return augment(img), label

 return augment_with_labels if with_labels else augment
def build_dataset(paths, labels = None, batch_size = 32, cache = True,
      decode_fn = None, augment_fn = None, augment = True,
      repeat = True, shuffle = 1024, cache_dir = ”, drop_remainder = False):
 if cache_dir != ” and cache is True:
  os.makedirs(cache_dir, exist_ok = True)

 if decode_fn is None:
  # build_decoder の with_labels を not None でインスタンスを作る?
  decode_fn = build_decoder(labels is not None)

 if augment_fn is None:
  augment_fn = build_augmenter(labels is not None)

 AUTO = tf.data.experimental.AUTOTUNE # tfで良しなにデータの処理とモデルの学習を平行処理できるような設定
 slices = paths if labels is None else(paths, labels)

 ds = tf.data.Dataset.from_tensor_slices(slices)
 ds = ds.map(decode_fn, num_parallel_calls=AUTO) # tfで並行処理を調整できるように設定
 ds = ds.cache(cache_dir) if cache else ds
 ds = ds.repeat() if repeat else ds
 if shuffle:
  ds = ds.shuffle(shuffle, seed = CFG.seed)
  opt = tf.data.Options()
  opt.experimental_deterministic = False
  ds = ds.with_options(opt)
 ds = ds.map(augment_fn, num_parallel_calls = AUTO) if augment else ds # tfで並行処理を調整できるように設定
 ds = ds.batch(batch_size, drop_remainder = drop_remainder)
 ds = ds.prefetch(AUTO) # tfで並行処理を調整できるように設定
 return ds

Visualization(視覚化)

Augmentation が動いているかをチェックする
def display_batch(batch, size=2):
 imgs, tars = batch
 plt.figure(figsize = (size * 5, 5))
 for img_idx in range(size):
  plt.subplot(1, size, img_idx + 1)
  plt.title(f'{CFG.target_col[0]}: {tars[img_idx].numpy()[0]}’, fontsize=15)
  plt.imshow(imgs[img_idx, :, :, :])
  plt.xticks([])
  plt.yticks([])
 plt.tight_layout()
 plt.show()
Kaggle notebookだと、ここで写真がいくつか出てくるはずだけど、このnotebookでは出てこない、、、
fold = 0
fold_df = df.query(‘fold==@fold’)[:1000]
paths = fold_df.image_path.tolist()
labels = fold_df[CFG.target_col].values
ds = build_dataset(paths, labels, cache = False, batch_size = CFG.batch_size * REPLICAS,
        repeat = True, shuffle = True, augment = True)
ds = ds.unbatch().batch(20)
batch = next(iter(ds))
display_batch(batch, 5)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

Loss Function(損失関数)

このノートブックでは損失関数 として、回帰から分析に変換されるときの BCE:Binary Crossentropy を利用します。
y^i は予測値で、yi はインスタンス i のオリジナルの値です。

Metric

メトリクスには、RMSE:Root Mean Squared Errorを利用します。
y^i は予測値で、yi はインスタンス i のオリジナルの値です。 予測値を[0-1]の範囲から、[0-100]の範囲に変換するために、予測値を非正規化する必要があります

なので、RMSEを利用します

def RMSE(y_true, y_pred, denormalize=True):
 if denormalize:
  # denormalizeing
  y_true = y_true * 100
  y_pred = y_pred * 100
 # rmse

 loss = tf.math.sqrt(tf.math.reduce_mean(tf.math.square(tf.subtract(y_true, y_pred))))
 return loss
RMSE.name=’rmse’  

Build Model

Vision Transformer(ViT)

  • ViTはNLP Transformerの単語埋め込み生成のように、インプットの画像16*16をsequenceパッチで壊します
  • それぞれのパッチは全てのパッチ内のピクセルと相互接続をした一次元の行列を取得して、それを目的の入力次元に投影(入力)する
  • トランスフォーマーはselt-attentionモードで動作し、入力要素の構造に必ずしも依存しないため、アーキテクチャーがまばらに分散した情報より効率良く学習して関連付けることができる。
  • 画像内のパッチ間の関係は不明なので、トレーニングデータからより関連性の高い特徴を学習し、ViTの一埋め込みにエンコードすることができる
How it works?

https://analyticsindiamag.com/hands-on-vision-transformers-with-pytorch/
An Image is Worth 16×16 Words: Transformers for Image Recognition at Scale

https://arxiv.org/abs/2010.11929
import efficientnet.tfkeras as efn
from vit_keras import vit, utils, visualize, layers

name2effnet = {
 ’efficientnet_b0′: efn.EfficientNetB0,
 ’efficientnet_b1′: efn.EfficientNetB1,
 ’efficientnet_b2′: efn.EfficientNetB2,
 ’efficientnet_b3′: efn.EfficientNetB3,
 ’efficientnet_b4′: efn.EfficientNetB4,
 ’efficientnet_b5′: efn.EfficientNetB5,
 ’efficientnet_b6′: efn.EfficientNetB6,
 ’efficientnet_b7′: efn.EfficientNetB7,
}

# CFG.img_size = [512, 512]
def build_model(model_name=CFG.model_name, DIM=CFG.img_size[0], compile_model=True, include_top=False):
 base = getattr(vit, model_name)(image_size=(DIM, DIM),
            include_top=False,
            pretrained_top=False,
            pretrained=True,
            weights=’imagenet21k+imagenet2012′)
 inp = base.inputs
 out = base.output
 out = tf.keras.layers.Dense(64, activation=’selu’)(out)
 out = tf.keras.layers.Dense(1, activation=’sigmoid’)(out)
 model = tf.keras.Model(inputs=inp, outputs=out)
 if compile_model:
  # optimizer
  opt = tf.keras.optimizers.Adam(learning_rate=0.001)
  # loss
  loss = tf.keras.losses.BinaryCrossentropy(label_smoothing=0.01)
  # metric
  rmse = RMSE
  model.compile(optimizer=opt,
       loss=loss,
       metrics=[rmse])
 return model

Model Check

/home/user/.local/lib/python3.7/site-packages/vit_keras/utils.py:83: UserWarning: Resizing position embeddings from 24, 24 to 32, 32
   UserWarning,
tmp = build_model(CFG.model_name, DIM=CFG.img_size[0], compile_model=True)
/home/user/.local/lib/python3.6/site-packages/vit_keras/utils.py:83: UserWarning: Resizing position embeddings from 24, 24 to 16, 16
UserWarning,

Learning-Rate Scheduler

def get_lr_callback(batch_size=8, plot=False):
 lr_start = 0.000005
 lr_max = 0.00000125 * REPLICAS * batch_size # REPLICASはTPU CONFIGの部分で指定している
 lr_min = 0.000001
 lr_ramp_ep = 5
 lr_sus_ep = 0
 lr_decay = 0.8

 # この関数が何やっているかちょっとわからない
 def lrfn(epoch):
  if epoch < lr_ramp_ep:
   lr = (lr_max – lr_start) / lr_ramp_ep * epoch + lr_start
  elif epoch < lr_ramp_ep + lr_sus_ep:
   lr = lr_max elif CFG.scheduler == ‘exp’: lr = (lr_max – lr_min) * lr_decay ** (epoch – lr_ramp_ep – lr_sus_ep) + lr_min
  elif CFG.scheduler == ‘cosine’:
   decay_total_epochs = CFG.epochs – lr_ramp_ep – lr_sus_ep + 3 # decay:減衰する、崩壊する
   decay_epoch_index = epoch – lr_ramp_ep – lr_sus_ep
   phase = math.pi * decay_epoch_index / decay_total_epochs
   cosine_decay = 0.5 * (1 + math.cos(phase))
   lr = (lr_max – lr_min) * cosine_decay + lr_min
  return lr

 if plot:
  plt.figure(figsize=(10,5))
  plt.plot(np.arange(CFG.epochs), [lrfn(epoch) for epoch in np.arange(CFG.epochs)], marker=’o’) # ここどうなっているのかちょっとわからない
  plt.xlabel(‘epoch’); plt.ylabel(‘learning rate’)
  plt.title(‘Learning Rate Scheduler’)
  plt.show()
 lr_callback = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose = False)
 return lr_callback

_ = get_lr_callback(CFG.batch_size, plot=True)

Attention Map

Attention MapはCiTから生成され、結果を解釈するのに役立ちます。
import matplotlib.cm as cm, cv2
from tensorflow import keras
from tensorflow.keras.preprocessing.image import array_to_img

def attention_map(model, image):
 ”””
 論文(非公式)の付録D.7で説明されている手法を利用して、画像とモデルのAttention_mapを取得する
 Args:
  model : ViTモデル
 image : Attention mapを計算する画像
 ”””

 size = model.input_shape[1]
 grid_size = int(np.sqrt(model.layers[5].output_shape[0][-2] – 1))

 # 入力を準備する
 X = cv2.resize(image, (size, size))[np.newaxis, :] # type: ignore

 # それぞれのTransformerからattention weights を取得
 outputs = [l.output[1] for l in model.layers if isinstance(l, layers.TransformerBlock)]
 weights = np.array(tf.keras.models.Model(inputs = model.inputs, outputs = outputs).predict(X))
 num_layers = weights.shape[0]
 num_heads = weights.shape[2]
 reshaped = weights.reshaped((num_layers, num_heads, grid_size ** 2 + 1, grid_size ** 2 + 1))

 # 非公式な論文の付録D.7より
 # 全てのheadのattention weightsを平均化します
 reshaped = reshaped.mean(axis=1)

 # From Section 3 in https://arxiv.org/pdf/2005.00928.pdf …
 # 残余接続を説明するために、attention行列に単位行列を追加して、重みを再正規化します。
 reshaped = reshaped + np.eye(reshaped.shape[1])
 reshaped = reshaped / reshaped.sum(axis=(1, 2)[:, np.newaxis, np.newaxis])

 # 重み行列を再帰的に乗算する
 v = reshaped[-1]
 for n in range(1, len(reshaped)):
  v = np.matmul(v, reshaped[-1 – n])

 # 出力トークンから入力トークンへのAttention
 mask = v[0, 1:].reshaped(grid_size, grid_size)
 mask = mask / mask.max()
 mask = np.uint8(255 * mask)

 # jet カラーマップを利用してヒートマップを色付けする
 jet = cm.get_cmap(“jet”)

 # カラーマップにRGB値を利用する
 jet_colors = jet(np.arange(256))[:, :3]
 jet_heatmap = jet_colors[mask]

 # RGBで色付けされたヒートマップの画像を作成する
 jet_heatmap = cv2.resize(jet_heatmap, dsize=(image.shape[1], image.shape[0]))

 # 元の画像にヒートマップを重ねる
 superimposed_img = jet_heatmap * 0.5 + image
 superimposed_img = array_to_img(superimposed_img)
 return superimposed_img

Wandb Logger

Log:
  • Best Score
  • Attention Map
if CFG.wandb:
 def wandb_init(fold):
  config = {k:v for k,v in dict(vars(CFG)).items() if ” not in k} # CFGのキーにが入っていないものだけを自書型のままconfigに入れる
  config.update({“fold”: int(fold)})
  yaml.dump(config, open(f'{CFG.output_dir}/kaggle/working/config fold-{fold}.yaml’, ‘w’),)
  config = yaml.load(open(f'{CFG.output_dir}/kaggle/working/config fold-{fold}.yaml’, ‘r’), Loader = yaml.FullLoader)
  run = wandb.init(project=”petfinder-public”,
        name=f’fold-{fold}|dim-{CFG.img_size[0]}|model-{CFG.model_name}’,
        config=config,
        anonymous=None,
        group=CFG.exp_name)
  return run

def log_wandb(fold):
 ”log best result and grad-cam for error analysis”

 valid_df = df.loc[df.fold == fold].copy()
 if CFG.debug:
  valid_df = valid_df.iloc[:1000]
 valid_df[‘pred’] = oof_pred[fold].reshape(-1)
 valid_df[‘diff’] = abs(valid_df.Pawpularity – valid_df.pred)
 valid_df = valid_df[valid_df.fold == fold].reset_index(drop=True)
 valid_df = valid_df.sort_values(by=’diff’, ascending=False)

 noimg_cols = [‘Id’, ‘fold’, ‘Subject Focus’,’Eyes’,’Face’,’Near’,’Action’,’Accessory’,’Group’,
      ‘Collage’,’Human’,’Occlusion’,’Info’,’Blur’,
      ‘Pawpularity’, ‘pred’, ‘diff’]

 # topとworstの10ケースを選択する
 gradcam_df = pd.concat((valid_df.head(10), valid_df.tail(10)), axis=0)
 gradcam_ds = build_dataset(gradcam_df.image_path, labels=None, cache=False, batch_size=1,
          repeat=False, shuffle=False, augment=False)
 data = []
 for idx, img in enumerate(gradcam_ds):
  gradcam = array_to_img(attention_map(model=model, image=img.numpy()[0]))
  row = gradcam_df[noimg_cols].iloc[idx].tolist()
  data += [[row, wandb.Image(img.numpy()[0]), wandb.Image(gradcam)]]  wandb_table = wandb.Table(data=data, columns=[noimg_cols, ‘image’, ‘gradcam’])
 wandb.log({‘best_rmse’ : oof_val[-1],
     ‘best_rmse_tta’ : rmse,
     ‘best_epoch’ : np.argmin(history.history[‘val_rmse’]),
     ‘viz_table’ : wandb_table})

Callback

カスタム進捗表示用のCallback関数定義
“””
進捗表示用のCallback関数です。
Batch終了時とEpoch終了時にデータを収集して、表示しています。
ポイントとしては print 出力時に /r で行先頭にカーソルを戻しつつ、引数 end=” で改行を抑制している点です。
“””
import datetime

class DisplayCallBack(tf.keras.callbacks.Callback):
 # コンストラクタ
 def init(self):
  self.last_acc, self.last_loss, self.last_val_acc, self.last_val_loss = None, None, None, None
  self.now_batch, self.now_epoch = None, None
  self.epochs, self.samples, self.batch_size = CFG.epochs, None, CFG.batch_size

 #カスタム進捗表示 (表示部本体)
# def print_progress(self):
# epoch = self.now_epoch
# batch = self.now_batch

# epochs = self.epochs
# samples = self.samples
# batch_size = self.batch_size
# sample = batch_size*(batch)

# # ‘\r’ と end=” を使って改行しないようにする
# if self.last_val_acc and self.last_val_loss:
# # val_acc/val_loss が表示可能
# print(“\rEpoch %d/%d (%d/%d) — acc: %f loss: %f – val_acc: %f val_loss: %f” % (epoch+1, epochs, sample, samples, self.last_acc, self.last_loss, self.last_val_acc, self.last_val_loss), end=”)
# else:
# # val_acc/val_loss が表示不可
# print(“\rEpoch %d/%d (%d/%d) — acc: %f loss: %f” % (epoch+1, epochs, sample, samples, self.last_acc, self.last_loss), end=”)

 # fit開始時
 def on_train_begin(self, logs={}):
  print(‘\n##### Train Start ##### ‘ + str(datetime.datetime.now()))
 # パラメータの取得
# self.epochs = self.params[‘epochs’]
# self.samples = self.params[‘samples’]
# self.batch_size = self.params[‘batch_size’]

 # 標準の進捗表示をしないようにする
# self.params[‘verbose’] = 0

 # batch開始時
 def on_batch_begin(self, batch, logs={}):
  return None

# self.now_batch = batch
# print(‘bachがスタートしました’) # うざいので外した

 # batch完了時 (進捗表示)
 def on_batch_end(self, batch, logs={}):
   return None
# # 最新情報の更新
# self.last_acc = logs.get(‘acc’) if logs.get(‘acc’) else 0.0
# self.last_loss = logs.get(‘loss’) if logs.get(‘loss’) else 0.0

# # 進捗表示
# self.print_progress()
# print(‘bachが完了しました’) # うざいので外した

 # epoch開始時
 def on_epoch_begin(self, epoch, log={}):
# self.now_epoch = epoch
 print(‘epochがスタートしました’)

 # epoch完了時 (進捗表示)
 def on_epoch_end(self, epoch, logs={}):
# # 最新情報の更新
# self.last_val_acc = logs.get(‘val_acc’) if logs.get(‘val_acc’) else 0.0
# self.last_val_loss = logs.get(‘val_loss’) if logs.get(‘val_loss’) else 0.0

# # 進捗表示
 self.print_progress()
 print(‘epochが完了しました’)

 # fit完了時
 def on_train_end(self, logs={}):
 print(‘\n##### Train Complete ##### ‘ + str(datetime.datetime.now()))

#コールバック関数用のインスタンス生成
cbDisplay = DisplayCallBack()

Train Model

  • Cross-Validation: 5 fold
  • wandb ダッシュボードは全てのfoldの最後を見せてくれます。なので、色々とプロットする必要はないです。そこから、ベストなモデルを選択だけすれば良いです。
oof_pred = []; oof_tar = []; oof_val = []; oof_ids = []; oof_folds = []
preds = np.zeros((test_df.shape[0], 1))

for fold in np.arange(CFG.folds):
 if fold not in CFG.selected_folds:
  continue
 if CFG.wandb:
  run = wandb_init(fold)
  WandbCallback = wandb.keras.WandbCallback(save_model=False)
 if CFG.device == ‘TPU’:
  if tpu: tf.tpu.experimental.initialize_tpu_system(tpu)

 #Train and Valid Dataframe
 train_df = df.query(“fold!=@fold”)
 valid_df = df.query(“fold==@fold”)

 #Create Train and Validation Subsets
 train_paths = train_df.image_path.values; train_labels = train_df[CFG.target_col].values.astype(np.float32)
 valid_paths = valid_df.image_path.values; valid_labels = valid_df[CFG.target_col].values.astype(np.float32)
 test_paths = test_df.image_path.values

 #Shuffle Image and Labels
 index = np.arange(len(train_paths))
 np.random.shuffle(index)
 train_paths = train_paths[index]
 train_labels = train_labels[index]

 if CFG.debug:
  train_paths = train_paths[:2000]; train_labels = train_labels[:2000]
  valid_paths = valid_paths[:1000]; valid_labels = valid_labels[:1000]

 print(‘#’25); print(‘#### FOLD’,fold)
 print(‘#### IMAGE_SIZE: (%i, %i) | MODEL_NAME: %s | BATCH_SIZE: %i’%
   (CFG.img_size[0],CFG.img_size[1],CFG.model_name,CFG.batch_sizeREPLICAS))
  train_images = len(train_paths)
  val_images = len(valid_paths)
  if CFG.wandb:
   wandb.log({‘num_train’:train_images,
       ‘num_valid’:val_images})
 print(‘#### NUM_TRAIN %i | NUM_VALID: %i’%(train_images, val_images))
 #BUILD MODEL
 K.clear_session()
 with strategy.scope():
  model = build_model(CFG.model_name, DIM=CFG.img_size[0], compile_model=True)

 #DATASET
 train_ds = build_dataset(train_paths, train_labels, cache=True, batch_size=CFG.batch_sizeREPLICAS,
       repeat=True, shuffle=True, augment=CFG.augment)
 val_ds = build_dataset(valid_paths, valid_labels, cache=True, batch_size=CFG.batch_sizeREPLICAS,
       repeat=False, shuffle=False, augment=False)

 print(‘#’*25)
 #SAVE BEST MODEL EACH FOLD
 sv = tf.keras.callbacks.ModelCheckpoint(
  ’fold-%i.h5’%fold, monitor=’val_rmse’, verbose=CFG.verbose, save_best_only=True,
  save_weights_only=False, mode=’min’, save_freq=’epoch’)
 callbacks = [sv, get_lr_callback(CFG.batch_size), cbDisplay] # コールバック関数を定義
 if CFG.wandb:
  callbacks.append(WandbCallback)
 
 # モデルのサマリーを確認する
 print(‘#’25)
 print(‘model summary::’)
 print(model.summary())
 print(‘#’25)

 # モデルの学習
 print(‘Training…’)
 history = model.fit(
  train_ds,
  epochs=CFG.epochs if not CFG.debug else 2,
  callbacks = callbacks,
  steps_per_epoch=len(train_paths)/CFG.batch_size//REPLICAS,
  validation_data=val_ds,
  #class_weight = {0:1,1:2},
  verbose=CFG.verbose
 )

 # Loading best model for inference
 print(‘Loading best model…’)
 model.load_weights(‘fold-%i.h5’%fold)

 # PREDICT OOF USING TTA
 print(‘Predicting OOF with TTA…’)
 ds_valid = build_dataset(valid_paths, labels=None, cache=False, batch_size=CFG.batch_sizeREPLICAS2,
         repeat=True, shuffle=False, augment=True if CFG.tta>1 else False)
 ct_valid = len(valid_paths); STEPS = CFG.tta * ct_valid/CFG.batch_size/2/REPLICAS
 pred = model.predict(ds_valid,steps=STEPS,verbose=CFG.verbose)[:CFG.ttact_valid,]
 oof_pred.append(np.mean(pred.reshape((ct_valid,-1,CFG.tta),order=’F’),axis=-1)100.0 )

 # GET OOF TARGETS AND idS
 oof_tar.append(valid_df[CFG.target_col].values[:(1000 if CFG.debug else len(valid_df))])
 oof_folds.append(np.ones_like(oof_tar[-1],dtype=’int8′)*fold )
 oof_ids.append(valid_df.Id.values)

 # PREDICT TEST USING TTA
 print(‘Predicting Test with TTA…’)
 ds_test = build_dataset(test_paths, labels=None, cache=True,
    batch_size=(CFG.batch_size2 if len(test_df)>8 else 1)REPLICAS,
    repeat=True, shuffle=False, augment=True if CFG.tta>1 else False)
 ct_test = len(test_paths); STEPS = 1 if len(test_df)<=8 else (CFG.tta * ct_test/CFG.batch_size/2/REPLICAS)


 STEPS = 10
 pred = model.predict(ds_test,steps=STEPS,verbose=CFG.verbose)[:CFG.ttact_test,]
 preds[:ct_test, :] += np.mean(pred.reshape((ct_test,-1,CFG.tta),order=’F’),axis=-1) / CFG.folds100

 # REPORT RESULTS
 y_true =oof_tar[-1]; y_pred = oof_pred[-1]
 rmse =RMSE(y_true.astype(np.float32),y_pred, denormalize=False).numpy()
 oof_val.append(np.min( history.history[‘val_rmse’] ))
 print(‘#### FOLD %i OOF RMSE without TTA = %.3f, with TTA = %.3f’%(fold,oof_val[-1],rmse))

 if CFG.wandb:
  log_wandb(fold) # log result to wandb
  wandb.run.finish() # finish the run
  display(ipd.IFrame(run.url, width=1080, height=720)) # show wandb dashboard
#########################
#### FOLD 0
#### IMAGE_SIZE: (256, 256) | MODEL_NAME: vit_b16 | BATCH_SIZE: 2
#### NUM_TRAIN 4956 | NUM_VALID: 4956
/home/user/.local/lib/python3.6/site-packages/vit_keras/utils.py:83: UserWarning: Resizing position embeddings from 24, 24 to 16, 16
UserWarning,
#########################
#########################
model summary::
Model: “model”
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 256, 256, 3)]     0         
_________________________________________________________________
embedding (Conv2D)           (None, 16, 16, 768)       590592    
_________________________________________________________________
reshape (Reshape)            (None, 256, 768)          0         
_________________________________________________________________
class_token (ClassToken)     (None, 257, 768)          768       
_________________________________________________________________
Transformer/posembed_input ( (None, 257, 768)          197376    
_________________________________________________________________
Transformer/encoderblock_0 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_1 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_2 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_3 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_4 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_5 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_6 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_7 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_8 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_9 ( ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_10  ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoderblock_11  ((None, 257, 768), (None, 7087872   
_________________________________________________________________
Transformer/encoder_norm (La (None, 257, 768)          1536      
_________________________________________________________________
ExtractToken (Lambda)        (None, 768)               0         
_________________________________________________________________
dense (Dense)                (None, 64)                49216     
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 65        
=================================================================
Total params: 85,894,017
Trainable params: 85,894,017
Non-trainable params: 0
_________________________________________________________________
None
#########################
Training...
epochがスタートしました

Calsulate OOF Score

# COMPUTE OVERALL OOF RMSE
oof = np.concatenate(oof_pred); true = np.concatenate(oof_tar);
ids = np.concatenate(oof_ids); folds = np.concatenate(oof_folds)
rmse = RMSE(true.astype(np.float32),oof, denormalize=False)
print(‘Overall OOF RMSE with TTA = %.3f’%rmse)
Overall OOF RMSE with TTA = 21.614
# SAVE OOF TO DISK
columns = [‘Id’, ‘fold’, ‘true’, ‘pred’]
df_oof = pd.DataFrame(np.concatenate([ids[:,None], folds[:, 0:1], true, oof], axis=1), columns=columns)
df_oof.to_csv(CFG.output_dir + ‘/oof.csv’,index=False)
df_oof.head()
Idfoldtruepred
00007de18844b0dbbb5e1f607da0606e006331.9496
10013fd999caf9a3efe1352ca1b0d937e02844.5821
2001dd4f6fafb890610b1635f967ea08107441.5105
30023b8a3abc93c712edd6120867deb5302216.1942
40042bc5bada6d1cf8951f8f9f0d399fa05323.4327

Pawpularity Distribution of OOF & Train

モデル学習とoofでのPawpularity分布を確認する
import seaborn as sns
sns.set(style=’dark’)

plt.figure(figsize=(10*2,6))

plt.subplot(1, 2, 1)
sns.kdeplot(x=train_df[CFG.target_col[0]], color=’b’,shade=True);
sns.kdeplot(x=df_oof.pred.values, color=’r’,shade=True);
plt.grid(‘ON’)
plt.xlabel(CFG.target_col[0]);plt.ylabel(‘freq’);plt.title(‘KDE’)
plt.legend([‘train’, ‘oof’])

plt.subplot(1, 2, 2)
sns.histplot(x=train_df[CFG.target_col[0]], color=’b’);
sns.histplot(x=df_oof.pred.values, color=’r’);
plt.grid(‘ON’)
plt.xlabel(CFG.target_col[0]);plt.ylabel(‘freq’);plt.title(‘Histogram’)
plt.legend([‘train’, ‘oof’])

plt.tight_layout()
plt.show()

まとめ

GPUSOROBANでコードの実行検証を確認することができました。今回簡単に検証を行うため、ステップ数及びデータサイズを減らしました。 実際の場合、データサイズも大きくなるので、GPUメモリは10Gb以上を推奨します。

関連記事

ページ上部へ戻る