본문 바로가기
TYLER SONGBlog
블로그 목록
LLM Research

주간 리뷰(Week 2): NumPy ML 코드를 안 보고 다시 쓸 수 있는가

이번 주 배운 softmax, cross entropy, linear regression을 아무 자료도 안 보고 백지에서 재작성하며 진짜 이해했는지 점검하는 날이다. 학습률 실험을 다시 돌려보고, 틀렸던 문제를 다시 풀면서 어디가 구멍인지 확인한다. 새로운 개념은 없고, 지난 5일치를 스스로 재현할 수 있는지가 오늘의 유일한 목표다.

송민성6분 읽기

오늘의 목표

이번 주(Week 2) 동안 NumPy로 만든 softmax, cross entropy, linear regression 코드를 자료를 하나도 보지 않고 처음부터 다시 써본다. "지난번에 봤을 때는 이해했는데" 상태와 "지금 바로 짤 수 있다" 상태는 완전히 다르다. 오늘은 후자로 넘어가는 날이다.

개념과 직관

오늘은 새 개념 학습이 없다. 대신 이번 주에 쌓은 세 가지 조각을 다시 떠올려보자.

  • 선형회귀(linear regression): 입력 x에 대해 예측값을 y_hat = w * x + b 형태의 직선(또는 평면)으로 만드는 것. 손실(loss)은 예측과 실제값의 차이를 제곱한 평균, 즉 평균제곱오차(MSE, mean squared error)로 잰다.
  • 경사하강(gradient descent): 손실이 줄어드는 방향으로 w, b를 아주 조금씩 옮기는 것. 학습률(learning rate)은 한 걸음의 보폭이다.
  • softmax: 여러 개의 실수 점수(score)를 "확률처럼 보이게" 만드는 함수. 각 값을 지수함수(exp)로 키운 뒤 전체 합으로 나눠서, 다 더하면 1이 되는 양수들로 바꾼다.
  • cross entropy: 모델이 예측한 확률 분포가 정답 분포와 얼마나 다른지 재는 손실. 정답 클래스에 준 확률이 낮을수록 손실이 커진다.

비유로 정리하면: 선형회귀는 "직선 자를 얼마나 기울여야 점들에 잘 맞는지" 찾는 것, 경사하강은 "눈을 감고 산비탈을 한 걸음씩 내려가는 것", softmax는 "여러 후보의 점수를 투표 비율로 바꾸는 것", cross entropy는 "내가 낸 투표 비율이 정답과 얼마나 동떨어졌는지 벌점 매기기"다.

오늘 확인할 건 이 네 가지를 말로 설명할 수 있는가가 아니라 손 코드로 재현할 수 있는가다. 둘은 다르다. 코드를 짤 때 막히는 지점이 바로 이번 주 복습에서 다시 봐야 할 부분이다.

코드로 직접 해보기

자료를 보지 않고 아래 순서대로 직접 짜본다. 막히면 그 부분만 힌트를 살짝 보고, 다시 지우고 처음부터 쓴다. (아래 코드는 "정답 예시"이지, 보고 베끼는 용도가 아니다.)

1단계: softmax

python
import numpy as np def softmax(x): # x: (batch, num_classes) 형태의 2D 배열이라고 가정 x_shifted = x - np.max(x, axis=1, keepdims=True) # 오버플로 방지 exp_x = np.exp(x_shifted) return exp_x / np.sum(exp_x, axis=1, keepdims=True) scores = np.array([[2.0, 1.0, 0.1]]) print(softmax(scores)) # 합이 1이 되는지 확인

2단계: cross entropy

python
def cross_entropy(probs, labels): # probs: (batch, num_classes) softmax 결과 # labels: (batch,) 정답 클래스 인덱스 n = probs.shape[0] correct_probs = probs[np.arange(n), labels] return -np.mean(np.log(correct_probs + 1e-9)) # log(0) 방지용 작은 값 추가 probs = softmax(np.array([[2.0, 1.0, 0.1], [0.1, 2.0, 0.5]])) labels = np.array([0, 1]) print(cross_entropy(probs, labels))

3단계: linear regression + 경사하강

python
def train_linear_regression(x, y, lr=0.01, epochs=100): w, b = 0.0, 0.0 n = len(x) losses = [] for epoch in range(epochs): y_pred = w * x + b loss = np.mean((y_pred - y) ** 2) losses.append(loss) # 손실을 w, b로 편미분한 결과 (직접 유도했던 부분) dw = np.mean(2 * (y_pred - y) * x) db = np.mean(2 * (y_pred - y)) w -= lr * dw b -= lr * db return w, b, losses x = np.array([1, 2, 3, 4, 5], dtype=float) y = np.array([2, 4, 6, 8, 10], dtype=float) w, b, losses = train_linear_regression(x, y, lr=0.01, epochs=200) print(w, b, losses[-1])

