[학습 일기 #34] 딥러닝 2일차

Ariel_Jeong·2026년 2월 27일

[학습 일기 시리즈]

목록 보기
34/44

0. 오늘 배운 흐름 한 장 요약

딥러닝 학습은 결국 이 루프를 반복하는 것과 같다.




1. MLP(다층 퍼셉트론) 구조 이해

1.1 MLP(Multi-Layer Perceptron)란?

입력층(Input Layer) + 은닉층(Hidden Layer, 1개 이상) + 출력층(Output Layer) 로 이루어진 신경망이다.

  • 단일 퍼셉트론: 선형 문제만 잘 푼다(직선 하나로 분리 가능한 문제)
  • MLP: 은닉층을 쌓고, 그 결과에 비선형 활성화 함수(Activation Function)를 적용하여 비선형성을 부여하기 때문에 비선형 문제(XOR 같은)도 풀 수 있다.

1.2. 각 층의 역할

역할예시(헬스케어)
입력층데이터를 받는 곳나이, 혈압, 혈당…
은닉층특징을 추출/변환/조합패턴 인식, 조합
출력층최종 결과를 내는 곳질병 확률, 분류 결과

1.3 층이 깊어질수록 무엇이 달라지나?

깊어질수록 은닉층은 더 복잡한 특징을 학습한다.

  • 1층: 단순 특징(기본 패턴)
  • 2층: 중간 특징(형태/조합)
  • 3층: 복잡 특징(이상 징후)
  • 출력층: 최종 판단(진단 결과)




2. 순전파(Forward Propagation): 입력에서 출력까지 “값”이 흐르는 과정

2.1 순전파란?

순전파 = 입력값이 레이어를 통과하면서 예측값 y^\hat{y}를 만드는 과정

각 층에서 기본 형태는 동일하다:


2.2 수식으로 보는 2층 MLP 예시

여기서 h1,h2ℎ_1, ℎ_2 는 은닉층 출력(중간 표현) 이라고 생각하면 된다.



3. Pytorch로 MLP 구현하기

3.1. 방법 1

import torch
import torch.nn as nn

class MLP(nn.Module): # nn.Module의 모든 성질을 이어받겠다!
    def __init__(self, input_size, hidden_size, output_size):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size) # fc = fully connected
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, output_size)
        self.relu = nn.ReLU()

    def forward(self, x): # 실제로 모델이 순전파를 할 때 작동하는 함수
        h = self.relu(self.fc1(x)) # -> self.fc1 의 forward함수를 작동 -> weight @ x + bias
        h = self.relu(self.fc2(h))
        out = self.fc3(h)
        return out

model = MLP(input_size=4, hidden_size=16, output_size=2) # 레이어가 생성
X = torch.tensor([1, 2, 3, 4])
pred = model(X) # -> forward가 실행되는 것 model.forward(X)랑 같음

3.2. 방법 2

model = nn.Sequential(
    nn.Linear(4, 16),
    nn.ReLU(),
    nn.Linear(16, 16),
    nn.ReLU(),
    nn.Linear(16, 2)
)




4. 손실함수(Loss Function)

=Error Function = Cost Function

4.1. 손실함수란?

손실함수는 예측 y^\hat{y}와 정답 𝑦𝑦의 차이를 하나의 점수로 만든다.

  • loss가 클수록 더 틀림
  • loss가 작을수록 더 맞음
  • 학습의 목표는 loss를 최소화 하는 것

4.2 회귀(Regression) 손실: MSE / MAE

4.2.1. MSE(Mean Squared Error)

  • 연속값 예측에 사용(예: 혈당 수치)
  • 큰 오차를 제곱으로 더 크게 벌점 → 이상치에 민감
  • 코드
import torch.nn as nn
criterion = nn.MSELoss()
loss = criterion(prediction, target)

4.2.2. MAE(Mean Absolute Error)

  • MSE보다 이상치에 덜 민감

4.3 분류(Classification) 손실: Cross-Entropy / BCE

4.3.1. BCE(이진 분류)

  • 사용: nn.BCEWithLogitsLoss()
    • 주의!: 모델 마지막에 Sigmoid 붙이면 안됨.(이중으로 처리하는 셈)
      만약, nn.BCE()를 사용한다면 Sigmoid 붙여야 함

4.3.2. Cross-Entropy(다중 분류)

  • 정답(one-hot)과 예측 확률분포의 차이를 측정
  • 사용: nn.CrossEntropyLoss()
    • 주의!: 모델 마지막에 Softmax 붙이면 안됨.

