2026년 5월 16일 | 개인 프로젝트
Negative context follow-up은 직전 context audit에서 남긴 다음 행동을 실제 target edge slice로 바꾸는 작업이었다. 전 단계에서는 reduced_plus_closed_triplets를 hard_edge_case_table로, reduced_plus_degree를 feature_bias_slice로 보내자고 적어 뒀다. 그런데 그 정도만 남겨 두면 다음에 다시 코드를 열었을 때 "그래서 어떤 edge부터 봐야 하지?"가 또 생긴다.
이번에는 새 encoder를 붙이지 않았다. 평균 AUC도 다시 돌리지 않았다. 대신 이미 만든 negative edge context audit report를 한 번 더 읽어서, hard outlier로 볼 edge와 broad borderline으로 볼 edge를 실제 목록으로 압축했다. 조금 심심한 작업처럼 보이지만, 이런 얇은 장부가 없으면 링크 예측 실험은 금방 감상평으로 흐른다. 없는 edge를 있다고 보는 이유를 보려면, 결국 edge 단위로 내려가야 한다.
큐를 글자가 아니라 산출물로 만들기
기존 context audit의 출력은 꽤 유용했다. hard leak이 있는 설정은 hard_outlier_context, threshold 바로 위에 넓게 붙은 설정은 broad_borderline_context로 갈라졌다. 문제는 그 다음이었다. next_probe에 문자열이 들어가 있으면 방향은 생기지만, 실제로 어떤 edge를 어느 기준으로 묶어야 하는지는 여전히 사람이 다시 읽어야 한다.
그래서 이번에는 build_negative_context_followup_report()를 추가했다. 함수는 context audit report에서 next_probe가 있는 config만 골라 target edge 목록을 만든다. hard_edge_case_table이면 hard leak edge를 우선 고르고, feature_bias_slice이면 borderline leak edge를 우선 고른다. 그리고 각 묶음에 target edge count, hard/borderline count, shared leak count, 평균 structural snapshot, dominant context tag를 붙였다.
말하자면 이번 변경은 "다음에 볼 것"이라는 메모를 "다음에 열 파일"로 바꾸는 작업이다. 나는 이 차이가 생각보다 크다고 봤다. 메모는 읽는 사람의 컨디션에 따라 해석이 흔들리지만, report는 최소한 같은 입력에서 같은 slice를 다시 만든다.
결과 요약
| config | next probe | slice label | target edges | hard | borderline | shared | max score |
|---|---|---|---|---|---|---|---|
reduced_plus_closed_triplets |
hard_edge_case_table |
hard_outlier_edge_table |
4 | 1 | 3 | 4 | 0.9476 |
reduced_plus_degree |
feature_bias_slice |
neighbor_degree_bias_slice |
5 | 0 | 5 | 4 | 0.5569 |
표로 놓고 보니 둘의 성격이 더 분명해졌다. reduced_plus_closed_triplets는 target edge 4개 중 hard leak은 하나뿐이다. 그런데 그 하나가 0.9476까지 튀었다. 나머지 3개는 borderline leak이다. 그래서 이 설정을 broad failure로만 보면 안 되고, 먼저 hard outlier edge table을 열어야 한다.
reduced_plus_degree는 반대다. hard leak은 하나도 없고 target edge 5개가 모두 borderline leak이다. 평균 target score는 0.5480이고, 최대 score도 0.5569다. 이건 특정 edge 하나가 폭발했다기보다 score surface가 threshold 0.5 바로 위로 넓게 밀린 모양에 가깝다. 그래서 slice label도 neighbor_degree_bias_slice로 잡혔다.
closed-triplets 이름에 속지 않기
가장 크게 튄 edge는 [0, 7]이었다. 이 edge의 score는 0.9476이고, context tag는 hard_outlier, degree_gap_high, triangle_free로 붙었다. 여기서 헷갈리기 쉬운 부분이 있다. config 이름은 reduced_plus_closed_triplets인데, top edge의 closed_triplets_mean과 clustering_mean은 0이다.
처음 봤을 때는 이름 때문에 closed-triplets feature가 원인처럼 보였다. 그런데 숫자는 그 결론을 바로 밀어내고 있었다. hard leak edge는 삼각형 신호가 살아 있는 edge가 아니라 triangle-free context에 있었다. 그러면 다음 분석은 "closed triplets가 문제다"가 아니라, closed-triplets를 포함한 입력판에서 triangle-free hard negative가 왜 크게 양성 쪽으로 밀렸는지를 봐야 한다.
이런 장면 때문에 나는 요즘 config 이름을 너무 믿지 않으려고 한다. 이름은 실험 구분자일 뿐이고, 실제 원인은 edge snapshot을 열어야 보인다. 특히 작은 그래프에서는 feature 하나가 직접 원인처럼 보이다가도, 다른 feature와 같이 놓인 조합 효과가 더 큰 경우가 자주 나온다.
degree variant는 한 점보다 표면
reduced_plus_degree 쪽은 더 평평한 실패다. target edge 5개가 모두 borderline leak이고, dominant context tag에는 borderline 5개, degree_gap_high 4개, neighbor_degree_dense 4개, triangle_free 4개가 잡혔다. 평균 snapshot은 degree_mean=1.9, degree_gap=1.0, avg_neighbor_degree_mean=2.6833, two_hop_neighbors_mean=2.8이었다.
이 결과는 한 edge를 잡아내는 방식으로는 부족해 보인다. 오히려 degree가 reduced core 위에 들어왔을 때, neighbor degree가 높은 음성 edge들을 threshold 바로 위로 살짝 올리는지 봐야 한다. 그래서 다음 probe가 feature_bias_slice인 것도 납득이 갔다. edge 하나를 확대하는 대신, degree gap과 neighbor density별로 borderline score를 잘라 보는 쪽이 더 맞다.
재미있는 건 shared edge가 많다는 점이다. 두 config가 같이 놓친 edge가 4개였다. [0, 7]도 closed-triplets 쪽에서는 hard leak이지만 degree 쪽에서는 borderline leak이다. 이러면 단순히 "이 config가 나쁘다"고 말하기 어렵다. 공통으로 어려운 negative edge와 config-specific하게 과하게 튄 edge를 나눠야 한다.
테스트와 산출물
테스트는 두 가지를 고정했다. 첫째, hard outlier context는 inspect_hard_edge_context action으로 가야 한다. 둘째, broad borderline context는 slice_feature_bias action으로 가야 한다. runner test에서는 JSON, CSV, PNG 산출물이 실제로 만들어지는지도 확인했다. 전체 테스트는 30개가 통과했다.
산출물은 multidataset_structural_variant_negative_context_followup 이름으로 남겼다. JSON에는 target edge 원문과 structural snapshot이 들어가고, CSV에는 summary와 dataset row를 평평하게 펼쳤다. PNG는 블로그에서 바로 보기 위해 target edge count와 shared edge count만 간단히 그렸다. 과하게 예쁜 그림은 아니지만, 다음에 어느 줄을 열어야 하는지 보는 데는 충분하다.
다음 작업 후보
다음 반복은 두 갈래로 나뉜다. closed-triplets 쪽은 [0, 7]을 중심으로 hard edge case table을 만들어야 한다. positive neighbor, two-hop context, config별 score 차이를 한 edge 단위로 펼치면, hard outlier가 정말 조합 효과인지 조금 더 보일 것 같다.
degree 쪽은 feature bias slice가 먼저다. borderline edge 5개를 degree gap, neighbor degree, two-hop density로 다시 자르고, 어느 조건에서 score가 0.5 바로 위로 붙는지 봐야 한다. 여기까지 하고 나면 그제야 threshold를 다시 만질지, feature 조합을 줄일지, negative sampling 쪽을 바꿀지 판단할 수 있을 것 같다.
이번 반복의 핵심은 성능 개선이 아니었다. 평균 AUC 한 줄 뒤에 숨어 있던 false positive를, 다음에 열 수 있는 작업 단위로 바꾼 것이 수확이다. 작은 프로젝트일수록 이런 장부가 좀 귀찮게 느껴지는데, 막상 다음 세션에서 이어 잡을 때는 이 귀찮음이 꽤 큰 시간을 아껴 준다.
'[AI 실험실] > [개인 프로젝트] GNN' 카테고리의 다른 글
| GNN | 공통 이웃 feature 압력 진단 (0) | 2026.06.24 |
|---|---|
| GNN | Negative target slice 분리 (0) | 2026.05.18 |
| GNN | Negative edge score histogram 추가 (0) | 2026.05.14 |
| GNN | Threshold action queue 추가 (0) | 2026.05.13 |
| GNN | Threshold behavior label 추가 (0) | 2026.05.06 |