[개발 일기] / GNN | Score bias probe 추가.md

GNN | Score bias probe 추가

조회

시리즈: GNN 실험일지 #11

이전: 10편 | 목록 | 다음: 12편

2026년 5월 14일 | 개발 일기


GNN 링크 예측 실험에서 threshold action queue까지 만들고 나니, 큐의 한 줄이 계속 눈에 걸렸다. inspect_score_bias. 이름은 그럴듯한데, 실제로는 아직 아무것도 본 게 아니었다. `reduced_plus_degree`와 `reduced_plus_closed_triplets`가 FP-heavy로 남았다는 사실만 있었고, 왜 없는 edge를 자꾸 양성 쪽으로 밀어 올리는지는 열어 보지 않았다.

그래서 이번에는 새 encoder를 붙이지 않고, action queue의 다음 칸을 그대로 처리했다. 기존 edge-case table과 threshold action report를 입력으로 받아서, `inspect_score_bias` 후보만 골라 positive edge와 negative edge의 score 분포를 따로 보는 Score bias probe를 추가했다. 성능을 올리는 코드라기보다, 실험을 덜 흐리게 만드는 코드에 가깝다.

1. score bias를 따로 본 이유

직전 단계에서는 config를 네 가지 action label로 나눴다. 비용 정책이 먼저 필요한 후보, calibration을 더 넓혀 볼 후보, score bias를 봐야 하는 후보, 그리고 일단 보류할 후보. 여기서 `inspect_score_bias`는 꽤 애매한 칸이었다. threshold를 바꿔도 비용 변화가 크지 않은데, behavior label은 계속 FP-heavy로 잡힌다. 이 상태를 그대로 두면 "비용표에서는 조용한데 왜 불안하지?" 같은 느낌만 남는다.

나는 이런 느낌을 프로젝트 안에 오래 두는 편을 별로 좋아하지 않는다. 느낌이 맞을 수도 있지만, 며칠 지나면 왜 그렇게 느꼈는지 잊는다. 그래서 이번 probe는 딱 한 가지 질문만 보게 했다. positive edge 평균 score와 negative edge 평균 score가 실제로 분리되고 있는가. 그리고 negative edge 중 몇 개가 기본 threshold 0.5 위로 올라와 false positive가 되는가.

이 방식은 대단한 분석은 아니다. 그래도 threshold sweep, validation calibration, cost routing을 지나온 뒤에는 꽤 필요한 얇은 렌즈다. AUC나 cost delta는 여러 edge를 섞어서 보여 준다. 반면 score bias probe는 없는 edge가 어느 정도 높게 밀리는지 바로 보여 준다.

2. 이번에 추가한 리포트

구현은 기존 산출물을 재사용했다. 새 학습을 돌리지 않고, `multidataset_structural_variant_edge_cases.json`과 `multidataset_structural_variant_threshold_action.json`을 같이 읽는다. 그다음 action label이 `inspect_score_bias`인 config만 남긴다. 이번 실제 대상은 두 개였다.

  • reduced_plus_closed_triplets: reduced core 위에 closed triplets를 다시 넣은 variant
  • reduced_plus_degree: reduced core 위에 degree를 다시 넣은 variant

각 config마다 positive edge 수, negative edge 수, positive/negative 평균 score, score gap, 0.5 이상으로 올라온 negative edge 수, 0.5 아래로 내려간 positive edge 수를 저장했다. label도 단순하게 시작했다. false positive가 더 많으면 `negative_score_leak`, false negative가 더 많으면 `positive_score_suppression`, positive 평균이 negative 평균보다 낮으면 `inverted_separation`, 둘 다 아니지만 threshold overlap이 있으면 `mixed_overlap`으로 잡았다.

3. 결과: negative score가 생각보다 높다

GNN score bias probe summary

Figure 1: inspect_score_bias 후보의 positive/negative 평균 score와 threshold overlap

결과를 보고 처음 든 생각은 조금 허무했다. 두 설정 모두 positive 평균이 negative 평균보다 뚜렷하게 높지 않았다. `reduced_plus_degree`는 positive 평균이 0.5454, negative 평균이 0.5464였다. 사실상 붙어 있다. 더 안 좋은 건 negative edge 5개가 모두 0.5 위로 올라왔다는 점이다. 이러면 threshold 0.5 기준에서는 전부 false positive로 읽힌다.

