DL

Deep Learning #


2026-03-03 ⋯ BERT 뉴스 분류 #8 모델 성능평가 - Precision / Recall / F1

평균의 함정 전체 F1이 0.9482라고 했다. 이 숫자는 네 클래스의 가중 평균이다. 평균이라는 건 잘하는 쪽과 못하는 쪽이 서로 상쇄된다는 뜻이다. 시험에서 수학 100점, 영어 100점, 국어 100점, 과학 70점을 받으면 평균 92.5점이다. 꽤 좋은 점수다. 하지만 이 학생의 약점이 과학이라는 사실은 평균만 봐서는 보이지 않는다. 클래스별 세부 지표를 보는 이유가 정확히 이것이다. Precision / Recall / F1 | 클래스 | Precision | Recall | F1 | 해석 | |---|:---:|:---:|:---:|---| | **World** | **1.0000** | 0.9467 | 0.9726 | 예측이면 무조건 맞음, 일부 World를 다른 클래스로 분류 | | **Sports** | 0.9886 | **1.0000** | **0.9943** | 거의 완벽, Sports 뉴스는 하나도 안 놓침 | | **Business** | 0.8197 | 0.9259 | 0.8696 | Precision 낮음 = Sci/Tech가 Business로 잘못 유입 | | **Sci/Tech** | 0.9524 | 0.9091 | 0.9302 | Recall 낮음 = Sci/Tech 일부가 Business로 누락 | Sports부터 보자. F1이 0.9943으로 거의 완벽하다. Precision이 0.9886이고 Recall이 1.0000이다. Recall이 1.0이라는 건 실제 스포츠 기사 76건을 단 하나도 놓치지 않고 전부 잡아냈다는 뜻이다. Precision이 0.9886이라는 건 모델이 스포츠라고 판정한 것 중 하나만 실제로는 스포츠가 아니었다는 뜻이다. 혼동 행렬에서 봤듯이 스포츠는 어휘가 너무 독특해서 모델이 쉽게 구별한다. World는 흥미로운 패턴을 보인다. Precision이 1.0000이고 Recall이 0.9467이다. Precision이 1.0이라는 건 모델이 "이건 World 기사야"라고 판정하면 100% 맞다는 뜻이다. 한 번도 틀린 적이 없다. 하지만 Recall이 0.9467이라는 건 실제 World 기사 78건 중 75건만 잡아내고 3건을 놓쳤다는 뜻이다. 모델이 World를 예측할 때는 매우 보수적으로 행동하는 것이다. 확실한 것만 World라고 말하고, 조금이라도 애매하면 다른 클래스로 분류한다. 그래서 예측하면 무조건 맞지만 일부를 놓친다. Sci/Tech는 Precision이 0.9524로 꽤 높고 Recall이 0.9091로 상대적으로 낮다. 모델이 Sci/Tech라고 판정하면 95%는 맞지만, 실제 Sci/Tech 기사의 9%가 Business로 빠져나간다. 혼동 행렬에서 본 10건의 유출이 여기서 Recall을 깎아먹고 있는 것이다. Business가 가장 문제다. Precision이 0.8197로 네 클래스 중 유일하게 0.9 아래다. 모델이 Business라고 판정한 것 중 약 18%가 실제로는 Business가 아니다. 주로 Sci/Tech 기사가 Business로 잘못 유입된 것이다. Recall은 0.9259로 나쁘지 않다. 실제 Business 기사 대부분은 잡아내지만, 다른 클래스의 기사가 Business 예측에 섞여 들어와서 Precision이 떨어지는 구조다. AUROC / AUPRC | 클래스 | AUROC | AUPRC | 의미 | |---|:---:|:---:|---| | **World** | 0.9854 | 0.9815 | 매우 좋음 | | **Sports** | **0.9995** | **0.9988** | 거의 완벽 (분류 경계가 명확) | | **Business** | 0.9827 | 0.8778 | AUPRC가 상대적으로 낮음 | | **Sci/Tech** | 0.9931 | 0.9816 | 좋음 | AUROC를 클래스별로 보면 Sports가 0.9995로 거의 1.0이고, 나머지도 0.98~0.99 범위에서 모두 훌륭하다. AUROC 관점에서는 네 클래스 모두 잘 구분되고 있다. 하지만 AUPRC에서 차이가 드러난다. Sports는 0.9988, World는 0.9815, Sci/Tech는 0.9816인데, Business만 0.8778로 눈에 띄게 낮다. AUROC와 AUPRC 사이에 이렇게 큰 격차가 벌어지는 이유가 있다. AUROC는 양성과 음성의 순위만 본다. Business 기사의 점수가 비Business 기사의 점수보다 대체로 높기만 하면 AUROC는 좋게 나온다. 하지만 AUPRC는 모델이 높은 확률을 부여한 예측들의 정밀도를 더 엄격하게 평가한다. Business의 경우 Sci/Tech 기사 중 기술 기업 관련 기사들이 꽤 높은 Business 확률을 받는다. 이 기사들이 AUROC에서는 큰 영향을 미치지 않지만 AUPRC에서는 Precision을 직접 깎아먹는다. 비유하자면 AUROC는 "전체적으로 순서가 맞는가"를 보고, AUPRC는 "상위권에 엉뚱한 게 섞여 있는가"를 본다. Business 예측의 상위권에 Sci/Tech 기사가 섞여 있으니 AUPRC가 떨어지는 것이다. 이 분석이 개선 방향을 어떻게 알려주는가 각 클래스의 지표가 서로 다른 패턴을 보이므로, 개선 전략도 클래스별로 달라야 한다. Business의 Precision을 올리려면 Sci/Tech 기사가 Business로 잘못 유입되는 것을 막아야 한다. Business 뉴스만의 고유한 특징, 예를 들어 "revenue", "quarterly earnings", "stock price", "merger" 같은 순수 비즈니스 어휘를 모델이 더 강하게 인식하도록 학습시키는 것이 방법이다. FinBERT처럼 금융 텍스트로 사전학습된 모델을 쓰면 이런 어휘의 표현이 이미 더 정교하게 학습되어 있어서 효과적일 수 있다. Sci/Tech의 Recall을 올리려면 Sci/Tech 기사가 Business로 빠져나가는 것을 막아야 한다. 특히 "기술 기업의 경영 뉴스"처럼 두 카테고리의 경계에 있는 기사들을 더 많이 학습시켜서, 모델이 이 경계에서 더 세밀한 판단을 할 수 있게 만들어야 한다. 이전 망막증 프로젝트에서 혼동 행렬을 보고 "등급 2와 3의 경계가 문제"라는 진단을 내린 것과 같은 구조다. 전체 점수는 대략적인 건강 상태를 알려주고, 클래스별 지표는 정확히 어디가 아픈지를 짚어주고, 그 진단을 바탕으로 구체적인 치료 방향을 잡는 것이다.


2026-03-03 ⋯ BERT 뉴스 분류 #7 모델 성능평가 - Confusion matrix

F1이 0.9482라는 건 알겠다. 하지만 이 숫자 하나로는 모델이 어디서 잘하고 어디서 못하는지를 알 수 없다. 네 카테고리 모두를 골고루 95% 맞추는 모델과, 세 카테고리는 100% 맞추고 한 카테고리는 80%만 맞추는 모델이 같은 F1을 가질 수 있다. 둘의 약점은 완전히 다른데도 전체 점수로는 구분이 안 된다. 혼동 행렬은 이 한계를 해결한다. 실제 정답과 모델 예측을 4×4 격자로 교차 집계해서 "어떤 클래스를 어떤 클래스로 오분류했는가"를 빠짐없이 보여준다. 이전 망막증 챕터에서 5×5 혼동 행렬로 어떤 등급 쌍에서 오분류가 집중되는지를 진단했던 것과 같은 도구다. confusion matrix 읽기 confusion matrix는 4개 클래스 각각에 대해 "실제 정답"과 "모델 예측"을 교차 집계한 표다. 행렬의 각 행은 실제 정답 클래스이고, 각 열은 모델이 예측한 클래스다. 대각선에 있는 숫자는 모델이 정확히 맞힌 것이고, 대각선 바깥에 있는 숫자는 오분류다. | | World | Sports | Business | Sci/Tech | |---|:---:|:---:|:---:|:---:| | **World** | **75** | 1 | 2 | 0 | | **Sports** | 0 | **76** | 0 | 0 | | **Business** | 0 | 0 | **75** | 6 | | **Sci/Tech** | 0 | 0 | 10 | **59** | World 행을 보자. 실제 World 기사 78건 중 75건을 World로 정확히 분류했고, 1건을 Sports로, 2건을 Business로 잘못 분류했다. Sci/Tech로 잘못 분류한 건은 0건이다. Sports 행은 완벽하다. 실제 Sports 기사 76건을 전부 Sports로 맞혔다. 오분류가 단 한 건도 없다. Business 행을 보면 81건 중 75건을 맞혔지만 6건을 Sci/Tech로 잘못 분류했다. Sci/Tech 행은 69건 중 59건만 맞혔고, 10건을 Business로 잘못 분류했다. 여기서 뚜렷한 패턴이 보인다. 스포츠 기사는 다른 카테고리와 어휘가 거의 겹치지 않는다. "touchdown", "championship", "quarterback", "goal", "match" 같은 단어들은 스포츠에서만 등장한다. BERT의 Self-Attention이 이런 단어들을 보면 다른 모든 맥락을 볼 필요도 없이 스포츠라고 확신할 수 있다. [CLS] 토큰의 768차원 벡터 안에 "이건 확실히 스포츠"라는 신호가 매우 강하게 인코딩되는 것이다. 이걸 DNA 분석과 비교하면, 특정 전사인자의 결합 모티프가 매우 뚜렷해서 Conv1D 필터가 쉽게 잡아내는 경우와 같다. 신호가 강하면 모델은 쉽게 분류한다. 전체 오분류 16건 중 무려 16건이 Business와 Sci/Tech 사이에서 발생했다. Business를 Sci/Tech로 잘못 분류한 게 6건, Sci/Tech를 Business로 잘못 분류한 게 10건이다. 이 두 카테고리가 모델에게 가장 어려운 경계라는 게 명확하다. 왜 이 둘이 혼동되는가를 생각해보면, 현실 세계에서 이 두 카테고리의 경계 자체가 모호하기 때문이다. "Apple announces new chip partnership with TSMC"라는 기사를 생각해보자. 애플이라는 기업의 사업 전략에 초점을 맞추면 Business이고, TSMC의 반도체 기술 혁신에 초점을 맞추면 Sci/Tech다. 같은 기사를 사람이 분류해도 의견이 갈릴 수 있다. 기업이 기술 혁신을 주도하는 현대 경제에서 "기업 뉴스"와 "기술 뉴스"의 경계는 본질적으로 흐릿하다. 오분류 사례들의 신뢰도를 보면 이 해석이 더 확실해진다. Business를 Sci/Tech로 잘못 분류한 사례의 확신도가 0.38, Sci/Tech를 Business로 잘못 분류한 사례의 확신도가 0.40이다. 4개 클래스의 균등 확률이 0.25이니까, 0.38~0.40은 "아주 약간 이쪽 같은데 확신은 없다"는 수준이다. 모델 자체도 이 경계 사례들이 어렵다는 걸 인지하고 있는 셈이다. 이건 이전 Step 3에서 본 ECE 문제와도 연결된다. 모델이 낮은 확신도로 내린 예측들이 실제로 많이 틀린다면, 이 낮은 확신도의 예측들을 별도로 처리하는 전략이 효과적일 수 있다. "확신도 0.5 미만은 사람이 검토한다"는 규칙을 세우면 Business-Sci/Tech 경계의 오분류 대부분을 걸러낼 수 있다. recall 분석 | 클래스 | 맞힌 수 / 전체 | 재현율 | |---|---|---| | World | 75 / 78 | **0.96** | | Sports | 76 / 76 | **1.00** | | Business | 75 / 81 | **0.93** | | Sci/Tech | 59 / 69 | **0.86** | 각 클래스의 재현율(실제 해당 클래스 중 모델이 정확히 잡아낸 비율)을 보면, Sports가 1.00으로 완벽하고, World가 0.96, Business가 0.93, Sci/Tech가 0.86으로 가장 낮다. Sci/Tech 기사 69건 중 10건이 Business로 빠져나갔기 때문이다. 방향성도 주목할 만하다. Sci/Tech에서 Business로의 유출(10건)이 Business에서 Sci/Tech로의 유출(6건)보다 많다. 이건 기술 기사에 등장하는 기업명, 시장 규모, 투자 관련 어휘가 모델을 Business 쪽으로 끌어당기기 때문일 수 있다. "TSMC plans $40 billion investment in new chip fabrication facility"라는 문장에서 "$40 billion investment"라는 표현이 Business의 강한 신호로 작용하는 것이다. 이전 망막증 혼동 행렬에서 등급 2(중등도)와 등급 3(중증)이 혼동되었던 것과 같은 패턴이다. 인접한 카테고리, 즉 의미적으로 가까운 카테고리 사이에서 오분류가 집중된다. 망막증에서는 질병의 심각도가 연속적이어서 인접 등급이 혼동되었고, 뉴스 분류에서는 기술과 비즈니스라는 주제가 현실에서 밀접하게 연관되어 있어서 혼동되는 것이다. 정리 혼동 행렬이 알려주는 핵심 메시지는 "모델의 약점이 어디에 있는가"다. F1 0.9482라는 전체 점수만 보면 모델이 전반적으로 훌륭하다는 것만 안다. 혼동 행렬을 보면 "Sports는 완벽하고, World도 거의 완벽하고, 문제는 Business-Sci/Tech 경계에 집중되어 있다"는 구체적인 진단이 나온다. 이 진단이 있어야 개선 방향을 잡을 수 있다. 모든 카테고리의 데이터를 무차별적으로 늘리는 대신, Business-Sci/Tech 경계의 애매한 기사들을 집중적으로 추가해서 학습시킬 수 있다. 또는 이 두 카테고리에 특화된 사전학습 모델을 쓰거나, 아예 두 카테고리를 "Business/Tech"로 합쳐서 3클래스 분류로 바꾸는 것도 검토할 수 있다. 혼동 행렬 없이는 이런 구체적인 전략을 세울 수 없다.


