Machine Learning

선형회귀 with Pytorch

해파리냉채무침 2024. 4. 8. 19:00

출처: 차근차근 실습하며 배우는 파이토치 딥러닝 프로그래밍

선형 함수

다음 레이어 함수는 2차원의 입력을 받아 3차원 텐서로 출력하는 함수이다.

 l3 = nn.Linear(2,3)

 

단일 회귀 함수는 다음과 같이 구현된다

1입력 1출력으로 표시되고 있고, bias(정수항)은 기본으로 True로 설정되어있다. 

torch.manual_seed(123)

# 입력 :1 출력 :1 선형 함수의 정의
l1 = nn.Linear(1, 1)

# 선형 함수 확인
print(l1)

Linear(in_features=1, out_features=1, bias=True)

for param in l1.named_parameters():
    print('name: ', param[0])
    print('tensor: ', param[1])
    print('shape: ', param[1].shape)

named_parameters()를 통해 어떤 파라미터를 포함하고 있는지 본다.

입출력 차원이 모두 1차원이므로, 원래 weight와 bias는 스칼라(0계텐서)여야 하지만, 입출력 텐서 차원수가 2이상으로 늘어났을 때를 대비해 확장하기 위해 [1,1] 행렬 [1] 벡터 형태를 띄운다. 레이어 함수안의 파라미터는 학습의 대상이므로 처음부터 자동적으로 requires_grad=True로 설정한다. 

nn.init.constant_(l1.weight,2.0)
nn.init.constant_(l1.bias,1.0)

print(l1.weight)
print(l1.bias)

다음과 같이 파라미터 값을 nn.init.constant_값을 통해 직접설정할 수 있다. y = 2x+1을 정의하였다.

# 테스트용 데이터 생성

# x_np를 넘파이 배열로 정의
x_np = np.arange(-2, 2.1, 1)

# 텐서 변수화
x = torch.tensor(x_np).float()

# (N,1) 사이즈로 변경
x = x.view(-1,1)

# 결과 확인
print(x.shape)
print(x)

입력변수 x를 2차원 텐서로 변환하여 스칼라가 아닌 [1,1] 형태의 weight의 연산을 한다. 

y= l1(x)
print(y.shape)
print(y.data)

위에서 출력한 x를 y = 2x+1에 입력값으로 넣어 출력한다. y의 shape 역시 [5,1]임을 알 수 있다.

 

다중회귀

다중회귀는 2입력 1출력 선형함수이다. 2입력은 wieght의 요소수가 2개임을 의미한다. y= x1+x2+2  다중회귀를 구현해보았다.

l2 = nn.Linear(2,1)
nn.init.constant_(l2.weight,1.0)
nn.init.constant_(l2.bias,2.0)
print(l2.weight)
print(l2.bias)

weight값은 1, bias는 2값을 입력하였다. 

# 2차원 넘파이 배열
x2_np = np.array([[0, 0], [0, 1], [1, 0], [1,1]])

# 텐서 변수화
x2 =  torch.tensor(x2_np).float()

# 결과 확인
print(x2.shape)
print(x2)

입력으로 들어갈 텐서를 입력한다. y= x1+x2+2에 [0,0] [0,1]을 대입했을 때 다음과 같이 출력된다.

y2= l2(x2)
print(y2.shape)
print(y2.data)

 

다중회귀 분석의 출력을 늘려서 2입력 3출력 으로 진행해보았다.

l3 = nn.Linear(2,3)
nn.init.constant_(l3.weight[0,:], 1.0)
nn.init.constant_(l3.weight[1,:], 2.0)
nn.init.constant_(l3.weight[2,:], 3.0)
nn.init.constant_(l3.bias, 2.0)

print(l3.weight)
print(l3.bias)

y3= l3(x2)
print(y3.shape)
print(y3.data)

class 를 이용한 모델 정의

class Net(nn.Module):
  def __init__(self,n_input,n_output):
    super().__init__() #부모 클래스 nn.Module 초기화
    self.l1 = nn.Linear(n_input,n_output) #출력층 정의

  def forward(self,x): #예측함수
    x1= self.l1(x)
    return x1
inputs = torch.ones(100,1)
n_input = 1
n_output = 1
net = Net(n_input,n_output)
outputs = net(inputs)

클래스 Net의 인스턴스 변수 net은 함수 그자체로 동작한다.

 

MSELoss 손실함수

손실을 파라미터로 편미분하는 것이 경사 계산이며, 경사 계산의 결과를 경사 하강법의 파라미터 수정에 사용한다.

criterion = nn.MSELoss()
outputs = net(inputs)
loss= criterion(outputs,labels1)/2.0
loss.backward() #경사 계산

criterion을 nn.MSELoss 클래스의 인스턴스로 정의한다. MSE는 평균제곱오차(Mean square Loss) 이다. 

