728x90
1. Optimization (Normal Equation vs Gradient Descent)
- d-dimension vector : row 형태로 들어가 있음
- $\theta_{0}$에 곱해지는 input parameter = 1로 설정하기 위해 각 vector의 first entry = 1로 처리
- 최적화 parameter $\theta$ : cost function을 가장 최소화하는 것
- Vector $X$, $\Theta$ 내적 : $X^{T}\Theta$ (Transpose가 앞에 붙는다)
- Transpose 성질 : $(X \Theta)^{T} = \Theta^{T} X^{T}$ : Transpose를 분배하면 순서가 바뀐다
Normal Equation
행렬 미분 법칙
- 선형 모형: 선형 모형을 미분하면 Gradient Vector는 Weight Vector이다
- 이차 형식: 이차 형식을 미분하면 행렬과 벡터의 곱으로 나타난다
- n >= m 이므로 데이터가 변수 개수보다 많거나 같아야 함
- 선형회귀분석 연립방정식과 달리 행의 개수(식의 개수) > 열의 개수(변수의 개수)이므로 방정식을 푸는 것은 불가능함
- 해가 없는 Linear Systerm 이기에 $\Theta$는 근사해이다
- Normal Equation 를 이용해서 해를 구하는 것 : Data의 Sample수가 늘어날 경우 비효율적 (Matrix의 dimension이 늘어나게 될 경우 inverse matrix 연산이 복잡해짐)
Gradient Descent
- Iterative 하게 시행하여 gradient = 0인 지점을 찾는 방법
- Gradient : 함수의 변화도가 가장 큰 방향으로 이동
- Greedy Algorithm의 특성 : GD는 경우에 따라 Local Optimum만을 달성하기가 쉽다 (전체에서 최적화된 point가 아닐 가능성 존재)
- Gradient w/ Numerical Method
# 수치미분을 이용해 기울기를 구하는 함수
def numerical_gradient(function, x):
# x와 형상이 같은 배열 생성
grad = np.zeros_like(x)
h = 1e-4
for idx in range(x.size):
# 처음의 x[idx]를 보전하기 위해 temp 임시변수에 저장
temp = x[idx]
x[idx] = temp + h
fxh1 = function(x)
x[idx] = temp - h
fxh2 = function(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
# 임시 변수에 저장된 값을 다시 원상복구
x[idx] = temp
return grad
- Gradient Descent Implementation
#경사하강법 코드
def gradient_descent(f, init_x, lr=0.01, step_num=100):
# 초깃값 설정
x = init_x
# step_num 만큼 반복하면서 기울기를 구하고 가중치 갱신
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
- Optimial X 구하기
# 함수 지정
def function(x):
return x[0] ** 2 + x[1] ** 2
# 초깃값 설정
init_x = np.array([-3.0, 4.0])
# 위에서 정의한 gradient_descent 함수를 이용해 함숫값이 최소가 되는 x를 구해주세요.
# learning_rate은 0.1, step_num은 100으로 설정해주세요.
print(gradient_descent(function, init_x, lr=0.1, step_num=100))
[-6.11110793e-10 8.14814391e-10]
- Hyperparameter: 사전에 정의된 parameter (update X)
- Learnable parameter: GD 알고리즘을 통해 학습하고자 하는 parameter (update O)
Q1. MSE를 작성할 때 $\sum_{i=1}^{N}$ 까지 총 Sample의 수는 $N$개지만 $2N$으로 나누는 이유
- Derivation이 수학적으로 더 깔끔하기 때문: Partial Derivation시 $2$가 $\frac {1} {2}$와 상쇄됨
- "To make the derivations mathematically more convenient"
- Loss Function을 최소화하는 Parameter를 찾는데 있어서 Constant를 곱하는 행위는 아무런 영향도 주지 않음
https://mccormickml.com/2014/03/04/gradient-descent-derivation/
실제로 같은 결과를 expect 할 수 있다
Q2. 원래는 (실제값 - 예측값)^2 으로 처리했으나 (예측값 - 실제값)^2으로 MSE 작성 후에도 Gradient값이 동일한가?
Yes
- (실제값 - 예측값)으로 처리한 경우: Partial Derivation시 (-)가 앞으로 튀어나온다
- $\frac{\partial R(w)}{\partial w_{1}} = \frac {-2} {N} \sum_{i=1}^{N} x_{i} * (실제값_{i} - 예측값_{i})$
- $\frac{\partial R(w)}{\partial w_{0}} = \frac {-2} {N} \sum_{i=1}^{N} (실제값_{i} - 예측값_{i})$
- (예측값 - 실제값)으로 처리한 경우: (-)가 선반영 되어있다
- $\frac{\partial R(w)}{\partial w_{1}} = \frac {2} {N} \sum_{i=1}^{N} x_{i} * (예측값_{i} - 실제값_{i})$
- $\frac{\partial R(w)}{\partial w_{0}} = \frac {2} {N} \sum_{i=1}^{N} (예측값_{i} - 실제값_{i})$
Gradient Descent VS Normal Equation
2. Gradient Descent
- $\alpha$ : step size
- $\alpha$가 작다면 : 수렴속도가 느리지만 수렴하는 형태가 안정적
- $\alpha$가 크다면 : error surface에서 global minimum을 찾기가 힘들고, 발산하는 형태로 학습이 진행됨
- $L$이라는 function을 얼마만큼의 data로 정의하느냐에 따라 구분됨
- Batch gradient descent (entire n)
- Mini-batch gradient descent (k < n data)
- Stochastic gradient descent (k < n data with unbiased gradient estimation)
2. Batch / Mini-Batch / Stochastic Gradient Descent: $\theta \: \leftarrow \: \theta - \alpha g$
- Loss function : Data point에 대한 loss의 summation으로 표시
- 현재 parameter를 gradient와 반대 방향으로 업데이트
- $\Theta _{k+1} = \Theta _{k} - \gamma _{k} \nabla L(\Theta _{k})^{T} = \Theta_{k}- \gamma_{k}\sum_{n=1}^{N}\nabla L_{n}(\Theta_{k})^{T}$
- 매 update 순간마다 $\sum_{n=1}^{N}\nabla L_{n}(\Theta_{k})^{T}$ 를 계산하는 것이 매우 계산량이 많음
- Batch Gradient : Gradient를 모든 data point를 다 고려해서 계산하는 update
- original full batch gradient의 noisy apporximation : mini-batch GD, SGD
- Data가 매우 많은 상황 = 전체 gradient를 구하기 힘든 상황
- Local Minimum , Saddle point에 취약
- Mini-batch gradient : data point가 n개 있으면 그 중 특정 subset을 구해서 그 subset에 있는 gradient만 계산해서 update
- Stochastic Gradient Descent : subset을 구할 때 이 subset을 구한 gradient의 expectation의 original full batch gradient와 동일하도록 (original batch gradient를 잘 근사할 수 있게 다음과 같은 방식으로 디자인하기)
- $\sum_{n=1}^{N}\nabla L_{n}(\Theta_{k})^{T} = E\left [ \sum_{n \in K}\nabla L_{n}(\Theta_{k})^{T} \right ] $
- Batch - GD에 비해 빠르게 iteration을 돌 수 있으나, 각 sample 하나마다 parameter를 연산하기 때문에 noise의 영향을 많이 받음 -> Osciliation이 많이 발생
3. Solution: Momentum
- $Actual \: Step = Momentum \: Step + Gradient \: Step $
- 과거에 gradient가 업데이트 되어오던 방향 및 속도를 어느정도 반영해서 현재 포인트에서 Gradient가 0이 되더라도 계속해서 학습을 진행할 수 있는 동력을 제공하는 것
- Recursive Equation : $\nu_{t} = \rho^{k}\nu_{t-k} + (1-\rho)\left [ g_{t} + \rho g_{t-1} + ... + \rho^{k-1}g_{t-k+1} \right ]$
- 현재 Momentum = 과거 Momentum * $\rho^{k}$ + 과거 Gradient를 누적해서 계산 (현재시점에서 멀수록 $\rho$가 연속적으로 곱해짐)
- Update : 현재 Momentum = 직전 Momentum * $\rho$ + $(1-\rho)$ * 현재 Gradient
- $\rho$ 가 1보다 작기 때문에 연속적으로 곱해질수록 더욱 작아짐
- Saddle point나 작은 noise gradient값에 더 안정적으로 수렴하게끔 함
SGD + Momentum
- $\nu \: \leftarrow \: \rho\nu - \alpha g$
- $\theta \: \leftarrow \: \theta + \nu$
- Update되는 Learnable Parameter에 Momentum을 곱하여 반영
- Local Minimum이나 Saddle Point 에서 Gradient가 0이 되는 지점이 발생하더라도, 과거에 이어오던 Momentum을 반영하여 계속해서 학습을 진행할 수 있도록 해줌
Nesterov Momentum
- $ \nu \: \leftarrow \: \rho\nu - \alpha \nabla_{\theta}J(\theta + \rho \nu) $
- $\theta \: \leftarrow \: \theta + \nu$
- $Actual \: Step = Momentum \: Step + Lookahead \: Gradient \: Step$
- Gradient를 먼저 평가하고 업데이트를 해주게 됨 : lookahead gradient step을 이용
- Gradient : $\nabla_{\theta}J(\theta)$
- Lookahead Gradient : $\nabla_{\theta}J(\theta + \rho \nu)$
4. Adagrad, RMSProp, Adam
AdaGrad
accumulated gradient를 이용하여 learning rate를 조절
- 각 방향으로의 learning rate를 적응적으로 조절하여 학습효율을 높이는 방법
- $r \leftarrow \: r + g \cdot g$
- $\Delta \theta \leftarrow \: \frac {\alpha} {\sqrt{r + \varepsilon}} \cdot g$
- $\theta \leftarrow \: \theta - \Delta \theta$
- gradient $g$가 누적이 되면서 $r$이 점점 커짐 -> $\Delta \theta$가 줄어들음
- gradient $g$가 크다 : 해당 방향으로의 학습이 많이 진행됨 -> 수렴속도가 점점 줄음
- 단점 : gradient 값이 누적되면서 learning rate 값이 굉장히 작아짐 -> 학습이 일어나지 않게된다
RMSProp
- $r \leftarrow \: \rho r + (1-\rho) g \cdot g$
- $\Delta \theta \leftarrow \: \frac {\alpha} {\sqrt{r + \varepsilon}} \cdot g$
- $\theta \leftarrow \: \theta - \Delta \theta$
- $r, g \cdot g$에 각각 $\rho, 1-\rho$를 곱하기 때문에 반영비가 들어감
- r의 값을 어느정도 조질하게 됨 : gradient가 극단적으로 누적되면서 learning rate가 줄어드는 것이 아니라, 어느정도 완충된 상태로 learning rate가 줄어듬
Adam : RMSProp + Momentum
- First Moment from Momentum : $s \leftarrow \: \rho_{1} s + (1-\rho_{1}) g \cdot g$
- Second Moment from RMSProp : $r \leftarrow \: \rho_{2} r + (1-\rho_{2}) g \cdot g$
- Correct the bias : $s^{'} \leftarrow \: \frac {s} {1-\rho_{1}} $, $r^{'} \leftarrow \: \frac {r} {1-\rho_{2}} $
- $\Delta \theta \leftarrow \: \frac {\alpha} {\sqrt{r^{'} + \varepsilon}} \cdot s^{'}$
- $\theta \leftarrow \: \theta - \Delta \theta$
Learning rate scheduling
- hyperparameter $\alpha$를 학습과정에 따라 조정
Overfitting : Model 과적합 문제
- Model이 지나치게 복잡하여, 학습 Parameter의 숫자가 많아서 제한된 학습 샘플에 너무 과하게 학습이 되는 것
Solution
- Feature의 개수 줄이기
- Regularization
- 학습과정에서 모델의 복잡도에 대한 페널티를 줘서 모델이 overfitting되지 않도록함
- 모델은 가급적 적은 수의 parameter를 사용하면서 주어진 문제 sample들을 fitting할 것
5. Backpropagation
- $a_{i} ^{(j)}$: $j$번째 Layer에 있는 $i$번째 노드의 Output (Activation Function의 결과값, 활성화함수 적용 후 Node)
- $W^{j}$: $j$번째 Layer에서 $j+1$번째 Layer으로 mapping하는 Weight Matrix
- $Z_{i} ^{(j)}$: $j$번째 Layer에 있는 $i$번째 노드 기준 Linear Combination
- Linear Combination (Input Feature & Model Parameter) -> Activation Function에 합성
- Output $a_{i} ^{(j)}$는 다시 Input Feature로 작용
- Multi-Layer Perceptron의 각 layer별로 일어나는 계산과정을 이렇게 compact한 표현으로 나타냄
- 각 Layer는 Weight Matrix을 이용하여 j-th Layer에서 (j+1)-th Layer로 Mapping하는데, Linear Combination을 적용하므로 Linear Layer or Fully-Connected Layer이라 한다
Model Training
각 epoch 마다 Feed Forward / Backpropagation 수행
- Feed Forward (순전파)
- 입력 데이터를 기반으로 신경망을 따라 입력층(Input Layer)부터 출력층(Output Layer)까지 차례대로 변수들을 계산하고 추론(Inference)한 결과를 의미
- 모델(Model)에 입력값($x$)을 입력하여 순전파(Forward) 연산을 진행
- 이 과정에서 계층(Layer)마다 가중치(Weight)와 편향(Bias)으로 계산된 값이 활성화 함수(Activation Function)에 전달
- 최종 활성화 함수에서 출력값($ \hat{y}$)이 계산되고 이 값을 손실 함수(Loss Function)에 실젯값($y$)과 함께 연산하여 오차(Cost)를 계산
- Backpropagation (역전파)
- Input과 Output을 알고 있는 상태에서 신경망을 학습시키는 방법
- 순전파(Forward Propagation)의 방향과 반대로 연산이 진행
- 순전파(Forward Propagation) 과정을 통해 나온 오차(Cost)를 활용해 각 계층(Layer)의 가중치(Weight)와 편향(Bias)을 최적화
- 역전파 과정에서는 각각의 가중치와 편향을 최적화 하기 위해 연쇄 법칙(Chain Rule)을 활용
새로 계산된 가중치는 최적화(Optimization) 알고리즘을 통해
실젯값과 예측값의 차이를 계산하여 오차를 최소로 줄일 수 있는 가중치(Weight)와 편향(Bias)을 계산
역전파 동작 순서
1. 모델에 입력 데이터를 전달하여 예측값을 계산
2. 예측값과 실제값 간의 오차를 계산
3. 손실 함수의 그래디언트를 계산하여 각 파라미터에 대한 미분값을 구함
4. Optimizer를 사용하여 파라미터를 업데이트
6. Backpropagation을 이용한 sin function 구현
import numpy as np
import matplotlib.pyplot as plt
# input, target
input_data = np.arange(0, np.pi * 2, 0.1)
correct_data = np.sin(input_data)
n_data = len(correct_data)
# setting values
n_in = 1 # input layer의 뉴런 수
n_hid = 3 # hidden layer의 뉴런 수
n_out = 1 # output layer의 뉴런 수
eta = 0.1 # learning rate
epoch = 3000
interval = 500 # 경과 표시 간격
- HiddenLayer, OutputLayer: 객체 생성시 인자로 (layer input/output) 뉴런 수 입력
- n_upper: Input의 노드(뉴런 수)
- n: ouput의 노드(뉴런 수)
- x: Row Vector 형태 (1 X n_upper)
- self.w, self.grad_w: Matrix 형태 (n_upper X n)
- self.b, self.grad_b, u, self.y, delta: Row Vector 형태 (1 X n)
Q. delta는 어차피 1 X n 형태인데, np.sum(delta, axis=0)와 np.sum(delta)은 차이가 없지 않나?
정확하게 같은 결과를 기대하는 상황에서는, 특히 batch_size가 1보다 클 때를 고려하여 코드를 작성할 때는 np.sum(delta, axis=0)을 사용하는 것이 좋습니다.
이는 코드의 명시성과 유지보수성을 향상시키며, 다양한 batch_size에 대해 올바르게 작동하도록 보장합니다.
np.sum(delta)를 사용하면, batch_size가 1일 때만 올바르게 작동하고, 더 큰 batch_size에서는 의도하지 않은 결과를 초래할 수 있습니다.
Q. y.reshape(-1)과 y.flatten( )은 기능적으로 똑같지 않나?
둘 다 1차원 배열로 변환해주는 역할은 동일하다.
다만 reshape(-1)은 원본 배열의 뷰를 사용하여 메모리 사용을 최적화하려고 하는 반면
flatten( )은 항상 원본 데이터의 복사본을 생성하여 새로운 메모리 공간을 사용한다.
y.reshape(-1): 이 메소드는 y 배열을 1차원 배열로 변환합니다.
여기서 -1은 배열의 전체 요소 수를 기반으로 새로운 shape를 자동으로 계산하라는 의미입니다.
reshape 메소드는 원본 배열의 뷰(view)를 반환할 수 있으며, 메모리를 새로 할당하지 않는 경우가 많습니다.
그러나 reshape을 사용할 때는 원하는 새로운 shape를 명시적으로 지정해야 합니다.
y.flatten(): 이 메소드는 y 배열을 1차원 배열로 "평평하게" 만듭니다.
flatten 메소드는 항상 원본 데이터의 복사본을 반환합니다. 즉, 메모리에 새로운 배열을 생성합니다.
이는 데이터의 뷰가 아닌 실제 복사본을 작업할 때 유용할 수 있습니다.
결과적으로, 두 방식 모두 y를 1차원 배열로 변환하지만,
reshape(-1)은 가능한 경우 원본 배열의 뷰를 사용하여 메모리 사용을 최적화하려고 시도하는 반면,
flatten()은 항상 원본 데이터의 복사본을 생성하여 새로운 메모리 공간을 사용합니다. 따라서 성능과 메모리 사용 측면에서 미묘한 차이가 있을 수 있으나, 단순히 배열을 1차원으로 만들기만 한다면 두 방법은 사실상 동일한 결과를 제공합니다.
# hidden layer
class HiddenLayer:
def __init__(self, n_upper, n): # 초기설정
# 가중치 행렬과 편향 벡터
self.w = np.random.randn(n_upper, n)
self.b = np.random.randn(n)
def forward(self, x):
self.x = x
u = np.dot(x, self.w) + self.b
self.y = 1 / (1 + np.exp(-u)) # sigmoid function
def backward(self, grad_y): # backpropagation
delta = grad_y * (1 - self.y) * self.y
self.grad_w = np.dot(self.x.T, delta)
self.grad_b = np.sum(delta, axis=0)
self.grad_x = np.dot(delta, self.w.T)
def update(self, eta): # 가중치와 편향 수정
self.w -= eta * self.grad_w
self.b -= eta * self.grad_b
# hidden layer
class OutputLayer:
def __init__(self, n_upper, n): # 초기설정
# 가중치 행렬과 편향 벡터
self.w = np.random.randn(n_upper, n)
self.b = np.random.randn(n)
def forward(self, x):
self.x = x
u = np.dot(x, self.w) + self.b
self.y = u
def backward(self, t): # backpropagation
delta = self.y - t
self.grad_w = np.dot(self.x.T, delta)
self.grad_b = np.sum(delta, axis=0)
self.grad_x = np.dot(delta, self.w.T)
def update(self, eta): # 가중치와 편향 수정
self.w -= eta * self.grad_w
self.b -= eta * self.grad_b
- t, x는 모두 1-dimension (shape = (1, )) Scalar
- x.reshape(-1, 1) & t.reshape(1, 1): (1, 1) 형태의 2-dimension Matrix
- output_layer.y.reshape(-1): (1, ) 형태의 1-dimension Matrix
# 각 층의 초기화
hidden_layer = HiddenLayer(n_in, n_hid)
output_layer = OutputLayer(n_hid, n_out)
# 학습
for i in range(epoch):
# index 임의로 섞기
index_random = np.arange(n_data)
np.random.shuffle(index_random)
# 결과표시
total_error = 0
plot_x = []
plot_y = []
for idx in index_random:
x = input_data[idx : idx + 1] # input
t = correct_data[idx : idx + 1] # output
# Feed Forward
hidden_layer.forward(x.reshape(1, 1)) # 입력을 행렬로 변환
output_layer.forward(hidden_layer.y)
# Backpropagation
output_layer.backward(t.reshape(-1, 1)) # 정답을 행렬로 변환
hidden_layer.backward(output_layer.grad_x)
# 가중치와 편향 수정
hidden_layer.update(eta)
output_layer.update(eta)
if i == 0 or (i + 1) % interval == 0:
y = output_layer.y.reshape(-1) # 행렬을 벡터로 되돌림
# 오차 제곱합 계싼
total_error += 1.0 / 2.0 * (np.sum(np.square(y - t)))
# 출력리고
plot_x.append(x)
plot_y.append(y)
if i == 0 or (i + 1) % interval == 0:
# 출력 그래프 표시
plt.plot(input_data, correct_data, linestyle="dashed")
plt.scatter(np.array(plot_x), plot_y, marker="+", color="red")
plt.show()
# epoch 수와 오차 표시
print(f"Epoch: {i+1}, Error: {total_error / n_data}")
728x90