갈루아의 반서재


Logistic Regression


이진 분류에 대해 이야기 할 때, 즉, 0과 1 이라는 결과물에 대해서 말할 때 가장 먼저 떠오르는 것은 로지스틱 회귀이다. 하지만 딥러닝에 있어 로지스틱 회귀를 어떻게 활용할 수 있는가? 사실 로지스틱 회귀는 심플한 신경망이다. 그런데 신경망과 딥러닝은 사실 같은 것이다. 인공신경망을 다룰 때 "deep" 이라는 용어에 대해 상세히 설명할 기회가 있을 것이다. 이에 앞서 로지스틱 회귀를 이해하기 위해 computation graph 에 대해 알아보자. 


Computation graph 


일종의 수학적 표현의 시각화 정도라고 생각하면 될 것이다. 예를 들어, 

의 경우 다음과 같이 표현할 수 있다.

http://image.ibb.co/hWn6Lx/d.jpg


그러면 로지스틱 회귀의 computation graph 를 그려보자. 

http://preview.ibb.co/cxP63H/5.jpg


  • 파라메터는 가중치(weight) 와 편향(bias)이다.
  • 가중치는 각 픽셀의 공분산이다.
  • 편향은 절편(Intercept)이다. 

즉, 다음 수식과 같이 표현된다. 

z = (w.t)x + b 

가중치의 전치에 입력 x 를 곱한 후 편향을 더한 것이다. 다른 말로 하면 다음과 같다. 

z = b + px1w1 + px2w2 + ... + px4096*w4096


y_head = sigmoid(z)

시그모이드 함수는 0과 1사이의 값 z 로 확률을 나타낸다.   


그러면 왜 시그모이드 함수를 사용하는가? 시그모이드 함수는 확률적 결과를 보여준다. 그리고 경사하강법(gradient descent) 알고리즘에 사용할 수 있기 때문이다. 예를 들어보자. z = 4 이고  z 를 시그모이드 함수에 넣어보면, 결과값(y_head) 는 거의 0.9로 분류 결과가 1일 확률이 90% 라는 의미이다. 그러면 아래에서 computation graph 의 요소 하나 하나를 살펴보자. 


Initializing parameters


입력은 4096 픽셀을 가진 이미지이다. 각 픽셀은 자기만의 가중치를 가진다. 첫번째로 해야할 것은 각각의 픽셀을 각각의 가중치로 곱하는 것이다. 그러면 가중치의 초기값은 무엇인가하는 의문이 들 것이다. 거기에는 일종의 테크닉이 있는데, 이 부분은 이후 ANN 에서 다루기로 하고 여기서는 0.01 이라고 초기값을 주도록 하자. 

그럼 가중치가 0.01 이면 가중치 배열은 어떤 형태를 가지는가? 로지스틱 회귀의 계산 그래프에서 이미 본대로 (4096, 1) 이 된다. 그리고 편향 기초값은 0 이다. 

그러면 가중치과 편향 초기값을 부여하는 코드를 작성해보자. 

1

2

3

4

5

6

7

# lets initialize parameters

# So what we need is dimension 4096 that is number of pixels as a parameter for our initialize method(def)

def initialize_weights_and_bias(dimension):

    w = np.full((dimension,1),0.01)

    b = 0.0

    return w, b

Colored by Color Scripter

cs


1

#w,b = initialize_weights_and_bias(4096)

cs


Forward Propagation


픽셀에서 코스트로 가는 모든 단계를 순방향전파 (forward propagation) 라고 부른다.

z = (w.T)x + b

위의 식에서 x 가 픽셀 배열이라는 것은 이미 알고 있다. 그리고 w (가중치), b (편향) 역시 알고 있으므로, 남은 것은 단지 계산 절차 뿐이다 (T는 전치를 의미한다).

그리고 z 를 시그모이드 함수에 넣어 확률을 나타내는 y_head 를 반환한다. 헷갈릴 때는 항상 계산 그래프를 참조해라. 

다음 손실 함수를 계산한다.

