본문 바로가기
Python/ML&DL

[파이썬, Python] 파이토치(Pytorch)로 논리 회귀(Logistic Regression)구현하기!

by coding-choonsik 2023. 6. 26.
728x90
반응형
SMALL

1. 단항 논리 회귀 실습

  • 논리 회귀(Logistic Regression)
  • 분류를 할 때 사용하며 선형 회귀 공식으로부터 나왔기 때문에 논리 회귀라는 이름이 붙여짐

▲ 직선 하나(선형 회귀)를 사용해서 예측한다면 제대로 예측할 수 없음(정확도가 떨어짐)

 

▲ Sigmoid 함수(Logistic 함수)를 사용하여 정확도를 높임

 

1-1. 시그모이드 함수

  • 예측값이 0에서 1사이 값이 되도록 만듦
  • 0에서 1사이의 연속된 값을 출력으로 하기 때문에 보통 0.5를 기준으로 구분

▲ 시그모이드 함수식


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

#  동일한 코드를 실행할 때마다 동일한 난수 시퀀스를 얻을 수 있도록 함
# 모델 초기화나 데이터 분할 등에서 난수를 사용하는 경우에 일관된 결과를 얻을 수 있음
torch.manual_seed(10)

 

x_train = torch.FloatTensor([[0], [1], [5], [9], [11], [15], [20]])  # 텐서형으로 데이터 생성
y_train = torch.FloatTensor([[0], [0], [0], [0], [1], [1], [1]])  # 11시간 공부한것부터 합격

print(x_train.shape, y_train.shape)

>>> torch.Size([7, 1]) torch.Size([7, 1])

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

▲ 선형으로 분류 시 애매하게 분류되는 데이터들이 있음

 

# Sequential(): Pytorch에서 머신러닝/딥러닝 모델을 만들때 여러가지 레이어를 합칠 때 사용하는 메서드
model = nn.Sequential(
    nn.Linear(1, 1),  # 선형회귀
    nn.Sigmoid()      # 시그모이드 함수를 거쳐서
)

print(model)

>>> Sequential(
  (0): Linear(in_features=1, out_features=1, bias=True)
  (1): Sigmoid()
)

print(list(model.parameters()))  # 임의의 값(학습 전)
>>> [Parameter containing:
tensor([[-0.0838]], requires_grad=True), Parameter containing:
tensor([-0.0343], requires_grad=True)]

1-2. 비용 함수

  • 논리 회귀에서는 nn.BCELoss() 함수를 사용하여  Loss를 계산
  • BCE: Binary Cross Entropy

 

# 예측값
y_pred = model(x_train)
y_pred  # 의미없는 결과(학습 전)

>>> 
tensor([[0.4914],
        [0.4705],
        [0.3885],
        [0.3124],
        [0.2776],
        [0.2156],
        [0.1530]], grad_fn=<SigmoidBackward0>)
        
        
loss = nn.BCELoss()(y_pred, y_train)
loss

>>> tensor(0.9817, grad_fn=<BinaryCrossEntropyBackward0>)

# 옵티마이저 설정
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()       # 업데이트

  # 100번마다 프린트
  if epoch % 100 ==0:
    print(f'Epoch: {epoch}/{epochs}  Loss: {loss: 6f}')

# W, b 알아보기
print(list(model.parameters()))

>>>
[Parameter containing:
tensor([[0.2138]], requires_grad=True), Parameter containing:
tensor([-1.6024], requires_grad=True)]


# 2.5와 15.5의 테스트값으로 예측
x_test = torch.FloatTensor([[2.5], [15.5]])
y_pred = model(x_test)
print(y_pred)

>>> tensor([[0.2558],
        [0.8470]], grad_fn=<SigmoidBackward0>)
        
        
# 임계치 설정하기
# 0.5 보다 크거나 같으면 1, 0.5보다 작으면 0
y_bool = (y_pred >= 0.5).float()  # float형으로
print(y_bool)  # 2.5는 0, 15.5는 1로 예측함

>>> tensor([[0.],
        [1.]])

2. 다항 논리 회귀 실습

x_train = [[1, 2, 1, 1],
           [2, 1, 3, 2],
           [3, 1, 3, 4],
           [4, 1, 5, 5],
           [1, 7, 5, 5],
           [1, 2, 5, 6],
           [1, 6, 6, 6],
           [1, 7, 7, 7]]
y_train = [0, 0, 0, 1, 1, 1, 2, 2]  # 결과가 3개

x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)  # Long형 Tensor

print(x_train.shape)
print(y_train.shape)

>>> 
torch.Size([8, 4])
torch.Size([8])

 

# Sequential() : 하나의 레이어만 있어도 써주는것이 좋음!(순서 중요)
model = nn.Sequential(
    nn.Linear(4,3)  # 4개의 입력(독립변수), 3개의 출력
)
print(model)
>>> Sequential(
  (0): Linear(in_features=4, out_features=3, bias=True)
)
# 예측값
y_pred = model(x_train)
print(y_pred)

