{"cells":[{"metadata":{},"cell_type":"markdown","source":"# <center>3rd ML Month - Keras Semi-supervised Learning </center>"},{"metadata":{},"cell_type":"markdown","source":"### 배경\n- 이번 대회의 class는 196개로 매우 많습니다. \n- 훈련데이터셋을 class로 나누게 되면 데이터가 매우 적고 그러므로 더욱 더 많은 데이터가 있으면 잘 학습시킬 수 있지않을까? 생각했습니다.\n\n### Semi-supervised Learning\n- 그래서 Semi-supervised Learning 기법 중 Pseudo Labelling을 사용하려고 합니다.\n- Pseudo Labelling의 절차는  1) 훈련데이터를 통한 모델생성 2) 테스트데이터 예측 3) 훈련데이터에 확실하게 예측된 테스트데이터 추가 4) 결합데이터를 통한 모델생성 5) 테스트데이터 예측입니다.\n\n### reference\n- [version2](https://www.kaggle.com/kimtaegwan/keras-semi-supervised-learning?scriptVersionId=18397555)[Public 0.930] 을 통하여 test셋의 label을 0.99이상으로 예측한 testset만 가져왔습니다.\n\n baseline 잡을 때 도움되는 커널입니다. 참고하시고 많이 upvote 눌러주세요~\n- [3rd ML Month Car Model Classification Baseline](https://www.kaggle.com/fulrose/3rd-ml-month-car-model-classification-baseline)\n- [3rd ML Month Car Model Classification (Xception)](https://www.kaggle.com/janged/3rd-ml-month-car-model-classification-xception)\n- [3rd ML Month Keras EfficientNet](https://www.kaggle.com/easter3163/3rd-ml-month-keras-efficientnet)\n- [Cutout Augmentation (on Keras EfficientNet)](https://www.kaggle.com/seriousran/cutout-augmentation-on-keras-efficientnet)"},{"metadata":{},"cell_type":"markdown","source":"# Package"},{"metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","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\nfrom tqdm import tqdm_notebook\nfrom sklearn.model_selection import train_test_split\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.models import Sequential, Model\nfrom keras.layers import Dense, Dropout, Flatten, Activation, Conv2D, GlobalAveragePooling2D\nfrom keras import layers\nfrom keras.optimizers import SGD, RMSprop\n\n\nimport os\nprint(os.listdir(\"../input\"))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#efficientnet download\n!pip install -U efficientnet==0.0.4\nfrom efficientnet import EfficientNetB3","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# File Directory Setting"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"#crop data directory\nDATA_PATH = '../input/car-crop'\nos.listdir(DATA_PATH)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#original data directory\nDATA_PATH2 = '../input/2019-3rd-ml-month-with-kakr'\nos.listdir(DATA_PATH2)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#semi_data directory\nDATA_PATH3 = '../input/semi-detaset'\nos.listdir(DATA_PATH3)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#crop merge directory\nDATA_PATH4 = '../input/car-crop2'\nos.listdir(DATA_PATH4)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# 이미지 폴더 경로\nTRAIN_IMG_PATH = os.path.join(DATA_PATH, 'train')\nTEST_IMG_PATH = os.path.join(DATA_PATH, 'test')\n\n# CSV 파일 경로\ndf_train = pd.read_csv(os.path.join(DATA_PATH2, 'train.csv'))\ndf_test = pd.read_csv(os.path.join(DATA_PATH2, 'test.csv'))\ndf_class = pd.read_csv(os.path.join(DATA_PATH2, 'class.csv'))\n\n# 버전 1의 submission load\ndf_semi = pd.read_csv(os.path.join(DATA_PATH3, 'Pseudo Labelsing.csv'))\n\n#버전 1에서 test를 tset로 저장하여 변경해줌 \nname = list(map(lambda x:  x.replace(\"tset\", \"test\"),df_semi['img_file']))\ndf_semi['img_file']=name\ndf_semi['img_file'] = df_semi['img_file']+'.jpg'\ndf_semi.head(5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# train/test data Split"},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train[\"class\"] = df_train[\"class\"].astype('str')\ndf_semi[\"class\"] = df_semi[\"class\"].astype('str')\ndf_train = df_train[['img_file', 'class']]\ndf_test = df_test[['img_file']]\n\n# train과 semi 데이터 병합\ndf_train2 = pd.concat([df_train, df_semi],axis=0)\n\n\nits = np.arange(df_train2.shape[0])\ntrain_idx, val_idx = train_test_split(its, train_size = 0.8, random_state=42)\n\nX_train = df_train2.iloc[train_idx, :]\nX_val = df_train2.iloc[val_idx, :]\n\nprint(X_train.shape)\nprint(X_val.shape)\nprint(df_test.shape)\ndf_train2.head(5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Parameter"},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"#ref: https://github.com/yu4u/cutout-random-erasing/blob/master/cifar10_resnet.py\ndef get_random_eraser(p=0.5, s_l=0.02, s_h=0.4, r_1=0.3, r_2=1/0.3, v_l=0, v_h=255, pixel_level=False):\n    def eraser(input_img):\n        img_h, img_w, img_c = input_img.shape\n        p_1 = np.random.rand()\n\n        if p_1 > p:\n            return input_img\n\n        while True:\n            s = np.random.uniform(s_l, s_h) * img_h * img_w\n            r = np.random.uniform(r_1, r_2)\n            w = int(np.sqrt(s / r))\n            h = int(np.sqrt(s * r))\n            left = np.random.randint(0, img_w)\n            top = np.random.randint(0, img_h)\n\n            if left + w <= img_w and top + h <= img_h:\n                break\n\n        if pixel_level:\n            c = np.random.uniform(v_l, v_h, (h, w, img_c))\n        else:\n            c = np.random.uniform(v_l, v_h)\n\n        input_img[top:top + h, left:left + w, :] = c\n\n        return input_img\n\n    return eraser","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import keras.backend as K\nfrom keras.legacy import interfaces\nfrom keras.optimizers import Optimizer\n\n\nclass AdamAccumulate(Optimizer):\n\n    def __init__(self, lr=0.001, beta_1=0.9, beta_2=0.999,\n                 epsilon=None, decay=0., amsgrad=False, accum_iters=1, **kwargs):\n        if accum_iters < 1:\n            raise ValueError('accum_iters must be >= 1')\n        super(AdamAccumulate, self).__init__(**kwargs)\n        with K.name_scope(self.__class__.__name__):\n            self.iterations = K.variable(0, dtype='int64', name='iterations')\n            self.lr = K.variable(lr, name='lr')\n            self.beta_1 = K.variable(beta_1, name='beta_1')\n            self.beta_2 = K.variable(beta_2, name='beta_2')\n            self.decay = K.variable(decay, name='decay')\n        if epsilon is None:\n            epsilon = K.epsilon()\n        self.epsilon = epsilon\n        self.initial_decay = decay\n        self.amsgrad = amsgrad\n        self.accum_iters = K.variable(accum_iters, K.dtype(self.iterations))\n        self.accum_iters_float = K.cast(self.accum_iters, K.floatx())\n\n    @interfaces.legacy_get_updates_support\n    def get_updates(self, loss, params):\n        grads = self.get_gradients(loss, params)\n        self.updates = [K.update_add(self.iterations, 1)]\n\n        lr = self.lr\n\n        completed_updates = K.cast(K.tf.floordiv(self.iterations, self.accum_iters), K.floatx())\n\n        if self.initial_decay > 0:\n            lr = lr * (1. / (1. + self.decay * completed_updates))\n\n        t = completed_updates + 1\n\n        lr_t = lr * (K.sqrt(1. - K.pow(self.beta_2, t)) / (1. - K.pow(self.beta_1, t)))\n\n        # self.iterations incremented after processing a batch\n        # batch:              1 2 3 4 5 6 7 8 9\n        # self.iterations:    0 1 2 3 4 5 6 7 8\n        # update_switch = 1:        x       x    (if accum_iters=4)  \n        update_switch = K.equal((self.iterations + 1) % self.accum_iters, 0)\n        update_switch = K.cast(update_switch, K.floatx())\n\n        ms = [K.zeros(K.int_shape(p), dtype=K.dtype(p)) for p in params]\n        vs = [K.zeros(K.int_shape(p), dtype=K.dtype(p)) for p in params]\n        gs = [K.zeros(K.int_shape(p), dtype=K.dtype(p)) for p in params]\n\n        if self.amsgrad:\n            vhats = [K.zeros(K.int_shape(p), dtype=K.dtype(p)) for p in params]\n        else:\n            vhats = [K.zeros(1) for _ in params]\n\n        self.weights = [self.iterations] + ms + vs + vhats\n\n        for p, g, m, v, vhat, tg in zip(params, grads, ms, vs, vhats, gs):\n\n            sum_grad = tg + g\n            avg_grad = sum_grad / self.accum_iters_float\n\n            m_t = (self.beta_1 * m) + (1. - self.beta_1) * avg_grad\n            v_t = (self.beta_2 * v) + (1. - self.beta_2) * K.square(avg_grad)\n\n            if self.amsgrad:\n                vhat_t = K.maximum(vhat, v_t)\n                p_t = p - lr_t * m_t / (K.sqrt(vhat_t) + self.epsilon)\n                self.updates.append(K.update(vhat, (1 - update_switch) * vhat + update_switch * vhat_t))\n            else:\n                p_t = p - lr_t * m_t / (K.sqrt(v_t) + self.epsilon)\n\n            self.updates.append(K.update(m, (1 - update_switch) * m + update_switch * m_t))\n            self.updates.append(K.update(v, (1 - update_switch) * v + update_switch * v_t))\n            self.updates.append(K.update(tg, (1 - update_switch) * sum_grad))\n            new_p = p_t\n\n            # Apply constraints.\n            if getattr(p, 'constraint', None) is not None:\n                new_p = p.constraint(new_p)\n\n            self.updates.append(K.update(p, (1 - update_switch) * p + update_switch * new_p))\n        return self.updates\n\n    def get_config(self):\n        config = {'lr': float(K.get_value(self.lr)),\n                  'beta_1': float(K.get_value(self.beta_1)),\n                  'beta_2': float(K.get_value(self.beta_2)),\n                  'decay': float(K.get_value(self.decay)),\n                  'epsilon': self.epsilon,\n                  'amsgrad': self.amsgrad}\n        base_config = super(AdamAccumulate, self).get_config()\n        return dict(list(base_config.items()) + list(config.items()))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Parameter\nimg_size = (300, 300)\nimage_size = 300\nnb_train_samples = len(X_train)\nnb_validation_samples = len(X_val)\nnb_test_samples = len(df_test)\nepochs = 30\nbatch_size = 32\n\n# Define Generator config\ntrain_datagen =ImageDataGenerator(\n    rescale=1./255,\n    rotation_range=10,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    horizontal_flip=True,\n    vertical_flip=False,\n    fill_mode='nearest',\n    preprocessing_function = get_random_eraser(v_l=0, v_h=1),\n    )\n\nval_datagen = ImageDataGenerator(rescale=1./255)\ntest_datagen = ImageDataGenerator(rescale=1./255)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#generator\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe=X_train, \n    directory='../input/car-crop2/train2_crop',\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\nvalidation_generator = val_datagen.flow_from_dataframe(\n    dataframe=X_val, \n    directory='../input/car-crop2/train2_crop',\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)\n\ntest_generator = test_datagen.flow_from_dataframe(\n    dataframe=df_test,\n    directory='../input/car-crop/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":{},"cell_type":"markdown","source":"# Model"},{"metadata":{"trusted":true},"cell_type":"code","source":"#model\nopt = AdamAccumulate(lr=0.001, decay=1e-5, accum_iters=5)\nEfficientNet_model = EfficientNetB3(weights='imagenet', include_top=False, input_shape=(image_size, image_size, 3))\n\n\nmodel = Sequential()\nmodel.add(EfficientNet_model)\nmodel.add(layers.GlobalAveragePooling2D())\nmodel.add(layers.Dense(2048, activation='relu'))\nmodel.add(layers.Dropout(0.5))\nmodel.add(layers.Dense(196, activation='softmax'))\nmodel.summary()\n\n#compile\nmodel.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['acc'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def 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","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nfrom keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\n\n#model path\nMODEL_SAVE_FOLDER_PATH = './model/'\nif not os.path.exists(MODEL_SAVE_FOLDER_PATH):\n    os.mkdir(MODEL_SAVE_FOLDER_PATH)\n\nmodel_path = MODEL_SAVE_FOLDER_PATH + '{epoch:02d}-{val_loss:.4f}.hdf5'\n\npatient = 3\ncallbacks_list = [\n     EarlyStopping(\n        # 모델의 검증 정확도 모니터링\n        monitor='val_loss',\n        # patient(정수)보다 정확도가 향상되지 않으면 훈련 종료\n        patience=patient, \n        # 검증에 대해 판단하기 위한 기준, val_loss경우 감소되는 것이므로 min\n        mode='min', \n        #얼마나 자세하게 정보를 나타낼것인가.\n        verbose=1\n                          \n    ),\n    ReduceLROnPlateau(\n        monitor = 'val_loss', \n        #콜백 호출시 학습률(lr)을 절반으로 줄임\n        factor = 0.5, \n        #위와 동일\n        patience = patient / 2, \n        #최소학습률\n        min_lr=0.00001,\n        verbose=1,\n        mode='min'\n    ),\n    ModelCheckpoint(\n        filepath=model_path,\n        monitor ='val_loss',\n        # val_loss가 좋지 않으면 모델파일을 덮어쓰지 않는다\n        save_best_only = True,\n        verbose=1,\n        mode='min') ]\n\n    \n\nhistory = model.fit_generator(\n    train_generator,\n    steps_per_epoch = get_steps(nb_train_samples, batch_size),\n    epochs=epochs,\n    validation_data = validation_generator,\n    validation_steps = get_steps(nb_validation_samples, batch_size),\n    callbacks = callbacks_list\n)\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# acc / loss Plot"},{"metadata":{"trusted":true},"cell_type":"code","source":"acc = history.history['acc']\nval_acc = history.history['val_acc']\nloss = history.history['loss']\nval_loss = history.history['val_loss']\n\nepochs = range(len(acc))\n\nplt.plot(epochs, acc, label='Training acc')\nplt.plot(epochs, val_acc, label='Validation acc')\nplt.title('Training and validation accuracy')\nplt.legend()\nplt.ylim(0.9,1)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(epochs, loss, label='Training loss')\nplt.plot(epochs, val_loss, label='Validation loss')\nplt.title('Training and validation loss')\nplt.legend()\nplt.ylim(0,0.5)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Predict"},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntest_generator.reset()\nprediction = model.predict_generator(\n    generator = test_generator,\n    steps = get_steps(nb_test_samples, batch_size),\n    verbose=1\n)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Submission"},{"metadata":{"trusted":true},"cell_type":"code","source":"submission = pd.read_csv(os.path.join(DATA_PATH2, 'sample_submission.csv'))\npredicted_class_indices=np.argmax(prediction, axis=1)\n\n# Generator class dictionary mapping\nlabels = (train_generator.class_indices)\nlabels = dict((v,k) for k,v in labels.items())\npredictions = [labels[k] for k in predicted_class_indices]\n\nsubmission[\"class\"] = predictions\nsubmission.to_csv(\"submission_all.csv\", index=False)\nsubmission.head()","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.6.6"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"0afe27514ffc4845b2fd9265e299e084":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.4.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.4.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.4.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_68cf59be223843859db7e9201fd4e593","IPY_MODEL_4f1e33bb74df43a285cd228409d19b5c"],"layout":"IPY_MODEL_e10a418cfa374efba3e447c3c15fa3d5"}},"4f1e33bb74df43a285cd228409d19b5c":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.4.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.4.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.4.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_79fa3afa6eed49be9d1a9d91fb27cb1d","placeholder":"​","style":"IPY_MODEL_94f2cad503d9400fb42aed59b22e9a6f","value":"100% 10/10 [08:23&lt;00:00, 49.99s/it]"}},"6894fef9cf324e528f0f46fcf5aff254":{"model_module":"@jupyter-widgets/base","model_module_version":"1.1.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.1.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.1.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"68cf59be223843859db7e9201fd4e593":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.4.0","model_name":"IntProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.4.0","_model_name":"IntProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.4.0","_view_name":"ProgressView","bar_style":"success","description":"","description_tooltip":null,"layout":"IPY_MODEL_6894fef9cf324e528f0f46fcf5aff254","max":10,"min":0,"orientation":"horizontal","style":"IPY_MODEL_923932ba26154348a206e34c416eda8f","value":10}},"79fa3afa6eed49be9d1a9d91fb27cb1d":{"model_module":"@jupyter-widgets/base","model_module_version":"1.1.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.1.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.1.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"923932ba26154348a206e34c416eda8f":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.4.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.4.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.1.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"94f2cad503d9400fb42aed59b22e9a6f":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.4.0","model_name":"DescriptionStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.4.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.1.0","_view_name":"StyleView","description_width":""}},"e10a418cfa374efba3e447c3c15fa3d5":{"model_module":"@jupyter-widgets/base","model_module_version":"1.1.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.1.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.1.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}}},"version_major":2,"version_minor":0}}},"nbformat":4,"nbformat_minor":1}