{"cells":[{"metadata":{},"cell_type":"markdown","source":"안녕하세요. \n\n먼저 이 글은 제가 여태까지 겪었던 것을 총 정리하는 것입니다.  \n\n향후, 대회가 끝나고 tutorial로 접근하려는 분들에게 도움이 되었으면 합니다.\n\nlibrary는 Keras를 사용합니다.\n\n고수님들의 커널은 아래에 있으니 꼭 읽어보시고 업보트 누르시길 바랍니다!(저도 눌렀습니다)\n\n또한, 커널을 통해 정보를 공유해주신 모든 커널 author님들께 감사드립니다.\n\nhttps://www.kaggle.com/fulrose/3rd-ml-month-car-model-classification-baseline\nbaseline kernel을 읽는 것은 당연!\n\n저는 주로 Local server와 colab을 주로 사용하였고, 이 대회를 통해 캐글 커널을 처음 접해보았습니다.  \n따라서, Commit이 무엇인지 Dataset을 어떻게 추가하는지에 대해 전혀 모르고 시작하여 시간을 많이 소비하였습니다 ㅠㅠ\n\n**## Add Crop Dataset  \n**\n\n처음에 데이터셋 추가를 할줄 몰랐던지라 직접 아래 커널의 코드를 다운받아 local에서 만든 뒤, dataset을 추가하여 사용하려고 했습니다.  \n하지만, 무슨 문제 때문인지 자꾸 다운되어 업로드가 되지 않아서 다시 colab으로 돌아갔습니다...  \n\nhttps://www.kaggle.com/tmheo74/3rd-ml-month-car-image-cropping-updated-7-10 # 허태명님\nhttps://www.kaggle.com/seriousran/xception-model-in-keras-for-car-classification # 김찬란님\nhttps://www.kaggle.com/yangsaewon/pytorch-baseline-updated-7-10 # 양새원님  \nhttps://www.kaggle.com/janged/3rd-ml-month-car-model-classification-xception # Jang 님\n\n또 미숙한 질문에 빠른 답변을 가져다 주셨던 김재관님도 감사드립니다."},{"metadata":{"trusted":true},"cell_type":"code","source":"import gc\nimport os\nimport warnings\nimport numpy as np \nimport pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\nimport math\nfrom keras.callbacks import Callback\nfrom keras import backend\nfrom keras.models import Sequential, Model\nfrom keras.layers import Dense, Dropout, Flatten, Activation, Conv2D, GlobalAveragePooling2D, BatchNormalization, Input\nfrom keras.optimizers import Adam, SGD, Nadam\nfrom keras.metrics import categorical_accuracy\nfrom keras.callbacks import ModelCheckpoint, EarlyStopping\nfrom keras.callbacks import LearningRateScheduler, ReduceLROnPlateau, TensorBoard\n\nimport cv2\nimport PIL\nfrom PIL import ImageOps, ImageFilter, ImageDraw\n\nfrom keras import backend as K\nwarnings.filterwarnings(action='ignore')\n\n# Efficient Net은 성능도 좋고, 가벼운 비교적 최신 모델입니다.\n!pip install git+https://github.com/qubvel/efficientnet\nfrom efficientnet import EfficientNetB3\n\nK.image_data_format()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**# 경로때문에 헤매다.**  \n커널을 처음 생성하고 비교적 접근성이 쉽고 자주 사용하는 jupyter notebook 형태의 커널을 사용하게 됩니다.  \n경로는 알기 쉽게 해주는 코드가 보였습니다. # os.listdir(path)\n\n이후 mkdir을 이용하고, 직접 경로가 존재하는 것 까지 확인. 학습 이후에 모델을 저장하고  \n불러와서 사용만 하면 됩니다. \n\n여기서 저의 멍청함이 +1 되어 Commit의 존재를 모른체 셀만 실행시키기 시작합니다.  \n학습을 시작하고 9시간의 여정이 끝났네요.  \n불러와서 prediction을 해야겠는데.... 없습니다. 아무리 찾아봐도 없습니다.\n\n이렇게 하루 반정도를 날렸네요.   \n\n답답함에 글을 올리고 김재관님의 답글에 느낌을 받아 어떻게 사용하는지 알게 되었습니다.\n\nCommit을 누르고 완료가 되면 open version으로 들어가서, output tab에서 저장된 파일을 다운로드 받아 Dataset으로 추가하면 됩니다.  \n+ 커널에 보시면 Help -> Feedback이 있습니다. 여기에 글을 올리시면 비교적 빠르게 Kaggle Team에서 직접 답변해줍니다.  \n+ 다만, 영어로 질문을..\n+ 아무튼 저 같은 초보자에게 중요한 것은 Commit의 존재유무(저만 이런 실수를 했을 수도 있겠네요 ㅎㅎ)\n+ Commit 후 Log를 유심히 관찰하시기 바랍니다. Traceback ~ 으로 이어지는 에러 메시지가 나온다면-->\n+ output tab이 생기지 않아 기다려도 결과를 얻으실 수 없습니다.(그나마 에러로 죽으면 다행입니다)\n+ 에러가 없다면 페이지를 끄고 멍때리셔도 됩니다. 저는 주로 멍때리다가 다른걸 해야지 하고 정신을 차립니다.\n+ 캐글 커널이 좋은 점은 loss의 변화를 보여주지 않기 때문에, 다른걸 할 수 있게 해줘요"},{"metadata":{"trusted":true},"cell_type":"code","source":"model_path = './model/'\n\nif(not os.path.exists(model_path)):\n    os.mkdir(model_path)\n    \nimg_path = os.listdir('../input')\nprint(img_path)\n\nCROP_PATH = '../input/' + img_path[0]\nDATA_PATH = '../input/' + img_path[1]\n\n# image folder path\nTRAIN_IMG_PATH = CROP_PATH +'/train_crop/'\nTEST_IMG_PATH = CROP_PATH +'/test_crop/'\n\n# read csv\ndf_train = pd.read_csv(DATA_PATH + '/train.csv')\ndf_test = pd.read_csv(DATA_PATH + '/test.csv')\ndf_class = pd.read_csv(DATA_PATH + '/class.csv')\n\nfrom sklearn.model_selection import train_test_split\n\ndf_train[\"class\"] = df_train[\"class\"].astype('str')\n\ndf_train = df_train[['img_file', 'class']]\ndf_test = df_test[['img_file']]\n\nfrom keras.preprocessing.image import ImageDataGenerator\n\n# Parameter\nnb_test_samples = len(df_test)\n\n# batch size는 제가 사용하기론 32가 최대였습니다. 양새원님 커널을 보면 128로 되어있는데,\n# 어떻게 큰 배치를 사용하셧는지 신기할 따름입니다..\nbatch_size = 32\n\n# Define Generator config\n# ImageGenerator에는 여러가지 param이 존재합니다. 공식 API문서를 참고하시길 추천드립니다.\ntrain_datagen = ImageDataGenerator(\n    rotation_range = 60,\n#     shear_range = 0.25,\n    width_shift_range=0.30,\n    height_shift_range=0.30,\n    horizontal_flip = True, \n    vertical_flip = False,\n    zoom_range=0.25,\n    fill_mode = 'nearest',\n    rescale = 1./255)\n\nval_datagen = ImageDataGenerator(rescale = 1./255)\ntest_datagen = ImageDataGenerator(rescale = 1./255)\n\ndef get_steps(num_samples, batch_size):\n    if (num_samples % batch_size) > 0 :\n        return (num_samples // batch_size) + 1\n    else :\n        return num_samples // batch_size\n    \nbase_model = EfficientNetB3(weights='imagenet', include_top=False, input_shape=(299, 299, 3))\n\n# 데이터셋이 편향되어있다는 분석글을 본 후에 몇 가지 찾아보았습니다.\n# sklearn을 활용하여 class weight를 적용해주는 방식입니다.\n# 사실 있고 없고, 성능 향상은 가져다주지 않았습니다.\nfrom sklearn.utils import class_weight\nclass_weights = class_weight.compute_class_weight('balanced',\n                                                 np.unique(df_train['class']),\n                                                 df_train['class'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**2. Train 과정**  \n\n학습과정은 굳이 과정을 설명하지 않아도 위에 링크해둔 고수님들 커널을 보시면 충분할 것 같습니다.  \n\n사실 저는 제 프로젝트에서 EalryStopping을 잘 사용하지 않습니다. 언제 어디서 올라갈지 모르기 떄문이라는 기대감에요..  \n\n그리고 나서 2일정도 돌려놓으면 알아서 완성되어 있습니다.  \n\n근데 커널을 사용하는 경우에는 이게 상당히 중요하다는 것을 느꼇습니다.  \n\n혹시라도 죽어버리면 너무....\n\n처음엔 patience도 10으로 고정시켰다가 확 줄였습니다. 도저히 감당이 안되는 것을 직접 체험할 수 있습니다.\n(한번 해보세요!)\n\n저는 Dropout보다는 BatchNormalization을 우선적으로 사용합니다.  \n\n하지만 해본 결과 BN보다는 Dropout의 결과가 더 좋았습니다 :)\n\n+ SGD를 통해 거의 12시간을 학습시켜보았지만 local minima에서 빠져나오질 못했습니다.\n+ 고수님들은 어떻게 이 optimizer를 사용하시는지 또 한번 감탄!"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","collapsed":true,"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":false},"cell_type":"code","source":"# init params\nlr = 2e-4\n\nepochs = 100\nT_max = 10 # Cosine Annealing을 위한 param, 일부러 지우지 않았습니다.\nn_cycles = epochs / 100\n\ndef get_callback(model_path):\n    callback_list = [\n              ModelCheckpoint(filepath=model_path, monitor='val_loss',\n                      verbose=1, save_best_only=True),\n              ReduceLROnPlateau(monitor='val_loss',\n                        factor=0.2,\n                        patience=3,\n                        min_lr=1e-6,\n                        cooldown=1,\n                        verbose=1),\n      EarlyStopping(monitor = 'val_f1_m', patience = 5)\n              ]\n    return callback_list\n\ndef get_model(input_size):\n    inputs = Input(shape = (input_size, input_size, 3), name = 'input_1')\n    x = base_model(inputs)\n    x = GlobalAveragePooling2D()(x)\n    x = Dense(2048, kernel_initializer='he_normal')(x)\n    x = Dropout(0.3)(x)\n    x = Activation('relu')(x)\n    x = Dense(196, activation = 'softmax')(x)\n\n    model = Model(inputs = inputs, outputs = x)\n#     sgd = SGD(lr=lr, decay=1e-8, momentum=0.9, nesterov=True)\n    nadam = Nadam(lr = lr)\n    model.compile(optimizer= nadam, loss='categorical_crossentropy', metrics=[categorical_accuracy,f1_m, precision_m, recall_m])\n    return model\n\nfrom sklearn.model_selection import StratifiedKFold\n\nk_folds = 5\nimg_size = (299, 299)\nskf = StratifiedKFold(k_folds, random_state = 2019)\n\ntest_generator = test_datagen.flow_from_dataframe(\n    dataframe=df_test,\n    directory='../input/' + img_path[0] + '/test_crop',\n    x_col='img_file',\n    y_col=None,\n    target_size= img_size,\n    color_mode='rgb',\n    class_mode=None,\n    batch_size=batch_size,\n    shuffle=False\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"j = 1\n\nimg_size = (299, 299)\nmodel_names = []\nfor (train_index, valid_index) in skf.split(\n    df_train['img_file'], \n    df_train['class']):\n    \n    traindf = df_train.iloc[train_index, :].reset_index()\n    validdf = df_train.iloc[valid_index, :].reset_index()\n    nb_train_samples = len(traindf)\n    nb_validation_samples = len(validdf)\n\n    print(\"=========================================\")\n    print(\"====== K Fold Validation step => %d/%d =======\" % (j,k_folds))\n    print(\"=========================================\")\n\n    # Make Generator\n    train_generator_299 = train_datagen.flow_from_dataframe(\n        dataframe=traindf, \n        directory=TRAIN_IMG_PATH,\n        x_col = 'img_file',\n        y_col = 'class',\n        target_size = img_size,\n        color_mode='rgb',\n        class_mode='categorical',\n        batch_size=batch_size,\n        seed=42\n    )\n\n    validation_generator_299 = val_datagen.flow_from_dataframe(\n        dataframe=validdf, \n        directory=TRAIN_IMG_PATH,\n        x_col = 'img_file',\n        y_col = 'class',\n        target_size = img_size,\n        color_mode='rgb',\n        class_mode='categorical',\n        batch_size=batch_size,\n        shuffle=True\n    )\n\n    model_name = model_path + str(j) + '_EFFnet_f1.hdf5'\n    model_names.append(model_name)\n    model_EFF = get_model(img_size[0])\n    \n    try:\n        model_EFF.load_weights(model_name)\n    except:\n        pass\n\n    history = model_xception.fit_generator(\n    train_generator_299,\n    steps_per_epoch = get_steps(nb_train_samples, 32),\n    epochs=epochs,\n    validation_data = validation_generator_299,\n    validation_steps = get_steps(nb_validation_samples, 32),\n    callbacks =  get_callback(model_name),\n    class_weight = class_weights\n      )\n        \n    j+=1\n    print(gc.collect())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**성능향상에 도움을 준 것들**\n+ TTA\n+ EarlyStopping을 통한 빠른 학습\n+ SKfold(good)"},{"metadata":{},"cell_type":"markdown","source":"---------\n**keras memory issue**\n\n저는 현재 inceptionResnetV2 + Xception + Efficient를 섞어서 submission을 생성하고 있습니다.\n\n3 모델을 전부 부르기엔 RAM 초과로 인해 Kernel Died 현상이 발생합니다.\n\n자세한 이유는 모르지만 케라스 내부적으로 model이 가지고 있는 RAM을 전부 제거하지 못하는 버그가 있다 카더라...?\n\n구글 검색을 하면 여러가지가 나오는데요.\n\n1. gc.collect()\n2. gc.collect 여러번\n3. tensorflow의 as_default_graph() 안에서 처리\n4. K.clear_session()\n\n위의 것을 전부 하나씩 두고 사용해본 결과. gc.collect()는 별 효과가 없었습니다.\n\nkeras를 사용하시고 여러번 모델을 load할 필요가 있으시다면 K.clear_session() 을 강력 추천드립니다.\n(실제로 저같은 경우엔 RAM 7GB이상 잡아먹지 않게 해주었습니다)"},{"metadata":{},"cell_type":"markdown","source":"---------------------------------------------------------------------------------\n다음은 제가 몇시간을 들여 사용해보려던 **Cosine Annealing**입니다.\n\nCNN에 필요한 몇가지 기술들에 관해 적어놓은 논문을 보고 꼭 사용해보고 싶었는데, 마침 이 대회가 열려서 시도해보았습니다.\n\n결과부터 말씀드리자면 결국 좋은 성능을 얻진 못했네요..  \n\n혹시 쓰실일이 있으시다면 참고만 해주시면 됩니다. 완전한 코드라고는 말씀 못드리겠습니다.\n\nCosine Annealing을 사용하기 위해 검색해보면, 몇몇 대표적인 블로그와 github가 있는데,\n\n전부 표현이 다릅니다.\n\n따라서 제가 사용해본 결과 pytorch의 코드를 추천드립니다."},{"metadata":{"trusted":true},"cell_type":"code","source":"# 생성자의 n_cycles를 주기로 작동하게 됩니다.\n# 이 코드의 단점은 min_lr 파라미터가 없는점.\nclass CosineAnnealingLearningRateSchedule(Callback):\n    # constructor\n    def __init__(self, n_epochs, n_cycles, lrate_max, verbose = 0):\n        self.epochs = n_epochs\n        self.cycles=  n_cycles\n        self.lr_max = lrate_max\n        self.lrates = list()\n    \n    # caculate learning rate for an epoch\n    def cosine_annealing(self, epoch, n_epochs, n_cycles, lrate_max):\n        epochs_per_cycle = math.floor(n_epochs/n_cycles)\n        cos_inner = (math.pi * (epoch % epochs_per_cycle)) / (epochs_per_cycle)\n        return lrate_max/2 * (math.cos(cos_inner) + 1)\n  \n    # calculate and set learning rate at the start of the epoch\n    def on_epoch_begin(self, epoch, logs = None):\n        if(epoch < 101):\n            # calculate learning rate\n            lr = self.cosine_annealing(epoch, self.epochs, self.cycles, self.lr_max)\n            print('\\nEpoch %05d: CosineAnnealingScheduler setting learng rate to %s.' % (epoch + 1, lr))\n        \n        # 이 밑의 코드는 학습이 너무 느리고, cycle을 돌기떄문에 일정 epoch이상은 고정된 lr을 사용하려고 한 흔적입니다.\n        #     elif((epoch >= 65) and (epoch < 75)):\n        #       lr = 1e-5\n        #       print('\\n No CosineAnnealingScheduler set lr 1e-5')\n        #     elif((epoch >= 75) and (epoch < 85)):\n        #       lr = 1e-6\n        #       print('\\n No CosineAnnealingScheduler set lr 1e-6')\n        #     elif((epoch >= 85)):\n        #       lr = 1e-7\n        #       print('\\n No CosineAnnealingScheduler set lr 1e-7')\n\n        # set learning rate\n        backend.set_value(self.model.optimizer.lr, lr)\n        # log value\n        self.lrates.append(lr)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# T_max를 주기로 돌게 됩니다.\n# 이때, T_mult는 제가 알기론 rewarmstarting을 위한 파라미터로서,\n# 검색해보면 어느 블로그에서 이 parameter의 변화에 따른 성능변화를 정리한 글이 있습니다.\n\n# 예를 들어서 T_max가 10이고, T_mult = 2이면,\n# 첫 주기 10, 다음 주기 20, 그 다음주기 40\n# 이런식으로 lr의 변화폭이 점점 줄어들게 됩니다.\n# 위 코드보다 편리한점은 더 깔끔하고, lr_min(eta_min)을 적용할 수 있다는 점\n\nclass CosineAnnealingLearningRateSchedule(Callback):\n    def __init__(self, n_epochs, init_lr, T_mult = 1, eta_min = 0,restart_decay = 0, verbose = 0):\n        self.T_max = n_epochs\n        self.T_mult = T_mult\n        self.cycle_cnt = 0\n        self.restart_decay = restart_decay\n        self.init_lr = init_lr\n        self.eta_min = eta_min\n        self.lrates = list()\n  # caculate learning rate for an epoch\n\n    def cosine_annealing(self, epoch):\n        lr = self.eta_min + (self.init_lr - self.eta_min) * (1 + math.cos(math.pi * (epoch / self.T_max))) / 2\n        if(epoch == self.T_max):\n            self.cycle_cnt += 1\n            self.T_max = self.T_mult * self.T_max\n\n        if(self.restart_decay >0):\n            self.init_lr *= self.restart_decay\n            print('change init learning rate {}'.format(self.init_lr))\n\n    return lr\n  # calculate and set learning rate at the start of the epoch\n\n    def on_epoch_begin(self, epoch, logs = None):\n        lr = self.cosine_annealing(epoch)\n        print('\\nEpoch %05d: CosineAnnealingScheduler setting learng rate to %s.' % (epoch + 1, lr))\n        # set learning rate\n        backend.set_value(self.model.optimizer.lr, lr)\n        # log value\n        self.lrates.append(lr)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"일정 수준의 epoch까지 Cossine Annealing을 한 후, fine tuning(고정 lr)을 적용하는 것이 가장 좋다고 생각됩니다.\n\n제 나름 찾아본 결과 optimizer을 변경하기 위해선 model.compile을 다시해야한다고 하는데요...\n\n이럴 땐, tensorflow low api가 역시.... 혹시 다른 방법이 있을까요??\n\n아래 코드는 inference시 사용한 코드입니다. 헷갈릴때만 참고해주세요"},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.models import load_model\n\nincepres_model = ['_incepres.hdf5', InceptionResNetV2]\nxception_model = ['_Xception.hdf5', Xception]\neff_model = ['_EFFnet.hdf5', EfficientNetB3]\n\nmodel_list = [xception_model, incepres_model, eff_model]\n# total predictions list\npreds_list = []\n\nlr = 1e-4\nTTA_STEPS = 10\n\nfor model_name, base_model in model_list:\n    print(model_name)\n    \n    # prediction each fold\n    predictions = []\n    if(model_name == '_EFFnet.hdf5'):\n        model_load_dir = MODEL_EFF_PATH\n    else:\n        model_load_dir = MODEL_PATH\n    for i in range(1, 6):\n        model = get_model(base_model, 299, False)\n        model.load_weights(os.path.join(model_load_dir, str(i)) + model_name)\n        # tta prediction list\n        tta_preds = []\n        for _ in range(TTA_STEPS):\n            test_generator_299.reset()\n            pred = model.predict_generator(\n            generator = test_generator_299, \n            steps = get_steps(nb_test_samples, batch_size),\n            verbose = 1\n            )\n            tta_preds.append(pred) # (5, 6150, 196)\n        tta_preds = np.mean(tta_preds, axis = 0) # (6150, 196)\n        \n        # for memory leaky\n        del model # 별 효과 없음\n        for _ in range(10): # 별 효과 없음\n            gc.collect()\n        K.clear_session() # 이 친구가 대장\n        predictions.append(tta_preds) # (5, 6150, 196)\n    preds_list.append(np.mean(predictions, axis = 0))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"코드보다는 중간중간 제가 적은 글을 읽어주세요.  \n\n5분도 안걸릴것 같습니다. \n\n헤맨다는 건 좋은 의미긴 하지만 저보다 더 좋은 헤맴을 꼭 하시길 바라면서....\n\n감사합니다."},{"metadata":{},"cell_type":"markdown","source":"---------------\n**더하여서, **\n+ 커널은 gpu를 사용하여 최대 몇개까지 running시킬 수 있나요??\n+ keras tuner https://github.com/keras-team/keras-tuner 를 적용해보려고 했습니다만, OOM.....  \n+ 케라스를 이용하려고 하시는 분들이라면 꼭 예제를 해보시길 바랍니다.\n(+ 케라스 코리아의 운영진이신 김태영님의 블로그에도 잘 설명되어 있습니다)"},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}