수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

파이토치로 시작하는 딥러닝 - 텐서, 선형 회귀, 논리 회귀 실습

텐서부터 회귀·분류까지 한 번에

PyTorch는 파이썬 기반 오픈소스 딥러닝 프레임워크로, 직관적인 코드로 모델을 만들고 학습할 수 있게 해 준다. 동적 계산 그래프 덕분에 디버깅이 쉽고, GPU 가속과 자동 미분으로 대규모 모델도 다룰 수 있다. 이번 포스트에서는 텐서 기초, GPU 사용, 차원 조작, 자동 미분을 짚은 뒤, 선형 회귀(단항·다중)와 논리 회귀(이진·다중 분류)를 파이토치로 직접 구현해 본다.

(파이토치 프레임워크, 파이토치로 구현한 선형 회귀, 파이토치로 구현한 논리 회귀를 기반으로 재구성)

pip install torch

1. 파이토치와 동적 계산 그래프

PyTorch는 실행 시점(runtime) 에 계산 그래프를 실시간으로 만들고 수정하는 동적 계산 그래프 방식을 쓴다. 조건문·반복문 같은 복잡한 구조를 유연하게 넣을 수 있고, 역전파로 미분을 구해 가중치를 갱신한다. 그래서 디버깅이 쉽고 연구·개발 속도가 빠르다.

1-1. 스칼라

스칼라는 숫자 하나를 담는 자료형이다. 파이토치에서는 0차원 텐서로 표현한다.

import torch

var1 = torch.tensor(5)
print(var1)
print(var1.shape)   # torch.Size([]) → 0차원 텐서

var2 = torch.tensor([10])  # 1차원 텐서(벡터), 스칼라 아님
print(var2.shape)

var3 = torch.tensor(3)
result = var1 + var3
print(result)
print(result.item())   # 텐서 값을 파이썬 숫자로 추출

1-2. 벡터(Vector)

벡터는 원소가 일렬로 나열된 1차원 텐서다. shape(n,) 형태다.

var1 = torch.tensor([1.0, 2.0, 3.0])
print(var1)
print(var1.shape)   # torch.Size([3])

var2 = var1 + 10    # 원소별 덧셈
var3 = var1 * 2     # 원소별 곱셈
print(var2)
print(var3)

var4 = torch.tensor([4.0, 5.0, 6.0])
result = var1 + var4   # 같은 위치 원소끼리 덧셈
print(result)

1-3. 행렬(Matrix)

행렬은 2차원 텐서로, shape(m, n)이다. 행렬 곱은 torch.mm 또는 @로 한다.

var1 = torch.tensor([[1, 2],
                     [3, 4]])
var2 = torch.tensor([[5, 6],
                     [7, 8]])
print(var1)
print(var1.shape)   # torch.Size([2, 2])

result1 = var1 + var2      # 원소별 덧셈
result2 = var1 * var2      # 원소별 곱셈
print(result1)
print(result2)

result3 = torch.mm(var1, var2)   # 행렬 곱
result4 = var1 @ var2            # 행렬 곱 (동일)
print(result3)
print(result4)

1-4. 다차원 텐서

0차원(스칼라), 1차원(벡터), 2차원(행렬)을 넘어 3차원 이상을 다차원 텐서라고 부른다. 이미지, 음성, 시계열 등 여러 축이 필요한 데이터를 표현할 때 쓴다.

var1 = torch.tensor([
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]],
    [[9, 10], [11, 12]]
])
print(var1)
print(var1.shape)   # torch.Size([3, 2, 2])

2. 텐서 다루기

텐서는 딥러닝에서 쓰는 다차원 배열로, NumPy 배열과 비슷하지만 GPU 연산과 자동 미분(autograd) 이 가능하다.

2-1. 생성과 NumPy 변환

data = [[1, 2], [3, 4]]
t1 = torch.tensor(data)
print(t1)

