การวินิจฉัยโรคจากภาพ X-ray ต้องอาศัยความเชี่ยวชาญสูง แต่ในหลายพื้นที่ยังขาดแคลนรังสีแพทย์ AI จึงเข้ามาช่วยเป็น "ผู้ช่วยแพทย์" ในการคัดกรองเบื้องต้นได้อย่างรวดเร็วและแม่นยำ
Convolutional Neural Network (CNN) เป็น Deep Learning สถาปัตยกรรมที่ออกแบบมาเพื่อประมวลผลข้อมูลแบบ Grid (เช่น รูปภาพ) โดยเลียนแบบการทำงานของ Visual Cortex ในสมองมนุษย์
ทำหน้าที่คล้ายการใช้แว่นขยายสแกนทีละส่วนของภาพ เพื่อหาลักษณะสำคัญ (Features) เช่น:
Conv2D(32, kernel_size=(3, 3), activation='relu') # 32 = จำนวน filters (ตัวกรอง) # (3, 3) = ขนาดของ kernel # relu = activation function
ลดขนาดข้อมูลแต่เก็บข้อมูลสำคัญไว้ ช่วยให้:
MaxPooling2D(pool_size=(2, 2)) # เลือกค่าสูงสุดจากทุกๆ 2×2 พิกเซล # ลดขนาดภาพลงครึ่งหนึ่ง
รวบรวมข้อมูลทั้งหมดเพื่อตัดสินใจ:
Dense(128, activation='relu') # Hidden layer Dense(64, activation='relu') # Hidden layer Dense(1, activation='sigmoid') # Output layer
# แบ่งข้อมูลเป็น 3 ส่วน train_dir = 'train' # 70% สำหรับ training (4,192 ภาพ) val_dir = 'val' # 15% สำหรับ validation (1,040 ภาพ) test_dir = 'test' # 15% สำหรับ testing (624 ภาพ) # Data Augmentation - สร้างภาพเพิ่มจากภาพเดิม train_datagen = ImageDataGenerator( rescale=1./255, # ปรับค่าพิกเซล 0-255 → 0-1 shear_range=0.2, # บิดภาพ zoom_range=0.2, # ซูมเข้า-ออก horizontal_flip=True # พลิกภาพซ้าย-ขวา )
# คำนวณ class weights เพื่อแก้ปัญหา imbalanced data # Normal: 1,082 ภาพ (weight = 1.94) # Pneumonia: 3,110 ภาพ (weight = 0.67) weights = compute_class_weight( class_weight='balanced', classes=np.unique(train_generator.classes), y=train_generator.classes )
# Compile model model.compile( optimizer='adam', # Algorithm สำหรับ optimize loss='binary_crossentropy', # Loss function สำหรับ binary classification metrics=['accuracy'] # ติดตามความแม่นยำ ) # Train model epochs = 30 history = model.fit( train_generator, callbacks=[learning_rate_reduction], steps_per_epoch=train_generator.samples // batch_size, epochs=epochs, # จำนวนรอบการเรียนรู้ 30 รอบ validation_data=validation_generator, validation_steps=validation_generator.samples // batch_size, class_weight=cw # ใช้ class weights )
learning_rate_reduction = ReduceLROnPlateau( monitor='val_loss', # ติดตาม validation loss patience=2, # รอ 2 epochs ถ้าไม่ดีขึ้น factor=0.1, # ลด learning rate 10 เท่า min_lr=0.000001 # ค่าต่ำสุดของ learning rate )
Confusion Matrix แสดงผลการทำนายเทียบกับความจริง ช่วยให้เข้าใจว่าโมเดลผิดพลาดอย่างไร
Confusion Matrix | Predicted | ||
---|---|---|---|
Normal | Pneumonia | ||
Actual | Normal | 204 (TN) | 30 (FP) |
Pneumonia | 23 (FN) | 367 (TP) |
# คำนวณ ROC และ AUC fpr, tpr, threshold = roc_curve(test_generator.classes, y_score) roc_auc = auc(fpr, tpr) # AUC = 0.9564
Large Language Model (LLM) เป็นโมเดล AI ขนาดใหญ่ที่ถูกฝึกด้วยข้อมูลข้อความมหาศาล สามารถเข้าใจและสร้างข้อความที่มีความหมาย รวมถึงวิเคราะห์รูปภาพได้ (Multimodal LLM)
# การใช้ MedGemma วิเคราะห์ภาพ X-ray pipe = pipeline( "image-text-to-text", model="google/medgemma-4b-it", torch_dtype=torch.bfloat16, device="cuda" ) messages = [{ "role": "user", "content": [ {"type": "text", "text": "Describe this X-ray"}, {"type": "image", "image": xray_image} ] }] # โมเดลจะวิเคราะห์และให้คำอธิบายทางการแพทย์
import tensorflow as tf from tensorflow.keras.preprocessing.image import ImageDataGenerator import matplotlib.pyplot as plt from PIL import Image import numpy as np # Download dataset จาก Kaggle import kagglehub path = kagglehub.dataset_download("pcbreviglieri/pneumonia-xray-images")
# กำหนด path ของแต่ละ class normal_path = os.path.join(train_path, 'normal') opacity_path = os.path.join(train_path, 'opacity') # แสดงภาพตัวอย่าง plt.figure(figsize=(10, 5)) normal_img = Image.open(normal_img_path) plt.imshow(normal_img, cmap='gray') plt.title('Normal') plt.show()
# สร้าง ImageDataGenerator สำหรับ training train_datagen = ImageDataGenerator( rescale=1./255, # Normalize pixels to [0,1] shear_range=0.2, # Random shearing zoom_range=0.2, # Random zoom horizontal_flip=True # Random horizontal flip ) # Load images from directories train_generator = train_datagen.flow_from_directory( train_dir, target_size=(350, 350), # Resize all images batch_size=32, # Process 32 images at a time class_mode='binary', # Binary classification color_mode='grayscale' # Use grayscale images )
# สร้างโมเดลด้วย Functional API # Input inputs = tf.keras.Input(shape=(350, 350, 1)) # Feature Extraction x = tf.keras.layers.Conv2D(32, (3, 3), activation='relu')(inputs) x = tf.keras.layers.MaxPooling2D((2, 2))(x) x = tf.keras.layers.Conv2D(32, (3, 3), activation='relu')(x) x = tf.keras.layers.MaxPooling2D((2, 2))(x) x = tf.keras.layers.Conv2D(64, (3, 3), activation='relu')(x) x = tf.keras.layers.MaxPooling2D((2, 2))(x) x = tf.keras.layers.Conv2D(64, (3, 3), activation='relu')(x) x = tf.keras.layers.MaxPooling2D((2, 2))(x) x = tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same', name='last_conv_layer')(x) # Image Classification x = tf.keras.layers.Flatten()(x) x = tf.keras.layers.Dense(128, activation='relu')(x) x = tf.keras.layers.Dense(64, activation='relu')(x) outputs = tf.keras.layers.Dense(1, activation='sigmoid')(x) # Model model = tf.keras.Model(inputs=inputs, outputs=outputs)
# นับจำนวนภาพในแต่ละ class from collections import Counter Counter(train_generator.classes) # Output: {0: 1082, 1: 3110} # Normal:Opacity ratio # คำนวณ class weights from sklearn.utils.class_weight import compute_class_weight weights = compute_class_weight( 'balanced', np.unique(train_generator.classes), train_generator.classes ) # weights = [1.937, 0.674] # Give more weight to minority class
# Learning rate scheduler learning_rate_reduction = ReduceLROnPlateau( monitor='val_loss', patience=2, # Wait 2 epochs factor=0.1, # Reduce LR by factor of 10 verbose=1, min_lr=0.000001 ) # Train the model history = model.fit( train_generator, epochs=10, validation_data=validation_generator, callbacks=[learning_rate_reduction], class_weight=dict(zip(np.unique(train_generator.classes), weights)) )
# Evaluate on test set loss, accuracy = model.evaluate(test_generator, steps=test_generator.samples // batch_size) print(f"Test Loss: {loss:.4f}") print(f"Test Accuracy: {accuracy:.4f}") # Generate predictions predicted_classes = (model.predict(test_generator, verbose=1) > 0.5).astype("int32")[:,0] # Create confusion matrix from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(test_generator.classes, predicted_classes) # Generate classification report report = classification_report( test_generator.classes, predicted_classes, target_names=['normal', 'opacity'], digits=4 )
# Calculate ROC curve from sklearn.metrics import roc_curve, auc y_score = model.predict(test_generator) y_score = y_score[:,0] fpr, tpr, threshold = roc_curve(test_generator.classes, y_score) roc_auc = auc(fpr, tpr) # Plot ROC curve import plotly.express as px roc_df = pd.DataFrame({ 'False Positive Rate': fpr, 'True Positive Rate': tpr, 'Threshold': threshold }) fig = px.area( roc_df, x='False Positive Rate', y='True Positive Rate', title=f'ROC Curve (AUC={roc_auc:.4f})' )
# Save the trained model model.save('pneumonia_detection_model.keras') print("Model saved successfully as 'pneumonia_detection_model.keras'")
# ติดตั้ง tf-keras-vis !pip install tf-keras-vis # หา last convolutional layer for layer in reversed(model.layers): if isinstance(layer, tf.keras.layers.Conv2D): last_conv_layer_name = layer.name break # สร้าง GradCAM from tf_keras_vis.gradcam import Gradcam gradcam = Gradcam(model, model_modifier=None, clone=True) def custom_score(output): return output heatmap = gradcam(custom_score, image_array, penultimate_layer=last_conv_layer_name) # แสดงผล heatmap overlay บนภาพต้นฉบับ plt.imshow(image.convert('RGB')) plt.imshow(heatmap[0], cmap='jet', alpha=0.5) # Overlay heatmap plt.title('GradCAM Heatmap') plt.show()
เมื่อ AI บอกว่า "ภาพนี้เป็นปอดอักเสบ" แพทย์อยากรู้ว่า AI ดูตรงไหนของภาพ? เหตุผลคืออะไร? GradCAM คือคำตอบที่ทำให้เราเห็น "สมอง" ของ AI
GradCAM (Gradient-weighted Class Activation Mapping) เป็นเทคนิคที่สร้าง "แผนที่ความร้อน" (Heatmap) บนภาพ เพื่อแสดงว่า AI กำลังให้ความสนใจกับส่วนไหนของภาพมากที่สุดในการตัดสินใจ
# Forward Pass prediction = model.predict(image) # ได้ 0.91 (91% ปอดอักเสบ) # Layer สุดท้ายที่เก็บ spatial information last_conv_layer = model.get_layer('last_conv_layer') # Output shape: 20×20×128 (20×20 คือตำแหน่ง, 128 คือ features)
💡 ทำไมต้องใช้ Conv Layer สุดท้าย?
เพราะ layer นี้ยังเก็บข้อมูล "ตำแหน่ง" ในภาพไว้ (20×20 grid)
ต่างจาก Dense Layer ที่ข้อมูลถูก flatten แล้ว
# Backpropagation เพื่อหา gradients gradients = tape.gradient(output, last_conv_output) # gradients shape: 20×20×128 # คำนวณค่าเฉลี่ย gradient ของแต่ละ channel weights = tf.reduce_mean(gradients, axis=(0, 1, 2)) # weights shape: 128 (ความสำคัญของแต่ละ feature)
รูป: การคำนวณ Gradient weights - แสดงการ reduce mean จาก feature map (ซ้าย) เพื่อหาค่าความสำคัญของแต่ละ channel (ขวา)
# สร้าง weighted activation map heatmap = np.sum(weights * last_conv_output, axis=-1) # ReLU - เก็บแต่ค่าบวก (ส่วนที่สนับสนุนการตัดสินใจ) heatmap = np.maximum(heatmap, 0) # Normalize ให้อยู่ในช่วง 0-1 heatmap /= np.max(heatmap) # Resize กลับเป็นขนาดภาพเดิม heatmap = cv2.resize(heatmap, (350, 350))
แพทย์เห็นว่า AI มองจุดเดียวกับที่แพทย์สนใจ ไม่ใช่ดูพื้นหลังหรือขอบภาพ
ถ้า AI มองผิดจุด (เช่น ดูที่ตัวอักษร X-ray แทนที่จะดูปอด) แพทย์จะรู้ว่าโมเดลมีปัญหา
นักศึกษาแพทย์เห็นว่าจุดไหนของภาพ X-ray ที่บ่งบอกถึงความผิดปกติ
ลักษณะ Heatmap | การแปลผล | ความน่าเชื่อถือ |
---|---|---|
จุดสีแดงอยู่ที่ปอดพอดี | AI มองถูกจุด ตรงกับพยาธิสภาพ | ✅ สูง |
สีแดงกระจายทั่วปอด | อาจมีการติดเชื้อลุกลาม | ⚠️ ปานกลาง |
สีแดงอยู่นอกปอด | AI อาจมองผิด หรือมี artifact | ❌ ต่ำ |
ไม่มีจุดสีแดงเด่นชัด | AI ไม่แน่ใจ หรือภาพปกติ | 🔍 ต้องตรวจเพิ่ม |
📌 ข้อความสำคัญ: ระบบ AI นี้พัฒนาขึ้นเพื่อเป็นเครื่องมือช่วยแพทย์ในการคัดกรองเบื้องต้น ไม่สามารถใช้แทนการวินิจฉัยโดยแพทย์ผู้เชี่ยวชาญ ผลการวิเคราะห์ต้องผ่านการพิจารณาจากแพทย์เสมอก่อนการรักษา