TensorFlow2를 이용한 간단한 회귀분석

TensorFlow v2가 정식버전으로 배포된지 몇달이 지났습니다. 필자는 딥러닝 라이브러리로 PyTorch를 주력으로 하고 있으나, TensorFlow로 만들어진 많은 코드 분석 및 협업을 위해 TensorFlow에 대한 API도 관심이 많습니다. 이 글에서는 TensorFlow 버전2에서 sin 함수에 대한 회귀분석에 대한 샘플 코드를 설명합니다.

필요한 패키지에 대한 import 및 훈련 데이터와 테스트 데이터를 아래 코드를 통해 준비합니다.

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import SGD, RMSprop, Adam
from tensorflow.keras import metrics

np.random.seed(0)

train_x = np.linspace(0,np.pi*2,10).reshape(10,1)
train_y = np.sin(train_x)
test_x = np.linspace(0,np.pi*2,100).reshape(100,1)
test_y = np.sin(test_x)

sin 함수에 대한 회귀분석을 위한 신경망 모델은 다음과 같습니다.

sin 함수는 1개의 입력값을 받아 1개의 출력값을 가지므로 입력층과 출력층의 뉴런 개수는 1개입니다. 중간의 은닉층의 뉴런은 임의로 2개로 잡았습니다. 이 신경망 모델을 구성하는 코드는 다음과 같습니다.

input_nodes = 1
hidden_nodes = 2
output_nodes = 1

model = Sequential()

model.add(Dense(hidden_nodes, input_dim=input_nodes, activation='sigmoid'))
model.add(Dense(output_nodes))

print(model.summary())

모델 상세 정보가 콘솔에 표시되는데, 다음과 같습니다.

파라메터의 개수는 가중치 w 뿐만 아니라 편차값인 b 값도 고려해야 합니다.

신경망을 학습할 것인데, 학습에 사용할 최적화 방식으로 4가지를 사용합니다. 아래 코드에 사용할 최적화 방법에 대한 구체적인 코드는 다음과 같습니다.

optimizers = {
    'SGD': SGD(lr=0.1),
    'Momentum': SGD(lr=0.1, momentum=0.9), 
    'RMSProp': RMSprop(lr=0.01),
    'Adam': Adam(lr=0.01)
}

다음 코드는 학습입니다.

train_results = []
train_y_predicted = []
test_y_predicted = []

for optimizer_name, optimizer in optimizers.items():
    print(optimizer_name, 'Training ...')

    model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])

    result = model.fit(train_x, train_y, epochs=1000, verbose=0)
    train_results.append(result)

    train_result = model.predict(train_x)    
    train_y_predicted.append(train_result)

    test_result = model.predict(test_x)
    test_y_predicted.append(test_result)

위의 코드 중 8번의 model.compile은 모델 학습하기 위해 먼저 호출해야 하는 코드입니다. 인자로 loss와 metrics가 있는데, 각각 가질 수 있는 값은 ‘mean_squared_error'(주로 회귀용), ‘categorilcal_crossentropy'(주로 다중분류), binary_crossentropy'(주로 이진분류) 등과 ‘mse'(주로 회귀용), ‘accuracy'(주로 분류용) 등입니다. 그리고 실제 학습은 10번 코드를 통해 이뤄집니다. 단 1줄로 말입니다. 이 부분은 케라스의 장점이죠. model.fit 함수의 결과값은 손실값과 정확도에 대한 값을 포함합니다. 13번 코드와 16번 코드는 훈련된 모델을 통해 실제 계산을 수행하는 코드입니다. 각각 학습 데이터와 테스트 데이터로 계산을 수행해 그 결과를 배열에 담습니다.

앞의 코드에서는 중간 결과를 배열에 담았는데요. 이렇게 담은 배열은 최종 결과 그래프를 생성하기 위해 다음처럼 사용됩니다.

fig, axes = plt.subplots(3,1)
axes[0].plot(train_x, train_y, '-o', label = 'sin(x)')
for i, optimizer_name in enumerate(optimizers.keys()):
    axes[0].plot(train_x, train_y_predicted[i], '--', label=optimizer_name)