t1 = torch.tensor([5])
t2 = torch.tensor([7])
ndarr1 = (t1 + t2).numpy()
print(ndarr1, type(ndarr1))

result = ndarr1 * 10
t3 = torch.from_numpy(result)
print(t3, type(t3))

2-2. 인덱싱

t1 = torch.tensor([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
])
print(t1[0])         # 첫 번째 행 → [1, 2, 3, 4]
print(t1[:, 0])      # 첫 번째 열 → [1, 5, 9]
print(t1[:, -1])     # 마지막 열 → [4, 8, 12]
print(t1[..., -1])  # 마지막 축의 마지막 원소 (다차원에서 유용)
t1 = torch.tensor([[[1, 2, 3], [4, 5, 6]],
                   [[7, 8, 9], [10, 11, 12]]])
print(t1.shape)
print(t1[..., -1])  # 앞 차원 유지, 마지막 축만 마지막 원소 선택

2-3. GPU 사용

GPU는 대량의 행렬·벡터 연산을 병렬로 처리해 딥러닝 학습을 가속한다. Google Colab에서는 런타임 → 런타임 유형 변경에서 GPU를 선택할 수 있다.

data = [[1, 2], [3, 4]]
t1 = torch.tensor(data)
print(t1.is_cuda)   # GPU에 있는지 여부

t1 = t1.cuda()      # GPU로 이동
print(t1.is_cuda)

t1 = t1.cpu()       # CPU로 이동
print(t1.is_cuda)

서로 다른 장치에 있는 텐서끼리는 연산할 수 없다. 한쪽을 .cpu() 등으로 맞춰 주어야 한다.

t1 = torch.tensor([[1, 1], [2, 2]]).cuda()
t2 = torch.tensor([[5, 6], [7, 8]])
# torch.matmul(t1, t2)  # 오류: 장치 불일치
print(torch.matmul(t1.cpu(), t2))
print(f"Device: {t1.device}")

3. 텐서 연산과 함수

t1 = torch.tensor([[1, 2], [3, 4]])
t2 = torch.tensor([[5, 6], [7, 8]])
print(t1 + t2)
print(t1 - t2)
print(t1 * t2)
print(t1 / t2)

print(t1.matmul(t2))
print(torch.matmul(t1, t2))
t1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8]])
print(t1.mean())        # 전체 평균
print(t1.mean(dim=0))   # 열 방향 평균
print(t1.mean(dim=1))   # 행 방향 평균

print(t1.sum())
print(t1.sum(dim=0))
print(t1.sum(dim=1))

print(t1.argmax())      # 최댓값 인덱스 (전체)
print(t1.argmax(dim=0))
print(t1.argmax(dim=1))

※ dim 참고

  • dim=0: 행 방향으로 축소 → 열 수 유지
  • dim=1: 열 방향으로 축소 → 행 수 유지

4. 텐서 차원 조작

t1 = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
result = torch.cat([t1, t1, t1], dim=0)   # 행 방향 이어 붙이기
print(result)
result = torch.cat([t1, t1, t1], dim=1)   # 열 방향 이어 붙이기
print(result)
t1 = torch.tensor([2], dtype=torch.int)
t2 = torch.tensor([5.0])
print(t1.dtype, t2.dtype)
print(t1 + t2)                      # t1이 float로 자동 변환
print(t1 + t2.type(torch.int32))   # t2를 int로 변환 후 연산

view, clone, permute

view()는 같은 데이터를 다른 shape로 보여 준다. 원본을 바꾸면 view 결과도 바뀐다. 복사가 필요하면 clone()view() 한다.

t1 = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8])
t2 = t1.view(4, 2)
print(t2)
t1[0] = 7
print(t1)
print(t2)   # t2도 변경됨

t3 = t1.clone().view(4, 2)
t1[0] = 9
print(t3)   # t3는 그대로
t1 = torch.rand((64, 32, 3))
t2 = t1.permute(2, 1, 0)   # 축 순서 변경 → (3, 32, 64)
print(t2.shape)

