RF 사이토카인 SHAP 분석 #3 랜덤 포레스트 모델 훈련

RF 사이토카인 SHAP 분석 #3 랜덤 포레스트 모델 훈련 #

#2026-03-01


전처리된 사이토카인 데이터가 있고, 각 샘플에 PPG 레이블이 붙었다. 이제 핵심 질문을 던질 차례다. 166개 사이토카인 중 어떤 것들이 COVID-19 중증도를 결정하는가?이 질문에 답하려면 머신러닝 모델이 필요하다. 하나의 사이토카인만으로는 중증도를 설명할 수 없기 때문이다. “CD274이 높으면 중증"이라고 단순하게 말할 수 없다. CD274이 높지만 IL-10도 높으면 항염증 반응이 작동하고 있다는 뜻일 수 있고, CD274이 낮지만 CXCL10과 IL-6가 동시에 치솟으면 다른 경로로 중증화가 진행되고 있을 수 있다. 166개 변수가 복잡하게 얽힌 패턴을 사람이 눈으로 파악하는 건 불가능하다. 모델이 이 복잡한 관계를 학습해야 한다. 랜덤 포레스트 모델을 훈련해서 이를 수행한다.

작업설명
훈련 데이터 구성건강군 25명 + 중증군 35명 선별
모델 학습Random Forest (트리 100개, Gini)
성능 검증80/20 분할 + 5-fold 교차 검증
중증 확률 예측전체 549개 샘플에 Severe_Prob 계산
레이블링 검증WHO/NLR/LDH와 Severe_Prob 상관관계 확인

#

#1 랜덤 포레스트

모델을 선택할 때 이 연구에서 가장 중요하게 고려한 것은 정확도가 아니라 해석 가능성이다. 이건 이전 챕터들과 결정적으로 다른 점이다. 망막 영상 분류에서는 ResNet이 “이 사진은 등급 3"이라고 맞추기만 하면 됐다. 하지만 의학 연구에서는 “왜 등급 3인가"를 설명할 수 있어야 한다. 어떤 사이토카인이 중증도에 기여하는지, 얼마나 기여하는지를 정량적으로 밝혀내야 치료 표적을 찾을 수 있다.

신경망은 이 해석이 극도로 어렵다. 166개 입력이 128개 뉴런을 거치고 64개를 거치고 32개를 거쳐서 하나의 출력이 나온다. 수천 개의 가중치가 복잡하게 얽혀 있어서 “CD274이 결과에 얼마나 기여했는가"를 분리해내기가 거의 불가능하다. 블랙박스인 것이다.

딥러닝 (예: 신경망):
  입력 (166개 사이토카인)
  [레이어 1: 128 뉴런]
  [레이어 2:  64 뉴런]
  [레이어 3:  32 뉴런]
  출력 (중증 확률)

  → 정확도는 높지만
  → "왜 이 샘플이 중증인가?"를 설명할 수 없음
  → 생물학적 인사이트 추출 불가

랜덤 포레스트는 다르다. 내부가 결정 트리들의 집합이고, 각 트리는 “이 사이토카인이 이 값보다 높은가 낮은가"라는 질문의 연쇄로 이루어져 있다. 어떤 사이토카인이 트리의 어느 위치에서 얼마나 자주 사용되었는지를 추적하면 각 사이토카인의 기여도를 직접 측정할 수 있다. 그리고 다음 단계에서 SHAP이라는 기법과 결합하면 개별 샘플 수준에서 “이 환자가 중증으로 분류된 이유는 CD274이 높고 IL-10이 낮았기 때문"이라는 해석까지 가능해진다.

Random Forest:
  → 어떤 사이토카인이 분류에 기여했는지 정량화 가능
  → SHAP(Step 4)와 결합하면 샘플 수준의 해석까지 가능
  → 의학 연구에서 해석 가능성(interpretability)이 필수

#

#2 결정 트리 한 개의 작동 방식

랜덤 포레스트를 이해하려면 먼저 결정 트리 한 그루를 이해해야 한다.

60개 샘플이 있다고 하자. 건강한 사람 25명과 중증 환자 35명이 섞여 있다. 이 60명을 가장 잘 분리하는 사이토카인을 하나 고른다. 예를 들어 CD274의 값이 5.5를 넘는지를 기준으로 나누면, 왼쪽에는 대부분 중증 환자가 모이고 오른쪽에는 대부분 건강한 사람이 모인다고 하자. 이게 트리의 첫 번째 분기점(루트 노드)이다.

