본문 바로가기
TYLER SONGBlog
← Blog index
LLM Research6 min read

월말 시험 (Month 1): NumPy 로지스틱 회귀와 PyTorch MLP를 백지에서 구현한다

이번 시험은 한 달 동안 배운 배열 연산, 분류 손실, 경사하강, 신경망 학습 흐름을 백지에서 재구성하는 시간이다. 제한 시간 4시간 안에 NumPy 모델과 PyTorch 모델을 각각 구현하고, 학습률에 따른 결과 차이를 표로 남긴다. 실행 성공보다 각 코드 줄의 역할을 설명할 수 있는지가 핵심이다.

By 송민성

오늘의 목표

자료를 보지 않고 분류 모델의 학습 과정을 처음부터 끝까지 구현한다. 막힌 부분과 잘못 이해했던 부분을 기록해 Month 2 학습 전에 기초 빈틈을 찾는다.

권장 시간 배분:

  1. NumPy 로지스틱 회귀: 90분
  2. PyTorch MLP: 90분
  3. 학습률 실험과 결과 정리: 60분

개념과 직관

오늘 새로 배우는 개념은 없다. 구현 전에 다음 흐름을 종이에 직접 써 본다.

입력 → 점수 → 확률 → 손실 → 기울기 → 파라미터 수정

1. 로지스틱 회귀

로지스틱 회귀(logistic regression, 입력을 클래스별 점수로 바꾼 뒤 확률을 계산하는 선형 분류 모델)는 다음 계산을 수행한다.

  • 입력 행렬: X
  • 가중치: W
  • 편향: b
  • 클래스별 점수: 점수 = XW + b

그림으로 보면 입력 특징들이 각 클래스 상자에 연결되어 있고, 연결선마다 가중치가 하나씩 붙어 있다. 입력값과 가중치를 곱해 더하면 클래스별 점수가 나온다.

2. 소프트맥스와 교차 엔트로피

소프트맥스(softmax, 여러 점수를 합이 1인 확률로 바꾸는 함수)는 큰 점수에 더 높은 확률을 준다.

클래스 i의 확률 = exp(점수_i) / 모든 exp(점수)의 합

지수 계산이 너무 커지는 것을 막기 위해 각 행의 점수에서 최댓값을 먼저 뺀다.

안정화된 점수 = 점수 - 그 행의 최댓값

교차 엔트로피(cross entropy, 정답 클래스에 낮은 확률을 준 만큼 크게 벌점을 주는 손실)는 다음처럼 계산한다.

샘플 손실 = -log(정답 클래스의 확률)

전체 손실은 샘플 손실의 평균이다.

3. 경사하강

경사하강(gradient descent, 손실을 줄이는 방향으로 파라미터를 조금씩 옮기는 것)은 다음 규칙을 반복한다.

새 파라미터 = 기존 파라미터 - 학습률 × 기울기

학습률(learning rate, 한 번에 파라미터를 얼마나 움직일지 정하는 값)이 너무 작으면 학습이 느리다. 너무 크면 최솟값을 지나치며 손실이 흔들리거나 커질 수 있다.

4. MLP와 학습 반복문

다층 퍼셉트론(MLP, 여러 선형층 사이에 비선형 함수를 넣은 신경망)은 직선 하나로 나누기 어려운 데이터도 표현할 수 있다.

입력 → 선형층 → ReLU → 선형층 → 클래스 점수

ReLU(Rectified Linear Unit, 음수는 0으로 바꾸고 양수는 유지하는 함수)는 ReLU(x) = max(0, x)다.

PyTorch 학습 반복문(training loop, 예측부터 파라미터 수정까지 반복하는 코드)의 순서는 다음과 같다.

  1. 이전 기울기 초기화
  2. 순전파로 예측값 계산
  3. 손실 계산
  4. 역전파로 기울기 계산
  5. 옵티마이저로 파라미터 수정

코드로 직접 해보기

먼저 아래 정답 코드를 보지 않고 백지에서 구현한다. 구현이 끝났거나 제한 시간이 지나면 비교한다.

1. 공통 데이터 만들기

두 모델이 같은 문제를 풀도록 2차원 3클래스 데이터를 만든다.

python
import numpy as np np.random.seed(42) n = 100 X = np.vstack([ np.random.randn(n, 2) + [-2, 0], np.random.randn(n, 2) + [2, 0], np.random.randn(n, 2) + [0, 3], ]) y = np.repeat([0, 1, 2], n) order = np.random.permutation(len(X)) X = X[order] y = y[order]

2. NumPy로 소프트맥스 분류기 구현

외워서 입력하기보다 각 배열의 모양을 먼저 적는다.

  • X: (300, 2)
  • W: (2, 3)
  • b: (3,)
  • 점수와 확률: (300, 3)
