1. 파이토치로 이해하는 논리 회귀: 선형식이 확률이 되는 과정
분류 문제를 처음 배울 때 가장 먼저 마주치는 모델 중 하나가 논리 회귀다. 이름에 회귀가 들어가지만 실제로는 값을 예측하는 회귀보다는, 어떤 입력이 특정 클래스에 속할 확률을 예측하는 분류 모델에 가깝다. 구조는 단순하지만, 이 안에는 선형 모델, 확률 해석, 손실 함수, 경사하강법, 그리고 다중 클래스 확장까지 머신러닝의 핵심 문법이 압축되어 있다.
특히 파이토치로 논리 회귀를 구현해 보면, 모델이 결국 무엇을 배우는지 훨씬 선명하게 보인다. 입력 텐서를 선형 변환한 뒤, 그것을 확률로 바꾸고, 정답과의 차이를 손실 함수로 계산한 다음, 역전파를 통해 가중치를 갱신하는 흐름이 그대로 드러나기 때문이다. 이 과정은 이후 신경망을 공부할 때도 거의 같은 형태로 반복된다.
이 글에서는 가장 기본적인 이진 분류부터 시작해, 여러 클래스 중 하나를 맞히는 다중 분류까지 자연스럽게 이어지는 구조로 논리 회귀를 설명한다. 단순히 코드를 읽는 방식이 아니라, 왜 시그모이드를 쓰는지, 왜 BCE와 CrossEntropyLoss가 서로 다른지, 텐서 shape는 왜 그렇게 맞춰야 하는지까지 함께 짚어보겠다.
2. 논리 회귀가 필요한 이유
선형 회귀로는 분류를 다루기 어렵다
어떤 입력 $x$가 주어졌을 때 결과가 0 또는 1로 나뉘는 문제를 생각해 보자. 예를 들어 공부 시간이 주어졌을 때 시험 합격 여부를 예측하는 문제는 전형적인 이진 분류 문제다. 이때 선형 회귀처럼
[
z = wx + b
]
형태의 출력을 그대로 쓰면 값의 범위가 $(-\infty, \infty)$가 되어 버린다. 그런데 확률은 반드시 $0$과 $1$ 사이에 있어야 한다. 따라서 선형식의 결과를 확률처럼 해석할 수 있는 형태로 바꾸는 과정이 필요하다.
논리 회귀는 바로 이 지점에서 등장한다. 먼저 선형식으로 입력의 점수를 계산하고, 그 점수를 시그모이드 함수에 통과시켜 확률로 해석한다.
확률 예측으로 바꾸는 핵심 함수: 시그모이드
시그모이드 함수는 다음과 같다.
[
\sigma(z) = \frac{1}{1 + e^{-z}}
]
여기서 $z$는 선형식의 출력이고, $\sigma(z)$는 0과 1 사이의 값이다.
이 함수의 의미는 단순하다.
- $z$가 매우 크면 예측 확률은 1에 가까워진다.
- $z$가 매우 작으면 예측 확률은 0에 가까워진다.
- $z = 0$이면 확률은 0.5가 된다.
즉, 선형 모델이 만든 점수를 확률로 바꾸는 문이다.
이 구조 덕분에 논리 회귀는 "이 샘플이 클래스 1일 확률"을 직접 예측하는 모델이 된다.
3. 단항 논리 회귀: 입력 하나로 이진 분류하기
가장 단순한 형태의 데이터
가장 먼저 다루는 형태는 입력 변수가 하나인 단항 논리 회귀다.
학습 데이터는 대략 다음과 같은 구조를 가진다.
- 입력 $x$: $[[0], [1], [3], [5], [8], [11], [15], [20]]$
- 정답 $y$: $[[0], [0], [0], [0], [1], [1], [1], [1]]$
여기서 입력 텐서의 shape는 $(8, 1)$이고, 정답 텐서의 shape도 $(8, 1)$이다.
이 shape는 꽤 중요하다. 샘플이 8개이고, 각 샘플이 특성 1개를 가진다는 뜻이기 때문이다.
초보자가 자주 놓치는 부분이 바로 여기다. 파이토치의 선형층인 nn.Linear(1, 1)은 입력의 마지막 차원을 특성 수로 해석하므로, 입력이 단순히 $(8,)$ 형태가 아니라 $(8, 1)$이어야 의도한 대로 동작한다.
모델 구조
이진 분류에서는 가장 단순하게 다음 구조를 쓴다.
model = nn.Sequential(
nn.Linear(1, 1),
nn.Sigmoid()
)
이 모델은 사실상 아래 식을 그대로 구현한 것이다.
[
z = wx + b
]
[
\hat{y} = \sigma(z)
]
여기서
- $w$는 가중치
- $b$는 바이어스
- $\hat{y}$는 클래스 1일 확률이다
nn.Linear(1, 1)은 입력 특성 1개를 받아 출력 1개를 만든다.
즉, 단 하나의 선형 결정 경계를 학습하는 셈이다.
그리고 nn.Sigmoid()는 그 출력을 확률처럼 해석 가능한 값으로 바꾼다.
왜 선형층 뒤에 시그모이드를 붙이는가
이 구조는 "점수 계산"과 "확률 변환"을 분리해서 본다는 점에서 중요하다.
- nn.Linear는 입력으로부터 클래스 1 쪽으로 얼마나 기울어져 있는지를 계산한다.
- nn.Sigmoid는 그 점수를 확률로 바꾼다.
이 분리가 익숙해지면, 이후 다층 신경망도 본질적으로는 여러 번의 선형 변환과 비선형 변환의 조합이라는 점이 자연스럽게 이해된다.
함께 해볼 만한 확장 실험
이 단계에서는 입력 값을 정규화했을 때 학습 속도가 어떻게 달라지는지 확인해 보면 좋다.
예를 들어 $x$를 0에서 1 사이로 스케일링하면, 가중치의 크기와 손실 감소 속도가 어떻게 변하는지 비교할 수 있다. 논리 회귀는 단순한 모델이라 이런 차이가 더 직관적으로 보인다.
4. 이진 분류에서의 손실 함수: BCE의 의미
정답과 예측 확률의 차이를 어떻게 측정할까
이진 분류에서는 단순 평균제곱오차보다 Binary Cross Entropy가 훨씬 더 자연스럽다.
예측값이 확률이고, 정답이 0 또는 1이기 때문이다.
BCE는 다음과 같이 쓸 수 있다.
[
\mathcal{L} = -\frac{1}{N}\sum_{i=1}^{N} \left( y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i) \right)
]
여기서
- $N$은 샘플 수
- $y_i$는 실제 레이블
- $\hat{y}_i$는 예측 확률이다
이 식의 의미는 명확하다.
- 정답이 1인데 $\hat{y}$가 1에 가까우면 손실이 작다
- 정답이 0인데 $\hat{y}$가 0에 가까우면 손실이 작다
- 반대로 확신 있게 틀릴수록 손실이 크게 증가한다
즉, 단순히 맞았는지 틀렸는지만 보는 것이 아니라, 얼마나 자신 있게 맞추거나 틀렸는지를 반영하는 손실 함수다.
파이토치에서의 구현
이 구조에서는 이미 모델 마지막에 시그모이드가 포함되어 있으므로, 손실 함수는 nn.BCELoss()를 그대로 사용할 수 있다.
loss = nn.BCELoss()(y_pred, y_train)
이때 두 텐서의 shape가 같아야 한다.
현재는 둘 다 $(8, 1)$이므로 문제없이 계산된다.
BCEWithLogitsLoss를 쓰지 않은 이유도 이해해야 한다
실전에서는 종종 nn.BCELoss() 대신 nn.BCEWithLogitsLoss()를 더 많이 쓴다. 이유는 수치적으로 더 안정적이기 때문이다. 이 손실 함수는 내부적으로 시그모이드와 BCE를 함께 처리한다.
즉,
- 현재 구조: 선형층 → 시그모이드 → BCELoss
- 실전에서 자주 쓰는 구조: 선형층 → BCEWithLogitsLoss
두 방식은 개념적으로 비슷하지만, 후자는 큰 값이나 작은 값에서 발생할 수 있는 수치적 불안정을 줄여 준다. 다만 논리 회귀의 원리를 학습하는 단계에서는 시그모이드를 명시적으로 드러내는 현재 구조가 훨씬 이해하기 쉽다.
함께 해볼 만한 확장 실험
모델의 마지막 시그모이드를 제거하고 nn.BCEWithLogitsLoss()로 바꿔 보자.
예측값이 이제 확률이 아니라 로짓이 된다는 점, 추론 시에는 직접 시그모이드를 다시 적용해야 한다는 점을 비교해 보면 손실 함수와 출력의 관계가 더 분명해진다.
5. 학습 루프가 실제로 하는 일
경사하강법으로 파라미터를 업데이트하는 흐름
이진 분류 부분의 학습 루프는 전형적인 파이토치 학습 구조를 따른다.
for epoch in range(epochs + 1):
y_pred = model(x_train)
loss = nn.BCELoss()(y_pred, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
겉보기에는 짧지만, 안에서는 중요한 단계가 모두 일어난다.
1) 순전파
[
x \rightarrow z = wx+b \rightarrow \hat{y} = \sigma(z)
]
입력으로부터 예측 확률을 계산하는 단계다.
2) 손실 계산
[
\hat{y}, y \rightarrow \mathcal{L}
]
예측 확률과 정답 사이의 차이를 BCE로 계산한다.
3) 기울기 초기화
optimizer.zero_grad()는 이전 step에서 누적된 gradient를 비우는 역할을 한다.
파이토치는 기본적으로 gradient를 누적하기 때문에, 이 코드를 생략하면 이전 epoch의 gradient가 계속 더해져 의도하지 않은 업데이트가 발생한다.
이 부분은 초보자가 가장 자주 놓치는 디테일 중 하나다.
학습 루프에서 zero_grad는 거의 필수라고 봐도 된다.
4) 역전파
loss.backward()는 손실을 기준으로 모든 학습 가능한 파라미터에 대한 미분값을 계산한다.
즉, 현재 손실을 줄이기 위해 가중치와 바이어스를 어느 방향으로 얼마나 움직여야 하는지를 구한다.
5) 파라미터 갱신
optimizer.step()은 계산된 gradient를 바탕으로 실제 파라미터를 갱신한다.
여기서는 SGD를 사용하고 있으므로, 업데이트는 개념적으로 다음과 같다.
[
\theta \leftarrow \theta - \eta \nabla_{\theta}\mathcal{L}
]
여기서
- $\theta$는 모델 파라미터
- $\eta$는 학습률
- $\nabla_{\theta}\mathcal{L}$는 손실의 기울기다
학습률이 중요한 이유
학습률이 너무 크면 손실이 진동하거나 발산할 수 있고, 너무 작으면 학습이 지나치게 느려진다.
현재처럼 작은 모델에서는 0.01 정도의 SGD도 무난하지만, 입력 스케일이 크게 달라지거나 데이터가 복잡해지면 적절한 학습률 선택이 성능에 직접적인 영향을 준다.
함께 해볼 만한 확장 실험
같은 구조에서 옵티마이저를 SGD와 Adam으로 바꿔 비교해 보면 좋다.
손실 감소 속도와 최종 결정 경계가 어떻게 달라지는지 확인하면, 옵티마이저가 단순한 부품이 아니라 학습 동역학을 바꾸는 요소라는 점이 잘 드러난다.
6. 추론 단계: 확률에서 클래스 레이블로
학습이 끝난 뒤에는 새로운 입력에 대해 예측을 수행한다.
예를 들어 입력이 8일 때 모델은 하나의 확률 값을 출력한다.
[
\hat{y} = P(y=1 \mid x=8)
]
이 확률을 실제 클래스 레이블로 바꾸기 위해 보통 임계값 0.5를 사용한다.
[
\hat{c} = \begin{cases} 1 & \text{if } \hat{y} \ge 0.5 \\ 0 & \text{otherwise} \end{cases}
]
파이토치에서는 이를 다음처럼 처리할 수 있다.
y_bool = (y_pred >= 0.5).float()
여기서 float으로 바꾸는 이유는 결과를 텐서 연산에 자연스럽게 연결하기 위해서다.
정수형으로 바꿔도 되지만, 후속 계산이나 비교를 고려하면 float 텐서로 유지하는 경우가 많다.
추론에서 no_grad를 쓰는 이유
간단한 예제에서는 생략되기도 하지만, 추론만 할 때는 보통 다음처럼 감싼다.
with torch.no_grad():
y_pred = model(x_test)
이유는 두 가지다.
- gradient 계산이 필요 없으므로 메모리를 절약할 수 있다
- 불필요한 연산 그래프 생성을 막아 속도를 조금 더 아낄 수 있다
학습과 추론의 가장 큰 차이 중 하나가 바로 이 부분이다.
학습은 미분이 필요하지만, 추론은 보통 필요하지 않다.
7. 다항 논리 회귀: 클래스가 셋 이상이면 무엇이 달라질까
이진 분류는 클래스가 두 개일 때만 통한다.
그렇다면 클래스가 3개 이상이면 어떻게 해야 할까. 이때 사용하는 것이 다항 논리 회귀, 즉 다중 클래스 분류를 위한 논리 회귀다.
예제 데이터는 각 샘플이 4개의 특성을 가지고 있고, 정답은 0, 1, 2 중 하나다.
- 입력 shape: $(10, 4)$
- 정답 shape: $(10,)$
이 shape 차이는 매우 중요하다.
입력은 왜 $(10, 4)$인가
샘플이 10개이고, 샘플 하나당 특성이 4개이기 때문이다.
따라서 선형층은 nn.Linear(4, 3) 형태가 된다.
- 입력 차원 4개
- 출력 차원 3개
즉, 각 샘플에 대해 클래스 3개에 대한 점수를 한 번에 계산한다.
정답은 왜 LongTensor이고 shape가 $(10,)$인가
다중 분류에서 CrossEntropyLoss는 정답을 원-핫 벡터로 받지 않는다.
정답 클래스의 인덱스를 정수로 받는다.
예를 들어 정답이 클래스 2라면, 정답 텐서에는 단순히 숫자 2가 들어간다.
그래서 dtype도 정수형인 LongTensor여야 한다.
이 부분은 초보자가 특히 자주 헷갈린다.
- 이진 BCE: 정답이 보통 float
- 다중 CrossEntropy: 정답이 class index이므로 long
모델 구조
다중 분류에서는 다음처럼 선형층 하나만 둔다.
model = nn.Sequential(
nn.Linear(4, 3)
)
이 구조에서 출력은 확률이 아니라 로짓이다.
즉, 클래스별 점수 벡터다.
[
\mathbf{z} = W\mathbf{x} + \mathbf{b}
]
여기서
- $\mathbf{x} \in \mathbb{R}^4$
- $W \in \mathbb{R}^{3 \times 4}$
- $\mathbf{b} \in \mathbb{R}^3$
- $\mathbf{z} \in \mathbb{R}^3$
각 샘플마다 클래스 3개에 대한 점수 3개가 출력된다.
8. Softmax와 Cross Entropy가 함께 작동하는 방식
클래스별 점수를 확률로 바꾸는 Softmax
다중 분류에서는 시그모이드 대신 Softmax를 사용한다.
Softmax는 여러 클래스의 점수를 전체 합이 1인 확률 분포로 바꾼다.
[
P(y=k \mid \mathbf{x}) = \frac{e^{z_k}}{\sum_{j=1}^{C} e^{z_j}}
]
여기서
- $C$는 클래스 수
- $z_k$는 클래스 $k$의 로짓이다
이 식을 보면 각 클래스의 점수가 독립적으로 확률이 되는 것이 아니라, 모든 클래스와 경쟁하면서 확률이 정해진다는 점이 중요하다.
그래서 다중 분류는 "어느 클래스가 가장 그럴듯한가"를 비교하는 구조가 된다.
그런데 모델에 Softmax가 직접 들어가 있지 않다
여기서 중요한 포인트가 하나 있다.
모델은 선형층만 가지고 있고, 마지막에 Softmax가 없다. 그 이유는 nn.CrossEntropyLoss()가 내부적으로 LogSoftmax와 NLLLoss를 함께 처리하기 때문이다.
즉, 현재 구조는 다음과 같다.
- 모델 출력: 로짓
- 손실 함수 내부: Softmax 성격의 정규화 + 로그 손실 계산
그래서 학습할 때는 굳이 Softmax를 따로 붙이지 않는다.
오히려 학습 단계에서 중복으로 Softmax를 넣으면 비효율적이거나 수치적으로 불리해질 수 있다.
CrossEntropyLoss의 의미
다중 분류에서의 손실은 다음과 같은 형태로 볼 수 있다.
[
\mathcal{L} = -\frac{1}{N}\sum_{i=1}^{N} \log P(y_i \mid \mathbf{x}_i)
]
즉, 정답 클래스에 부여한 확률이 높을수록 손실이 작아진다.
반대로 정답 클래스의 확률이 낮으면 손실이 커진다.
이 손실은 본질적으로 "정답 클래스의 로그 확률을 최대화하라"는 뜻과 같다.
함께 해볼 만한 확장 실험
현재는 클래스가 3개지만, 출력 차원을 4나 5로 늘리고 가짜 클래스를 추가해 보면 손실과 확률 분포가 어떻게 달라지는지 관찰할 수 있다.
클래스 수가 늘어나면 Softmax가 경쟁해야 할 대상도 늘어나므로, 확률 해석이 어떻게 달라지는지 감각적으로 이해하는 데 도움이 된다.
9. 입력 gradient를 확인하는 이유
다중 분류 부분에서는 입력 텐서에 대해 requires_grad_(True)를 설정하고, 손실 backward 이후 X_train.grad를 확인한다.
실무에서는 보통 입력 gradient를 직접 쓰지 않는 경우가 많지만, 학습의 역전파가 어디까지 전달되는지를 이해하는 데는 매우 좋은 확인 방법이다.
역전파가 실행되면 gradient는 가중치뿐 아니라, 미분 가능한 모든 텐서에 대해 계산될 수 있다.
입력에 gradient를 달아 두면 다음을 확인할 수 있다.
- 현재 손실이 입력의 어느 방향 변화에 민감한가
- 연산 그래프가 정상적으로 연결되어 있는가
- 역전파가 실제로 어떻게 흐르는가
이는 이후 saliency map, adversarial example, input attribution 같은 주제를 이해할 때도 중요한 감각이 된다.
detach가 필요한 순간
입력 gradient나 중간 텐서를 추적하다 보면 연산 그래프를 끊고 싶을 때가 있다.
이럴 때 사용하는 것이 detach()다.
예를 들어 어떤 텐서를 후처리나 시각화만 하고 싶다면, 그래프에서 분리해서 다루는 편이 안전하다.
학습 그래프를 의도치 않게 유지하면 메모리가 계속 남거나 backward 경로가 꼬일 수 있기 때문이다.
10. 학습과 추론의 차이: 로짓, 확률, 예측 클래스
다중 분류에서 테스트 입력 하나를 넣으면 모델은 3개의 값을 출력한다.
이 값은 확률이 아니라 로짓이다.
예를 들어
[
\mathbf{z} = [z_0, z_1, z_2]
]
처럼 나온다고 하자.
이 상태에서는 단순히 값이 큰 클래스가 더 유력하다는 것만 알 수 있다.
확률로 보고 싶다면 Softmax를 적용해야 한다.
[
\mathbf{p} = \text{softmax}(\mathbf{z})
]
파이토치에서는 보통 다음처럼 쓴다.
y_prob = nn.Softmax(dim=1)(y_pred)
여기서 dim=1인 이유는 배치 차원을 제외한 클래스 차원에 대해 정규화해야 하기 때문이다.
입력 하나의 shape가 $(1, 3)$이라면
- dim=0은 배치 축
- dim=1은 클래스 축
이므로 클래스별 확률의 합이 1이 되도록 하려면 dim=1을 써야 한다.
이 역시 shape를 이해하지 못하면 자주 틀리는 부분이다.
그다음 실제 예측 클래스는 가장 큰 확률을 가진 인덱스를 고르면 된다.
[
\hat{c} = \arg\max_k p_k
]
즉, 다중 분류의 추론은
- 로짓 계산
- 필요하면 Softmax로 확률화
- argmax로 최종 클래스 선택
이라는 순서로 이해하면 된다.
11. 이 구현에서 특히 중요한 디테일들
nn.Linear의 파라미터 shape
단항 논리 회귀의 nn.Linear(1, 1)은 가중치 shape가 $(1, 1)$, 바이어스 shape가 $(1,)$이다.
다항 논리 회귀의 nn.Linear(4, 3)은 가중치 shape가 $(3, 4)$, 바이어스 shape가 $(3,)$이다.
즉, 출력 차원 수만큼 하나의 선형 분류기가 생긴다고 생각하면 이해가 쉽다.
다중 분류에서는 클래스마다 하나의 점수 함수를 동시에 학습하는 셈이다.
dtype이 다른 이유
- 입력은 실수 연산이 필요하므로 FloatTensor
- BCE의 정답도 확률 계산과 맞물리므로 FloatTensor
- CrossEntropyLoss의 정답은 클래스 인덱스이므로 LongTensor
이 차이를 이해하면 손실 함수에서 발생하는 dtype 오류를 훨씬 쉽게 해결할 수 있다.
왜 모델이 이렇게 단순한가
논리 회귀는 의도적으로 은닉층이 없는 구조를 사용한다.
복잡한 비선형 패턴을 표현하는 데는 한계가 있지만, 그만큼 각 파라미터의 역할과 손실 함수의 의미가 직접적으로 드러난다.
즉, 이 모델은 성능보다 원리를 배우기에 좋은 구조다.
시각화가 주는 의미
단항 예제에서 산점도를 그려 보면, 입력이 커질수록 클래스 1에 가까워지는 패턴이 보인다.
논리 회귀는 이런 흐름을 직선 기반의 점수 함수로 잡아내고, 그것을 시그모이드로 휘어서 확률 곡선으로 만드는 모델이라고 볼 수 있다.
이 그림을 머릿속에 그릴 수 있으면, 논리 회귀는 단순한 함수 호출이 아니라 "선형 경계 위에 확률을 입히는 모델"로 이해된다.
12. 마무리: 논리 회귀는 가장 작은 분류기이자 가장 좋은 출발점이다
논리 회귀는 겉보기에는 단순하지만, 분류 모델의 핵심 구조를 가장 압축적으로 보여 준다.
입력을 선형 변환하고, 그 출력을 확률로 해석하며, 손실 함수를 통해 오차를 수치화하고, 역전파와 최적화를 통해 파라미터를 갱신한다는 흐름이 모두 이 안에 들어 있다.
이진 분류에서는 시그모이드와 BCE가 결합되고, 다중 분류에서는 로짓과 CrossEntropyLoss가 결합된다.
겉으로는 함수 몇 개 차이처럼 보여도, 실제로는 출력의 의미, 정답 텐서의 형식, 확률 해석 방식이 모두 달라진다.
바로 그 차이를 정확히 이해하는 것이 이후 신경망 분류 모델을 공부할 때 중요한 발판이 된다.
더 깊게 들어가면 논리 회귀는 단순한 입문용 모델이 아니라, 확률적 분류, 로그우도 최대화, 선형 결정 경계, 다중 클래스 확장 같은 주제를 연결하는 기본 문법이기도 하다.
그래서 파이토치로 직접 구현해 보는 경험은 단순히 한 모델을 익히는 수준을 넘어, 딥러닝 모델 전반의 학습 구조를 이해하는 첫 단계가 된다.
'AI 공부' 카테고리의 다른 글
| GPT와 함께 배우는 CNN (1) | 2026.03.24 |
|---|---|
| GPT와 함께 공부하는 딥러닝(MLP) (0) | 2026.03.18 |
| 파이토치로 구현하는 선형회귀 (0) | 2026.03.18 |
| GPT와 함께 정리한 ACER(Actor-Critic with Experience Replay) (1) | 2026.03.18 |
| GPT와 함께하는 클러스터링 정리 (1) | 2026.03.17 |