오늘의 수업 요약

X-ray 남성 골연령 예측 모델

의료 데이터(X-ray)에서 성별과 폴더 정보를 추출하여 나이를 정교하게 예측하는 딥러닝 실습

무엇을 배울까요?

💡 쉬운 예시: 손 엑스레이 사진만 보고도 "이 소년의 나이는 몇 살이겠구나"라고 정확하게 맞히는 '인공지능 정형외과 의사'를 만드는 중이에요.

1

X-ray 데이터 필터링 및 로드

왜 이 코드를 실행하나요? 골연령(Age) 분석을 위해 X-ray 사진들을 읽어옵니다. 이번 실습에서는 성별에 따른 차이를 고려해 '남성(M)' 데이터만 골라내어 나이 정보를 추출하는 과정에 집중합니다.
from PIL import Image
import numpy as np
import glob
all_images = []
# 사진을 담을 빈 리스트를 만들어요.
filenames = []
# 파일 이름을 기억해둘 리스트를 만들어요.
ages = []
# 진짜 정답인 '나이'를 담을 리스트예요.
img_size = (80, 100)
# 80x100 해상도로 통일하기로 결정해요.
for f in glob.glob('c:/data/xray/**/*.jpg', recursive=True):
# 모든 X-ray 폴더 하위의 .jpg 파일을 찾아요.
arr = f.split('\')
# 파일 경로를 잘라 폴더 정보를 확인해요.
arr2 = arr[3].split("_")
# 폴더명(BA_M_10yr 등)을 다시 언더바로 잘라요.
if arr2[1] == 'M':
# 성별이 남성(M)인 데이터만 골라내요.
img = Image.open(f)
# 이미지 파일을 열어요.
filenames.append(arr[-1])
# 원본 파일명만 따로 저장해요.
img_resize = img.resize((img_size[0], img_size[1]))
# 작은 사이즈로 해상도를 조절해요.
all_images.append(img_resize)
# 바뀐 사진 데이터를 리스트에 넣어요.
age = int(arr2[2].replace('yr', ''))
# 폴더명에서 'yr'을 떼고 숫자 나이만 추출해요.
ages.append(age)
# 나이 정답 리스트에 추가해요.
실제 출력
(남성 X-ray 데이터 총 665장 필터링 완료)
출력 해설: 경로 내의 수천 장 데이터 중 남성 데이터인 665장만 성공적으로 선별했습니다.
2

데이터 병합 및 전처리

왜 이 코드를 실행하나요? 로드된 이미지들을 하나의 거대한 수치 행렬로 합치고, 나이 정답을 배열로 변환합니다. 이미지 픽셀값은 0~1 사이로 정규화하여 학습 준비를 마칩니다.
X = np.empty((1, img_size[0], img_size[1], 3))
# 데이터를 쌓을 빈 행렬 틀을 만들어요.
for img in all_images:
# 낱개 사진들을 하나씩 행렬 뒤로 합쳐요.
X = np.vstack((X, np.array(img).reshape(1, img_size[0], img_size[1], 3)))
# 0번으로 만들었던 가짜 틀을 제거해요.
X = np.delete(X, (0), axis=0)
# 나이 리스트를 Numpy 배열로 바꿔요.
y = np.array(ages)
# 학습용과 테스트용으로 8:2 비율로 나눠요.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, shuffle=True, random_state=10)
# 공부할 데이터의 픽셀값을 0~1로 스케일링해요.
X_train = X_train / 255.
# 시험 볼 데이터도 똑같이 스케일링해요.
X_test = X_test / 255.
실제 출력
y_train[:5]: [10, 10, 10, 10, 10]
출력 해설: 이미지는 수치 행렬로 변환되었고, 각 이미지의 정답인 나이 데이터가 준비되었습니다.
로드된 남성 X-ray 샘플
3

골연령 예측 CNN 모델 설계

왜 이 코드를 실행하나요? X-ray 사진에서 뼈의 발달 상태 특징을 잡기 위해 3단계의 Convolution 층을 쌓습니다. 이번에는 분류가 아니라 '나이(숫자)'를 맞히는 회귀 문제이므로 마지막 출구는 1개의 값만 나오게 설정합니다.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Dense, Flatten
model = Sequential()
# 모델 설계를 시작해요.
model.add(Conv2D(64, (3, 3), padding="same", input_shape=X_train.shape[1:], activation='relu'))
# 첫 번째 필터(64개)로 뼈 마디 등의 특징을 찾아요.
model.add(MaxPooling2D(2, 2))
# 이미지를 압축해요.
model.add(Conv2D(64, (3, 3), activation="relu"))
# 두 번째 특징 추출.
model.add(MaxPooling2D(2, 2))
# 다시 압축.
model.add(Conv2D(64, (3, 3), activation="relu"))
# 세 번째 특징 추출.
model.add(Flatten())
# 2D 정보를 1D로 펴요.
model.add(Dense(64, activation="relu"))
# 지능 뉴런 층을 둬서 특징을 조합해요.
model.add(Dense(1))
# 최종 목표인 '나이' 숫자 1개를 출력해요.
model.compile(optimizer="adam", loss="mae", metrics=["mae"])
# 회귀 문제이므로 평균 절대 오차(MAE)를 규칙으로 써요.
model.summary()
# 모델 구조 전체 지도를 확인해요.
예상 출력
Model: "sequential" _________________________________________________________________ conv2d (Conv2D) (None, 80, 100, 64) 1792 ... dense_1 (Dense) (None, 1) 65 =================================================================
출력 해설: X-ray 사진을 보고 '이 뼈의 주인은 12.5세입니다'라고 답할 준비를 마친 인공지능입니다.
4

학습 및 평가

왜 이 코드를 실행하나요? 준비한 데이터를 모델에 넣어 학습시키고, 결과적으로 얼마나 오차가 발생하는지 확인합니다. MAE가 낮을수록 인공지능이 실제 나이를 정확하게 맞히고 있다는 뜻입니다.
from tensorflow.keras.callbacks import ModelCheckpoint
checkpoint = ModelCheckpoint("c:/data/models/xray_m_best.keras", monitor='val_loss', verbose=1, save_best_only=True, mode='min')
# 가장 오차가 적은 순간의 모델을 저장하는 도구를 만들어요.
hist = model.fit(X_train, y_train, batch_size=32, validation_split=0.2, epochs=30, callbacks=[checkpoint])
# 30번 반복해서 학습을 진행해요.
from tensorflow.keras.models import load_model
model = load_model('c:/data/models/xray_m_best.keras')
# 최고 성능을 기록했던 모델 파일을 다시 불러와요.
scores = model.evaluate(X_test, y_test, verbose=1)
# 테스트 데이터로 최종 성적을 매겨요.
print("최종 오차(MAE):", scores[1])
# 실제 나이와 인공지능 예측치의 평균 차이를 출력해요.
예상 출력
Epoch 1/30 ... loss: 2.5401 - mae: 2.5401 ... 최종 오차(MAE): 1.4203
출력 해설: MAE가 1.4라면 인공지능이 평균적으로 실제 나이와 약 1.4세 정도의 차이로 오차 없이 잘 맞히고 있다는 의미입니다.