[AI 실험실]/[개인 프로젝트] GNN / GNN | Threshold sweep로 본 FP/FN 균형.md

GNN | Threshold sweep로 본 FP/FN 균형

조회

시리즈: GNN 실험일지 #6

이전: 5편 | 목록 | 다음: 7편

2026년 4월 28일 | 개인 프로젝트


Threshold sweep 리포트를 하나 더 붙이면서, 지난번 edge-case table에서 남겨 둔 찜찜함을 조금 더 선명하게 봤다. edge별 probability를 저장해 놓고 보니 0.5 하나로 자른 결과만 보는 게 생각보다 위험했다. 어떤 설정은 없는 edge를 너무 쉽게 붙이고, 어떤 설정은 있어야 할 positive edge를 계속 놓쳤다. AUC 표에서는 둘 다 비슷하게 나빠 보이는데, 실제 오류 비용은 완전히 다른 쪽으로 기울어 있었다.

이번에 한 일은 거창한 모델 개선이 아니다. 기존 GNN structural variant edge-case table을 다시 읽고, 각 config의 probability를 threshold 0.30부터 0.70까지 훑었다. 그리고 default 0.50 기준의 false positive, false negative가 threshold를 조금 바꿨을 때 어떻게 움직이는지 따로 저장했다. 나는 처음엔 이게 단순한 부록 표 정도일 거라고 생각했는데, 막상 찍어 보니 다음 실험을 어디로 갈지 정하는 데 꽤 직접적인 신호가 됐다.

중요한 전제는 하나 있다. 이번 sweep은 배포용 calibration이 아니다. hardest seed의 test edge가 sample_collab 4개, bipartite_bridge 6개라 표본이 너무 작다. 여기서 고른 threshold를 그대로 쓰겠다는 뜻은 전혀 아니다. 대신 “이 config가 0.5 기준에서 너무 보수적인가, 너무 낙관적인가, 아니면 threshold를 움직여도 해결이 안 되는가”를 보는 진단 레이어로만 썼다.

1. 구현한 것: edge-case table 위에 threshold curve 얹기

기존 산출물에는 dataset별 hardest seed, test edge, label, config별 probability와 margin이 들어 있었다. 이번에는 그 probability를 그대로 재사용했다. 새로 학습을 다시 많이 돌린 게 아니라, 이미 저장한 edge score를 기준으로 threshold별 confusion matrix를 만드는 쪽에 가깝다.

  • 입력: multidataset structural variant edge-case JSON
  • 대상 config: hybrid_full, reduced_plus_closed_triplets, reduced_plus_degree, hybrid_reduced, propagation_only
  • threshold 범위: 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70
  • 저장 지표: TP, FP, TN, FN, error count, precision, recall, specificity, F1, balanced accuracy
  • 비교 기준: default threshold 0.50과 best threshold의 오류 개수 차이

테스트는 먼저 작은 in-memory edge-case payload로 잡았다. 일부러 `loose_config`처럼 positive 쪽으로 쉽게 넘어가는 설정과 `strict_config`처럼 0.5 근처에서 애매하게 갈리는 설정을 넣고, best threshold가 기대한 쪽으로 움직이는지 확인했다. 그다음 runner가 JSON, CSV, PNG를 모두 쓰는지 smoke test를 붙였다. 마지막 전체 테스트는 14개 모두 통과했다.

GNN structural variant threshold sweep 요약 그래프

Figure 1: 0.5 고정 threshold와 sweep 이후 best threshold의 edge error count 비교

2. 전체 요약: hybrid_full만 error가 줄었다

전체 hardest seed edge 기준으로 보면, 실제로 총 error count가 줄어든 건 hybrid_full 하나뿐이었다. default 0.50에서는 7개를 틀렸는데, threshold sweep 이후에는 4개까지 내려갔다. 숫자만 보면 꽤 괜찮아 보인다. 그런데 안쪽을 보면 조금 더 조심해야 한다. false negative 4개가 0개로 줄어든 대신, false positive는 3개에서 4개로 늘었다.

config mean best threshold default errors best errors default FP/FN best FP/FN balanced accuracy
hybrid_full 0.375 7 4 3 / 4 4 / 0 0.3333 → 0.6250
hybrid_reduced 0.450 5 5 2 / 3 5 / 0 0.5000 → 0.5000
propagation_only 0.450 5 5 1 / 4 5 / 0 0.5000 → 0.5000
reduced_plus_closed_triplets 0.500 5 5 4 / 1 4 / 1 0.5000 → 0.5000
reduced_plus_degree 0.500 5 5 5 / 0 5 / 0 0.5000 → 0.5000

이 표에서 내가 제일 먼저 본 건 “best threshold가 얼마냐”가 아니었다. threshold를 움직였을 때 어떤 오류가 다른 오류로 바뀌는지였다. hybrid_full은 threshold를 낮추면 positive recall을 살릴 수 있다. 대신 negative edge를 더 많이 positive로 받아들인다. 즉 이 설정은 0.5 기준에서 너무 보수적이었지만, 보정이 곧바로 깨끗한 개선으로 이어지는 건 아니다.

반대로 `reduced_plus_degree`는 움직이지 않았다. default에서도 false negative가 0개였고, false positive가 5개였다. threshold를 조금 만져서 고칠 수 있는 calibration 문제가 아니라, 이 설정 자체가 negative edge를 positive 쪽으로 과하게 당기는 scoring bias를 갖고 있다고 보는 편이 맞다.

