{"cells":[{"metadata":{},"cell_type":"markdown","source":"สวัสดีครับ สำหรับ workshop ของ [ThAIKeras](https://thaikeras.com/) ในครั้งนี้ จะเป็นการทดลองใช้ deep learning มาจำแนกหมวดหมู่ของงานศิลปะในพิพิธภัณฑ์ [The Metropolitan Museum of Art](https://www.metmuseum.org/) (หรือ the Met) โดยพิพิธภัณฑ์นี้เป็นพิพิธภัณฑ์ศิลปะที่ใหญ่ที่สุดในอเมริกา และมีผู้เข้าชมเป็นอันดับสามของโลก รองจาก Musée du Louvre และ National Museum of China เลยครับ\n\n<table width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n\t<tr>        \n        <td style=\"width:31%\"><img src=\"https://i.imgur.com/CjpiU69.png\" alt=\"the Met\"></td>\n        <td style=\"width:69%\"><img src=\"https://i.imgur.com/P6sPZRk.png\" alt=\"Arts\"></td>\n    </tr>\n</table>\n\n<center>(the Met และตัวอย่างงานศิลปะอันเลื่องชื่อที่จัดแสดงภายในพิพิธภัณฑ์)</center>"},{"metadata":{},"cell_type":"markdown","source":"ด้วยความที่เป็นพิพิธภัณฑ์ขนาดใหญ่ จึงทำให้มีของสะสมอยู่เป็นจำนวนมาก ประมาณ 1.5 ล้านชิ้น ในจำนวนนี้มีอยู่สองแสนชิ้นที่ได้ทำการถ่ายภาพ และจัดหมวดหมู่เรียบร้อยแล้ว ในการจัดหมวดหมู่ของงานศิลปะ ถ้าสามารถใช้ AI เข้ามาช่วย ก็จะลดเวลาและความผิดพลาดจากการทำงานของมนุษย์ลงได้ จึงเป็นที่มาของการแข่งขัน [iMet Collection 2019](https://www.kaggle.com/c/imet-2019-fgvc6/) ซึ่งเป็นส่วนนึงของ [FGVC6 workshop](https://sites.google.com/view/fgvc6/) ในงาน [CVPR conference](http://cvpr2019.thecvf.com/) ปีนี้\n\n<img src=\"https://i.imgur.com/KtKv0Nr.png\" alt=\"logo\">\n<center>(โลโกของการแข่งขัน iMet Collection 2019)</center>"},{"metadata":{},"cell_type":"markdown","source":"สำหรับ notebook นี้ จะแบ่งเป็นสองส่วนใหญ่ๆ โดยในส่วนแรกเป็นการแนะนำถึงข้อมูลที่ใช้ และทำการจัดเตรียมข้อมูล ก่อนจะไปถึงส่วนที่สอง ซึ่งเราจะสร้างโมเดลเพื่อจำแนกประเภทของงานศิลปะกันครับ\n\n<hr>"},{"metadata":{},"cell_type":"markdown","source":"# Data"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-output":true,"_kg_hide-input":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\nimport random\nfrom tqdm import tqdm\nfrom pathlib import Path\n\nimport cv2\nimport matplotlib.pyplot as plt\n\nfrom sklearn.model_selection import train_test_split\nfrom skmultilearn.model_selection import iterative_train_test_split\n\nimport itertools\nfrom scipy.sparse import lil_matrix, coo_matrix\nfrom collections import defaultdict, Counter\n\nfrom sklearn.metrics import fbeta_score\nfrom albumentations import (OneOf, Compose, HorizontalFlip, RandomCrop, \n                            RandomBrightness, RandomContrast, \n                            ShiftScaleRotate, IAAAdditiveGaussianNoise)\n\nimport keras\nimport keras.backend as K\nfrom keras.models import Model\nfrom keras.optimizers import Adam\nfrom keras.callbacks import Callback, ReduceLROnPlateau, ModelCheckpoint\nfrom keras.layers import Input, GlobalAveragePooling2D, Dense, Dropout, Concatenate, Lambda, Layer","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ในขั้นแรก เราจะมาดูหมวดหมู่ทั้งหมดกันก่อน โดยงานนี้เราจะต้องจำแนกว่าภาพต่างๆ จัดอยู่ในหมวดหมู่ใดได้บ้าง โดยแต่ละภาพก็สามารถจัดอยู่ในหลายหมวดหมู่ได้ ดังนั้นปัญหานี้จึงถือว่าเป็น multi-label classification นอกจากนี้ก็ยังนับว่าเป็น fine-grained visual recognition นั่นคือในการจะแยกแยะเป็นหมวดหมู่ต่างๆ ได้นั้น จำเป็นต้องลงลึกไปถึงรายละเอียดที่อยู่ในรูปภาพด้วย\n\nในงานนี้มีอยู่ทั้งหมด 1103 หมวดหมู่ โดยแบ่งออกเป็นสองส่วน คือ culture ซึ่งบอกว่างานศิลป์ชิ้นนี้มาจากวัฒนธรรมไหน และ tag ที่บอกว่างานศิลป์ชิ้นนี้เกี่ยวข้องกับอะไรบ้าง"},{"metadata":{"trusted":true},"cell_type":"code","source":"DATA_ROOT = Path('../input/imet-2019-fgvc6/')\n\nlabel_df = pd.read_csv(DATA_ROOT/'labels.csv')\nprint(f\"Number of attributes = {len(label_df)}\")\n\nculture_df = label_df[label_df['attribute_name'].str.startswith('culture')]\nprint(f\"Number of cultures = {len(culture_df)}\")\n\ntag_df = label_df[label_df['attribute_name'].str.startswith('tag')]\nprint(f\"Number of tags = {len(tag_df)}\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ไฟล์ labels.csv จะบอกหมวดหมู่ที่มีทั้งหมด หลังจากที่อ่านไฟล์มาแล้ว เราลองดูตัวอย่างหมวดหมู่ต่างๆ กันครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"label_df.sample(n=25).sort_values('attribute_id')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ด้านล่างนี้เป็นการอ่านข้อมูล และแสดงตัวอย่าง ซึ่งจะประกอบด้วยรูปภาพของงานศิลป์ กับหมวดหมู่ของงานศิลป์ชิ้นนั้นๆ"},{"metadata":{"trusted":true},"cell_type":"code","source":"data_df = pd.read_csv(DATA_ROOT/'train.csv')\ndata_df['attribute_ids'] = data_df['attribute_ids'].str.split().map(lambda x_list: [int(x) for x in x_list])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_df = data_df.sample(n=10)\n\nfor _, row in sample_df.iterrows():\n    img = cv2.imread(str(DATA_ROOT / 'train' / (row['id']+'.png')))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    labels = '\\n'.join([label_df.loc[label]['attribute_name'] for label in row['attribute_ids']])\n\n    _, axs = plt.subplots(1, 2, figsize=(12, 4))\n        \n    axs[0].imshow(img)\n    axs[0].title.set_text(row['id'])\n    \n    axs[1].text(0, 0.5, labels, fontsize=12, ha='left', va='center')\n    axs[1].axis('off')\n    \n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ต่อมาเราจะทำการแบ่งข้อมูลทั้งหมด ออกเป็น training data และ validation data โดยควรจะแบ่งให้การกระจายของคลาสมีค่าเหมือนๆ กัน ในข้อมูลทั้งสองชุด และเนื่องจากข้อมูลของเรามีลักษณะเป็น multi label การแบ่งข้อมูลจะใช้วิธี [iterative stratification](http://lpis.csd.auth.gr/publications/sechidis-ecmlpkdd-2011.pdf)\n\nแต่ทว่าเมื่อแบ่งข้อมูลแบบนี้ โดยลองใช้โค้ดของ [scikit-mulitlearn](http://scikit.ml/) หรือ[โค้ดของคุณ Lopuhin](https://github.com/lopuhin/kaggle-imet-2019/blob/master/imet/make_folds.py) แล้วพบว่าการกระจายของคลาสในข้อมูลทั้งสองชุดไม่ค่อยจะเหมือนกัน อย่างไรก็ตาม เมื่อนำไปใช้งาน ค่าความถูกต้องจาก validation data และค่าความถูกต้องของ test data ก็ยังสอดคล้องกันอยู่ครับ"},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"# train_df, valid_df = train_test_split(data_df, test_size=0.2, random_state=42)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"# attributes = data_df['attribute_ids'].tolist()\n\n# row = list(itertools.chain(*[[i]*len(attributes[i]) for i in range(len(data_df))]))\n# col = list(itertools.chain(*attributes))\n\n# y = coo_matrix(([1]*len(row), (row, col)), shape=(len(data_df), len(label_df))).tolil()\n\n# train_df, _, valid_df, _ = iterative_train_test_split(data_df.values, y, test_size=0.2)\n# train_df = pd.DataFrame(train_df, columns=['id', 'attribute_ids'])\n# valid_df = pd.DataFrame(valid_df, columns=['id', 'attribute_ids'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def make_folds(n_folds: int) -> pd.DataFrame:\n    df = pd.read_csv(DATA_ROOT / 'train.csv')\n    cls_counts = Counter(cls for classes in df['attribute_ids'].str.split() for cls in classes)\n    fold_cls_counts = defaultdict(int)\n    folds = [-1] * len(df)\n    for item in tqdm(df.sample(frac=1, random_state=42).itertuples(), total=len(df)):\n        cls = min(item.attribute_ids.split(), key=lambda cls: cls_counts[cls])\n        fold_counts = [(f, fold_cls_counts[f, cls]) for f in range(n_folds)]\n        min_count = min([count for _, count in fold_counts])\n        random.seed(item.Index)\n        fold = random.choice([f for f, count in fold_counts if count == min_count])\n        folds[item.Index] = fold\n        for cls in item.attribute_ids.split():\n            fold_cls_counts[fold, cls] += 1\n    df['fold'] = folds\n    return df\n\nfold_df = make_folds(5)\nfold_df['attribute_ids'] = fold_df['attribute_ids'].str.split().map(lambda x_list: [int(x) for x in x_list])\n\ntrain_df = fold_df[fold_df['fold'] != 0].reset_index(drop=True)\nvalid_df = fold_df[fold_df['fold'] == 0].reset_index(drop=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_attributes = list(itertools.chain(*train_df['attribute_ids'].tolist()))\nprint(\"Total train images: \", len(train_df))\nprint(\"Total train attributes: \", len(train_attributes))\n\nplt.figure(figsize=(12, 3))\nvalues, counts = np.unique(train_attributes, return_counts=True)\nplt.bar(values, counts)\nplt.show()\n\nvalid_attributes = list(itertools.chain(*valid_df['attribute_ids'].tolist()))\nprint(\"Total validation images: \", len(valid_df))\nprint(\"Total validation attributes: \", len(valid_attributes))\n\nplt.figure(figsize=(12, 3))\nvalues, counts = np.unique(valid_attributes, return_counts=True)\nplt.bar(values, counts)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Model"},{"metadata":{},"cell_type":"markdown","source":"ในที่นี้ เราจะนำโมเดล [EfficientNet](https://arxiv.org/abs/1905.11946) ซึ่งเป็น SOTA ทาง computer vision ในปัจจุบันมาใช้งาน โดยเริ่มจากการโหลดโค้ด EfficientNet ที่เป็นเวอร์ชัน Keras มาครับ"},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"!pip install -U git+https://github.com/qubvel/efficientnet","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from efficientnet.keras import EfficientNetB3, preprocess_input","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"EPOCHS = 8\nBATCH_SIZE = 32\n\nINPUT_SHAPE = (288, 288, 3)\nNUM_CLASS = len(label_df)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"รูปภาพที่ส่งเป็น input ให้กับโมเดลจะมีการทำ image augmentation ก่อน สำหรับเรื่อง image augmentation สามารถศึกษาเพิ่มเติมได้จากอีก [workshop ของเรา](https://www.kaggle.com/ratthachat/workshop-augmentation-image-ai-for-eyes-2)"},{"metadata":{"trusted":true},"cell_type":"code","source":"def augment(p=1.0):\n    return Compose([\n        HorizontalFlip(p=0.5),\n#         OneOf([\n#             RandomBrightness(0.1, p=1.0),\n#             RandomContrast(0.1, p=1.0),\n#         ], p=0.3),\n        ShiftScaleRotate(shift_limit=0.1, scale_limit=0.0, rotate_limit=15, p=0.3),\n#         IAAAdditiveGaussianNoise(p=0.3),      \n        RandomCrop(INPUT_SHAPE[0], INPUT_SHAPE[1])\n    ], p=p)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ส่วน input ที่ป้อนให้กับโมเดล จะสร้างด้วย generator ซึ่งในที่นี้ทำโดยใช้คลาสที่เป็น extension ของ `keras.utils.Sequence` และมีฟังก์ชัน `__getitem__` เป็นหลักที่จะส่ง minibatch กลับมา สำหรับรายละเอียดของการสร้าง generator เพิ่มเติม สามารถอ่านได้[ที่นี่](https://stanford.edu/~shervine/blog/keras-how-to-generate-data-on-the-fly)ครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"class DataGenerator(keras.utils.Sequence):\n    def __init__(self, df, batch_size, shuffle=True):\n        self.df = df        \n        self.indices = np.arange(len(self.df))\n        \n        self.batch_size = batch_size        \n        self.shuffle = shuffle\n        \n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n        self.path = DATA_ROOT / 'train'\n\n    def __len__(self):\n        return int(np.ceil(len(self.df)/self.batch_size))\n\n    def __getitem__(self, idx):\n        batch_indices = self.indices[idx*self.batch_size: (idx+1)*self.batch_size]        \n        \n        batch_images = np.zeros((len(batch_indices), *INPUT_SHAPE))\n        batch_labels = np.zeros((len(batch_indices), NUM_CLASS))\n        \n        for i in range(len(batch_indices)):\n            row = self.df.iloc[batch_indices[i]]\n            \n            path = self.path / (row['id']+'.png')\n            img = cv2.cvtColor(cv2.imread(str(path)), cv2.COLOR_BGR2RGB)\n        \n            img = augment()(image=img)['image']\n            batch_images[i] = preprocess_input(img)\n            \n            for label in row['attribute_ids']:\n                batch_labels[i][label] = 1\n                \n        batch_images = np.array(batch_images, np.float32)\n        return batch_images, batch_labels\n    \n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_generator = DataGenerator(train_df, BATCH_SIZE)\nvalid_generator = DataGenerator(valid_df, BATCH_SIZE, shuffle=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ในส่วนนี้จะเป็นการสร้างโมเดล EfficientNet แบบ B3 โดยมี pretrained weight จาก ImageNet และทำ global average pooling ต่อจาก convolutional layer ชั้นสุดท้าย แล้วเพิ่ม dense layer ชั้นนึง ก่อนที่จะเป็นชั้น output ที่ใช้ sigmoid activation จำนวนเท่ากับหมวดหมู่ที่มีครับ"},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"def create_model(input_shape):\n    base_model = EfficientNetB3(weights='imagenet', include_top=False, input_shape=input_shape)\n    input_tensor = base_model.input\n    \n    x = GlobalAveragePooling2D()(base_model.output)\n    x = Dense(512, activation='relu',name='final_features')(x)\n    x = Dropout(0.5)(x)\n    output = Dense(NUM_CLASS, activation='sigmoid')(x)\n\n    model = Model(input_tensor, output)\n    return model\n\nmodel = create_model(INPUT_SHAPE)\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ในการแข่งขันครั้งนี้ วัดผลโดยใช้ [F2 score](https://en.wikipedia.org/wiki/F1_score#Definition) เราจึงสร้างคลาส `F2Evaluation` เป็น callback ของ Keras เพื่อสังเกตการณ์ค่า F2 หลังจากเสร็จสิ้นการรันทุกรอบ\n\nเนื่องจาก output จากโมเดลเป็น probability ของหมวดหมู่ต่างๆ จึงต้องกำหนด threshold ไว้ว่าค่า probability สูงเกินกว่าเท่าไหร่ จึงจะตอบหมวดหมู่นี้ออกมา โดยได้ทดลองใช้ค่า threshold ตั้งแต่ 0.05 ถึง 0.5 ดูว่าค่า threshold ไหน จะให้ความถูกต้องสูงสุด และพบว่าค่า threshold เท่ากับ 0.1 ดีที่สุดครับ\n\nนอกจากนี้ จะใส่เงื่อนไขเพิ่มเติมว่าไม่ให้ผลลัพธ์ออกมาเกิน 10 หมวดหมู่ และอย่างน้อยให้ตอบออกมา 1 หมวดหมู่เสมอ โดยกระบวนการสร้างคำตอบจาก output ของโมเดล จะอยู่ในฟังก์ชัน `binarize_prediction`"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_f2_hist = []\nvalid_f2_hist = []\n\ndef generate_labels(df):\n    labels = np.zeros((len(df), NUM_CLASS))\n\n    for i, row in df.iterrows():\n        for label in row['attribute_ids']:\n            labels[i][label] = 1\n            \n    return labels\n\ndef _make_mask(argsorted, top_n: int):\n    mask = np.zeros_like(argsorted, dtype=np.uint8)\n    col_indices = argsorted[:, -top_n:].reshape(-1)\n    row_indices = [i // top_n for i in range(len(col_indices))]\n    mask[row_indices, col_indices] = 1\n    return mask\n\ndef binarize_prediction(predictions, threshold: float, min_labels=1, max_labels=10):\n    assert predictions.shape[1] == NUM_CLASS\n    argsorted = predictions.argsort(axis=1)\n    max_mask = _make_mask(argsorted, max_labels)\n    min_mask = _make_mask(argsorted, min_labels)\n    prob_mask = predictions > threshold\n    return (max_mask & prob_mask) | min_mask\n\nclass F2Evaluation(Callback):\n    def __init__(self, interval=1):\n        super(Callback, self).__init__()\n\n        self.interval = interval        \n        self.train_generator = DataGenerator(train_df, BATCH_SIZE, shuffle=False)\n        \n        self.train_y = generate_labels(train_df)\n        self.valid_y = generate_labels(valid_df)        \n        \n    def predict(self, generator, y_true):\n        predictions = self.model.predict_generator(generator, verbose=1)\n        \n        best_threshold = 0.0\n        best_score = 0.0\n        \n        for threshold in np.arange(0.05, 0.55, 0.05):\n            #y_pred = np.where(predictions > threshold, 1, 0)\n            y_pred = binarize_prediction(predictions, threshold)\n            \n            f2_score = fbeta_score(y_true, y_pred, beta=2, average='samples')\n            \n            if f2_score > best_score:\n                best_score = f2_score\n                best_threshold = threshold\n            \n        return best_score, best_threshold        \n\n    def on_epoch_end(self, epoch, logs={}):\n        if epoch % self.interval != 0:\n            return\n        \n        train_f2_score, train_threshold = self.predict(self.train_generator, self.train_y)\n        valid_f2_score, valid_threshold = self.predict(valid_generator, self.valid_y)\n        \n        train_f2_hist.append(train_f2_score)             \n        print(\"train f2 = %.4f (threshold = %.2f)\" % (train_f2_score, train_threshold))        \n\n        valid_f2_hist.append(valid_f2_score)             \n        print(\"valid f2 = %.4f (threshold = %.2f)\" % (valid_f2_score, valid_threshold))\n\n        if valid_f2_score >= max(valid_f2_hist):\n            print('save checkpoint: ', valid_f2_score)\n            self.model.save_weights('model_bestf2.h5')\n\nf2_metric = F2Evaluation(interval=1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เมื่อถึงตอนนี้ก็จะเริ่มเทรนโมเดลได้ โดยให้ loss เป็น `binary_crossentropy` ซึ่งเหมาะกับปัญหา multi-label classification ส่วน optimizer ก็ใช้เป็น [Adam](https://arxiv.org/abs/1412.6980) ปกติครับ"},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"model.compile(loss='binary_crossentropy', optimizer=Adam(3e-4))\n\nhist = model.fit_generator(train_generator, \n                           validation_data=valid_generator, \n                           epochs=EPOCHS, verbose=1,\n                           callbacks=[f2_metric], \n                           use_multiprocessing=True, workers=2)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(range(1, EPOCHS+1), hist.history['loss'], label='train_loss')\nplt.plot(range(1, EPOCHS+1), hist.history['val_loss'], label='valid_loss')\nplt.legend()\nplt.ylabel('loss')\nplt.xlabel('epoch')\nplt.show()\n\nplt.plot(range(1, EPOCHS+1), train_f2_hist, label='train_f2')\nplt.plot(range(1, EPOCHS+1), valid_f2_hist, label='valid_f2')\nplt.legend()\nplt.ylabel('f2')\nplt.xlabel('epoch')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เมื่อเทรนเสร็จแล้ว เราจะมาลองทดสอบโมเดลที่ได้ โดยมีการทำ tta (test-time augmentation) คือทำการทำนายหลายครั้ง ด้วย augmentation ที่แตกต่างกัน แล้วเอาผลลัพธ์ที่ได้มาเฉลี่ย วิธีนี้จะทำให้มีความถูกต้องสูงขึ้น"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.load_weights('model_bestf2.h5')\npredictions = np.zeros((len(valid_df), NUM_CLASS))\n\nfor _ in range(4):\n    predictions += model.predict_generator(valid_generator, verbose=1)\npredictions /= 4\n\ny_true = generate_labels(valid_df)\ny_pred = binarize_prediction(predictions, 0.1)\n\nvalid_f2_score = fbeta_score(y_true, y_pred, beta=2, average='samples')\nprint(\"valid tta f2 = %.4f (threshold = 0.10)\" % valid_f2_score)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"สุดท้ายนี้ เรามาดูตัวอย่างภาพใน validation set พร้อมหมวดหมู่จริงเปรียบเทียบกับหมวดหมู่ที่โมเดลทำนายได้กันครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_df = valid_df.sample(n=10)\n\nfor i, row in sample_df.iterrows():\n    img = cv2.imread(str(DATA_ROOT / 'train' / (row['id']+'.png')))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    \n    plt.title(row['id'])\n    plt.imshow(img)\n    plt.show()\n\n    true_labels = ', '.join([label_df.loc[label]['attribute_name'] \n                             for label in row['attribute_ids']])\n    \n    pred_labels = ', '.join([label_df.loc[label]['attribute_name'] \n                             for label in np.where(y_pred[i]==1)[0]])\n    \n    print(\"True labels = \" + true_labels)\n    print(\"Predicted labels = \" + pred_labels)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"<hr>\n\nจะเห็นว่าปัญหานี้มีความยากอยู่พอสมควร ในการทายหมวดหมู่ให้ถูกต้องตรงกับที่ผู้เชี่ยวชาญกำหนดมา ทั้งนี้เป็นเพราะหลายสาเหตุด้วยกัน อาทิเช่น\n* บางหมวดหมู่ค่อนข้างจะเฉพาะเจาะจง และมีตัวอย่างใน training data น้อย\n* ส่วน culture ในบางภาพอาจไม่สามารถดูออกได้อย่างชัดเจน และส่วน tag ในบางภาพต้องสังเกตรายละเอียดเล็กๆ น้อยๆ ให้ดี จึงจะเห็นว่าเป็นหมวดหมู่นั้น\n* บางครั้งคำตอบที่โมเดลทำนายมาก็ถือว่าถูกต้องแล้ว แต่ใน label จริงตกหล่นไป (การแข่งขันครั้งนี้วัดผลด้วย F2 score ที่เน้น recall มากกว่า ถ้าโมเดลตอบเกินมาจะดีกว่าตอบขาดไปครับ)\n\nโค้ดนี้เป็นเพียงการสาธิตเบื้องต้น ยังสามารถปรับปรุงให้ความถูกต้องสูงขึ้นได้อีก ท่านที่สนใจอาจศึกษาเพิ่มเติมจากวิธีการของผู้เข้าแข่งขันที่ได้อันดับสูง ในหน้า [discussion](https://www.kaggle.com/c/imet-2019-fgvc6/discussion) หรือจาก[เปเปอร์ที่เกี่ยวข้อง](https://paperswithcode.com/task/fine-grained-image-classification)"},{"metadata":{},"cell_type":"markdown","source":"สำหรับการแข่งขัน iMet Collection 2019 นี้ ทาง ThAIKeras ก็ได้เข้าร่วม และสามารถคว้าเหรียญเงินมาครอง ในเวลานั้นเราได้ใช้[โค้ด](https://www.kaggle.com/lopuhin/imet-2019-submission)ชั้นดีของคุณ [Konstantin Lopuhin](https://www.kaggle.com/lopuhin) เป็นจุดตั้งต้น ซึ่งตอนนี้คุณ Lopuhin ก็ได้เป็น grandmaster แล้ว ต้องขอขอบคุณและแสดงความยินดีไว้ ณ ที่นี้ด้วยครับ\n\nท่านผู้อ่านสามารถนำ notebook นี้ ไปรันเล่นหรือแก้ไขเพิ่มเติมได้ หากพบข้อผิดพลาดหรือมีข้อสงสัยตรงไหน สามารถบอกมาได้ที่ด้านล่างนี้ หรือที่[เว็บบอร์ด](https://thaikeras.com/community/)ของ ThAIKeras ก็ได้ครับ"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":1}