[AI 실험실]/[개인 프로젝트] GNN / GNN | Threshold behavior label 추가.md

GNN | Threshold behavior label 추가

조회

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


Threshold behavior label을 붙여 보니, 지난 calibration 표가 훨씬 빨리 읽혔다. 바로 전 반복에서는 validation edge에서 threshold를 고르고 그 값을 test edge에 적용했다. 그 결과 자체는 꽤 차분했다. hybrid_full만 error가 7개에서 6개로 줄었고, 나머지 config는 총 error가 그대로였다. 그런데 표를 다시 읽을 때마다 같은 말을 반복하고 있었다. threshold가 실제로 도움이 된 경우, false negative를 false positive로 바꿨을 뿐인 경우, 아예 false positive 쪽으로만 기운 경우를 매번 눈으로 분류해야 했다.

그래서 이번에는 모델을 더 키우지 않고, inspection queue를 줄이는 작은 리포트를 붙였다. 이름은 거창하게 잡지 않았다. calibration 결과를 읽어서 config마다 threshold_sensitive, tradeoff_only, fp_heavy, fn_heavy, balanced_static 중 하나를 붙인다. 성능 지표라기보다는 다음 실험을 어디로 보낼지 정하는 라벨에 가깝다.

1. 이번에 바꾼 코드

변경 범위는 작다. 기존 validation threshold calibration JSON을 입력으로 받아 dataset별, config별 behavior row를 만들고, config 단위로 label count를 다시 모았다. 출력은 JSON과 CSV, 그리고 label count heatmap 세 가지다. 나중에 graph family를 더 늘려도 같은 형식으로 이어 붙일 수 있게 dataset row와 summary row를 분리했다.

  • build_threshold_behavior_report: calibration report를 읽어 behavior label과 summary를 만든다.
  • run_structural_variant_threshold_behavior_report: JSON, CSV, PNG를 한 번에 저장하는 runner다.
  • plot_threshold_behavior_summary: config별 label count를 heatmap으로 저장한다.
  • tests/test_smoke.py: in-memory label 판정과 runner smoke test를 추가했다.

테스트는 일부러 작은 payload로 시작했다. threshold를 움직여 error가 줄면 threshold_sensitive, threshold는 움직였지만 FP/FN만 서로 바뀌면 tradeoff_only, threshold가 그대로인데 false positive가 더 많으면 fp_heavy, 반대면 fn_heavy로 고정했다. 전체 테스트는 18개 모두 통과했다.

Threshold behavior label heatmap

Figure 1. validation threshold 결과를 config별 behavior label count로 다시 묶은 heatmap

2. behavior label 기준으로 다시 읽은 표

가장 먼저 보이는 건 hybrid_full이다. 두 dataset 모두 threshold가 0.5 아래로 움직였고, 한 dataset에서는 실제 test error도 줄었다. 그래서 dominant label은 threshold_sensitive로 잡혔다. 다만 이게 곧바로 “hybrid_full이 좋아졌다”는 뜻은 아니다. sample_collab에서는 false negative 하나를 false positive 하나로 바꿨을 뿐이고, bipartite_bridge에서만 error가 하나 줄었다.

config dominant label moved datasets improved datasets tradeoff datasets mean selected threshold default FP/FN calibrated FP/FN
hybrid_full threshold_sensitive 2 1 1 0.425 3 / 4 4 / 2
hybrid_reduced tradeoff_only 1 0 1 0.450 2 / 3 5 / 0
propagation_only tradeoff_only 1 0 1 0.475 1 / 4 4 / 1
reduced_plus_closed_triplets fp_heavy 0 0 0 0.500 4 / 1 4 / 1
reduced_plus_degree fp_heavy 0 0 0 0.500 5 / 0 5 / 0

hybrid_reducedpropagation_only는 tradeoff_only 쪽으로 묶였다. 둘 다 bipartite_bridge에서 threshold를 낮추면 false negative 3개가 false positive 3개로 바뀌었다. 총 error는 그대로다. 이 경우에는 threshold를 더 촘촘히 찾는 것보다, false positive와 false negative의 비용을 먼저 정하는 편이 맞다.

reduced_plus_degree는 두 dataset 모두 fp_heavy로 잡혔다. threshold도 0.50에서 움직이지 않고, false positive 5개가 그대로 남았다. 이건 cutoff 문제가 아니라 score distribution이나 feature 조합 bias를 봐야 한다는 신호다. 나중에 score histogram을 붙인다면 제일 먼저 열어 볼 후보가 여기에 가깝다.