하지만 한 번의 분기로 완벽하게 나누어지지 않을 수 있다. 왼쪽 그룹에 건강한 사람 몇 명이 섞여 있을 수 있다. 그러면 이 왼쪽 그룹 안에서 다시 가장 잘 나누는 사이토카인을 찾는다. FLT3LG가 4.2를 넘는지를 기준으로 또 나눈다. 이런 식으로 모든 그룹이 순수해질 때까지(한 그룹에 건강인만 있거나 중증만 있을 때까지) 계속 쪼개나간다.

      CD274 > 5.5?
      ┌──────┴──────┐
     Yes            No
      ↓              ↓
  FLT3LG > 4.2?   GDF15 > 3.8?
  ┌────┴────┐     ┌────┴────┐
 Yes       No   Yes       No
  ↓         ↓    ↓         ↓
[중증]    [?]  [중증]    [건강]

→ 각 분기는 "이 사이토카인 값으로 가장 잘 나눌 수 있나?"
  라는 질문에 답하는 과정

각 분기점에서 “어떤 사이토카인으로 나눌 것인가"를 결정하는 기준이 Gini 불순도다. Gini 불순도는 그 그룹이 얼마나 섞여 있는지를 측정하는 수치다. 건강인 10명과 중증 10명이 반반 섞인 그룹의 Gini는 1 - (10/20)² - (10/20)² = 0.5로 최대다. 가장 불순한 상태다. 건강인 20명만 있는 그룹의 Gini는 1 - (20/20)² = 0으로 최소다. 완전히 순수한 상태다. 분기를 할 때마다 Gini 불순도를 가장 많이 줄이는 사이토카인을 선택한다. 가장 효과적으로 두 그룹을 갈라놓는 사이토카인이 트리의 위쪽(루트에 가까운 쪽)에 위치하게 된다.

#

#3 랜덤 포레스트

결정 트리 한 그루의 문제는 과적합이다. 훈련 데이터에 지나치게 맞춰져서 새로운 데이터에는 잘 작동하지 않을 수 있다. 한 그루의 나무는 훈련 데이터의 작은 잡음까지 외워버리는 경향이 있다.

랜덤 포레스트는 이 문제를 100그루의 나무를 심어서 해결한다. 각 나무는 의도적으로 조금씩 다르게 만든다. 두 가지 무작위성을 주입한다.

첫째, 부트스트랩 샘플링이다. 60개 샘플에서 복원 추출로 60개를 뽑는다. 어떤 샘플은 두 번 뽑히고 어떤 샘플은 한 번도 안 뽑힌다. 각 나무마다 다른 샘플 조합으로 학습하는 것이다.

둘째, 특성 서브샘플링이다. 각 분기점에서 166개 사이토카인 전부를 후보로 놓지 않고, 무작위로 선택한 일부(보통 √166 ≈ 13개 정도)만 후보로 본다. 그래서 어떤 나무는 CD274을 루트에 놓고 다른 나무는 GDF15를 루트에 놓게 된다.

최종 예측은 100그루의 투표 결과다. 75그루가 “중증"이라고 하고 25그루가 “건강"이라고 하면 중증 확률은 0.75다. 이 투표 방식이 개별 나무의 과적합을 상쇄시킨다. 한 나무가 잡음에 반응해서 틀린 예측을 해도, 나머지 나무들이 다수결로 바로잡는다.

#

#4 훈련 전략 (건강 vs 중증): PPG를 직접 분류하지 않는 이유

여기서 중요한 설계 결정이 하나 있다. PPG가 네 가지(mDP, mRP, sDP, sRP)인데, 왜 이 네 가지를 직접 분류하지 않고 “건강 vs 중증” 이진 분류를 하는가?

현실적인 이유가 있다. sDP 샘플이 53개밖에 안 된다. 네 가지를 동시에 분류하려면 각 클래스에 충분한 샘플이 필요한데, 53개는 랜덤 포레스트가 안정적으로 패턴을 학습하기에 적다. 게다가 mDP와 mRP, sDP와 sRP 사이의 사이토카인 차이는 mDP와 sDP 사이의 차이보다 미묘하다. 네 가지를 한꺼번에 학습하면 경계가 모호해져서 모델 성능이 떨어질 수 있다.

직접 4분류 (mDP, mRP, sDP, sRP):

  mDP: 94개
  mRP: 182개
  sDP: 53개  ← 너무 적음
  sRP: 75개

  → sDP 샘플이 53개밖에 없어 모델 학습 불안정
  → 4개 클래스를 동시에 학습하면 경계가 모호해짐