2026-03-03 ⋯ BERT 뉴스 분류 #6 모델 성능평가 - 핵심 지표

핵심 성능 비교 | 지표 | Baseline | Enhanced | 향상폭 | |:----:|:--------:|:--------:|:------:| | Accuracy | 0.9145 | **0.9474** | +0.0329 | | Precision | 0.9172 | **0.9509** | +0.0337 | | Recall | 0.9145 | **0.9474** | +0.0329 | | F1 | 0.9146 | **0.9482** | +0.0336 | | AUROC (macro) | - | **0.9902** | - | | AUPRC (macro) | - | **0.9599** | - | 먼저 가장 기본적인 질문부터. 우리가 추가한 기법들(EnhancedClassifier, 학습률 전략, Label Smoothing 등)이 실제로 효과가 있었는가? Baseline은 기본 BERT에 단순한 Linear(768→4) 분류 헤드를 얹고 별다른 기법 없이 학습한 모델이다. Enhanced는 지금까지 설명한 모든 기법을 적용한 모델이다. Accuracy가 0.9145에서 0.9474로, F1이 0.9146에서 0.9482로 올랐다. 약 3.3%포인트의 향상이다. 3.3%포인트라는 숫자가 작아 보일 수 있다. 하지만 이미 91%라는 높은 성능에서의 3.3%포인트 향상은 쉽지 않다. 90%에서 93%로 올리는 것은 50%에서 53%로 올리는 것과 차원이 다르다. 남은 오류의 약 3분의 1을 줄인 셈이다. 이전 챕터들에서도 봤듯이 높은 성능 구간에서의 소수점 개선이 실제 서비스에서는 수천, 수만 건의 오분류 감소를 의미한다. 각 지표를 왜 봐야 하는가? - Accuracy vs F1 Accuracy는 가장 직관적인 지표다. 304개의 테스트 기사 중 288개를 맞췄으면 Accuracy는 0.9474다. 단순하고 이해하기 쉽다. 하지만 Accuracy에는 함정이 있다. 이전 망막증 챕터에서 봤던 문제를 떠올려보자. 등급 0이 25,000장이고 등급 3이 800장인 데이터에서, 모델이 무조건 "등급 0"이라고 답해도 Accuracy가 70%를 넘는다. 아무것도 학습하지 않았는데도 높은 Accuracy가 나오는 것이다. F1은 이 문제를 해결한다. Precision(모델이 특정 클래스라고 판정한 것 중 실제로 맞는 비율)과 Recall(실제 특정 클래스인 것 중 모델이 잡아낸 비율)의 조화평균이다. 조화평균은 두 값 중 하나라도 낮으면 전체가 크게 떨어지는 성질이 있다. Precision이 0.99인데 Recall이 0.01이면 산술평균은 0.50이지만 조화평균은 0.02에 불과하다. 한쪽만 높여서는 F1을 속일 수 없다는 뜻이다. AG News에서는 Accuracy와 F1이 거의 같다. 0.9474와 0.9482로 0.001도 차이나지 않는다. 이건 4개 클래스가 균등하게 분포되어 있기 때문이다. 클래스 불균형이 없으면 Accuracy와 F1이 자연스럽게 수렴한다. 하지만 둘 다 보고해야 하는 이유는, 데이터 분포가 바뀔 수 있는 실제 서비스 환경에서는 두 지표가 벌어질 수 있기 때문이다. 특정 카테고리의 뉴스가 폭증하면 Accuracy는 유지되는데 F1이 떨어지는 상황이 올 수 있다. 각 지표를 왜 봐야 하는가? - AUROC vs AUPRC AUROC가 0.9902다. 이전 DNA 분석에서 ROC-AUC를 평가 지표로 썼던 것과 같은 지표다. 그때 설명했듯이, "임의로 고른 양성 샘플의 예측 점수가 임의로 고른 음성 샘플의 예측 점수보다 높을 확률"이다. 4클래스 문제에서는 각 클래스를 "이 클래스 vs 나머지"로 나눠서 AUROC를 각각 계산한 뒤 평균을 낸다. 이것이 macro 평균이다. 0.9902라는 값은 모델이 네 카테고리를 거의 완벽하게 구분하고 있다는 뜻이다. 스포츠 기사의 점수 분포와 경제 기사의 점수 분포가 거의 겹치지 않는다는 것이다. AUROC의 장점은 분류 임계값(threshold)에 독립적이라는 것이다. Accuracy나 F1은 "가장 높은 확률의 클래스를 정답으로 선택한다"는 특정 결정 규칙에 묶여 있다. AUROC는 모든 가능한 임계값에서의 성능을 종합하므로, 모델의 근본적인 구별 능력을 측정한다. AUPRC가 0.9599다. AUROC와 비슷해 보이지만 더 엄격한 기준이다. AUROC와 AUPRC의 차이를 이해하려면 극단적인 예를 생각해보자. 1,000개의 샘플 중 양성이 10개뿐인 상황에서, 모델이 상위 100개를 양성으로 예측했고 그 안에 10개의 실제 양성이 모두 포함되어 있다면 AUROC는 매우 높다. 하지만 AUPRC는 Precision(10/100=0.10)이 낮다는 걸 반영해서 더 낮은 값을 준다. AUPRC는 양성 예측의 정밀도를 더 강하게 평가하는 것이다. AG News에서 AUPRC가 0.9599로 AUROC(0.9902)보다 약간 낮지만 여전히 매우 높다. 이건 모델이 높은 확률을 부여한 예측들이 실제로도 거의 다 맞다는 뜻이다. cf 모델 설정 요약 | 항목 | 값 | |---|---| | 기반 모델 | bert-base-uncased | | 분류기 | EnhancedClassifier (768→256→4, GELU, Dropout=0.3) | | 최대 토큰 길이 | 192 | | 학습률 전략 | Layer-wise Discriminative LR (base=3.5e-5, decay=0.9) | | Label Smoothing | ε=0.1 | | LR 스케줄러 | Cosine + Warmup 0.1 | | Early Stopping | patience=3 | | 학습 데이터 | Train=4,320 / Val=480 / Test=304 (1/25 스케일) | | 학습 steps | 40 (Early Stopping 동작) |


2026-03-01 ⋯ RF 사이토카인 SHAP 분석 #4 SHAP + UMAP + DBSCAN + 통계 분석

