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

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

ในชุดข้อมูลประกอบด้วย ตัวอย่างเคสแผลผิวหนังที่เม็ดสีผิดปกติ แบบต่าง ๆ ได้แก่ 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 คืออะไร

เนื่องจากจำนวนข้อมูลตัวอย่าง แต่ละ 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 คืออะไร
เรามาเริ่มกันเลยดีกว่า
โรคมะเร็งผิวหนัง นับเป็นปัญหาใหญ่ในทางสาธารณสุข ทุก ๆ ปี ในประเทศสหรัฐอเมริกา เราจะพบผู้ป่วยใหม่ มากกว่า 5 ล้านราย มะเร็วผิวหนัง Melanoma เป็นมะเร็งผิวหนังชนิดที่ร้ายแรงที่สุด เป็นมะเร็วผิวหนังชนิดที่คร่าชีวิตคนมากที่สุด ในปี 2015 ทั่วโลก มีการตรวจพบ Melanoma มากกว่า 350,000 เคส โดยมีผู้ป่วยเสียชีวิต 60,000 คน ถึงแม้อัตราการเสียชีวิตจะสูง แต่ถ้ามีการตรวจพบตั้งแต่ระยะเริ่มต้น เราสามารถเพิ่มอัตราการรอดชีวิต ได้มากกว่า 95%
0. Magic Commands¶
ให้ใส่ไว้บนสุดทุก Jupyter Notebook เป็นการสั่งให้ Notebook ก่อนรัน ให้รีโหลด Library ภายนอกที่เรา import ไว้ใหม่โดยอัตโนมัติ
และให้พล็อตกราฟ matplotlib ใน Output ของ cell แบบ code ได้เลย
%reload_ext autoreload
%autoreload 2
%matplotlib inline
! nvidia-smi
1. Install & Import Library¶
ติดตั้ง fastai2 หรือ fastai version 2 และ Import Library ที่เราจะใช้
เราจะต้อง Install kaggle เพื่อ Download Dataset
# 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
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 จะได้ผลลัพธ์ที่เหมือนกันทุกครั้ง จะได้สะดวกในการเปรียบเทียบ
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
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"
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
# !kaggle datasets download {dataset} -p "{data_path}" --unzip
ดูว่าแตก zip มาได้ไฟล์อะไรบ้าง
data_path.ls()
# (data_path/"ham10000_images_part_1").ls()
ย้ายข้อมูลรูปภาพไปไว้ Folder เดียวกัน จะได้สะดวก
# ! mv {(data_path/"ham10000_images_part_2")}/* {(data_path/"ham10000_images_part_1")}
4. Data¶
อ่านไฟล์ CSV ขึ้นมา ดูข้อมูลเพิ่มเติม เช่น Label
csv_file = data_path/"HAM10000_metadata.csv"
Label อยู่ใน Column dx เป็นตัวย่อ
df = pd.read_csv(csv_file)
df.head()
สร้าง Dict ตัวย่อ เป็น คำแปล ชื่อหมวดหมู่ของโรค
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
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)
นับจำนวนข้อมูลแต่ละ Class
df.lesion.value_counts()
fig, ax1 = plt.subplots(1, 1, figsize= (10, 5))
df['lesion'].value_counts().plot(kind='bar', ax=ax1)
5. Data Pipeline¶
สร้าง Data Pipeline ในการแปลงข้อมูลก่อน Feed เข้าสู่ Model
กำหนดขนาด Batch Size
bs = 32
e = 2
Transform แต่ละรูป ด้วยการ สุ่มย่อ แล้ว Crop ให้ได้ขนาดเท่ากันทุกรูป เพื่อจัดเข้า Batch
item_tfms = RandomResizedCrop(420, min_scale=0.7, ratio=(1., 1.))
ใช้ Data Echoing, ทำ Data Augmentation ทำ Transform ทีละ Batch ด้วย GPU แล้ว Normalize
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
batch_tfms = [EchoingTransform(e), *aug_transforms(size=128, max_rotate=180., flip_vert=True), Normalize()]
สร้าง DataBlock กำหนดค่าต่าง ๆ ใน Data Pipeline
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)
# cancer.summary(df)
สร้าง DataLoaders จาก DataBlock
dls = cancer.dataloaders(df, bs=bs)
แสดงตัวอย่าง ข้อมูล 1 Batch
dls.show_batch(max_n=9, figsize=(11, 12))
ดูใน vocab Dictionary มี 7 Class
dls.vocab
6. Model¶
Focal Loss¶
เนื่องจากจำนวนข้อมูล แต่ละ Class แตกต่างกันมาก เรียกว่า Class Imbalance เราจะใช้ Loss Function ที่แก้ปัญหานี้ เรียกว่ FocalLoss
เราจะสร้าง class ใหม่มาห่อ focal.FocalLoss เนื่องจาก fastai ต้องการ method activation และ decodes ในการเรียกใช้ show_results, plot_top_losses ด้านล่าง
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)
# focal.FocalLoss??
kwargs = {"alpha": 0.5, "gamma": 2.0, "reduction": 'mean'}
loss_func = CustomFocalLoss(**kwargs)
ResNet50¶
ใช้ Ranger Optimizer ซึ่งเป็น การผสมกันระหว่าง RAdam และ LookAhead จะอธิบายต่อไป
# ranger??
opt_func = ranger
สำหรับ metrics เราจะใช้ Error Rate และ F1 Score
# F1Score??
# RocAuc??
metrics = [error_rate, F1Score(average='micro')]
เราจะใช้โมเดล Architecture ResNet50 เวอร์ชันพิเศษของ fastai ชื่อ XResNet50 ที่ถูก Pre-trained กับ ImageNet มาเรียบร้อยแล้ว มาทำ Transfer Learning แล้วเทรนแบบ Mixed Precision
arch = resnet50
# arch = xresnet50
learn = cnn_learner(dls, arch, pretrained=True,
loss_func=loss_func,
opt_func=opt_func,
cbs=[ShowGraphCallback],
metrics=metrics).to_fp16()
# learn.summary()
7. Train¶
Fine-Tune¶
เรียก Fine-tune ให้ Learner เทรนแบบ Head 1 Epoch แล้ว Unfreeze เทรนต่อทั้งโมเดล
learn.fine_tune(6)
learn.save("fine-tune1")
เพียงสิบกว่านาที เราได้ Error Rate เท่ากับ 0.15 หรือ Accuracy เท่ากับ 85%
8. ดูผลลัพธ์¶
ถ้าเราดูแค่ Metrics Error Rate อย่างเดียว ว่ากี่เปอร์เซ็นต์ เราอาจจะไม่เห็นภาพว่า Model ทำงานได้ผลลัพธ์อย่างไร เราควรดูข้อมูลจริง รูปจริง Label จริง ด้วย ว่าโมเดล Predict อะไรออกมา
learn.show_results(max_n=9, figsize=(11, 12))