3. dataset별로 보면 trade-off가 더 노골적이다

dataset별 표는 더 작고 더 거칠지만, 방향은 오히려 잘 보였다. sample_collab에서는 hybrid_full threshold가 0.35까지 내려가면서 error가 2개에서 1개로 줄었다. 하지만 이 개선도 false negative를 줄이는 대신 false positive를 하나 더 받아들이는 방식이었다. bipartite_bridge에서는 hybrid_full이 0.40에서 5개 error를 3개로 줄였고, 여기서도 positive를 회수하는 쪽으로 움직였다.

dataset config best threshold default errors best errors FP delta FN delta
sample_collab hybrid_full 0.35 2 1 -1 +2
sample_collab propagation_only 0.45 2 2 -1 +1
bipartite_bridge hybrid_full 0.40 5 3 0 +2
bipartite_bridge hybrid_reduced 0.40 3 3 -3 +3
bipartite_bridge propagation_only 0.45 3 3 -3 +3
bipartite_bridge reduced_plus_degree 0.50 3 3 0 0

여기서 delta는 “default에서 best로 갔을 때 얼마나 줄었는가”로 읽었다. 그래서 FP delta가 음수면 false positive가 늘어난 것이다. sample_collab의 hybrid_full은 FP delta가 -1, FN delta가 +2였다. 즉 positive edge를 회수하는 데는 성공했지만, 없는 edge 하나를 더 붙였다. 이건 추천 시스템이라면 꽤 중요한 차이다. 없는 관계를 추천하는 비용과 실제 관계를 놓치는 비용이 같지 않을 수 있기 때문이다.

bipartite_bridge의 hybrid_reduced와 propagation_only도 재밌었다. 둘 다 threshold를 낮추면 false negative 3개를 false positive 3개로 바꾼다. 총 error는 그대로다. 이건 “threshold를 낮추면 좋아진다”가 아니라, 한 종류의 실수를 다른 종류의 실수로 교환한다에 가깝다.

4. 0.5가 틀렸다는 말은 아니다

이번 결과를 보면서 조심하려고 한 부분이 있다. threshold sweep을 하면 항상 “best threshold”라는 숫자가 나온다. 그런데 표본이 작으면 그 숫자는 쉽게 과적합된다. 특히 지금은 hardest seed edge만 펼친 상태라, threshold를 test edge에서 고르는 순간 이미 평가와 선택이 섞인다.

그래서 나는 이번 산출물을 calibration 결과가 아니라 calibration이 필요한지 보는 진단표로만 읽었다. 예를 들어 hybrid_full은 threshold-sensitive한 설정이다. 0.5에서 positive edge를 너무 많이 놓친다. 반면 reduced_plus_degree는 threshold-stable하게 false positive 쪽으로 기운다. 이 둘은 다음 실험에서 같은 방식으로 다루면 안 된다.

  • hybrid_full: threshold를 낮추면 error가 줄지만 FP 비용이 늘어난다.
  • reduced_plus_degree: threshold 이동보다 feature 조합 자체의 negative-edge bias를 봐야 한다.
  • propagation_only: positive edge 회수는 약하지만 false positive는 적은 쪽이었다.
  • hybrid_reduced: threshold만으로는 총 error가 줄지 않고 FP/FN 방향만 바뀐다.
  • reduced_plus_closed_triplets: 단순 threshold 이동보다 특정 edge context에서 크게 튀는 문제를 따로 봐야 한다.

이렇게 나누고 나니, 다음 작업도 조금 더 명확해졌다. 이제는 “어떤 config가 제일 높냐”보다 “어떤 config가 어떤 오류 비용을 만든다고 봐야 하냐”가 먼저다. 작은 그래프 실험에서는 이 차이를 빨리 드러내는 게 더 중요하다는 생각이 든다.

5. 다음에는 validation 기준 calibration으로 옮기기

다음 반복에서는 이 sweep을 그대로 확장하기보다, 먼저 synthetic graph family를 1~2개 더 늘리는 쪽이 좋아 보인다. 지금은 edge 수가 너무 적어서 threshold curve가 계단처럼 튄다. graph family를 늘리면 `triangle-rich`, `triangle-free`, `bridge-heavy` 같은 조건에서 FP/FN trade-off가 계속 유지되는지 볼 수 있다.

그다음에는 threshold를 test edge에서 고르지 않고, validation edge에서 고른 뒤 test edge에 적용하는 작은 calibration loop를 붙일 생각이다. 그렇게 해야 “진단용 sweep”에서 “재현 가능한 decision rule”로 넘어갈 수 있다. 지금 단계에서 결론을 세게 말하면 오히려 위험하다. 다만 한 가지는 분명해졌다. GNN 링크 예측 실험에서 AUC 하나만 보면, false positive가 많은 모델과 false negative가 많은 모델을 너무 쉽게 같은 칸에 넣게 된다.

이번 반복은 그 칸을 한 번 더 쪼갠 작업이었다. 모델을 크게 바꾸지는 않았지만, 다음 실험의 질문은 꽤 많이 바뀌었다. 나는 이런 작은 진단 레이어가 결국 프로젝트를 오래 끌고 가는 데 더 도움이 된다고 본다. 점수 하나를 올리는 것보다, 점수가 같은데 실패 방향이 다른 경우를 빨리 발견하는 쪽이 다음 코드의 모양을 더 잘 정해 주기 때문이다.

시리즈: GNN 실험일지 #6

이전: 5편 | 목록 | 다음: 7편

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.