랜덤 포레스트 모델이 건강인과 중증 환자를 100% 정확도로 구별한다. 하지만 "왜?"라는 질문에는 아직 답하지 못했다. 166개 사이토카인 중 어떤 것이 결정적인 역할을 했는가? 모든 환자에서 같은 사이토카인이 중요한가, 아니면 환자마다 다른가? 악화 단계와 회복 단계에서 중요한 사이토카인이 달라지는가? 중등도와 중증에서 다른 메커니즘이 작동하는가? 이 단계는 이 모든 질문에 답하는 분석 단계다. 모델의 블랙박스를 열어서 내부를 들여다보고, 통계적으로 엄밀하게 검증하고, 최종적으로 중증 COVID-19의 면역학적 특징을 밝혀낸다. | 분석 | 방법 | 목적 | |------|------|------| | 사이토카인 기여도 분해 | SHAP (TreeExplainer) | 샘플별 사이토카인 중요도 | | 고차원 시각화 | UMAP | 166차원 SHAP 공간 → 2D | | 샘플 군집화 | DBSCAN | 면역 패턴이 비슷한 샘플 묶기 | | 차등 발현 검정 | Welch's t-검정 + BH FDR | DP vs RP 사이토카인 발굴 | | 중증도별 비교 | ΔRank 분석 | 중등도/중증 특이적 바이오마커 | SHAP: 각 사이토카인의 기여도를 분해하기 SHAP을 이해하는 가장 좋은 비유는 팀 스포츠다. 야구팀이 7대 3으로 이겼다. 4점 차이가 났다. 이 4점의 승리에 각 선수가 얼마나 기여했는가? 투수가 상대 타선을 억제해서 2점을 막았고, 4번 타자가 타점을 올려서 1.5점을 보탰고, 2번 타자가 출루로 0.8점을 기여했고, 유격수가 실책으로 0.3점을 까먹었다. 이런 식으로 팀 전체의 성과를 각 선수의 기여분으로 정확하게 분해할 수 있다면 팀을 이해하는 데 엄청나게 유용할 것이다. SHAP이 정확히 이 일을 한다. 어떤 샘플의 중증 확률이 0.82라고 하자. 전체 샘플의 평균 중증 확률이 0.45다. 이 샘플이 평균보다 0.37만큼 높은 이유를 166개 사이토카인 각각의 기여분으로 분해한다. CD274이 높아서 +0.15, FLT3LG가 높아서 +0.08, CXCL10이 높아서 +0.09, TNFSF10이 낮아서 +0.05(역방향 기여)... 이런 식으로 166개의 기여분을 다 더하면 정확히 0.37이 된다. 0.45 + 0.37 = 0.82로 원래 예측값과 일치한다. 이 분해가 수학적으로 정확하다는 점이 SHAP의 핵심이다. 대충 추정하는 게 아니라, 게임 이론의 샤플리 값이라는 수학적 프레임워크에 기반해서 유일하게 공정한 분배를 계산한다. 샤플리 값의 계산은 다음과 같다. 샤플리 값의 계산 원리를 직관적으로 이해해보자. CD274의 기여도를 알고 싶다. 핵심 질문은 "CD274이 있을 때와 없을 때 예측이 얼마나 달라지는가?"다. 그런데 CD274의 기여도는 다른 사이토카인이 이미 고려되었느냐에 따라 달라진다. CD274 혼자만 있을 때의 기여와, FLT3LG와 CXCL10이 이미 있는 상태에서 CD274을 추가했을 때의 기여가 다를 수 있다. 샤플리 값은 가능한 모든 조합을 고려한다. CD274을 아무것도 없는 상태에 추가했을 때의 기여, FLT3LG만 있는 상태에 추가했을 때의 기여, FLT3LG와 CXCL10이 있는 상태에 추가했을 때의 기여... 이 모든 경우의 기여를 평균 낸 것이 CD274의 샤플리 값이다. 166개 사이토카인의 모든 부분집합을 고려하면 2의 166제곱 가지 조합이 되므로, 이걸 직접 계산하는 건 불가능하다. 하지만 TreeExplainer라는 알고리즘이 결정 트리의 구조를 활용한 동적 프로그래밍으로 이 계산을 정확하고 빠르게 수행한다. 트리 구조가 이미 "어떤 특성이 어떤 순서로 사용되었는가"를 인코딩하고 있기 때문에, 모든 부분집합을 일일이 열거하지 않아도 같은 결과를 얻을 수 있다. 이것이 랜덤 포레스트를 선택한 또 다른 이유다. 신경망에서는 이런 정확한 SHAP 계산이 불가능하고 근사치만 구할 수 있다. cf feature importance와의 차이 랜덤 포레스트에는 원래 내장된 feature importance가 있다. 각 사이토카인이 트리 분기에서 Gini 불순도를 평균적으로 얼마나 줄였는지를 측정한 값이다. 하지만 이 값은 "CD274이 평균적으로 중요하다"는 것만 알려준다. 방향이 없고, 샘플 수준의 해석이 불가능하다. SHAP은 두 가지를 추가한다. 첫째, 방향이 있다. CD274의 SHAP 값이 +0.15라면 이 사이토카인이 중증 확률을 올리는 방향으로 기여했다는 뜻이고, -0.05라면 낮추는 방향으로 기여했다는 뜻이다. 둘째, 샘플 수준이다. 환자 P010의 2일째 채혈에서 CD274이 어떻게 기여했는지를 구체적으로 알 수 있다. 다른 환자에서는 CD274의 기여 방향과 크기가 다를 수 있다. 이 샘플별 기여도가 549개 샘플 × 166개 사이토카인 = (549, 166) 크기의 행렬로 나온다. SHAP 절대값의 평균을 구해서 정렬하면 전체적으로 가장 중요한 사이토카인 순위가 나온다. 1위는 CD274(PD-L1)이다. 면역 체크포인트 분자로, T세포의 활성을 억제하는 역할을 한다. 중증 환자에서 이 분자가 높다는 건 면역 시스템이 스스로를 억제하고 있다는 뜻이다. 바이러스와 싸워야 하는데 브레이크가 걸려 있는 상황이다. 2위 KIT은 줄기세포 인자 수용체, 3위 FLT3LG는 수지상세포와 NK세포의 성장인자, 4위 GDF15는 조직 손상 지표, 5위 FAP은 조직 염증 관련 단백질이다. 상위 사이토카인들이 면역 억제, 면역세포 동원, 조직 손상이라는 세 가지 축을 반영하고 있어서, 모델이 임상적으로 의미 있는 패턴을 학습했음을 보여준다. | 순위 | 사이토카인 | 역할 | |------|-----------|------| | 1 | **CD274 (PD-L1)** | 면역 체크포인트 → T세포 억제 | | 2 | **KIT** | 줄기세포 인자 수용체 | | 3 | **FLT3LG** | 수지상세포·NK세포 성장인자 | | 4 | **GDF15** | 스트레스 반응 → 조직 손상 지표 | | 5 | **FAP** | 섬유모세포 활성화 → 조직 염증 | | 6 | KLK6 | 칼리크레인 프로테아제 | | 7 | HGF | 간세포 성장인자 → 폐 손상 시 상승 | | 8 | LBP | 내독소 결합 단백질 | | 9 | **PTX3** | 급성기 반응 단백질 | | 10 | GPNMB | 당단백질 NMB | UMAP 차원 축소 각 샘플은 이제 166개의 SHAP 값으로 표현된다. 이건 166차원 공간의 한 점이다. 비슷한 면역 패턴을 가진 샘플들은 이 공간에서 가까이 있을 것이다. 하지만 166차원을 눈으로 볼 수 없다. 2차원으로 압축해야 한다. 왜 사이토카인 발현량이 아니라 SHAP 값으로 UMAP을 돌리는가? 사이토카인 발현량으로 UMAP을 돌리면 "CD274이 절대적으로 얼마인가"를 기준으로 샘플을 배치한다. 하지만 같은 CD274 수치라도 다른 사이토카인과의 조합에 따라 의미가 달라질 수 있다. CD274이 높지만 IL-10도 높으면 면역 조절이 작동하는 것이고, CD274만 높으면 일방적 면역 억제다. SHAP 값으로 UMAP을 돌리면 "CD274이 중증 예측에 얼마나 기여했는가"를 기준으로 배치한다. 이건 절대적 수치가 아니라 모델이 해석한 기능적 의미다. 같은 면역 메커니즘이 작동하는 샘플들이 가까이 모이게 된다. 발현량 공간에서는 섞여 있던 샘플들이 SHAP 공간에서는 깔끔하게 분리될 수 있다. 측정 잡음에 덜 민감하고 생물학적으로 더 의미 있는 군집 구조가 드러난다. UMAP은 고차원 공간에서 점들 사이의 이웃 관계를 파악한 다음, 이 이웃 관계가 최대한 보존되도록 2차원에 점들을 재배치한다. 166차원에서 가까웠던 점들은 2차원에서도 가깝게, 멀었던 점들은 멀게 놓는다. n_neighbors=15는 각 점의 이웃 15개를 기준으로 지역 구조를 파악한다는 뜻이다. 이 값이 작으면 아주 가까운 이웃만 보므로 세밀한 지역 구조가 살아나지만 전체 그림이 파편화될 수 있다. 크면 멀리 있는 이웃까지 보므로 전체적인 구조는 보존되지만 세밀한 차이가 뭉개질 수 있다. 15는 이 둘의 균형점이다. min_dist=0.1은 2차원에서 점들이 얼마나 촘촘히 모일 수 있는지를 제어한다. 작으면 군집이 빽빽하게 뭉쳐서 군집 경계가 선명해지고, 크면 점들이 퍼져서 부드러운 분포가 된다. 0.1은 군집 구조를 볼 수 있을 만큼은 뭉치되 개별 점들이 완전히 겹치지는 않는 수준이다. 건강 대조군은 제외하고 환자 404명의 샘플만 UMAP에 넣는다. 건강인의 SHAP 패턴은 환자들과 너무 달라서 포함하면 UMAP이 "건강인 vs 환자"라는 거대한 차이에 압도되어 환자들 사이의 미세한 차이가 보이지 않게 된다. 결과적으로 (404, 166) 행렬이 (404, 2) 좌표로 변환된다. 각 샘플이 2차원 평면 위의 한 점이 된다. DBSCAN 클러스터링: 비슷한 면역 패턴끼리 자동으로 묶기 UMAP으로 2차원에 샘플들을 뿌렸다. 이제 mDP vs mRP를 비교해서 악화 단계와 회복 단계에서 어떤 사이토카인이 다른지를 찾고 싶다. 전체 mDP와 전체 mRP를 바로 비교하면 안 되는가? 안 된다. 두 가지 이유가 있다. 첫째, 중증 환자(sDP, sRP)의 사이토카인 패턴이 중등도 환자와 질적으로 다르다. 전체를 섞어서 비교하면 중증 환자의 극단적 값이 중등도의 미세한 DP/RP 차이를 가려버린다. 둘째, 환자 간 개인차가 크다. 나이, 기저질환, 바이러스 변이 유형에 따라 면역 반응이 다르다. 너무 이질적인 샘플들을 한꺼번에 비교하면 통계적 신호가 잡음에 묻힌다. 해결책은 먼저 비슷한 샘플들을 군집으로 묶고, 각 비교에 적합한 군집만 골라서 분석하는 것이다. UMAP이 면역 패턴이 비슷한 샘플들을 가까이 배치했으므로, 가까이 모인 점들을 하나의 군집으로 묶으면 "면역학적으로 비슷한 환자 그룹"이 자연스럽게 형성된다. cf DBSCAN이 K-means보다 나은 이유 K-means를 쓰려면 군집 수를 미리 정해야 한다. 4개? 6개? 10개? 정답을 모른다. 데이터가 자연스럽게 형성하는 군집이 몇 개인지를 사전에 알 수 없다. 게다가 K-means는 원형 군집만 잘 찾는다. UMAP의 출력에서 군집이 초승달 모양이나 불규칙한 형태를 가질 수 있는데, K-means는 이런 형태를 다루지 못한다. 또한 K-means는 이상점(어느 군집에도 속하지 않는 특이한 샘플)도 억지로 어떤 군집에 배정한다. DBSCAN은 이 세 가지 문제를 모두 해결한다. 밀도가 높은 영역을 자동으로 군집으로 인식하므로 군집 수를 미리 정하지 않아도 된다. 밀도 기반이므로 군집의 형태에 제약이 없다. 밀도가 낮은 곳에 있는 점은 노이즈(-1)로 표시해서 자동으로 제외한다. DBSCAN에는 두 가지 파라미터가 있다. eps=0.8은 "이 거리 이내에 있으면 이웃으로 간주한다"는 기준이다. UMAP의 2차원 좌표에서 0.8 단위 이내에 있는 점들이 서로 이웃이다. min_samples=20은 "이웃이 20개 이상인 점만 군집의 핵심으로 인정한다"는 기준이다. 주변에 20개 이상의 점이 빽빽하게 모여 있어야 그곳이 군집의 중심부라고 판단하는 것이다. 핵심 점들이 서로 이웃이면 같은 군집에 속한다. 핵심 점은 아니지만 핵심 점의 이웃인 점은 경계 점으로 군집에 포함된다. 어떤 핵심 점의 이웃도 아닌 점은 노이즈로 분류된다. 결과적으로 11개 군집이 발견된다. 각 군집의 PPG 구성을 확인해서, mDP와 mRP가 많은 군집 3개를 중등도 분석용으로 선택하고, sDP와 sRP가 많은 군집 3개를 중증 분석용으로 선택한다. 이렇게 하면 중등도 비교에서는 중증의 극단적 패턴이 섞이지 않고, 중증 비교에서는 중등도의 패턴이 섞이지 않는다. 통계 분석 — Welch's t-검정 + BH FDR 보정 선택된 클러스터 안에서, 77개 주요 사이토카인 각각에 대해 두 가지 비교를 수행한다. mDP vs mRP(악화 단계와 회복 단계의 차이)와 sDP vs sRP(중증 악화와 중증 회복의 차이)다. Welch's t-검정을 사용한다. 일반적인 Student's t-검정은 두 그룹의 분산(데이터가 퍼진 정도)이 같다고 가정한다. 하지만 사이토카인 데이터에서 이 가정이 성립하지 않을 수 있다. 악화 단계의 CD274은 환자마다 천차만별로 퍼져 있는데, 회복 단계의 CD274은 비교적 일정한 수준으로 모여 있을 수 있다. 분산이 다른 두 그룹에 일반 t-검정을 적용하면 거짓 양성이 늘어난다. Welch's t-검정은 각 그룹의 분산을 별도로 추정해서 이 문제를 회피한다. 코드에서 equal_var=False가 이 설정이다. 여기서 심각한 통계적 함정이 있다. 77개 사이토카인에 대해 각각 t-검정을 수행하면 77번의 검정을 하는 셈이다. p < 0.05 기준을 쓰면, 두 그룹 사이에 아무런 차이가 없더라도 우연만으로 0.05 × 77 ≈ 3.85개의 사이토카인이 "유의하다"고 나올 수 있다. 20번에 1번은 우연히 유의하게 나오는데, 77번 시도하면 우연이 3~4번 일어나는 것이다. 이걸 다중 검정 문제라고 한다. Benjamini-Hochberg(BH) FDR 보정이 이 문제를 해결한다. FDR은 False Discovery Rate, 거짓 발견율이다. "유의하다고 선언한 것들 중 실제로 거짓인 비율"을 5% 이하로 통제한다. 작동 방식은 이렇다. 77개 p-value를 작은 것부터 큰 것으로 정렬한다. 가장 작은 p-value(가장 유의한 것)에는 가장 엄격한 기준 (1/77) × 0.05 = 0.00065를 적용한다. 두 번째로 작은 p-value에는 (2/77) × 0.05 = 0.00130을 적용한다. 이런 식으로 순위가 올라갈수록 기준이 점점 느슨해진다. 가장 덜 유의한 것에는 (77/77) × 0.05 = 0.05를 적용한다. 직관적으로 이해하면, 순위가 높은(가장 유의한) 사이토카인일수록 더 확실한 증거를 요구하고, 순위가 낮은 사이토카인에는 상대적으로 관대한 기준을 적용하되, 전체적으로 거짓 발견이 5%를 넘지 않도록 조절하는 것이다. 분석 결과 중등도 비교(mDP vs mRP)에서 77개 중 38개(49.4%)가 유의하다. 거의 절반의 사이토카인이 악화 단계와 회복 단계에서 통계적으로 유의한 차이를 보인다. 가장 유의한 것은 CD274(보정된 p = 2.70 × 10⁻¹³)과 FLT3LG(같은 수준)다. 이 두 사이토카인의 p-value가 극도로 작다는 건 우연일 가능성이 사실상 0이라는 뜻이다. 중증 비교(sDP vs sRP)에서는 77개 중 22개(28.6%)가 유의하다. 중등도보다 유의한 사이토카인이 적다. 이건 중증 샘플 수가 적어서(64개 vs 195개) 통계적 검정력이 낮은 것도 있지만, 중증 환자에서는 사이토카인 폭풍으로 전반적인 수준이 극도로 높아져서 DP와 RP의 상대적 차이가 덜 뚜렷해지는 생물학적 이유도 있다. ΔRank — 이 연구의 핵심 발견 여기까지는 중등도와 중증을 따로따로 분석했다. 이제 두 분석을 비교한다. 같은 사이토카인의 유의성 순위가 중등도와 중증에서 어떻게 달라지는가? ΔRank는 단순하다. 중증 비교(sDP vs sRP)에서의 유의성 순위에서 중등도 비교(mDP vs mRP)에서의 유의성 순위를 뺀다. 순위는 보정된 p-value 기준으로 1위가 가장 유의한 것이다. ΔRank가 큰 양수면, 중등도에서는 별로 중요하지 않았는데 중증에서 갑자기 중요해진 사이토카인이다. 중증 특이적 바이오마커의 후보다. ΔRank가 큰 음수면, 중등도에서는 중요했는데 중증에서는 덜 중요해진 사이토카인이다. | 사이토카인 | mDP vs mRP 순위 | sDP vs sRP 순위 | ΔRank | 해석 | |-----------|:--------------:|:--------------:|:-----:|------| | **MPO** | 67위 (비유의, P=0.84) | 12위 (P=0.007) | **+60** | 중증 특이적 | | **HGF** | 65위 (비유의, P=0.82) | 13위 (P=0.008) | **+57** | 중증 특이적 | | **TNFSF10** | 58위 (비유의, P=0.42) | 4위 (P=0.002) | **+54** | 중증 특이적 (역방향!) | | **IL3** | 64위 (비유의, P=0.81) | 17위 (P=0.019) | **+52** | 중증 특이적 | | **BDNF** | 63위 (비유의, P=0.75) | 21위 (P=0.043) | **+47** | 중증 특이적 | | LGALS9 | 4위 (P=7.07E-9) | 15위 (P=0.012) | **-11** | 중등도 특이적 | | VCAM1 | 5위 (P=1.07E-7) | 16위 (P=0.015) | **-11** | 중등도 특이적 | | S100A12 | 3위 (P=6.19E-12) | 11위 (P=0.006) | **-8** | 중등도 특이적 | 결과를 보면 가장 극적인 변화를 보이는 사이토카인이 MPO다. 중등도 비교에서 67위(보정 p = 0.84, 전혀 유의하지 않음)였는데 중증 비교에서 12위(보정 p = 0.007, 매우 유의함)로 뛰어올랐다. ΔRank가 +60이다. 중등도 환자에서는 악화기와 회복기 사이에 MPO 차이가 없지만, 중증 환자에서는 극적인 차이가 나타난다는 뜻 즉 중증 특이적 사이토카인의 발견이다. MPO는 호중구가 분비하는 효소다. 중증에서만 차이가 나타난다는 건 중증 악화에서 호중구의 과잉 활성화라는 특수한 메커니즘이 작동한다는 증거다. HGF(+57)는 간세포 성장인자로 폐 손상 시 상승한다. 중증에서만 유의해진다는 건 중증 악화에서 폐 조직 손상이 특히 심하다는 뜻이다. IL3(+52)는 면역세포 생산을 조절하는 인자다. 가장 흥미로운 발견은 TNFSF10(TRAIL)이다. 중등도에서 58위(보정 p = 0.42, 비유의)였는데 중증에서 4위(보정 p = 0.002)로 올라갔다. ΔRank가 +54다. 그런데 이 사이토카인의 발현 패턴이 다른 것들과 다르다. 보통 중증 악화에서 중요한 사이토카인은 sDP에서 높게 나타난다. 염증이 심하니까 사이토카인도 높은 것이다. 하지만 TNFSF10은 반대다. sDP에서 가장 낮다. 네 그룹 중 sDP에서만 유독 떨어진다. TNFSF10(TRAIL)의 기능을 알면 이것이 왜 중요한지 이해된다. TRAIL은 바이러스에 감염된 세포를 표적으로 삼아 세포 사멸(apoptosis)을 유도하는 단백질이다. 선천 면역의 핵심 무기 중 하나다. sDP에서 TRAIL이 낮다는 건, 중증으로 악화되는 환자에서 바이러스 감염 세포를 제거하는 메커니즘이 제대로 작동하지 않는다는 뜻이다. 면역 시스템이 감염 세포를 처리하지 못하니 바이러스가 퍼지고, 그에 대한 반응으로 호중구가 과잉 동원되고(MPO 상승), 조직이 손상되고(HGF 상승), 악순환이 가속되는 것이다. 이 발견은 같은 분석에서 나온 다른 결과와도 맞물린다. CXCL10은 sDP vs sRP에서 3위로 매우 유의하며 sDP에서 높게 나타난다. CXCL10은 염증성 케모카인으로 면역세포를 감염 부위로 불러모으는 신호다. "CXCL10은 높고(면역세포를 부르는 신호는 강하고) TNFSF10은 낮다(감염 세포를 제거하는 무기는 약하다)"라는 조합이 중증 악화의 면역학적 특징으로 떠오르는 것이다. 군대를 소집하는 명령은 내려졌는데 무기가 없는 상황과 비슷하다. 반대 방향도 있다. LGALS9는 중등도에서 4위였는데 중증에서 15위로 밀려났다(ΔRank = -11). VCAM1도 5위에서 16위로 밀려났다(ΔRank = -11). 이 사이토카인들은 중등도 환자의 악화/회복을 구별하는 데 특히 유용하지만, 중증에서는 상대적 중요도가 줄어든다. 즉 중등도 특이적 사이토카인이다. 중증에서는 사이토카인 폭풍으로 거의 모든 것이 극단적으로 올라가므로, 중등도에서 의미 있던 미세한 차이가 폭풍에 묻혀버리는 것이다. 정리 이 단계에서 수행한 다섯 가지 분석은 각각이 독립적인 것이 아니라 하나의 논리적 사슬로 연결되어 있다. SHAP이 랜덤 포레스트의 내부를 열어서 각 샘플에서 각 사이토카인의 기여도를 분해했다. UMAP이 이 기여도 공간을 2차원으로 압축해서 면역 패턴의 유사성을 눈으로 볼 수 있게 만들었다. DBSCAN이 이 2차원 공간에서 비슷한 패턴의 샘플들을 자동으로 묶어서 분석 단위를 만들었다. Welch's t-검정과 BH FDR 보정이 각 군집 안에서 악화와 회복 사이에 통계적으로 유의한 차이를 보이는 사이토카인을 엄밀하게 찾아냈다. 마지막으로 ΔRank가 중등도와 중증을 가로지르는 비교를 해서, 중증에서만 특이적으로 중요해지는 사이토카인을 발굴했다. 이 사슬의 끝에서 나온 결론은 구체적이고 실행 가능한 생물학적 가설이다. 중증 악화에서는 TRAIL 매개 세포 사멸이 억제되고(TNFSF10↓) 염증성 신호는 폭주하며(CXCL10↑) 호중구가 과잉 활성화되고(MPO↑) 조직 손상이 심화된다(HGF↑, GDF15↑). 이 사이토카인들은 단백질 상호작용 네트워크에서도 서로 연결되어 있다. 이런 발견이 바로 3단계에서 랜덤 포레스트를 선택한 궁극적인 이유다. 해석 가능한 모델이었기 때문에 SHAP으로 분해하고, 통계로 검증하고, 생물학적 메커니즘까지 도달할 수 있었다.


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

