NumPy로 직접 만든 작은 GPT를 MCP 도구 선택기로 학습시키고, 역전파(gradient)로 어떤 단어가 그 결정을 만들었는지 추적해 본 공부 노트입니다.
모델이 틀렸을 때 원인을 찾는 과정을 일곱 단계로 따라갑니다. 각 단계는 질문 하나 → 기법 하나 → 답 하나입니다. 아래 단계를 누르거나 ← → 키로 넘겨 보세요. 자세한 설명은 이어지는 1~8번 섹션에 있습니다.
문장 끝에 <call> 표시를 붙이고, 그 위치에서 "다음에 올 단어"를 예측하게 합니다.
그 다음 단어가 곧 도구 이름입니다. 학습할 때도 이 한 자리에만 정답을 알려 줍니다(masked loss).
도구 10개(파일 읽기·쓰기·목록, 웹 검색·가져오기, 날씨, 일정, 메일, DB, git) 중 확률이 가장 높은 것을 고릅니다. 학습에 없던 동사·목적어 조합 60문장을 모두 맞혔습니다.
학습할 때와 같은 역전파를 쓰되 두 가지만 바꿉니다: loss 대신 점수 S = Z[<call>, y](출력 토큰 y = 도구의 logit 하나)를 미분하고, 파라미터가 아니라 입력 단어 임베딩까지 내려가서 멈춥니다. 순서는 ∂S/∂Z → ∂S/∂H → ∂S/∂X입니다.
# tool_attribution.py · score_and_grad
logits, _, caches = model.forward_embedded(X_token) # ① forward (임베딩부터)
G[k, y] = 1 # ② ∂S/∂Z : S = Z[k, y]
GX = G @ E # ③ ∂S/∂H (Z = H·Eᵀ)
for n in reversed(range(N)): # ④ 블록을 거꾸로
GX, _ = model.block_backward(GX, caches[n], n) # (파라미터 기울기는 버림)
return s, GX # ⑤ ∂S/∂X : 단어마다 벡터 하나
∂S/∂X는 단어마다 64차원 벡터입니다. 이것을 단어마다 숫자 하나로 줄이는 방법이 여러 가지이고, 결과가 꽤 다릅니다.
예문 → , 점수 S = Z[<call>, 선택 도구].
html은 grad×input이 로 작지만, IG는 로 가장 큽니다.
한 점의 기울기(국소)만 보면 놓치는 기여를, 경로 전체를 적분하는 IG는 잡아냅니다.단어 임베딩을 0(기준점)에서 실제 값까지 조금씩 키우면서(α: 0→1), 매 순간의 기울기를 모아 더합니다. 슬라이더를 움직여 보세요. 막대는 α까지 쌓인 IG, 아래 곡선은 그 순간의 점수입니다.
S(입력) − S(기준점)과 같습니다.
점수 변화 전체를 단어들에게 남김없이 나눠 준다는 뜻이고, 이 값이 맞지 않으면 적분 단계가 부족한 것입니다.모델이 확신할수록 확률이 1에 붙어 버려, 기울기가 사실상 0이 됩니다. 그러면 "아무 단어도 중요하지 않다"는 엉뚱한 결론이 나옵니다.
log P = (P ≈ 1)
softmax를 거치지 않은 logit이라 포화가 없습니다.
막대 길이는 단어별 기울기 크기 |∂S/∂x|입니다. 왼쪽은 값이 1e-6 수준이라 막대가 보이지 않습니다.
실제 요청 로그처럼 "상황 설명 + 요청" 형태의 긴 문장으로 학습했습니다. 상황 설명 30개는 모든 도구가 공유하고, 주제가 맞는 도구와 약 70%만 같이 나옵니다. 정확도는 괜찮아 보이는데, 정말 요청을 보고 고르고 있을까요?
요청 부분은 그대로라 정답도 그대로여야 합니다. 그런데 명사 하나로 판정이 뒤집히는 경우가 대부분이었습니다. 아래는 판정을 가장 많이 뒤집은 단어입니다(뒤집힌 횟수).
server를 draft로 바꾸자 파일 읽기가 파일 쓰기로 바뀌었습니다.
바뀐 문장에서는 모델이 고른 오답 도구의 logit Z[<call>, 오답]을 점수로 잡고, 어떤 단어가 그 오답을 끌어올렸는지 봅니다.
바꾸기 전 →
바꾼 후 →
바꾼 문장에서 draft 임베딩만 0으로 지우면 → .
정답으로 돌아오므로 원인이 확정됩니다. (attribution은 가설을 주고, 검증은 따로 합니다.)
학습 데이터에서 draft가 어떤 도구와 같이 나왔는지 셌습니다.
draft는 파일 쓰기 요청의 목적어("the draft to a file")에도 들어 있어서, 모델은 위치와 상관없이 "draft = 쓰기"로 배웠습니다.
같은 요청에 무작위 상황 설명을 붙인 사본을 추가해 다시 학습했습니다(counterfactual augmentation).
같은 문장의 IG를 보면 draft의 기여가 거의 0으로 줄고, 요청 단어가 결정을 이끕니다.
보강 후, 바꾼 문장 →
| 정확도 | 명사 하나로 뒤집히는 문장 | 뒤집는 치환 수 |
|---|
folder, weekend입니다.
이 단어들은 다른 도구 요청의 진짜 목적어라서, 상황 설명을 섞는 것만으로는 풀리지 않습니다.
attribution이 다음에 고칠 곳을 알려 준 것입니다.IG는 "어떤 단어"를 알려 줍니다. 같은 draft 오판을 세 가지 다른 기법으로 다시 보면,
그 단어가 몇 번째 층에서, 어떤 경로로 결정을 뒤집었는지까지 보입니다.
A = the server … (정답 ), B = the draft … (오답 ).
칸의 진하기 = ‖hB − hA‖ / ‖hA‖. embed 단계에서는 바뀐 단어 칸만 차이가 있고,
층을 지날수록 attention이 그 차이를 다른 위치로 옮깁니다. 테두리 칸이 결정 위치 <call>입니다.
각 단계의 <call> hidden state를 출력층에 바로 통과시킨 값입니다.
0보다 크면 정답 도구가 앞섭니다. 중간층은 출력용으로 학습되지 않았으므로 근사적인 보기입니다.
B는 1층을 지나도 정답 쪽이고, 2층 attention에서 오답으로 넘어갑니다.
칸의 진하기 = norm 기반 기여도 ‖Σh A[k,j] · V[j] WO‖ (가중치 × 그 단어가 보내는 값의 크기). 가중치만 보면 "많이 봤지만 영향은 작은" 단어를 구별할 수 없어서 norm을 씁니다.
A (정답)
B (오답)
B에서 단어를 하나씩 0으로 지우고, 오답 도구 점수 Z[<call>, 오답]이 얼마나 떨어지는지 직접 쟀습니다. 가장 큰 원인은 IG와 같지만, 작은 기여는 서로 다르게 봅니다(하나만 지운 효과 vs 경로 적분).
draft가 2층 attention을 통해 결정 위치로 들어가 도구 선택을 뒤집었고,
그 연결은 학습 데이터에서 draft가 거의 항상 파일 쓰기와 함께 나왔기 때문에 생겼습니다.
IG ✓ · occlusion ✓ · 지우면 정답 복귀 ✓ · 2층 attention ✓| 바뀐 단어를 가리키나 | 처음 모델 | 보강 후 |
|---|
네 가지가 모두 일치하면 근거가 되고, 어긋나는 경우(주로 attention)는 다음에 따라가 볼 공부거리입니다.
| 방법 | 보는 것 | 장점 | 주의 |
|---|---|---|---|
| |grad| | 한 점에서의 민감도 | 역전파 1번 | 부호 없음, 포화되면 0 |
| grad×input | 1차 테일러 근사 | 역전파 1번, 부호 있음 | 비선형 구간에서 틀림 |
| IG | 기준점→입력 경로 적분 | completeness로 검산 가능 | 역전파 여러 번, 기준점 선택에 따라 달라짐 |
| occlusion | 단어 하나를 지운 효과 | 정확하고 직관적, 내부를 못 보는 모델에도 사용 | 한 번에 하나라 단어 간 상호작용을 놓침 |
| hidden state 비교 | 정답·오답 입력이 갈라지는 층 | "어디서" 바뀌는지 보임 | 중간층 logit lens는 근사 |
| attention | 결정 위치가 정보를 가져오는 곳 | 경로가 보임 | 가중치 ≠ 영향, norm과 함께 봐야 함 |
S = Z[<call>, y]를 쓰면 포화가 없습니다.