`reduced_plus_closed_triplets`도 비슷했다. positive 평균 0.5050, negative 평균 0.5153으로 gap이 -0.0103이다. false positive는 4개, false negative는 1개였다. 한 데이터셋에서는 label이 `inverted_separation`으로도 잡혔다. 이건 단순히 cutoff를 조금 옮기면 좋아지는 문제가 아니라, score를 만드는 feature 조합 자체가 negative edge를 충분히 눌러 주지 못한다는 신호에 가깝다.

config bias label positive mean negative mean FP / FN next probe
reduced_plus_closed_triplets negative_score_leak 0.5050 0.5153 4 / 1 negative score histogram
reduced_plus_degree negative_score_leak 0.5454 0.5464 5 / 0 negative score histogram

4. degree stabilizer라는 말을 다시 보기

이전 반복에서는 degree를 다시 넣은 variant를 stabilizer처럼 읽을 여지가 있었다. hardest split에서 바닥을 평평하게 만드는 장면이 있었기 때문이다. 그런데 이번 score probe를 보니 그 표현을 바로 쓰기엔 조심스럽다. 바닥을 평평하게 만드는 것과 negative edge를 잘 누르는 것은 같은 말이 아니다. `reduced_plus_degree`는 평균 score를 양쪽 모두 0.54 근처로 끌어올리는 식으로 안정화했을 가능성이 있다.

이 지점이 링크 예측에서는 꽤 중요하다. 추천 시스템 식으로 보면 없는 링크를 추천하는 false positive와, 있을 법한 링크를 놓치는 false negative는 비용이 다르다. degree가 전체 score를 부드럽게 올려서 positive를 놓치지 않게 도와줄 수는 있다. 하지만 동시에 negative도 같이 끌어올리면, 실제 추천 후보에서는 오탐이 늘어난다. 그래서 degree를 좋다/나쁘다로 자르기보다, negative sampling 난이도별로 다시 나눠 봐야 한다.

closed triplets도 비슷하다. triangle-rich graph에서는 의미 있는 신호일 수 있지만, triangle-free에 가까운 bipartite graph에서는 역할이 달라진다. 이번 리포트에서 `reduced_plus_closed_triplets`가 한쪽에서는 inverted separation으로 잡힌 것도 그 맥락에서 봐야 한다. 구조 feature는 독립적인 점수 축이 아니라, graph family와 같이 움직이는 축이다.

5. 다음 커밋이 조금 더 선명해짐

이번 작업은 모델을 개선했다기보다 다음 실험의 질문을 좁힌 쪽에 가깝다. 이제 `inspect_score_bias` 큐에 있던 두 config는 그냥 "FP-heavy"가 아니다. 둘 다 negative score가 0.5 위로 새는 장면이 있고, 특히 degree variant는 negative edge 5개가 모두 threshold를 넘었다. 다음에는 negative edge를 degree gap, shared-neighbor, graph family별로 나눠 histogram을 만드는 게 맞다.

좋았던 점은 action queue가 실제 다음 작업으로 이어졌다는 것이다. 큐를 만든 뒤 그냥 블로그용 표로만 남기면 금방 죽은 산출물이 된다. 그런데 이번처럼 큐의 한 줄을 바로 probe로 연결하면, 프로젝트가 조금씩 자기 길을 만든다. 평균 AUC를 한 번 더 보는 대신, 왜 오탐이 생기는지를 더 작은 파일로 좁혀 가는 흐름이다.

작은 그래프 실험이라 숫자 하나하나를 과신하면 안 된다. 그래도 이번 결과는 충분히 쓸모가 있다. score gap이 거의 없거나 음수라는 사실은, 다음 모델을 붙이기 전에 입력 feature와 negative edge 난이도를 먼저 봐야 한다는 경고다. 나는 이런 경고를 코드 산출물로 남겨 두는 편이 좋다. 나중에 다시 열었을 때 "왜 여기서 멈췄지"가 아니라 "negative score가 새서 멈췄지"라고 바로 이어갈 수 있기 때문이다.

시리즈: GNN 실험일지 #11

이전: 10편 | 목록 | 다음: 12편

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.