전처리된 사이토카인 데이터가 있고, 각 샘플에 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 상관관계 확인 | 랜덤 포레스트 모델을 선택할 때 이 연구에서 가장 중요하게 고려한 것은 정확도가 아니라 해석 가능성이다. 이건 이전 챕터들과 결정적으로 다른 점이다. 망막 영상 분류에서는 ResNet이 "이 사진은 등급 3"이라고 맞추기만 하면 됐다. 하지만 의학 연구에서는 "왜 등급 3인가"를 설명할 수 있어야 한다. 어떤 사이토카인이 중증도에 기여하는지, 얼마나 기여하는지를 정량적으로 밝혀내야 치료 표적을 찾을 수 있다. 신경망은 이 해석이 극도로 어렵다. 166개 입력이 128개 뉴런을 거치고 64개를 거치고 32개를 거쳐서 하나의 출력이 나온다. 수천 개의 가중치가 복잡하게 얽혀 있어서 "CD274이 결과에 얼마나 기여했는가"를 분리해내기가 거의 불가능하다. 블랙박스인 것이다. 랜덤 포레스트는 다르다. 내부가 결정 트리들의 집합이고, 각 트리는 "이 사이토카인이 이 값보다 높은가 낮은가"라는 질문의 연쇄로 이루어져 있다. 어떤 사이토카인이 트리의 어느 위치에서 얼마나 자주 사용되었는지를 추적하면 각 사이토카인의 기여도를 직접 측정할 수 있다. 그리고 다음 단계에서 SHAP이라는 기법과 결합하면 개별 샘플 수준에서 "이 환자가 중증으로 분류된 이유는 CD274이 높고 IL-10이 낮았기 때문"이라는 해석까지 가능해진다. 결정 트리 한 개의 작동 방식 랜덤 포레스트를 이해하려면 먼저 결정 트리 한 그루를 이해해야 한다. 60개 샘플이 있다고 하자. 건강한 사람 25명과 중증 환자 35명이 섞여 있다. 이 60명을 가장 잘 분리하는 사이토카인을 하나 고른다. 예를 들어 CD274의 값이 5.5를 넘는지를 기준으로 나누면, 왼쪽에는 대부분 중증 환자가 모이고 오른쪽에는 대부분 건강한 사람이 모인다고 하자. 이게 트리의 첫 번째 분기점(루트 노드)이다. 하지만 한 번의 분기로 완벽하게 나누어지지 않을 수 있다. 왼쪽 그룹에 건강한 사람 몇 명이 섞여 있을 수 있다. 그러면 이 왼쪽 그룹 안에서 다시 가장 잘 나누는 사이토카인을 찾는다. FLT3LG가 4.2를 넘는지를 기준으로 또 나눈다. 이런 식으로 모든 그룹이 순수해질 때까지(한 그룹에 건강인만 있거나 중증만 있을 때까지) 계속 쪼개나간다. 각 분기점에서 "어떤 사이토카인으로 나눌 것인가"를 결정하는 기준이 Gini 불순도다. Gini 불순도는 그 그룹이 얼마나 섞여 있는지를 측정하는 수치다. 건강인 10명과 중증 10명이 반반 섞인 그룹의 Gini는 1 - (10/20)² - (10/20)² = 0.5로 최대다. 가장 불순한 상태다. 건강인 20명만 있는 그룹의 Gini는 1 - (20/20)² = 0으로 최소다. 완전히 순수한 상태다. 분기를 할 때마다 Gini 불순도를 가장 많이 줄이는 사이토카인을 선택한다. 가장 효과적으로 두 그룹을 갈라놓는 사이토카인이 트리의 위쪽(루트에 가까운 쪽)에 위치하게 된다. 랜덤 포레스트 결정 트리 한 그루의 문제는 과적합이다. 훈련 데이터에 지나치게 맞춰져서 새로운 데이터에는 잘 작동하지 않을 수 있다. 한 그루의 나무는 훈련 데이터의 작은 잡음까지 외워버리는 경향이 있다. 랜덤 포레스트는 이 문제를 100그루의 나무를 심어서 해결한다. 각 나무는 의도적으로 조금씩 다르게 만든다. 두 가지 무작위성을 주입한다. 첫째, 부트스트랩 샘플링이다. 60개 샘플에서 복원 추출로 60개를 뽑는다. 어떤 샘플은 두 번 뽑히고 어떤 샘플은 한 번도 안 뽑힌다. 각 나무마다 다른 샘플 조합으로 학습하는 것이다. 둘째, 특성 서브샘플링이다. 각 분기점에서 166개 사이토카인 전부를 후보로 놓지 않고, 무작위로 선택한 일부(보통 √166 ≈ 13개 정도)만 후보로 본다. 그래서 어떤 나무는 CD274을 루트에 놓고 다른 나무는 GDF15를 루트에 놓게 된다. 최종 예측은 100그루의 투표 결과다. 75그루가 "중증"이라고 하고 25그루가 "건강"이라고 하면 중증 확률은 0.75다. 이 투표 방식이 개별 나무의 과적합을 상쇄시킨다. 한 나무가 잡음에 반응해서 틀린 예측을 해도, 나머지 나무들이 다수결로 바로잡는다. 훈련 전략 (건강 vs 중증): PPG를 직접 분류하지 않는 이유 여기서 중요한 설계 결정이 하나 있다. PPG가 네 가지(mDP, mRP, sDP, sRP)인데, 왜 이 네 가지를 직접 분류하지 않고 "건강 vs 중증" 이진 분류를 하는가? 현실적인 이유가 있다. sDP 샘플이 53개밖에 안 된다. 네 가지를 동시에 분류하려면 각 클래스에 충분한 샘플이 필요한데, 53개는 랜덤 포레스트가 안정적으로 패턴을 학습하기에 적다. 게다가 mDP와 mRP, sDP와 sRP 사이의 사이토카인 차이는 mDP와 sDP 사이의 차이보다 미묘하다. 네 가지를 한꺼번에 학습하면 경계가 모호해져서 모델 성능이 떨어질 수 있다. 전략적인 이유도 있다. 건강한 사람과 중증 COVID-19 환자의 사이토카인 프로파일은 극적으로 다르다. 건강한 사람은 사이토카인 수준이 낮고 안정적이다. 중증 환자는 사이토카인 폭풍으로 여러 사이토카인이 극도로 상승해 있다. 이 두 극단을 학습시키면 모델은 "사이토카인 프로파일에서 중증과 관련된 신호가 무엇인가"를 매우 명확하게 배운다. 학습된 모델을 전체 549개 샘플에 적용하면, 각 샘플이 0과 1 사이의 중증 확률(Severe_Prob)을 받는다. 건강인은 0에 가깝고, 중증 환자는 1에 가깝고, 중등도 환자나 회복기 환자는 그 사이 어딘가에 위치한다. 이 연속적인 점수가 PPG 간의 차이를 반영하는 생물학적 좌표계 역할을 한다. 0/1 이진 레이블보다 훨씬 풍부한 정보를 담고 있다. RF 모델 훈련 cf Gini 불순도? 성능 평가 훈련 정확도 100%, 테스트 정확도 100%, 5-fold 교차 검증 100%. 모든 지표가 완벽하다. 보통 100% 정확도는 과적합을 의심해야 하지만, 이 경우는 과적합이 아니다. |------|------|------------| | Train 정확도 | 100% | 100% | | Test 정확도 | 100% | 100% | | 5-fold CV 평균 | 100% | 100% (전 폴드) | 이유는 두 그룹의 차이가 압도적으로 크기 때문이다. 건강한 사람의 IL-6는 1 pg/mL 수준이다. 중증 COVID-19 환자의 IL-6는 1,000 pg/mL 이상이다. 천 배 차이다. 이건 IL-6 하나만의 이야기가 아니다. 166개 사이토카인 중 상당수가 비슷한 수준의 극적인 차이를 보인다. 사이토카인 폭풍이라는 현상 자체가 수십 종의 사이토카인을 동시에 폭발시키기 때문이다. 비유하자면, 여름 사진과 겨울 사진을 구별하는 것과 같다. 하늘 색깔, 나뭇잎 유무, 사람들의 옷차림, 눈 유무 등 수십 가지 단서가 동시에 차이를 보인다. 어떤 분류기를 쓰든 100%에 가까운 정확도가 나올 수밖에 없다. 건강인과 중증 환자의 사이토카인 프로파일 차이는 이 정도로 극명하다. 5-fold 교차 검증이 이걸 확인해준다. 데이터를 다섯 묶음으로 나눠서, 네 묶음으로 학습하고 한 묶음으로 테스트하는 과정을 다섯 번 반복한다. 다섯 번 모두 100%라는 건 어떤 샘플이 테스트에 들어가든 결과가 같다는 뜻이다. 특정 샘플 조합에 의존한 우연이 아니라 진짜 분리 가능한 차이라는 확인이다. 중증 확률: 단순 분류를 넘어선 연속 점수 모델이 100%로 학습된 후, 이 모델을 전체 549개 샘플에 적용한다. predict_proba라는 함수가 각 샘플에 대해 100그루의 나무가 투표한 결과를 확률로 돌려준다. 클래스 1(중증)에 투표한 나무의 비율이 곧 중증 확률이다. 이 확률값이 왜 중요한가? 모델은 "건강 vs 중증"만 배웠지만, 중간 상태의 샘플에 적용하면 흥미로운 일이 벌어진다. 중등도 악화기(mDP) 환자의 사이토카인 패턴은 건강인보다는 중증에 가깝지만 완전한 중증은 아니다. 모델은 이 패턴에 대해 0.3이나 0.5 같은 중간 확률을 내놓을 것이다. 중등도 회복기(mRP) 환자는 면역 반응이 가라앉고 있으므로 건강인에 더 가까운 패턴을 보이고, 0.1이나 0.2 같은 낮은 확률을 받을 것이다. 중증 악화기(sDP)는 사이토카인 폭풍 한가운데에 있으므로 0.9나 0.95 같은 높은 확률을 받을 것이다. 이렇게 되면 0에서 1 사이의 연속적인 점수가 PPG 네 그룹의 순서를 자연스럽게 반영하는 좌표축이 된다. 건강인은 0 근처, mRP는 낮은 영역, mDP는 중간, sRP는 중상위, sDP는 상위에 분포할 것이다. 모델에게 PPG를 직접 가르치지 않았는데도 사이토카인 패턴만으로 PPG 순서가 재현되는 것이다. 이건 PPG 레이블링이 사이토카인의 실제 생물학적 변화와 일치한다는 간접적 증거다. 레이블링 검증: 모든 지표가 같은 방향을 가리키는가 이 중증 확률과 임상 지표들 사이의 상관관계를 분석해서 전체 시스템의 일관성을 검증한다. (Supplementary Figure 2) 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. 이 세 층위가 모두 같은 방향을 가리킨다. 사이토카인이 심하게 변한 샘플일수록 모델이 높은 점수를 주고, 실제 임상 상태도 심각하다. 어느 한 곳에서 불일치가 나타났다면 전처리, 레이블링, 또는 모델 중 어딘가에 문제가 있다는 신호였을 것이다. 모든 곳에서 일치가 나타났으므로 파이프라인 전체가 생물학적 현실을 올바르게 반영하고 있다는 확인이다. 정리 1단계 전처리는 원시 데이터에서 기술적 잡음을 제거하고 생물학적 신호만 남겼다. 2단계 PPG 레이블링은 각 샘플에 "어디에 있고 어느 방향으로 가는가"라는 의미 있는 정답지를 붙였다. 3단계인 이 단계는 깨끗한 데이터와 의미 있는 레이블을 이용해서 사이토카인 패턴과 중증도의 관계를 학습하는 모델을 만들었다. 하지만 아직 핵심 질문에 답하지 못했다. "어떤 사이토카인이 중요한가?" 랜덤 포레스트가 100%로 분류한다는 건 알겠는데, 166개 중 어떤 것이 결정적인 역할을 했는지, 각 환자에서 어떤 사이토카인이 어떤 방향으로 작용했는지는 아직 모른다. 이 질문에 답하는 것이 다음 단계인 SHAP 분석의 역할이다. 랜덤 포레스트가 해석 가능한 모델이라는 장점은 SHAP과 결합할 때 비로소 완전히 발휘된다.