unsqueeze / squeeze

unsqueeze(dim): 해당 위치에 크기 1인 차원 추가. squeeze(): 크기 1인 차원을 모두 제거.

t1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8]])
print(t1.shape)
t1 = t1.unsqueeze(0)
print(t1.shape)
t1 = t1.unsqueeze(3)
print(t1.shape)
t1 = t1.squeeze()
print(t1.shape)

5. 자동 미분과 기울기

requires_grad=True로 두면 해당 텐서가 참여한 연산이 기록되고, backward()로 기울기를 계산할 수 있다.

x = torch.tensor([3.0, 4.0], requires_grad=True)
y = torch.tensor([1.0, 2.0], requires_grad=True)

z = x + y
out = z.mean()   # 스칼라여야 미분 가능

out.backward()
print('x.grad:', x.grad)   # [0.5, 0.5]
print('y.grad:', y.grad)

※ 체인 룰 관점

  • out = (z[0] + z[1]) / 2 이므로 out이 z에 주는 기울기는 [0.5, 0.5]
  • z = x + y 이므로 z가 x, y에 주는 기울기는 1
  • 체인 룰: [0.5, 0.5] * 1 = [0.5, 0.5]

즉, 평균은 “합/개수”이므로 각 원소가 out에 기여하는 비율이 1/n이 되어 기울기가 0.5가 된다.


6. 선형 회귀 분석

선형 회귀는 입력(독립 변수)과 출력(종속 변수)의 관계를 직선(또는 평면) 으로 나타내고, 새 입력에 대한 출력을 예측하는 기법이다. 식은 Y = WX + b 형태이며, 비용 함수(예: MSE)를 최소화하는 경사 하강법 등으로 W, b를 학습한다.

6-1. 단항 선형 회귀

입력 하나(X), 출력 하나(Y)인 경우다.

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(2026)

x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])
print(x_train, x_train.shape)
print(y_train, y_train.shape)

plt.figure(figsize=(6, 4))
plt.scatter(x_train, y_train)

모델은 nn.Linear(1, 1)로 한 개의 선형층(입력 1, 출력 1)을 쓴다.

model = nn.Linear(1, 1)   # y = Wx + b
print(model)

y_pred = model(x_train)
print(y_pred)
print(list(model.parameters()))   # W, b (초기값은 무작위)

※ 손실 함수 (Loss Function)

손실 함수는 예측값과 실제값의 차이를 하나의 숫자로 나타낸다. 이 값을 최소화하는 방향으로 가중치를 조정한다. 회귀에서는 주로 MSE(평균 제곱 오차) 를 쓴다.

((y_pred - y_train) ** 2).mean()
loss = nn.MSELoss()(y_pred, y_train)
loss

※ 최적화와 경사 하강법

최적화는 손실을 줄이기 위해 W, b를 조정하는 과정이다. 경사 하강법(Gradient Descent) 은 손실 함수의 기울기(그래디언트) 반대 방향으로 조금씩 파라미터를 옮긴다. 학습률(Learning Rate) 은 한 번에 옮기는 크기다. 너무 크면 불안정하고, 너무 작으면 학습이 느리다.

데이터를 어떻게 나눠 쓰느냐에 따라 배치 / 확률적(SGD) / 미니배치 경사 하강법으로 구분한다.

SGD(확률적 경사 하강법) 로 한 스텝 업데이트하는 예시는 다음과 같다.

optimizer = optim.SGD(model.parameters(), lr=0.01)

optimizer.zero_grad()   # 이전 스텝의 기울기 초기화
loss.backward()        # 현재 loss로 기울기 계산
optimizer.step()       # W, b 업데이트

print(list(model.parameters()))

여러 에포크 동안 반복하면 손실이 줄어든다.

epochs = 1000

