[개발 일기] / GNN Baseline: Propagated Cosine.md

GNN Baseline: Propagated Cosine

조회

2026년 4월 5일 | 개발 일기


adjacency 기반 표현과 진짜 메시지 패싱 사이에 비어 있던 한 칸을 메우는 작업이 이번 단계의 중심이었다. 이미 이 프로젝트에는 Common Neighbors, Jaccard, Adamic-Adar, Preferential Attachment, adjacency cosine, structural cosine, 그리고 최근에 붙인 feature_linear가 있었다. 그런데 막상 다음 단계를 생각해 보니, adjacency 기반 표현과 진짜 메시지 패싱 사이에 한 칸이 비어 있었다. 그래서 여기서는 Propagated Cosine을 그 중간 레이어로 세웠다.

propagated cosine이라는 이름을 붙였는데, 말 그대로 각 노드의 adjacency row를 자기 자신과 1-hop 이웃의 정보로 한 번 평균 내고, 그 전파된 벡터끼리 cosine similarity를 계산하는 방식이다. 아직 학습은 없다. 대신 내가 궁금했던 건 아주 단순했다. 이웃 정보를 한 번만 섞어도 링크 예측 점수의 결이 얼마나 달라지는가. 그 질문에 답할 수 있는 최소한의 기준선을 오늘 만든 셈이다.

1. 오늘 왜 이 한 칸이 필요했는지

요즘 이 프로젝트를 만지면서 계속 드는 생각이 있다. 휴리스틱과 단순 표현 baseline은 빨리 만들 수 있고 설명도 쉽다. 반면 GraphSAGE나 GCN 같은 학습형 encoder는 구현 복잡도도 올라가고, 숫자가 좋아져도 왜 좋아졌는지 해석하기가 훨씬 어려워진다. 문제는 그 사이를 이어 주는 중간 단계가 없으면, 나중에 성능 변화가 생겼을 때 그 차이가 학습 덕분인지, 메시지 패싱 자체 덕분인지, 아니면 단순히 평가 분산 때문인지 읽기가 애매해진다는 점이다.

특히 지금처럼 작은 그래프와 작은 샘플 결과를 다루는 구간에서는 더 그렇다. 너무 큰 모델을 바로 붙이면 프로젝트가 앞으로 간 것 같으면서도, 비교 기준은 오히려 흐려질 수 있다. 그래서 오늘은 모델을 크게 키우기보다 비교 축을 한 칸 더 세분화하는 쪽을 택했다. adjacency cosine이 현재 이웃을 직접 비교하는 기준선이라면, propagated cosine은 그 이웃 정보를 한 번 퍼뜨린 뒤 비교하는 기준선이 된다. 내 입장에서는 이게 나중에 진짜 GNN을 붙이기 전에 꼭 필요한 중간 발판이었다.

  • adjacency 기반 표현과 학습형 GNN 사이에 중간 단계가 필요했다.
  • 메시지 패싱의 효과를 학습과 분리해서 보고 싶었다.
  • 다음 단계에서 GraphSAGE나 GCN을 붙였을 때 비교 기준을 더 촘촘하게 만들고 싶었다.

2. 구현은 단순했지만, 질문은 꽤 분명했다

오늘 구현 자체는 복잡하지 않았다. 각 노드에 대해 기존 adjacency row를 기본 벡터로 잡고, 자기 자신과 이웃들의 벡터를 평균 내서 새 표현을 만들었다. 그다음 두 노드의 전파된 표현 사이 cosine similarity를 계산하면 된다. 수식으로 보면 아주 소박한 수준인데, 오히려 그래서 마음에 들었다. 아직 학습 파라미터가 없으니 결과가 어떻게 흔들리는지 읽기가 쉽고, baseline 실험 러너에 무리 없이 끼워 넣을 수 있기 때문이다.

node adjacency row
  -> self + neighbors average
  -> propagated node vector
  -> cosine similarity for candidate edge

나는 이런 단계를 자주 좋아한다. 큰 모델을 붙이기 전에, 지금 가진 재료로 어디까지 갈 수 있는지 먼저 보는 방식이다. 특히 GNN 계열은 이름만 커지기 시작하면 실험이 금방 무거워진다. 그 전에 전파 자체가 실제로 어떤 신호를 더해 주는지를 확인해 두면, 나중에 학습형 encoder를 붙였을 때도 무엇이 바뀌었는지 훨씬 또렷하게 읽을 수 있다.

3. 오늘도 테스트를 먼저 건드렸다

작업 순서는 이번에도 테스트부터였다. 실험 결과 목록에 propagated_cosine가 들어가야 한다는 조건을 먼저 추가했고, 그 상태에서 테스트가 깨지는 걸 확인했다. 새 baseline 하나 추가하는 일은 겉으로 보기엔 단순한데, 실제로는 import 경로, 결과 JSON, 정렬 로직, 실험 러너가 같이 얽혀 있어서 조용히 놓치기 쉽다. 나는 요즘 이런 변경일수록 먼저 실패를 보고 시작하는 편이 훨씬 덜 불안하다.