>>> 
tensor([[-0.1358,  1.5655, -0.0104],
        [-1.0986,  1.3559,  0.5590],
        [-1.4625,  1.2105,  0.7214],
        [-2.3103,  1.4702,  1.2348],
        [-1.0576,  3.7047,  0.7548],
        [-1.7209,  1.1885,  1.0922],
        [-1.5469,  3.1835,  1.0722],
        [-1.8061,  3.6010,  1.2775]], grad_fn=<AddmmBackward0>)

 

  • 다항 논리 회귀에서는 BCELoss() 함수 대신에 CrossEntropyLoss() 함수를 사용
  • CrossEntropyLoss(): softmax 함수가 포함되어있음
  • softmax 함수: 클래스의 개수만큼 확률을 반환(0일 확률, 1일 확률, 2일 확률)
loss = nn.CrossEntropyLoss()(y_pred, y_train)
print(loss)

>>> tensor(1.7613, grad_fn=<NllLossBackward0>)

 

✅ 학습

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

epochs = 1000

for epoch in range(epochs + 1):
  y_pred = model(x_train)
  loss = nn.CrossEntropyLoss()(y_pred, y_train)  # softmax 포함, 내부적으로 원핫인코딩 -> 각각의 확률 반환

  optimizer.zero_grad()  # 초기화
  loss.backward()        # 역전파
  optimizer.step()       # 업데이트

  # 100번마다 프린트
  if epoch % 100 ==0:
    print(f'Epoch: {epoch}/{epochs}  Loss: {loss: 6f}')

 

✅ test 값으로 확인

x_test = torch.FloatTensor([[1,2,5,6]])
y_pred = model(x_test)
y_pred

>>> 
tensor([[-4.9985,  3.4779,  2.0804]], grad_fn=<AddmmBackward0>)

 

✅ 예측값과 확률 구하기

y_prob = nn.Softmax(1)(y_pred) # 1차원 데이터를 넣음
y_prob
>>> tensor([[1.6701e-04, 8.0166e-01, 1.9817e-01]], grad_fn=<SoftmaxBackward0>)

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

>>> 
0일 확률: 0.00
1일 확률: 0.80
2일 확률: 0.20

 

# argmax: y_prob에서 가장 큰 값의 인덱스를 반환하는 연산
# axis=1 매개변수는 텐서의 두 번째 차원을 따라 연산을 수행하라는 것
torch.argmax(y_prob, axis=1)

>>> tensor([1])  # 1로 예측함

3. 경사 하강법(Gradient Descent)

  • 주어진 손실 함수(Loss Function)를 최소화하기 위해 사용되며, 모델의 가중치와 편향을 업데이트하는 방법

 

3-1. 배치 사이즈(Batch Size)

  • 배치 사이즈(batch size)는 경사 하강법 알고리즘에서 한 번의 파라미터 업데이트를 위해 사용되는 학습 데이터의 샘플 개수
  • 즉, 학습 데이터를 작은 묶음(batch)으로 나누어서 모델을 학습하는 방법
  • 일반적으로 배치 사이즈는 하이퍼파라미터로 설정되며, 선택하는 방법은 다양
  • 작은 배치 사이즈는 메모리 사용량이 적고 계산 효율성이 높지만, 노이즈가 많을 수 있음
  • 큰 배치 사이즈는 메모리 사용량이 늘어나고 계산 효율성이 감소하지만, 더 안정적인 그래디언트 추정치를 얻을 수 있음

3-2. 배치 경사 하강법(Vanilla Gradient Descent)

  • 가장 기본적인 경사 하강법
  • 데이터셋 전체를 고려하여 손실함수를 계산
  • 한 번의 Epoch에 모든 파라미터 업데이트를 단 한번만 수행
  • Batch의 개수와 Iteraion은 1이고 Batch size는 전체 데이터의 개수
  • 파라미터 업데이트 할 때 한 번에 전체 데이터셋을 고려하기 때문에 모델 학습 시 많은 시간과 메모리가 필요하다는 단점

3-3. 확률적 경사 하강법(SGD, Stochastic Gredient Descent)

  • 확률적 경사 하강법(SGD, Stochastic Gredient Descent)은 배치 경사 하강법이 모델 학습 시 많은 시간과 메모리가 필요하다는 단점을 개선하기 위해 제안된 기법
  • Batch size를 1로 설정하고 파라미터를 업데이트 하기 때문에 배치 경사 하강법보다 훨씬 빠르고 적은 메모리로 학습이 진행
  • 파라미터 값의 업데이트 폭이 불안정하기 때문에 정확도가 낮은 경우가 생길 수 있음

▲ 배치경사 하강법과 확률적 경사하강법


