[개발 일기] / GNN | FP/FN 비용 queue 추가.md

GNN | FP/FN 비용 queue 추가

조회

2026년 5월 7일 | 개발 일기


GNN 링크 예측 실험에서 threshold behavior label까지 붙이고 나니, 표를 읽는 방식이 한 번 더 애매해졌다. `tradeoff_only`라는 라벨은 꽤 유용했지만, 막상 다음 실험으로 보내려면 질문이 하나 남았다. false negative를 false positive로 바꿨을 뿐이라면, 그게 좋은 변화인지 나쁜 변화인지는 오류 비용을 어떻게 잡느냐에 달려 있기 때문이다.

이번에는 그 애매한 칸을 그대로 두지 않고, validation threshold 결과 위에 FP/FN 비용 시나리오를 얹었다. 목적은 성능을 갑자기 올리는 게 아니라, 어떤 config를 threshold 후보로 계속 볼지, 어떤 config는 score distribution이나 feature bias 쪽으로 넘길지 더 빨리 가르는 것이었다. 나는 이런 작은 routing layer가 실험을 오래 굴릴 때 꽤 중요하다고 느낀다. 평균 AUC만 보면 그럴듯한데, 실제 다음 행동은 잘 안 보이는 경우가 많기 때문이다.

1. 왜 비용 queue가 필요했나

직전 단계에서는 validation edge에서 threshold를 고르고 test edge에 적용한 뒤, 결과를 다섯 가지 라벨로 묶었다. `threshold_sensitive`는 cutoff를 움직였을 때 실제 test error가 줄어든 경우, `tradeoff_only`는 total error는 그대로인데 FP와 FN 방향만 바뀐 경우, `fp_heavy`와 `fn_heavy`는 남은 오류가 한쪽으로 기운 경우다.

여기서 가장 골치 아픈 건 `tradeoff_only`였다. 예를 들어 어떤 config가 false negative 3개를 false positive 3개로 바꾸면 total error는 똑같다. Balanced accuracy도 별 차이가 없을 수 있다. 그런데 추천 시스템이나 링크 예측에서 놓치는 비용잘못 붙이는 비용은 항상 같지 않다. missing link를 줄이는 게 더 중요하면 그 변화는 좋아 보이고, 엉뚱한 link를 붙이는 게 더 위험하면 나빠 보인다.

그래서 이번 반복에서는 기본 시나리오를 세 개로만 잡았다. 복잡한 비용 함수를 바로 넣기보다, 우선 판독면을 작게 고정했다.

  • balanced: false positive와 false negative를 같은 비용으로 계산한다.
  • false_negative_heavy: 놓친 positive edge를 더 비싸게 본다.
  • false_positive_heavy: 잘못 붙인 negative edge를 더 비싸게 본다.

계산은 단순하다. 각 scenario에서 `default cost - calibrated cost`를 구하고, 양수면 validation-selected threshold가 그 비용 기준에서는 유리하다고 표시한다. 음수면 오히려 손해다. 이 숫자를 config별로 모으면, `tradeoff_only`였던 config가 어떤 비용 가정에서만 살아나는지 바로 보인다.

2. 이번에 붙인 구현 단위

구현은 기존 calibration report를 다시 읽는 얇은 레이어로 붙였다. 새 학습을 다시 돌리는 구조가 아니라, 이미 저장된 validation/test FP/FN count를 cost scenario별 weighted error로 재해석한다. 덕분에 반복 비용은 낮고, 결과 JSON·CSV·PNG만 새로 남기면 된다.

이번에 추가한 산출물은 크게 세 가지다. 첫째, dataset/config 단위로 각 scenario의 default cost와 calibrated cost를 저장한다. 둘째, config 단위 summary에서 best helpful scenario와 worst harmful scenario를 뽑는다. 셋째, 최종 routing label을 붙인다. 여기서 routing label은 다음 네 가지로 나뉜다.

  • cost_sensitive_tradeoff: 어떤 비용에서는 좋아지고, 다른 비용에서는 나빠지는 config
  • calibration_candidate: 적어도 한 scenario에서 좋아지고, 뚜렷한 손해 scenario는 없는 config
  • calibration_risk: 좋아지는 scenario 없이 손해 scenario만 보이는 config
  • cost_neutral: threshold 이동이 없거나 비용 기준으로도 변화가 없는 config

나는 여기서 이름을 조금 고민했다. `cost_sensitive_tradeoff`가 길긴 한데, 이 이름이 제일 덜 속인다. 이 config는 좋다/나쁘다로 결론 낸 게 아니라, 비용 정의를 먼저 정해야 하는 후보라는 뜻이다. 실험 로그에서 이름이 길어지는 건 싫지만, 이런 단계에서는 짧은 이름보다 오해가 덜한 이름이 낫다.

3. 결과: tradeoff_only가 둘로 갈라짐

GNN threshold cost routing heatmap

Figure 1: config별 비용 시나리오에서 validation-selected threshold가 얼마나 유리하거나 불리한지 표시한 heatmap