셋 다 막힘없이 짤 수 있으면 이번 주 핵심은 잡힌 것이다. 막혔다면 어느 줄에서 멈췄는지 적어두자. (예: "dw 공식이 안 떠올랐다" → 손실 함수 미분 부분을 다시 손으로 유도)

오늘의 실험

재작성한 train_linear_regression 함수로 학습률(learning rate) 실험을 다시 재현한다.

  1. 학습률을 [0.001, 0.01, 0.1, 0.5, 1.0] 처럼 여러 값으로 바꿔가며 같은 데이터에 대해 훈련한다.
  2. 각 학습률마다 losses 리스트를 기록하고, 마지막 손실값과 손실이 얼마나 빨리 줄어드는지를 비교한다.
  3. 학습률이 너무 크면 손실이 줄어들지 않고 발산(값이 점점 커지거나 nan이 나옴)하는지 직접 확인한다.
  4. 결과를 표나 간단한 print로 정리: 학습률 | 최종 손실 | 발산 여부

기록할 때는 "학습률 1.0에서 loss가 nan이 됐다" 같은 구체적 관찰을 남긴다. 이 관찰이 나중에 신경망 학습에서 "loss가 갑자기 폭발했다"를 만났을 때 원인을 짐작하는 데 그대로 쓰인다.

왜 이걸 배우나 (LLM / Post-training 연결)

지금 손으로 짠 softmax와 cross entropy는 언어모델이 다음 단어를 예측할 때 쓰는 손실 함수와 구조적으로 동일하다. 다만 클래스 개수가 3개가 아니라 어휘(vocab) 크기만큼(보통 수만~수십만) 커질 뿐이다. 경사하강과 학습률 실험도 마찬가지로, 나중에 PyTorch로 신경망을 학습시킬 때 "loss가 발산했다", "학습이 너무 느리다" 같은 문제를 진단하는 감각의 기초가 된다. Post-training 실무에서 학습이 이상하게 흘러갈 때 가장 먼저 의심하는 곳 중 하나가 학습률이고, 그 감을 지금 작은 예제로 미리 만들어두는 것이다.

초보자가 막히는 지점

  • softmax에서 `axis=1` 방향을 헷갈림: batch 차원(행)마다 확률 합이 1이 되어야 하는데, 축을 잘못 잡으면 전체 배열 합이 1이 되어버린다. np.sum(probs, axis=1)을 찍어서 전부 1인지 확인하는 습관을 들이자.
  • cross entropy에서 log(0) 에러: 확률이 정확히 0이 되면 np.log(0)-inf가 된다. 아주 작은 값(예: 1e-9)을 더해주는 이유를 잊지 말 것.
  • dw, db 공식을 외우려다 막힘: 공식을 외우지 말고, 손실 함수 (y_pred - y)^2 을 w에 대해 직접 미분해보는 걸 다시 시도한다. 체인룰을 손으로 한 줄씩 적어보면 dw = 2*(y_pred - y)*x 가 왜 나오는지 보인다.
  • 학습률 실험에서 loss가 nan이 나왔을 때 당황함: 이건 버그가 아니라 정상적인 발산 현상이다. 오히려 "왜 발산했는지"를 설명할 수 있으면 이번 주 목표 달성이다.

알고리즘 (병행 1시간)

이번 주에 풀었던 문제 중 틀렸거나 막혔던 문제만 골라 다시 푼다. 새 문제를 풀지 말 것 — 오늘의 목적은 범위를 넓히는 게 아니라 구멍을 메우는 것이다.

풀이 노트에 아래 세 가지를 반드시 기록한다.

  • 접근: 이번엔 어떤 순서로 생각을 풀어나갔는지 (막혔던 지점을 이번엔 어떻게 뚫었는지)
  • 시간복잡도: Big-O로 표현 (예: O(n), O(n log n))
  • 틀린 이유: 처음에 왜 못 풀었는지 — 몰랐던 개념 때문인지, 실수(인덱스 오류 등) 때문인지, 시간 부족 때문인지 구분해서 적는다.

이 기록은 다음 주 이후에도 계속 쌓아가는 것이 좋다. "틀린 이유"가 반복되는 패턴을 찾으면 그게 진짜 취약점이다.

© 2026 Tyler Song