분류 전체보기
-
딥러닝 직접 구현하기 프로젝트 2-4차시 - Model 제작하기2020.06.23
-
딥러닝 직접 구현하기 프로젝트 0차시 - 프로젝트 설명2020.05.30
딥러닝 직접 구현하기 프로젝트 2-5차시 - Optimizer (Momentum, NAG) 구현하기 (1)
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
* 시작하기에 앞서, 해당 포스트는 "Gradient Descent Optimization Algorithms 정리" 포스팅
(http://shuuki4.github.io/deep%20learning/2016/05/20/Gradient-Descent-Algorithm-Overview.html)
을 참조하였습니다. 구현 중인 알고리즘에 대해 알고 싶다면, 위 포스팅에서 확인해 주세요.
또한, 이번 차시에 구현할 optimizer는 위 링크의 Momentum과 NAG를 구현할 것입니다.
* 저번 시간의 코드 그대로 이어집니다.
import numpy as np
import pandas as pd
from Model import Model
import Layers
import Optimizer
# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values
# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot
# initialize parameters
num_classes = y_train.shape[1]
hidden = 50
model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 32), name="w1")
model.addlayer(Layers.AddLayer(), input_size=32, name='b1')
model.addlayer(Layers.SigmoidLayer(), activation=True, name='sigmoid1')
model.addlayer(Layers.MulLayer(), input_size=(32, 10), name="w2")
model.addlayer(Layers.AddLayer(), input_size=10, name='b2')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')
optimizer = Optimizer.Momentum(batch_size=128, momentum=0.9, nesterov=True)
model.train(x_train, y_train, optimizer, 10000, 0.01)
위 코드는 원래 저번 코드에서 딱 두 부분만 바뀌었습니다. Optimizer를 import하였고, 새로 optimizer를 만들어 train하는 방식입니다.
또, Model.py의 train함수 부분을 다음과 같이 바꾸고 시작하겠습니다.
def train(self, x_train, y_train, optimizer, epoch, learning_rate):
optimizer.train(x_train, y_train, epoch, learning_rate, self)
전체적인 훈련은 Optimizer.py라는 코드에서 진행하도록 하고, 훈련시키는 방식에 대한 코드는 모두 Optimizer.py에 집어 넣는 방식입니다.
그리고, 위에서 바꾸기 전의 train 함수 부분을 복사해서, Optimizer.py에 다음과 같이 넣어봅시다.
import numpy as np
class SGD:
def __init__(self, batch_size):
self.x = None
self.y = None
self.model = None
self.batch_size = batch_size
def train(self, x_train, y_train, epoch, learning_rate, model):
for epochs in range(epoch):
batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
x = x_train[batch_mask]
y = y_train[batch_mask]
model.predict(x, y)
dout = model.layers[model.keys[-1]].backward()
for i in reversed(range(len(model.keys) - 1)):
key = model.keys[i]
dout = model.layers[key].backward(dout)
if key in model.params:
model.grads[key] = model.layers[key].grad
model.params[key] -= learning_rate * model.grads[key]
if epochs % (epoch / 10) == 0:
print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
print("LOSS on epoch %d : " % epochs, model.loss)
model.predict(x_train, y_train)
print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
print("Final train_LOSS : ", model.loss)
Optimizer.py에서는 다양한 optimization function (SGD, Momentum, NAG, ...)들이 클래스 형식으로 구현될 것입니다.
SGD의 구현은 그냥 동일하니, 바로 Momentum을 만들어 봅시다.
Momentum optimization은 기존의 SGD와 다르게, 미분값에 일종의 "가속도"를 붙여 주며 학습시킵니다.
이는 학습 시의 자잘한 local minima들을 피하기 위함인데, 실제로 SGD의 경우 작은 local minima들에 gradient가 갇히게 되는 경우도 존재하고, 빠져나온다 하더라도 그 안에서 오랜 시간 머무르게 됩니다.
따라서, 진행 방향에 가속도를 붙여주어 빠르게 local minima를 벗어나고, global minima에 다가가게 하는 것이죠.
Momentum의 식은 다음과 같습니다.
$$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $$
$$ \theta_t = \theta_{t-1} - v_t $$
(참고로, 위 식은 아래와 같이 부호를 살짝 바꾸어 쓴 것과 동일합니다.)
$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$
$$ \theta_t = \theta_{t-1} + v_t $$
이제, 각각의 파라미터들을 설명하겠습니다.
v는 velocity의 약어로, 파라미터가 이동할 속도 (방향과 속력)을 나타냅니다. 즉, 파라미터는 $v_t$만큼 이동하게 됩니다.
$\gamma$는 momentum의 hyperparameter인데, 0과 1 사이의 값을 넣습니다. (보통 0.9로 많이 넣습니다.) 해당하는 값이 높으면 가속도가 크고, 값이 낮으면 가속도가 낮습니다.
$\eta$는 learning rate의 값입니다.
$\theta$는 학습시킬 parameter (w1, w2, b1, b2, ..)입니다.
$\nabla_\theta J(\theta_{t-1})$ 는 현재 학습시킬 parameter에 대한, 오차함수인 $J(\theta)$의 미분값을 의미합니다.
이제 생각해야 할 것은 어떻게 velocity를 구현할지만 고민하면 됩니다.
class Momentum:
def __init__(self, batch_size, momentum):
self.x = None
self.y = None
self.model = None
self.batch_size = batch_size
self.momentum = momentum
def train(self, x_train, y_train, epoch, learning_rate, model):
velocity = {}
for p in model.params:
velocity[p] = np.zeros(model.params[p].shape)
for epochs in range(epoch):
batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
x = x_train[batch_mask]
y = y_train[batch_mask]
model.predict(x, y)
dout = model.layers[model.keys[-1]].backward()
for i in reversed(range(len(model.keys) - 1)):
key = model.keys[i]
dout = model.layers[key].backward(dout)
if key in model.params:
model.grads[key] = model.layers[key].grad
velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
model.params[key] -= velocity[key]
if epochs % (epoch / 10) == 0:
print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
print("LOSS on epoch %d : " % epochs, model.loss)
model.predict(x_train, y_train)
print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
print("Final train_LOSS : ", model.loss)
위 코드의 대부분은 SGD와 동일합니다.
특이할 부분만 살펴보자면,
1. __init__에 momentum을 입력한다. ($\gamma$)
2. 변수 velocity가 새로 생겼고, grad를 바로 학습시키는 대신 velocity의 연산을 한 후에 학습시킨다.
정도로만 보면 되겠습니다.
우선 momentum의 hyperparameter (변수 momentum)은 이 optimizer에만 사용될 것이므로, 클래스에 선언하였습니다.
velocity = {}
for p in model.params:
velocity[p] = np.zeros(model.params[p].shape)
velocity는 다른 grads나 params같이 딕셔너리로 만들었습니다.
그리고 model.params의 크기(model.grads의 크기와 동일함)만큼 그대로 0으로 초기화해서 만들어 줍니다.
$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $ 였으므로, 초기값은 0이 되어야 그 뒤의 가속도 값을 구할 수 있기 때문입니다.
model.grads[key] = model.layers[key].grad
velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
model.params[key] -= velocity[key]
그리고, 위에서 언급했던 식과 동일하게 구현해 주면 됩니다.
momentum은 이렇게나 구현이 간단합니다.
다음은 NAG (Nesterov Accelerated Gradient)입니다.
NAG는 기본 틀이 momentum과 동일하기 때문에, 보통 다른 라이브러리에서도 SGD 안에 momentum과 nesterov를 넣어서 하나로 만듭니다.
그래서, NAG를 구현할 때는 momentum에서 nesterov를 True로 할지, False로 할지만 정해서 NAG를 적용시킬지 아닐지 결정하는 방식으로 가겠습니다.
(사실 원래 위의 momentum도 SGD에 넣어서 하려 했는데, 그냥 느낌 내려고 뺐습니다 ㅎㅎ)
NAG는 위의 momentum보다는 조금 까다로운 면이 있습니다.
momentum은 그냥 가속도만 붙여서 간다면, NAG는 가속이 붙은 방향으로 갔을 때의 미분값까지 계산해야 합니다.
우선 수식으로 볼까요?
$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$
$$ \theta_t = \theta_{t-1} + v_t $$
(파라미터들은 모두 위와 동일하므로 설명하진 않겠습니다.)
이게 무슨 뜻인고 하니, "가속도가 붙은 방향으로 그대로 돌진하지 말고, 돌진했을 때를 생각해서 적절하게 가자" 하는 것입니다.
위의 momentum만을 활용하게 되면, local minima도 빠르게 빠져나올 수 있지만, 사실 global minima에서도 빠져나올 수 있다는 단점도 있고, 가속도가 너무 크게 붙었을 경우 제동이 쉽지 않다는 단점도 있습니다.
하지만 미리 "내가 이 방향으로 갔을 때, 제동을 걸어야 할까?" 하는 것을 $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$로 계산해 줄 수 있는 것입니다.
하지만, NAG도 위의 momentum처럼 식만 보고 구현할 수만 있다면 정말 좋았겠으나 그렇게 간단하지는 않습니다.
우리가 구현한 Layer들은 오차함수를 각각의 파라미터에 대해서만 미분 가능하지, $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$의 값을 계산할 수는 없기 때문입니다.
물론, NAG용 파라미터를 하나 더 만들어서 $\theta = \theta - v_t$로 만들어서 미분을 처음부터 다시 하고 ... 하면 되겠습니다...만
그러면 한번 연산할 것이 두배가 되고, 그러면 애초에 시간복잡도가 (거의) 두배가 되어 버립니다.
조금 더 효율적인 방법은 없을까요?
*혹시라도 아래 설명이 이해가 잘 안된다면, 제가 참고한 아래 논문들의 원문을 읽어보시는 것을 추천드립니다.
https://arxiv.org/pdf/1212.0901v2.pdf (3.5. Simplified Nesterov Momentum)
https://www.cs.utoronto.ca/~ilya/pubs/ilya_sutskever_phd_thesis.pdf
자, 아까 위에 언급했던 NAG의 수식은 다음과 같았습니다.
$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$
$$ \theta_t = \theta_{t-1} + v_t $$
그런데, 이는 위에서 언급했던 다음과 같은 momentum의 모습과 상당히 유사합니다.
$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$
$$ \theta_t = \theta_{t-1} + v_t $$
우리의 목적은 위의 NAG의 수식을 위의 momentum과 같이 theta에 대한 미분값만으로 계산할 수 있도록 하는 것입니다.
일단, $ \theta_{t-1} + \gamma v_{t-1} $ 를 $k_{t-1}$라는 새로운 파라미터로 생각해 봅시다.
그러면 위의 NAG 식은 다음과 같이 쓸 수 있습니다.
$$ v_t = \gamma v_{t-1} - \eta\nabla_k J(k_{t-1})$$
$$ \theta_t = \theta_{t-1} + v_t $$
$$ \theta_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) $$
이렇게 하면 원래 momentum의 velocity update와 동일한 식을 만들 수 있었습니다.
또한, $k_t$의 식을 조금 써본다면...
$$k_t = \theta_t + \gamma v_t $$
$$k_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) + \gamma v_t $$
$$k_t = k_{t-1} + \gamma v_t - \eta\nabla_k J(k_{t-1}) $$
자, 이렇게 $\theta$를 대체할 $k$라는 파라미터의 update식을 구할 수 있습니다!
또한, 아까 언급했듯 $k_{t-1} = \theta_{t-1} + \gamma v_{t-1}$이었습니다.
그런데 우선 최초의 velocity값인 $v_1$은 0이므로, $t=1$일 때의 $k_t$값은 $\theta_t$의 값과 동일합니다.
그리고, optimal convergence ($v_{t-1}=0$인 지점)에서는 $\theta_t$와 $k_t$는 완벽하게 동일합니다.
그러므로 우리는 원래의 파라미터 $\theta$를 우리가 새로 만든 파라미터 $t$로 근사가 가능합니다.
즉, update rule을 다음과 같이 정의할 수 있습니다.
$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1})$$
$$ \theta_t = \theta_{t-1} + \gamma v_t - \eta\nabla_\theta J(\theta_{t-1}) $$
이제 이것을 코드로 구현해 봅시다.
class Momentum:
def __init__(self, batch_size, momentum, nesterov=False):
self.x = None
self.y = None
self.model = None
self.batch_size = batch_size
self.momentum = momentum
self.nesterov = nesterov
def train(self, x_train, y_train, epoch, learning_rate, model):
velocity = {}
for p in model.params:
velocity[p] = np.zeros(model.params[p].shape)
for epochs in range(epoch):
batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
x = x_train[batch_mask]
y = y_train[batch_mask]
model.predict(x, y)
dout = model.layers[model.keys[-1]].backward()
for i in reversed(range(len(model.keys) - 1)):
key = model.keys[i]
dout = model.layers[key].backward(dout)
if key in model.params:
model.grads[key] = model.layers[key].grad
if self.nesterov:
velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
else:
velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
model.params[key] += velocity[key]
if epochs % (epoch / 10) == 0:
print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
print("LOSS on epoch %d : " % epochs, model.loss)
model.predict(x_train, y_train)
print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
print("Final train_LOSS : ", model.loss)
위의 코드에서 바뀐 점만을 집중해서 봐 주시길 바랍니다.
1. __init__에 nesterov=False와, self.nesterov = nesterov를 넣어줌으로써 momentum 클래스에 nesterov를 키고 끌 수 있도록 했습니다.
2. if self.nesterov: 로 nesterov를 켰을 경우에 nesterov를 적용시켜 학습시킵니다.
for i in reversed(range(len(model.keys) - 1)):
key = model.keys[i]
dout = model.layers[key].backward(dout)
if key in model.params:
model.grads[key] = model.layers[key].grad
if self.nesterov:
velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
else:
velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
model.params[key] += velocity[key]
이 부분만 다시 자세히 보도록 하겠습니다.
if 구문으로 nesterov가 True일 때와 False일 때를 나누어서 계산합니다.
nesterov가 False인 경우에는 당연히 위의 Momentum과 동일한 식으로 나오게 됩니다.
nesterov가 True인 경우엔, 위에서 언급한 식 그대로 코드로 구현하시면 됩니다.
테스트하고 싶으시다면, 원래 코드에서 변수로 nesterov = True를 넣어주시면 됩니다.
이렇게, Momentum과 NAG의 구현을 마무리하도록 하겠습니다.
나머지 Adagrad, Adadelta, RMSProp, Adam은 다음 시간에 구현해 보도록 합시다!
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-6차시 - 가중치 초기화, 정규화, 드롭아웃 (0) | 2020.07.29 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-5차시 - Gradient Descent Optimizer 구현하기 (2) (2) | 2020.07.07 |
딥러닝 직접 구현하기 프로젝트 2-4차시 - Model 제작하기 (0) | 2020.06.23 |
딥러닝 직접 구현하기 프로젝트 2-3차시 - Activation Function (활성화 함수) 구현하기 (2) | 2020.06.17 |
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 2-4차시 - Model 제작하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
원래는 바로 Optimizer를 제작하려 했으나, 생각해 보니 코드가 이래서는 바로 들어가기는 좀 그럴 것 같습니다.
따라서 이번에는 코드를 대충 정리를 하고 "예쁘게" 만들어 보겠습니다.
(이론 설명같은 건 없다는 뜻)
그냥 어떤 부분이 바뀌었는지, 어떻게, 왜 바꾸었는지 간결하게만 설명하고 넘어가겠습니다.
import numpy as np
import pandas as pd
from Model import Model
import Layers
# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values
# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot
# initialize parameters
num_classes = y_train.shape[1]
hidden = 50
일단 ModelTest.py (테스트용 파일)을 설명하겠습니다.
저번과 아주 동일한 훈련 방식을 거칠 것이니, 데이터도 그대로 가져옵니다. (복붙)
model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 32), name="w1")
model.addlayer(Layers.AddLayer(), input_size=32, name='b1')
model.addlayer(Layers.SigmoidLayer(), activation=True, name='sigmoid1')
model.addlayer(Layers.MulLayer(), input_size=(32, 10), name="w2")
model.addlayer(Layers.AddLayer(), input_size=10, name='b2')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')
model.train(x_train, y_train, 10000, 0.01, 128)
달라진 부분은 이 부분입니다.
직접 데이터를 하나하나 넣어주는 것이 아니라, Model이라는 모듈을 만들어서 예쁘게 만들어 주자는 것이 주요 내용입니다.
import numpy as np
from Layers import *
class Model:
def __init__(self):
self.params = {}
self.grads = {}
self.keys = []
self.layers = {}
self.num = 0
self.loss = None
self.pred = None
def addlayer(self, layer, activation=False, input_size=None, name=None):
if name is None:
name = str(self.num)
self.keys.append(name)
self.num += 1
self.layers[name] = layer
if not activation:
self.params[name] = np.random.uniform(-1, 1, input_size)
self.layers[name].param = self.params[name]
def predict(self, x, y):
for i in range(len(self.keys) - 1):
key = self.keys[i]
x = self.layers[key].forward(x)
self.loss = self.layers[self.keys[-1]].forward(x, y)
self.pred = softmax(x)
def train(self, x_train, y_train, optimizer, epoch, learning_rate, batch_size):
for epochs in range(epoch):
batch_mask = np.random.choice(x_train.shape[0], batch_size)
x = x_train[batch_mask]
y = y_train[batch_mask]
self.predict(x, y)
dout = self.layers[self.keys[-1]].backward()
for i in reversed(range(len(self.keys) - 1)):
key = self.keys[i]
dout = self.layers[key].backward(dout)
if key in self.params:
self.grads[key] = self.layers[key].grad
self.params[key] -= learning_rate * self.grads[key]
if epochs % (epoch / 10) == 0:
print("ACC on epoch %d : " % epochs, (self.pred.argmax(1) == y.argmax(1)).mean())
print("LOSS on epoch %d : " % epochs, self.loss)
Model.py 부분입니다.
원래 Main에 있었던 grads, params 등등을 모두 이 Model 안에 넣었습니다.
그 뒤, addlayer로 Layer을 모델에 각각 추가하는 방식으로 모델의 레이어가 구현되도록 하였습니다.
이 때, activation 여부를 확인해서 activation function인 경우에는 grads와 params가 존재하지 않게 막아주었습니다.
(저번 코드의 if 'Relu'... 부분을 바꾼 것입니다.)
predict는 원래 train하는 과정에서의 forward 하는 과정을 따 온 함수입니다.
원래는 train 안에 그대로 넣고자 하였으나, model에서 직접 predict를 해야 할 일이 있을 것 같아서 따로 뺐습니다.
그리고 train 부분은 원래 훈련시키는 부분과 매우 유사합니다.
차이점은, lastlayer를 따로 빼지 않는 대싱 반복문을 range(len(self.keys)-1)로 바꾸어서, 마지막 layer은 따로 빼서 연산하도록 했습니다.
기반은 SGD로 구현하였으나, 다음 차시에 바로 train을 Optimizer 모듈에 구현하게 될 것 같습니다.
그래서 다음 차시에는, SGD 말고도 RMSPROP과 같은 다른 훈련 방식을 사용해 보도록 하겠습니다.
class MulLayer:
def __init__(self, param=None):
self.x = None
self.param = param
self.grad = None
def forward(self, x):
self.x = x
return x.dot(self.param)
def backward(self, dout):
self.grad = np.dot(self.x.T, dout)
return np.dot(dout, self.param.T)
class AddLayer:
def __init__(self, param=None):
self.x = None
self.param = param
self.grad = None
def forward(self, x):
self.x = x
return x + self.param
def backward(self, dout):
self.grad = dout.mean()
return dout
이 부분은 Layers.py 부분입니다.
MulLayer와 AddLayer에서 w, b를 그냥 param으로 통일하고, __init__시에 직접 parameter를 집어넣지 않아도 되게 만들었습니다.
param으로 이름을 통일한 것은 반복문에서의 train때의 편의성을 위함이고, __init__시에 직접 param을 집어넣지 않아도 되게 만든 것은 Model에서의 AddLayer를 만들기 위함입니다.
(위에서 보시면 아시겠지만, model.addlayer()부분에서 MulLayer과 AddLayer에 따로 params를 집어넣지 않고, Input_size만을 넣어서 자동으로 랜덤 값이 들어가도록 했습니다.)
이렇게 코드를 갈아엎어 보았습니다.
다음 시간에는 드디어, Optimizer를 구현해 보겠습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-5차시 - Gradient Descent Optimizer 구현하기 (2) (2) | 2020.07.07 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-5차시 - Optimizer (Momentum, NAG) 구현하기 (1) (4) | 2020.06.30 |
딥러닝 직접 구현하기 프로젝트 2-3차시 - Activation Function (활성화 함수) 구현하기 (2) | 2020.06.17 |
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 2-3차시 - Activation Function (활성화 함수) 구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
이번에는 ReLU 계열의 활성화 함수를 구현해 보겠습니다.
위 그래프는 ReLU 계열의 활성화 함수를 그래프로 나타낸 것입니다.
ReLU, Leaky ReLU, ELU 모두 입력값이 0보다 크면 그대로 y=x를 사용하지만,
0보다 작을때는 ReLU는 그냥 모든 입력에서 0, Leaky ReLU는 약 0.001x, ELU는 $e^x - 1$을 나타냅니다.
그렇다면 바로 Layers.py 구현부로 가서 활성화 함수들을 구현해 봅시다.
이번 시간에 구현할 것들은 좀 쉬워서, 사실 안보고도 하실 수 있을 겁니다. 글을 보지 않고, 스스로 한번 만들어 보셔도 좋겠습니다.
class ReLU:
def __init__(self):
self.out = None
def forward(self, z):
self.out = z
self.out[self.out <= 0] = 0
return self.out
def backward(self, dout):
self.out[self.out > 0] = 1
return self.out * dout
우선, ReLU 함수입니다.
z값이 0보다 클때는 그대로 z를 출력하고, 0보다 작을 때는 0을 출력합니다.
forward의 구현은 그것을 그대로 따라갔습니다.
backward의 경우, 당연히 ReLU함수가 0보다 클 때는 1, 작을 때는 0이 됩니다만...
ReLU가 0일 경우 미분값을 어떻게 처리해 주어야 할까요?
사실 원래는 0일 경우 미분값을 구할 수는 없지만, 보통 그냥 0으로 만들어 줍니다.
딱히 큰 의미가 있는 것은 아니고, 어차피 뭘 해도 정확도에 큰 영향이 없기 때문입니다.
class LeakyReLU:
def __init__(self):
self.out = None
def forward(self, z):
self.out = z
self.out[self.out <= 0] *= 0.001
return self.out
def backward(self, dout):
self.out[self.out > 0] = 1
self.out[self.out <= 0] = 0.001
return self.out * dout
다음은 Leaky ReLU입니다.
위의 ReLU를 그대로 복사한 뒤, self.out <= 0 부분에서 0을 만들어 버리는 대신, 0.001을 곱해주면 됩니다.
backward도 마찬가지로, 0보다 작은 경우의 미분값은 당연히 0.001이 되게 됩니다.
class ELU:
def __init__(self):
self.out = None
def forward(self, z):
self.out = z
self.out[self.out <= 0] = np.exp(self.out[self.out <= 0]) - 1
return self.out
def backward(self, dout):
self.out[self.out > 0] = 1
self.out[self.out <= 0] += 1
return self.out * dout
ELU도 전혀 다를 것이 없습니다.
forward 함수에서는 값이 0보다 작으면 $e^x - 1$을 출력값으로 내놓습니다.
그리고 해당 값을 미분하면 $e^x$이 나오므로, 그냥 원래 $e^x - 1$이었던 값에다가 1만 더해주면, 그것이 미분값이 됩니다.
사실 이번 활성화 함수 제작은 여기서 끝입니다.
보통 실제 훈련 시킬 때는 ReLU계열의 활성화 함수만 사용하기 때문에, 이렇게만 준비해 봤습니다.
하지만, 다른 Activation function도 위의 방식을 따라만 간다면 만들기에 크게 어렵지 않을 겁니다.
다른 활성화 함수를 구현해 보고 싶으시다면, 직접 forward와 backward를 구현하는 것도 꽤 재밌는 일입니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-5차시 - Optimizer (Momentum, NAG) 구현하기 (1) (4) | 2020.06.30 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-4차시 - Model 제작하기 (0) | 2020.06.23 |
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
자, 드디어 Multi-Layer로 향할 차례입니다!
저번에 한번 구현한 바가 있던 MNIST 손글씨 인식 프로그램을 이번엔 Multi-Layer로 구현하도록 하겠습니다.
이번에도 이론 설명보단 코드 설명에 집중되어 있으니, 가벼운 마음으로 읽고 복붙하시면 되겠습니다.
import numpy as np
import pandas as pd
import Layers
# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values
# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot
# initialize parameters
num_classes = y_train.shape[1]
hidden = 50
keys = ['w1', 'b1', 'sigmoid', 'w2', 'b2']
params = {}
params['w1'] = np.random.uniform(-1, 1, (x_train.shape[1], hidden))
params['b1'] = np.random.uniform(-1, 1, (hidden))
params['w2'] = np.random.uniform(-1, 1, (hidden, num_classes))
params['b2'] = np.random.uniform(-1, 1, (num_classes))
layers = {}
layers['w1'] = Layers.MulLayer(params['w1'])
layers['b1'] = Layers.AddLayer(params['b1'])
layers['sigmoid'] = Layers.SigmoidLayer()
layers['w2'] = Layers.MulLayer(params['w2'])
layers['b2'] = Layers.AddLayer(params['b2'])
lastlayer = Layers.SoftmaxLayer()
grads = {}
# initialize hyperparameters
learning_rate = 0.01
epochs = 10000
batch_size = 128
우선, 파라미터 초기화 및 데이터 불러오기 등은 지금까지와 아주, 매우, 비슷합니다.
바뀐 점으로는, 기존에 w, b로 사용했던 인자들 대신 params라는 dictionary를 활용해서 학습을 진행하게 됩니다.
w와 b값이 여러개가 될 수 있기 때문이죠.
그리고, Perceptron에서 사용되는 Hidden Layer의 개수를 "hidden"이라는 변수를 통해 선언했습니다.
이 값을 늘리게 되면 연산은 느려지지만 비교적 정확하게 학습이 될 가능성이 높고, 낮춘다면 연산은 빨라지지만 조금 부정확한 학습이 될 수 있겠죠.
또, layer 사이에 sigmoid로 activation function을 넣었고, 더 빠른 학습을 위해 mini-batch를 활용한 SGD를 사용하도록 하겠습니다.
이번에 새로 추가된 Layer들을 소개하면, 각각 "SigmoidLayer"과 "SoftmaxLayer"입니다.
Layers.py에 가서 새로 추가된 Layer들을 확인해 보도록 합시다.
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
def softmax(a):
C = np.max(a)
exp_a = np.exp(a - C)
if a.ndim == 1:
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
else:
sum_exp_a = np.sum(exp_a, 1)
sum_exp_a = sum_exp_a.reshape(sum_exp_a.shape[0], 1)
y = exp_a / sum_exp_a
return y
def cross_entropy_loss(y, t):
C = 1e-7
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + C)) / batch_size
일단 당연하게도, sigmoid, softmax, cross_entropy_loss 함수를 선언해야 합니다.
참고로, 위 코드는 저번 코드와 이어지므로, numpy 선언 등은 제외하도록 하겠습니다.)
(혹시라도 위 코드에 대한 설명이 보고 싶으시다면, 이 포스팅과 이 포스팅을 참고해 주세요.)
class SigmoidLayer:
def __init__(self):
self.out = None
def forward(self, z):
self.out = sigmoid(z)
return self.out
def backward(self, dout):
return (1 - self.out) * self.out * dout
우선, 비교적 간단한 SigmoidLayer부터 확인해 봅시다.
당연하게도, forward 함수에는 들어온 값을 sigmoid 함수를 거쳐 return해 줍니다.
그리고, Logistic Regression 구현부에서도 설명했듯, Sigmoid 함수를 미분하면 $pred * (1 - pred)$가 되므로, 위와 같은 코드가 나옵니다.
class SoftmaxLayer:
def __init__(self):
self.loss = None
self.y = None
self.t = None
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_loss(self.y, self.t)
return self.loss
def backward(self):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
다음으로, SoftmaxLayer입니다.
이 Layer에서는 Softmax함수 뿐만 아니라, Cross Entropy Loss까지 한번에 구현하였습니다.
그래서, forward 함수에서는 self.y에 softmax함수를 지나친 값, self.t에는 입력된 정답 라벨값을 넣어서, loss값을 구합니다.
그리고 backward함수에서는 저번 Softmax Classification 구현부에서 설명했던대로, y-t값을 뱉어냅니다.
사실 Layer 구현은 이론적인 부분이 없어서 그냥 설명을 빨리 넘기겠습니다. 혹시라도 이해가 안되신다면, 댓글로 남겨주시면 감사하겠습니다.
for epoch in range(epochs):
batch_mask = np.random.choice(x_train.shape[0], batch_size)
x = x_train[batch_mask]
y = y_train[batch_mask]
for key in keys:
x = layers[key].forward(x)
loss = lastlayer.forward(x, y)
if epoch % (epochs / 10) == 0:
pred = Layers.softmax(x)
print("ACC on epoch %d : " % epoch, (pred.argmax(1) == y.argmax(1)).mean())
print("LOSS on epoch %d : " % epoch, loss)
dout = lastlayer.backward()
for key in reversed(keys):
dout = layers[key].backward(dout)
if key != 'sigmoid':
grads[key] = layers[key].grad
params[key] -= learning_rate * grads[key]
이제 본격적으로 학습을 시켜줍시다.
SGD를 사용한다고 했으니 batch_mask를 만들어서 계산해 주고, keys에 저장했던 key값을 토대로 지속적으로 forward를 해 나갑니다.
그렇게 forward를 다 한 뒤에, lastlayer (즉 softmax layer)에서 loss 값을 얻어냅니다.
그리고, 그 아래에서는 backward를 실행시켜 오차역전파법을 계산해 줍니다.
참고로, 아래 고드의 if key != 'sigmoid' 부분은 조금 예쁘진 않지만, 추후에 activation function을 구현하면서 같이 개선해 나갈 코드입니다.
# load data
test = pd.read_csv("../Data/MNIST_data/mnist_test.csv")
y_test = test["label"]
x_test = test.drop("label", 1)
x_test = x_test.values / x_test.values.max()
y_test = y_test.values
# y to one-hot
one_hot = np.zeros((y_test.shape[0], y_test.max() + 1))
one_hot[np.arange(y_test.shape[0]), y_test] = 1
y_test = one_hot
x = x_test
for key in keys:
x = layers[key].forward(x)
pred = Layers.softmax(x)
print("ACC : ", (pred.argmax(1) == y_test.argmax(1)).mean())
이제 학습이 잘 되었는지, 마지막으로 test할 시간입니다!
test 코드는 pred값을 layers를 통해서 얻는 것 빼고는 저번 softmax classification과 동일합니다.
이렇게 Multi-Layer Softmax Classification을 직접 구현해 보았습니다.
저번에 구현했던 single-layer 보다 조금 더 좋은 성능을 내는 것을 확인하실 수 있을 것입니다.
한번 layer를 늘리거나, hidden의 크기를 늘리고 줄이면서 더욱 높은 성과를 낼 수 있을지 해 보는것도 재밌을 것입니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-4차시 - Model 제작하기 (0) | 2020.06.23 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-3차시 - Activation Function (활성화 함수) 구현하기 (2) | 2020.06.17 |
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
지금까지는 간단한 머신 러닝만 다뤘다면, 이제는 진짜 다중 계층의 딥 러닝을 다룰 시간입니다!
그 다중 계층을 향한 가장 첫 번째 순서는, 바로 Single-Layer Gradient Descent입니다.
직접 Layer를 구현해 보고, 제대로 학습이 되는지 보도록 합시다.
이번에는 이미 했던 계산들만 계속 나오니, 계산은 필요가 없고, 어떻게 구현하면 될지만 보도록 합시다.
import numpy as np
import Layers
train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))
learning_rate = 0.001
epoch = 10000
우선 데이터는 저번 Multi-Variable Gradient Descent 부분과 동일하게 진행하도록 해 봅시다. (근본적으로 이번에 할 것이 그것과 똑같이 때문이죠.)
keys = ['w', 'b']
layers = {}
layers['w'] = Layers.MulLayer(w)
layers['b'] = Layers.AddLayer(b)
lastlayer = Layers.MSELayer(train_y)
grads = {}
이제 훈련할 때의 변수를 선언해 봅시다.
layers에는 당연히 layer들을 집어 넣을 것이고, keys에는 훈련의 코드를 조금 더 간결하게 하기 위한 'w','b'를 넣어 줍시다.
그러면 이제 Layers 함수에 가서 Layers 함수를 어떻게 구현할지 확인해 볼까요?
import numpy as np
class MulLayer:
def __init__(self, w):
self.x = None
self.w = w
self.grad = None
def forward(self, x):
self.x = x
return x.dot(self.w)
def backward(self, dout):
self.grad = np.dot(self.x.T, dout)
return np.dot(dout, self.w.T)
class AddLayer:
def __init__(self, b):
self.x = None
self.b = b
self.grad = None
def forward(self, x):
self.x = x
return x + self.b
def backward(self, dout):
self.grad = dout.mean()
return dout
class MSELayer:
def __init__(self, y):
self.x = None
self.y = y
self.loss = None
def forward(self, x):
self.x = x
self.loss = np.square(x-self.y).mean()
return self.loss
def backward(self):
return self.x - self.y
일단 지금까지의 Layer 모듈에는 이렇게 들어있습니다.
각각의 클래스 구조를 보시면, 레이어들은 __init__, forward, backward의 세 가지 함수로 이루어져 있습니다.
우선 __init__에는 해당 함수가 필요로 하는 값들을 지정하고, 선언해 줍니다.
가령 MulLayer같은 경우에는 w 파라미터를 init해주고, AddLayer같은 경우에는 b 파라미터는 init해줍니다.
forward는 순방향 계산입니다. 쉽게 말해, pred 값을 찾아가는 것입니다.
저번 Gradient Descent에서 pred = x.dot(w) + b 라는 식으로 prediction값을 구했다면, 이제는 각각의 layer에 대한 forward 연산으로 해당 값을 구할 것입니다.
backward는 역전파법 계산입니다. 이 연산을 통해 각각의 layer에 대한 gradient 값을 알아갈 수 있습니다.
그리고, 이 gradient 값들을 통해 인공지능 학습을 해야 하므로, 이 값들을 각각의 layer에 저장해 주어야 합니다. (grad 변수의 역할)
이제 클래스를 하나씩 순서대로 설명하도록 하겠습니다.
class MulLayer:
def __init__(self, w):
self.x = None
self.w = w
self.grad = None
def forward(self, x):
self.x = x
return x.dot(self.w)
def backward(self, dout):
self.grad = np.dot(self.x.T, dout)
return np.dot(dout, self.w.T)
MulLayer, 즉 곱연산 레이어를 먼저 설명하도록 하겠습니다.
이 Layer에서는 forward, 즉 순방향 연산 때는 input 값인 x에 대해 x*w (x.dot(w))의 값을 전달합니다.
그리고, x * w를 w에 대해 미분하면 x가 나오므로, gradient는 x가 됩니다.
그런데, 왜 x에 dout을 곱해주는 걸까요?
우리는 다중 레이어로 구성된 딥 러닝 모델을 만들 것이기 때문입니다.
나중에 전체 코드를 보시면 아시겠지만, 가장 첫 dout에서는 MSELayer, 즉 Loss 값에 대한 Layer의 pred값에 대한 미분값이 들어갑니다.
그리고, Chain Rule $ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} $ 에 의하여, 역전파법 계산을 하면서 dout을 계속 곱해주면 됩니다.
그렇기에, 위 코드에서도 backward 함수에서 x와 dout을 곱해준 값이 grad가 되는 것입니다.
class AddLayer:
def __init__(self, b):
self.x = None
self.b = b
self.grad = None
def forward(self, x):
self.x = x
return x + self.b
def backward(self, dout):
self.grad = dout.mean()
return dout
AddLayer도 위와 비슷합니다.
AddLayer, 즉 합에서는 b에 대해 미분하면 1이 나오므로, backward는 dout 그대로 나오는 것을 알 수 있습니다.
class MSELayer:
def __init__(self, y):
self.x = None
self.y = y
self.loss = None
def forward(self, x):
self.x = x
self.loss = np.square(x-self.y).mean()
return self.loss
def backward(self):
return self.x - self.y
다음은 MSELayer입니다.
MSE가 뭐지? 하시는 분이 있었을 것도 같습니다. 하지만, 사실 이미 MSE는 본 적이 있습니다.
Gradient Descent에서 $\frac{1}{m}(pred - y)^2$로 loss 값을 설정한 적이 있었죠?
이것을 바로 Mean Squared Error, 줄여서 MSE라 합니다.
그리고 이를 pred에 대해 미분하면 $pred - y$값이 나온다는 것은 이미 계산해 본 적이 있습니다.
따라서, forward에는 loss값의 계산 식이 들어가고, backward에는 $pred - y$, 즉 위 함수에선 x - y가 들어갑니다.
이렇게 Layer 의 함수에 대한 설명은 끝내고, 나머지 메인 코드 부분을 끝장내러 가봅시다.
for epoch in range(epoch):
x = train_x
y = train_y
for key in keys:
x = layers[key].forward(x)
loss = lastlayer.forward(x)
if epoch % 100 == 0:
print("err : ", loss)
dout = lastlayer.backward()
for key in reversed(keys):
dout = layers[key].backward(dout)
grads[key] = layers[key].grad
db = layers['b'].grad
dw = layers['w'].grad
w -= learning_rate * dw
b -= learning_rate * db
아까 전에 위에서 언급했듯, keys를 사용하여 forward와 backward, 그리고 grad값 저장을 쉽게 할 수 있습니다.
사실 이번에는 w와 b가 하나씩밖에 나오지 않았기에 db와 dw의 초기화를 저렇게 수동으로 했지만, 저 dw와 db의 초기화도 for문 안에 넣을 수도 있습니다.
다음 시간에는, 이를 활용한 Multi-Layer Gradient Descent로 MNIST 데이터셋을 다시 한번 훈련시켜 보겠습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-3차시 - Activation Function (활성화 함수) 구현하기 (2) | 2020.06.17 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
이번 시간에는 Softmax Classification을 구현해 보고, 이를 MNIST 데이터셋으로 테스트 해보겠습니다.
혹시라도 MNIST 데이터셋이 조금 부담스럽다 하시는 분들은, 제 깃허브 (https://github.com/cdjs1432/DeepLearningBasic)에 작은 데이터인 Iris로 훈련하는 코드가 있으니, 확인해 보셔도 좋을 것 같습니다. (IrisTest.py)
들어가기에 앞서, 간단하게 Softmax 함수와 Cross-Entropy Loss가 무엇인지 먼저 알아봅시다.
우선, Softmax 함수는 다음과 같이 정의됩니다.
$$ y_i(z) = \frac{e^{z_i}}{\sum_{i=1}^N e^{z_i}} $$
여기서 z는 (적어도 이번 차시에서는) x.dot(w) + b입니다.
이 Softmax 함수를 지나온 값들의 합은 1이 되는데, 그렇기에 위의 값들을 "확률"로 생각해도 좋습니다.
이렇게만 말하면 조금 이해가 안될 테니, 그냥 코드로 알아보도록 합시다.
import numpy as np
import pandas as pd
import ComputeGrad
일단은 필요한 모듈을 모두 import합시다.
지금 당장은 안쓰더라도, 이번 시간에 모두 쓸 것들입니다.
def softmax(a):
C = np.max(a)
exp_a = np.exp(a - C)
if a.ndim == 1:
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
else:
sum_exp_a = np.sum(exp_a, 1)
sum_exp_a = sum_exp_a.reshape(sum_exp_a.shape[0], 1)
y = exp_a / sum_exp_a
return y
a = np.array([1.3, 2.4, 3.7])
print(softmax(a))
위 코드를 실행하면, [0.06654537 0.19991333 0.73354131] 라는 값이 출력될 것입니다.
사실 계산 부분은 위의 softmax의 수식과 동일하지만, C값이 눈에 들어옵니다.
이 C 값을 놓는 이유는, 만약 이 값 없이 입력인 a값이 너무 큰 값으로 들어오게 되면 오버플로우가 날 확률이 높기 때문입니다.
그러나 이렇게 a를 C로 빼 준 뒤 exp 연산을 하게 되면, 그렇게 오버플로우가 날 확률이 현저히 낮아집니다.
참고로, softmax 함수는 저런 식으로 연산 내부 값을 빼 준다고 하더라도 결과값이 동일하게 나오게 됩니다.
(왜 그런지는 직접 계산해 보시면서 해보셔도 재밌을 것 같습니다.)
그리고 직접 보시면 아시겠지만, 위 실행 결과를 더해보면 1이 나오게 됩니다. (물론 출력값은 소수점이 살짝은 잘리기에 완벽히 1이 되진 않습니다.)
그리고, 위의 값들을 "0번째 인덱스가 정답일 확률은 0.06, 2번째 인덱스가 정답일 확률은 0.73이겠군" 이라고 생각할 수 있습니다.
바로 이런 성질을 사용하여 Classification을 할 수 있는 것이죠.
그리고 a가 행렬로 입력될 경우, 각 행에서의 합이 1이어야 하므로, a.ndim==... 코드를 넣어서 경우를 나눠주었습니다.
(이 구현이 좀 예쁜 구현은 아닐수는 있겠지만, 그래도 시간복잡도는 비슷하니 뭐...)
다음은 Cross-Entropy Loss입니다.
이 오차 함수를 식으로 쓰면,
$$ E=-\sum_{i} t_i \ln y_i $$
가 됩니다.
여기서 t는 정답 label, y는 우리의 예측의 출력값이 됩니다.
이제 이를 직접 구현해 봅시다.
(위의 코드와 이어집니다.)
def cross_entropy_loss(y, t):
return -np.sum(t * np.log(y))
y = softmax(a)
print(y)
t = np.array([1, 0, 0])
print(cross_entropy_loss(y, t))
t = np.array([0, 1, 0])
print(cross_entropy_loss(y, t))
t = np.array([0, 0, 1])
print(cross_entropy_loss(y, t))
위 코드를 실행시키면,
2.7098713687418052
1.6098713687418051
0.309871368741805
라는 값이 나옵니다. 아까 전의 softmax(a), 즉 y값이 대략 [0.06, 0.19, 0.73] 이었던 것을 고려한다면, y가 가장 높은 인덱스인 0.73에 해당하는 loss값이 가장 낮으므로, 성공적으로 구현됐다고 생각할 수 있겠습니다.
그러나, 우리는 Cross Entropy Loss를 미니배치에 적용시켜서 학습을 할 것이므로, 위 함수를 다음과 같이 바꿔줍시다.
def cross_entropy_loss(y, t):
c = 1e-7
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + c)) / batch_size
위의 y.ndim==1은 y(혹은 t, 어차피 같은 shape로 들어와야 하므로 상관없음)가 2차원 배열이 아니라 1차원 배열일 때도 사용할 수 있게 하기 위한 코드입니다.
여기서의 c값은 np.log 연산을 할 때 오버플로우가 나오는 것을 방지해 주는 역할입니다.
log의 진수로 0이 들어오면 분명 오버플로우가 발생할테니까요.
자, 이제 데이터를 불러와 볼까요?
# load data
train = pd.read_csv("./Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values
# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot
# initialize parameters and hyperparameters
num_classes = y_train.shape[1]
w = np.random.uniform(-1, 1, (x_train.shape[1], num_classes))
b = np.zeros(num_classes)
learning_rate = 0.01
epoch = 10000
batch_size = 128
w, b = ComputeGrad.SoftmaxGD(x_train, y_train, w, b, learning_rate, epoch, batch_size)
아, 참고로 해당 데이터는 제 깃허브에 올려놓았으니 사용하셔도 되고, 아니면 다른 방식을 사용해서 MNIST를 불러와도 무방합니다.
간단히 코드를 설명하자면, load data에서는 당연히 데이터를 불러오고,
그리고, 그 아래에는 y_train을 one-hot vector로 만드는 코드가 있습니다.
이 글을 볼 사람들이라면 웬만하면 다 알겠지만 그래도 설명하자면, one-hot vector란 정답 클래스를 숫자로 4, 8, ... 이런 식으로 두는 것이 아니라, [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]과 같은 방식으로 두는 것입니다.
그렇다면 이제 SoftmaxGD를 구현해 봅시다.
우선, 수식을 먼저 다시 확인해 보겠습니다.
$$ y_i(z) = \frac{e^{z_i}}{\sum_{i=1}^N e^{z_i}} $$
$$ E=-\sum_{i} t_i \ln y_i $$
이 식을 바탕으로 Cross-Entropy Loss E를 w, b에 대해 미분한 값을 구해 봅시다.
우선, Cross-Entropy Loss E를 $z_j$에 미분한다 하면, 다음과 같은 식이 얻어집니다.
$$\frac{\partial E}{\partial z_j} = -\frac{\partial {\sum_{i} t_i \ln y_i} }{\partial z_j} = -\sum_{i} t_i \frac{\partial \ln y_i}{\partial z_j}$$
이 때, chain rule에 의하여, 위 식은 다음과 같이 변형이 가능합니다.
$$-\sum_{i} t_i \frac{\partial \ln y_i}{\partial z_j} = -\sum_{i} t_i \frac{\partial \ln y_i}{\partial y_i} \frac{\partial y_i}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} \frac{\partial y_i}{\partial z_j}$$
자, 이제 $\frac{\partial y_i}{\partial z_j}$를 구해 봅시다.
그런데 이 때, 조심해야 할 점이 있습니다.
그것은 바로 $\frac{\partial y_i}{\partial z_j}$를 계산할 때 j==i인 경우와 j!=i인 경우를 나누어서 생각해 주어야 한다는 것입니다.
일단, j와 i가 둘 다 k로 같은 경우로 계산을 해보도록 하겠습니다.
$$\frac{\partial y_k}{\partial z_k} = \frac{\partial \frac{e^{z_k}}{\sum_{i=1}^N e^{z_i}}}{\partial z_k} $$
여기서, $\sum_{i=1}^N e^{z_i}$를 S라고 치환한다면, $\frac{\partial S}{\partial z_k} = e^{z_k}$ 이므로,
$$\frac{\partial y_k}{\partial z_k} = \frac{\partial \frac{e^{z_k}}{S}}{\partial z_k} = \frac{e^{z_k}(S - e^{z_k})}{S^2}$$
가 됩니다.
그런데, 이 때 $\frac{e^{z_k}}{S} = y_k$이므로, 식을 다음과 같이 정리할 수 있습니다.
$$\frac{\partial y_k}{\partial z_k} = \frac{e^{z_k}(S - e^{z_k})}{S^2} = y_k(1 - y_k)$$
그러면, $\frac{\partial E}{\partial z_k}$는 다음과 같이 나오게 됩니다.
$$ \frac{\partial E}{\partial z_k} = \frac{\partial E}{\partial y_k} * \frac{\partial y_k}{\partial z_k} = -\frac{t_k}{y_k} * y_k(1 - y_k) = t_k(y_k - 1) $$
그리고, 만약 i!=j인 상황이라면, 계산은 다음과 같이 이루어집니다.
$$\frac{\partial y_i}{\partial z_j} = \frac{\partial \frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}}}{\partial z_j} $$
$$\sum_{i=1}^N e^{z_i} = S, \frac{\partial S}{\partial z_j} = e^{z_j}$$
$$\frac{\partial y_i}{\partial z_j} = \frac{\partial \frac{e^{z_i}}{S}}{\partial z_j} = \frac{0 - e^{z_i}e^{z_j}}{S^2} = {y_i}{y_j}$$
아주 훌륭합니다!
이제 위 두 식을 하나로 합치게 되면 다음과 같은 식이 완성됩니다.
$$\frac{\partial y_i}{\partial z_j} = y_i(1 \lbrace i==j \rbrace - y_j)$$
이 때, $1 \lbrace i==j \rbrace$ 는 i==j일때는 1, 아니면 0이라는 뜻입니다.
자, 이제 원래 구하고 있던 $\frac{\partial E}{\partial z_j}$을 구해보자면,
$$ \frac{\partial E}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} \frac{\partial y_i}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} y_i(1 \lbrace i==j \rbrace - y_j) = -\sum_{i} t_i(1 \lbrace i==j \rbrace - y_j) = -\sum_{i} t_i(1 \lbrace i==j \rbrace) + y_j\sum_{i} t_i$$
라는 식이 나옵니다!
이 때, 식 $-\sum_{i} t_i(1 \lbrace i==j \rbrace)$는 i==j 일 때 $-t_j$, i!=j 일 때 0으로 정의되므로 시그마가 사라지며 값 자체를 $-t_j$로 바꿀 수 있습니다.
또한, $t_i$는 단 하나의 정답 인덱스에만 1의 값을 가지고 다른 모든 인덱스에 대해선 0의 값을 가지기 때문에 $\sum_{i} t_i=1$입니다.
따라서, 위 식을 다시 정리해서 쓰면,
$$\frac{\partial E}{\partial z_j} = y_j - t_j$$
로 아름답게 정리가 됩니다!
드디어 끝이 보이네요!
마지막으로, 정말 원래 구하려 했던 식인 $\frac{\partial E}{\partial w}$ 와 $\frac{\partial E}{\partial b}$를 구해 보면,
$$\frac{\partial E}{\partial w} = \frac{\partial E}{\partial z_j}\frac{\partial z_j}{\partial w} = (y_j - t_j)x$$
$$\frac{\partial E}{\partial b} = \frac{\partial E}{\partial z_j}\frac{\partial z_j}{\partial b} = (y_j - t_j)$$
*(2021-06-04 수정: 설명과 index값 오류를 수정했습니다. 제보해 주셔서 감사드립니다.)
이제 위 식을 코드로 구현해 봅시다!!
def SoftmaxGD(x, y, w, b, learning_rate=0.01, epoch=100000, batch_size=128):
for epochs in range(epoch):
batch_mask = np.random.choice(x.shape[0], batch_size)
x_batch = x[batch_mask]
y_batch = y[batch_mask]
z = x_batch.dot(w) + b
pred = softmax(z)
dz = (pred - y_batch) / batch_size
dw = np.dot(x_batch.T, dz)
db = dz * 1.0
w -= dw * learning_rate
b -= (db * learning_rate).mean(0)
if epochs % (epoch / 10) == 0:
pred = softmax(x.dot(w) + b)
print("ACC : ", (pred.argmax(1) == y.argmax(1)).mean())
err = cross_entropy_loss(pred, y)
print("ERR : ", err)
return w, b
우선 코드를 보시면 batch가 눈에 띄실 건데요, MNIST는 60000개의 데이터로 이루어진 데이터셋이기에 SGD가 아닌 그냥 GD를 쓰면 너무 훈련이 오래 걸리기에, SGD를 적용하였습니다.
그 외의 나머지 부분은 딱히 설명할 부분이 없는 것 같네요. 수식을 코드에 직접 적용시켰을 뿐입니다.
위에서 hyperparameter를 바꾸지 않고 학습시켰다면, 아마 마지막에는 ACC가 대략 0.8이상 (80% 이상)이 나올 것입니다.
계층 하나만으로도 손글씨 인식률이 80%나 되다니!
그렇다면, 이제 test 데이터에서도 잘 작동하나 확인해 봅시다.
# load data
test = pd.read_csv("./Data/MNIST_data/mnist_test.csv")
y_test = test["label"]
x_test = test.drop("label", 1)
x_test = x_test.values / x_test.values.max()
y_test = y_test.values
# y to one-hot
one_hot = np.zeros((y_test.shape[0], y_test.max() + 1))
one_hot[np.arange(y_test.shape[0]), y_test] = 1
y_test = one_hot
pred = x_test.dot(w) + b
pred = softmax(pred)
print("ACC : ", (pred.argmax(1) == y_test.argmax(1)).mean())
이 부분의 코드는 그냥 윗부분 그대로 복사 붙여넣기 한 부분이니, 그냥 그대로 넣으시면 될 것 같습니다.
그리고, 결과를 확인해 보니 test 데이터에서도 좋은 결과가 나오는 것을 확인할 수 있습니다!
그러면 이제 이것으로 Softmax Classification의 구현을 마치도록 하겠습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-2차시 - Multi-Layer Softmax Classification 구현하기 (0) | 2020.06.16 |
---|---|
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-1차시 - Gradient Descent의 구현 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
이번 시간엔 Logistic Regression을 구현하고, 이를 breast cancer 데이터셋으로 테스트까지 해보도록 하겠습니다.
(30가지 데이터를 토대로, 해당 유방암이 악성인지 양성인지 확인하는 데이터셋)
import numpy as np
from sklearn import datasets
import ComputeGrad
일단 필요한 모듈 먼저 import해 줍시다.
원래는 직접 데이터셋을 다운받아서 해도 되겠지만, 편의를 위해 sklearn 모듈을 import하겠습니다.
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
그리고, Logistic Regression에 필수적인, 시그모이드 함수를 정의해 줍시다.
cancer = datasets.load_breast_cancer()
x = cancer.data
x /= x.mean()
y = cancer.target
w = np.random.uniform(-1, 1, x.shape[1])
b = 0
learning_rate = 0.0001
epoch = 10000
w, b = ComputeGrad.LogisticGD(x, y, w, b, learning_rate, epoch)
그리고, 이제 필요한 변수들을 모두 불러와 봅시다.
아까 import 해왔던 datasets에서 breast_cancer을 불러오고, 각각의 data와 target을 x, y에 집어넣어 줍시다.
(이 때, 당연히 data는 원래 x값에 들어갈 데이터를, target은 y값에 들어갈 데이터를 가지고 있습니다.)
그런데 이번 코드에는 저번과는 다르게 x값을 x의 평균으로 나누는 코드까지 같이 들어있습니다.
왜 이렇게 하는걸까요?
이유를 잠깐 간단히 설명해 보도록 하겠습니다.
만약 x값의 범위가 위아래로 너무 커버리면, w값과 곱할 때도 더욱 범위가 커지게 됩니다.
그러면, 한번에 너무 크게 dw값이 진동하게 되고, 이는 결국 learning_rate를 아주 큰 값으로 둔 것과 비슷한 결과를 낳게 됩니다.
(참고로, 사실 x를 그냥 평균으로 나눠버리는 것 보다 x값을 표준화 시키는 더욱 좋은 방법들이 많이 있지만, 그것은 나중에 언급하도록 하고 우선 지금은 이정도로 둡시다. 이정도도 충분히 잘 작동하거든요.)
아무튼, 그 뒤 w값과 b값, learning_rate와 epoch값을 초기화 해 줍니다.
이제, LogisticGD 함수를 확인하러 갑시다.
def LogisticGD(x, y, w, b=0, learning_rate=0.001, epoch=10000):
for epochs in range(epoch):
z = x.dot(w) + b
pred = sigmoid(z)
err = -np.log(pred) * y - np.log(1 - pred) * (1 - y)
err = err.mean()
"""
to minimize err --> differentiate pred
err = -ln(pred)*y - ln(1-pred)*(1-y)
--> -y/pred + (1-y) / (1-pred)
differentiate pred by z
pred = 1 / (1+e^-z)
--> e^z / (1+e^-z)^2
--> e^z / (1+e^-z) * 1 / (1+e^-z)
--> pred * (1 - pred)
chain rule : dl/dpred * dpred/dz = dl/dz
as the chain rule, derivative of the loss function respect of z
--> (-y/pred + (1-y) / (1-pred)) * (pred * 1-pred)
--> (1-pred) * -y + pred * (1-y)
--> -y +y*pred +pred -y*pred
--> pred - y
now, let's find dl/dw
dl/dw = dl/dz * dz/dw
= (pred - y) * dz/dw
let's find dz/dw...
z = wT.x + b
--> dz/dw = x
so, dl/dw = (pred - y) * dz/dw
= (pred - y) * x
similarly, find dl/db...
dl/db = dl/dz * dz/db
= (pred - y) * dz/db
z = wT.x + b
--> dz/db = 1
so, dl/db = (pred - y)
"""
dw = (pred - y).dot(x)
db = (pred - y).mean()
w -= dw * learning_rate
b -= db * learning_rate
if epochs % 1000 == 0:
print(err)
return w, b
주석이 더럽게 깁니다. 계산 과정을 보고싶은 것이 아니라면, 그냥 무시하고 지워버리셔도 됩니다.
일단, logistic regression에서의 prediction은 사실 Gradient Descent에다가 sigmoid만 집어넣은 것입니다.
그리고, Logistic Regression의 err도 아래의 loss function을 사용하도록 하겠습니다.
$ err = -ln(pred) * y - ln(1 - pred) * (1-y) $
이제 저희가 생각해야 할 부분은, 이 err 함수를 각각 w와 b에 대해서 미분하는 것입니다.
우리는 여기서 chain rule(연쇄 법칙)이라는 것을 사용할 것입니다.
chain rule이란, $ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} $인 것을 말합니다.
저희는 이제부터 $ \frac{\partial err}{\partial w} $의 값을 찾을 겁니다.
우선, 가장 간단하게 err을 pred에 대해서 미분해 볼까요?
$ err = -ln(pred) * y - ln(1 - pred) * (1-y) $ 이었고, 이를 pred에 미분해보면...
$$ \frac{\partial err}{\partial pred} = \frac{-y}{pred} + \frac{1-y}{1-pred} $$
가 됩니다.
또, pred는 $ sigmoid(z) = \frac{1}{1+e^{-z}} $ 였으므로, 이 pred를 z에 대해서 미분하면,
$$ \frac{\partial pred}{\partial z} = \frac{e^{-z}}{(1+e^{-z})^2}$$
이 된다는 것을 알 수 있습니다.
그리고, z=wx+b이므로 z를 w에 대해 미분하면 그냥 x, z를 b에 대해 미분하면 그냥 1이 나옵니다.
그리고 지금까지 구한 것들을 chain rule을 사용하여 다시 정리해 봅시다.
$$ \frac{\partial err}{\partial w} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial w} $$
$$ \frac{\partial err}{\partial w} = (\frac{-y}{pred} + \frac{1-y}{1-pred}) * \frac{e^{-z}}{(1+e^{-z})^2} * x$$
자! 이제 이 위의 pred와 z에 한번 식을 대입해서 계산해 봅시다!!
는 사실 좀;;;; 너무하죠 그쵸?
사실 아까 전에, $ \frac{\partial pred}{\partial z} = \frac{e^{-z}}{(1+e^{-z})^2} $ 라고 했던 걸 조금 정리를 해 보면,
$pred = \frac{1}{1+e^{-z}}$라고 했으므로,
$$ \frac{e^{-z}}{(1+e^{-z})^2} = \frac{1}{1+e^{-z}} * \frac{e^{-z}}{1+e^{-z}} = pred * (1-pred)$$
로 정리가 됩니다!!
자, 그럼 이제 다시 위 식으로 돌아가서 하던 계산을 마저 해 봅시다!
$$ \frac{\partial err}{\partial w} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial w} $$
$$ = (\frac{-y}{pred} + \frac{1-y}{1-pred}) * pred * (1 - pred) * x$$
$$ = ((pred - 1) * y + pred * (1 - y)) * x$$
$$ = (pred - y) * x$$
이제 이렇게 두니, 아주 예쁜 식이 나왔군요!
또한,$ \frac{\partial err}{\partial b}$의 값도 구해 보자면, $ \frac{\partial z}{\partial b} = 1 $이었으므로,
$$ \frac{\partial err}{\partial b} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial b} $$
$$ = pred - y$$
라는 식이 도출됩니다!!
따라서, dw, db를 위의 코드와 같이 update시켜줄 수 있습니다.
이제, 제대로 훈련이 되었나 확인해야겠죠?
원래 쓰던 코드에, 다음과 같은 코드를 넣어서 확인해 봅시다.
z = x.dot(w) + b
pred = sigmoid(z)
pred[pred > 0.5] = 1
pred[pred <= 0.5] = 0
print("ACC : ", (pred == y).mean())
pred 값을 직접 계산해서, 0.5보다 크면 1(양성), 0.5보다 작거나 같으면 0(악성)인 종양으로 판별하게 하는 것입니다.
그리고, (pred ==y).mean()의 코드로 정확도를 보면, 대충 0.92, 즉 92%의 정확도를 보이네요!
(참고 - (pred == y)를 하면 pred와 y가 같으면 1, 다르면 0을 각각의 인덱스에 따라 출력하니, 그 평균을 맞추면 정확도가 됩니다.)
이렇게, Logistic Regression의 구현까지 마쳤습니다. 다음 시간에는 Softmax Classification의 구현을 해보도록 하겠습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
---|---|
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-1차시 - Gradient Descent의 구현 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 0차시 - 프로젝트 설명 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
이번 차시에서는 Stochastic Gradient Descent, 줄여서 SGD를 구현해 보도록 하겠습니다.
일단 SGD가 뭔지 아주 짤막하게만 설명하자면, 기존 Gradient Descent에서 약간의 mini-batch만을 뽑아서 학습시키는 방법입니다.
이 방법을 사용하면 조금 학습이 불안정해지지만, 더욱 빠른 시간에 꽤 괜찮은 값으로 수렴하게 됩니다.
import numpy as np
import ComputeGrad
train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))
w, b = ComputeGrad.SGD(train_x, train_y, 3, w)
print(ans_w)
print(w)
print(ans_b)
print(b)
일단 테스트를 돌리는 코드는 저번 시간과 크게 다르지 않습니다.
그렇다면, SGD는 어떻게 생겼을지 확인해 봅시다.
def SGD(x, y, b, w, learning_rate=0.01, epoch=1000, batch_size=16):
if type(w) != np.ndarray:
w = float(w)
w = np.reshape(w, (1, 1))
if x.size == x.shape[0]:
x = x.reshape(x.shape[0], 1)
if y.size == y.shape[0]:
y = y.reshape(y.shape[0], 1)
for epochs in range(epoch):
batch_mask = np.random.choice(x.shape[0], batch_size)
x_batch = x[batch_mask]
y_batch = y[batch_mask]
pred = x_batch.dot(w) + b
dw = ((pred - y_batch) * x_batch).mean(0)
dw = dw.reshape(dw.shape[0], 1)
db = (pred - y_batch).mean()
w -= dw * learning_rate
b -= db * learning_rate
if epochs % (epoch / 10) == 0:
pred = x.dot(w) + b
err = np.mean(np.square(pred - y))
print("error : ", err)
return w, b
...라고는 해도, 저번 Gradient Descent와 크게 달라지지 않았습니다.
어떤 부분이 달라졌는지 확인해 봅시다.
우선, 함수에 batch_size와 batch_mask라는 변수가 생겼습니다.
batch_size는 말 그대로 mini-batch의 크기를 정해 주고, batch_mask는 mini-batch를 만드는 역할을 해 주는데요,
위에서 np.random.choice 함수는 0부터 x.shape[0]-1까지의 랜덤한 숫자를 batch_size만큼 뽑아주는 역할을 합니다.
그러므로, x[batch_mask]를 하게 되면, 랜덤하게 골라진 batch_size개의 인덱스를 참조하여 mini-batch를 만들게 됩니다.
그리고, 이 과정이 전부입니다.
저번 Gradient Descent 코드에서 크게 달라진 부분은 없으니, 쉽게 이해가 되실 거라 믿습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
---|---|
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-1차시 - Gradient Descent의 구현 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 0차시 - 프로젝트 설명 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 1-1차시 - Gradient Descent의 구현
* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.
import numpy as np
import ComputeGrad
data = np.array([[1, 2, 3], [4, 6, 8]]) # y=2x+2 --> w=2, b=2
train_x = data[0]
train_y = data[1]
w = 1
b = 1
learning_rate = 0.1
epoch = 10000
w, b = ComputeGrad.GradientDescent(train_x, train_y, b, w, learning_rate, epoch)
print(w)
print(b)
일단 가장 먼저 할 것은, 간단한 Linear Regression 및 Gradient Descent의 구현입니다.
어차피 설명할 부분이 적으니, 코드로 바로 들어가도록 하겠습니다.
data = np.array([[1, 2, 3], [4, 6, 8]]) # y=2x+2 --> w=2, b=2
train_x = data[0]
train_y = data[1]
w = 1
b = 1
learning_rate = 0.1
epoch = 10000
우선 처음 시작은 이렇게 간단한 데이터로 하겠습니다.
이정도로 간단한 데이터라면 분명 Gradient Descent를 통해 w=2, b=2라는 정답을 찾아낼 수 있을 겁니다.
위 코드를 통해 train_x에는 [1, 2, 3]이, train_y에는 [4, 6, 8]이 들어가게 됩니다.
또, w값과 b값은 초기 값을 1로 설정하고, learning rate와 epoch까지 설정했습니다.
def GradientDescent(x, y, b, w, learning_rate=0.01, epoch=10000):
if type(w) != np.ndarray:
w = float(w)
w = np.reshape(w, (1, 1))
if x.size == x.shape[0]:
x = x.reshape(x.shape[0], 1)
if y.size == y.shape[0]:
y = y.reshape(y.shape[0], 1)
for epochs in range(epoch):
pred = x.dot(w) + b # y = w1x1 + w2x2 + ... + wmxm + b
"""
err = 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)
to minimize err --> differentiate w and b
dw = 2/m * ((Wx1 + b - y1) * x1 + (Wx2 + b - y2) * x2 + ... + (Wxm + b - ym) * xm)
db = 2/m * ((Wx1 + b - y1) * 1 + (Wx2 + b - y2) * 1 + ... + (Wxm + b - ym))
"""
dw = ((pred - y) * x).mean(0)
db = (pred - y).mean()
dw = dw.reshape(dw.shape[0], 1)
w -= dw * learning_rate
b -= db * learning_rate
if epochs % 1000 == 0:
err = np.mean(np.square(pred - y)) # err = 1/m * (w1x1 + w2x2 + ... + wmxm + b - y)^2
print("error : ", err)
return w, b
다음은 Gradient Descent 부분 코드입니다.
if type(w) != np.ndarray:
w = float(w)
w = np.reshape(w, (1, 1))
if x.size == x.shape[0]:
x = x.reshape(x.shape[0], 1)
if y.size == y.shape[0]:
y = y.reshape(y.shape[0], 1)
원래 Multi Variable Linear Regression을 먼저 만들 생각이었기에, w와 x의 값을 행렬곱에 적합한 형태로 바꾸어 주어야 합니다.
for epochs in range(epoch):
pred = x.dot(w) + b # y = w1x1 + w2x2 + ... + wmxm + b
"""
err = 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)
to minimize err --> differentiate w and b
dw = 2/m * ((Wx1 + b - y1) * x1 + (Wx2 + b - y2) * x2 + ... + (Wxm + b - ym) * xm)
db = 2/m * ((Wx1 + b - y1) * 1 + (Wx2 + b - y2) * 1 + ... + (Wxm + b - ym))
"""
dw = ((pred - y) * x).mean(0)
db = (pred - y).mean()
dw = dw.reshape(dw.shape[0], 1)
w -= dw * learning_rate
b -= db * learning_rate
if epochs % 1000 == 0:
err = np.mean(np.square(pred - y)) # err = 1/m * (w1x1 + w2x2 + ... + wmxm + b - y)^2
print("error : ", err)
return w, b
다음은 훈련 과정입니다.
우선, Linear Regression에서의 예측값을 pred, loss값을 err로 설정해 두었습니다.
계산 과정은 주석으로 달아놨지만, 조금 더 자세히 써보도록 하겠습니다.
우선 err값은 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)으로 설정해 두었습니다.
하지만 지금은 multi variable gradient descent가 아니므로, w와 x는 값을 각 한개씩을 가지게 됩니다.
따라서 err값은 $ err = 1/m * (wx + b - y)^2 $ 로 설정이 됩니다.
이제 이 err 값을 w와 b에 대하여 미분해 보면,
$$ \frac{\partial{err}}{{\partial{w}}} = 2/m * (wx + b - y) * x$$
$$ \frac{\partial{err}}{{\partial{b}}} = 2/m * (wx + b - y)$$
가 됩니다.
따라서, 이를 코드로 구현하면,
dw = ((pred - y) * x).mean(0)
db = (pred - y).mean()
가 됩니다.
(2를 곱하지 않은 이유는, 딱히 중요한 이유가 있는게 아니라 코드에 2 곱하는 코드 있으면 더러워 보여서 그런겁니다.)
그리고, 아까 위에서 잠깐 언급했듯, 위 코드는 Multi Variable일때도 작동합니다.
import numpy as np
import ComputeGrad
train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))
w, b = ComputeGrad.GradientDescent(train_x, train_y, 3, w)
print(ans_w)
print(w)
print(ans_b)
print(b)
위의 코드가 바로 그 Multi Variable일 경우의 코드입니다.
train_x와 ans_w, ans_b를 각각 만들어 주어 ans_w * train_x + b의 값으로 y를 만들어 주었고,
w와 b의 초기값을 랜덤으로 설정해 두며 Gradient Descent를 작동시켰습니다.
마지막의 결과값을 보면 ans_w와 w, ans_b와 b가 서로 비슷해져 있는 모습을 볼 수 있습니다.
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
---|---|
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 0차시 - 프로젝트 설명 (0) | 2020.05.30 |
딥러닝 직접 구현하기 프로젝트 0차시 - 프로젝트 설명
지금부터 별로 쓸데는 없지만 재미있는 프로젝트를 하나 할까 합니다.
지금까지는 계속 인공지능에 관한 새로운 내용, 새로운 알고리즘 등을 배우면서 텐서플로우 및 파이토치로 시연하기만 했었는데, 이렇게 하니 알고리즘에 대한 확실한 이해가 부족해지는 것 같았습니다. 응용 능력도 많이 떨어지는것 같구요.
따라서, 이 프로젝트에서는 기본적인 데이터셋 관련 함수나 numpy, pandas와 같은 모듈만을 사용하여 딥러닝 및 머신 러닝 기술을 구현할 예정입니다.
역전파법과 순전파의 계산과 같은 수학적인 부분들도 직접 계산해서 코드로 구현할 예정입니다.
또한, 이 프로젝트의 게시글은 다른 게시글처럼 인공지능을 모르는 사람들을 위한 게시글이 아니라, 인공지능이 뭔지는 잘 아는 사람들을 위한 게시글입니다.
따라서, 기본적인 인공지능과 관련된 내용은 버리고, 특정 함수의 미분법이나 구현한 방식 등에 대해서만 다룰 예정입니다.
(인공지능의 기본이 부족하다면 모두를 위한 딥러닝 게시글 및 영상을 참조하시면 되겠습니다.)
일단 이 프로젝트의 첫 목표는 "Policy Gradient로 오목/지뢰찾기 인공지능 만들기"가 될 것 같습니다. 매주 주말마다 열심히 코드를 짜면서 1학기가 끝나기 전까지 완성이 되기를 바랍니다.
참고로, "밑바닥부터 시작하는 딥 러닝" 서적을 참고하여 제작하기에, 코드가 일부 비슷한 부분이 있을 수 있습니다.
https://github.com/cdjs1432/DeepLearningBasic
그리고, 이번에 처음으로 깃허브를 좀 활용할까 합니다.
코딩하다 뻘짓해서 코드 날리는 경우가 꽤 흔했던지라 ㅠ
글 올라오면 바로 해당 코드를 깃허브에 업로드 할 예정이니, 전체 코드가 필요하시다면 깃허브에서 가져가시면 되겠습니다.
일단 지금 구상하고 있는 프로젝트의 목차는 다음과 같습니다.
1. 기본 머신 러닝
1-1. Linear Regression / Gradient Descent 구현하기
1-2. Stochastic Gradient Descent 구현하기
1-3. Logistic Regression 구현하기 (Iris dataset)
1-4. Softmax Classification 구현하기 (MNIST dataset)
2. 딥 러닝
2-1. Single-layer Gradient Descent 구현하기
2-2. Multi-layer Softmax Classification 구현하기 (MNIST dataset)
2-3. 다양한 활성화 함수 (activation function) 구현하기 (ReLU, Leaky ReLU, maxout, ...)
2-4. 부록) 코드 리팩토링
2-5. 다양한 optimization 기법 구현하기 (RMSprop, Adam, ...)
2-6. Xavier Initialization, Regularization, Dropout 구현하기
3. CNN(Convolutional Neural Network) 구현하기
3-1. Convolution Layer 구현하기
3-2. Pooling / Padding 구현하기
4. 강화 학습 (원래는 CNN구현을 먼저 하려 했는데, 우선 오목/지뢰찾기 인공지능이 급해서..)
4-1. 기본적인 Q-Learning 구현하기 (gym 모듈 활용)
4-2. Policy Gradient 구현하기 (직접 만든 지뢰찾기 활용)
4-...
위의 목차는 그냥 단순한 구상일 뿐이고, 앞으로 계속해서 수정해 나가겠습니다.
한번 시작해 보겠습니다!
'인공지능 > 딥러닝 직접 구현하기 프로젝트' 카테고리의 다른 글
딥러닝 직접 구현하기 프로젝트 2-1차시 - Single-Layer Gradient Descent 구현하기 (0) | 2020.06.16 |
---|---|
딥러닝 직접 구현하기 프로젝트 1-4차시 - Softmax Classification & Cross-Entropy Loss구현하기 (8) | 2020.06.14 |
딥러닝 직접 구현하기 프로젝트 1-3차시 - Logistic Regression 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-2차시 - Stochastic Gradient Descent 구현하기 (0) | 2020.06.12 |
딥러닝 직접 구현하기 프로젝트 1-1차시 - Gradient Descent의 구현 (0) | 2020.05.30 |