그림은 `default cost - calibrated cost`를 보여 준다. 초록색 양수는 threshold 적용이 해당 비용 기준에서 유리하다는 뜻이고, 붉은색 음수는 오히려 손해라는 뜻이다. 여기서 핵심은 `hybrid_reduced`와 `propagation_only`가 balanced에서는 0.0으로 중립인데, false-negative-heavy에서는 +3.0, false-positive-heavy에서는 -3.0으로 갈라진다는 점이다. 즉 이 둘은 성능 순위표에서 바로 탈락시킬 대상이 아니라, 어떤 오류를 더 비싸게 보는지부터 정해야 하는 대상이다.

config routing label dominant behavior best scenario worst scenario
hybrid_reduced cost_sensitive_tradeoff tradeoff_only false_negative_heavy (+3.0) false_positive_heavy (-3.0)
propagation_only cost_sensitive_tradeoff tradeoff_only false_negative_heavy (+3.0) false_positive_heavy (-3.0)
hybrid_full calibration_candidate threshold_sensitive false_negative_heavy (+3.0) 없음 (0.0)
reduced_plus_degree cost_neutral fp_heavy 없음 없음

이 표를 보고 나서 해석이 조금 선명해졌다. `hybrid_full`은 threshold 후보로 계속 볼 수 있다. balanced에서도 +1.0이고, false-negative-heavy에서는 +3.0이다. 반대로 false-positive-heavy에서도 손해가 0.0이라서, 적어도 이 작은 기준선에서는 calibration risk가 크게 보이지 않는다.

반면 `hybrid_reduced`와 `propagation_only`는 방향이 다르다. 이 둘은 false negative를 줄이고 싶을 때는 좋아 보이지만, false positive를 엄격하게 막고 싶다면 바로 나빠진다. 그래서 다음 단계에서 이 둘을 다룰 때는 "threshold를 더 잘 찾자"가 아니라, 먼저 어떤 오류를 더 줄이고 싶은지를 명시해야 한다. 이건 작은 차이처럼 보여도 실험 설계를 꽤 많이 바꾼다.

4. 삽질한 지점: total error가 같다는 말의 함정

처음에는 `tradeoff_only`를 보고도 별생각 없이 "나중에 cost-sensitive로 보면 되겠지" 정도로 넘겼다. 그런데 막상 report를 만들려고 보니, cost-sensitive라는 말만 있고 실제로는 어떤 cost를 비교할지 정하지 않으면 아무것도 줄어들지 않았다. 그래서 이번에는 일부러 기본 시나리오를 세 개로 제한했다. 1:1, 1:2, 2:1. 단순하지만, 적어도 방향은 보인다.

또 하나는 routing label의 위치였다. dataset row에만 label을 붙이면 sample graph와 bipartite graph를 번갈아 열어 봐야 한다. 반대로 config summary에만 붙이면 어느 dataset에서 갈라졌는지 사라진다. 결국 둘 다 남겼다. dataset row에는 실제 cost delta를 두고, config summary에는 전체 routing label을 둔다. 이러면 블로그 표처럼 한 화면에서 후보군을 먼저 보고, 필요할 때만 dataset row로 내려갈 수 있다.

이런 작은 설계가 실험 코드에서는 은근히 중요하다. 모델을 한 번 더 학습시키는 일보다, 이미 나온 결과를 다시 열 때 덜 헷갈리게 만드는 일이 다음 반복 속도를 더 많이 바꿀 때가 있다. 나는 특히 작은 그래프 실험에서는 이쪽이 더 체감된다. 데이터가 작아서 숫자 하나하나가 쉽게 흔들리기 때문에, 숫자를 곧장 결론으로 승격하기보다 다음에 볼 queue로 남기는 쪽이 덜 위험하다.

5. 다음에 볼 것

이번 반복으로 결론이 난 건 아니다. 오히려 다음 질문이 더 구체적으로 바뀌었다. `hybrid_full`은 threshold rule 후보로 계속 남기고, `hybrid_reduced`와 `propagation_only`는 비용 정의를 먼저 붙인 뒤 다시 봐야 한다. `reduced_plus_degree`처럼 fp-heavy인데 cost-neutral로 남은 config는 threshold보다 score histogram이나 feature 조합 bias를 보는 편이 맞다.

  • edge type별 비용: 모든 FP/FN을 같은 비용으로 보지 말고, edge context별로 cost를 나눌 수 있는지 확인한다.
  • 반복 seed 검증: hardest seed 하나에서만 cost delta가 보이는지, 여러 seed에서도 반복되는지 다시 본다.
  • negative edge 분석: false-positive-heavy config가 어떤 negative edge를 반복적으로 높게 주는지 structural snapshot과 묶어 본다.

이번 작업은 모델 성능을 크게 올린 작업은 아니지만, 나한테는 꽤 필요한 정리였다. `tradeoff_only`라는 이름을 붙였을 때는 "애매한 후보"였는데, 비용 queue를 붙이고 나니 "비용 정의가 먼저 필요한 후보"로 바뀌었다. 이 정도만 되어도 다음 실험을 열 때 훨씬 덜 헤맨다. 작은 프로젝트를 오래 굴릴 때는 이런 식으로 애매함의 이름을 조금씩 바꿔 가는 일이 생각보다 큰 차이를 만든다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.