비용함수는 모든 손실의 합이다. 

z 를 가지고 시그모이드 정의 메서드를 만들자. z 를 입력 파라메터로 넣어 확률값인 y_head 를 출력하는 함수이다. 

1

2

3

4

5

6

7

import numpy as np

 

# calculation of z

#z = np.dot(w.T,x_train)+b

def sigmoid(z):

    y_head = 1/(1+np.exp(-z))

    return y_head

cs


1

2

y_head = sigmoid(0)

y_head

cs

0.5

시그모이드 메서드를 통해 y_head 를 계산할 수 있게 되었다. 이제 손실 함수에 대해 알아보자. 예를 통해 생각해보자. 하나의 이미지를 입력하고, 거기에 가중치를 곱하고 편향을 더해 z 를 얻었다. 그리고 z 에 시그모이드 메서드를 적용하여, y_head 를 구한다. 여기까지가 지금까지 학습한 내용이다. 다음 예를 들어 y_head 가 0.9라고 하면, 그 값은 0.5 보다는 크므로 그 이미지는 1을 나타낼 가능성이 크다고 볼 수 있다. 

뭐 특별히 문제가 없는 것처럼 보인다. 하지만, 1일 거라는 우리의 예상이 정확한지를 어떻게 체크할 수 있을까? 그 답은 바로 손실함수에 있다. 손실함수의 수학적인 표현은 다음과 같다.

https://image.ibb.co/eC0JCK/duzeltme.jpg

실제 이미지는 기호 1 이고, 라벨은 1 (y=1) 이다. 그러면 예측 y_head = 1 이다. y 와 y_head 를 손실 함수에 넣으면 0 이라는 결과가 나온다. 이 경우 올바른 예측을 하였고, 따라서 손실이 0 인 셈이다. 하지만, 잘못된 예측을 하게 되면, 예를 들어 y_head = 0 이면, 손실함수는 무한대 값이 나온다. 

그리고 비용함수는 손실함수의 합이다. 각각의 이미지는 손실 함수를 만들고, 비용함수는 각각의 입력된 이미지가 생성하는 손실함수의 합이 된다. 

그러면 순방향 전파를 코드로 구현해보자.

1

2

3

4

5

6

7

8

9

10

11

12

# Forward propagation steps:

# find z = w.T*x+b

# y_head = sigmoid(z)

# loss(error) = loss(y,y_head)

# cost = sum(loss)

def forward_propagation(w,b,x_train,y_train):

    z = np.dot(w.T,x_train) + b

    y_head = sigmoid(z) # probabilistic 0-1

    loss = -y_train*np.log(y_head)-(1-y_train)*np.log(1-y_head)

    cost = (np.sum(loss))/x_train.shape[1]      # x_train.shape[1]  is for scaling

    return cost 

Colored by Color Scripter

cs



Optimization Algorithm with Gradient Descent


이제 cost = error 라는 것을 안다. 따라서 만약 비용이 높다면 - 즉, 잘못된 예측을 하고 있다는 뜻 - 그 비용을 낮추기 위한 논의가 필요하다. 

우선적으로 가중치와 편향을 초기화하는데서 시작한다. 따라서 비용은 그것들에 달려있다. 비용을 줄이기 위해 결국 가중치와 편향을 업데이트해야하는 것이다. 다른 말로 하면, 모델이 비용 함수를 최소화하는 파라메터 가중치와 인수를 익히도록 해야하는 것이다. 이러한 테크닉을 경사하강법 (gradient descent) 이라고 부른다. 예를 들어보자. 

w = 5, bias = 0 (그러므로 일단 편향은 무시한다) 라고 가정하자. 다음 순방향전파를 생성하고 비용함수는 1.5 라고 하자. 대략 다음과 같은 형태일 것이다 (빨간선 참고).


그래프에서 보는 바와 같이 w=5 는 비용함수의 최소값을 가져오지 못하므로, 가중치를 업데이트한다 (:= 은 업데이트를 나타낸다). 

w := w - step 