for epoch in range(epochs + 1):
    y_pred = model(x_train)
    loss = nn.MSELoss()(y_pred, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 100 == 0:
        print(f'Epoch: {epoch}/{epochs} Loss: {loss:.6f}')

print(list(model.parameters()))

x_test = torch.FloatTensor([[5]])
y_pred = model(x_test)
print(y_pred)

6-2. 다중 선형 회귀

여러 입력 변수로 하나의 출력을 예측하는 경우다. 식은 Y = W1X1 + W2X2 + … + WnXn + b 형태다.

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'device: {device}')

X_train = torch.FloatTensor([[73, 80, 75],
                             [93, 88, 93],
                             [89, 91, 90],
                             [96, 98, 100],
                             [73, 66, 70],
                             [85, 90, 88],
                             [78, 85, 82]]).to(device)

y_train = torch.FloatTensor([[152], [185], [180], [196], [142], [175], [155]]).to(device)

print(X_train.shape)
print(y_train.shape)

model = nn.Linear(3, 1).to(device)
print(model)

※ Adam

Adam은 Momentum(기울기 누적으로 방향을 부드럽게)과 RMSProp(파라미터별 학습률 조절)을 합친 최적화 알고리즘이다. SGD보다 안정적이고 수렴이 빠른 경우가 많다.

아이디어 설명
Momentum 지금까지의 기울기를 누적해 방향을 부드럽게 (관성)
RMSProp 파라미터마다 학습률을 다르게 조절해 효율적 학습
optimizer = optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()
epochs = 1000

