Deep Learning
#
2026-03-04 ⋯ RF 사이토카인 SHAP 분석 #7 결과 분석 - Welch's t-test / ΔRank Bar
UMAP에서 mDP와 mRP 환자 그룹, sDP와 sRP 환자 그룹을 깨끗하게 선별했다. 이제 각 그룹 안에서 77개 사이토카인을 하나씩 비교해야 한다. 비교할 때 두 가지를 동시에 알아야 한다. 첫째, 이 사이토카인이 DP 환자에서 높은가 RP 환자에서 높은가? 이건 발현 차이의 방향이다. 둘째, 이 차이가 우연이 아닌 진짜 차이인가? 이건 통계적 유의성이다.
방향만 보면 우연에 의한 차이를 걸러내지 못한다. 동전을 10번 던져서 앞면이 7번 나왔다고 동전이 편향되었다고 확신할 수 없는 것과 같다. 유의성만 보면 방향을 모른다. "CD274가 두 그룹에서 다르다"는 건 알지만 어느 쪽에서 높은지를 모른다. Volcano Plot은 X축에 방향을, Y축에 유의성을 놓아서 두 질문에 동시에 답한다. 통계 검정: 두 그룹의 차이가 우연인가 mDP 환자 100명에서 CD274 발현이 [2.1, 3.4, 1.8, 2.9, ...]이고, mRP 환자 95명에서 [0.9, 1.2, 0.8, 1.1, ...]이라고 하자. 평균을 내보면 mDP가 확실히 높다. 하지만 이 차이가 진짜인지, 아니면 표본을 다시 뽑으면 사라질 우연인지를 판단해야 한다. Welch's t-test가 이 판단을 해준다. 일반 t-test는 두 그룹의 분산이 같다고 가정하는데, 현실에서 악화 환자와 회복 환자의 사이토카인 분산이 같을 이유가 없다. Welch's t-test는 이 가정을 풀어서 더 현실적인 검정을 한다. t-test의 핵심은 "두 그룹의 평균 차이를 불확실성으로 나눈다"는 것이다. 분자는 mDP 평균에서 mRP 평균을 뺀 값이다. 분모는 이 차이의 불확실성, 즉 각 그룹의 분산을 샘플 수로 나눈 것들의 합에 제곱근을 씌운 값이다. 평균 차이가 크고 분산이 작고 샘플이 많으면 t 값이 커지고, 이는 차이가 우연이 아닐 가능성이 높다는 뜻이다. t 값을 p-값으로 변환하면 "이 두 그룹이 실제로는 같은데 우연히 이 정도 차이가 관찰될 확률"이 나온다. p=0.001이면 1,000번 중 1번꼴로밖에 이런 차이가 우연히 나오지 않는다는 뜻이니, 두 그룹이 다르다는 강한 증거다. p=0.3이면 거의 3번 중 1번은 이 정도 차이가 우연히 나올 수 있다는 뜻이니, 차이가 있다고 결론 내릴 수 없다. ΔRank 계산하기 mDP vs mRP (중등도 악화 vs 중등도 회복), sDP vs sRP (중증 악화 vs 중증 회복) 사이 SHAP value에 대한 통계 분석을 수행했다. 중등도 악화 vs 회복에서는 별로 중요하지 않았던 사이토카인이, 중증 악화 vs 회복에서는 갑자기 중요해지는 것이 있는가? 이것이 바로 ΔRank (Delta Rank, 순위 변화)가 대답하는 질문이다. 각 통계 분석에서 사이토카인의 유의성 순위를 매긴다. -log10(P_adj)가 높을수록, 즉 p-값이 작을수록 높은 순위를 받는다. CD274이 mDP vs mRP에서 1위이고 sDP vs sRP에서도 1위라면 ΔRank는 0이다. 이 사이토카인은 중등도든 중증이든 일관되게 중요하다. TNFSF10이 mDP vs mRP에서 30위(유의하지 않은 구간)인데 sDP vs sRP에서 10위(상당히 유의한 구간)로 뛰어올랐다면, 순위가 20등이나 올라간 것이다. 이 연구에서 실제로 계산된 TNFSF10의 ΔRank는 +54로 더 극적이다. 중등도에서는 악화와 회복을 거의 구분하지 못했던 사이토카인이 중증에서는 54등이나 뛰어오를 만큼 강력한 구분 지표가 된 것이다. ΔRank가 양수라는 건 중증으로 갈수록 그 사이토카인이 더 중요해진다는 뜻이다. 음수라는 건 중등도에서 더 중요했고 중증에서는 상대적으로 덜 중요해졌다는 뜻이다. 양수 ΔRank: 중증에서 새롭게 등장하는 메커니즘 ΔRank 상위 5개를 보면 MPO(+60), HGF(+57), TNFSF10(+54), IL3(+52), BDNF(+47)다. 이 사이토카인들은 중등도 비교에서는 존재감이 미미했는데 중증 비교에서 급부상했다. 이것이 왜 중요한가? 중등도에서 중요한 사이토카인과 중증에서 새롭게 중요해지는 사이토카인이 다르다는 건, 중증화가 단순히 "기존 면역 반응이 더 심해진 것"이 아니라 "새로운 면역 메커니즘이 작동하기 시작한 것"임을 의미하기 때문이다. MPO가 ΔRank 1위라는 건 특히 의미심장하다. MPO는 호중구가 병원체를 죽일 때 분비하는 효소다. SHAP Dot Plot에서 MPO가 역방향 패턴을 보였던 걸 떠올려보자. MPO가 낮을수록 중증 확률이 올라갔다. 중등도 환자에서는 호중구가 아직 정상적으로 기능해서 MPO의 악화-회복 간 차이가 크지 않았다. 하지만 중증 환자에서는 호중구가 기능적으로 고갈되어 MPO가 급격히 떨어지고, 이것이 악화와 회복을 강력하게 구분하는 지표가 된 것이다. TNFSF10(TRAIL)도 같은 맥락이다. 감염된 세포를 죽이는 선천 면역의 핵심 무기인데, 중등도에서는 아직 작동하고 있어서 악화-회복 간 차이가 크지 않다. 중증에서는 이 무기가 바닥나면서 바이러스 제거에 실패하고, 이것이 악화 환자의 결정적 특징이 된다. HGF(간세포 성장인자)는 방향이 다르다. 조직 손상이 심할 때 재생 신호로 분비된다. 중등도에서는 아직 조직 손상이 심하지 않아서 HGF의 차이가 작지만, 중증에서는 폐와 내피 세포의 심각한 손상으로 HGF가 급등한다. 이 급등이 악화 환자에서 두드러지는 것이다. 이 세 가지를 종합하면 하나의 이야기가 된다. 중증 악화 환자에서는 선천 면역이 약화되고(TNFSF10↓, MPO↓), 그 결과 바이러스가 통제되지 못하면서 조직 손상이 극대화되고(HGF↑), 이것이 중등도에서는 보이지 않았던 새로운 악화 메커니즘으로 작동하는 것이다. 음수 ΔRank: 중등도에서만 두드러지는 마커 LGALS9(-11), VCAM1(-11), S100A12(-8)은 ΔRank가 음수다. 중등도 Volcano에서는 상위권이었는데 중증 Volcano에서는 상대적으로 밀려났다.
이 사이토카인들이 갑자기 중요하지 않게 된 건 아니다. 중증에서도 여전히 악화 환자에서 높다. 하지만 중증에서는 MPO, HGF, TNFSF10 같은 더 강력한 새 마커들이 등장하면서 상대적 순위가 밀린 것이다. 비유하면, 중등도에서 LGALS9는 반 1등이었는데 중증에서는 전교 1등감들이 전학 와서 반 등수가 내려간 격이다. 생물학적으로 이 사이토카인들은 "일반적 염증 마커"에 가깝다. LGALS9는 T세포 억제, VCAM1은 세포 부착과 내피 활성화, S100A12는 호중구 활성화를 나타낸다. 면역 반응이 활성화되면 중등도든 중증이든 올라가는 지표들이다. 중등도에서는 이런 일반적 염증이 악화와 회복의 주된 차이였지만, 중증에서는 염증을 넘어서 면역 기능 자체의 붕괴가 더 결정적인 차이가 되면서 이 마커들의 상대적 중요도가 떨어진 것이다. cf 양수 ΔRank 사이토카인 정보 임상적 시사점 ΔRank의 양수와 음수 패턴을 합치면 실용적인 모니터링 전략이 나온다. 중등도 환자를 모니터링할 때는 LGALS9, VCAM1, S100A12를 추적하는 것이 효과적이다. 이 마커들이 중등도 단계에서 악화와 회복을 가장 잘 구분해주니까. 이 마커들이 오르면 악화 위험이 있다는 경고다. 중등도에서 중증으로의 전환을 예측할 때는 MPO와 TNFSF10의 저하, HGF의 상승을 주시해야 한다. 이 패턴이 나타나면 단순한 악화를 넘어서 면역 기능 붕괴라는 새로운 단계로 진입하고 있다는 신호다. 기존 염증 마커만 보고 있으면 이 전환을 놓칠 수 있다. 정리 SHAP Dot Plot이 "어떤 사이토카인이 중요하고 어떤 방향인가"를 보여줬다. UMAP이 "비슷한 패턴의 환자가 실제로 모이는가"를 시각화하고 분석 그룹을 선별했다. Welch's t-test가 "선별된 그룹 사이의 차이가 통계적으로 유의한가"를 검증했다. 그리고 ΔRank가 "중등도에서 중증으로 가면서 어떤 사이토카인의 중요도가 변하는가"를 정량화했다. 각 단계가 이전 단계의 질문에 답하면서 새로운 질문을 열었고, ΔRank에서 최종적으로 "중증화의 새로운 메커니즘"이라는 핵심 발견에 도달했다. 다음 단계인 Key Cytokine Boxplot에서는 이 발견에서 핵심적인 사이토카인 몇 개의 실제 발현 분포를 PPG별로 확인해서, 지금까지의 모든 분석을 최종 검증할 것이다.
2026-03-04 ⋯ RF 사이토카인 SHAP 분석 #6 결과 분석 - SHAP UMAP Scatter
각 환자는 166개 사이토카인에 대한 SHAP 값을 갖고 있다. 이건 166차원 공간의 한 점이다. 환자 404명이면 166차원 공간에 404개의 점이 찍혀 있는 셈이다.이전 Clustermap에서 이 점들의 관계를 히트맵으로 봤다. 비슷한 환자끼리 모으고 색상으로 패턴을 드러냈다. 하지만 히트맵은 "비슷한 것끼리 옆에 배치한다"는 1차원적인 재배열이다. 실제로 환자들 사이의 관계는 훨씬 복잡하다. 어떤 환자 그룹은 서로 가깝지만 다른 그룹과는 멀고, 어떤 그룹은 두 그룹 사이에 걸쳐 있다. 이런 다차원적인 거리 관계를 한눈에 보려면 2차원 평면 위에 점으로 찍어야 한다. 문제는 166차원을 2차원으로 압축하면 정보가 필연적으로 손실된다는 것이다. 핵심은 어떤 정보를 보존하고 어떤 정보를 버릴 것인가다. 가장 단순한 차원 축소 방법은 PCA다. PCA는 데이터가 가장 많이 퍼져 있는 방향을 찾아서 그 방향으로 투영한다. 데이터의 전체적인 분산을 최대한 보존하는 방식이다. PCA의 한계는 선형 구조만 포착한다는 것이다. 만약 166차원 공간에서 환자들이 구불구불한 곡면 위에 분포하고 있다면, PCA는 이 곡면을 직선으로 펴버린다. 곡면 위에서 가까웠던 두 점이 직선으로 펴면 멀어질 수 있고, 멀었던 점이 가까워질 수 있다. 원래의 이웃 관계가 깨지는 것이다. UMAP은 다른 철학을 택한다. 전체적인 분산을 보존하는 대신 이웃 관계를 보존한다. "고차원에서 가까웠던 점들은 2차원에서도 가깝게, 멀었던 점들은 멀게 배치한다"는 원칙으로 2차원 좌표를 최적화한다. 구체적으로 UMAP은 세 단계로 작동한다. 첫 번째 단계에서 각 점의 가장 가까운 이웃 15개(n_neighbors=15)를 찾는다. 환자 A의 166차원 SHAP 벡터와 나머지 403명의 벡터 사이의 거리를 계산해서 가장 가까운 15명을 골라내는 것이다. 이 이웃 관계가 고차원 공간의 국소적 구조를 포착한다. 두 번째 단계에서 404개의 점을 2차원 평면에 임의로 배치한다. 세 번째 단계에서 반복적인 최적화를 통해 점들의 2차원 위치를 조정한다. 고차원에서 이웃이었던 점 쌍은 2차원에서 가까워지도록 당기고, 이웃이 아니었던 점 쌍은 밀어낸다. 이 당기기와 밀기를 수백 번 반복하면 166차원의 이웃 구조가 2차원에 최대한 보존된 배치가 완성된다. min_dist=0.1은 같은 클러스터 안의 점들이 얼마나 빽빽하게 모일 수 있는지를 조절한다. 0에 가까울수록 점들이 단단하게 뭉치고, 클수록 느슨하게 퍼진다. 0.1은 클러스터 구조를 선명하게 보여주면서도 개별 점이 완전히 겹치지는 않는 적절한 값이다. cf 왜 원본 발현량이 아닌 SHAP 값에 적용하는가? 이 선택이 중요한 이유를 생각해보자. 원본 사이토카인 발현량으로 UMAP을 돌리면 "생물학적으로 비슷한 면역 상태의 환자"끼리 모인다. 전반적으로 면역이 활성화된 환자, 면역이 억제된 환자 등으로 나뉠 것이다. SHAP 값으로 UMAP을 돌리면 "모델이 중증을 예측할 때 비슷한 이유로 비슷한 결론을 낸 환자"끼리 모인다. 두 환자가 같은 클러스터에 있다는 건, 이 두 환자에서 모델이 같은 사이토카인들을 같은 방향으로 중요하게 봤다는 뜻이다. CD274과 CXCL10이 중증 예측을 밀어올린 환자끼리 모이고, TNFSF10의 부재가 중증 예측을 밀어올린 환자끼리 모이는 식이다. 이 차이가 의학적으로 의미가 있다. 같은 발현 패턴이라도 모델이 다른 이유로 중증을 예측할 수 있고, 다른 발현 패턴이라도 같은 메커니즘으로 중증이 될 수 있다. SHAP 공간에서의 클러스터링은 "중증화의 메커니즘"에 따른 분류에 가깝고, 이것이 원본 발현량 기반보다 더 의학적으로 유용한 군집을 만든다. UMAP이 2차원 평면에 점들을 펼쳐놓으면, 비슷한 환자들이 뭉쳐 있는 군집이 눈에 보인다. 하지만 "여기서부터 여기까지가 하나의 군집"이라는 경계를 사람이 주관적으로 그으면 재현 가능성이 없다. DBSCAN이 이 경계를 자동으로 찾아준다. DBSCAN의 작동 방식은 직관적이다. 두 가지 기준만 있다. 반경 ε=0.8 안에 점이 최소 20개(MinPts=20) 이상 있는 점을 "핵심 점"이라고 부른다. 밀집된 구역의 중심에 있는 점이다. 핵심 점에서 시작해서 반경 안의 이웃을 모두 같은 클러스터에 넣고, 그 이웃이 또 핵심 점이면 거기서도 확장한다. 더 이상 확장할 수 없으면 하나의 클러스터가 완성되고, 다른 밀집 구역에서 새 클러스터를 시작한다. DBSCAN의 중요한 특성은 어떤 클러스터에도 속하지 못한 점을 "노이즈"로 표시한다는 것이다. 밀집 구역에서 떨어져 있는 점, 즉 다른 환자들과 면역 패턴이 상당히 다른 환자는 -1 레이블을 받는다. 이 노이즈 환자들은 이후 분석에서 제외된다. 전형적인 패턴을 가진 환자들만으로 깨끗한 비교를 하기 위해서다. 이전 BERT 프로젝트에서 FPR @ TPR=0.95라는 특정 조건에서의 성능을 봤던 것과 비슷한 논리다. 모든 데이터를 무차별적으로 분석하는 대신, 특정 조건이나 기준을 적용해서 더 의미 있는 분석을 하는 것이다. UMAP 결과 UMAP 그림은 각 점을 DBSCAN 클러스터에 따라 색칠한다. 이 그림을 보면 "데이터가 자연스럽게 몇 개의 군집으로 나뉘는가"를 보여준다. Bar plot은 클러스터 별 PP 레이블 구성을 보여준다. 이는 "SHAP 기반 면역 패턴이 비슷한 환자들이 실제로 같은 임상 그룹에 속하는가"를 알 수 있다. 특정 레이블의 조성이 다른 클러스터에 비해 높다면, 해당 클러스터는 해당 레이블의 특성을 반영한다고 볼 수 있다. 실제 결과에서 DBSCAN으로 네 PPG가 완벽하게 분리되지는 않는다. sDP와 sRP가 어느 정도 겹치고, mDP와 mRP도 완전히 다른 공간을 차지하지 않는다. 이건 분석의 실패가 아니라 현실의 반영이다. 중증 환자와 중등도 환자가 완전히 다른 면역 패턴을 가졌다면 임상에서도 구분이 쉬웠을 것이다. 하지만 실제로 면역 반응은 연속적인 스펙트럼이다. 중등도와 중증의 경계에 있는 환자들, 중등도에서 중증으로 넘어가는 과정에 있는 환자들이 존재한다. 이런 환자들이 2차원 지도에서 경계 영역에 위치하는 건 자연스러운 일이다. UMAP Scatter의 진짜 역할은 예쁜 그림을 그리는 것이 아니다. 이후 ΔRank 분석의 샘플을 선택하는 근거를 제공하는 것이다. "mDP와 mRP에서 어떤 사이토카인이 다른가"를 분석하려면 mDP 환자와 mRP 환자를 비교해야 한다. 하지만 전체 404명 중에서 mDP로 분류된 환자 중 일부가 실제로는 sDP에 가까운 면역 패턴을 가지고 있을 수 있다. 이런 환자가 비교에 포함되면 결과가 오염된다. UMAP에서 mDP/mRP 클러스터에 확실히 속한 환자만 골라내면 195명의 깨끗한 비교 그룹이 만들어진다. sDP/sRP 클러스터에서는 64명이 선택된다. 이 선별된 샘플들로 Volcano Plot을 그리면 두 그룹 사이의 진짜 차이가 노이즈 없이 드러난다. 이건 이전 BERT 프로젝트에서 혼동 행렬로 오분류 패턴을 파악한 뒤 "Business-Sci/Tech 경계 기사를 집중 분석하자"고 방향을 잡은 것과 같은 논리다. 전체를 무차별적으로 보는 대신, 의미 있는 하위 그룹을 먼저 정의하고 그 그룹 안에서 깊이 파고드는 것이다. UMAP이 그 하위 그룹의 정의를 데이터 기반으로 해주는 것이다.
2026-03-04 ⋯ RF 사이토카인 SHAP 분석 #5 결과 분석 - SHAP Dot Plot
Random Forest 모델이 COVID-19 환자의 중증 여부를 예측한다. 166개의 사이토카인 수치를 입력으로 받아서 "이 환자는 중증이 될 것인가"를 판단한다. 모델이 잘 예측한다는 건 알겠는데, 왜 그런 예측을 내렸는지는 모른다. 어떤 사이토카인이 중요한 건지, 그 사이토카인이 높으면 중증인 건지 낮으면 중증인 건지, 이런 질문에 모델 자체는 답해주지 않는다. SHAP Dot Plot은 이 세 가지 질문에 동시에 답한다. 어떤 사이토카인이 얼마나 중요한가, 어떤 방향으로 영향을 미치는가, 그리고 실제 발현량과 예측 방향이 어떻게 연결되는가를 하나의 그림에 담는다. SHAP 값: 게임 이론에서 온 공정한 기여도 분배 SHAP(Shapley Additive Explanations) 값을 이해하려면 먼저 공정한 기여도 분배라는 문제를 생각해야 한다. 네 명이 팀을 이뤄 축구 경기에서 5골을 넣었다. 각자의 기여도를 어떻게 공정하게 나눌까? 선수 A가 빠졌을 때 팀이 2골만 넣었다면 A의 기여는 3골이다. 하지만 이것만으로는 부족하다. A와 B가 함께 있을 때의 시너지, A와 C가 함께 있을 때의 시너지가 다르기 때문이다. Shapley 값은 모든 가능한 팀 조합에서 A가 추가되었을 때의 기여도를 계산하고, 그 평균을 A의 공정한 기여분으로 정한다. 이것이 게임 이론의 Shapley 값이다. SHAP은 이 아이디어를 머신러닝에 적용한 것이다. 환자 X의 중증 확률을 모델이 0.73으로 예측했고, 전체 환자의 평균 예측값이 0.45라고 하자. 0.73과 0.45의 차이인 0.28을 166개 사이토카인에 공정하게 분배하는 것이 SHAP 분석이다. CD274가 없었다면(평균값으로 대체했다면) 예측이 0.55였을 거라면, CD274의 SHAP 값은 +0.18이다. CD274가 이 환자의 중증 예측을 0.18만큼 올렸다는 뜻이다. TNFSF10이 없었다면 예측이 0.80이었을 거라면, TNFSF10의 SHAP 값은 -0.07이다. TNFSF10이 오히려 중증 확률을 0.07만큼 낮췄다는 뜻이다.
모든 사이토카인의 SHAP 값을 합하면 그 환자의 예측값과 전체 평균의 차이와 정확히 일치한다. 0.18 + (-0.07) + 나머지 164개의 합 = 0.28이다. 기여도가 빠짐없이, 중복 없이 분배된다. SHAP 행렬: 549명 × 166개 이 분석에서 계산된 SHAP 행렬은 549행 166열이다. 549명의 환자 각각에 대해 166개 사이토카인의 SHAP 값이 계산되어 있다. 행렬의 (i, j) 셀은 "환자 i에서 사이토카인 j가 중증 예측에 얼마나 기여했는가"를 나타낸다. Dot Plot의 세 차원 SHAP Dot Plot은 이 549×166 행렬의 정보를 하나의 그림에 압축한다. 그림에서 각 점이 하나의 환자에서의 하나의 사이토카인에 대한 SHAP 값이다. X축은 SHAP 값이다. 점이 오른쪽에 있으면 그 사이토카인이 그 환자에서 중증 확률을 높이는 방향으로 기여했고, 왼쪽에 있으면 중증 확률을 낮추는 방향으로 기여했다. 0에 있으면 기여가 없다. Y축은 사이토카인의 종류다. 상위 20개가 중요도 순서로 나열되어 있다. 중요도는 각 사이토카인의 SHAP 값 절대값 평균으로 결정된다. 절대값의 평균이 크다는 건 양수든 음수든 그 사이토카인이 예측에 큰 영향을 미친다는 뜻이다. 점의 색깔이 세 번째 차원이다. 빨간 점은 그 환자에서 해당 사이토카인이 높게 발현된 것이고, 파란 점은 낮게 발현된 것이다. 이 색깔이 핵심이다. SHAP 값(X축)과 실제 발현량(색깔)의 관계를 보면 사이토카인이 어떤 방향으로 작용하는지가 드러나기 때문이다. CD274 행을 보자. 빨간 점(발현 높음)들이 오른쪽(SHAP 양수)에 모여 있고, 파란 점(발현 낮음)들이 왼쪽(SHAP 음수)에 모여 있다. 이건 명쾌한 정방향 관계다. CD274가 높으면 중증 확률이 올라가고, CD274가 낮으면 중증 확률이 내려간다. CD274 발현량 자체가 중증도의 직접적인 지표라는 뜻이다. 상위 13개 사이토카인 대부분이 이 정방향 패턴을 보인다. KIT, FLT3LG, GDF15, CXCL10, PTX3 모두 발현이 높을수록 중증 방향으로 기여한다. 이 사이토카인들이 높다는 건 과도한 면역 활성화, 조직 손상, 염증 반응이 진행 중이라는 신호다. 그런데 14번째인 TNFSF10에서 패턴이 뒤집힌다. 빨간 점(발현 높음)이 왼쪽(SHAP 음수)에 있고, 파란 점(발현 낮음)이 오른쪽(SHAP 양수)에 있다. 정반대다. TNFSF10이 높으면 오히려 중증 확률이 내려가고, TNFSF10이 낮으면 중증 확률이 올라간다. 이것은 생물학적으로 의미가 있다. TNFSF10은 TRAIL이라고도 불리는데, 바이러스에 감염된 세포를 선천 면역이 제거하는 과정에서 중요한 역할을 한다. TNFSF10이 높다는 건 선천 면역이 정상적으로 작동하고 있다는 뜻이다. 반대로 TNFSF10이 낮다는 건 선천 면역이 약화되어 바이러스를 효과적으로 제거하지 못하고 있다는 뜻이다. 그래서 이 사이토카인의 부재 자체가 중증의 신호인 것이다. MPO도 같은 역방향 패턴을 보인다. 상위 20개 사이토카인의 생물학적 의미 이전 BERT 프로젝트에서 혼동 행렬이 "Business와 Sci/Tech 사이에서 오분류가 집중된다"는 구체적 진단을 내려줬듯이, SHAP Dot Plot은 "어떤 사이토카인이 중증 예측에 어떻게 기여하는가"라는 구체적 진단을 내려준다. 단순히 "CD274가 중요하다"고만 말하면 의사가 할 수 있는 일이 별로 없다. 하지만 "CD274가 높을수록 중증 위험이 올라가고, TNFSF10이 낮을수록 중증 위험이 올라간다"고 말하면 구체적인 모니터링 전략이 나온다. CD274를 추적해서 올라가면 경고를 보내고, TNFSF10을 추적해서 떨어지면 경고를 보내는 시스템을 만들 수 있다. 더 깊이 들어가면, 상위 20개 사이토카인이 세 가지 생물학적 경로로 분류된다는 점도 중요하다. CD274, CXCL10, LGALS9, IL18은 면역 활성화와 염증 경로에 속한다. 과도한 면역 반응, 이른바 사이토카인 폭풍의 지표들이다. GDF15, PTX3, CHI3L1, ST2는 조직 손상과 스트레스 경로에 속한다. 폐와 내피 세포가 손상되고 있다는 신호다. KIT, FLT3LG, VCAM1은 선천 면역과 조혈 경로에 속한다. 면역 세포의 생산과 이동에 관여하는 지표들이다.
하나의 사이토카인이 높다고 중증이 되는 게 아니라, 여러 경로에서 동시에 이상이 생길 때 중증으로 진행된다는 것이 이 그림의 핵심 발견이다. 정리: 이 그림으로 알수있는것 cf 다른 그림과의 연결
2026-03-04 ⋯ BERT 뉴스 분류 #10 모델 성능평가 - FPR @ TPR=0.95
지금까지 본 지표들은 모델의 전반적인 능력을 측정했다. AUROC는 모든 임계값에서의 종합 성적이었고, F1은 특정 결정 규칙에서의 성적이었다. 하지만 실제 서비스를 운영하는 사람이 묻는 질문은 좀 더 구체적이다. "Business 뉴스의 95%를 반드시 잡아내야 한다. 이 조건을 걸면 오탐이 얼마나 생기는가?" 이게 FPR @ TPR=0.95가 답하는 질문이다. TPR(재현율)을 0.95로 고정한 상태에서 FPR(오탐율)이 얼마인지를 본다. 왜 이런 질문이 필요한가? 실제 서비스에서는 재현율의 최소 기준이 정해져 있는 경우가 많기 때문이다. 암 진단 모델을 생각해보자. 실제 암 환자를 놓치면 생명이 위험하다. 그래서 "암 환자의 95% 이상을 반드시 잡아내야 한다"는 요구사항이 먼저 정해진다. 이 조건을 만족시키려면 임계값을 낮춰야 하는데, 임계값을 낮추면 정상인을 암이라고 잘못 판정하는 오탐이 늘어난다. 이 오탐이 얼마나 되는지가 모델의 실용성을 결정한다. 뉴스 분류에서도 같은 논리가 적용된다. "Business 사용자에게 Business 뉴스를 보여주는데, Business 뉴스의 95%는 반드시 전달되어야 한다"라는 서비스 요구사항이 있다면, 그 조건에서 비Business 뉴스가 얼마나 섞여 들어가는지를 알아야 한다. ROC 곡선 위의 한 점 이전 단계에서 ROC 곡선을 봤다. 임계값을 바꿔가면서 TPR과 FPR이 어떻게 변하는지를 그린 곡선이었다. AUROC는 이 곡선 아래의 전체 면적, 즉 모든 임계값에서의 종합 성적이었다. FPR @ TPR=0.95는 이 곡선 위의 특정한 한 점이다. TPR=0.95라는 수평선을 그어서 ROC 곡선과 만나는 지점을 찾고, 그 지점의 FPR 값을 읽는 것이다. AUROC가 곡선 전체의 요약이라면, FPR @ TPR=0.95는 서비스 요구사항에 해당하는 딱 그 지점에서의 성능이다. ROC 곡선이 왼쪽 상단에 바짝 붙어 있는 모델일수록 TPR=0.95 지점에서의 FPR이 작다. 곡선이 왼쪽 상단에 붙어 있다는 건 아주 낮은 FPR에서도 높은 TPR을 달성한다는 뜻이니까. 그래서 AUROC가 높은 클래스일수록 FPR @ TPR=0.95도 대체로 낮다. | 클래스 | FPR @ TPR=0.95 | 판정 | 해석 | |---|:---:|:---:|---| | **World** | **0.0044** | 양호 | 99.56% 특이도 | | **Sports** | **0.0046** | 양호 | 99.54% 특이도 | | **Business** | **0.0840** | 양호 | 91.60% 특이도 | | **Sci/Tech** | **0.0185** | 양호 | 98.15% 특이도 | World의 FPR이 0.0044다. 이게 뜻하는 바는 이렇다. World 기사의 95%를 잡아내도록 임계값을 설정하면, 비World 기사 1,000건 중 약 4.4건만 World로 잘못 분류된다. 1,000건 중 4건이면 거의 무시해도 될 수준의 오탐이다. Sports도 0.0046으로 거의 같은 수준이다. Sci/Tech는 0.0185다. 비Sci/Tech 기사 1,000건 중 약 18.5건이 Sci/Tech로 잘못 분류된다. World나 Sports보다는 높지만 여전히 2% 미만이니 양호하다. Business가 0.0840으로 가장 높다. 비Business 기사 1,000건 중 약 84건이 Business로 잘못 분류된다. 다른 클래스의 10배에서 20배에 달하는 오탐율이다. 이 84건의 대부분이 Sci/Tech 기사일 것이다. 혼동 행렬에서 봤듯이 Business로 잘못 유입되는 건 거의 전부 Sci/Tech이니까. 이 숫자들이 이전 단계들에서 본 모든 분석과 정확히 일치한다는 점이 중요하다. 혼동 행렬에서 Business-Sci/Tech 경계의 오분류가 집중되어 있었고, 클래스별 지표에서 Business의 Precision이 유독 낮았고, AUPRC에서 Business만 AUROC 대비 큰 격차를 보였다. FPR @ TPR=0.95에서도 Business만 유독 오탐이 많다. 모든 지표가 같은 약점을 다른 각도에서 가리키고 있는 것이다. 뉴스 분류에서 이 지표의 의미 뉴스 분류에서 FPR 0.084가 허용 가능한지는 서비스의 성격에 달려 있다. 뉴스 추천 시스템에서 Business 피드에 Sci/Tech 기사가 8.4% 섞여 있다면, 사용자 입장에서 크게 불편하지 않을 수 있다. 기술 기업의 투자 소식 같은 건 Business 사용자도 관심을 가질 만한 내용이니까. 뉴스 분류 도메인에서 FPR 0.10 미만이면 실용적으로 충분하다고 보는 기준에 비추면 0.084는 기준 안에 든다. 하지만 보안이나 의료 도메인이었다면 이야기가 완전히 달라진다. 악성 코드 탐지에서 정상 파일의 8.4%를 악성으로 오탐하면 시스템이 마비된다. 암 진단에서 정상인의 8.4%에게 불필요한 추가 검사를 받게 하면 의료 자원이 낭비된다. 이런 도메인에서는 FPR 0.01 미만을 요구하는 경우가 많다. 같은 숫자라도 적용 도메인에 따라 "충분히 좋다"와 "전혀 안 된다"가 갈리는 것이다. 이전 망막증 프로젝트에서 QuadWeightedKappa를 핵심 지표로 쓴 이유도 같은 맥락이다. 의료 도메인에서는 "얼마나 심각한 오류인가"가 중요해서 등급 간 거리에 가중치를 주는 지표를 택했다. 뉴스 분류에서는 "특정 재현율에서 오탐이 허용 범위인가"가 중요해서 FPR @ TPR=0.95를 본다. 지표의 선택 자체가 도메인의 요구사항을 반영하는 것이다. 정리 모든 클래스에서 FPR@TPR95 < 0.10 기준을 충족한다. Business가 0.0840으로 가장 높지만, 뉴스 분류 기준에서는 양호하다.
2026-03-04 ⋯ BERT 개념이해 #9 Softmax
Softmax가 필요한 이유: 분류기는 “점수”를 내고, 우리는 “확률”을 원한다 EnhancedClassifier의 마지막 Linear(256→4)는 숫자 4개를 뱉는다. 이 값들을 logits라고 부른다. logits는 “각 클래스에 대한 점수”이지 확률이 아니다. 그래서 음수도 나올 수 있고, 네 값의 합이 1일 필요도 없다. 그런데 우리가 사람에게 결과를 설명하거나, loss를 계산하거나, “Business일 확률이 몇 %인가?” 같은 해석을 하려면 결국 확률 형태가 필요하다. 즉 네 값이 모두 0 이상이고, 합이 정확히 1이 되는 분포가 필요하다. Softmax는 바로 이 변환을 해주는 마지막 단계다. 임의의 실수 벡터를 확률 분포로 바꾸는 함수라고 생각하면 된다. Softmax의 수식은 간단하다. 각 logit zᵢ에 exp를 씌우고, 전체 exp 합으로 나눠서 비율로 만든다. exp를 쓰는 순간 모든 값은 반드시 양수가 된다. 그리고 그 양수들을 전체 합으로 나누면 합이 1이 된다. 그래서 결과는 확률처럼 해석 가능해진다. 네 클래스라면 Softmax 결과는 “World 0.03, Sports 0.001, Business 0.96, Sci/Tech 0.004” 같은 형태가 된다. 이 숫자들은 합이 1이고 모두 0 이상이므로 확률 분포다. 여기서 중요한 점은 Softmax가 “순위를 바꾸지 않는다”는 것이다. 가장 큰 logit이 가장 큰 확률이 된다. 즉 argmax로 고른 클래스는 Softmax를 거친 뒤에도 그대로 가장 높다. Softmax는 “누가 1등인지”를 바꾸는 게 아니라, “1등이 2등보다 얼마나 압도적인지”를 확률로 표현해주는 역할을 한다. exp가 핵심인 이유: 단순 비율이 아니라 “차이를 증폭”해서 자신감을 반영한다 만약 exp를 쓰지 않고 그냥 점수를 합으로 나눠 비율을 만들면 음수가 끼어들어 확률이 깨진다. 그래서 exp는 “음수 문제를 없애기 위한 장치”이기도 하다. 그런데 exp의 더 중요한 역할은 따로 있다. exp는 차이를 아주 강하게 증폭한다. logit이 5.7과 2.3이면 차이는 3.4인데, exp를 하면 비율이 exp(3.4)만큼 벌어진다. 이건 약 30배 수준이다. 즉 점수 차이가 조금만 나도 확률 차이는 크게 벌어진다. 그래서 모델이 “이건 Business가 맞다”라고 강하게 확신할 때, 그 확신이 확률 분포에 분명하게 드러난다. 이 성질 때문에 Softmax는 “최댓값을 부드럽게 만드는 함수”라고 불린다. 가장 큰 값이 있으면 그쪽 확률이 크게 올라가지만, 그래도 나머지 클래스 확률이 0이 되지는 않는다. 완전히 딱 잘라 결정하는 argmax와 달리, Softmax는 불확실성도 함께 표현한다. Softmax와 Argmax의 차이: 학습에는 Softmax가 필요하고, 결정에는 Argmax가 필요하다 분류 결과를 하나 고를 때는 결국 argmax를 쓴다. 확률이 제일 높은 클래스를 선택하는 것이다. 하지만 argmax는 미분이 안 된다. “조금 바꾸면 출력이 조금 바뀌는” 연속성이 없기 때문이다. 신경망 학습은 미분 기반 최적화인데, argmax를 중간에 넣으면 역전파가 끊겨버린다. Softmax는 확률을 연속적으로 만들기 때문에 미분이 가능하고, 그래서 학습에 쓸 수 있다. 학습 과정에서는 Softmax로 확률을 만들고, 그 확률과 정답을 비교하는 방식으로 가중치를 업데이트한다. 추론에서는 Softmax로 확률을 보고 싶으면 보고, 최종 라벨이 필요하면 argmax로 고른다. Cross-Entropy Loss와의 연결: Softmax는 “정답 확률을 키우는 방향”으로 학습을 만든다 학습에서 핵심은 “정답 클래스 확률을 최대한 높이자”다. Cross-Entropy는 그걸 수식으로 정확히 강제한다. 정답이 Business라면, loss는 사실상 -log(p_business)가 된다. 정답 확률 p가 0.963이면 -log가 매우 작아서 “잘했다”가 되고, p가 0.02면 -log가 커져서 “큰 벌점”을 받는다. 이 구조 덕분에 모델은 자연스럽게 정답 클래스의 logit을 올리고 다른 클래스 logit을 내리는 방향으로 학습된다. 여기서 실무적으로 아주 중요한 포인트가 있다. PyTorch의 CrossEntropyLoss는 Softmax를 따로 요구하지 않는다. 내부에서 log-softmax와 NLL loss를 결합해서, 수치적으로 훨씬 안정적인 방식으로 처리한다. 그래서 학습 코드에서 logits에 Softmax를 먼저 적용해버리면 오히려 손해다. 학습할 때는 logits 그대로 CrossEntropyLoss에 넣고, 추론할 때 확률이 필요할 때만 Softmax를 명시적으로 적용하는 것이 표준이다. Label Smoothing과 Softmax: 과확신을 줄여서 일반화를 높인다 Softmax와 hard one-hot 정답을 함께 쓰면 모델은 극단적으로 학습하기 쉽다. 정답 클래스 확률을 1에 가깝게 만들면 loss가 거의 0이 되기 때문이다. 그러다 보면 모델이 “나는 항상 확실해”라는 태도를 갖게 되고, logits가 과도하게 커지는 방향으로 학습될 수 있다. 이런 과확신은 새로운 데이터에서 오히려 오분류를 더 크게 만들고, 확률이 믿을 수 없게 되는 문제를 만든다. Label smoothing은 이걸 완화한다. 정답을 [0,0,1,0]처럼 딱 1로 두지 않고, 정답을 약간 낮추고 나머지에 아주 작은 확률을 나눠준다. 그러면 모델은 “정답 확률을 무한히 1로 밀어붙이는” 대신, “적당히 높은 수준이면 충분하다”는 방향으로 학습된다. 결과적으로 확률 분포가 더 부드러워지고, 불확실한 문장에 대해 “불확실하다”는 표시를 내는 모델이 된다. 실전에서는 이런 확률의 신뢰도가 후처리나 thresholding에 매우 중요하다. 수치 안정성이 중요한 이유: exp는 커지면 터지고, 작아지면 0이 된다 Softmax는 exp를 쓰기 때문에 숫자 안정성이 매우 중요하다. logits가 1000 같은 값이면 exp(1000)은 float 범위를 넘어서 overflow가 난다. 그래서 실제 구현에서는 log-sum-exp 트릭을 쓴다. 가장 큰 logit c를 빼서 exp(zᵢ - c)를 계산하면, 최대가 exp(0)=1이 되어 overflow를 피할 수 있다. Softmax는 “입력에 동일한 상수를 더하거나 빼도 결과가 변하지 않는다”는 성질이 있어서 이런 트릭이 가능하다. PyTorch는 이 과정을 자동으로 해주기 때문에, 우리는 그냥 F.softmax나 CrossEntropyLoss를 쓰면 된다. 정리: Softmax는 “점수”를 “확률”로 바꾸는 분류기의 마지막 번역기다 EnhancedClassifier가 내는 logits는 비교 점수다. Softmax는 그 점수를 exp로 양수화하고, 전체 합으로 나눠 합이 1인 분포로 만들어 확률로 해석 가능하게 한다. exp는 점수 차이를 증폭시켜 모델의 자신감을 확률에 반영한다. 학습에서는 Softmax가 Cross-Entropy와 결합되어 정답 확률을 키우는 방향으로 가중치를 학습시키고, PyTorch는 이를 안정적으로 처리하기 위해 Softmax를 내부에 포함한다. Label smoothing은 Softmax가 과확신으로 치닫는 것을 막아 더 잘 일반화되게 한다. 그래서 Softmax는 단순한 수학 함수가 아니라, “모델의 판단을 사람이 해석 가능한 확률로 바꾸고, 학습을 가능하게 만드는 핵심 연결 고리”다. - Softmax = `exp(zᵢ) / Σ exp(zⱼ)`: logit → 확률
- 특성: 모두 양수 + 합=1 → 확률 분포로 해석 가능
- exp()의 증폭 효과: 높은 logit이 더 강하게 확률에 반영
- 학습 시**: CrossEntropyLoss에 Softmax 내장 → logit 직접 전달
- 추론 시**: `F.softmax(logits, dim=-1)` 명시적으로 적용
- Label Smoothing (ε=0.1): Softmax 출력이 과확신하지 않도록 → 일반화 향상
- 수치 안정성: PyTorch가 log-sum-exp 트릭으로 오버플로우 자동 방지
- 파라미터 없음: 순수한 함수 변환
2026-03-04 ⋯ BERT 개념이해 #8 [CLS] Token Extraction
왜 [CLS]를 뽑아야 하나: 모델 출력은 “토큰별 결과”인데 우리는 “문장 하나의 답”이 필요하다 BERT를 12층 다 통과하고 나면 출력은 (batch, L, 768)이다. 여기서 L은 최대 192이고, 각 토큰마다 768차원 벡터가 하나씩 있다. 즉 모델은 “문장 전체에 대한 한 번의 답”을 바로 주는 게 아니라, “문장 안의 각 위치(토큰)에 대한 표현”을 만들어준다. 그런데 뉴스 분류 같은 문제는 “이 문장은 스포츠냐 비즈니스냐”처럼 문장 전체에 대해 딱 하나의 라벨이 필요하다. 그래서 우리는 이 L개의 벡터 중에서 문장 전체를 대표할 단 하나의 벡터를 뽑아야 한다. 이때 가장 표준적인 선택이 [CLS] 토큰의 벡터를 쓰는 방식이다. 결국 이 단계는 이렇게 요약된다. “토큰별로 만들어진 표현들 중에서, 문장을 대표하도록 설계된 자리 하나를 골라서 (batch, 768)로 만들자.” 그 자리가 바로 position 0, 즉 [CLS]의 자리다. [CLS]는 일반 단어가 아니다. 토크나이저가 문장 맨 앞에 일부러 붙이는 특수 토큰이다. BERT를 설계할 때부터 “[CLS] 위치의 출력 벡터를 문장 단위 작업에 쓰자”라는 의도가 들어 있다. 특히 BERT의 사전학습 과제 중 하나였던 NSP(Next Sentence Prediction)에서는 입력이 [CLS] 문장A [SEP] 문장B [SEP] 형태로 들어가고, 모델은 “문장A 다음에 문장B가 자연스럽게 이어지는가?”를 맞힌다. 이때 분류 결정을 만드는 데 사용되는 대표 벡터가 바로 [CLS]의 출력이다. 다시 말해 사전학습 과정에서 [CLS]는 “두 문장을 통째로 보고 요약해서 판단하라”는 훈련을 받는다. 이 경험 때문에 [CLS]는 자연스럽게 “문장 전체 정보를 담기 좋은 그릇”으로 학습된다. fine-tuning에서 문장 분류를 할 때 [CLS]를 쓰는 이유가 바로 여기에 있다. 이미 사전학습으로 “문장 대표 역할”을 해본 자리이기 때문이다. Self-Attention 관점에서 보면: [CLS]는 모든 토큰을 바라보며 매층 업데이트된다 [CLS]가 문장 정보를 어떻게 모으는지 가장 직관적으로 이해하려면 Self-Attention을 떠올리면 된다. Self-Attention은 모든 위치가 모든 위치를 볼 수 있게 한다. [CLS]도 예외가 아니다. 첫 번째 레이어에서 [CLS]는 문장 안의 “wall”, “bears”, “back” 같은 토큰들을 보면서 자기 벡터를 업데이트한다. 두 번째 레이어에서는 이미 업데이트된 [CLS]가 다시 전체 토큰을 보면서 또 업데이트된다. 이런 일이 12번 반복된다. 중요한 포인트는 “요약이 한 번에 만들어지는 게 아니라, 레이어를 지나며 누적된다”는 점이다. 얕은 레이어의 [CLS]는 아직 토큰 임베딩과 가까운, 비교적 표면적인 정보에 머물 수 있다. 하지만 레이어가 깊어질수록 [CLS]는 중요한 토큰에 더 집중하고, 문장 의미를 더 고도로 압축한 표현으로 변해간다. 그래서 마지막 레이어의 [CLS] 벡터는 “문장 전체를 문맥적으로 이해한 요약”에 가깝게 된다. 실제 추출은 그냥 인덱싱이다. [:, 0, :] 하나로 끝난다. BERT 출력 텐서를 last_hidden_state라고 부르면, 그 모양은 (batch, L, 768)이다. 여기서 0번 위치가 [CLS]이므로, last_hidden_state[:, 0, :]를 하면 배치마다 [CLS] 벡터만 골라서 (batch, 768)이 된다. 이 연산은 학습 가능한 파라미터가 전혀 없다. “학습을 해서 만드는 층”이 아니라, “이미 만들어진 결과에서 대표 벡터 하나를 꺼내는 선택”이다. 그래서 이 단계는 수학적으로는 매우 단순하지만, 의미적으로는 모델 설계 의도를 그대로 활용하는 매우 중요한 연결 고리다. 다른 pooling이 있는데도 [CLS]를 쓰는 이유: 가장 표준적이고, 사전학습과 가장 일치한다 문장 벡터를 만드는 방법은 [CLS] 말고도 많다. 모든 토큰을 평균내는 mean pooling은 문장 전체를 균등하게 반영한다는 장점이 있고, max pooling은 강한 신호를 잡는 장점이 있다. attention 가중합 같은 weighted pooling은 “중요한 토큰에 더 주목”하도록 만들 수 있다. 하지만 이런 방법들은 BERT가 사전학습에서 직접 “문장 대표 자리”로 훈련했던 구조와는 결이 조금 다르다. AG News처럼 문장이 짧고 카테고리가 비교적 명확한 분류 문제에서는, 사전학습 의도와 일치하는 [CLS] 방식이 안정적으로 잘 먹히는 경우가 많다. 그래서 이 모델은 표준적인 [CLS] 추출을 선택했고, 실제로 높은 정확도까지 이어진다. 즉 이 선택은 “가장 단순하면서도 가장 BERT다운 방식”이라고 볼 수 있다. (batch, 768)이 분류기의 입력 [CLS] 추출을 하고 나면, 더 이상 시퀀스 차원 L이 없다. 이제는 각 문장마다 768차원 벡터 하나만 남는다. 이 벡터는 EnhancedClassifier로 들어간다. Dropout으로 과적합을 누르고, 768→256 Linear로 분류에 필요한 특징을 뽑고, GELU로 비선형 조합을 만들고, 다시 Dropout을 거친 후 256→4 Linear로 최종 로짓을 만든다. 즉 [CLS] 추출은 “토큰 단위 표현”을 “문장 단위 결정”으로 바꾸기 위해 반드시 거쳐야 하는 접합부다. cf 실제로 [CLS]에 어떤 정보가 담기는가 정리 BERT는 토큰마다 벡터를 출력하지만, 분류는 문장 하나당 한 벡터가 필요하다. [CLS]는 애초에 문장 대표 자리로 설계되고 NSP 사전학습에서 그 역할을 훈련받았다. Self-Attention을 거치며 [CLS]는 문장 전체 토큰을 참조하면서 정보가 누적되고, 마지막 레이어의 [CLS] 벡터는 문장 의미를 압축한 요약 표현이 된다. 그래서 우리는 단순히 last_hidden_state[:, 0, :]로 [CLS] 벡터를 뽑아 (batch, 768)로 만들고, 그걸 분류기에 넣어 최종 카테고리를 예측한다. - [CLS] Token Extraction = `last_hidden_state[:, 0, :]`
- `(batch, ≤192, 768)` → `(batch, 768)`: 시퀀스 차원 제거
- [CLS]가 특별한 이유: Self-Attention 12번을 통해 문장 전체 정보를 집약
- BERT 사전학습(NSP)에서 [CLS]가 "문장 쌍 판별"을 담당 → 자연스럽게 문장 표현 학습
- 학습 가능한 파라미터 없음: 단순 인덱싱 `[:, 0, :]`
- 이후 단계: `(batch, 768)` → EnhancedClassifier 입력
2026-03-04 ⋯ BERT 개념이해 #7 Residual Connection
Residual Connection이 필요한 이유: 깊어질수록 “처음 레이어”가 점점 학습을 못 한다 BERT는 Transformer layer를 12개 쌓는다. 레이어를 많이 쌓으면 표현이 정교해지고, 멀리 떨어진 단어 관계도 잘 잡고, 문맥 이해도 깊어진다. 그런데 깊은 신경망에는 오래된 고질병이 있다. 뒤쪽 레이어에서 나온 손실(loss)의 신호가 앞쪽 레이어까지 거슬러 올라갈 때, 그 신호가 길을 잃거나 너무 약해지거나, 반대로 너무 커져서 터지는 문제다. 역전파는 기본적으로 “곱셈의 연쇄”다. 12개의 레이어를 통과했다면, 그래디언트도 12번의 변화율을 연속으로 곱한다. 그 변화율들이 대부분 1보다 조금 작기만 해도, 곱을 반복하면 급격히 0에 가까워진다. 이게 그래디언트 소실이다. 반대로 대부분이 1보다 조금 크기만 해도 곱을 반복하면 폭발한다. 이런 상황이 되면 앞쪽 레이어는 “수정하라는 신호”를 거의 못 받거나, 너무 큰 신호를 받아 학습이 불안정해진다. 결국 깊은 모델을 만들고 싶어도 학습이 안 되는 벽을 만난다. Residual connection은 이 벽을 깨기 위해 “그래디언트가 확실히 흐르는 고속도로”를 하나 만들어주는 장치다. 핵심 아이디어: “변환 결과”에 “원본 입력”을 그냥 더해버린다 Residual이 없는 레이어는 보통 이렇게 생각한다. 입력 x를 받아서 어떤 복잡한 함수 F(x)로 바꿔서 그걸 출력으로 내보낸다. 즉 출력은 F(x)다. 그런데 residual이 있으면 출력은 F(x)가 아니라 x + F(x)가 된다. 입력을 그대로 복사해두었다가, 변환 결과에 그냥 더해버리는 것이다. 이게 왜 중요한지 음악 스튜디오 비유로 보면 바로 감이 온다. 보컬 원본 트랙이 있고, 그 위에 리버브나 EQ 같은 효과를 입힌 트랙이 있다. 최종 출력은 원본 + 효과다. 만약 효과가 마음에 안 들면? 효과 트랙을 0으로 만들면 된다. 그러면 출력은 원본 그대로다. 즉 시스템은 최소한 “원본을 망치지 않는 상태”에서 출발할 수 있다. 신경망에서도 똑같다. residual 구조에서는 F(x)가 0이 되면 출력은 x가 된다. 레이어가 “아무것도 안 하는 상태”를 쉽게 만들 수 있다. 이게 학습 초반에 엄청난 차이를 만든다. 랜덤 초기화된 레이어가 괜히 표현을 망가뜨리더라도, residual이 있으면 원본 정보가 지름길로 그대로 다음으로 전달될 수 있다. Residual의 또 다른 핵심은 학습 목표를 바꿔버린다는 점이다. residual이 없으면 레이어는 “원하는 출력 전체”를 F(x)로 만들어야 한다. 즉 처음부터 끝까지 다 책임져야 한다. 반면 residual이 있으면 레이어는 “x를 얼마나 바꿀지”, 즉 변화량만 학습하면 된다. 말 그대로 잔차(residual)를 학습하는 셈이다. 이 관점이 왜 쉬운지 직관적으로 생각해보자. 우리가 이미 꽤 괜찮은 초안이 있는데, 거기에 빨간펜으로 수정만 하면 훨씬 쉽다. 완전히 빈 종이에 처음부터 다시 쓰는 것보다, “필요한 부분만 조금 바꾸는 일”이 훨씬 수월하다. Transformer layer는 residual 덕분에 매 층마다 “이전 표현을 전부 갈아엎는” 대신 “이전 표현에 부족한 정보만 덧붙이는” 방식으로 작동할 수 있다. 그래서 12층을 쌓아도 점진적으로 의미가 정교해진다. 그래디언트가 안정적으로 흐르는 이유: 미분에 ‘1’이 항상 포함된다 Residual이 진짜로 학습을 살리는 이유를 수학적으로 한 줄로 보면 이거다. 출력이 x + F(x)이면, x에 대한 미분은 1 + ∂F/∂x가 된다. 여기서 ‘1’이 굉장히 큰 의미를 가진다. 레이어의 변환 F(x)가 어떤 이유로 미분값이 작아져도, 최소한 1이 남는다. 즉 그래디언트가 “완전히 끊겨서 0이 되는 길”이 구조적으로 막힌다. 이걸 상상해보면, 원래는 12개의 레이어를 거치면서 매번 “좁은 길”을 지나야 했는데, residual이 있으면 각 레이어마다 “기본적으로 열려 있는 넓은 길(=1)”이 같이 존재하는 것이다. 그래서 깊이가 깊어져도 앞단으로 신호가 도달할 가능성이 훨씬 높아진다. 물론 1 + ∂F/∂x가 항상 1 이상이라는 식의 직관은 “대략적인 방향성”으로 이해하는 게 좋다. 실제로는 LayerNorm이나 다양한 연산이 섞이면서 스케일이 조정되고, 완전히 단순한 곱셈처럼만 되지는 않는다. 그럼에도 핵심 메시지는 변하지 않는다. residual이 있으면 “그래디언트가 흐를 수 있는 직접 경로”가 생기고, 그게 깊은 네트워크 학습을 가능하게 만든다. BERT에서 Residual이 들어가는 위치: 한 레이어에 두 번, 중요한 구간마다 Transformer layer는 크게 두 블록으로 나뉜다. 첫 번째는 Self-Attention 블록이고, 두 번째는 FFN(MLP) 블록이다. BERT는 이 두 블록 각각에 residual을 붙인다. Self-Attention은 “문장 내에서 누구를 얼마나 참고할지”를 계산해 새로운 표현을 만들지만, 이 과정에서 표현이 크게 흔들릴 수도 있다. 그래서 attention 출력에 dropout을 한 뒤 입력 x를 더해준다. 그러면 attention이 실수해도 원본이 살아남는다. 그 다음 LayerNorm으로 스케일을 안정화한다. FFN은 “토큰별로 비선형 조합을 만들어 특징을 재구성”하는 부분이라, 역시 표현이 크게 변할 수 있다. 그래서 FFN 출력에도 dropout을 하고 다시 입력을 더해준다. 그리고 LayerNorm으로 마무리한다. 즉 BERT는 레이어 안에서 가장 큰 변화가 일어나는 두 지점마다 “원본을 지키는 지름길”을 깔아둔 셈이다. Post-Norm과 Pre-Norm: ‘정규화를 어디에 두느냐’가 안정성에 영향을 준다 BERT 원본 구조는 Post-Norm이다. 즉 Add를 먼저 하고 그 다음에 LayerNorm을 한다. 이 구조는 사전학습된 BERT 가중치와 형태가 동일하기 때문에, 그대로 가져와서 fine-tuning하기에 일관성이 좋다. 반면 최근의 많은 트랜스포머 변형은 Pre-Norm을 쓴다. 정규화를 먼저 하고 변환을 한 뒤 마지막에 residual로 더한다. 이 구조는 residual 경로에 “정규화의 영향이 덜 끼는” 형태가 되어, 아주 깊은 모델에서 학습이 더 안정적이라고 알려져 있다. 다만 구조가 달라지면 기존 사전학습 가중치와 정확히 맞지 않는 문제가 생길 수 있어서, BERT 계열에서는 원본 호환을 위해 Post-Norm을 유지하는 경우가 많다. 표현 관점에서의 의미: residual은 “원본 의미가 사라지지 않게” 지켜준다 Residual은 그래디언트만 살리는 장치가 아니다. 표현 자체도 보호한다. 레이어가 깊어질수록 표현은 점점 문맥화되지만, 그 과정에서 원본 단어 정보나 초기 신호가 완전히 사라지면 곤란하다. residual은 매 층마다 “이전 표현을 그대로 가져가는 길”을 남겨두기 때문에, 모델이 필요하면 초기 정보를 계속 유지할 수 있다. 그래서 “bank” 같은 단어가 문맥에 따라 금융기관/강둑으로 갈라지는 과정에서도, 완전히 새로 덮어쓰는 게 아니라 “기존 의미 위에 문맥 정보를 누적해서 얹는” 방식으로 발전할 수 있다. cf2 Pre-Norm vs Post-Norm: 그래디언트 비교 정리: Residual은 깊은 트랜스포머를 가능하게 만든 가장 단순한 핵심 장치다 Residual connection은 output = x + F(x)라는 단순한 덧셈이지만, 그 효과는 결정적이다. 학습 관점에서는 그래디언트가 흐르는 지름길을 만들어 소실과 폭발을 완화하고, 최적화 관점에서는 레이어가 전체 변환이 아니라 변화량만 학습하게 만들어 학습을 쉽게 한다. 표현 관점에서는 원본 정보가 레이어를 지나며 사라지는 것을 막고, 문맥 정보를 단계적으로 누적시키는 토대를 제공한다. 그래서 BERT처럼 레이어를 여러 개 쌓은 모델이 실제로 학습 가능해진다. - Residual = `output = x + F(x)` (입력을 변환 결과에 그대로 더함)
- 역할: 깊은 네트워크에서 그래디언트 소실/폭발 방지
- 직관: F(x)가 "전체 변환"이 아닌 "잔차(변화량)"만 학습 → 더 쉬운 최적화
- BERT 각 레이어에서 2번 사용: Attention Sublayer 후, FFN Sublayer 후
- 항상 `Add → LayerNorm` 순서로 (Post-Norm, BERT 원본 구조)
- 파라미터 없음: 단순한 덧셈 연산이지만 깊은 Transformer를 가능하게 하는 핵심 설계
2026-03-04 ⋯ BERT 개념이해 #6 GeLU
GELU가 필요한 이유: Linear만 쌓으면 “아무리 깊어도 한 장짜리”가 된다 먼저 가장 중요한 사실부터 다시 확인해보자. Linear는 입력을 섞어서 새 벡터를 만들지만, 그 자체는 어디까지나 선형 변환이다. 선형 변환을 두 번 연속으로 하면 어떤 일이 생기냐면, 겉보기에는 레이어가 두 개지만 실제로는 하나의 큰 행렬로 합쳐진다. z = W₂(W₁x)는 결국 z = (W₂W₁)x가 된다. 레이어를 10개 쌓아도 마찬가지다. 결국 모델은 “직선으로만” 세상을 나누는 수준에서 벗어나지 못한다. 언어는 직선으로 나뉘지 않는다. “it”이 누구를 가리키는지, “not … because …” 같은 구조가 어떤 의미를 만드는지, 이런 건 선형 결합만으로는 충분히 표현하기 어렵다. 그래서 Linear와 Linear 사이에 “한 번 꺾어주는 장치”, 즉 비선형 활성화 함수가 반드시 필요하다. GELU는 트랜스포머에서 그 역할을 맡는 대표적인 비선형 함수다. 활성화 함수를 가장 직관적으로 이해하는 방법은 ‘게이트’라고 생각하는 것이다. Linear가 재료를 섞어서 어떤 신호를 만들어냈다면, 활성화 함수는 그 신호를 보고 “이 신호를 다음으로 얼마나 보낼까?”를 결정한다. 완전히 보내기도 하고, 완전히 막기도 하고, 그 중간 정도만 보내기도 한다. 이 게이팅이 있어야 모델은 “조건부 규칙”을 만들 수 있다. 예를 들어 “이 특징이 일정 수준 이상이면 강하게 반응하고, 그렇지 않으면 반응하지 않는다” 같은 규칙은 선형만으로는 만들기 어렵지만, 활성화 함수가 들어가면 가능해진다. 언어에서 이런 조건부 판단은 너무 흔하다. 그래서 트랜스포머의 FFN에서 활성화 함수는 사실상 “복잡한 패턴을 만드는 엔진”에 가깝다. ReLU는 아주 단순하다. 입력이 0보다 크면 그대로 통과시키고, 0보다 작으면 0으로 만들어버린다. 그래서 마치 스위치를 ON/OFF 하는 느낌이다. 이 단순함 덕분에 빠르고 강력하지만, 동시에 부작용이 있다. 음수 영역은 완전히 차단되니까 음수에 담긴 정보는 사라진다. 그리고 어떤 뉴런이 계속 음수만 출력하게 되면 영원히 0만 내놓는 “죽은 뉴런”이 될 수 있다. 또한 x=0에서 함수가 꺾이기 때문에 미분이 매끈하지 않다. GELU는 같은 “통과/차단” 목적을 가지면서도 더 부드럽게 작동한다. 음수라고 해서 무조건 0으로 만들지 않는다. 대신 “음수면 대부분 막되, 아주 약하게는 남겨둔다”는 식으로 처리한다. 그래서 경계가 매끈하고, 음수 신호도 조금은 흐르게 해서 정보가 완전히 끊기지 않는다. ReLU가 칼로 자르는 느낌이라면, GELU는 볼륨을 서서히 줄이는 느낌이다. 주요 활성화 함수 비교 GELU의 한 줄 정의: “입력 x에 통과 확률 Φ(x)를 곱한 값” GELU의 핵심 수식은 GELU(x) = x · Φ(x)다. 여기서 Φ(x)는 표준정규분포의 누적분포함수(CDF)다. CDF는 “정규분포에서 값이 x 이하일 확률”이니까, x가 작으면 Φ(x)는 0에 가깝고, x가 크면 Φ(x)는 1에 가까워진다. 이걸 게이트로 해석하면 아주 자연스럽다. 입력 x가 크고 의미 있는 신호라면, Φ(x)가 1에 가까워져서 거의 x를 그대로 통과시킨다. 입력 x가 음수로 내려가면 Φ(x)가 0에 가까워져서 거의 막는다. 그런데 완전히 ‘딱’ 막는 게 아니라, -1 같은 값에서도 Φ(-1)이 약 0.16 정도니까 아주 약하게는 통과시킨다. 그래서 GELU는 “크면 거의 통과, 작으면 거의 차단, 경계는 부드럽게”라는 성질을 가지게 된다. GELU를 더 직관적으로 이해하는 방법은 확률적 해석이다. “입력 x가 주어졌을 때, 이 뉴런이 켜질 확률이 Φ(x)다”라고 생각해보자. x가 2이면 Φ(2)는 0.98쯤이라 거의 항상 켜진다. x가 0이면 Φ(0)=0.5라 반반이다. x가 -2이면 Φ(-2)는 0.02쯤이라 거의 꺼진다. 그러면 x · Φ(x)는 “켜질 확률만큼만 평균적으로 남는 값”, 즉 기대값처럼 해석된다. 그래서 GELU는 ‘확률적으로 게이팅하는 것’을 연속 함수 하나로 매끈하게 표현한 형태가 된다. 이 관점은 Dropout과도 잘 맞는다. Dropout은 확률적으로 뉴런을 꺼서 일반화를 돕는데, GELU는 입력 크기에 따라 확률이 바뀌는, 더 “데이터 적응적인” 게이팅이라고 볼 수 있다. 같은 확률 개념이지만 Dropout은 학습 규제 목적이고, GELU는 표현력 목적이라는 차이가 있다. “음수도 조금 통과”가 왜 도움이 되나: 정보 손실을 완전히 만들지 않기 위해서다 언어 모델에서 음수 값이 의미 없는 값이냐 하면 그렇지 않다. 어떤 차원에서는 “특정 속성이 있음”이 양수, “반대 속성이 있음”이 음수로 표현될 수 있다. ReLU는 음수면 전부 0으로 잘라버리니까, “반대 속성” 신호가 통째로 사라질 수 있다. GELU는 음수 영역에서도 아주 조금은 통과시키기 때문에, 그런 신호가 완전히 죽지 않는다. 그리고 학습 관점에서도, ReLU는 음수 영역에서는 기울기가 0이라 업데이트가 끊기기 쉬운데, GELU는 부드럽게 변화하므로 학습이 더 안정적으로 진행되는 경향이 있다. BERT에서 GELU가 쓰이는 위치: FFN의 “비선형 조합 공장” 한가운데 트랜스포머 블록을 떠올리면 Self-Attention이 먼저 나오고, 그 다음에 FFN이 나온다. Self-Attention은 토큰들 사이에서 정보를 어디서 얼마나 가져올지 정하고 섞는 단계다. 반면 FFN은 그 섞인 정보를 가지고 “토큰별로” 복잡한 조합을 만드는 단계다. 여기서 768을 3072로 확장하는 이유는 조합 공간을 크게 만들기 위해서였고, 그 확장된 3072 차원에서 GELU가 작동한다. 즉 GELU는 “확장된 공간에서 수많은 중간 특징들을 켰다 껐다 하며” 비선형 조합을 만들고, 다시 768로 압축될 때 유용한 특징만 남도록 돕는다. 그래서 BERT에서 GELU는 사실상 FFN이 단순한 두 번의 Linear가 아니라 “진짜 표현력을 갖는 모듈”이 되게 만드는 핵심 부품이다. EnhancedClassifier에서도 같은 논리로 GELU를 쓴다. 768→256으로 줄인 뒤 바로 4로 보내면 너무 직선적인 분류가 되기 쉬운데, GELU를 끼우면 중간층에서 비선형 경계를 만들 수 있어 분류기가 더 유연해진다. cf 왜 BERT는 ReLU가 아닌 GELU를 쓰는가 정리 GELU는 Linear만으로는 만들 수 없는 비선형성을 제공해서, 트랜스포머가 복잡한 언어 패턴을 학습할 수 있게 한다. ReLU처럼 양수는 잘 통과시키되, 경계가 부드럽고 음수도 조금 남겨서 정보가 완전히 끊기지 않는다. 수식으로는 x · Φ(x)라는 형태라 “통과 확률을 곱하는 게이트”로 해석할 수 있고, 이 확률적 직관 덕분에 트랜스포머의 FFN에서 특히 자연스럽게 작동한다. 결국 GELU는 트랜스포머에서 “비선형 조합을 만들고 학습을 매끈하게 하는 핵심 스위치”라고 보면 된다. - GELU = 입력값에 따라 통과 확률이 달라지는 부드러운 게이팅
- 수식: `GELU(x) = x · Φ(x)` (Φ: 표준 정규 CDF)
- ReLU와 달리 음수 입력도 일부 통과 → 죽은 뉴런 문제 없음
- 모든 점에서 미분 가능 → 부드러운 역전파
- BERT에서 FFN(768→3072→768)과 EnhancedClassifier(768→256→4)에서 사용
- ReLU 대비 BERT 성능 더 높음 (BERT 논문 실험 결과)
- 학습 가능한 파라미터 없음 (순수한 함수)