3-4. 미니 배치 경사 하강법(Mini-batch Gradient Descent)

  • 미니 배치 경사하강법(Mini-batch Gradient Descent)은 Batch size가 1도 전체 데이터 개수도 아닌 경우
  • 배치 경사 하강법보다 모델 학습 속도가 빠르고, 확률적 경사 하강법보다 안정적인 장점이 있음
  • 딥러닝 분야에서 가장 많이 활용되는 경사 하강법
  • 일반적으로 Batch size를 32, 64, 128과 같이 2의 n제곱에 해당하는 값으로 사용하는게 보편적 (CPU 내부 설계 자체가 2의 n제곱 연산이 가장 속도가 빠르기 때문에

4. 경사하강법의 여러가지 기술들

 

4-1. 확률적 경사 하강법(SGD)

  • 매개변수 값을 조정 시 전체 데이터가 아니라 랜덤으로 선택한 하나의 데이터에 대해서만 계산하는 방법

 

4-2. 모멘텀(Momentum)

  • 관성이라는 물리학의 법칙을 응용한 방법
  • 경사 하강법에 관성을 더 해줌
  • 접선의 기울기에 한 시점 이전의 접선의 기울기 값을 일정한 비율만큼 반영
  • 언덕에서 공이 내려올 때 중간의 작은 웅덩이에 빠지더라도 관성의 힘으로 넘어서는 효과를 줄 수 있음
    local minimum에서 빠져나올 수 있도록 함

▲ local minimum에서 빠져나와 global minumum을 볼 수 있음

 

4-3. 아다그라드(Adagrad)

  • 모든 매개변수에 동일한 학습률(learning rate)을 적용하는 것은 비효율적이라는 생각에서 만들어진 학습 방법
  • AdaGrad는 Feature별로 학습률(Learning rate) Adaptive하게, 즉 다르게 조절하는 것이 특징
  • 처음에는 크게 학습하다가 작게 학습시킴

 

 

4-4. 아담(Adam)

  • 아다그라드 + 모멘텀
  • 학습의 방향과 크기(=Learning rate)를 모두 개선한 기법으로 딥러닝에서 가장 많이 사용되어오던 최적화 기법
  • 여러 기술들이 합쳐졌기 때문에 속도가 느릴 수 있음

# Adam으로 바꿔서 다시 학습 - Accuracy가 매우 좋아짐!
model = nn.Sequential(
    nn.Linear(13, 3)
)

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

epochs = 1000

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 % 100 == 0:
        y_prob = nn.Softmax(1)(y_pred)  # 클래스별 확률을 반환(차원을 1)
        y_pred_index = torch.argmax(y_prob, axis=1)   # 각 샘플별로 클래스별 확률 중 가장 큰값을 가지는 클래스의 인덱스값(axis=1:각 행에서)
        y_train_index = torch.argmax(y_train, axis=1)   # 실제 클래스 레이블에서 각 샘플별로 가장 큰 값을 가지는 클래스의 인덱스(axis=1: 각 행에서)
        accuracy = (y_train_index == y_pred_index).float().sum() / len(y_train) * 100   # bool값으로 반환된 데이터를 float으로 형변환 후 더함
        print(f'Epoch { epoch:4d}/{epochs} Loss: {loss:.6f} Accuracy: {accuracy:.2f}%')

 

y_pred = model(x_test)
y_pred[:5]

>>> tensor([[38.3035, 41.4661, 36.4269],
        [23.5532, 30.4381, 23.5679],
        [48.5956, 47.4519, 42.9946],
        [29.9729, 36.5060, 29.0688],
        [65.1588, 60.0035, 59.2508]], grad_fn=<SliceBackward0>)

 

y_prob = nn.Softmax(1)(y_pred)
y_prob[:5]  # 예측값에 대한 확률

>>> tensor([[4.0348e-02, 9.5347e-01, 6.1778e-03],
        [1.0210e-03, 9.9794e-01, 1.0361e-03],
        [7.5624e-01, 2.4096e-01, 2.7938e-03],
        [1.4515e-03, 9.9796e-01, 5.8772e-04],
        [9.9158e-01, 5.7203e-03, 2.6948e-03]], grad_fn=<SliceBackward0>)
        
print(f'0번 품종일 확률: {y_prob[0][0]: .2f}')
print(f'1번 품종일 확률: {y_prob[0][1]: .2f}')
print(f'2번 품종일 확률: {y_prob[0][2]: .2f}')

>>> 
0번 품종일 확률:  0.04
1번 품종일 확률:  0.95
2번 품종일 확률:  0.01


y_pred_index = torch.argmax(y_prob, axis=1)
y_test_index = torch.argmax(y_test, axis=1)  # 예측된 레이블에서 각 샘플별로 가장 큰 값을 가지는 클래스의 인덱스(axis=1: 각 행에서)
accuracy = (y_test_index == y_pred_index).float().sum() / len(y_test) * 100
print(f'테스트 정확도는 {accuracy:.2f}% 입니다')

>>> 테스트 정확도는 88.89% 입니다

 

728x90
반응형
LIST