axes[0].legend()    

axes[1].plot(test_x, test_y, '-o', label = 'sin(x)')
for i, optimizer_name in enumerate(optimizers.keys()):
    axes[1].plot(test_x, test_y_predicted[i], '--', label=optimizer_name)
axes[1].legend()

for i, optimizer_name in enumerate(optimizers.keys()):
    axes[2].plot(train_results[i].history['loss'], '--', label=optimizer_name)
axes[2].legend()

plt.show()

결과는 다음과 같습니다.

이 경우 Momentum와 RMSprop의 장점을 섞은 Adam 최적화 방식이 가장 좋은 결과를 제공하는 것을 알 수 있습니다.

신경망을 이용한 비선형 모델의 회귀분석

딥러닝을 위한 신경망은 기본적으로 선형회귀분석을 기반으로 합니다. 선형 회귀 분석이라는 전제 조건은 아주 복잡한 모델, 즉 비선형인 형태의 모델은 추론할 수 없지만, 신경망의 층(Layer)를 깊게 쌓으면서 그 중간에 비선형성을 부여하는 활성화 함수를 넣어주게 되면 선형회귀분석에 기반한 신경망으로도 아주 복잡한 비선형 모델도 추론할 수 있다는 것입니다. 예를 들어, 아래와 같은 분포를 가지는 데이터셋에 대한 회귀분석도 가능합니다.

위의 데이터는 다음과 같은 공식에 대해서, y값에 표준편차 30인 정규분포의 잡음(Noise)를 추가해 생성한 것입니다.

    $$y = 0.5 \times x^{3} - 0.5 \times x^{2} - 90 \times sin(x^{2}) + 1 $$

이제 위의 데이터셋을 이용해 딥러닝 학습을 통해 비선형 모델에 대한 추론에 대한 코드를 정리하겠습니다. 코드는 파이선으로, 그리고 딥러닝 라이브러리는 파이토치를 사용했습니다.

먼저 필요한 패키지를 임포트합니다.

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

학습을 위해 데이터가 필요한데, 앞서 언급한 공식을 활용하여 총 5000개의 (x, y) 값의 데이터를 생성합니다. 물론 y 값에는 마찬가지로 앞서 언급한 표준편차가 30인 정규분포로 생성된 잡음을 반영합니다. 아래는 이에 대한 코드입니다.

num_data = 5000
noise = init.normal_(torch.FloatTensor(num_data,1), std=30)
x = init.uniform_(torch.Tensor(num_data,1),-10,10)

def func(x): return 0.5*(x**3) - 0.5*(x**2) - torch.sin(2*x)*90 + 1 
y_noise = func(x) + noise

신경망 모델을 생성합니다. 신경망 모델에 대한 코드는 아래와 같습니다.

model = nn.Sequential(
    nn.Linear(1,5),
    nn.LeakyReLU(0.2),
    nn.Linear(5,10),
    nn.LeakyReLU(0.2),
    nn.Linear(10,10),
    nn.LeakyReLU(0.2),    
    nn.Linear(10,10),
    nn.LeakyReLU(0.2),        
    nn.Linear(10,5),
    nn.LeakyReLU(0.2),          
    nn.Linear(5,1),
)

위의 신경망을 도식화하면 다음과 같습니다.

활성화 함수로 Leaky ReLU를 사용한 이유는, Sigmoid의 경우 기울기 소실이 발생하여 학습이 잘이루어지지 않고 일반 ReLU를 사용할 경우 학습 대상이 되는 가중치와 편향이 음수가 될 경우에 입력값까지 음수가 되면 최종 활성화 값이 항상 0이 되어 이 값이 뉴런에 전달되고, 전달 받은 뉴런이 제 역활을 하지 못하는 현상(문헌에서는 Dying Neuron이라고 함)이 발생하기 때문입니다 Leaky ReLU는 기울기 소실 문제와 입력값이 음수일때에도 일반 ReLU처럼 0이 아닌 가중치(위에서는 0.2)가 반영된 값이 활성값으로 결정되어 Dying Neuron 현상을 막아줍니다.

다음은 학습에 대한 코드입니다.

