KRAFTON AI R&D 해커톤 예선: 파라미터 7천 개로 곱셈을 배우고, 노이즈 속에서 규칙을 되찾다
See also
댓글 불러오는 중…
댓글 불러오는 중…
본선에는 가지 못했지만, 2026년 3월 28~29일 KRAFTON AI R&D 해커톤 온라인 예선 이틀은 최근 한 공부 중 가장 밀도가 높았다. 첫째 날은 정해진 학습 조건 안에서 6비트 곱셈을 해내는 아주 작은 트랜스포머를 만드는 과제(MultiplierBoard), 둘째 날은 20% 노이즈가 섞인 수열에서 숨은 XOR 규칙을 되찾는 과제(SparseTap)였다. 결과부터 말하면 첫날은 파라미터 6,978개로 정확도 99.98%, 둘째 날은 이론 상한 80%에 맞는 79.99%까지 갔다. 이 글은 각 과제에서 막힌 곳, 푼 방법, 그리고 그래도 본선 컷을 넘지 못한 이유를 적는다.

포스터 기준으로 지원 마감은 2026.03.27 12:00, 1차 예선은 3.28~3.29 온라인, 본선은 4.4~4.5 오프라인이었다.
처음에는 소형 언어 모델을 만드는 것과 비슷한 감각으로 label smoothing, scheduled sampling 등을 적용했고, ReLU에 강한 임계를 두어 AND·캐리를 흉내 내는 방식도 시도했다. 두 방향 모두 검증 정확도나 안정적인 학습으로 이어지지 않았다.
타깃을 0으로 길게 패딩해 두면 모델이 곱셈 절차 대신 “앞부분이 0이면 뒤도 0으로 둔다” 는 지름길을 학습하기 쉽다. teacher forcing 형식은 맞아도, 실질적으로는 패딩으로 만든 분포에 맞춰지는 현상이었다.
임계값 기반 활성화로 논리 게이트를 흉내 내려다 기울기가 끊기고, 정확도가 3%대에서 더 이상 오르지 않았다. 미분 가능한 경로를 스스로 차단한 셈이다.
언어 모델에서 흔히 쓰는 요령은 모두 걷어 내고, 구조와 입력만으로 풀도록 다시 짰다.
곱 비트의 앞부분만 teacher 쪽에 붙이는 식으로 파이프라인을 다시 짰다. 학습 시에는 A + B + P[:-1] + [0] 형태로 넣고, 평가할 때는 A + B + [0]\*12에서 한 자리씩 채우는 방식으로 맞췄다.
vocab이 {0,1}뿐이라 임베딩이랑 마지막 선형층 가중치를 공유했다 (fc.weight = embed.weight).
고정 sin 대신 nn.Parameter로 두니 곱셈할 때 자주 나오는 대각 정렬 같은 패턴을 스스로 잡더라.
d_model=16, nhead=2, layer=2, FFN 64, dropout 0, norm_first, GELU.
마스크는 미래 자리를 가리는 causal mask를 그대로 썼다.
1class TinyTransformer(nn.Module):
2 def __init__(self):
3 super().__init__()
4 self.d_model = 16
5 self.nhead = 2
6 self.num_layers = 2
7 self.embed = nn.Embedding(2, self.d_model)
8 self.pos_encoding = nn.Parameter(torch.randn(1, 24, self.d_model) * 0.02)
9 encoder_layer = nn.TransformerEncoderLayer(
10 d_model=self.d_model,
11 nhead=self.nhead,
12 dim_feedforward=self.d_model * 4,
13 batch_first=True,
14 norm_first=True,
15 activation="gelu",
16 dropout=0.0,
17 )
18 self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=self.num_layers)
19 self.fc = nn.Linear(self.d_model, 2, bias=True)
20 self.fc.weight = self.embed.weight # weight tying
21
22 def forward(self, x):
23 seq_len = x.size(1)
24 mask = torch.triu(
25 torch.full((seq_len, seq_len), float("-inf"), device=x.device),
26 diagonal=1,
27 )
28 x = self.embed(x) + self.pos_encoding[:, :seq_len, :]
29 x = self.transformer(x, mask=mask, is_causal=True)
30 return self.fc(x)| 항목 | 값 |
|---|---|
| P1 (이론 쪽에서 주장한 하한 규모) | 36 |
| P2 (학습 파라미터) | 6,978 |
| Acc2 | 99.98% |
P1은 AND 게이트 단위로 필요한 최소 규모를 대략 계수해 적었고, P2는 위 모델을 그대로 세어 산출한 값이다.
손실 경치가 평탄하고 XOR는 이산적이라, 전형적인 LPN(Learning Parity with Noise) 유형에 가깝다.
규정상 수치 방법도 돌려야 해서 {0,1}을 {-1,1}로 바꿔 XOR를 곱으로 보이게 하고, sin(30·Wx) 쓰는 SIREN 두 층짜리 + Sign-SGD를 돌렸다.
한동안 정확도 49.8% 근처에서 그럴듯한 후보가 나와 기대했다. 그런데 노이즈가 20%라면, 규칙을 제대로 찾은 모델의 검증 정확도는 80% 근처여야 한다. 50%는 동전 던지기 수준이고, 노이즈에 맞춰진 가짜 해라고 보는 게 맞았다.
GPU 기반 상관·조합 탐색은 S가 6 이하일 때는 동작했으나, 실제 탭 수는 8에 가까웠고, C(64,8) 규모에서는 GPU 메모리 한계에 부딪혔다.
64개 방정식을 무작위로 뽑아 GF(2)에서 가우스 소거 → 해의 해밍 무게가 16 넘으면 버림 → 후보는 uint64로 패킹한 뒤 popcount로 앞부분만 빠르게 검증.
64개가 전부 깨끗할 확률은 대략 0.8의 64제곱이라 기대 시도는 백만 단위로 잡을 수 있다. 소거는 분기 많은 편이라 Numba JIT + CPU 한 코어가 GPU보다 낫다.
제출에 사용한 solution.py의 run_isd_ultra_fast와 동일한 구조를 아래에 요약했다.
1@njit
2def run_isd_ultra_fast(X_packed, Y, max_trials=5000000):
3 N = len(Y)
4 N_sub = 10000
5 X_sub_packed = X_packed[:N_sub]
6 Y_sub = Y[:N_sub]
7 for trial in range(1, max_trials + 1):
8 A_packed = np.zeros(64, dtype=np.uint64)
9 B = np.zeros(64, dtype=np.int8)
10 for i in range(64):
11 idx = np.random.randint(0, N)
12 A_packed[i], B[i] = X_packed[idx], Y[idx]
13 # GF(2) Gaussian elimination on A_packed, B
14 # drop if Hamming weight > 16
15 # pack B, verify on X_sub_packed vs Y_sub; if correct/N_sub > 0.75 return
16 return np.zeros(0, dtype=np.int8)이틀 모두 핵심 파이프라인은 동작했고, 둘째 날은 수치가 이론과도 맞췄으나 본선 컷은 넘지 못했다. 스스로 돌아보건대 원인은 다음과 같이 정리할 수 있다.
선발이 코드만으로 되지 않는다. 리포트, P1 쪽 서술, 파라미터 타이브레이커 같은 게 섞였을 가능성이 크다. P2는 꽤 줄였는데 P1 글이나 정리가 밀렸을 수도 있다.
둘째 날 시간 배분. SIREN 쪽에 시간을 썼다. 규정 충족용으로는 의미 있었지만 점수로 바로 가는 건 ISD였다. 다음엔 결정론 해부터 붙잡고 부가 실험은 나중에 할 것 같다.
총점 공식. 공개 안 된 가중치·동점 처리에서 밀렸을 수 있다. 내가 확인할 수 있는 건 로컬에서 이론이랑 맞았다는 것뿐이다.
재현성. 막판에 한 번 성공한 난수 시퀀스에 의존하기 쉽다. 여러 시드로 반복 검증하고, 최악의 실행 시간까지 README나 스크립트에 명시해 두었어야 했다.
구현이 성공해도 선발은 그걸로 끝나지 않는다는 걸 배운 이틀이었다. 다음에는 확실히 풀리는 해법부터 고정하고, 보고서는 코드와 같이 다듬고, 과제마다 시간을 미리 나눠 쓰겠다.