2026-03-01 ⋯ RF 사이토카인 SHAP 분석 #2 Progression 레이블링

전처리가 끝나서 깨끗한 사이토카인 데이터가 준비되었다. 549개 샘플, 166개 사이토카인, 빈칸 없고 스케일도 맞춰진 행렬이다. 이제 이 데이터로 무엇을 할 것인가? 혈액 샘플 하나를 꺼내서 166개 사이토카인 농도를 본다고 하자. CD274이 6.8이고 CXCL10이 7.2이고 IL-6가 5.1이다. 이 숫자들만 봐서는 이 환자가 지금 나빠지고 있는 건지, 좋아지고 있는 건지 알 수 없다. 스냅사진 한 장만 봐서는 사람이 달리고 있는지 서 있는지 알 수 없는 것과 같다. 방향을 알려면 시간 축이 필요하다. 이전 사진과 비교해야 한다. 이 단계의 목표는 각 혈액 샘플에 "이 시점에 이 환자는 악화 중인가, 회복 중인가"라는 레이블을 붙이는 것이다. 그리고 "얼마나 심각한 상태인가"도 함께 표시한다. 이 두 정보를 합쳐서 네 가지 레이블을 만든다. mDP는 중등도 환자의 악화 단계, mRP는 중등도 환자의 회복 단계, sDP는 중증 환자의 악화 단계, sRP는 중증 환자의 회복 단계다. PPG는 Pathological Progression Group, 즉 병리적 진행 그룹이라는 뜻이다. | 레이블 | 의미 | 기준 | |--------|------|------| | **mDP** | 중등도 악화 단계 | WHO < 6, NLR 피크 이전 채혈 | | **mRP** | 중등도 회복 단계 | WHO < 6, NLR 피크 이후 채혈 | | **sDP** | 중증 악화 단계 | WHO ≥ 6, NLR 피크 이전 채혈 | | **sRP** | 중증 회복 단계 | WHO ≥ 6, NLR 피크 이후 채혈 | NLR이라는 지표 레이블을 붙이려면 "악화"와 "회복"을 판단할 기준이 필요하다. 이 연구에서 핵심 기준으로 사용하는 것이 NLR이다. NLR은 Neutrophil-to-Lymphocyte Ratio, 호중구 대 림프구 비율이다. 이걸 이해하려면 면역 시스템의 두 부대를 알아야 한다. 호중구는 면역의 선봉대다. 감염이 발생하면 가장 먼저 달려가서 병원체와 싸운다. 빠르고 거칠고 비특이적이다. 누구든 일단 공격한다. COVID-19가 심해지면 호중구가 폭발적으로 증가한다. 림프구는 면역의 특수부대다. T세포, B세포 같은 정교한 면역세포들이다. 특정 병원체를 정밀하게 인식하고 항체를 만든다. COVID-19에서는 바이러스가 림프구를 직접 공격하거나 과도한 면역 반응이 림프구를 소진시켜서 림프구가 감소하는 현상이 흔히 나타난다. NLR은 호중구 비율을 림프구 비율로 나눈 값이다. 호중구가 늘고 림프구가 줄면 NLR이 급등한다. 건강한 사람의 NLR은 보통 1~3 정도인데, 논문의 데이터를 보면 중등도 환자(mDP, mRP)의 NLR은 약 3.0 수준이고 중증 환자(sDP, sRP)의 NLR은 약 9~10 수준이다. 세 배 이상 차이가 난다. NLR이 높다는 것은 "선봉대가 총동원되고 특수부대는 궤멸된" 상태, 즉 면역 시스템이 극한 상황에 처해 있다는 뜻이다.NLR의 시간 변화를 추적하면 환자의 경과를 읽을 수 있다. NLR이 올라가고 있으면 상태가 악화되고 있다는 뜻이고, NLR이 내려가고 있으면 회복되고 있다는 뜻이다. NLR이 최대가 되는 날, 즉 피크가 악화에서 회복으로 전환되는 분기점이다. | PPG | 호중구 % | 림프구 % | 계산된 NLR 범위 | |-----|----------|----------|----------------| | mDP | 67.49 ± 13.57 | 22.79 ± 11.3 | ~3.0 | | mRP | 67.68 ± 12.72 | 22.93 ± 10.58 | ~3.0 | | sDP | 84.99 ± 8.98 | 8.21 ± 6.6 | ~10.4 | | sRP | 83.91 ± 9.05 | 9.08 ± 6.66 | ~9.2 | cf 왜 WHO 점수가 아니라 NLR을 기준으로 쓰는가 환자의 중증도를 나타내는 공식적인 지표로 WHO 점수가 있다. 1부터 10까지의 정수로, 1~3은 경증 외래 환자, 4~5는 입원했지만 산소가 불필요하거나 일반 산소만 필요한 수준, 6 이상은 기계적 호흡 보조가 필요한 중증이다. 10은 사망이다. WHO 점수를 DP/RP 경계로 쓰면 될 것 같지만 한 가지 문제가 있다. WHO 점수는 이산적이다. 1, 2, 3, 4, 5 같은 정수값만 가진다. 환자가 입원 내내 WHO 5를 유지하는 경우가 많다. 마스크로 산소를 공급받는 상태가 입원 기간 내내 변하지 않는 것이다. 이런 환자는 WHO 점수만 보면 악화도 회복도 없어 보인다. 하지만 NLR을 보면 분명히 올라갔다가 내려가는 패턴이 있다. 면역 반응의 역동적인 변화가 WHO라는 거친 척도에는 포착되지 않지만 NLR이라는 연속적인 수치에는 잡히는 것이다. 논문에서 WHO와 NLR의 상관계수가 0.653(p < 0.001)으로 나왔다. 이건 NLR이 WHO와 가장 강하게 동기화되는 지표라는 뜻이다. LDH(조직 손상 지표)와 WHO의 상관은 0.634로 약간 낮다. 세 지표 중 NLR이 WHO를 가장 잘 반영하면서도 WHO보다 더 세밀한 변화를 잡아내므로, NLR 피크를 악화-회복 경계로 쓰는 것이 가장 합리적이다. 레이블링 알고리즘 구체적인 레이블링 과정을 따라가보자. 환자 P086을 예로 들겠다. 이 환자는 입원 기간 동안 WHO 최대 점수가 7이므로 중증 환자다. 매일 NLR을 측정한 기록이 있는데, 입원 1일째 NLR이 10이었고, 2일째 15, 3일째 25, 4일째 35, 5일째 40으로 최고치에 달했다가, 6일째 30, 7일째 20, 8일째 15, 9일째 10으로 내려갔다. 5일째가 NLR 피크다. 이 환자에서 채혈이 네 번 있었다. 2일째, 4일째, 6일째, 9일째에 혈액을 뽑았다. 각 채혈 시점의 레이블을 결정한다. 2일째는 피크(5일째) 이전이므로 악화 단계(DP)다. 4일째도 피크 이전이므로 DP다. 6일째는 피크 이후이므로 회복 단계(RP)다. 9일째도 피크 이후이므로 RP다. 이 환자의 WHO 최대값이 7로 6 이상이니까 중증(s) 접두사가 붙는다. 최종적으로 2일째 샘플은 sDP, 4일째도 sDP, 6일째는 sRP, 9일째도 sRP가 된다. 같은 환자에서 채혈 시점에 따라 레이블이 달라진다는 점이 핵심이다. 환자 한 명이 하나의 레이블을 갖는 게 아니라, 같은 환자의 같은 질병 경과 안에서도 악화기의 혈액과 회복기의 혈액은 서로 다른 면역 상태를 반영한다. 이것이 PPG의 핵심 통찰이다. 환자를 분류하는 게 아니라 샘플을 분류하는 것이다. 세 가지 실제 패턴 처리 (논문 보충 그림 S1) 이론적으로는 NLR 피크를 찾아서 그 전후로 나누면 깔끔하다. 하지만 실제 환자 데이터는 그렇게 깔끔하지 않다. 첫 번째 패턴은 이상적인 경우다. WHO, NLR, LDH 세 지표가 모두 비슷한 시점에 올라갔다가 내려간다. 세 지표의 피크가 같은 날 근처에 있다. 이런 경우 NLR 피크를 기준으로 자르면 WHO와 LDH 기준으로 잘라도 같은 결과가 나온다. 레이블링 신뢰도가 가장 높다. 두 번째 패턴은 WHO가 변하지 않는 경우다. 환자가 입원 내내 WHO 5를 유지한다. WHO만 보면 이 환자는 내내 같은 상태다. 하지만 NLR과 LDH를 보면 분명히 올라갔다가 내려가는 곡선이 있다. 면역 반응은 역동적으로 변하고 있지만, WHO라는 거친 격자에는 이 변화가 포착되지 않는 것이다. 이런 경우 NLR 기준이 유일하게 의미 있는 판단 근거가 된다. 앞서 WHO 대신 NLR을 쓰는 이유를 설명했는데, 이 패턴이 바로 그 이유의 실증이다. 세 번째 패턴은 가장 어려운 경우다. WHO는 3일째에 피크이고, LDH는 5일째에 피크이고, NLR은 7일째에 피크인 식으로 세 지표가 제각각 다른 시점에 피크를 찍는다. 4일째에 채혈했다면 WHO 기준으로는 회복기인데 NLR 기준으로는 악화기다. 어느 기준을 따르느냐에 따라 레이블이 달라진다. 논문에서는 이런 불일치 케이스에 대해 전문가의 수동 판단을 거쳤고, 코드에서는 NLR을 최우선 기준으로 적용한다. WHO와의 상관이 0.653으로 가장 높은 NLR을 기준으로 삼는 것이 통계적으로 가장 타당하기 때문이다. 중증도 판단: WHO 6 악화/회복은 NLR 피크로 나누고, 중증도는 WHO 최대값으로 나눈다. 기준은 WHO 6이다. 왜 6인가? WHO 1~5는 자발 호흡이 가능한 범위다. 산소를 코에 대주거나 마스크를 씌우는 수준이지만 환자가 스스로 숨을 쉰다. WHO 6부터는 질적인 전환이 일어난다. 비침습적 환기나 고유량 산소가 필요해진다. 기계의 도움 없이는 충분한 산소를 확보하지 못하는 상태다. WHO 7 이상은 기계 환기(인공호흡기)가 필요하고, 9는 다발 장기 부전이다. 6이 "자발 호흡 가능"과 "기계적 호흡 보조 필요" 사이의 경계선이다. 면역학적으로도 이 경계에서 질적 차이가 나타난다. 중증 환자(WHO ≥ 6)의 호중구 비율은 평균 85%로 중등도(67%)보다 크게 높고, 림프구 비율은 8%로 중등도(23%)보다 크게 낮다. NLR로 환산하면 중증이 약 10, 중등도가 약 3이다. 단순히 같은 면역 반응의 강도 차이가 아니라, 면역 시스템의 작동 방식 자체가 질적으로 달라지는 것이다. 정상적인 면역 조절이 무너지고 호중구 위주의 과잉 반응이 지배하는 상태로 전환되는 것이다. 환자의 입원 기간 중 WHO 최대값이 한 번이라도 6 이상이었으면 그 환자의 모든 샘플에 "s"(severe) 접두사가 붙고, 한 번도 6에 도달하지 않았으면 "m"(moderate) 접두사가 붙는다. 최대값을 쓰는 이유는 그 환자가 도달한 가장 심각한 상태가 그 환자의 질병 심각도를 대표한다고 보기 때문이다. 레이블링 결과 최종적으로 mDP 94개, mRP 182개, sDP 53개, sRP 75개 샘플이 만들어진다. | PPG | 샘플 수 | 환자 수 | WHO 평균 | LDH 평균 (U/L) | |-----|---------|---------|----------|----------------| | mDP | 94 | 52 | 4.24 ± 0.43 | 447 ± 221 | | mRP | 182 | 72 | 4.29 ± 0.45 | 711 ± 975 | | sDP | 53 | 28 | 6.39 ± 1.34 | 1,519 ± 1,385 | | sRP | 75 | 27 | 6.29 ± 1.34 | 1,761 ± 1,312 | 이 숫자들에서 재미있는 패턴이 보인다. mRP가 mDP보다 거의 두 배 많다. 중등도 환자는 악화기보다 회복기에 채혈이 더 많이 이루어졌다는 뜻이다. 상태가 심각해지기 전에는 덜 자주 검사하고, 회복기에 추적 관찰을 더 자주 하는 임상 관행이 반영된 것일 수 있다. WHO 평균을 보면 흥미로운 점이 있다. mDP와 mRP의 WHO 평균이 4.24와 4.29로 거의 같다. sDP와 sRP도 6.39와 6.29로 거의 같다. WHO만 보면 악화기와 회복기를 구별할 수 없다는 뜻이다. 앞서 설명한 "WHO가 이산적이어서 둔감하다"는 문제를 숫자로 확인하는 셈이다. 반면 LDH를 보면 차이가 있다. mRP의 LDH(711)가 mDP(447)보다 높다. 직관적으로는 회복기에 LDH가 낮아야 할 것 같지만 실제로는 그 반대다. LDH는 조직 손상의 지표이기 때문이다. 면역 반응이 피크에 달하면서 조직이 손상되고, 그 손상의 잔해가 혈액에 남아 있는 시간이 있다. NLR은 피크를 지나서 내려가고 있지만 조직 손상의 흔적은 아직 남아 있는 것이다. 중증 환자에서도 sRP의 LDH(1,761)가 sDP(1,519)보다 높은데, 중증 환자일수록 이 "잔해 효과"가 더 극적이다. 레이블링 검증 레이블을 붙였으면 이 레이블이 실제로 의미 있는 것인지 검증해야 한다. 아무리 논리적으로 그럴듯해도 데이터가 뒷받침하지 않으면 소용없다. 검증 방법은 주요 임상 지표들 사이의 상관관계를 분석하는 것이다. WHO와 NLR의 피어슨 상관계수가 0.653이고 p값이 0.001 미만이다. 이건 두 가지를 말해준다. 첫째, NLR이 올라가면 WHO도 올라가는 경향이 통계적으로 매우 강하다. 둘째, 이 관계가 우연일 확률이 0.1% 미만이다. 따라서 NLR 피크를 기준으로 악화/회복을 나누는 것은 WHO로 측정한 실제 임상 중증도와 일치한다. WHO와 LDH의 상관도 0.634로 높다. NLR과 LDH는 0.396으로 상대적으로 낮은데, 이건 앞서 본 "시간 지연 효과" 때문이다. NLR은 면역 반응의 현재 상태를 반영하고, LDH는 과거 손상의 누적을 반영하므로 두 지표가 완벽하게 동기화되지는 않는다. 정리: 왜 레이블링이 중요한가 이전 단계의 전처리가 "깨끗한 데이터를 만드는 것"이었다면, 이 단계는 "의미 있는 정답지를 만드는 것"이다. 다음 단계에서 랜덤 포레스트 모델을 학습시킬 때, 이 네 가지 레이블(mDP, mRP, sDP, sRP)이 모델이 맞춰야 할 정답이 된다. 단순히 "중증/경증"으로 나누는 것과 "악화/회복 × 중등도/중증"으로 나누는 것의 차이는 크다. 같은 중증 환자라도 악화 중인 환자와 회복 중인 환자는 면역 프로파일이 다를 것이다. 악화 중인 환자에서는 염증 사이토카인이 급격히 올라가는 패턴이 보일 것이고, 회복 중인 환자에서는 항염증 사이토카인이 활성화되는 패턴이 보일 것이다. 시간 축을 무시하고 모두 "중증"이라는 하나의 레이블로 뭉치면 이 중요한 차이가 묻혀버린다. PPG 레이블링은 정적인 스냅사진에 동적인 방향 정보를 부여하는 작업이다. 혈액 샘플 하나에 "이 환자는 지금 여기쯤에 있고, 이 방향으로 가고 있다"는 지도 위의 화살표를 그려주는 것이다. 이 화살표가 있어야 다음 단계에서 사이토카인 패턴과 질병 진행 방향 사이의 관계를 학습할 수 있다.