전략적인 이유도 있다. 건강한 사람과 중증 COVID-19 환자의 사이토카인 프로파일은 극적으로 다르다. 건강한 사람은 사이토카인 수준이 낮고 안정적이다. 중증 환자는 사이토카인 폭풍으로 여러 사이토카인이 극도로 상승해 있다. 이 두 극단을 학습시키면 모델은 “사이토카인 프로파일에서 중증과 관련된 신호가 무엇인가"를 매우 명확하게 배운다.

대신: 이진 분류 (건강 vs 중증)

  건강군: 145명 중 25명 선택
  중증군: WHO ≥ 6인 샘플 중 35명 선택

  → 두 그룹의 사이토카인 차이가 매우 명확
  → 모델이 사이토카인의 "중증 관련 신호"를 학습
  → 학습된 모델로 전체 샘플에 "중증 점수" 부여
  → 이 점수가 PPG 간 차이를 반영하는 생물학적 좌표계가 됨

학습된 모델을 전체 549개 샘플에 적용하면, 각 샘플이 0과 1 사이의 중증 확률(Severe_Prob)을 받는다. 건강인은 0에 가깝고, 중증 환자는 1에 가깝고, 중등도 환자나 회복기 환자는 그 사이 어딘가에 위치한다. 이 연속적인 점수가 PPG 간의 차이를 반영하는 생물학적 좌표계 역할을 한다. 0/1 이진 레이블보다 훨씬 풍부한 정보를 담고 있다.

#

#5 RF 모델 훈련

# 건강군에서 25명 무작위 선택
healthy_df = cytokine_df[cytokine_df.index.str.contains('Healthy')]
train_healthy = healthy_df.sample(n=25, random_state=42)
train_healthy['label'] = 0   # 0 = 건강

# 중증군: WHO ≥ 6인 샘플에서 35명 선택
severe_mask = patient_meta['Severity'] >= 6
severe_samples = patient_meta[severe_mask]['Sample']
train_severe = cytokine_df.loc[severe_samples].sample(n=35, random_state=42)
train_severe['label'] = 1    # 1 = 중증
rf_model = RandomForestClassifier(
    n_estimators=100,      # 결정 트리 100개
    criterion='gini',      # 분기 기준: Gini 불순도
    min_samples_split=2,   # 분기에 필요한 최소 샘플 수
    min_samples_leaf=1,    # 리프 노드 최소 샘플 수
    random_state=42,
    n_jobs=-1              # 병렬 처리
)

#

#cf Gini 불순도?

어떤 노드에 건강 10명, 중증 10명이 섞여 있으면:
  Gini = 1 - (10/20)² - (10/20)² = 0.5   ← 가장 불순함

건강 20명만 있으면:
  Gini = 1 - (20/20)² - (0/20)² = 0       ← 완전히 순수함

분기를 할 때마다 Gini를 가장 많이 줄이는 사이토카인을 선택
→ 가장 잘 구분 짓는 사이토카인이 트리의 위쪽(루트에 가깝게) 위치

#

#6 성능 평가

훈련 정확도 100%, 테스트 정확도 100%, 5-fold 교차 검증 100%. 모든 지표가 완벽하다. 보통 100% 정확도는 과적합을 의심해야 하지만, 이 경우는 과적합이 아니다.

|——|——|————| | Train 정확도 | 100% | 100% | | Test 정확도 | 100% | 100% | | 5-fold CV 평균 | 100% | 100% (전 폴드) |

# confusion matrix
────────────────────────────────────
      [Train set]    예측: 건강  예측: 중증
실제: 건강         │    20    │     0    │
실제: 중증         │     0    │    28    │
────────────────────────────────────
FP = 0, FN = 0   ← 완벽한 분류

────────────────────────────────────
       [Test set]    예측: 건강  예측: 중증
실제: 건강         │     5    │     0    │
실제: 중증         │     0    │     7    │
────────────────────────────────────
FP = 0, FN = 0   ← Test에서도 완벽

이유는 두 그룹의 차이가 압도적으로 크기 때문이다. 건강한 사람의 IL-6는 1 pg/mL 수준이다. 중증 COVID-19 환자의 IL-6는 1,000 pg/mL 이상이다. 천 배 차이다. 이건 IL-6 하나만의 이야기가 아니다. 166개 사이토카인 중 상당수가 비슷한 수준의 극적인 차이를 보인다. 사이토카인 폭풍이라는 현상 자체가 수십 종의 사이토카인을 동시에 폭발시키기 때문이다.