gpu = torch.device('cuda')
loss_func = nn.L1Loss().to(gpu)
optimizer = torch.optim.Adam(model.parameters(), lr=0.002)

model = model.to(gpu)
x = x.to(gpu)
y_noise = y_noise.to(gpu)

num_epoch = 20000
loss_array = []
for epoch in range(num_epoch):
    optimizer.zero_grad()
    output = model(x)
    
    loss = loss_func(output,y_noise)
    loss.backward()
    optimizer.step()
    
    loss_array.append(loss)

    if epoch % 100 == 0:
        print('epoch:', epoch, ' loss:', loss.item())

손실값은 매우 단순한 L1 손실을 사용는데, 위의 학습을 위한 데이터셋의 경우 오차값의 절대값이 L1 값이고 오차값에 대해 손실값이 비례하므로 L1 손실은 적당하고 학습 속도가 빠릅니다. 그리고 가중치에 대한 최적화 방법은 Adam을 사용했습니다. 일반 SGD 방식은 그 방식이 매우 단순해서 좀처럼 학습이 되지 않습니다.

이제 학습 동안 손실값의 추이와 추론된 신경망의 모델에 대한 결과를 그래프로 나타내기 위한 코드는 다음과 같습니다.

plt.plot(loss_array)
plt.show()

plt.figure(figsize=(10,10))

x = x.cpu().detach().numpy()
y_noise = y_noise.cpu().detach().numpy()
output = output.cpu().detach().numpy()

plt.scatter(x, y_noise, s=1, c="gray")
plt.scatter(x, output, s=1, c="red")

plt.show()

위 코드에서 손실에 대한 그래프 결과는 다음과 같습니다.

손실값이 매 에폭마다 감소하는 것을 보면 학습이 제대로 이루어지고 있다는 것을 알 수 있습니다. 그리고 가장 중요한 그래프인, 신경망 학습의 추론 결과에 대한 그래프입니다.

회색 지표는 학습 데이터이고 빨간색 지표가 학습된 모델이 추론한 결과입니다. 데이테에 매우 근접한 추론 결과를 나타내고 있는 것을 볼 수 있습니다. 그래프가 곡선처럼 보이지만 사실은 직선으로 구성된 그래프입니다. 이는 앞서 언급했듯이 신경망이 선형회귀에 기반하고 있기 때문입니다.

잠재벡터(Latent) z의 공간분포 시각화(Visualization)

의미적으로 같은 성질의 데이터들을 공간상에 분포시켜 가시화해 본다면, 같은 의미를 가지는 데이터들은 공간 위치상으로 한곳에 모여있을 것입니다. 이렇게 데이터를 공간상에 분포시켜 놓을 수 있다면 해석 결과로써의 데이터가 얼마나 잘 해석되었는지를 시각화할 수 있고, 새로운 데이터에 대한 공간상의 위치를 통해 어떤 성질군에 해당하는지를 시각적으로 쉽게 파악할 수 있습니다.

그러나 문제는 사람이 인지하는 공간은 2차원 또는 3차원이라는 점이고, 데이터는 이보다 더 큰 차원을 갖는다는 것이 일반적입니다. 만약 3차원보다 큰 다 차원의 데이터에 대해 어떤 유사도 값이 있다고 할때, 이 유사도와 비슷한 2차원 또는 3차원의 데이터를 얻을 수 있도록 학습시킨다면 아무리 큰 차원의 데이터라도 공간상에 분포시켜 가시화할 수 있게 됩니다. 바로 이런 경우에 활용할 수 있는 매우 강력한 기술이 t-SNE입니다. SNE는 Stochastic Neighbor Embedding의 약자이고 t는 정규분포와 유사한 그래프를 나타냅니다. 아래는 t 분포의 한 예에 대한 이미지입니다.

이 t-SNE를 이용하여 GAN이나 AutoEncoder 등에서 얻어지는 잠재벡터 z를 2차원 공간상에 분포시켜보는 내용을 구체적으로 살펴보겠습니다. Python의 Scikit-Learn 라이브러리에서 제공하는 t-SNE API를 이용하고, 잠재벡터 z는 아래의 AutoEncoder 포스팅에서 소개한 신경망에서 생성된 잠재벡터 값을 이용하겠습니다.

