{"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.11.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":1322517,"sourceType":"datasetVersion","datasetId":689329},{"sourceId":1324366,"sourceType":"datasetVersion","datasetId":762176}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":null,"end_time":null,"environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-09-17T17:14:58.459677","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Thư viện","metadata":{"papermill":{"duration":0.005148,"end_time":"2025-09-17T17:15:03.258543","exception":false,"start_time":"2025-09-17T17:15:03.253395","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"## Cài đặt thư viện efficienet","metadata":{}},{"cell_type":"code","source":"!pip install -q efficientnet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-26T13:51:56.29293Z","iopub.execute_input":"2025-11-26T13:51:56.29334Z","iopub.status.idle":"2025-11-26T13:52:02.314651Z","shell.execute_reply.started":"2025-11-26T13:51:56.293316Z","shell.execute_reply":"2025-11-26T13:52:02.313374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd, numpy as np\nfrom kaggle_datasets import KaggleDatasets\nimport tensorflow as tf, re, math\nfrom tensorflow.keras import Model\nimport tensorflow.keras.backend as K\nimport efficientnet.tfkeras as efn\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import roc_auc_score\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tensorflow.keras.layers import (Dropout,\n                                     Conv2D,\n                                     BatchNormalization,\n                                     Dense,\n                                     GlobalAveragePooling2D,\n                                     Input,\n                                     Activation,\n                                     Lambda,\n                                     multiply)\n\nsns.set_style(\"darkgrid\")","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2025-11-26T13:52:02.31604Z","iopub.execute_input":"2025-11-26T13:52:02.316351Z","iopub.status.idle":"2025-11-26T13:52:20.921372Z","shell.execute_reply.started":"2025-11-26T13:52:02.316322Z","shell.execute_reply":"2025-11-26T13:52:20.920522Z"},"papermill":{"duration":23.044413,"end_time":"2025-09-17T17:15:26.307537","exception":false,"start_time":"2025-09-17T17:15:03.263124","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cấu hình","metadata":{"papermill":{"duration":0.004107,"end_time":"2025-09-17T17:15:26.316463","exception":false,"start_time":"2025-09-17T17:15:26.312356","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Thiết bị huấn luyện: \"TPU\" hoặc \"GPU\"\nDEVICE = \"TPU\"\n\n# Seed để đảm bảo kết quả tái lập, nên thay đổi cho từng Stratified KFold\nSEED = 42\n\n# Số lượng K-Folds (thường dùng 3, 5 hoặc 15)\nFOLDS = 5\n\n# Kích thước ảnh cho từng Fold (chọn trong {128, 192, 256, 384, 512, 768})\nIMG_SIZES = [384] * FOLDS  # ở đây dùng 384 cho tất cả các Fold\n\n# Có bao gồm dữ liệu từ các cuộc thi cũ hay không (1 = Có, 0 = Không)\nINC2019 = [1] * FOLDS  # bao gồm dữ liệu năm 2019\nINC2018 = [0] * FOLDS  # không dùng dữ liệu năm 2018\n\n# Batch size và số epochs cho mỗi Fold\nBATCH_SIZES = [32] * FOLDS\nEPOCHS = 30\n\n# Chọn phiên bản EfficientNet (B0-B7), ở đây dùng B6 cho tất cả\nEFF_NETS = [6] * FOLDS\n\n# Trọng số cho mỗi Fold khi ensemble dự đoán test\nWGTS = [1 / FOLDS] * FOLDS\n\n# Số bước Test Time Augmentation (TTA)\nTTA = 11\n","metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","execution":{"iopub.status.busy":"2025-11-26T13:52:20.923715Z","iopub.execute_input":"2025-11-26T13:52:20.92626Z","iopub.status.idle":"2025-11-26T13:52:20.932108Z","shell.execute_reply.started":"2025-11-26T13:52:20.926226Z","shell.execute_reply":"2025-11-26T13:52:20.931205Z"},"papermill":{"duration":0.012239,"end_time":"2025-09-17T17:15:26.3328","exception":false,"start_time":"2025-09-17T17:15:26.320561","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if DEVICE == \"TPU\":\n    print(\"connecting to TPU...\")\n    try:\n        tpu = tf.distribute.cluster_resolver.TPUClusterResolver()\n        print('Running on TPU ', tpu.master())\n    except ValueError:\n        print(\"Could not connect to TPU\")\n        tpu = None\n\n    if tpu:\n        try:\n            print(\"initializing  TPU ...\")\n            tf.config.experimental_connect_to_cluster(tpu)\n            tf.tpu.experimental.initialize_tpu_system(tpu)\n            strategy = tf.distribute.experimental.TPUStrategy(tpu)\n            print(\"TPU initialized\")\n        except _:\n            print(\"failed to initialize TPU\")\n    else:\n        DEVICE = \"GPU\"\n\nif DEVICE != \"TPU\":\n    print(\"Using default strategy for CPU and single GPU\")\n    strategy = tf.distribute.get_strategy()\n\nif DEVICE == \"GPU\":\n    print(\"Num GPUs Available: \", len(tf.config.experimental.list_physical_devices('GPU')))\n    \n\nAUTO     = tf.data.experimental.AUTOTUNE\nREPLICAS = strategy.num_replicas_in_sync\nprint(f'REPLICAS: {REPLICAS}')","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:20.933122Z","iopub.execute_input":"2025-11-26T13:52:20.933731Z","iopub.status.idle":"2025-11-26T13:52:20.969055Z","shell.execute_reply.started":"2025-11-26T13:52:20.933693Z","shell.execute_reply":"2025-11-26T13:52:20.967883Z"},"papermill":{"duration":0.745121,"end_time":"2025-09-17T17:15:27.082296","exception":false,"start_time":"2025-09-17T17:15:26.337175","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Chuấn bị dữ liệu","metadata":{"papermill":{"duration":0.004297,"end_time":"2025-09-17T17:15:27.091345","exception":false,"start_time":"2025-09-17T17:15:27.087048","status":"completed"},"tags":[]}},{"cell_type":"code","source":"GCS_PATH = [None]*FOLDS; GCS_PATH2 = [None]*FOLDS\nfor i,k in enumerate(IMG_SIZES):\n    GCS_PATH[i] = KaggleDatasets().get_gcs_path('melanoma-%ix%i'%(k,k))\n    GCS_PATH2[i] = KaggleDatasets().get_gcs_path('isic2019-%ix%i'%(k,k))\nfiles_train = np.sort(np.array(tf.io.gfile.glob(GCS_PATH[0] + '/train*.tfrec')))\nfiles_test  = np.sort(np.array(tf.io.gfile.glob(GCS_PATH[0] + '/test*.tfrec')))","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:20.9703Z","iopub.execute_input":"2025-11-26T13:52:20.97061Z","iopub.status.idle":"2025-11-26T13:52:23.783128Z","shell.execute_reply.started":"2025-11-26T13:52:20.970586Z","shell.execute_reply":"2025-11-26T13:52:23.782294Z"},"papermill":{"duration":2.393023,"end_time":"2025-09-17T17:15:29.48975","exception":false,"start_time":"2025-09-17T17:15:27.096727","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Tăng cường dữ liệu","metadata":{"papermill":{"duration":0.004516,"end_time":"2025-09-17T17:15:29.499219","exception":false,"start_time":"2025-09-17T17:15:29.494703","status":"completed"},"tags":[]}},{"cell_type":"code","source":"ROT_ = 180.0\nSHR_ = 2.0\nHZOOM_ = 8.0\nWZOOM_ = 8.0\nHSHIFT_ = 8.0\nWSHIFT_ = 8.0","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.784083Z","iopub.execute_input":"2025-11-26T13:52:23.784382Z","iopub.status.idle":"2025-11-26T13:52:23.789728Z","shell.execute_reply.started":"2025-11-26T13:52:23.784348Z","shell.execute_reply":"2025-11-26T13:52:23.7885Z"},"papermill":{"duration":0.011288,"end_time":"2025-09-17T17:15:29.515069","exception":false,"start_time":"2025-09-17T17:15:29.503781","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_mat(rotation, shear, height_zoom, width_zoom, height_shift, width_shift):\n    # Trả về ma trận biến đổi 3x3 (homogeneous) để biến đổi chỉ số pixel\n        \n    # Đổi đơn vị độ (degree) sang radian\n    rotation = math.pi * rotation / 180.\n    shear    = math.pi * shear    / 180.\n\n    def get_3x3_mat(lst):\n        # Gộp danh sách phần tử thành tensor 3x3\n        return tf.reshape(tf.concat([lst],axis=0), [3,3])\n    \n    # Ma trận QUAY (rotation)\n    c1   = tf.math.cos(rotation)\n    s1   = tf.math.sin(rotation)\n    one  = tf.constant([1],dtype='float32')\n    zero = tf.constant([0],dtype='float32')\n    \n    rotation_matrix = get_3x3_mat([c1,   s1,   zero, \n                                   -s1,  c1,   zero, \n                                   zero, zero, one])    \n    \n    # Ma trận XIÊN (shear)\n    c2 = tf.math.cos(shear)\n    s2 = tf.math.sin(shear)    \n    \n    shear_matrix = get_3x3_mat([one,  s2,   zero, \n                                zero, c2,   zero, \n                                zero, zero, one])        \n    # Ma trận THU PHÓNG (zoom) theo trục cao (height) và rộng (width)\n    zoom_matrix = get_3x3_mat([one/height_zoom, zero,           zero, \n                               zero,            one/width_zoom, zero, \n                               zero,            zero,           one])    \n    \n    # Ma trận TỊNH TIẾN (shift) theo trục cao/rộng\n    shift_matrix = get_3x3_mat([one,  zero, height_shift, \n                                zero, one,  width_shift, \n                                zero, zero, one])\n    \n    # Nhân chuỗi ma trận theo thứ tự: rotation → shear → zoom → shift\n    return K.dot(K.dot(rotation_matrix, shear_matrix), \n                 K.dot(zoom_matrix,     shift_matrix))\n\n\ndef transform(image, DIM=256):    \n    # input: 1 ảnh dạng [DIM, DIM, 3] (không phải batch)\n    # output: ảnh sau khi xoay/xiên/zoom/tịnh tiến ngẫu nhiên\n    XDIM = DIM%2 # xử lý đặc biệt cho kích thước lẻ (vd 331)\n    \n    # Lấy tham số biến đổi ngẫu nhiên (phân phối chuẩn)\n    rot = ROT_ * tf.random.normal([1], dtype='float32')\n    shr = SHR_ * tf.random.normal([1], dtype='float32') \n    h_zoom = 1.0 + tf.random.normal([1], dtype='float32') / HZOOM_\n    w_zoom = 1.0 + tf.random.normal([1], dtype='float32') / WZOOM_\n    h_shift = HSHIFT_ * tf.random.normal([1], dtype='float32') \n    w_shift = WSHIFT_ * tf.random.normal([1], dtype='float32') \n\n    # Tạo MA TRẬN BIẾN ĐỔI tổng hợp\n    m = get_mat(rot,shr,h_zoom,w_zoom,h_shift,w_shift) \n\n    # Liệt kê tọa độ pixel ĐÍCH (destination) trong hệ toạ độ ảnh\n    x   = tf.repeat(tf.range(DIM//2, -DIM//2,-1), DIM)\n    y   = tf.tile(tf.range(-DIM//2, DIM//2), [DIM])\n    z   = tf.ones([DIM*DIM], dtype='int32')\n    idx = tf.stack( [x,y,z] )\n    \n    # Ánh xạ từ điểm ĐÍCH về điểm NGUỒN qua ma trận m\n    idx2 = K.dot(m, tf.cast(idx, dtype='float32'))\n    idx2 = K.cast(idx2, dtype='int32')\n    idx2 = K.clip(idx2, -DIM//2+XDIM+1, DIM//2)  # giới hạn trong khung ảnh\n    \n    # Lấy giá trị pixel NGUỒN tương ứng (gather theo chỉ số)\n    idx3 = tf.stack([DIM//2-idx2[0,], DIM//2-1+idx2[1,]])\n    d    = tf.gather_nd(image, tf.transpose(idx3))\n        \n    # Trả về ảnh đã biến đổi, reshape về [DIM, DIM, 3]\n    return tf.reshape(d,[DIM, DIM,3])\n","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.790848Z","iopub.execute_input":"2025-11-26T13:52:23.791113Z","iopub.status.idle":"2025-11-26T13:52:23.833217Z","shell.execute_reply.started":"2025-11-26T13:52:23.791091Z","shell.execute_reply":"2025-11-26T13:52:23.832292Z"},"papermill":{"duration":0.018556,"end_time":"2025-09-17T17:15:29.538489","exception":false,"start_time":"2025-09-17T17:15:29.519933","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_labeled_tfrecord(example):\n    # Định nghĩa schema cho TFRecord có nhãn (các trường và kiểu dữ liệu)\n    tfrec_format = {\n        'image'                        : tf.io.FixedLenFeature([], tf.string),  # ảnh dạng bytes (JPEG)\n        'image_name'                   : tf.io.FixedLenFeature([], tf.string),  # tên ảnh\n        'patient_id'                   : tf.io.FixedLenFeature([], tf.int64),   # id bệnh nhân\n        'sex'                          : tf.io.FixedLenFeature([], tf.int64),   # giới tính (mã hoá số)\n        'age_approx'                   : tf.io.FixedLenFeature([], tf.int64),   # tuổi xấp xỉ\n        'anatom_site_general_challenge': tf.io.FixedLenFeature([], tf.int64),   # vị trí giải phẫu (mã hoá)\n        'diagnosis'                    : tf.io.FixedLenFeature([], tf.int64),   # chẩn đoán (mã hoá)\n        'target'                       : tf.io.FixedLenFeature([], tf.int64)    # nhãn mục tiêu (0/1)\n    }           \n    # Parse 1 example theo schema trên\n    example = tf.io.parse_single_example(example, tfrec_format)\n    # Trả về bytes ảnh và nhãn target\n    return example['image'], example['target']\n\n\ndef read_unlabeled_tfrecord(example, return_image_name):\n    # Định nghĩa schema cho TFRecord không có nhãn (chỉ cần ảnh và tên ảnh)\n    tfrec_format = {\n        'image'                        : tf.io.FixedLenFeature([], tf.string),  # ảnh bytes\n        'image_name'                   : tf.io.FixedLenFeature([], tf.string),  # tên ảnh\n    }\n    # Parse 1 example\n    example = tf.io.parse_single_example(example, tfrec_format)\n    # Trả về (image_bytes, image_name) nếu cần, ngược lại trả về 0 cho tên ảnh\n    return example['image'], example['image_name'] if return_image_name else 0\n\n \ndef prepare_image(img, augment=True, dim=256):    \n    # Giải mã JPEG -> tensor ảnh RGB\n    img = tf.image.decode_jpeg(img, channels=3)\n    # Chuyển sang float32 và chuẩn hoá về [0,1]\n    img = tf.cast(img, tf.float32) / 255.0\n    \n    if augment:\n        # Biến đổi hình học tùy chỉnh (xoay/xiên/zoom/dịch)\n        img = transform(img, DIM=dim)\n        # Lật ngang ngẫu nhiên\n        img = tf.image.random_flip_left_right(img)\n        # Tăng cường màu sắc/độ tương phản/độ sáng ngẫu nhiên (mức nhẹ)\n        # img = tf.image.random_hue(img, 0.01)  # tuỳ chọn: thay đổi hue rất nhỏ\n        img = tf.image.random_saturation(img, 0.7, 1.3)\n        img = tf.image.random_contrast(img, 0.8, 1.2)\n        img = tf.image.random_brightness(img, 0.1)\n                      \n    # Đảm bảo shape cố định [dim, dim, 3]\n    img = tf.reshape(img, [dim,dim, 3])\n            \n    # Trả về ảnh đã tiền xử lý\n    return img\n\ndef count_data_items(filenames):\n    # Trích số lượng mẫu từ tên file theo pattern \"-{số}.\"\n    # Ví dụ: \"train-230.tfrec\" -> lấy 230, sau đó cộng tất cả lại\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(filename).group(1)) \n         for filename in filenames]\n    # Tổng số mẫu ước lượng trong danh sách file\n    return np.sum(n)","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.834135Z","iopub.execute_input":"2025-11-26T13:52:23.834379Z","iopub.status.idle":"2025-11-26T13:52:23.853374Z","shell.execute_reply.started":"2025-11-26T13:52:23.834359Z","shell.execute_reply":"2025-11-26T13:52:23.852457Z"},"papermill":{"duration":0.015482,"end_time":"2025-09-17T17:15:29.558697","exception":false,"start_time":"2025-09-17T17:15:29.543215","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_dataset(files, augment = False, shuffle = False, repeat = False, \n                labeled=True, return_image_names=True, batch_size=16, dim=256):\n    \n    ds = tf.data.TFRecordDataset(files, num_parallel_reads=AUTO)\n    ds = ds.cache()\n    \n    if repeat:\n        ds = ds.repeat()\n    \n    if shuffle: \n        ds = ds.shuffle(1024*8)\n        opt = tf.data.Options()\n        opt.experimental_deterministic = False\n        ds = ds.with_options(opt)\n        \n    if labeled: \n        ds = ds.map(read_labeled_tfrecord, num_parallel_calls=AUTO)\n    else:\n        ds = ds.map(lambda example: read_unlabeled_tfrecord(example, return_image_names), \n                    num_parallel_calls=AUTO)      \n    \n    ds = ds.map(lambda img, imgname_or_label: (prepare_image(img, augment=augment, dim=dim), \n                                               imgname_or_label), \n                num_parallel_calls=AUTO)\n    \n    ds = ds.batch(batch_size * REPLICAS)\n    ds = ds.prefetch(AUTO)\n    return ds","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.855832Z","iopub.execute_input":"2025-11-26T13:52:23.856139Z","iopub.status.idle":"2025-11-26T13:52:23.880334Z","shell.execute_reply.started":"2025-11-26T13:52:23.856085Z","shell.execute_reply":"2025-11-26T13:52:23.879246Z"},"papermill":{"duration":0.013678,"end_time":"2025-09-17T17:15:29.577318","exception":false,"start_time":"2025-09-17T17:15:29.56364","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Xây dựng mô hình với cơ chế Attention ","metadata":{"papermill":{"duration":0.004188,"end_time":"2025-09-17T17:15:29.586225","exception":false,"start_time":"2025-09-17T17:15:29.582037","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Danh sách class EfficientNet (B0..B6) từ package efficientnet.tfkeras (đã import là efn)\nEFNS = [efn.EfficientNetB0, efn.EfficientNetB1, efn.EfficientNetB2, efn.EfficientNetB3, \n        efn.EfficientNetB4, efn.EfficientNetB5, efn.EfficientNetB6]\n\n# Import TensorFlow/Keras cần thiết\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\n# from tensorflow.keras.applications import EfficientNetB6  # dùng backbone B6 từ tf.keras.applications\nfrom tensorflow.keras.applications import EfficientNetB3\n\ndef build_model(dim=384):\n    # Input ảnh: H = W = dim, 3 kênh RGB\n    inp = layers.Input((dim, dim, 3))\n\n    # Khởi tạo EfficientNetB6 pretrained ImageNet, bỏ phần top (FC của ImageNet), dùng inp làm input\n    # LƯU Ý: nếu lỗi 'drop_connect_rate' thì bỏ tham số đó (tuỳ phiên bản Keras)\n    base = EfficientNetB3(\n        weights=\"imagenet\",     # tải trọng số ImageNet\n        include_top=False,\n        input_tensor=inp\n        # drop_connect_rate=0.3  # bật nếu phiên bản Keras hỗ trợ\n    )\n    # Chuẩn hoá đặc trưng từ backbone để ổn định phân phối\n    x = layers.BatchNormalization()(base.output)\n\n    # ----- Spatial attention: tạo mặt nạ chú ý theo không gian -----\n    # Chuỗi conv 1x1 giảm kênh dần với kích hoạt swish\n    att = layers.Conv2D(64, 1, activation='swish')(x)\n    att = layers.Conv2D(16, 1, activation='swish')(att)\n    att = layers.Conv2D(8, 1, activation='swish')(att)\n    # Ra 1 kênh duy nhất với sigmoid => mask ∈ [0,1], kích thước [B,H,W,1]\n    att = layers.Conv2D(1, 1, activation='sigmoid', name=\"att_mask\")(att)\n\n    # Broadcast: nhân bản mask theo trục kênh để khớp với số kênh C của x\n    c = x.shape[-1]\n    att_b = layers.Lambda(lambda m: tf.repeat(m, c, axis=-1), name=\"broadcast\")(att)\n\n    # Áp mask: nhân element-wise để làm nổi bật vùng quan trọng\n    masked = layers.Multiply()([x, att_b])\n    # GAP của đặc trưng đã mask (tổng có trọng số attention)\n    gap_f = layers.GlobalAveragePooling2D()(masked)\n    # GAP của chính mask (độ phủ/chuẩn hoá)\n    gap_m = layers.GlobalAveragePooling2D()(att)\n    # Chuẩn hoá: weighted GAP / coverage (tránh lệch theo kích thước vùng)\n    gap = layers.Lambda(lambda t: t[0] / (t[1] + 1e-6), name=\"RescaleGAP\")([gap_f, gap_m])\n\n    # Head phân loại: Dropout -> Dense 128 ReLU -> Dropout -> Dense 1 sigmoid\n    d = layers.Dropout(0.5)(gap)\n    d = layers.Dense(128, activation='relu')(d)\n    d = layers.Dropout(0.2)(d)\n    out = layers.Dense(1, activation='sigmoid')(d)\n\n    # Tạo model từ inp đến out\n    model = models.Model(inp, out)\n    # Compile với Adam, BinaryCrossentropy (có label smoothing) và AUC làm metric\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(1e-3),\n        loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=0.05),\n        metrics=[tf.keras.metrics.AUC(name=\"auc\")]\n    )\n    return model","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.882075Z","iopub.execute_input":"2025-11-26T13:52:23.882396Z","iopub.status.idle":"2025-11-26T13:52:23.912647Z","shell.execute_reply.started":"2025-11-26T13:52:23.882373Z","shell.execute_reply":"2025-11-26T13:52:23.911666Z"},"papermill":{"duration":0.018903,"end_time":"2025-09-17T17:15:29.609749","exception":false,"start_time":"2025-09-17T17:15:29.590846","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train Schedule","metadata":{"papermill":{"duration":0.004711,"end_time":"2025-09-17T17:15:29.619283","exception":false,"start_time":"2025-09-17T17:15:29.614572","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def get_lr_callback(batch_size=8):\n    lr_start   = 0.000005\n    lr_max     = 0.00000125 * REPLICAS * batch_size\n    lr_min     = 0.000001\n    lr_ramp_ep = 5\n    lr_sus_ep  = 0\n    lr_decay   = 0.8\n   \n    def lrfn(epoch):\n        if epoch < lr_ramp_ep:\n            lr = (lr_max - lr_start) / lr_ramp_ep * epoch + lr_start\n            \n        elif epoch < lr_ramp_ep + lr_sus_ep:\n            lr = lr_max\n            \n        else:\n            lr = (lr_max - lr_min) * lr_decay**(epoch - lr_ramp_ep - lr_sus_ep) + lr_min\n            \n        return lr\n\n    lr_callback = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose=False)\n    return lr_callback","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.913758Z","iopub.execute_input":"2025-11-26T13:52:23.914064Z","iopub.status.idle":"2025-11-26T13:52:23.93291Z","shell.execute_reply.started":"2025-11-26T13:52:23.914041Z","shell.execute_reply":"2025-11-26T13:52:23.931722Z"},"papermill":{"duration":0.012613,"end_time":"2025-09-17T17:15:29.63642","exception":false,"start_time":"2025-09-17T17:15:29.623807","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Huấn luyện mô hình","metadata":{"papermill":{"duration":0.004508,"end_time":"2025-09-17T17:15:29.645564","exception":false,"start_time":"2025-09-17T17:15:29.641056","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Cell 0 — chạy TRƯỚC khi import tensorflow, sau đó Restart\nimport os\nos.environ[\"KERAS_BACKEND\"] = \"tensorflow\"                 # chọn backend Keras là TensorFlow\nos.environ[\"TF_XLA_FLAGS\"] = \"--tf_xla_enable_xla_devices=false\"  # tắt XLA devices để tránh JIT không mong muốn\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"                   # giảm mức log của TF (ẩn thông báo INFO)\nos.environ[\"TF_FORCE_GPU_ALLOW_GROWTH\"] = \"true\"            # cho phép TF cấp phát VRAM theo nhu cầu (không chiếm toàn bộ)\n\nimport tensorflow as tf\ntf.config.optimizer.set_jit(False)  # đảm bảo không bật JIT compile (XLA)\n\n# Tham số hiển thị log huấn luyện/biểu đồ\n# VERBOSE=0: im lặng; 1: từng batch; 2: từng epoch\nVERBOSE = 0\nDISPLAY_PLOT = True\n\n# KFold chia dữ liệu thành FOLDS phần, xáo trộn với SEED\nskf = KFold(n_splits = FOLDS,shuffle = True,random_state = SEED)\n\n# Các mảng lưu OOF predictions/targets/giá trị tốt nhất/ tên ảnh / fold index\noof_pred = []; oof_tar = []; oof_val = []; oof_names = []; oof_folds = [] \n\n# Mảng lưu dự đoán test (tổng hợp qua các fold)\npreds = np.zeros((count_data_items(files_test),1))\n\n# Vòng lặp qua từng fold\nfor fold,(idxT,idxV) in enumerate(skf.split(np.arange(15))):\n    \n    # Thông tin fold & khởi tạo TPU nếu cần\n    if DEVICE=='TPU':\n        if tpu: tf.tpu.experimental.initialize_tpu_system(tpu)\n    print('#'*25); print('#### FOLD',fold+1)\n    print('#### Image Size %i with EfficientNet B%i and batch_size %i'%\n          (IMG_SIZES[fold],EFF_NETS[fold],BATCH_SIZES[fold]*REPLICAS))\n    \n    # Tạo danh sách file TFRecord train/valid theo index của fold\n    files_train = tf.io.gfile.glob([GCS_PATH[fold] + '/train%.2i*.tfrec'%x for x in idxT])\n    if INC2019[fold]:\n        files_train += tf.io.gfile.glob([GCS_PATH2[fold] + '/train%.2i*.tfrec'%x for x in idxT*2+1])\n        print('#### Using 2019 external data')\n    if INC2018[fold]:\n        files_train += tf.io.gfile.glob([GCS_PATH2[fold] + '/train%.2i*.tfrec'%x for x in idxT*2])\n        print('#### Using 2018+2017 external data')\n    np.random.shuffle(files_train); print('#'*25)\n    files_valid = tf.io.gfile.glob([GCS_PATH[fold] + '/train%.2i*.tfrec'%x for x in idxV])\n    files_test = np.sort(np.array(tf.io.gfile.glob(GCS_PATH[fold] + '/test*.tfrec')))\n    \n    # Xây dựng model trong scope của strategy (TPU/GPU/CPU)\n    K.clear_session()\n    with strategy.scope():\n        model = build_model(dim=IMG_SIZES[fold])\n        \n    # Callback lưu trọng số tốt nhất theo val_auc (chỉ lưu weights)\n    # sv = tf.keras.callbacks.ModelCheckpoint(\n    #     'fold-%i.h5'%fold, monitor='val_loss', verbose=0, save_best_only=True,\n    #     save_weights_only=True, mode='min', save_freq='epoch')\n\n    sv = tf.keras.callbacks.ModelCheckpoint(\n        filepath=f'fold-{fold}.weights.h5',   # <- đổi đuôi\n        monitor='val_auc',                    # gợi ý: lưu theo AUC\n        mode='max',\n        save_best_only=True,\n        save_weights_only=True,\n        verbose=0\n    )\n\n    # Tính số bước mỗi epoch dựa trên số mẫu train và batch_size (có nhân REPLICAS)\n    n_train = count_data_items(files_train)\n    steps_per_epoch = int(np.ceil(n_train / (BATCH_SIZES[fold] * REPLICAS)))\n\n    # HUẤN LUYỆN\n    print('Training...')\n    history = model.fit(\n        get_dataset(\n            files_train,\n            augment = True, \n            shuffle = True, \n            repeat = True,\n            dim = IMG_SIZES[fold],\n            batch_size = BATCH_SIZES[fold]\n        ), \n        epochs = EPOCHS[fold],\n        callbacks = [sv,get_lr_callback(BATCH_SIZES[fold])],  # callback lưu model + lịch học LR\n        # steps_per_epoch = count_data_items(files_train) / BATCH_SIZES[fold]//REPLICAS,\n        steps_per_epoch=steps_per_epoch,          # số step/epoch cố định\n        \n        # Tập validation (không augment, không shuffle, không repeat)\n        validation_data = get_dataset(\n            files_valid,augment = False,\n            shuffle = False,\n            repeat = False,\n            dim = IMG_SIZES[fold]\n        ), # class_weight = {0:1,1:2},\n        verbose = VERBOSE\n    )\n    \n    print('Loading best model...')\n    # model.load_weights('fold-%i.h5'%fold)\n    model.load_weights(f'fold-{fold}.weights.h5')  # <- đổi đuôi cho khớp\n\n    \n    # DỰ ĐOÁN OOF VỚI TTA (nhiều lần tăng cường ở test-time)\n    print('Predicting OOF with TTA...')\n    ds_valid = get_dataset(files_valid,labeled=False,return_image_names=False,augment=True,\n            repeat=True,shuffle=False,dim=IMG_SIZES[fold],batch_size=BATCH_SIZES[fold]*4)\n    # ct_valid = count_data_items(files_valid); STEPS = TTA * ct_valid/BATCH_SIZES[fold]/4/REPLICAS\n    # pred = model.predict(ds_valid,steps=STEPS,verbose=VERBOSE)[:TTA*ct_valid,] \n\n    ct_valid = count_data_items(files_valid)\n    STEPS = int(np.ceil(TTA * ct_valid / (BATCH_SIZES[fold] * 4 * REPLICAS)))\n    pred = model.predict(ds_valid, steps=STEPS, verbose=VERBOSE)[:TTA*ct_valid,]\n\n    # Trung bình theo TTA để ra dự đoán OOF cho từng mẫu valid\n    oof_pred.append( np.mean(pred.reshape((ct_valid,TTA),order='F'),axis=1) )                 \n    # oof_pred.append(model.predict(get_dataset(files_valid,dim=IMG_SIZES[fold]),verbose=1))\n    \n    # LẤY NHÃN OOF VÀ TÊN ẢNH (để chấm AUC và lưu báo cáo)\n    ds_valid = get_dataset(files_valid, augment=False, repeat=False, dim=IMG_SIZES[fold],\n            labeled=True, return_image_names=True)\n    oof_tar.append( np.array([target.numpy() for img, target in iter(ds_valid.unbatch())]) )\n    oof_folds.append( np.ones_like(oof_tar[-1],dtype='int8')*fold )\n    ds = get_dataset(files_valid, augment=False, repeat=False, dim=IMG_SIZES[fold],\n                labeled=False, return_image_names=True)\n    oof_names.append( np.array([img_name.numpy().decode(\"utf-8\") for img, img_name in iter(ds.unbatch())]))\n    \n    # DỰ ĐOÁN TEST VỚI TTA\n    print('Predicting Test with TTA...')\n    ds_test = get_dataset(files_test,labeled=False,return_image_names=False,augment=True,\n            repeat=True,shuffle=False,dim=IMG_SIZES[fold],batch_size=BATCH_SIZES[fold]*4)\n\n    ct_test = count_data_items(files_test)\n    STEPS = int(np.ceil(TTA * ct_test / (BATCH_SIZES[fold] * 4 * REPLICAS)))\n    pred = model.predict(ds_test, steps=STEPS, verbose=VERBOSE)[:TTA*ct_test,]\n\n    # Cộng dồn dự đoán test (trung bình theo TTA, rồi cộng theo trọng số fold)\n    preds[:,0] += np.mean(pred.reshape((ct_test,TTA),order='F'),axis=1) * WGTS[fold]\n    \n    # BÁO CÁO KẾT QUẢ CHO FOLD (AUC tốt nhất trong train, AUC OOF)\n    auc = roc_auc_score(oof_tar[-1],oof_pred[-1])\n    oof_val.append(np.max( history.history['val_auc'] ))\n    print('#### FOLD %i OOF AUC without TTA = %.3f, with TTA = %.3f'%(fold+1,oof_val[-1],auc))\n    \n    # VẼ BIỂU ĐỒ HỌC (AUC/Loss train & val) nếu bật DISPLAY_PLOT\n    if DISPLAY_PLOT:\n        plt.figure(figsize=(15,5))\n        plt.plot(np.arange(EPOCHS[fold]),history.history['auc'],'-o',label='Train AUC',color='#ff7f0e')\n        plt.plot(np.arange(EPOCHS[fold]),history.history['val_auc'],'-o',label='Val AUC',color='#1f77b4')\n        x = np.argmax( history.history['val_auc'] ); y = np.max( history.history['val_auc'] )\n        xdist = plt.xlim()[1] - plt.xlim()[0]; ydist = plt.ylim()[1] - plt.ylim()[0]\n        plt.scatter(x,y,s=200,color='#1f77b4'); plt.text(x-0.03*xdist,y-0.13*ydist,'max auc\\n%.2f'%y,size=14)\n        plt.ylabel('AUC',size=14); plt.xlabel('Epoch',size=14)\n        plt.legend(loc=2)\n        plt2 = plt.gca().twinx()\n        plt2.plot(np.arange(EPOCHS[fold]),history.history['loss'],'-o',label='Train Loss',color='#2ca02c')\n        plt2.plot(np.arange(EPOCHS[fold]),history.history['val_loss'],'-o',label='Val Loss',color='#d62728')\n        x = np.argmin( history.history['val_loss'] ); y = np.min( history.history['val_loss'] )\n        ydist = plt.ylim()[1] - plt.ylim()[0]\n        plt.scatter(x,y,s=200,color='#d62728'); plt.text(x-0.03*xdist,y+0.05*ydist,'min loss',size=14)\n        plt.ylabel('Loss',size=14)\n        plt.title('FOLD %i - Image Size %i, EfficientNet B%i, inc2019=%i, inc2018=%i'%\n                (fold+1,IMG_SIZES[fold],EFF_NETS[fold],INC2019[fold],INC2018[fold]),size=18)\n        plt.legend(loc=3)\n        plt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-26T13:52:23.933957Z","iopub.execute_input":"2025-11-26T13:52:23.9343Z"},"papermill":{"duration":null,"end_time":null,"exception":false,"start_time":"2025-09-17T17:15:29.650136","status":"running"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Tính toán giá trị OOF AUC","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"cell_type":"code","source":"# COMPUTE OVERALL OOF AUC\noof = np.concatenate(oof_pred); true = np.concatenate(oof_tar);\nnames = np.concatenate(oof_names); folds = np.concatenate(oof_folds)\nauc = roc_auc_score(true,oof)\nprint('Overall OOF AUC with TTA = %.3f'%auc)\n\n# SAVE OOF TO DISK\ndf_oof = pd.DataFrame(dict(\n    image_name = names, target=true, pred = oof, fold=folds))\ndf_oof.to_csv('oof.csv',index=False)\ndf_oof.head()","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Hậu xử lý","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"cell_type":"code","source":"ds = get_dataset(files_test, augment=False, repeat=False, dim=IMG_SIZES[fold],\n                 labeled=False, return_image_names=True)\n\nimage_names = np.array([img_name.numpy().decode(\"utf-8\") \n                        for img, img_name in iter(ds.unbatch())])","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Lưu dữ liệu","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"cell_type":"code","source":"submission = pd.DataFrame(dict(image_name=image_names, target=preds[:,0]))\nsubmission = submission.sort_values('image_name') \nsubmission.to_csv('submission.csv', index=False)\nsubmission.head()","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.hist(submission.target,bins=100)\nplt.show()","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}