비유하자면, 여름 사진과 겨울 사진을 구별하는 것과 같다. 하늘 색깔, 나뭇잎 유무, 사람들의 옷차림, 눈 유무 등 수십 가지 단서가 동시에 차이를 보인다. 어떤 분류기를 쓰든 100%에 가까운 정확도가 나올 수밖에 없다. 건강인과 중증 환자의 사이토카인 프로파일 차이는 이 정도로 극명하다.

건강인 (COVID 미감염):
  사이토카인 수준이 낮고 안정적
  IL-6: ~1 pg/mL, CRP: 낮음

중증 COVID-19 (WHO ≥ 6, 기계 환기):
  사이토카인 폭풍으로 극도로 상승
  IL-6: ~1000+ pg/mL, CD274: 매우 높음

→ 두 그룹의 차이가 너무 명확해서
  166개 중 몇 개만으로도 완벽하게 구분 가능

5-fold 교차 검증이 이걸 확인해준다. 데이터를 다섯 묶음으로 나눠서, 네 묶음으로 학습하고 한 묶음으로 테스트하는 과정을 다섯 번 반복한다. 다섯 번 모두 100%라는 건 어떤 샘플이 테스트에 들어가든 결과가 같다는 뜻이다. 특정 샘플 조합에 의존한 우연이 아니라 진짜 분리 가능한 차이라는 확인이다.

#

#7 중증 확률: 단순 분류를 넘어선 연속 점수

모델이 100%로 학습된 후, 이 모델을 전체 549개 샘플에 적용한다. predict_proba라는 함수가 각 샘플에 대해 100그루의 나무가 투표한 결과를 확률로 돌려준다. 클래스 1(중증)에 투표한 나무의 비율이 곧 중증 확률이다.

# 전체 549개 샘플에 중증 확률 예측
severe_proba = rf_model.predict_proba(cytokine_df)[:, 1]
#                                                   ↑
#                                        클래스 1(중증)의 확률

# 결과: 0~1 사이의 실수값
#   0.01 → 건강인과 매우 비슷한 사이토카인 패턴
#   0.95 → 중증 환자와 매우 비슷한 사이토카인 패턴

이 확률값이 왜 중요한가? 모델은 “건강 vs 중증"만 배웠지만, 중간 상태의 샘플에 적용하면 흥미로운 일이 벌어진다. 중등도 악화기(mDP) 환자의 사이토카인 패턴은 건강인보다는 중증에 가깝지만 완전한 중증은 아니다. 모델은 이 패턴에 대해 0.3이나 0.5 같은 중간 확률을 내놓을 것이다. 중등도 회복기(mRP) 환자는 면역 반응이 가라앉고 있으므로 건강인에 더 가까운 패턴을 보이고, 0.1이나 0.2 같은 낮은 확률을 받을 것이다. 중증 악화기(sDP)는 사이토카인 폭풍 한가운데에 있으므로 0.9나 0.95 같은 높은 확률을 받을 것이다.

PPG별 중증 확률 분포 예상:

Severe_Prob
 1.0 ┤  · sDP ·  ← 중증 + 악화 중 → 높은 중증 확률 예상
     ┤  · sRP ·  ← 중증 + 회복 중 → 중증 확률 어느 정도
 0.5 ┤  · mDP ·  ← 중등도 + 악화 중 → 중간
     ┤  · mRP ·  ← 중등도 + 회복 중 → 낮음
 0.0 ┤  · Healthy ← 건강군 → 매우 낮음

→ Severe_Prob의 분포가 PPG 레이블링과 일치하면
  "레이블링이 타당함"을 간접적으로 검증

이렇게 되면 0에서 1 사이의 연속적인 점수가 PPG 네 그룹의 순서를 자연스럽게 반영하는 좌표축이 된다. 건강인은 0 근처, mRP는 낮은 영역, mDP는 중간, sRP는 중상위, sDP는 상위에 분포할 것이다. 모델에게 PPG를 직접 가르치지 않았는데도 사이토카인 패턴만으로 PPG 순서가 재현되는 것이다. 이건 PPG 레이블링이 사이토카인의 실제 생물학적 변화와 일치한다는 간접적 증거다.

#

#8 레이블링 검증: 모든 지표가 같은 방향을 가리키는가

이 중증 확률과 임상 지표들 사이의 상관관계를 분석해서 전체 시스템의 일관성을 검증한다. (Supplementary Figure 2)

from scipy.stats import pearsonr