여기서 step 는 무엇을 나타내는가? step 은 slope 를 의미한다. 최소 지점을 찾기 위해서 slope1 을 사용할 것이다. slope1 = 3 이라면 새로운 가중치는 w := w - slope1 => w = 2 로 업데이트된다. 

이제 가중치는 2가 되었다. 다시금 순방향전파를 가진 비용함수를 찾아야한다. w = 2 를 가진 순방향전파의 경우 비용함수는 0.4 이다. 비용함수가 줄어들고 있으므로 제대로 된 방향으로 접근하고 있는 셈이다. 이것만으로 충분하다고 확신할 수 없다. 한 단계 더 진행해보자. 

slope2 = 0.7 이고 w = 2 이므로 w := w - slope2 => w = 2- 0.7 = 1.3 이라는 새로운 가중치가 도출되었다. 새로운 비용을 찾아보면 이 경우 0.3 으로 결과값이 나온다. 한 단계 더 진행한다.

slope3 = 0.01 이고 w = 1.3 이므로 w := w - slope3 => w = 1.29 ~ 1.3 라는 결과가 나오므로 사실상 가중치는 변화되지 않았다. 

그러면 기울기(slope)는 어떻게 찾아내는가? 잘 아시다시피 주어진 점에서 함수의 기울기를 구하기 위해서는 도함수를 구하면 된다. 

업데이트된 식은 다음과 같다. 가중치와 편향을 가진 비용함수가 있고, 가중치와 편향에 따라 해당 함수의 도함수를 구한다. 그리고 비용함수의 도함수를 구한 뒤 학습속도 (learning rate) α 를 곱한다. 그리고 가중치를 업데이트한다. 

http://image.ibb.co/hYTTJH/8.jpg

그럼 학습속도란 무엇인가하는 의문이 들 것이다. 사실 배우는 속도와 학습률은 트레이프 오프 관계라고 말할 수도 있을 것이다. 그러므로 그 속도는 적절해야 한다. 

학습속도는 하이퍼파라메터라고도 불리는데 튜닝 또는 최적화 해야하는 주변수가 아니라 사람들이 선험적 지식으로 설정을 하거나 또는 외부 모델 메커니즘을 통해 자동으로 설정이 되는 변수를 말한다. 일단 이 부분에 대해서는 이후 자세히 설명하기로 하고 여기서는 1 이라고 가정하자. 

