โรคมะเร็งผิวหนัง นับเป็นปัญหาใหญ่ในทางสาธารณสุข ทุก ๆ ปี ในประเทศสหรัฐอเมริกา เราจะพบผู้ป่วยใหม่ มากกว่า 5 ล้านราย

มะเร็งผิวหนัง Melanoma เป็นมะเร็งผิวหนังชนิดที่ร้ายแรงที่สุด เป็นมะเร็งผิวหนังชนิดที่คร่าชีวิตคนมากที่สุด ในปี 2015 ทั่วโลก มีการตรวจพบ Melanoma มากกว่า 350,000 เคส โดยมีผู้ป่วยเสียชีวิต 60,000 คน ถึงแม้อัตราการเสียชีวิตจะสูง แต่ถ้ามีการวินิจฉัยโรคมะเร็งผิวหนังที่ง่ายขึ้น ตรวจพบตั้งแต่ระยะเริ่มต้น และรักษาได้อย่างทันท่วงที เราจะสามารถเพิ่มอัตราการรอดชีวิต ได้มากกว่า 95%

ใน ep นี้ เราจะมาสร้าง AI โมเดล Deep Learning ที่จะวินัจฉัยโรคมะเร็งผิวหนัง ด้วยการจำแนกรูปถ่ายผิวพรรณ ที่มีความผิดปกติของเม็ดสี ว่าเป็นโรคอะไรใน 7 โรคที่กำหนด ด้วยความแม่นยำ 94%

HAM10000 Dataset

dermatoscopic images pigmented-skin https://www.kaggle.com/kmader/skin-cancer-mnist-ham10000
dermatoscopic images pigmented-skin https://www.kaggle.com/kmader/skin-cancer-mnist-ham10000

HAM10000 (“Human Against Machine with 10000 training images”) ชุดข้อมูลรูปผิวหนัง จากประชากรกลุ่มต่าง ๆ ประกอบด้วย 10,015 รูป สำหรับเป็น Training Set ในการสร้างโมเดล Machine Learning เพื่อการศึกษาและวิจัย

Number of examples  Melanocytic nevi 6705 Melanoma  1113 Benign keratosis 1099 Basal cell carcinoma 514 Actinic keratoses 327 Vascular lesions  142 Dermatofibroma 115
Number of examples Melanocytic nevi 6705 Melanoma 1113 Benign keratosis 1099 Basal cell carcinoma 514 Actinic keratoses 327 Vascular lesions 142 Dermatofibroma 115

ในชุดข้อมูลประกอบด้วย ตัวอย่างเคสแผลผิวหนังที่เม็ดสีผิดปกติ แบบต่าง ๆ ได้แก่ Actinic keratoses and intraepithelial carcinoma / Bowen’s disease (akiec), basal cell carcinoma (bcc), benign keratosis-like lesions (solar lentigines / seborrheic keratoses and lichen-planus like keratoses, bkl), dermatofibroma (df), melanoma (mel), melanocytic nevi (nv) and vascular lesions (angiomas, angiokeratomas, pyogenic granulomas and hemorrhage, vasc).

รายละเอียดเพิ่มเติม ใน ep ที่แล้ว AI จำแนกปัญหาผิวพรรณ

Focal Loss คืออะไร

Focal Loss for Dense Object Detection. Credit https://arxiv.org/abs/1708.02002
Focal Loss for Dense Object Detection. Credit https://arxiv.org/abs/1708.02002

เนื่องจากจำนวนข้อมูลตัวอย่าง แต่ละ Class แตกต่างกันมาก เรียกว่า Class Imbalance แทนที่เราจะใช้ Cross Entropy Loss ตามปกติที่เรามักจะใช้ในงาน Classification ในเคสนี้เราจะเปลี่ยนไปใช้ Loss Function พิเศษ ที่ออกแบบมาเพื่อแก้ปัญหานี้ เรียกว่า Focal Loss ดังสมการด้านล่าง

\(\text{FL}(p_t) = -\alpha_t (1 – p_t)^{\gamma} \, \text{log}(p_t)\)

รายละเอียดเพิ่มเติม Focal Loss คืออะไร

