MLOps 부트캠프 by 한경+토스뱅크/Machine Learning

로지스틱 회귀 - 경사 하강법

나니니 2024. 9. 18. 23:46

로지스틱 회귀의 가설 함수와 손실 함수를 봤으므로 이제 경사 하강법을 알아보자. 

 

가설 함수와 손실 함수는 좀 다르지만, 경사 하강법을 하는 방법은 선형 회귀와 거의 동일하다. 

 

시작점을 위해 처음에는 세타 값들을 모두 0 또는 모두 랜덤으로 지정한다. 그러면 현재 세타 값들에 대한 손실, 즉 현재 가설 함수에 대한 손실을 계산할 수 있다. 여기서부터 시작해서 세타를 조금씩 조율하며 손실을 계속 줄여나가야 한다. 

 

예를 들어, $θ_0, θ_1, θ_2$... 이렇게 세타 값이 3개 있다고 가정하자. 

손실 함수를 $θ$에 대해 편미분하고, 그 결과에 학습률 알파를 곱한다. 

그리고 그 결과를 기존 $θ_0$에서 빼면 된다. 똑같이 $θ_1$과 $θ_2$도 업데이트 하면 된다. 

이렇게 모든 세타값들을 업데이트하면, 경사 하강을 한 번 했다고 할 수 있다. 

그리고 이걸 충분히 반복하면 손실을 최소화 할 수 있다. 

이는 선형 회귀든 로지스틱 회귀든 똑같이 사용할 수 있다. 차이점은 이 손실 함수 $J$가 다르다는 것이다. 

 

로지스틱 회귀를 위해 $J$에 로지스틱 회귀의 손실 함수를 대입해보자. 

 

편미분을 하면 아래와 같이 나온다. 

분명 손실 함수가 다른데, 편미분을 한 결과는 신기하게도 선형 회귀에서와 같다. 

 

세타값이 많아지면 식도 계속 늘어나게 되므로 일반화하여 표현하면 아래와 같다. 

$j$에 0을 넣어서 $θ_0$을 업데이트하고, $j$에 1을 넣어서 $θ_1$을 업데이트하고, $j$에 2를 넣어서 $θ_2$를 업데이트 하는 식으로 하면 된다. 

 

이렇게 선형 획귀와 거의 동일한데, 유일하게 다른 건 가설 함수 $h$이다. 

선형 회귀에서의 가설 함수는 그냥 일차 함수였으나 로지스틱 회귀에서의 가설 함수는 시그모이드 함수이므로 $h$에 시그모이드 함수를 대입하여 각 세타값을 업데이트 하면 된다. 

시그모이드 함수

코드로 구현하기

입력 변수와 파라미터 표현

입력 변수와 파라미터는 선형 회귀를 표현할 때와 동일하게 할 수 있다.

모든 입력 변수 데이터를 이렇게 설계 행렬을, 

입력 변수 데이터

그리고 파라미터를 하나의 벡터로 표현할 수 있다.

모든 데이터 예측 값

이제 가설 함수를 행렬 연산에서 어떻게 표현할 수 있는지 알아보자. 

위와 동일하게 입력 변수와 파라미터를 아래와 같이 표현하면

 

모든 데이터에 대해 $g_θ(x)= θ_0 + θ_1 x_1 +⋯+ θ_n x_n$을 계산할 수 있다. 이는 선형 회귀와 동일하다.

그러나

로지스틱 회귀는 선형 회귀와 가설 함수 $h_θ(x)$가 다르다. 

그러므로 위에서 계산한 모든 결과 값을 시그모이드 함수에 넣어줘야 하는데, 계산한 $Xθ$의 모든 원소를 시그모이드 함수에 넣는다는 의미로 아래처럼 표현하자. 

 

예측 오차

각 데이터의 목표 변수는 값이 하나이므로 선형 회귀와 동일하게 아래와 같이 표현할 수 있다.

$sigmoid(Xθ)$에서 $y$를 빼면, 아래처럼 표현할 수 있다.

모든 예측 값들과 목표 변수의 차이를 간단하게 표현할 수 있는 것이다. 

표현하기 쉽게 이 값을 $error$라고 부르자. 

경사 하강법

로지스틱 회귀는 선형 회귀 경사 하강법 공식이 동일하다. (*가설 함수 $h_0(x)$만 다름)

$0$부터 $n$까지의 $θ$값들을 아래처럼 업데이트 하게 되는데, 경사 하강법도 다중 선형 회귀를 할 때와 동일하게 표현해줄 수 있다.($α$는 학습률, $m$은 데이터 개수)

입력 변수, 파라미터, 예측값 오차를 아래처럼 표현할 때:

아래와 같이 정리할 수 있다.

이렇게 표현하는 계산 과정은 다중 선형 회귀에서의 방식과 동일한데, 이렇게 표현하게 되면 numpy를 이용하여 수학식들을 쉽게 구현할 수있다. 

 

예제 - 1

더보기

로지스틱 회귀 가정 함수를 구현해보자. 

입력 변수와 파라미터를 아래 왼쪽처럼, 행렬의 모든 원소를 시그모이드 함수에 넣어 계산해 준다는 표현을 $sigmoid()$라고 할 때 로지스틱 회귀 가설 함수는 아래 오른쪽처럼 표현할 수 있다.