cf) 왜 파이토치는 Softmax/Sigmoid를 빼고 Logits를 달라고 할까?

  • logit이란?
    신경망(MLP)의 마지막 출력층에서 활성화 함수(Softmax나 Sigmoid)를 거치기 전에 나오는 순수한 날것의 숫자들

  • 수학적인 정석대로라면 예측 결과에 Softmax를 씌워서 확률로 만든 다음,
    정답(Target)과 비교해서 Cross-Entropy Loss 계산 공식에 넣는 게 맞다.
    하지만 컴퓨터가 이 계산을 순서대로 하면 수치적 불안정성(Numerical Instability) 이라는 치명적인 문제가 발생한다.

  • 문제점: 크로스 엔트로피 수식에는 로그(log\log) 계산이 들어가는데, 만약 모델이 예측을 아주 잘못해서 Softmax 결과값이 0.000000000001처럼 0에 극도로 가까워졌다고 가정해 보자. 여기에 Log를 씌우면 컴퓨터는 처리 한계를 벗어나 마이너스 무한대(-\infty)나 NaN(Not a Number, 계산 불가) 에러를 뱉어버린다.

  • 파이토치의 해결책: 파이토치 개발자들은 "Softmax 계산과 Log 계산을 분리하지 말고, 수학적으로 식을 하나로 합쳐서(LogSoftmax) 한 번에 계산하자!" 라고 결정했다. 수식을 하나로 합쳐서 최적화하면 연산 속도도 훨씬 빠르고, 값이 0에 가까워져도 에러가 나지 않게 안전한 처리가 가능하기 때문이다.




5. 역전파(Backpropagation)

5.1 역전파란?

역전파는 손실 𝐿을 각 가중치 𝑤로 미분해서, Lw\frac{\partial L}{\partial w}를 계산하는 과정이다.
이 값이 “가중치를 조금 바꿨을 때 손실이 얼마나 변하는지”를 알려준다.


5.2. 왜 Chain Rule(연쇄법칙)이 역전파의 핵심인가?

5.2.1. 합성함수 = 함수 안에 함수

신경망은 레이어를 통과하며 함수가 연속해서 적용된다.
f(g(x))f(g(x))
그래서 미분도 합성함수 미분법(연쇄법칙)을 쓴다.

5.2.2. Chain Rule 핵심

  • 최종 결과 변화율 = 중간 단계 변화율들을 “곱”한 것




6. 파라미터(w, b) 업데이트

6.1. 경사하강법(Gradient Descent)

경사하강법 업데이트 식:

  • 𝜂 = 학습률(learning rate), 한 걸음 크기
  • gradient는 손실이 커지는 방향이므로, 마이너스(-) 를 붙여 내려간다.

6.2. 학습률(Learning Rate)의 중요성

  • 너무 작으면

    • 아주 천천히 내려감 → 시간 낭비(수렴이 너무 느림)
  • 적절하면

    • 안정적 수렴(보통 0.001~0.01 구간을 많이 시작점으로 잡음)
  • 너무 크면

    • 최저점을 지나치며 흔들리거나 발산
  • 학습률 선택 팁

    • 시작값 0.001
      → loss가 줄어드는지 관찰
      → 너무 느리면 ↑ / 발산하면 ↓

6.3. 옵티마이저(Optimizer)

역전파가 기울기(Lw\frac{\partial L}{\partial w})를 계산해주면, 옵티마이저는 그걸 이용해 실제로 𝑤를 업데이트한다.

6.3.1. SGD(Stochastic Gradient Descent) : "정직하게 기울기만 보고 걷기"

  • 기본 경사하강:
    현재 위치에서 가장 가파른 방향(기울기)으로 딱 정해진 보폭(Learning Rate)만큼만 이동
  • 단순하고 베이스라인으로 좋음
  • 한계: 방향을 바꿀 때마다 지그재그로 크게 흔들리며, 평탄한 길(기울기가 0에 가까운 곳)을 만나면 학습이 멈춰버리는(Local Minima) 치명적인 단점이 있음

6.3.2 Momentum: "관성을 줘서 평지를 돌파하자 (방향 개선)"

  • 이전 방향을 기억해 관성으로 더 안정적으로 이동
  • 흔들림 감소, 수렴 가속
    지그재그로 흔들리는 현상을 줄여주고, 얕은 웅덩이(Local Minima)를 관성의 힘으로 쑥 빠져나갈 수 있게 해줌