เรามาเริ่มกันเลยดีกว่า

Open In Colab

โรคมะเร็งผิวหนัง นับเป็นปัญหาใหญ่ในทางสาธารณสุข ทุก ๆ ปี ในประเทศสหรัฐอเมริกา เราจะพบผู้ป่วยใหม่ มากกว่า 5 ล้านราย มะเร็วผิวหนัง Melanoma เป็นมะเร็งผิวหนังชนิดที่ร้ายแรงที่สุด เป็นมะเร็วผิวหนังชนิดที่คร่าชีวิตคนมากที่สุด ในปี 2015 ทั่วโลก มีการตรวจพบ Melanoma มากกว่า 350,000 เคส โดยมีผู้ป่วยเสียชีวิต 60,000 คน ถึงแม้อัตราการเสียชีวิตจะสูง แต่ถ้ามีการตรวจพบตั้งแต่ระยะเริ่มต้น เราสามารถเพิ่มอัตราการรอดชีวิต ได้มากกว่า 95%

0. Magic Commands

ให้ใส่ไว้บนสุดทุก Jupyter Notebook เป็นการสั่งให้ Notebook ก่อนรัน ให้รีโหลด Library ภายนอกที่เรา import ไว้ใหม่โดยอัตโนมัติ

และให้พล็อตกราฟ matplotlib ใน Output ของ cell แบบ code ได้เลย

In [0]:
%reload_ext autoreload
%autoreload 2
%matplotlib inline
In [4]:
! nvidia-smi
Thu May 21 10:59:30 2020       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.82       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla P100-PCIE...  Off  | 00000000:00:04.0 Off |                    0 |
| N/A   43C    P0    27W / 250W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

1. Install & Import Library

ติดตั้ง fastai2 หรือ fastai version 2 และ Import Library ที่เราจะใช้

เราจะต้อง Install kaggle เพื่อ Download Dataset

In [0]:
# Colab

! pip install kaggle --upgrade -q
! pip install fastai2 -q
! pip install kornia -q

Import 3 Package ย่อย คือ basics, vision.all, callback.all

การ import * หมายความว่า import ทุกอย่างที่อยู่ใน package ทำให้เราไม่ได้ต้องมา import ทีละ class การ import แบบนี้ เหมาะสำหรับการทดลองอะไรใหม่ ๆ เพราะเราไม่ต้องย้อนมาแก้ import ทุกครั้งเมื่อต้องการใช้ class ใหม่ ๆ แต่ไม่แนะนำสำหรับใช้งานจริงบน Production

In [0]:
from fastai2.basics import *
from fastai2.vision.all import *
from fastai2.callback.all import *
from fastai2.callback.cutmix import CutMix

from kornia.losses import focal

เราจะกำหนด Random Seed จะได้ผลลัพธ์ที่เหมือนกันทุกครั้ง จะได้สะดวกในการเปรียบเทียบ

In [0]:
np.random.seed(42)

2. เตรียม Path สำหรับดาวน์โหลดข้อมูล

กำหนด path ของ Config File และ Dataset ว่าจะอยู่ใน Google Drive ถ้าเราใช้ Google Colab หรือ อยู่ใน HOME ถ้าเราใช้ VM ธรรมดา และกำหนด Environment Variable ไปยังโฟลเดอร์ที่เก็บ kaggle.json

ในกรณีใช้ Colab ให้ Mount Google Drive เพื่อดึง Config File มาจาก Google Drive ส่วนตัวของเรา เมื่อเรารัน Cell ด้านล่างจะมีลิงค์ปรากฎขึ้นมาให้เรา Login กด Approve แล้ว Copy Authorization Code มาใส่ในช่องด้านล่าง แล้วกด Enter

In [8]:
dataset = 'kmader/skin-cancer-mnist-ham10000'

# Google Colab
config_path = Path('/content/drive')
data_path_base = Path('/content/datasets/')

data_path = data_path_base/dataset

from google.colab import drive
drive.mount(str(config_path))
os.environ['KAGGLE_CONFIG_DIR'] = f"{config_path}/My Drive/.kaggle"

# # VM
# config_path = Path(os.getenv("HOME"))
# data_path = config_path/"datasets"/dataset