행렬의 모든 원소에 대해서 시그모이드 함수 결과 값을 계산해주는 sigmoid함수는 이미 구현해 놨으므로 sigmoid 함수를 사용해서 로지스틱 함수의 가정 함수를 구현해 보자. 

sigmoid 함수
이렇게 시그모이드 함수 안에 numpy 배열을 넣으면 모든 원소를 시그모이드 함수에 넣은 결과 값을 구할 수 있습니다.

np_array_1 = np.array([-3, -2, -1, 0, 1, 2, 3])

sigmoid(np_array_1)  # array([ 0.04742587, 0.11920292,  0.26894142,  0.5,   0.73105858,  0.88079708,  0.95257413])

prediction 함수
prediction 함수는 설계 행렬 $X$를 X로, 파라미터 $θ$를 theta로 받는다. 그리고 모든 데이터에 대해서 로지스틱 회귀 예측 값을 numpy 배열로 리턴한다.

 

import numpy as np

def sigmoid(x):
    """시그모이드 함수"""
    return 1 / (1 + np.exp(-x))
    

def prediction(X, theta): # prediction함수는 sigmoid(Xθ)를 구하는 함수이다. 
    """로지스틱 회귀 가정 함수"""
    return sigmoid(X @ theta)


# 입력 변수
hours_studied = np.array([0.2, 0.3, 0.7, 1, 1.3, 1.8, 2, 2.1, 2.2, 3, 4, 4.2, 4, 4.7, 5.0, 5.9])  # 공부 시간 (단위: 100시간)
gpa_rank = np.array([0.9, 0.95, 0.8, 0.82, 0.7, 0.6, 0.55, 0.67, 0.4, 0.3, 0.2, 0.2, 0.15, 0.18, 0.15, 0.05]) # 학년 내신 (백분률)
number_of_tries = np.array([1, 2, 2, 2, 4, 2, 2, 2, 3, 3, 3, 3, 2, 4, 1, 2])  # 시험 응시 횟수

# 설계 행렬 X 정의
X = np.array([
    np.ones(16),
    hours_studied,
    gpa_rank,
    number_of_tries
]).T

# 파라미터 theta 정의
theta = [0.5, 0.3, -2, 0.2]  

prediction(X, theta)

# 출력 결과
array([ 0.26114999,  0.28699984,  0.37989357,  0.39174097,  0.57199613,
        0.55971365,  0.59868766,  0.54735762,  0.72312181,  0.80218389,
        0.86989153,  0.87653295,  0.85814894,  0.91293423,  0.86989153,
        0.9289057 ])

예제 - 2

더보기

다중 선형 회귀를 구현할 때와 똑같이 아래처럼 표현할 수 있다. 이를 이용하여 경사 하강법을 직접 구현해 보자. 

gradient_descent함수
gradient_descent 함수는 파라미터로 입력 변수 X, 파라미터 theta, 목표 변수 y, 경사 하강 횟수 iterations 그리고 학습률 alpha를 받습니다. 이 입력 변수들로 경사 하강법을 사용해서 찾은 최적화된 theta를 리턴해 줍니다.

import numpy as np

def sigmoid(x):
    """시그모이드 함수"""
    return 1 / (1 + np.exp(-x))
    
    
def prediction(X, theta):
    """로지스틱 회귀 가정 함수"""
    return sigmoid(X @ theta)
    

def gradient_descent(X, theta, y, iterations, alpha):
    """로지스틱 회귀 경사 하강 알고리즘"""
    m = len(X)  # 입력 변수 개수 저장

    for _ in range(iterations):
        error = prediction(X, theta) - y 
        	# error=sigmoid(Xθ)−y
            	# prediction(X, theta)에는 모든 데이터에 대한 예측 값이, 그리고 y에는 모든 데이터의 실제 목표 변수가 저장돼 있다. 
                	# 둘의 오차를 구하면 error를 구할 수 있다. 
        theta = theta - alpha / m * (X.T @ error)
            
    return theta
    
    
 # 입력 변수
hours_studied = np.array([0.2, 0.3, 0.7, 1, 1.3, 1.8, 2, 2.1, 2.2, 3, 4, 4.2, 4, 4.7, 5.0, 5.9])  # 공부 시간 (단위: 100시간)
gpa_rank = np.array([0.9, 0.95, 0.8, 0.82, 0.7, 0.6, 0.55, 0.67, 0.4, 0.3, 0.2, 0.2, 0.15, 0.18, 0.15, 0.05]) # 학년 내신 (백분률)
number_of_tries = np.array([1, 2, 2, 2, 4, 2, 2, 2, 3, 3, 3, 3, 2, 4, 1, 2])  # 시험 응시 횟수

# 목표 변수
passed = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])  # 시험 통과 여부 (0: 탈락, 1:통과)

# 설계 행렬 X 정의
X = np.array([
    np.ones(16),
    hours_studied,
    gpa_rank,
    number_of_tries
]).T

# 입력 변수 y 정의
y = passed

theta = [0, 0, 0, 0]  # 파라미터 초기값 설정
theta = gradient_descent(X, theta, y, 300, 0.1)  # 경사 하강법을 사용해서 최적의 파라미터를 찾는다
theta

# 출력 결과
array([-1.35280508,  1.61640725, -1.83666046, -0.60286277])