내용
다층 퍼셉트론(Multilayer perceptron)
다층 퍼셉트론(Multilayer perceptron)이란?
- 가장 단순한 심층 네트워크를 다층 퍼셉트론(multilayer perceptron, mlp)
- 원시 데이터의 입력층(input layer) &rarrow; 1개 이상의 은닉층(hidden layer) &rarrow; 마지막 결과 출력층(output layer)
첫 입력층의 결과부터 이전 층의 결과는 다음 층의 입력이 되므로 모든 층들이 밀접하게 연결된 구조이므로 입력 데이터에 국한된 결과를 나타낼 가능성이 증가합니다. 즉, 과적합(over-estimate), 과소적합(under-estimate) 등에 대한 위험성이 증가되므로 그들에 대한 평가와 적절한 모델 선택이 중요합니다. 이러한 문제를 해결하는 데 도움이 되도록 가중치 감소 및 드롭아웃과 같은 정규화 기술들이 사용됩니다.
레이블(반응변수)이 아핀 변환(선형변환)에 의해 입력 데이터(특성, feature)와 관련되어 있다면 이 접근 방식으로 충분할 수 있지만 선형변환은 항상 증가 또는 감소의 변화만을 나타냅니다. 예를 들어 개인이 대출을 상환할지 여부를 예측하려는 경우 다른 모든 조건이 동일할 때 소득이 높은 신청자가 소득이 낮은 신청자보다 항상 상환할 가능성이 더 높다고 합리적으로 상상할 수 있습니다. 단조롭지만 이 관계는 상환 가능성과 선형적으로 연관되지 않을 가능성이 높습니다. 소득이 0에서 50,000으로 증가하면 100에서 105만으로 증가하는 것보다 상환 가능성이 더 크게 증가할 가능성이 높습니다. 이를 처리하는 한 가지 방법은 소득의 로그를 특성으로 사용하여 선형성이 더 명확해지도록 데이터를 전처리하는 것일 수 있습니다.
그러나 이러한 선형변환의 단조성은 여러 층들을 구성하는 것과 단일 층과의 차별이 어렵습니다. 그러므로 비선형으로 변환하는 하나 이상의 은닉층을 통해 선형 모델의 이러한 한계를 극복 할 수 있습니다. 이를 수행하는 가장 쉬운 방법은 서로 완전히 연결된 여러 레이어를 쌓는 것입니다. 각 레이어는 출력을 생성할 때까지 그 다음의 레이어에 공급됩니다. 첫 번째 L1 레이어를 통해 선형변환하고 은닉층을 통과하는 과정에서 비선형으로 변환되며 마지막 레이어를 선형 예측자로 구성할 수 있습니다. 이 아키텍처를 일반적으로 MLP로 약칭되는 다층 퍼셉트론이라고 합니다. 그림 1은 MLP를 도식적으로 나타낸 것입니다.
위 그림은 4개의 입력, 3개의 출력이 있으며 은닉층에는 5개의 은닉 유닛으로 구성된 MLP를 나타냅니다. 입력 계층에는 계산이 포함되지 않으므로 이 네트워크로 출력을 생성하려면 은닉 계층과 출력 계층 모두에 대한 계산을 구현해야 합니다. 따라서 이 MLP의 레이어 수는 2입니다. 이 레이어는 모두 완전히 연결되어 있습니다. 모든 입력은 은닉층의 모든 뉴런에 영향을 미치고, 이들 각각은 차례로 출력층의 모든 뉴런에 영향을 미칩니다.
입력(X)의 예제(인스턴스)가 n이고 특성수 d, 은닉층(hidden layer, H)의 유닛수 h, 출력층(O)의 출력유닛수를 q라고 하면 은닉층과 출력층에 적용되는 가중치와 편차 모두를 벡터단위로 다음과 같이 나타낼 수 있습니다.
- X ∈ ℝn × d
- W(1)∈ ℝd × h
- b(1)∈ ℝ1 × h
- H ∈ ℝn × h
- W(2)∈ ℝh × q
- b(1)∈ ℝ1 × q
- O ∈ ℝn × q
위의 각 층간의 연산은 다음과 같이 구성됩니다.
$$\begin{align}\tag{1} H&=XW^{(1)}+b^{(1)}\\ O&=HW^{(2)}+b^{(2)}\end{align}$$위와 같이 은닉층을 구성한 경우 선형변환과 선형변환이 이어지므로 은닉층에 대한 어떠한 의미를 부여할 수 없습니다. 선형변환의 결과에 다시 선형변환을 한다면 값의 크기의 변형만을 나타낼 뿐 각 변수간의 비율등의 차이나 방향등의 변형을 가져올 수 없기 때문입니다. 위의 두 식은 다음과 같이 식 2 즉, 단일 층과 같아집니다.
$$\begin{align}\tag{2}W&=W^{(1)}W^{(2)}, \quad b=b^{(1)}W^{(2)}+b^{(2)}\\O&=(XW^{(1)}+b^{(1)})W^{(2)}+b^{(2)}\\&=XW^{(1)}W^{(2)}+b^{(1)}W^{(2)}+b^{(2)}\\&=XW+b \end{align}$$그러므로 다층의 영향을 실현하려면 아핀 변환 후 각 은닉 유닛에 적용할 비선형 활성화 함수(activation function) σ라는 핵심 요소가 하나 더 필요합니다. 일반적으로 은닉층에 활성화 함수를 첨가하면 위와같이 선형모델로 축소하는 것은 불가능합니다.
$$\begin{align}\tag{2} H&=\sigma(XW^{(1)}+b^{(1)})\\ O&=HW^{(2)}+b^{(2)}\end{align}$$다음 그림은 mpl에 활성화 함수를 첨가한 경우를 나타낸 것입니다.
활성함수는 선형을 비선형으로 변환하는 것으로 여러개의 은닉층을 구성할 수 있습니다.
custom layer의 생성
자체 매개변수가 없는 사용자 지정 레이어를 구성합니다. 다음 CenteredLayer 클래스는 단순히 입력에서 평균을 뺍니다. 이를 구축하려면 기본 계층 클래스에서 상속하고 순방향 전파 기능을 구현하기만 하면 됩니다.
class CenteredLayer(nn.Module): def __init__(self): super().__init__() def forward(self, x): return x-x.mean()
layer=CenteredLayer()
layer(torch.FloatTensor([1,2,3,4,5]))
tensor([-2., -1., 0., 1., 2.])
이제 더 복잡한 모델을 구성할 때 레이어를 구성 요소로 통합할 수 있습니다.
net=nn.Sequential(nn.Linear(4, 5), CenteredLayer())
임의의 데이터를 위 모델에 전달
Y=net(torch.rand(3, 4)) Y
tensor([[-0.5496, -0.3306, 0.5292, -0.4686, 0.2001], [-0.5459, -0.1304, 0.4813, -0.1365, 0.8007], [-0.2125, -0.3299, 0.6147, -0.0980, 0.1761]], grad_fn=<SubBackward0>)
Y.mean()
tensor(-1.1921e-08, grad_fn=<MeanBackward0>)
위의 간단한 Layer 작성 과정을 기반으로 자체 매개변수를 가진 layer을 작성해 봅니다. 다음 layer는 그 자체가 하나의 선형모델이 됩니다.
class MyLinear(nn.Module): def __init__(self, inUnit, outUnit): super().__init__() self.weight=nn.Parameter(torch.randn(inUnit, outUnit)) self.bias=nn.Parameter(torch.randn(outUnit,)) def forward(self, X): linear=torch.matmul(X, self.weight.data)+self.bias.data return F.relu(linear) linear=MyLinear(5, 3) linear.weight
Parameter containing: tensor([[-0.3439, 0.3506, -0.0106], [-0.2679, 0.1326, 1.8362], [-2.0786, 0.0293, -0.6827], [ 1.0285, -0.0525, 0.2513], [-1.0882, -0.2500, 0.6304]], requires_grad=True)
linear(torch.rand(2, 5))
tensor([[0.0872, 0.0170, 3.1104], [0.0000, 0.1750, 1.1985]])
net=nn.Sequential(MyLinear(64, 8), MyLinear(8, 1)) net(torch.rand(2, 64))
tensor([[0.3151], [0.0000]])
x=torch.arange(4) x
tensor([0, 1, 2, 3])
저장과 호출
다음과 같이 생성되는 객체를 저장하고 다시 호출할 수 있습니다. 저장과 호출은 모든 형식이 가능합니다.
torch.save(x, 'x-file')#x-file이라는 파일명으로 x를 저장 x2=torch.load('x-file')#x-file을 읽어들임 x2
tensor([0, 1, 2, 3])
모델을 저장하고 호출할 수 있습니다. 그러나 모델 자체를 저장-호출은 과잉이 될 수 있으며 실제 저장이 필요한 것은 모델에서 생성되는 매개변수입니다. 이 경우 내장함수인 모델.state_dict()
을 사용합니다.
class MLP(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) self.output = nn.Linear(256, 10) def forward(self, x): return self.output(F.relu(self.hidden(x))) net = MLP() X = torch.randn(size=(2, 20)) Y = net(X)
torch.save(net.state_dict(), 'mlp_para')
모델을 복구하기 위해 원래 MLP 모델의 복제본을 인스턴스화합니다. 모델 매개변수를 무작위로 초기화하는 대신 파일에 저장된 매개변수를 직접 읽습니다.
clone=MLP() clone.load_state_dict(torch.load('mlp_para')) clone.eval()
MLP( (hidden): Linear(in_features=20, out_features=256, bias=True) (output): Linear(in_features=256, out_features=10, bias=True) )
Y_clone=clone(X) Y_clone == Y
tensor([[True, True, True, True, True, True, True, True, True, True], [True, True, True, True, True, True, True, True, True, True]])
주식자료에 적용
다음은 코스피 주가 자료에 대한 mlp를 적용하는 예 입니다.
import numpy as np import pandas as pd import matplotlib.pyplot as plt import torch from torch import nn from torchvision import datasets, transforms from sklearn import preprocessing from sklearn.model_selection import train_test_split
import FinanceDataReader as fdr st=pd.Timestamp(2000, 1, 1) et=pd.Timestamp(2022, 1, 4) data=fdr.DataReader("KS11", st, et) data.tail(3))
Close | Open | High | Low | Volume | Change | |
---|---|---|---|---|---|---|
Date | ||||||
2021-12-30 | 2977.65 | 2999.75 | 3005.36 | 2975.74 | 461040000.0 | -0.0052 |
2022-01-03 | 2988.77 | 2998.32 | 3010.77 | 2979.42 | 429030.0 | 0.0037 |
2022-01-04 | 2989.24 | 2991.97 | 2995.25 | 2973.08 | 614650.0 | 0.0002 |
다음으로 분석을 위해 원시자료의 결측치 제거등의 전처리 실행합니다. 또한 특성-레이블로 구분하고 표준화 또는 정규화를 실시합니다. 또한 모델을 생성하기 위해 pytorch 모듈의 여러 클래스와 함수를 적용합니다. 이러한 적용을 위해서는 그 모듈에 적합한 자료형으로 변환하여야 합니다. 이러한 처리과정은 결측치 조정과 데이터 분석을 위한 금융자료 I을 참고할 수 있습니다.
결측치와 0값 처리
np.where(data.isna()) (array([], dtype=int64), array([], dtype=int64)) np.where(data ==0)
(array([1011, 1136, 1275, 1662, 2005, 2442, 2619, 3083, 3106, 3174, 3204, 3276, 3322, 3464, 3537, 3693, 3751, 3906, 4638, 4714, 4758, 4783, 5009]), array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]))
data.iloc[1011, 5]
0.0
data1=data.replace(0, method="ffill") data1.iloc[1011,5]
-0.004699999999999999
np.where(data1==0)
(array([], dtype=int64), array([], dtype=int64))
np.where(data1.isna())
(array([], dtype=int64), array([], dtype=int64))
feature와 label의 분리
X=data1.values[:-1, 1:] y=data1.values[1:,0] new=data1.values[-1,1:] new
array([2.99197e+03, 2.99525e+03, 2.97308e+03, 6.14650e+05, 2.00000e-04])
각 feature와 label의 scale을 축소 또는 동일화하기 위해 표준화시킵니다. sklearn.preprocessing 모듈에서 여러 형식의 표준화 클래스를 제공합니다. 여기서는 StandardScaler()
클래스를 적용합니다. 일반적으로 label은 표준화에 자유롭지만 표준화를 적용하기 위해서는 feature와 별도로 진행합니다. 또한 전달하는 인수는 벡터가 아닌 행렬 즉, 2차원이어야 합니다.
indScaler=preprocessing.StandardScaler().fit(X) deScaler=preprocessing.StandardScaler().fit(y.reshape(-1,1)) Xn=indScaler.transform(X) yn=deScaler.transform(y.reshape(-1,1)) newn=indScaler.transform(new.reshape(1,-1)) newn
array([[ 2.03389488, 2.01982703, 2.03050827, -1.7728465 , -0.00757038]])
데이터 세트를 학습, 테스트 세트로 분할합니다. sklearn.model_selection의 함수 train_test_split()
를 사용할 수 있습니다.
Xtr, Xte, ytr, yte=train_test_split(Xn, yn, test_size=0.2, random_state=1) [i.shape for i in [Xtr, Xte, ytr, yte]]
[(4347, 5), (1087, 5), (4347, 1), (1087, 1)]
데이터의 크기가 큰 경우 미니배치로 학습을 합니다. 이 경우 PyTorch의 DataLoader 클래스를 사용합니다. 이 클래스의 인자는 Dataset 형입니다. 데이터 세트에는 주로 두 가지 유형이 있습니다. 하나는 맵 스타일 데이터 세트이고 다른 하나는 반복 가능한 스타일 데이터 세트입니다. pytorch의 경우 반복가능한 데이터 셋을 사용해야 합니다.
데이터 세트 클래스인 TensorDataset이라는 클래스를 사용하겠습니다. Scikit-learn의 scaler는 NumPy 배열을 출력하므로 TensorDatasets에 로드하려면 이를 Torch 텐서로 변환해야 합니다. 각 데이터 세트에 대한 Tensor 데이터 세트를 생성한 후 이를 사용하여 DataLoader를 생성하겠습니다.
#torch 형으로 전환 XtrTor, XteTor, ytrTor, yteTor=map(torch.tensor, [Xtr, Xte, ytr, yte]) XtrTor.shape
torch.Size([4347, 5])
XtrTor.dtype
torch.float64
from torch.utils.data import TensorDataset, DataLoader batSize=128 tr=TensorDataset(XtrTor, ytrTor) test=TensorDataset(XteTor, yteTor) trLoad=DataLoader(tr, batch_size=batSize, shuffle=True) testLoad=DataLoader(test, batch_size=batSize, shuffle=True) len(trLoad)
34
for id, smp in enumerate(trLoad) x, y=sm if id==(len(trLoad)-1) print(id, x[:3,:], y[:3])
33 tensor([[ 0.0383, 0.0275, 0.0360, -0.4584, -0.5651], [ 0.4927, 0.4811, 0.4795, 0.5257, 0.0467], [ 0.4604, 0.4586, 0.4757, 0.6460, 0.2846]], dtype=torch.float64) tensor([[0.0440], [0.4912], [0.4613]], dtype=torch.float64)
입력층-1개의 은닉층-출력층으로 구성되는 mlp를 구현합니다. 그러므로 모델을 위한 가중치와 편차는 두 쌍이 필요합니다. 가중치와 편차의 초기화를 위해 nn.Parameter()
를 적용합니다.
numIn, numOut, numHidden=5, 1, 16 torch.manual_seed(0) w1=nn.Parameter(torch.randn(numIn, numHidden, requires_grad=True, dtype=torch.double)*0.01) b1=nn.Parameter(torch.randn(numHidden, requires_grad=True, dtype=torch.double)) w2=nn.Parameter(torch.randn(numHidden, numOut, requires_grad=True, dtype=torch.double)*0.01) b2=nn.Parameter(torch.randn(numOut, requires_grad=True, dtype=torch.double)) params=[w1, b1, w2, b2]
[i.shape for i in params]
[torch.Size([5, 16]), torch.Size([16]), torch.Size([16, 1]), torch.Size([1])]
입력층으로 부터 선형변환된 결과는 활성함수에 의해 비선형으로 전환됩니다. 최종적으로 출력층에서 다시 선형변환에 의해 출력됩니다. 그러므로 활성함수가 필요하며 선형, 비선형, 선형변환으로 구성되는 통합 함수가 요구됩니다. 여기서는 활성함수로 relu를 적용합니다.
def relu(x): a=torch.zeros_like(x) return torch.max(x, a) def net(x): x=x.reshape((-1, numIn)) H=relu(x@w1+b1) return(H@w2+b2)
비용함수로 MSE, 최적화를 위해 SGD를 적용합니다.
criterion = torch.nn.MSELoss() optimizer = torch.optim.SGD(params, lr=1e-3) for t in range(2000+1): for batch in trLoad: x_tr, y_tr=batch y_pred = net(x_tr) loss = criterion(y_pred, y_tr) optimizer.zero_grad() loss.backward() optimizer.step() if t % 100 == 0: print(t, loss.item())
0 1.1390271499897222 100 0.010576993488951124 ⁌ 1900 0.0007562829169548848 2000 0.0009340339575225656
댓글
댓글 쓰기