# data_path.mkdir(parents=True, exist_ok=True)
# os.environ['KAGGLE_CONFIG_DIR'] = f"{config_path}/.kaggle"
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).

3. Dataset

ในเคสนี้ เราจะ Download ข้อมูล Dataset ที่เกี่ยวข้องทั้งหมดมาเก็บไว้ แบ่งเป็นรูปถ่ายโรคมะเร็งผิวหนังต่าง ๆ

Dataset เราจะดึงจาก Kaggle วิธี Download kaggle.json ให้ดูจาก ep ที่แล้ว

เมื่อได้ kaggle.json มาแล้ว ในกรณีใช้ Google Colab ให้นำมาใส่ไว้ในโฟลเดอร์ My Drive/.kaggle ใน Google Drive ของเรา เป็น My Drive/.kaggle/kaggle.json ถ้าใช้ VM ให้ใส่ใน HOME/.kaggle/

สั่งดาวน์โหลด Dataset จาก Kaggle พร้อมทั้ง unzip ไว้ใน data_path

In [0]:
# !kaggle datasets download {dataset} -p "{data_path}" --unzip 

ดูว่าแตก zip มาได้ไฟล์อะไรบ้าง

In [10]:
data_path.ls()
Out[10]:
(#9) [Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/HAM10000_images_part_2'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/ham10000_images_part_2'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/hmnist_28_28_L.csv'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/hmnist_8_8_L.csv'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/HAM10000_images_part_1'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/hmnist_28_28_RGB.csv'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/HAM10000_metadata.csv'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/hmnist_8_8_RGB.csv'),Path('/content/datasets/kmader/skin-cancer-mnist-ham10000/ham10000_images_part_1')]
In [0]:
# (data_path/"ham10000_images_part_1").ls()

ย้ายข้อมูลรูปภาพไปไว้ Folder เดียวกัน จะได้สะดวก

In [0]:
# ! mv {(data_path/"ham10000_images_part_2")}/* {(data_path/"ham10000_images_part_1")}

4. Data

อ่านไฟล์ CSV ขึ้นมา ดูข้อมูลเพิ่มเติม เช่น Label

In [0]:
csv_file = data_path/"HAM10000_metadata.csv"

Label อยู่ใน Column dx เป็นตัวย่อ

In [14]:
df = pd.read_csv(csv_file)
df.head()
Out[14]:
lesion_idimage_iddxdx_typeagesexlocalization
0HAM_0000118ISIC_0027419bklhisto80.0malescalp
1HAM_0000118ISIC_0025030bklhisto80.0malescalp
2HAM_0002730ISIC_0026769bklhisto80.0malescalp
3HAM_0002730ISIC_0025661bklhisto80.0malescalp
4HAM_0001466ISIC_0031633bklhisto75.0maleear

สร้าง Dict ตัวย่อ เป็น คำแปล ชื่อหมวดหมู่ของโรค

In [0]:
lesion_type_dict = {
    'nv': 'Melanocytic nevi',
    'mel': 'Melanoma',
    'bkl': 'Benign keratosis ',
    'bcc': 'Basal cell carcinoma',
    'akiec': 'Actinic keratoses',
    'vasc': 'Vascular lesions',
    'df': 'Dermatofibroma'
}

เอามา Map ลง Dataframe เพื่อใช้เป็น Label

In [16]:
df.dx = df.dx.astype('category', copy=True)
df['labels'] = df.dx.cat.codes 
df['lesion'] = df.dx.map(lesion_type_dict)
df.sample(5)
Out[16]:
lesion_idimage_iddxdx_typeagesexlocalizationlabelslesion
1617HAM_0007180ISIC_0033272melhisto65.0maleface4Melanoma
8128HAM_0007195ISIC_0031923nvhisto40.0femalelower extremity5Melanocytic nevi
2168HAM_0001835ISIC_0026652melhisto65.0maleback4Melanoma
1090HAM_0000465ISIC_0030583bklconsensus35.0femaletrunk2Benign keratosis
7754HAM_0001720ISIC_0034010nvhisto45.0maleabdomen5Melanocytic nevi

นับจำนวนข้อมูลแต่ละ Class

In [17]:
df.lesion.value_counts()
Out[17]:
Melanocytic nevi        6705
Melanoma                1113
Benign keratosis        1099
Basal cell carcinoma     514
Actinic keratoses        327
Vascular lesions         142
Dermatofibroma           115
Name: lesion, dtype: int64
In [18]:
fig, ax1 = plt.subplots(1, 1, figsize= (10, 5))
df['lesion'].value_counts().plot(kind='bar', ax=ax1)
Out[18]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f345420b4e0>

5. Data Pipeline

สร้าง Data Pipeline ในการแปลงข้อมูลก่อน Feed เข้าสู่ Model

กำหนดขนาด Batch Size

In [0]:
bs = 32
e = 2

Transform แต่ละรูป ด้วยการ สุ่มย่อ แล้ว Crop ให้ได้ขนาดเท่ากันทุกรูป เพื่อจัดเข้า Batch

In [0]:
item_tfms = RandomResizedCrop(420, min_scale=0.7, ratio=(1., 1.))

ใช้ Data Echoing, ทำ Data Augmentation ทำ Transform ทีละ Batch ด้วย GPU แล้ว Normalize

In [0]:
class EchoingTransform(ItemTransform):
    order = 2
    split_idx = 0
    def __init__(self, e): self.e = e
    def encodes(self, x):
        img, lbl = x
        img = img.repeat(self.e, 1, 1, 1)
        lbl = lbl.repeat(self.e)
        # print(img.shape)
        # print(lbl.shape)
        return img, lbl
In [0]:
batch_tfms = [EchoingTransform(e), *aug_transforms(size=128, max_rotate=180., flip_vert=True), Normalize()]

สร้าง DataBlock กำหนดค่าต่าง ๆ ใน Data Pipeline

In [0]:
cancer = DataBlock(blocks=(ImageBlock, CategoryBlock), 
                 get_x=ColReader('image_id', pref=f'{data_path}/ham10000_images_part_1/', suff='.jpg'), 
                 get_y=ColReader('lesion'), 
                 splitter=RandomSplitter(), 
                 item_tfms=item_tfms, 
                 batch_tfms=batch_tfms)
In [0]:
# cancer.summary(df)

สร้าง DataLoaders จาก DataBlock

In [25]:
dls = cancer.dataloaders(df, bs=bs)
/usr/local/lib/python3.6/dist-packages/torch/nn/functional.py:2854: UserWarning: The default behavior for interpolate/upsample with float scale_factor will change in 1.6.0 to align with other frameworks/libraries, and use scale_factor directly, instead of relying on the computed output size. If you wish to keep the old behavior, please set recompute_scale_factor=True. See the documentation of nn.Upsample for details. 
  warnings.warn("The default behavior for interpolate/upsample with float scale_factor will change "

แสดงตัวอย่าง ข้อมูล 1 Batch

In [26]:
dls.show_batch(max_n=9, figsize=(11, 12))
/usr/local/lib/python3.6/dist-packages/torch/nn/functional.py:2854: UserWarning: The default behavior for interpolate/upsample with float scale_factor will change in 1.6.0 to align with other frameworks/libraries, and use scale_factor directly, instead of relying on the computed output size. If you wish to keep the old behavior, please set recompute_scale_factor=True. See the documentation of nn.Upsample for details. 
  warnings.warn("The default behavior for interpolate/upsample with float scale_factor will change "

ดูใน vocab Dictionary มี 7 Class

In [27]:
dls.vocab
Out[27]:
(#7) ['Actinic keratoses','Basal cell carcinoma','Benign keratosis ','Dermatofibroma','Melanocytic nevi','Melanoma','Vascular lesions']
In [0]:
 

6. Model

Focal Loss

เนื่องจากจำนวนข้อมูล แต่ละ Class แตกต่างกันมาก เรียกว่า Class Imbalance เราจะใช้ Loss Function ที่แก้ปัญหานี้ เรียกว่ FocalLoss

$$\text{FL}(p_t) = -\alpha_t (1 - p_t)^{\gamma} \, \text{log}(p_t)$$

เราจะสร้าง class ใหม่มาห่อ focal.FocalLoss เนื่องจาก fastai ต้องการ method activation และ decodes ในการเรียกใช้ show_results, plot_top_losses ด้านล่าง

In [0]:
class CustomFocalLoss(focal.FocalLoss):
    def __init__(self, alpha: float, gamma: float = 2.0,
                 reduction: str = 'none') -> None:
        super(CustomFocalLoss, self).__init__(alpha, gamma, reduction)

    def activation(self, out): return F.softmax(out, dim=-1)
    def decodes(self, out):    return out.argmax(dim=-1)
In [0]:
# focal.FocalLoss??
In [0]:
kwargs = {"alpha": 0.5, "gamma": 2.0, "reduction": 'mean'}
loss_func = CustomFocalLoss(**kwargs)

ResNet50

ใช้ Ranger Optimizer ซึ่งเป็น การผสมกันระหว่าง RAdam และ LookAhead จะอธิบายต่อไป

In [0]:
# ranger??
In [0]:
opt_func = ranger

สำหรับ metrics เราจะใช้ Error Rate และ F1 Score

In [0]:
# F1Score??
# RocAuc??
In [0]:
metrics = [error_rate, F1Score(average='micro')]

เราจะใช้โมเดล Architecture ResNet50 เวอร์ชันพิเศษของ fastai ชื่อ XResNet50 ที่ถูก Pre-trained กับ ImageNet มาเรียบร้อยแล้ว มาทำ Transfer Learning แล้วเทรนแบบ Mixed Precision

In [0]:
arch = resnet50
# arch = xresnet50
In [0]:
learn = cnn_learner(dls, arch, pretrained=True, 
                    loss_func=loss_func, 
                    opt_func=opt_func, 
                    cbs=[ShowGraphCallback], 
                    metrics=metrics).to_fp16()
In [0]:
# learn.summary()

7. Train

Fine-Tune

เรียก Fine-tune ให้ Learner เทรนแบบ Head 1 Epoch แล้ว Unfreeze เทรนต่อทั้งโมเดล

In [36]:
learn.fine_tune(6)
epochtrain_lossvalid_losserror_ratef1_scoretime
00.7200420.4006830.3210180.67898202:10
/usr/local/lib/python3.6/dist-packages/torch/nn/functional.py:2854: UserWarning: The default behavior for interpolate/upsample with float scale_factor will change in 1.6.0 to align with other frameworks/libraries, and use scale_factor directly, instead of relying on the computed output size. If you wish to keep the old behavior, please set recompute_scale_factor=True. See the documentation of nn.Upsample for details. 
  warnings.warn("The default behavior for interpolate/upsample with float scale_factor will change "
epochtrain_lossvalid_losserror_ratef1_scoretime
00.4218450.2605270.2506240.74937602:14
10.2699980.1532180.1977030.80229702:14
20.1984130.1344820.1897150.81028502:15
30.1471500.1176870.1697450.83025502:15
40.1169830.1102070.1572640.84273602:15
50.1068360.1055670.1562660.84373402:14
In [0]:
learn.save("fine-tune1")

เพียงสิบกว่านาที เราได้ Error Rate เท่ากับ 0.15 หรือ Accuracy เท่ากับ 85%

8. ดูผลลัพธ์

ถ้าเราดูแค่ Metrics Error Rate อย่างเดียว ว่ากี่เปอร์เซ็นต์ เราอาจจะไม่เห็นภาพว่า Model ทำงานได้ผลลัพธ์อย่างไร เราควรดูข้อมูลจริง รูปจริง Label จริง ด้วย ว่าโมเดล Predict อะไรออกมา

In [38]:
learn.show_results(max_n=9, figsize=(11, 12))
/usr/local/lib/python3.6/dist-packages/torch/nn/functional.py:2854: UserWarning: The default behavior for interpolate/upsample with float scale_factor will change in 1.6.0 to align with other frameworks/libraries, and use scale_factor directly, instead of relying on the computed output size. If you wish to keep the old behavior, please set recompute_scale_factor=True. See the documentation of nn.Upsample for details. 
  warnings.warn("The default behavior for interpolate/upsample with float scale_factor will change "