python
import numpy as np def softmax(scores): scores = scores - scores.max(axis=1, keepdims=True) exp_scores = np.exp(scores) return exp_scores / exp_scores.sum(axis=1, keepdims=True) def train_logistic_regression(X, y, learning_rate=0.1, epochs=500): sample_count, feature_count = X.shape class_count = len(np.unique(y)) W = np.zeros((feature_count, class_count)) b = np.zeros(class_count) for epoch in range(epochs): scores = X @ W + b probabilities = softmax(scores) loss = -np.log( probabilities[np.arange(sample_count), y] + 1e-12 ).mean() gradient_scores = probabilities.copy() gradient_scores[np.arange(sample_count), y] -= 1 gradient_scores /= sample_count gradient_W = X.T @ gradient_scores gradient_b = gradient_scores.sum(axis=0) W -= learning_rate * gradient_W b -= learning_rate * gradient_b if epoch % 100 == 0: print(f"epoch={epoch:3d}, loss={loss:.4f}") return W, b W, b = train_logistic_regression(X, y) predictions = np.argmax(X @ W + b, axis=1) accuracy = np.mean(predictions == y) print(f"정확도: {accuracy:.3f}")

probabilities - 정답 형태의 기울기가 낯설다면 작은 예를 손으로 계산한다. 정답이 1번 클래스이고 예측 확률이 [0.2, 0.5, 0.3]이라면 점수에 대한 기울기는 [0.2, -0.5, 0.3]이다. 정답 클래스 확률은 높이고 나머지는 낮추는 방향이다.

3. PyTorch로 MLP와 학습 반복문 구현

PyTorch에서는 자동 미분(autograd, 연산 기록을 이용해 기울기를 자동 계산하는 기능)이 기울기 계산을 맡는다. 학습 순서는 직접 작성해야 한다.

python
import torch from torch import nn torch.manual_seed(42) X_tensor = torch.tensor(X, dtype=torch.float32) y_tensor = torch.tensor(y, dtype=torch.long) model = nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 3), ) loss_function = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.1) for epoch in range(500): optimizer.zero_grad() logits = model(X_tensor) loss = loss_function(logits, y_tensor) loss.backward() optimizer.step() if epoch % 100 == 0: print(f"epoch={epoch:3d}, loss={loss.item():.4f}") with torch.no_grad(): predictions = model(X_tensor).argmax(dim=1) accuracy = (predictions == y_tensor).float().mean() print(f"정확도: {accuracy.item():.3f}")

nn.CrossEntropyLoss()에는 소프트맥스를 통과시키지 않은 점수인 로짓(logits, 확률로 바꾸기 전 클래스별 점수)을 넣는다. 이 손실 함수가 내부에서 소프트맥스에 해당하는 계산과 교차 엔트로피를 안정적으로 처리한다.

오늘의 실험

NumPy 모델과 PyTorch 모델에서 학습률만 바꾼다. 초기값, 데이터, 반복 횟수는 유지한다.

시험할 학습률:

  • 0.001
  • 0.01
  • 0.1
  • 1.0

각 실행에서 다음 값을 기록한다.

모델학습률초기 손실최종 손실최종 정확도손실 변화
NumPy 로지스틱 회귀0.001
NumPy 로지스틱 회귀0.01
NumPy 로지스틱 회귀0.1
NumPy 로지스틱 회귀1.0
PyTorch MLP0.001
PyTorch MLP0.01
PyTorch MLP0.1
PyTorch MLP1.0

손실 변화에는 다음처럼 관찰한 현상을 적는다.

  • 꾸준히 감소
  • 거의 변하지 않음
  • 크게 흔들림
  • 숫자가 아님
  • 중간부터 증가

정확도만 비교하지 않는다. 같은 정확도에 도달해도 학습률에 따라 도달 속도와 손실의 안정성이 다를 수 있다.

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

LLM의 다음 토큰 예측도 수많은 후보 토큰에 대한 로짓을 만들고, 정답 토큰과 비교해 손실을 계산한 뒤 경사하강으로 파라미터를 수정한다. LLM 후속 학습(post-training, 이미 학습된 모델을 원하는 행동과 목적에 맞게 추가 학습하는 과정)에서도 모델 규모는 커지지만 예측 → 손실 → 기울기 → 업데이트 → 평가라는 실험 흐름은 그대로 반복된다.

초보자가 막히는 지점

  • NumPy에서 axis=1을 빼먹으면 전체 행렬을 하나의 확률 분포처럼 계산한다. 각 샘플의 클래스 확률 합이 1인지 probabilities.sum(axis=1)로 확인한다.
  • 큰 점수에 바로 np.exp()를 적용하면 overflow가 발생할 수 있다. 행별 최댓값을 먼저 뺀다.
  • np.log(0)은 계산할 수 없다. 로그를 적용하기 전에 작은 값 1e-12를 더한다.
  • PyTorch에서 optimizer.zero_grad()를 빼먹으면 이전 반복의 기울기가 계속 더해진다.
  • CrossEntropyLoss 앞에 softmax를 넣지 않는다. 정답 y_tensor는 클래스 번호를 담은 torch.long 자료형이어야 한다.

알고리즘 (병행 1시간)

총 90분 동안 LeetCode Medium 3문제를 푼다. 문제당 30분을 넘기지 말고, 시간이 끝나면 정답을 확인한 뒤 직접 다시 구현한다.

각 문제마다 다음 세 항목을 기록한다.

  1. 접근: 어떤 자료구조와 풀이 순서를 선택했는가
  2. 시간복잡도: 입력 크기를 n이라 할 때 연산 횟수가 어떻게 증가하는가
  3. 틀린 이유: 조건 누락, 인덱스 오류, 자료구조 선택 실패, 시간 초과 중 무엇이었는가