1. RSS 기반의 회귀 오류 추정
RSS (Residual Sum of Squares)
- 오류 값의 제곱을 더해서 구하는 방식
- 일반적으로 미분 등의 계산을 편리하기 위해 RSS 방식으로 오류의 합을 구함: Error = RSS
- Residual = 잔차 (실제값과 예측값의 차이) = 오류값
RSS (Residual Sum of Squares)의 이해
$RSS(w_{0}, w_{1}) = \frac {1} {N} \sum_{i=1}^{N} (y_{i} - (w_{0} + w_{1} * x_{i}))^{2}$
- RSS는 이제 변수가 $w_{0}, w_{1}$인 식으로 표현할 수 있으며, 이 RSS를 최소로 하는 $w_{0}, w_{1}$, 즉 회귀 계수를 학습을 통해서 찾는 것이 머신러닝 기반 회귀의 핵심 사항
- RSS는 회귀식의 독립변수 X, 종속변수 Y가 중심 변수가 아니라 w 변수(회귀 계수)가 중심 변수임을 인지하는 것이 매우 중요함 (학습 데이터로 입력되는 독립변수와 종속변수는 RSS에서 모두 상수로 간주)
- 일반적으로 RSS는 학습 데이터의 건수로 나누어서(N) 다음과 같이 정규화된 식으로 표현됨
RSS (Residual Sum of Squares) - 회귀의 비용 함수
- 회귀에서 이 RSS는 비용(Cost)이며 w 변수(회귀 계수)로 구성되는 RSS를 비용 함수라고 합니다.
- 머신러닝 회귀 알고리즘은 데이터를 계속 학습하면서 이 비용 함수가 반환하는 값(즉, 오류 값)을 지속해서 감소시키고 최종적으로는 더 이상 감소하지 않는 최소의 오류 값을 구하는 것
- 비용 함수를 손실 함수(loss function)라고도 함
RSS 최소화 - Gradient Descent
Key components of Deep Learning
- 모델이 학습할 수 있는 Data
- Data를 transform할 수 있는 Model
- Model의 성능을 결정하는 Loss Function (Model을 어떻게 학습할지)
- Loss를 최소화하는 parameter를 결정하는 Algorithm (Optimizer)
Gradient Descent (경사 하강법)
원래 함수의 최대, 최소를 점진적으로 근사하여 찾는 방법
- 고차원 방정식에 대한 문제를 해결해주면서 비용함수 RSS를 최소화하는 방법을 직관적으로 제공하는 뛰어난 방식
- 많은 W 파라미터가 있는 경우에 경사 하강법은 보다 간단하고 직관적인 비용함수 최소화 솔루션을 제공
- '점진적으로' 반복적인 계산을 통해 W 파라미터 값을 업데이트하면서 오류 값이 최소가 되는 W 파라미터를 구하는 방식
- 반복적으로 Cost Function의 반환 값, 즉 실제값과 예측값의 차이가 작아지는 방향성을 가지고 W 파라미터를 지속해서 보정해 나감
- 그리고 오류 값이 더 이상 작아지지 않으면 그 오류 값을 최소 비용으로 판단하고 그 때의 W 값을 최적 파라미터로 반환
미분을 통해 Cost Function의 최소값 찾기
- Cost Function이 아래로 볼록한 포물선 형태의 2차 함수라면:
- 포물선 형태의 2차 함수의 최저점 = 해당 2차 함수의 미분 값인 1차 함수의 기울기가 가장 최소일 때
- 최초 W에서부터 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 W를 업데이트함
- 마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주하고 그때의 W를 반환
2. 최소제곱법 OLS (Ordinary Least Squares)
RSS를 최소로 만드는 파라미터를 추정하는 방식
최소 제곱 법은 어떤 계의 해 방정식을 근사적으로 구하는 방법으로,
근사적으로 구하려는 해와 실제 해의 오차의 제곱의 합이 최소가 되는 해를 구하는 방법이다.
말이 어렵지만 결론은 최소 제곱 법을 통해 최선을 선을 그려낼 수 있도록 한다.
각 점들에서 가장 가까운 1차 함수 그래프 식을 구해내면, 구해낸 식을 기반으로 다음을 예측할 수 있다.
최선의 선을 그려낼 수 있도록 하기 위해 최소 제곱 법을 사용한다 했다.
최소제곱법을 사용하면 함수의 기울기 a와 y절편 b를 바로 구할 수 있기 때문이다.
기울기를 구하는 방법은 위와 같다.
분자는 (x-x평균)(y-y평균)의 합이고 분모는 (x-x평균) 제곱의 합이다.
a가 구해지면 아래 공식을 통해 y절편인 b를 구할 수 있다.
$( \bar{x}, \bar{y} )$을 지난다
여기까지 하면 일차함수 식 y = ax + b에서 a와 b를 알게 되었다.
a와 b를 알고 있기 때문에 x에 새로운 값을 넣으면 x에 대한 예측한 결과인 y값을 얻을 수 있다.
3. RSS의 편미분
$R(w) = RSS(w_{0}, w_{1}) = \frac {1} {N} \sum_{i=1}^{N} (y_{i} - (w_{0} + w_{1} * x_{i}))^{2}$
$\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})$
Chain Rule
Partial Differentiation
Gradient Descent 정리
$w_{0}, w_{1}$ Parameter Update 방식
$w_{1} \leftarrow w_{1} - \alpha * \frac{\partial R(w)}{\partial w_{1}} $
$w_{0} \leftarrow w_{0} - \alpha * \frac{\partial R(w)}{\partial w_{0}} $
4. Gradient Descent - 실습 Code
- Y = 4X+6 시뮬레이션하는 데이터 값 생성
- np.random.rand(shape): 0 ~ 1 사이의 uniform distribution(균일 분포) 상에서 난수를 추출
- np.random.randn(shape): 평균이 0, 표준편차가 1인 normal distribution(표준 정규분포)에서 난수를 추출
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0)
# y = 4X + 6 식을 근사(w1=4, w0=6). random 값은 Noise를 위해 만듬
X = 2 * np.random. rand(100,1)
y = 6 +4 * X+ np.random.randn(100,1)
# X, y 데이터 셋 scatter plot으로 시각화
plt.scatter(X, y)
- X, y 둘다 (N, 1) 형태
X.shape, y.shape
# ((100, 1), (100, 1))
Gradient Descent Process
STEP 1: $w_{0}, w_{1}$를 임의의 값으로 설정하고 첫 비용 함수의 값을 계산
STEP 2: $w_{1}$을 $w_{1} + \alpha * \frac {2} {N} \sum_{i=1}^{N} x_{i} * (실제값_{i} - 예측값_{i})$,
$w_{0}$을 $w_{1} + \alpha * \frac {2} {N} \sum_{i=1}^{N} (실제값_{i} - 예측값_{i})$ 으로 업데이트 한 후 다시 비용함수의 값을 계산
STEP 3: STEP 2를 주어진 횟수만큼 반복한다
np.dot(A, B)
[참고자료] https://jimmy-ai.tistory.com/75
- 1차원 X 1차원: 벡터 내적 (element-wise 방식으로 각 원소를 곱한 값들을 더한 내적 연산)
- 수식 : 보통 열벡터로 처리 (column vector)
- 코드 : 보통 행벡터로 처리 (row vector)
- Column vector로 들어오면 Transpose해서 Row vector로 처리
- 2차원 X 2차원: 행렬 곱 (@ 사용을 권장)
- N차원 X 스칼라: 단순 스칼라배 연산 (* 사용을 권장, np.dot 사용하지 말 것)
- N차원 X 1차원
- 왼쪽이 N차원인 경우 각 행마다 오른쪽의 벡터와 내적을 수행
- 오른쪽이 N차원이면 각 열마다 왼쪽의 벡터와 내적을 수행
- N차원 X M차원
- 왼쪽 array의 각 행, 오른쪽 array의 각 열끼리 순서대로 내적을 수행한 결과를 반환
Cost Function의 값을 최소화 할 수 있도록 w0과 w1의 업데이트 수행하는 함수 생성
- 예측 배열 y_pred는 np.dot(X, w1) + w0 임
- 100개의 데이터 X(1,2,...,100)이 있다면 예측값은 w0 + X(1)*w1 + X(2)*w1 +..+ X(100)*w1이며, 이는 입력 배열 X와 w1 배열의 내적임
- 새로운 w1과 w0를 update함
# w1 과 w0 를 업데이트 할 w1_update, w0_update를 반환.
def get_weight_updates(w1, w0, X, y, learning_rate=0.01):
N = len(y)
# 먼저 w1_update, w0_update를 각각 w1, w0의 shape와 동일한 크기를 가진 0 값으로 초기화
w1_update = np.zeros_like(w1)
w0_update = np.zeros_like(w0)
# 예측 배열 계산하고 예측과 실제 값의 차이 계산
y_pred = np.dot(X, w1.T) + w0
diff = y-y_pred
# w0_update를 dot 행렬 연산으로 구하기 위해 모두 1값을 가진 행렬 생성
w0_factors = np.ones((N,1))
# w1과 w0을 업데이트할 w1_update와 w0_update 계산
w1_update = -(2/N)*learning_rate*(np.dot(X.T, diff))
w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff))
return w1_update, w0_update
y_pred = np.dot(X, w1.T) + w0
- 2차원 X 2차원 연산이므로 행렬곱 shape을 맞춰줘야 함
- 단일 선형회귀의 경우 X feature의 개수가 1개 이므로 w1도 1개이고, 굳이 전치행렬을 사용할 필요가 없음
- 일반적으로 다중 선형회귀의 경우 X feature의 개수가 여러 개
- 종속변수 n개, Feature가 m개 있다고 하면 가중치는 [W1, W2, W3,,,Wm] 과 같은 row vector 형태로 표현 될 수 있음
- 이 경우에는 X feature 행렬과 가중치 벡터가 내적하여 올바른 회귀식을 도출하려면 가중치 벡터에 X를 내적하면 됩니다. 이를 일반화 하기 위해서 np.dot(X, W1.T)를 적용
- 만약 반복문을 사용하여 w를 계속해서 업데이트 해야하는 경우 or w가 column vector 형태로 들어왔다면 w1.T가 아니라 w1 형태로 Transpose 하지 않는다
- X size = $N(종속변수 \:개수 = Target \:값 \: 개수) \times M(Feature \: 개수)$
- M = Feature 개수 = 가중치 개수
- y_pred size = $N \times 1$
- w0 size = $N \times 1$
- Weight(w1) size = $M \times 1$ (column vector) or $1 \times M$ (row vector)
- W가 column vector로 들어온다면: W에 transpose를 시키지 X
Feature가 여러 개인 경우 회귀 계수 도출하기
w1_update = -(2/N)*learning_rate*(np.dot(X.T, diff))
- w1_update = $M \times 1$
- X size = $N(종속변수 \:개수 = Target \:값 \: 개수) \times M(Feature \: 개수)$
- diff size = $N \times 1$
W1_update 시에는 위와 같은 식을 구하려면 X집합과 실제값 - 예측값 집합을 내적하면 구할 수 있습니다. (개별 X 데이터 원소들을 하나씩 더하고 곱하는 식을 반복할 필요 없이 한번에 내적으로 구하는 방식입니다. )
X 데이터 셋의 Shape가 (100, 1) 즉 100개의 행을 가지는 1개의 feature로 되어 있으며 실제값 -예측값 집합 역시 각 개별 X 데이터 별로 타겟값과의 차이를 나타내는 diff 변수로서 Shape가 (100, 1)입니다. 이때 X.T (즉 (1, 100)으로 Shape 변환)한 결과와 diff 를 dot 연산하면 쉽게 결과를 얻을 수 있습니다.
w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff))
- w0_update = $M \times 1$ (모든 값이 1인 행렬)
- w0_factors = $N \times M$ (모든 값이 1인 행렬): X와 size가 동일
- X size = $N(종속변수 \:개수 = Target \:값 \: 개수) \times M(Feature \: 개수)$
- diff size = $N \times 1$
W0_update는 약간 복잡한데, 먼저 구하려는 식이 아래와 같이 개별 데이터들에 대한 실제값과 예측값들의 차이의 합만 계산하면 됩니다.
그런데 이게 오히려 선형 대수의 dot 같은 연산으로 바로 구하기가 어려워서 일부로 전체가 1의 값을 가지는 w0_factors = np.ones((100, 1)) 즉 100개의 행을 가지는 1개의 feature되어 있는 행렬을 만들고 이를 이용하여 dot 연산으로 유도하였습니다. 1에 각각 자기 원소를 곱하면 원래 자기 원소임을 이용한 것입니다.
w0 = np.zeros((1,1))
w1 = np.zeros((1,1))
y_pred = np.dot(X, w1.T) + w0
diff = y-y_pred
print(diff.shape)
w0_factors = np.ones((100,1))
w1_update = -(2/100)*0.01*(np.dot(X.T, diff))
w0_update = -(2/100)*0.01*(np.dot(w0_factors.T, diff))
print(w1_update.shape, w0_update.shape)
# (100, 1)
# (1, 1) (1, 1)
- 반복적으로 경사 하강법을 이용하여 get_weight_updates( )를 호출하여 w1과 w0을 업데이트 하는 함수 생성
# 입력 인자 iters로 주어진 횟수만큼 반복적으로 w1과 w0를 업데이트 적용함.
def gradient_descent_steps(X, y, iters=10000):
# w0와 w1을 모두 0으로 초기화.
w0 = np.zeros((1,1))
w1 = np.zeros((1,1))
# 인자로 주어진 iters 만큼 반복적으로 get_weight_updates() 호출하여 w1, w0 업데이트 수행.
for ind in range(iters):
w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate=0.01)
w1 = w1 - w1_update
w0 = w0 - w0_update
return w1, w0
- 예측 오차 비용 계산을수행하는 함수 생성 및 경사 하강법 수행
def get_cost(y, y_pred):
N = len(y)
cost = np.sum(np.square(y - y_pred)) / N
return cost
w1, w0 = gradient_descent_steps(X, y, iters=1000)
print("w1:{0:.3f} w0:{1:.3f}".format(w1[0, 0], w0[0, 0]))
y_pred = w1[0, 0] * X + w0
print("Gradient Descent Total Cost:{0:.4f}".format(get_cost(y, y_pred)))
# w1:4.022 w0:6.162
# Gradient Descent Total Cost:0.9935
- 시각화
plt.scatter(X, y)
plt.plot(X, y_pred)
Mini - Batch 확률적 경사 하강법을 이용한 최적 비용함수 도출
- X, y 대신에 sample_X, sample_y 설정
def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
w0 = np.zeros((1,1))
w1 = np.zeros((1,1))
prev_cost = 100000
iter_index =0
for ind in range(iters):
np.random.seed(ind)
# 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터 추출하여 sample_X, sample_y로 저장
stochastic_random_index = np.random.permutation(X.shape[0])
sample_X = X[stochastic_random_index[0:batch_size]]
sample_y = y[stochastic_random_index[0:batch_size]]
# 랜덤하게 batch_size만큼 추출된 데이터 기반으로 w1_update, w0_update 계산 후 업데이트
w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate=0.01)
w1 = w1 - w1_update
w0 = w0 - w0_update
return w1, w0
np.random.permutation(100)
array([66, 71, 54, 88, 82, 12, 36, 46, 14, 67, 10, 3, 62, 29, 97, 69, 70,
93, 31, 73, 60, 96, 28, 27, 21, 19, 33, 78, 32, 94, 1, 41, 40, 76,
37, 87, 24, 23, 50, 2, 47, 20, 77, 17, 56, 64, 68, 25, 15, 22, 16,
98, 63, 92, 86, 38, 6, 57, 95, 44, 9, 42, 81, 99, 35, 84, 59, 48,
75, 65, 85, 90, 55, 43, 58, 89, 30, 80, 34, 18, 51, 49, 52, 74, 26,
45, 39, 4, 11, 53, 91, 79, 8, 0, 5, 13, 61, 72, 7, 83])
- Stochastic Gradient Descent vs Gradient Descent 성능비교
w1, w0 = stochastic_gradient_descent_steps(X, y, iters=1000)
print("w1:", round(w1[0, 0], 3), "w0:", round(w0[0, 0], 3))
y_pred = w1[0, 0] * X + w0
print("Stochastic Gradient Descent Total Cost:{0:.4f}".format(get_cost(y, y_pred)))
# w1: 4.028 w0: 6.156
# Stochastic Gradient Descent Total Cost:0.9937
5. Stochastic Gradient Descent (SGD)
이라는 function을 얼마만큼의 data로 정의하느냐에 따라 구분됨
- Batch gradient descent (entire n)
- Mini-batch gradient descent (k < n data)
- Stochastic gradient descent (k < n data with unbiased gradient estimation)
Stochastic Gradient Descent
- 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를 구하기 힘든 상황
Mini-batch gradient : data point가 n개 있으면 그 중 특정 subset을 구해서 그 subset에 있는 gradient만 계산해서 update
Stochastic Gradient Descent : subset을 구할 때 이 subset을 구한 gradient의 expectation의 original full batch gradient와 동일하도록 (original batch gradient를 잘 근사할 수 있게 다음과 같은 방식으로 디자인하기)
- 랜덤하게 데이터를 하나씩 뽑아서 loss를 만듦 (random이라 stochastic)
- 즉, 하나만 보고 빠르게 방향 결정 (gradient가 전체 데이터가 아닌 mini-batch인 일부 데이터만 보고 방향 결정)
- local minimum으로부터 탈출의 기회가 된다