3. dataset별 label이 말해 주는 차이

aggregate만 보면 label이 깔끔해 보이지만, dataset별 row를 열면 조금 더 복잡하다. sample_collab에서 propagation_only는 balanced_static으로 남았고, reduced_plus_closed_triplets도 같은 쪽에 가까웠다. 반면 bipartite_bridge에서는 reduced_plus_closed_triplets가 fp_heavy로 간다. triad 계열 feature가 graph family에 따라 다르게 읽힌다는 이전 해석과 이어지는 지점이다.

dataset config label selected threshold test error delta default FP/FN calibrated FP/FN
sample_collab_graph hybrid_full tradeoff_only 0.40 0 0 / 2 1 / 1
sample_collab_graph hybrid_reduced fp_heavy 0.50 0 2 / 0 2 / 0
sample_collab_graph reduced_plus_degree fp_heavy 0.50 0 2 / 0 2 / 0
bipartite_bridge_graph hybrid_reduced tradeoff_only 0.40 0 0 / 3 3 / 0
bipartite_bridge_graph propagation_only tradeoff_only 0.45 0 0 / 3 3 / 0
bipartite_bridge_graph reduced_plus_closed_triplets fp_heavy 0.50 0 3 / 0 3 / 0
bipartite_bridge_graph reduced_plus_degree fp_heavy 0.50 0 3 / 0 3 / 0
bipartite_bridge_graph hybrid_full threshold_sensitive 0.45 1 3 / 2 3 / 1

이 표에서 내가 제일 조심해서 본 건 tradeoff_only다. 숫자로는 error가 줄지 않았지만, 오류 방향은 완전히 바뀔 수 있다. 예를 들어 bipartite_bridge의 hybrid_reduced는 default에서 FP/FN이 0 / 3인데, threshold를 낮추면 3 / 0이 된다. 추천 문제로 바꾸면 “추천해야 할 것을 놓치는 모델”에서 “추천하지 말아야 할 것을 붙이는 모델”로 성격이 바뀐 셈이다. 같은 error 3개라도 운영 비용은 전혀 다를 수 있다.

4. 라벨은 결론이 아니라 다음 queue다

이번 라벨을 성능 순위처럼 쓰면 안 된다. threshold_sensitive라고 해서 무조건 좋은 config가 아니고, fp_heavy라고 해서 곧바로 버릴 config도 아니다. 내가 붙인 라벨은 “다음에 어디를 열어 볼 것인가”를 줄이는 장치다. threshold_sensitive는 threshold rule을 더 검증할 후보이고, tradeoff_only는 비용 함수나 edge type별 cutoff를 볼 후보이며, fp_heavy와 fn_heavy는 feature 조합 bias를 볼 후보다.

  • threshold_sensitive: threshold 이동이 실제 test error를 줄인 적이 있는 후보
  • tradeoff_only: total error는 그대로지만 FP/FN 방향이 바뀐 후보
  • fp_heavy: cutoff가 움직이지 않거나 움직여도 false positive 쪽으로 남는 후보
  • fn_heavy: positive edge를 놓치는 쪽으로 남는 후보

이렇게 이름을 붙여 두니 다음 작업이 조금 더 명확해졌다. 예전 같으면 “AUC가 낮다”는 말에서 다시 encoder를 만지고 싶어졌을 텐데, 지금은 오류 유형별로 queue를 나누는 것이 먼저 보인다. 특히 reduced_plus_degree는 score histogram을 보고, hybrid_reduced와 propagation_only는 cost-sensitive summary를 붙이는 쪽이 자연스럽다.

5. 다음 단계

다음 반복에서는 이 라벨을 hardest seed 하나에만 붙이지 말고 여러 seed의 validation/test edge에 걸쳐 모아 볼 생각이다. 지금은 dataset이 두 개뿐이라 label count가 0, 1, 2 수준에서 끝난다. 그래도 구조는 만들어졌으니 graph family를 늘리거나 seed를 늘리면 같은 report가 그대로 커진다.

개인적으로는 이번 변경이 마음에 들었다. 결과를 더 멋지게 만든 건 아니지만, 표를 다시 읽는 시간을 줄였다. 작은 실험 프로젝트에서는 이런 얇은 장부가 꽤 중요하다. 숫자가 애매할수록 더 큰 모델을 붙이고 싶어지는데, 지금 필요한 건 모델 크기가 아니라 실패를 어느 칸에 넣을지 정하는 이름이었다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.