여기서도 인스턴스 변수 criterion을 함수로 이용하고 있다. backward를 통해 loss 계산의 경사 계산이 이루어진다.

 

회귀 / MSELoss/ 평균오차 

이진분류 / BCELoss/ 이진 분류용 교차 엔트로피 함수

다중분류 / CrossEntropyLoss/ 다중분류용 교차 엔트로피 함수

데이터셋 적용

보스턴 하우스 데이터셋을 적용하여 모델을 구현해보았다.

n_input = x.shape[1]
n_output = 1
print(f'입력 차원수: {n_input}  출력 차원수: {n_output}')

입력 차원수: 1 출력 차원수: 1

class Net(nn.Module):
  def __init__(self,n_input,n_output):
    super().__init__() #부모 클래스 nn.Module 초기화
    self.l1 = nn.Linear(n_input,n_output)
    nn.init.constant_(self.l1.weight, 1.0)
    nn.init.constant_(self.l1.bias, 1.0)
  def forward(self,x): #예측함수 정의
    x1= self.l1(x) #선형회귀
    return x1

가중치와 편향을 모두 1로 초기값을 설정해서 Net 클래스를 정의했다.차원 역시 nn.Linear(1,1) 이므로 l1.weight는 shape = [1,1]인 2계 텐서, l1.bias 는 shape=[1]인 1계 텐서이다. 

파이토치 에서는 클래스 내부에 forward함수를 정의해서 inputs을 받아 outputs을 반환하기 위한 처리를 기술한다. 

net = Net(n_input,n_output)
outputs = net(inputs)

class Net의 인스턴스 변수 net을 생성한다.

for parameter in net.named_parameters():
    print(f'변수명: {parameter[0]}')
    print(f'변숫값: {parameter[1].data}')

net 변수안의 파라미터를 확인한다. 가중치는 2계텐서, 편향은 1계텐서로 값은 모두 1로 되어있다.

criterion = nn.MSELoss() #loss function
lr = 0.01 #학습률
optimizer = optim.SGD(net.parameters(),lr=lr) #최적화

손실함수와 최적화를 진행한다.

inputs = torch.tensor(x).float()
labels = torch.tensor(yt).float()
print(inputs.shape)
print(labels.shape)

torch.Size([506, 1])

torch.Size([506])

입력값 x와 정답 yt를 텐서 변수화 한다.

labels1 = labels.view((-1, 1))
print(labels1.shape)

torch.Size([506, 1])

labels1 = labels.view((-1, 1))
print(labels1.shape)

torch.Size([506, 1])

텐서로 변환한 정답 labels는 criterion 함수로 예측값과 함께 넘겨주고 손실을 계산한다. 인수는 N차원 벡터 형식이 아니라 (N,1) 차원 행렬 형식을 만족해야하기 때문에 view함수를 이용해 데이터 사이즈를 변경한다.

 

# 예측 계산
outputs = net(inputs)

# 손실 계산
loss = criterion(outputs, labels1)

# 경사 계산
loss.backward()

# 경사 계산 결과를 취득 가능하도록 함
print(net.l1.weight.grad)
print(net.l1.bias.grad)

tensor([[-199.6421]])

tensor([-30.4963])

 

파라미터 수정은 step을 사용한다. 파라미터 수정은 w= w- lr* grad 으로 이루어진다.

# 파라미터 수정
optimizer.step()

# 파라미터 확인
print(net.l1.weight)
print(net.l1.bias)

초기값 1.0과 다른값으로 수정되었다. 이후 경사값을 초기화 하여 경사값이 누적되어 더해지는것을 피한다.

# 경삿값 초기화
optimizer.zero_grad()

print(net.l1.weight.grad)
print(net.l1.bias.grad)

 

반복 계산을 통해 에폭이 100번째 일때마다 손실값을 기록한다. 

lr = 0.01
net = Net(n_input,n_output)
criterion  = nn.MSELoss()
optimizer = optim.SGD(net.parameters(),lr = lr)
num_epochs = 5000
history = np.zeros((0,2))
# 반복 계산 메인 루프

for epoch in range(num_epochs):
    
    # 경삿값 초기화
    optimizer.zero_grad()

    # 예측 계산
    outputs = net(inputs)
  
    # 손실 계산
    loss = criterion(outputs, labels1) / 2.0

    # 경사 계산
    loss.backward()

    # 파라미터 수정
    optimizer.step()

    # 100회 마다 도중 경과를 기록
    if ( epoch % 100 == 0):
        history = np.vstack((history, np.array([epoch, loss.item()])))
        print(f'Epoch {epoch} loss: {loss.item():.5f}')
print(f'초기 손실값: {history[0,1]:.5f}')
print(f'최종 손실값: {history[-1,1]:.5f}')

초기 손실값: 154.22493

최종 손실값: 24.26480

손실값이 초기에 비해 개선된 것을 알 수 있다.