그다음에는 작은 그래프를 하나 만들어서, 같은 커뮤니티 안에서 거의 닫힐 것 같은 후보 edge와 전혀 다른 군집으로 건너가는 edge를 비교하는 테스트를 붙였다. 여기서 확인하고 싶었던 건 정답률 자랑이 아니라 방향성이다. 적어도 이 baseline이 같은 지역 구조를 공유하는 후보를 더 plausible하게 보는지는 확인해 두고 싶었다. 구현을 넣고 다시 돌렸을 때 그 테스트가 통과하니, 최소한 오늘 추가한 한 칸이 엉뚱한 쪽을 보고 있지는 않다는 확신이 생겼다.

  • baseline 목록에 새 모델명이 포함되는지 먼저 실패를 만들었다.
  • 같은 커뮤니티 후보를 더 높게 점수화하는지 별도 테스트를 붙였다.
  • 최종적으로 전체 테스트를 다시 통과시키고 실험 결과까지 재생성했다.

4. 결과를 다시 뽑아 보니, 숫자보다 해석이 더 좋아졌다

샘플 그래프 결과를 다시 생성해 보니, 오늘 추가한 propagated_cosine는 AUC 0.7222로 나왔다. 단독 최고는 아니었다. 이번에는 structural_cosine이 0.7778로 가장 위에 있었고, jaccardadjacency_cosine도 각각 0.7222로 같이 남았다. 숫자만 보면 아주 드라마틱한 변화는 아니다. 그런데 내 기준에서는 오히려 이런 결과가 더 쓸모 있다. 메시지 패싱을 한 번 흉내 낸다고 해서 갑자기 모든 게 좋아지는 건 아니라는 뜻이고, 동시에 adjacency 기반 표현이 전파 한 번 정도에는 쉽게 무너지지 않는다는 뜻이기도 하다.

반대로 오늘 다시 더 선명하게 보인 건 feature_linear였다. train AUC는 1.0000인데 test AUC는 0.1944까지 내려갔다. validation AUC가 0.7778이었는데도 test에서 이렇게 크게 꺾였다는 건, 지금 샘플 규모에서는 hand-crafted feature를 바로 학습형 점수기로 묶는 방식이 생각보다 예민하다는 신호로 읽혔다. 나는 이 부분이 오히려 좋았다. 이유는 간단하다. 이제는 무엇이 불안한지 더 명확하게 말할 수 있기 때문이다. 전파형 표현 baseline은 비교적 안정적으로 남았고, supervised baseline은 작은 그래프에서 과적합 냄새를 강하게 풍겼다. 이 차이가 다음 단계 우선순위를 정해 준다.

모델 AUC 내가 읽은 포인트
structural_cosine 0.7778 구조 요약 feature가 여전히 가장 안정적이었다.
adjacency_cosine 0.7222 현재 이웃 패턴만으로도 기본 성능이 단단했다.
propagated_cosine 0.7222 1-hop 전파를 넣어도 비교 축이 크게 흔들리지 않았다.
feature_linear 0.1944 작은 그래프에서 과적합 위험이 더 또렷하게 드러났다.

오늘 숫자에서 내가 얻은 건 승패보다 지형도에 가깝다. 어느 baseline이 지금 샘플에서 안정적인지, 무엇이 다음에 손볼 우선순위인지, 그리고 진짜 GNN encoder를 붙였을 때 어디와 비교해야 덜 착시가 생기는지가 조금 더 분명해졌다. 작은 프로젝트에서는 이런 해석 가능성이 꽤 중요하다. 숫자가 조금 덜 인상적이어도, 다음 반복을 더 정확하게 밀어 주면 충분히 값이 있다.

5. 코드만 바꾸지 않고 기록까지 같이 남겼다

오늘은 구현만 넣고 끝내지 않았다. README를 고치고, 새 iteration 문서를 추가하고, 샘플 결과 JSON도 다시 생성했다. 나는 이런 정리가 없으면 작업이 며칠 뒤 금방 뭉개진다는 걸 자주 느낀다. 특히 baseline 실험은 그때그때는 머리에 남아 있는 것 같아도, 다음 반복 때는 결국 어떤 기준선이 언제 왜 추가됐는지를 다시 찾아야 한다. 그래서 코드와 기록을 같이 묶어 두는 편이 훨씬 낫다.

그리고 오늘 변경은 sidecar git 기준으로 커밋하고 원격에도 올려 두었다. 이런 날의 커밋은 결과보다 문맥을 남기는 의미가 더 크다. 나는 프로젝트가 앞으로 간다는 감각이, 꼭 큰 모델을 붙였을 때만 생기지는 않는다고 생각한다. 오히려 오늘처럼 비교 기준을 하나 더 세우고, 그 이유와 숫자를 같이 남겨 두는 날이 나중에 더 오래 버틴다.

6. 오늘 회고: 큰 모델보다 비교 기준이 먼저였다

오늘 하루를 한 줄로 줄이면 이렇다. GNN을 학습시키기 전에, 메시지 패싱 한 번의 효과를 따로 볼 수 있는 자리를 만들었다. 나는 이런 단계가 생각보다 중요하다고 본다. 프로젝트가 아직 작은 데이터와 작은 그래프 위에서 움직일수록, 성능을 급하게 올리는 일보다 비교 기준을 제대로 세우는 일이 더 오래 간다.

지금은 propagated cosine이 최종 답이 아니다. 그래도 오늘 이 baseline을 넣어 두니 다음 질문이 훨씬 자연스럽게 이어진다. 2-hop으로 더 퍼뜨리면 어떤 일이 생길지, residual처럼 원래 표현을 같이 남기면 점수가 어떻게 달라질지, 그리고 그다음에 진짜 GraphSAGE나 GCN을 붙였을 때 어디서부터 학습의 기여가 시작되는지. 오늘 회고를 남기고 보니, 나는 성능을 당장 크게 올린 날보다 다음 판단이 더 명확해진 날을 더 좋은 작업일로 기억하게 되는 것 같다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.