corr_pairs = [
    ('Severity', 'NLR',         예상 r=0.653),
    ('Severity', 'LDH',         예상 r=0.634),
    ('Severity', 'Severe_Prob', 예상 r=0.569),
    ('NLR',      'LDH',         예상 r=0.396),
    ('NLR',      'Severe_Prob', 예상 r=0.463),
    ('LDH',      'Severe_Prob', 예상 r=0.508),
]

WHO와 중증 확률의 피어슨 상관이 0.569(p < 0.001)다. 모델이 높은 중증 확률을 준 샘플이 실제로 높은 WHO 점수를 가지고 있다는 뜻이다. 사이토카인 패턴으로 계산한 점수가 의사가 병상에서 매긴 임상 등급과 일치한다.

NLR과 중증 확률의 상관이 0.463(p < 0.001)이다. PPG 레이블링의 핵심 기준인 NLR과 모델의 예측이 같은 방향을 가리킨다. LDH와 중증 확률의 상관은 0.508(p < 0.001)이다. 조직 손상 지표와도 일치한다.

이 결과가 말하는 것은 세 층위의 일관성이다. 첫째 층위는 사이토카인 패턴이다. 166개 단백질의 농도 조합. 둘째 층위는 모델의 중증 확률이다. 사이토카인 패턴에서 추출한 요약 점수. 셋째 층위는 임상 지표다. WHO, NLR, LDH. 이 세 층위가 모두 같은 방향을 가리킨다. 사이토카인이 심하게 변한 샘플일수록 모델이 높은 점수를 주고, 실제 임상 상태도 심각하다. 어느 한 곳에서 불일치가 나타났다면 전처리, 레이블링, 또는 모델 중 어딘가에 문제가 있다는 신호였을 것이다. 모든 곳에서 일치가 나타났으므로 파이프라인 전체가 생물학적 현실을 올바르게 반영하고 있다는 확인이다.

WHO 점수 vs 중증 확률:  r = 0.569 ***
  → 모델이 높은 점수를 준 샘플이 실제로 WHO 점수도 높음
  → "중증 확률"이 임상적 중증도를 반영함을 확인

NLR vs 중증 확률:       r = 0.463 ***
LDH vs 중증 확률:       r = 0.508 ***
  → Step 2의 PPG 레이블링 기준(NLR, LDH)과
    모델의 예측 점수가 같은 방향을 가리킴

→ 결론: 사이토카인 패턴 → 중증 확률(Severe_Prob) → NLR/LDH/WHO
         세 층위가 모두 일관성 있게 연결됨

#

#9 정리

1단계 전처리는 원시 데이터에서 기술적 잡음을 제거하고 생물학적 신호만 남겼다. 2단계 PPG 레이블링은 각 샘플에 “어디에 있고 어느 방향으로 가는가"라는 의미 있는 정답지를 붙였다. 3단계인 이 단계는 깨끗한 데이터와 의미 있는 레이블을 이용해서 사이토카인 패턴과 중증도의 관계를 학습하는 모델을 만들었다.

[전처리 완료 데이터]
  cytokine_data (549 × 166)
  metadata (WHO, NLR, LDH, PPG)
[훈련 데이터 선별]
  건강군 25명 + 중증군(WHO≥6) 35명 = 60개
[80/20 분할]
  Train 48개 / Test 12개 (stratify 적용)
[Random Forest 학습]
  n_estimators=100, criterion='gini'
[성능 평가]
  Train: 100%, Test: 100%
  5-fold CV: 100% (전 폴드)
[전체 549개에 중증 확률 예측]
  severe_proba = rf_model.predict_proba(X)[:, 1]
[PPG 레이블링 검증]
  WHO vs Severe_Prob: r=0.569 ***
  NLR vs Severe_Prob: r=0.463 ***
  LDH vs Severe_Prob: r=0.508 ***
저장:
  ./model/rf_model.pkl
  metadata에 Severe_Prob 컬럼 추가

하지만 아직 핵심 질문에 답하지 못했다. “어떤 사이토카인이 중요한가?” 랜덤 포레스트가 100%로 분류한다는 건 알겠는데, 166개 중 어떤 것이 결정적인 역할을 했는지, 각 환자에서 어떤 사이토카인이 어떤 방향으로 작용했는지는 아직 모른다. 이 질문에 답하는 것이 다음 단계인 SHAP 분석의 역할이다. 랜덤 포레스트가 해석 가능한 모델이라는 장점은 SHAP과 결합할 때 비로소 완전히 발휘된다.