이제 순방향전파(가중치와 편향으로부터 비용으로) 뒤에 숨겨진 로직에 대해 어느 정도 이해했으리라고 생각한다. 그리고 경사강하법에 대해서도 학습했다. 이제 코드로 적용해야하는 부분은 비용함수의 도함수를 구하는 과정이다. 이 부분은 파이썬이나 코딩이라기보다 순수 수학에 가깝다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# In backward propagation we will use y_head that found in forward progation
# Therefore instead of writing backward propagation method, lets combine forward propagation and backward propagation
def forward_backward_propagation(w,b,x_train,y_train):
    # forward propagation
    z = np.dot(w.T,x_train) + b
    y_head = sigmoid(z)
    loss = -y_train*np.log(y_head)-(1-y_train)*np.log(1-y_head)
    cost = (np.sum(loss))/x_train.shape[1]      # x_train.shape[1]  is for scaling
    # backward propagation
    derivative_weight = (np.dot(x_train,((y_head-y_train).T)))/x_train.shape[1# x_train.shape[1]  is for scaling
    derivative_bias = np.sum(y_head-y_train)/x_train.shape[1]                 # x_train.shape[1]  is for scaling
    gradients = {"derivative_weight": derivative_weight,"derivative_bias": derivative_bias}
    return cost,gradients
 
cs

np.dot https://docs.scipy.org/doc/numpy-1.15.0/reference/generated/numpy.dot.html

이제 학습파라메터를 업데이트하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Updating(learning) parameters
def update(w, b, x_train, y_train, learning_rate,number_of_iterarion):
    cost_list = []
    cost_list2 = []
    index = []
    # updating(learning) parameters is number_of_iterarion times
    for i in range(number_of_iterarion):
        # make forward and backward propagation and find cost and gradients
        cost,gradients = forward_backward_propagation(w,b,x_train,y_train)
        cost_list.append(cost)
        # lets update
        w = w - learning_rate * gradients["derivative_weight"]
        b = b - learning_rate * gradients["derivative_bias"]
        if i % 10 == 0:
            cost_list2.append(cost)
            index.append(i)
            print ("Cost after iteration %i: %f" %(i, cost))
    # we update(learn) parameters weights and bias
    parameters = {"weight": w,"bias": b}
    plt.plot(index,cost_list2)
    plt.xticks(index,rotation='vertical')
    plt.xlabel("Number of Iterarion")
    plt.ylabel("Cost")
    plt.show()
    return parameters, gradients, cost_list
#parameters, gradients, cost_list = update(w, b, x_train, y_train, learning_rate = 0.009,number_of_iterarion = 200)
cs

예측 단계에서는 x_test 를 순방향예측의 입력으로 사용한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# prediction
def predict(w,b,x_test):
    # x_test is a input for forward propagation
    z = sigmoid(np.dot(w.T,x_test)+b)
    Y_prediction = np.zeros((1,x_test.shape[1]))
    # if z is bigger than 0.5, our prediction is sign one (y_head=1),
    # if z is smaller than 0.5, our prediction is sign zero (y_head=0),
    for i in range(z.shape[1]):
        if z[0,i]<= 0.5:
            Y_prediction[0,i] = 0
        else:
            Y_prediction[0,i] = 1
 
    return Y_prediction
# predict(parameters["weight"],parameters["bias"],x_test)
cs

이제 하나로 합쳐보자. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logistic_regression(x_train, y_train, x_test, y_test, learning_rate ,  num_iterations):
    # initialize
    dimension =  x_train.shape[0]  # that is 4096
    w,b = initialize_weights_and_bias(dimension)
    # do not change learning rate
    parameters, gradients, cost_list = update(w, b, x_train, y_train, learning_rate,num_iterations)
    
    y_prediction_test = predict(parameters["weight"],parameters["bias"],x_test)
    y_prediction_train = predict(parameters["weight"],parameters["bias"],x_train)
 
    # Print train/test Errors
    print("train accuracy: {} %".format(100 - np.mean(np.abs(y_prediction_train - y_train)) * 100))
    print("test accuracy: {} %".format(100 - np.mean(np.abs(y_prediction_test - y_test)) * 100))
    
logistic_regression(x_train, y_train, x_test, y_test,learning_rate = 0.01, num_iterations = 150)
cs

결과값은 다음과 같다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cost after iteration 014.014222
Cost after iteration 102.544689
Cost after iteration 202.577950
Cost after iteration 302.397999
Cost after iteration 402.185019
Cost after iteration 501.968348
Cost after iteration 601.754195
Cost after iteration 701.535079
Cost after iteration 801.297567
Cost after iteration 901.031919
Cost after iteration 1000.737019
Cost after iteration 1100.441355
Cost after iteration 1200.252278
Cost after iteration 1300.205168
Cost after iteration 1400.196168
cs


train accuracy: 92.816091954023 %
test accuracy: 93.54838709677419 %

이상의 내용을 sklearn 라이브러리를 사용하여 좀 더 손쉽게 구현해보자. 


Logistic Regression with Sklearn


sklearn 라이브러리에는 로지스틱 회귀를 구현해주는 로지스틱 회귀 메서드가 존재한다. 관심있는 분은 다음 링크를 참조하면 된다. 

https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html


1
2
3
4
from sklearn import linear_model
logreg = linear_model.LogisticRegression(random_state = 42,max_iter= 150)
print("test accuracy: {} ".format(logreg.fit(x_train.T, y_train.T).score(x_test.T, y_test.T)))
print("train accuracy: {} ".format(logreg.fit(x_train.T, y_train.T).score(x_train.T, y_train.T)))
cs
test accuracy: 0.967741935483871 
train accuracy: 1.0