for epoch in range(epochs + 1):
    y_pred = model(X_train)
    loss = loss_fn(y_pred, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 100 == 0:
        print(f'Epoch: {epoch}/{epochs}, Loss: {loss.item():.6f}')

for param in model.parameters():
    print(param)

x_test = torch.FloatTensor([[93, 93, 93]]).to(device)
y_pred = model(x_test)
print(f'새로운 입력 {x_test.tolist()}의 예측: {y_pred.item():.4f}')

7. 논리 회귀 (Logistic Regression)

논리 회귀는 입력을 바탕으로 여러 범주 중 하나로 분류하는 지도 학습이다. 주로 이진 분류에 쓰이며, 선형 결합 결과를 시그모이드로 0~1 확률로 바꾼 뒤, 임계값(예: 0.5)으로 클래스를 정한다.

※ 시그모이드 함수

시그모이드는 입력을 0과 1 사이로 압축하는 함수다. 확률 해석이 가능해 이진 분류의 출력에 많이 쓴다. (e는 자연상수 ≈ 2.718)

7-1. 시그모이드 계산 예

import torch
import torch.nn as nn

x = torch.tensor([1.0, 2.0, 3.0])
w = torch.tensor([0.1, 0.2, 0.3])
b = torch.tensor(0.5)

z = torch.dot(w, x) + b
print(z)

sigmoid = nn.Sigmoid()
output = sigmoid(z)
print(output)

7-2. 단항 논리 회귀 (이진 분류)

입력 하나로 합격/불합격 같은 이진 분류를 하는 경우다.

import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(2026)

x_train = torch.FloatTensor([[0], [1], [3], [5], [8], [11], [15], [20]])
y_train = torch.FloatTensor([[0], [0], [0], [0], [1], [1], [1], [1]])
print(x_train.shape)
print(y_train.shape)

plt.figure(figsize=(8, 5))
plt.scatter(x_train, y_train)

nn.Linear(1, 1) 뒤에 nn.Sigmoid()를 붙이면 0~1 확률이 나온다.

model = nn.Sequential(
    nn.Linear(1, 1),
    nn.Sigmoid()
)
print(model)
print(list(model.parameters()))

y_pred = model(x_train)
print(y_pred)

※ BCE 손실 함수

Binary Cross Entropy(BCE) 는 이진 분류에서 예측 확률과 정답 레이블(0 또는 1) 사이의 차이를 측정한다. 예측이 정답에 가까울수록 손실이 작아진다.

loss = nn.BCELoss()(y_pred, y_train)
loss
optimizer = optim.SGD(model.parameters(), lr=0.01)
epochs = 1000

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()

    if epoch % 100 == 0:
        print(f'Epoch: {epoch}/{epochs} Loss: {loss:.6f}')

print(list(model.parameters()))

x_test = torch.FloatTensor([[10]])
y_pred = model(x_test)
print(y_pred)

y_bool = (y_pred >= 0.5).float()   # 임계값 0.5로 이진 레이블
print(y_bool)

7-3. 다항 논리 회귀 (다중 분류)

종속 변수가 세 개 이상 범주일 때는 다항 논리 회귀를 쓴다. 각 클래스에 대한 점수를 Softmax로 확률로 바꾸고, CrossEntropyLoss로 학습한다.

x_train = [[1, 2, 1, 1], [2, 1, 3, 2], [3, 1, 3, 4], [4, 2, 5, 5],
           [1, 6, 5, 5], [1, 4, 5, 8], [1, 7, 7, 7], [2, 8, 7, 8],
           [2, 7, 6, 7], [2, 6, 6, 6]]
y_train = [0, 0, 0, 1, 1, 1, 2, 2, 2, 2]

x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
print(x_train.shape)
print(y_train.shape)

model = nn.Sequential(
    nn.Linear(4, 3)   # 입력 4, 클래스 3
)
print(model)
print(list(model.parameters()))

y_pred = model(x_train)
print(y_pred)

※ CrossEntropyLoss와 Softmax

CrossEntropyLoss는 다중 클래스 분류에서 예측 분포와 정답 레이블의 차이를 잰다. 내부적으로 Softmax를 포함할 수 있어, 마지막 레이어에 Softmax를 따로 넣지 않고 로짓(점수)만 넣어도 된다.

Softmax는 각 클래스 점수를 확률 분포로 바꾼다. 모든 클래스 확률의 합이 1이 된다.

loss = nn.CrossEntropyLoss()(y_pred, y_train)
loss
optimizer = optim.SGD(model.parameters(), lr=0.01)
epochs = 10000

for epoch in range(epochs + 1):
    y_pred = model(x_train)
    loss = nn.CrossEntropyLoss()(y_pred, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 1000 == 0:
        print(f'Epoch: {epoch}/{epochs} Loss: {loss:.6f}')

예측 시에는 로짓에 Softmax를 적용해 확률을 보고, argmax로 클래스를 고른다.

x_test = torch.FloatTensor([[2, 8, 8, 8]])
y_pred = model(x_test)
print(y_pred)

y_prob = nn.Softmax(dim=1)(y_pred)   # dim=1: 클래스 축
print(y_prob)

print(f'클래스 0 확률: {y_prob[0][0]:.2f}')
print(f'클래스 1 확률: {y_prob[0][1]:.2f}')
print(f'클래스 2 확률: {y_prob[0][2]:.2f}')

print(torch.argmax(y_prob, dim=1))   # 예측 클래스

마치며

  • PyTorch는 동적 계산 그래프, GPU, 자동 미분으로 딥러닝 구현과 실험이 편하다.
  • 텐서는 스칼라·벡터·행렬·다차원으로 확장되며, 연산·차원 조작·NumPy 변환을 잘 익혀 두면 좋다.
  • 선형 회귀nn.Linear + MSE + SGD/Adam으로, 논리 회귀는 선형층 + 시그모이드(이진) 또는 로짓 + CrossEntropy(다중)로 구현할 수 있다.

다음 단계로는 퍼셉트론·다층 퍼셉트론(MLP), CNN, RNN 등을 같은 방식으로 확장해 보면 된다.

(파이토치 프레임워크 · 선형 회귀 · 논리 회귀 참고)