2026-03-01 ⋯ RF 사이토카인 SHAP 분석 #1 사이토카인 데이터 전처리

혈액에서 사이토카인 농도를 측정한 원시 데이터가 있다. 이걸 바로 분석에 쓸 수 있을까? 쓸 수 없다. 세 가지 심각한 문제가 있기 때문이다. 첫째, 데이터에 구멍이 뚫려 있다. 191개 사이토카인을 측정했는데, 일부 환자에서 일부 사이토카인 값이 아예 없다. 장비 오작동이든 검출 한계 이하든 이유는 다양하지만, 결과적으로 13,203개의 빈칸이 존재한다. 빈칸이 있는 채로는 대부분의 분석 기법을 적용할 수 없다. 둘째, 값의 범위가 너무 넓다. 어떤 사이토카인은 1 pg/mL 수준이고, 어떤 사이토카인은 95,000 pg/mL까지 올라간다. 같은 사이토카인 안에서도 환자마다 수천 배 차이가 난다. 이런 데이터를 그대로 분석하면 값이 큰 사이토카인 몇 개가 전체 결과를 지배해버린다. 작지만 생물학적으로 중요한 신호는 묻혀서 보이지 않게 된다. 셋째, 측정 환경에 따른 체계적 편향이 있다. 같은 환자의 같은 혈액이라도 1월에 측정하면 전반적으로 값이 높게 나오고 7월에 측정하면 낮게 나올 수 있다. 시약 로트, 실험실 온도, 장비 상태 같은 기술적 요인 때문이다. 이런 차이를 "배치 효과"라고 부르는데, 이걸 제거하지 않으면 기술적 차이를 생물학적 차이로 오해하게 된다. 이 세 문제를 해결하지 않으면 다음 단계의 분석은 의미가 없다. 쓰레기가 들어가면 쓰레기가 나온다는 "Garbage In, Garbage Out" 원칙이다. 이에전처리 단계에서 아래 작업을 수행한다. | 작업 | 방법 | 목적 | |------|------|------| | 결측치 제거 | 15% 초과 결측 사이토카인 제외 | 신뢰도 낮은 변수 제거 | | 결측치 보완 | MissForest (랜덤 포레스트 회귀) | 생물학적 상관관계를 반영한 추정 | | 스케일 보정 | Log1p 변환 | 값 범위 압축, 정규분포 근사 | | 배치 효과 제거 | 분위수 정규화 | 측정 시점·배치 간 편차 제거 | 사이토카인 사이토카인을 이해하려면 면역 시스템의 통신 체계를 이해해야 한다. 몸에 바이러스가 침입하면 면역세포들이 활성화된다. 그런데 면역세포 하나가 혼자서 싸우는 게 아니다. 수많은 종류의 면역세포들이 팀을 이뤄서 대응한다. 이 팀원들이 서로 소통하는 방법이 사이토카인이다. 면역세포가 혈액 속으로 분비하는 단백질 메신저로, "여기 적이 있다, 와서 도와라"라는 염증 신호를 보내는 것도 있고(IL-6, TNF, CXCL10 같은 것들), "전투가 끝났다, 이제 진정해라"라는 항염증 신호를 보내는 것도 있다(IL-10, IL-1RN 같은 것들). COVID-19에서 특히 중요한 이유는 사이토카인 폭풍 때문이다. 바이러스에 대한 면역 반응이 과도하게 활성화되면 염증 사이토카인이 폭발적으로 분비되고, 이 과잉 반응 자체가 장기를 손상시킨다. 바이러스가 직접 몸을 파괴하는 것이 아니라, 내 면역 시스템의 과잉 반응이 몸을 파괴하는 것이다. 그래서 어떤 사이토카인이 얼마나 올라갔는지를 분석하면 환자의 중증도를 예측하고 치료 전략을 세울 수 있다. 이 연구에서는 한국의 COVID-19 환자 444명과 건강한 대조군 145명에게서 191종의 사이토카인을 측정했다. 환자들은 시간에 따라 여러 번 채혈했으므로 총 샘플 수는 1,159개다. 최종적으로 중등도 81명과 중증 31명, 총 112명의 환자를 집중 분석한다. | 항목 | 수치 | |------|------| | COVID-19 환자 | 444명 (한국, 2020.02 ~ 2022.07) | | 건강 대조군 | 145명 | | 총 사이토카인 샘플 수 | 1,159개 | | 초기 사이토카인 수 | 191개 | | 결측치 총 개수 | 13,203개 | | 최종 사이토카인 수 | **166개** | | 분석 대상 환자 | 112명 (중등도 81명, 중증 31명) | 결측치가 너무 많은 사이토카인 제거 191개 사이토카인 각각에 대해 결측률을 계산한다. 전체 1,159개 샘플 중 몇 개에서 값이 빠져 있는지를 비율로 구하는 것이다. 기준을 15%로 잡는다. 결측률이 15% 미만인 사이토카인만 살리고, 15% 이상인 것은 버린다. 이 기준으로 걸러내면 191개에서 166개가 남는다. 25개가 제외된다. 왜 15%인가? 이건 엄밀한 수학적 근거가 있는 숫자라기보다 실용적인 절충점이다. 기준을 5%로 잡으면 너무 엄격해서 남는 사이토카인이 지나치게 적어진다. 분석에 쓸 수 있는 변수가 줄어들면 중요한 생물학적 정보를 놓칠 수 있다. 반대로 50%로 잡으면 너무 느슨해서, 절반 이상이 추정값으로 채워진 사이토카인이 분석에 포함된다. 추정값의 비중이 너무 크면 실제 측정값이 아닌 추정 알고리즘의 편향이 결과에 반영될 위험이 있다. 15%는 이 두 극단 사이에서 문헌에서 흔히 사용되는 수준이다. 남은 결측치 보완 — MissForest 166개 사이토카인이 남았지만, 이 안에도 여전히 결측치가 있다. 15% 미만이니까 대부분의 값은 있지만 일부가 빠져 있는 것이다. 이 빈칸을 채워야 한다. 가장 단순한 방법은 평균 대체다. IL-6에 빈칸이 있으면 IL-6의 전체 평균으로 채운다. 하지만 이 방법에는 치명적인 결함이 있다. 사이토카인들은 서로 연관되어 있다는 사실을 완전히 무시한다. IL-6가 높은 환자는 대개 TNF도 높고 CRP도 높다. 면역 반응이 강하면 여러 사이토카인이 동시에 올라가기 때문이다. 그런데 평균 대체는 이 환자의 TNF가 8,000이든 80이든 상관없이 IL-6를 같은 평균값으로 채워버린다. 사이토카인 사이의 생물학적 상관관계가 파괴되는 것이다. MissForest는 이 문제를 영리하게 해결한다. 핵심 아이디어는 "나머지 165개 사이토카인 값을 보고 빠진 1개를 예측한다"는 것이다. 작동 방식을 따라가보자. 첫 번째 단계에서 모든 결측값을 일단 해당 사이토카인의 평균으로 채운다. 이건 최종 답이 아니라 임시값이다. 완전한 행렬을 만들어야 다음 단계의 학습이 가능하기 때문이다. 두 번째 단계에서 사이토카인을 하나 골라서, 그 사이토카인의 결측값을 다시 빈칸으로 되돌린다. 그리고 나머지 165개 사이토카인을 입력으로, 이 사이토카인의 알려진 값들을 정답으로 해서 랜덤 포레스트를 학습시킨다. 학습된 랜덤 포레스트가 빈칸이었던 샘플들의 값을 예측한다. 이 예측값은 나머지 165개 사이토카인의 패턴을 반영하므로, 단순 평균보다 훨씬 정확하다. TNF가 8,000이고 CRP가 높은 환자의 IL-6는 높게, TNF가 80인 환자의 IL-6는 낮게 예측될 것이다. 이 과정을 166개 사이토카인 각각에 대해 반복한다. 한 바퀴를 돌면 모든 결측값이 한 번씩 업데이트된다. 하지만 아직 끝이 아니다. 처음에 평균으로 채운 임시값이 다른 사이토카인의 예측에 영향을 줬을 수 있기 때문이다. 그래서 이 전체 과정을 여러 번 반복한다. 두 번째 바퀴에서는 첫 번째 바퀴의 예측값이 입력에 들어가므로 더 나은 예측이 나오고, 세 번째 바퀴에서는 더 나아지고, 이렇게 반복할수록 예측값이 안정화된다. 논문에서는 10회 반복한다. 값이 더 이상 크게 변하지 않으면 수렴한 것이다. MissForest의 핵심 장점은 비선형 관계도 포착한다는 점이다. 랜덤 포레스트는 트리 기반 모델이므로, "IL-6와 TNF가 둘 다 높을 때만 CRP가 급등한다" 같은 복잡한 상호작용도 학습할 수 있다. 단순 선형 회귀로는 이런 패턴을 놓친다. Log1p 변환 — 수천 배 차이를 다루기 쉽게 압축 결측치 없는 완전한 행렬이 만들어졌다. 하지만 값의 범위 문제가 남아 있다. CD274은 1~2,500 pg/mL 범위이고 CXCL10은 12~95,000 pg/mL 범위다. 이 상태로 분석하면 CXCL10의 수만 단위 값이 CD274의 수 단위 값을 완전히 압도한다. 히스토그램을 그려보면 문제가 더 명확하다. 대부분의 환자는 낮은 농도에 몰려 있고, 극소수의 환자만 극단적으로 높은 값을 갖는다. 왼쪽에 급격한 봉우리가 있고 오른쪽으로 아주 긴 꼬리가 늘어지는 모양이다. 이런 분포를 "오른쪽으로 꼬리가 긴 분포(right-skewed)"라고 부른다. 통계 분석 기법 대부분은 데이터가 대칭적인 종 모양(정규분포)에 가까울 때 잘 작동하므로, 이 꼬리 긴 분포는 분석에 부적합하다. Log1p 변환이 이 문제를 해결한다. log1p(x)는 log(x + 1)이다. 로그 함수의 특성상 큰 값은 크게 줄이고 작은 값은 상대적으로 덜 줄인다. 구체적으로 보면, 0은 log(0+1) = 0으로 그대로 유지된다. 10은 log(11) ≈ 2.4가 된다. 1,000은 log(1,001) ≈ 6.9가 된다. 10,000은 log(10,001) ≈ 9.2가 된다. 원래 1,000배 차이가 나던 값들이 변환 후에는 약 1.3배 차이로 줄어드는 것이다. 왜 log(x)가 아니라 log(x+1)인가? 사이토카인이 검출되지 않은 경우 값이 0이다. log(0)은 음의 무한대로, 수학적으로 정의되지 않는다. 컴퓨터에서 이 연산을 시도하면 오류가 발생한다. 1을 더해주면 log(0+1) = log(1) = 0이 되어 깔끔하게 처리된다. "검출 안 됨"이라는 의미를 0이라는 값으로 자연스럽게 보존하면서, 나머지 모든 양수 값에 대해서는 로그 변환의 압축 효과를 얻는다. 변환 후 히스토그램을 다시 그려보면 긴 꼬리가 사라지고 대칭적인 종 모양에 가까워진다. 극단값 몇 개가 전체 분석을 왜곡할 위험이 크게 줄어든다. 분위수 정규화 — 배치 효과 제거 Log1p까지 마쳤는데 아직 문제가 하나 남았다. 배치 효과다. 배치 효과가 왜 생기는지를 구체적으로 생각해보자. 환자 A의 혈액을 1월에 측정하고, 같은 환자의 혈액을 7월에 다시 측정한다. 1월과 7월 사이에 시약 로트가 바뀌었을 수 있다. 실험실의 온도와 습도가 달라졌을 수 있다. 장비의 보정 상태가 달라졌을 수 있다. 이런 기술적 요인들 때문에 1월 측정값이 전반적으로 7월보다 높게 나올 수 있다. 166개 사이토카인이 골고루 조금씩 높게 나오는 식이다. 이건 환자의 면역 상태가 변한 게 아니라 측정 환경이 변한 것이다. 이 차이를 제거하지 않으면 1월 배치의 환자들이 7월 배치의 환자들보다 면역 반응이 강한 것처럼 보이는 거짓 신호가 생긴다. 분위수 정규화는 이런 전반적인 수준 차이를 제거하는 기법이다. 핵심 가정은 "모든 샘플에서 166개 사이토카인의 전체적인 발현 분포는 같아야 한다"는 것이다. 어떤 환자든, 어떤 시점이든, 전체 사이토카인의 분포 형태는 비슷해야 한다. 차이가 있다면 그건 배치 효과일 가능성이 높다. 작동 원리를 따라가보자. 각 샘플 안에서 166개 사이토카인 값을 크기 순으로 정렬한다. 샘플 A에서 가장 작은 값이 0.5이고, 샘플 B에서 가장 작은 값이 0.3이고, 샘플 C에서 가장 작은 값이 0.8이라면, "1등(가장 작은 값)"의 평균은 (0.5 + 0.3 + 0.8) / 3 = 0.53이다. 2등의 평균, 3등의 평균, ... 166등의 평균을 모두 구하면 "기준 분포"가 만들어진다. 그 다음 각 샘플에서 원래 순위에 해당하는 기준 분포 값을 대입한다. 샘플 A에서 CD274이 3등이었으면 기준 분포의 3등 값을 CD274에 넣고, IL-6가 100등이었으면 기준 분포의 100등 값을 IL-6에 넣는다. 모든 샘플에 이 과정을 적용하면, 모든 샘플의 분포 형태가 동일해진다. 여기서 중요한 점은 순위는 보존된다는 것이다. 원래 CD274이 그 샘플 안에서 3번째로 낮은 사이토카인이었다면, 정규화 후에도 3번째로 낮다. "이 환자에서 어떤 사이토카인이 상대적으로 높고 어떤 것이 낮은가"라는 패턴, 즉 생물학적 신호는 그대로 유지된다. 제거되는 것은 "이 배치의 값이 전반적으로 높다/낮다"라는 기술적 편향뿐이다. 최종 결과물의 구조 네 단계의 전처리를 거치면 processed_data.pickle이라는 파일이 저장된다. 이 안에는 세 가지가 들어 있다. 핵심 데이터는 549행 × 166열의 행렬이다. 행은 각 샘플이고 열은 각 사이토카인이다. 행의 이름을 보면 Healthy_001 같은 건강 대조군과, P001_T01, P001_T02 같은 환자 샘플이 있다. P001_T01은 환자 1번의 첫 번째 채혈 시점이고, P001_T02는 같은 환자의 두 번째 채혈이다. T는 Timepoint의 약자다. 같은 환자에서 여러 시점의 데이터가 있으므로 시간에 따른 변화를 추적할 수 있다. 이 행렬의 모든 값은 결측치가 채워지고, log1p로 압축되고, 분위수 정규화가 적용된 상태다. 메타데이터에는 환자 404명의 임상 정보가 담겨 있다. 중증도(WHO 기준), NLR(호중구 대 림프구 비율, 염증 심각도의 혈액 지표), LDH(젖산탈수소효소, 조직 손상 지표) 같은 것들이다. 임상 데이터는 환자별로 일자별 경과를 담은 딕셔너리다. 각 환자의 매일매일 WHO 중증도 등급, NLR, LDH가 어떻게 변했는지를 추적할 수 있다. 정리 전체를 다시 한 걸음 물러서서 보자. 이 네 단계 전처리의 목적은 결국 하나다. 원시 데이터에서 "기술적 잡음"을 제거하고 "생물학적 신호"만 남기는 것이다. 결측치는 측정 기술의 한계에서 오는 잡음이다. MissForest가 생물학적 상관관계를 이용해서 이 빈칸을 합리적으로 채운다. 극단적인 값 범위는 면역 반응의 비선형적 특성에서 오는 왜곡이다. Log1p가 이 왜곡을 압축해서 분석에 적합한 분포로 바꾼다. 배치 효과는 실험 환경에서 오는 체계적 편향이다. 분위수 정규화가 이 편향을 제거한다. 이 세 겹의 정화 과정을 거친 데이터에서야 비로소 "중증 환자와 경증 환자의 사이토카인 패턴이 어떻게 다른가", "시간이 지나면서 사이토카인이 어떻게 변하는가" 같은 진짜 생물학적 질문에 답할 수 있다. 다음 단계에서 이 깨끗한 데이터를 가지고 PPG 레이블링과 모델 훈련을 수행하게 된다.