AutoEncoder

이 글에서 제시하는 최종 결과를 얻기 위해서는 위의 글을 통해 먼저 코드를 전제로 합니다. 먼저 위의 글의 소스코드에서 작성한 AutoEncdoer 신경망을 학습 한 이후에 다음의 코드를 통해 잠재 벡터 z를 얻어올 수 있습니다.

inp = transform(test_data.data.numpy().reshape(-1,28,28))
inp = inp.transpose(0,1)
inp = inp.reshape(-1,1,28,28).to(device)

z = model.encoder(inp)
z = z.detach().cpu().numpy()
y = test_data.targets.numpy()

AutoEncoder를 이해하고 있는 사람이라면 잠재벡터 z는 Encoder가 생성한 데이터라는 것을 알고 있을 것입니다. 이제 이렇게 생성한 z를 2차원 공간상에 시각화하는 위한 t-SNE 학습은 다음 코드와 같습니다.

from sklearn.manifold import TSNE
import numpy as np

tsne = TSNE(n_components=2, verbose=1, n_iter=300, perplexity=5)
tsne_v = tsne.fit_transform(z[:6000])

데이터의 양이 너무 많으면 학습 시간이 많이 소요되므로 일단 6000개만 이용해 학습하였습니다. 학습이 완료되면 z를 2차원 상에 각 z에 해당되는 원래 이미지와 함께 공간상에 시각해 보면 다음과 같습니다.

결과를 보면, 신발은 신발끼리 바지는 바지끼리.. 윗옷은 윗옷끼리 서로 그룹핑되어 분포하고 있는 것을 볼 수 있습니다. 좀더 세밀하게 관찰해보면 같은 신발이라도 신발의 세부 분류 항목으로 다시 그룹핑된다는 것입니다. 이는 AutoEncoder의 Encoder가 생성한 잠재벡터의 품질에 따라 그 성과가 달라질 것이고, 이러한 잠재벡터의 공간상 분포 가시화는 t-SNE를 통해 시각화가 가능하다라는 것입니다.

강화학습 용어

환경(Environment)은 행위자(Agent)가 행동하는 공간입니다. 어떤 상태(State)에서 행위자의 행위(Action)에 따라 새로운 상태(State)으로의 변화와 그 행위에 대한 보상(Reward)이 만들어집니다. 연속된 행위의 처음과 종료까지를 하나의 에피소드(Episode)라고 하는데, 하나의 에피소드를 통해 얻어진 보상의 합을 수익(G)이라고 합니다. 강화학습은 바로 이 수익을 최대화시키기 위한 행위가 선택될 수 있는 정책(Policy)를 강화시키는 것이 목표입니다.

환경이 미로일때, 위의 그림은 행위자가 할 수 있는 행위(Action)입니다. 즉, 미로에서 행위자가 어떤 위치(상태;State)에 있을때 위쪽, 오른쪽, 아래쪽, 왼쪽으로 이동할 수 있는 행위를 나타냅니다. s4에서 행위자가 a1이라는 행위를 했을때 항상 s1으로 이동하는 것을 보장한다면, 이러한 환경을 결정론적 환경이라고 하며, 상태전이확률이 1이라고 합니다.

행위에 대한 가치 또는 상태에 대한 가치를 파악하기 위한 그림으로 백업다이어그램(Backup-Diagram)을 사용하는데, 위의 그림처럼 처음 상태에 대해서 취할 수 있는 행위에 대한 새로운 상태로의 전이를 표현하고 있으며, 각 상태에서의 보상값도 표시하고 있습니다. 보상은 지금 당장의 상태에서 받는 것이 아니고 행위에 대한 그 다음 상태에서 받게 된다는 것에 유의해야 합니다.

강화학습에서 환경(Environment)은 보상 시스템입니다. 다른 학습과는 다르게 강화학습은 데이터로 진행되지 않고, 환경이라는 프로그래밍 시스템을 통해 이루어진다는 점이 큰 매력입니다.