6.3.3 Adam(Adaptive Moment Estimation) : "모멘텀(방향) + RMSprop(보폭)의 끝판왕"

  • 파라미터별로 적응적 학습률을 적용
  • 모멘텀 방식의 '관성(방향)'과 RMSprop의 '적응형 보폭(스텝 크기)' 두 가지 장점을 모두 합친 하이브리드 모델
  • 빠르고 안정적이라 “기본 시작”으로 많이 사용

6.3.4. RMSprop / AdaGrad : "자주 변하는 건 조금만, 안 변하는 건 크게 (보폭 개선)"

  • 개념: 기울기(방향)가 아니라 학습률(보폭)에 집중한 방식
    지금까지 많이 업데이트된 가중치는 보폭을 줄여서 세밀하게 조정하고, 거의 업데이트되지 않은 가중치는 보폭을 크게 해서 확확 변하게 만듦

6.3.5. 코드

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

6.3.6. 한눈에 비교하기

옵티마이저핵심 키워드특징실무 팁
SGD기본기울기 방향으로만 이동성능의 기준점(Baseline)으로 주로 사용
Momentum관성 (방향 유지)이전 이동 방향을 기억해 가속도 부여지그재그 흔들림 완화
RMSprop적응형 보폭상황에 따라 스텝 크기 조절RNN 등 시계열 데이터에서 종종 사용
Adam관성 + 보폭Momentum + RMSprop의 장점 결합현업 표준. 무조건 1순위로 시도할 것




[과제 고찰]

optimizer.zero_grad()(기울기 초기화) 코드의 위치

기울기 초기화 코드는 loss.backward() 이전 or optimizer.step() 이후에만 사용하면 된다.

하지만 가장 안전한건 가장 첫 줄에 위치하는 것이다.

그 이유는? → 가독성 및 안전성 때문

outputs = model(X_batch)
loss = criterion(outputs, y_batch)

# 만약 이런 코드가 중간에 끼어들면?
if loss.item() > 1000:
    continue   # backward 안 하고 넘어감

optimizer.zero_grad()
loss.backward()

공정 비교

루프 밖 seed 1번 고정은 언제 “괜찮다”고 말할 수 있나?

  • 목적이 “실험 전체를 재현 가능하게” 하는 것이라면 → ✅ 충분히 괜찮음
  • 목적이 “각 설정을 동일 초기 조건에서 공정 비교” 라면 → ❌ 추가 조치 필요

왜 그런가?

🎲 RNG를 카드 덱에 비유해보자

torch.manual_seed(42)

카드를 특정 순서로 섞어놓는 것

예를 들어:

덱 = [7, 3, 9, 1, 4, 8, 2, ...]

이 순서는 항상 똑같이 시작한다.

🔁 그런데 루프 안에서 모델을 만들 때마다?

모델 초기화는 “카드를 뽑는 것.”

model = MLP(...) 이 순간:

  • 카드 몇 장을 뽑아서 가중치를 만든다.
    • 예를 들어 첫 모델은 → [7, 3, 9] 사용
    • 그 다음 모델은? → [1, 4, 8] 사용
    • 즉, 같은 덱에서 계속 다음 카드를 뽑고 있는 상태 → 그래서 모델마다 가중치가 다르게 나오는 것임.

❗ 여기서 중요한 차이

🔹 루프 밖에서 seed 한 번만 설정

덱을 한 번만 섞음
→ 모델1: 앞 카드 사용
→ 모델2: 그 다음 카드 사용
→ 모델3: 그 다음 카드 사용

👉 실행을 다시 하면 같은 결과는 나오지만

👉 모델1, 모델2, 모델3는 서로 다른 초기값

🔹 매 실험마다 seed 다시 설정

for ...
    torch.manual_seed(42)
    model = MLP(...)

이건 모델 만들기 전에 덱을 다시 처음 상태로 돌린다는 것. 따라서?

모델1 = [7,3,9]
모델2 = [7,3,9]
모델3 = [7,3,9]

👉 완전히 같은 초기값

🔥 핵심 요약

목적seed 위치
실행을 다시 해도 같은 결과맨 위에 한 번
모든 실험이 같은 초기값루프 안에서 매번

추가 팁 (보충 팩트)

실제로 완벽한 재현을 위해서는 torch.manual_seednp.random.seed 외에도

다음과 같은 설정이 추가로 필요할 수 있다:

  • random.seed(seed) (파이썬 기본 난수 라이브러리)
  • torch.cuda.manual_seed_all(seed) (GPU 사용 시)
  • torch.backends.cudnn.deterministic = True (연산 결과의 결정론적 보장)
profile
R&D 분야의 경험을 토대로 커리어 확장에 도전중인 개발꿈나무입니다.

0개의 댓글