Pneumonia หรือ โรคปอดอักเสบเรื้อรัง ปอดบวม เป็นโรคที่พบได้ประมาณร้อยละ 8-10 ของผู้ป่วยที่มีการติดเชื้อเฉียบพลันระบบหายใจ นับเป็นสาเหตุการตายอันดับหนึ่งของโรคติดเชื้อในเด็กอายุต่ำกว่า 5 ปี เกิดจากสาเหตุหลัก 2 กลุ่ม คือ ปอดอักเสบที่เกิดจากการติดเชื้อและปอดอักเสบที่ไม่ได้เกิดจากการติดเชื้อ ถ้าเราสามารถพัฒนาระบบ AI ช่วยวินิจฉัยโรคเบื้องต้น จำแนกชนิดของโรค Pneumonia จะมีประโยชน์ในการวินิจฉัย และดูแลรักษาตั้งแต่แรก
Pneumonia คืออะไร

ปอดอักเสบจากการติดเชื้อ หรือ pneumonia (ปอดบวม) เป็นโรคที่เกิดจากการอักเสบของเนื้อปอดบริเวณหลอดลมฝอยส่วนปลาย (terminal และ respiratory bronchiole) ถุงลม (alveoli) และเนื้อเยื่อรอบถุงลม (interstitium เป็นชนิดของปอดอักเสบที่พบได้บ่อยที่สุด โดยเชื้อโรคที่เข้าสู่ปอดและทำให้เกิดการอักเสบของถุงลมปอดและเนื้อเยื่อโดยรอบ ได้แก่ เชื้อไวรัส เชื้อแบคทีเรีย และเชื้อรา

ซึ่งเชื้อที่พบจะแตกต่างกันในแต่ละกลุ่มอายุ และสภาพแวดล้อมที่เกิดโรค เป็นโรคที่พบได้ประมาณร้อยละ 8-10 ของผู้ป่วยที่มีการติดเชื้อเฉียบพลันระบบหายใจ นับเป็นสาเหตุการตายอันดับหนึ่งของโรคติดเชื้อในเด็กอายุต่ำกว่า 5 ปี เช่น ได้รับเชื้อจากที่ชุมชนทั่วไป หรือจากภายในโรงพยาบาล

ทั้งนี้ เชื้อแบคทีเรียที่พบมักได้แก่ เชื้อ Streptococcus pneumoniae, เชื้อ Haemophilus influenzae type b, เชื้อ Chlamydia pneumoniae, เชื้อ Legionella spp. และเชื้อ Mycoplasma pneumoniae ส่วนเชื้อไวรัส ได้แก่ เชื้อ Respiratory Syncytial Virus (RSV), เชื้อ Influenza หรือเชื้อไข้หวัดใหญ่ และเชื้อราจากมูลนกหรือซากพืชซากสัตว์

วิธีการติดต่อ จากการสำลักเชื้อที่สะสมรวมกลุ่มอยู่บริเวณทางเดินหายใจส่วนบน (upper airway colonization) เชื้อแบคทีเรียส่วนใหญ่ทำให้เกิดปอดอักเสบในชุมชนและปอดอักเสบในโรงพยาบาลจากการสำลักเชื้อที่สะสมรวมกันอยู่บริเวณหลอดคอ (oropharyngeal aspiration) ลงไปสู่เนื้อปอด เช่นสำลักน้ำลาย อาหาร หรือสารคัดหลั่งในทางเดินอาหาร หากในระยะนั้นผู้ป่วยมีร่างกายอ่อนแอ มีการติดเชื้อของทางเดินหายใจส่วนบน เป็นผู้สูงอายุ หรือมีโรคเรื้อรังทางอายุรกรรมร่วมด้วยก็จะทำให้เกิดปอดอักเสบได้

หรือการหายใจนำเชื้อเข้าสู่ปอดโดยตรง การสูดหายใจเอาเชื้อที่อยู่ในอากาศในรูปละอองฝอยขนาดเล็ก (droplet nuclei) เป็นวิธีสำคัญที่ทำให้เกิดปอดอักเสบจากเชื้อกลุ่ม atypical organisms เชื้อไวรัส เชื้อวัณโรค และเชื้อรา จึงทำให้เกิดการแพร่ระบาดของเชื้อเหล่านี้ได้ง่ายในกลุ่มคนที่อยู่รวมกัน โดยเฉพาะครอบครัว ชั้นเรียน ห้องทำงาน สถานรับเลี้ยงเด็กก่อนวัยเรียน โรงแรม หอพัก กองทหาร ค่ายผู้อพยพ คุก หรือในบริเวณที่มีคนอยู่แออัด

อาการของ Pneumonia หรือ โรคปอดอักเสบเรื้อรัง ปอดบวม ได้แก่ ไข้ ไอ หายใจเร็วอาจมีอาการหอบ หายใจลำบาก มี chest retraction, nasal flaring หรือ อาการอื่นๆของภาวะหัวใจล้มเหลว ฟังเสียงปอดอาจได้ยินเสียงกรอบแกรบ (tine or medium crepitations) อาจได้ยินเสียง rhonchi ร่วมด้วย ในกรณีที่พยาธิสภาพเป็นแบบ consolidation อาจได้ยินเสียง bronchial breath sound มีอาการแสดงอื่นๆที่ไม่จำเพาะ เช่น ท้องอืด อาเจียน ซึมโดยเฉพาะเด็กเล็ก

การวินิจฉัย Pneumonia หรือ โรคปอดอักเสบเรื้อรัง ปอดบวม จากอาการแสดงคือ ไข้ ไอ หายใจเร็ว ร่วมกับฟังปอดได้ยินเสียง crepitations หรือ bronchial breath sounds และ ภาพเอ็กซ์เรย์รังสีทรวงอก Chest X-Ray ช่วยยืนยันการวินิจฉัยในผู้ป่วยที่ประวัติและการตรวจร่างกายไม่ชัดเจน ในรายที่มั่นใจในการวินิจฉัยแล้วไม่จำเป็นต้องถ่ายภาพรังสีทรวงอก นอกจากต้องการประเมินว่าผู้ป่วยมีภาวะแทรกซ้อนจากปอดอักเสบหรือไม่
Weighted Cross Entropy Loss
ในเคสนี้จำนวนข้อมูลตัวอย่าง ในแต่ละ Class แตกต่างกันมาก เรียกว่า Class Imbalance เราจะใช้ Loss Function แบบใหม่ ชื่อว่า Weighted Cross Entropy Loss
เรามาเริ่มกันเลยดีกว่า
ปอดอักเสบ เป็นโรคที่พบได้ประมาณร้อยละ 8-10 ของผู้ป่วยที่มีการติดเชื้อเฉียบพลันระบบหายใจ นับเป็นสาเหตุการตายอันดับหนึ่งของโรคติดเชื้อในเด็กอายุต่ำกว่า 5 ปี เกิดจากสาเหตุหลัก 2 กลุ่ม คือ ปอดอักเสบที่เกิดจากการติดเชื้อและปอดอักเสบที่ไม่ได้เกิดจากการติดเชื้อ ถ้าเราสามารถพัฒนาระบบ AI ช่วยวินิจฉัยโรคเบื้องต้น จำแนกชนิดของโรค จะมีประโยชน์ในการวินิจฉัย และดูแลรักษาตั้งแต่แรก
Pneumonia คืออะไร¶
ปอดอักเสบจากการติดเชื้อ หรือ pneumonia (ปอดบวม) เป็นโรคที่เกิดจากการอักเสบของเนื้อปอดบริเวณหลอดลมฝอยส่วนปลาย (terminal และ respiratory bronchiole) ถุงลม (alveoli) และเนื้อเยื่อรอบถุงลม (interstitium เป็นชนิดของปอดอักเสบที่พบได้บ่อยที่สุด โดยเชื้อโรคที่เข้าสู่ปอดและทำให้เกิดการอักเสบของถุงลมปอดและเนื้อเยื่อโดยรอบ ได้แก่ เชื้อไวรัส เชื้อแบคทีเรีย และเชื้อรา
ซึ่งเชื้อที่พบจะแตกต่างกันในแต่ละกลุ่มอายุ และสภาพแวดล้อมที่เกิดโรค เป็นโรคที่พบได้ประมาณร้อยละ 8-10 ของผู้ป่วยที่มีการติดเชื้อเฉียบพลันระบบหายใจ นับเป็นสาเหตุการตายอันดับหนึ่งของโรคติดเชื้อในเด็กอายุต่ำกว่า 5 ปี เช่น ได้รับเชื้อจากที่ชุมชนทั่วไป หรือจากภายในโรงพยาบาล
ทั้งนี้ เชื้อแบคทีเรียที่พบมักได้แก่ เชื้อ Streptococcus pneumoniae, เชื้อ Haemophilus influenzae type b, เชื้อ Chlamydia pneumoniae, เชื้อ Legionella spp. และเชื้อ Mycoplasma pneumoniae ส่วนเชื้อไวรัส ได้แก่ เชื้อ Respiratory Syncytial Virus (RSV), เชื้อ Influenza หรือเชื้อไข้หวัดใหญ่ และเชื้อราจากมูลนกหรือซากพืชซากสัตว์
เช็ค GPU
! nvidia-smi
0. Magic Command¶
%reload_ext autoreload
%autoreload 2
%matplotlib inline
1. Install Library¶
Install Library ที่จำเป็น ในที่นี้เราจะใช้ fastai2
# ## Colab
# ! pip install fastai2 kornia -q
2. Import Library¶
Import Library ที่จำเป็น รวมถึง sklearn ในการคำนวน Metrics
import gc
from fastai2.basics import *
from fastai2.callback.all import *
from fastai2.metrics import *
from fastai2.vision.all import *
import kornia
import pandas as pd
from sklearn.metrics import *
กำหนด Random Seed จะได้ Reproduce ได้ค่าเดิมทุกครั้งที่รัน
seed=123456
set_seed(seed)
3. Dataset¶
ในเคสนี้เราจะใช้ Dataset ฟิล์ม Chest X-Ray Images (Pneumonia) จาก Kaggle
เราจะ Mount Drive ไปยัง Google Drive ที่เก็บ Token File ไว้
dataset = 'paultimothymooney/chest-xray-pneumonia'
# Google Colab
config_path = Path('/content/drive')
learner_path = config_path/"My Drive"
data_path_base = Path('/content/datasets/')
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"
ในการจะ Download ข้อมูลจาก Kaggle ต้องใช้ Token ดูวิธีได้ใน ep ก่อน
# !kaggle datasets download {dataset} -p "{path}" --unzip
ls ดูว่าได้ Folder อะไรมาบ้าง
path.ls()
(path/'chest_xray').ls()
ดูข้อมูลใน Training Folder
(path/'chest_xray/train').ls()
(path/'chest_xray/train/PNEUMONIA').ls()
(path/'chest_xray/train/NORMAL').ls()
ใช้ฟังก์ชัน get_image_files
ดึงไฟล์ทั้งหมดมาใส่ List ไว้ก่อน
items = get_image_files(path/'chest_xray/train')
items
4. Data¶
4.1 Image¶
ดูรูปภาพ
patient = 55
เคสนี้ไฟล์ถูกแปลงเป็น jpg แล้ว ไม่ใช่ DICOM
items[patient]
item = PILImage.create(items[patient])
item.show()
Metadata เป็นรูปสี RGB 3 Channel แบบสี่เหลี่ยมผืนผ้า
str(item)
Label อยู่ในชื่อ Parent Folder
parent_label(items[patient])
5. Data Pipeline¶
5.1 Exploratory Data Analysis (EDA)¶
สำรวจข้อมูล Exploratory Data Analysis จะเห็นว่า มี Class Imbalance แตกต่างกันในแต่ละ Set ประมาณ 1 ต่อ 3, 1 ต่อ 1 และ 1 ต่อ 1.7
trn0 = get_image_files(path/'chest_xray/train/NORMAL')
trn1 = get_image_files(path/'chest_xray/train/PNEUMONIA')
val0 = get_image_files(path/'chest_xray/val/NORMAL')
val1 = get_image_files(path/'chest_xray/val/PNEUMONIA')
tst0 = get_image_files(path/'chest_xray/test/NORMAL')
tst1 = get_image_files(path/'chest_xray/test/PNEUMONIA')
trn0, trn1, val0, val1, tst0, tst1
เนื่องจากใน val Folder มีแค่ class ละ 8 ไฟล์ เราจะรวม train กับ val Folder เข้าด้วยกัน แล้ว Split เอง 80/20
5.2 DataBlock¶
สร้าง DataBlock และ DataLoader โดยทำ Data Augmentation ตาม Default
ใช้ Data Echoing เพิ่มความเร็วในการเทรน
class EchoingTransform(ItemTransform):
order = 2
split_idx = 0
def __init__(self, e): self.e = e
def encodes(self, x):
img, lbl = x
# print(img.shape)
# print(lbl.shape)
if self.e > 1:
img = img.repeat(self.e, 1, 1, 1)
lbl = lbl.repeat(self.e)
return img, lbl
สร้าง DataBlock โดยใช้ Partial กำหนดให้ใช้ข้อมูลจาก Folder train และ val
def getDataLoaders(bs, size, e):
pneumonia = DataBlock(blocks=(ImageBlock(), CategoryBlock),
get_items=partial(get_image_files, folders=['train', 'val']),
get_y=parent_label,
splitter=RandomSplitter(),
item_tfms=RandomResizedCrop(size, min_scale=0.8),
batch_tfms=[EchoingTransform(e), *aug_transforms()]
)
# pneumotpneumoniahorax.summary(path/'chest_xray')
dls = pneumonia.dataloaders(path/'chest_xray', bs=bs)
return dls
สร้าง DataLoader ด้วย size 224 และ Batch Size 64 แล้วแสดงข้อมูลตัวอย่าง ใน Batch
bs, size, e = 64, 224, 3
dls = getDataLoaders(bs, size, e)
dls.show_batch(max_n=16)
เช็คว่ามี 2 Class
dls.vocab
6. Model¶
กำหนด Loss Function
class XFocalLoss(kornia.losses.FocalLoss):
y_int = True
def __init__(self, alpha: float, gamma: float = 2.0,
reduction: str = 'none') -> None:
super(XFocalLoss, self).__init__(alpha, gamma, reduction)
def forward( # type: ignore
self,
input: torch.Tensor,
target: torch.Tensor) -> torch.Tensor:
# set_trace()
# print(input.shape)
# print(target.shape)
return super().forward(input, target)
def decodes(self, x): return x.argmax(dim=1)
def activation(self, x): return F.softmax(x, dim=1)
เนื่องจาก Class Imbalance เราจะใช้ Weighted Cross Entropy Loss โดยให้น้ำหนักกับ Normal มากกว่า Pneumonia
weights = torch.tensor([[1.8]*1 + [0.4]]).cuda()
loss_func = CrossEntropyLossFlat(weight=weights)
# loss_func = CrossEntropyLossFlat()
# loss_func = XFocalLoss(alpha=1.0, gamma=2.0, reduction='mean')
ใช้ Convolutional Neural Network สถาปัตยกรรม ResNet34 เวอร์ชันพิเศษของ fastai ชื่อว่า xresnet
arch = xresnet34(pretrained=False, c_in=3, act_cls=Mish, sa=True, n_out=2)
arch[0]
สร้าง Learner จาก Architecture ด้านบน โดยมี Metrics เช่น Accuracy, F1 Score และ Recall
# learn = cnn_learner(dls, arch=arch, metrics=accuracy,
# loss_func=loss_func, opt_func=ranger,
# cbs=[ShowGraphCallback])
learn = Learner(dls, arch, loss_func=loss_func, opt_func=ranger,
cbs=[ShowGraphCallback], path=learner_path,
metrics=[accuracy, F1Score(axis=1), Recall(axis=1)])
7. Train¶
7.1 Fine-tune¶
เริ่มต้นเทรนตั้งแต่ต้น From Scratch ทั้งโมเดล ด้วย fit_flat_cos เนื่องจากเราใช้ Mish Activation Function
# learn.lr_find()
learn.fit_flat_cos(1, lr=slice(1e-4, 1e-2))
learn.fit_flat_cos(12, lr=slice(3e-5, 3e-3))
ได้ F1 Score 98.6% เซฟโมเดลไว้ก่อน
learn.save("01k_224-1")
7.2 Progressive Resizing¶
สร้าง Data Loader ใหม่ ด้วย size รูปที่ใหญ่ขึ้น เป็นขนาด 384 x 384 Pixel และ ลดขนาด Batch Size ลงเป็นเท่ากับ 32 เพื่อไม่ให้ GPU Memory เต็ม เราสามารถเช็คขนาดได้ด้วยคำสั่ง nvidia-smi
learn = None
dls = None
gc.collect()
torch.cuda.empty_cache()
bs, size, e = 32, 384, 2
dls = getDataLoaders(bs, size, e)
สร้าง Learner ใหม่จาก Data Loader ด้านบน
# learn = cnn_learner(dls, arch=arch, metrics=accuracy,
# loss_func=loss_func, opt_func=ranger,
# cbs=[ShowGraphCallback])
learn = Learner(dls, arch, loss_func=loss_func, opt_func=ranger,
cbs=[ShowGraphCallback], path=learner_path,
metrics=[accuracy, F1Score(axis=1), Recall(axis=1)])
โหลดโมเดล 224 ขึ้นมา ทำ Transfer Learning เทรนต่อ
learn.load("01k_224-1")
Freeze โมเดล ยกเว้น Layer Group สุดท้าย
learn.freeze()
เทรนต่อ
# learn.lr_find()
learn.fit_flat_cos(8, lr=slice(3e-4))
เซฟไว้ก่อน
learn.save("01k_384-1")
# learn.load("01k_384-1");
Unfreeze แล้วเทรนต่อทั้งโมเดล
learn.unfreeze()
# learn.lr_find()
learn.fit_flat_cos(4, lr=slice(1e-6, 1e-4))
learn.save("01k_384-2")
# learn.load("01k_384-2");
7.3 Data Augmentation Annealing¶
ก่อนจบ เราจะเทรนแบบไม่ใช้ Data Augmentation กันอีกสักหน่อย
bs, size = 32, 384
pneumonia = DataBlock(blocks=(ImageBlock(), CategoryBlock),
get_items=partial(get_image_files, folders=['train', 'val']),
get_y=parent_label,
splitter=RandomSplitter(valid_pct=0.05),
item_tfms=Resize(size, method='squish'),
batch_tfms=[]
)
# pneumonia.summary(path/'chest_xray')
dls = pneumonia.dataloaders(path/'chest_xray', bs=bs)
สร้าง Learner ใหม่จาก Data Loader ด้านบน
# learn = cnn_learner(dls, arch=arch, metrics=accuracy,
# loss_func=loss_func, opt_func=ranger,
# cbs=[ShowGraphCallback])
learn = Learner(dls, arch, loss_func=loss_func, opt_func=ranger,
cbs=[ShowGraphCallback], path=learner_path,
metrics=[accuracy, F1Score(axis=1), Recall(axis=1)])
โหลดโมเดลด้านบนขึ้นมา
learn.load("01k_384-2");
learn.unfreeze()
learn.fit_flat_cos(1, lr=slice(3e-7, 3e-5))
ได้ Accuracy 97.7%, F1 Score 98.5% และ Recall 97.0%
learn.save("01k_384-3")
# learn.load("01k_384-3");
แสดงผลลัพธ์การทำงาน
learn.show_results(max_n=16)