2026-03-01 ⋯ BERT 뉴스 분류 #5 최종 모델 구조, 학습 설정, 실험 기록, ONNX 변환

모델 구조 뉴스 기사 텍스트가 들어온다. 토크나이저가 이걸 토큰 번호 배열로 바꾸는데, max_length=192로 설정되어 있다. 512가 아니라 192다. 실제 데이터를 분석해서 95%의 기사가 180 토큰 이내라는 걸 확인한 뒤 내린 결정이다. DataCollatorWithPadding이 배치를 구성할 때 그 배치 안에서 가장 긴 시퀀스에 맞춰 동적으로 패딩하므로, 192보다 짧은 배치에서는 더 적은 패딩이 들어간다. 이 두 최적화만으로 Self-Attention의 계산량이 원래의 7분의 1 이하로 줄어든다. 토큰 번호 배열이 BERT의 임베딩 레이어에 들어간다. 여기서 각 토큰 번호가 768차원 벡터로 변환된다. 이 레이어의 학습률은 0.99e-5로 전체에서 가장 작다. 토큰의 기본 표현은 뉴스 분류를 하든 다른 과제를 하든 거의 같아야 하니까 최소한으로만 조정한다.768차원 벡터들이 12개의 Transformer 레이어를 순서대로 통과한다. 각 레이어에는 차등 학습률이 적용되어 있다. 레이어 0은 1.10e-5, 레이어 11은 3.50e-5다. 한 레이어 올라갈 때마다 0.9의 역수만큼 학습률이 커진다. 하위 레이어의 범용 언어 지식은 살짝만, 상위 레이어의 과제 특화 표현은 적극적으로 조정하는 전략이다. 12개 레이어를 통과하면 (batch, ≤192, 768) 형태의 텐서가 나온다. 여기서 [CLS] 토큰 위치의 768차원 벡터만 뽑아낸다. 이 벡터가 문장 전체의 의미를 담고 있다. Self-Attention을 12번 거치면서 문장의 모든 단어가 서로의 문맥을 흡수했고, 그 결과가 [CLS] 위치에 응축된 것이다. 이 768차원 벡터가 EnhancedClassifier로 들어간다. 기본 BERT의 단순한 Linear(768→4) 대신, 768→256→4 구조로 중간 단계를 둔 분류 헤드다. 첫 번째 Dropout(0.3)이 과적합을 방지하고, Linear(768→256)이 차원을 축소하면서 GELU 활성화가 비선형성을 도입하고, 두 번째 Dropout(0.3)을 거쳐 최종 Linear(256→4)가 4개 클래스의 logit을 만든다. Softmax가 이 logit을 확률 분포로 변환하면 뉴스 카테고리 예측이 완성된다. 기본 BERT에서 바뀐 것을 정리하면 세 가지다. 입력 길이가 512에서 192로 줄었고, 분류 헤드가 단일 선형 레이어에서 2층 비선형 네트워크로 확장되었고, 전체 모델에 동일하게 적용되던 학습률이 레이어별로 차등 적용된다. 각각은 독립적인 최적화이지만 합쳐지면 시너지를 낸다. 학습 설정 per_device_train_batch_size=16은 이전의 8에서 두 배로 늘어났다. 이게 가능한 이유가 max_length를 192로 줄인 덕분이다. 시퀀스가 짧아지면 Self-Attention 행렬이 작아지고 GPU 메모리를 덜 먹으니까, 같은 메모리로 더 많은 샘플을 한 배치에 넣을 수 있다. 배치가 커지면 그래디언트 추정이 더 안정적이 되어 학습이 부드러워진다. 입력 길이 최적화가 단순히 속도만 빠르게 하는 게 아니라 배치 크기라는 하이퍼파라미터의 선택지까지 넓혀주는 연쇄 효과가 있는 것이다. num_train_epochs=3은 전체 데이터를 세 바퀴 도는 것이다. 이전에 1 에포크만 돌았던 것보다 늘었지만, Early Stopping이 과적합을 감시하고 있으니 3 에포크를 다 채우지 않고 중간에 멈출 수도 있다. 넉넉하게 잡아놓고 Early Stopping이 최적 시점을 찾게 하는 전략이다. learning_rate=3.5e-5는 베이스 학습률이다. 이 값이 레이어 11에 적용되고, 아래로 갈수록 0.9씩 곱해져서 줄어든다. weight_decay=0.01은 L2 정규화로 가중치가 불필요하게 커지는 것을 방지한다. warmup_ratio=0.1은 전체 학습 스텝의 10%를 워밍업에 쓴다. 학습 초반에 랜덤 초기화된 분류 헤드가 엉뚱한 그래디언트를 역전파할 때 사전학습 가중치를 보호하기 위해서다. lr_scheduler_type="cosine"은 워밍업 후 학습률이 코사인 곡선을 따라 감쇠하게 만든다. 후반부까지 미세 조정이 지속되도록 하는 선택이다. label_smoothing_factor=0.1은 정답 레이블을 [0, 0, 1, 0]에서 [0.025, 0.025, 0.925, 0.025]로 부드럽게 만들어서 모델의 과도한 확신을 방지한다. e valuation_strategy="steps"와 eval_steps=200은 200 스텝마다 검증 성능을 체크한다. load_best_model_at_end=True와 metric_for_best_model="f1"은 F1 기준으로 가장 좋았던 체크포인트를 최종 모델로 선택한다. EarlyStoppingCallback(early_stopping_patience=3)은 F1이 3번 연속 개선되지 않으면 학습을 중단한다. optimizers=(optimizer, None)에서 optimizer는 레이어별 차등 학습률이 적용된 AdamW다. None은 학습률 스케줄러를 Trainer의 기본 설정(위에서 지정한 코사인 감쇠)에 맡긴다는 뜻이다. 이 설정들이 합쳐지면 학습의 전체 생애가 이렇게 그려진다. 시작할 때 학습률이 0에서 출발해서 워밍업 기간 동안 올라간다. 목표 학습률에 도달하면 본격적인 학습이 시작되는데, 각 레이어가 자기 깊이에 맞는 다른 학습률로 조정된다. 학습이 진행되면서 코사인 곡선을 따라 전체 학습률이 서서히 줄어든다. 200 스텝마다 검증 F1을 체크하면서 최고 성능 모델을 저장한다. 3번 연속 개선이 없으면 자동으로 멈추고 최고 시점의 모델을 복원한다. 평가 지표 compute_metrics 함수가 매 평가마다 네 가지 지표를 계산한다. | 지표 | 계산식 | 의미 | |---|---|---| | Accuracy | 맞은 수 / 전체 | 전체 정확도 | | Precision | TP / (TP+FP) | 모델이 Business라 한 것 중 실제 Business 비율 | | Recall | TP / (TP+FN) | 실제 Business 중 모델이 잡아낸 비율 | | F1 | 2·P·R / (P+R) | Precision과 Recall의 조화평균. Early Stopping 기준 | Accuracy는 전체 예측 중 맞힌 비율이다. 가장 직관적이지만 가장 거친 지표다. AG News는 4개 클래스가 균등하니까 Accuracy만으로도 어느 정도 유용하지만, 클래스가 불균형한 경우에는 오도할 수 있다. Precision은 모델이 특정 클래스라고 판정한 것 중 실제로 그 클래스인 비율이다. 모델이 "이건 경제 기사다"라고 100번 말했을 때 그중 95번이 정말 경제 기사면 Precision이 0.95다. 이 지표가 높으면 모델의 판정을 신뢰할 수 있다. Recall은 실제로 특정 클래스인 것 중 모델이 잡아낸 비율이다. 경제 기사가 100건 있는데 모델이 그중 90건을 경제라고 맞췄으면 Recall이 0.90이다. 이 지표가 높으면 모델이 놓치는 게 적다. F1은 Precision과 Recall의 조화평균이다. 둘 중 하나만 높고 다른 하나가 낮으면 F1도 낮아진다. 두 지표의 균형을 하나의 숫자로 요약하는 것이다. Early Stopping의 기준으로 F1을 쓰는 이유가 여기 있다. Precision만 높이려면 확실한 것만 판정하고 애매한 건 전부 다른 클래스로 밀면 되고, Recall만 높이려면 닥치는 대로 그 클래스라고 찍으면 된다. 둘 다 편향된 전략이다. F1은 이런 편향을 허용하지 않고 균형 잡힌 성능을 요구한다. weighted 평균은 각 클래스의 샘플 수에 비례해서 가중 평균을 내는 것이다. AG News는 클래스가 균등하니까 macro 평균(단순 평균)과 거의 같은 값이 나온다. 하지만 코드에서 weighted를 쓰는 건, 나중에 불균형한 데이터를 다루게 될 때도 같은 코드를 쓸 수 있도록 범용성을 확보하는 좋은 습관이다. MLflow: 실험 기록 MLflow에 기록하는 파라미터를 보면, 이번에는 단순한 학습률이나 에포크 수 같은 표준 하이퍼파라미터뿐 아니라 구조적 결정까지 기록한다. max_length=192, frozen_layers="0-5", lr_decay=0.9, classifier_hidden=256, classifier_dropout=0.3, label_smoothing=0.1 같은 것들이다. 이 기록이 왜 중요한가? 실험을 여러 번 반복하다 보면 "아, 저번에 192로 줄이고 Dropout 0.3으로 했을 때 F1이 얼마였지?"라는 질문이 반드시 나온다. 기록이 없으면 기억에 의존해야 하고, 기억은 틀린다. MLflow에 모든 설계 결정과 그 결과를 자동으로 기록해두면, 나중에 웹 인터페이스에서 실험들을 나란히 놓고 "어떤 조합이 가장 좋았는가"를 정확히 비교할 수 있다. 모델 아티팩트 자체도 저장한다. 나중에 이 모델을 다시 불러올 수 있고, 다른 모델과 성능을 비교할 수 있다. 연구 재현성의 기본 인프라인 셈이다. ONNX 변환 학습이 끝나면 PyTorch 모델을 ONNX 형식으로 변환한다. PyTorch는 동적 그래프 방식이다. 코드를 실행할 때마다 연산 그래프가 새로 만들어진다. 이건 학습할 때는 편리하다. 조건문이나 반복문을 자유롭게 쓸 수 있고 디버깅이 쉽다. 하지만 추론할 때는 이 유연성이 오히려 오버헤드가 된다. 매번 그래프를 새로 만들 필요가 없는데도 Python 인터프리터가 개입하니까 느려지는 것이다. ONNX 변환은 모델의 연산 그래프를 한 번 고정시키고, 그 위에 최적화를 적용한다. 연속된 작은 연산들을 하나의 큰 연산으로 합치고(연산 융합), 결과에 영향을 미치지 않는 연산을 제거하고, 메모리 접근 패턴을 최적화한다. Python 오버헤드가 완전히 사라지고 순수한 수치 연산만 남는다. dummy_input으로 샘플 입력을 만드는 이유는 ONNX가 연산 그래프를 추적하기 위해서다. 실제 입력을 한 번 흘려보내면서 어떤 연산이 어떤 순서로 일어나는지를 기록하고, 그 기록을 정적 그래프로 저장한다. max_length=192로 설정하는 건 학습 때와 동일한 입력 형태를 맞추기 위해서다. dynamic_axes 설정이 중요한데, batch_size와 sequence 차원을 가변적으로 만든다. 실제 서비스에서는 한 번에 기사 1개만 올 수도 있고 10개가 묶여서 올 수도 있다. 기사 길이도 20 토큰일 수도 있고 190 토큰일 수도 있다. 이 차원들을 고정하면 항상 같은 크기의 입력만 받을 수 있어서 실용성이 떨어진다. dynamic_axes로 가변 크기를 지원하면 어떤 크기의 입력이든 유연하게 처리할 수 있다. max_length를 192로 줄인 효과가 여기서도 나타난다. 192 토큰짜리 입력의 ONNX 추론은 512 토큰짜리보다 훨씬 빠르다. 입력 길이 최적화가 학습 속도뿐 아니라 서빙 속도에도 직접적으로 영향을 미치는 것이다. 학습 파이프라인에서 내린 하나의 결정이 서빙 파이프라인까지 이어지는 셈이다. 정리 이 단계에서 만들어진 것은 하나의 완성된 분류 시스템이다. 데이터 분석에 기반한 입력 최적화(192 토큰, 동적 패딩), 문제에 맞게 설계된 분류 헤드(768→256→4), 레이어별 차등 학습률, Label Smoothing, 코사인 학습률 스케줄링, Early Stopping이 모두 합쳐져서 하나의 학습 설정을 이룬다. 학습 과정은 MLflow에 자동 기록되고, 최종 모델은 ONNX로 변환되어 빠른 서빙이 가능한 형태로 출력된다. 이전 챕터들에서 DNA 분석, 망막증 분류, 분자 생성을 할 때는 모델을 만들고 평가하는 것이 끝이었다. 이 프로젝트에서는 그 다음 단계까지 간다. 만든 모델을 실제 서비스에 투입하고, 성능을 감시하고, 필요하면 재학습하는 운영 시스템을 구축하는 것이다. 지금까지는 그 시스템의 첫 번째 부분인 "모델 학습"을 완성한 것이고, 다음 단계에서 서빙, 감시, 재학습으로 나아간다.