관리 메뉴

한다 공부

[딥러닝 입문] 4장 : 이진 분류 - 로지스틱 회귀 본문

CS/AI

[딥러닝 입문] 4장 : 이진 분류 - 로지스틱 회귀

사과당근 2021. 9. 26. 23:57

이진 분류란 샘플 데이터를 True나 False로 구분하는 문제를 말한다.

이러한 이진 분류를 위한 알고리즘이 많이 존재한다.

 

퍼셉트론

초반 1957년에는 퍼셉트론 알고리즘이 발표되었다.

퍼셉트론은 3장의 선형 회귀와 유사한 구조이다.

입력신호를 받아 z를 만들고, 다시 z를 계단함수를 통해 -1 (0보다 작을 때) 또는 1 (0보다 크거나 같을 때) 로 출력을 해준다.

(입력 -> z -> y_hat)

이때 -1 또는 1로 출력된 y_hat을 역방향 계산하는데 사용해서 가중치와 절편을 업데이트시키며 학습하는 데에 사용해나간다.

 

선형 회귀와 다른 점은 입력 신호가 많다는 점이다.

z=b+w1x1+...+wnxn 으로 나타낼 수 있는데 이를 시그마를 이용해서 나타내기도 한다.

 

아달린

1960년에 퍼셉트론을 개선한 적응형 선형 뉴런이다.

위의 퍼셉트론과 다른 점을 y_hat을 역방향 계산하는데 쓰이는게 아니라 z를 역방향 계산하는데 사용한다.

 

로지스틱 회귀

로지스틱 회귀는 위의 알고리즘과 같이 입력 -> z -> y_hat으로 계산되는 것이 아니라, 입력 -> z -> a -> y_hat 으로 계산이 된다. 이때 역방향 계산은 a를 사용해서 한다.

 

a란 어떻게 도출된 결과냐면, z를 활성화 함수에 넣어 변형시킨 결과이다.

활성화 함수란, 계단 함수와 역할은 비슷하지만, 활성화 함수의 출력값을 계단 함수에 사용한다는 점에서 다르다.

활성화 함수로는 비선형 함수를 사용하는데, 활성화 함수가 선형 함수라면 결론적으로 뉴런 전체가 큰 선형 함수가 되어 별 의미가 없기 때문이다.

 

로지스틱 회귀의 활성화 함수를 시그모이드 함수라고 한다.


시그모이드 함수

위에서 말했듯이 z는 활성화 함수를 거쳐 a가 된다. 그럼 활성화 함수는 무슨 역할을 하는가?

z를 0~1 사이의 확률값으로 변환을 시켜준다.

예를 들어 z를 변환시킨 a가 0.5 이상이면 종양 판정에서 양성, 0.5 이하이면 음성. 이렇게 구분할 수 있다.

 

시그모이드 함수, 즉 로지스틱 함수는 p=1/(1+e^(-z)) 이다.

 

즉 입력된 값은 선형 함수를 통해 z (-무한대 ~ 무한대) 값을 가진다.

z는 활성화 함수를 거쳐 a (0~1) 라는 확률 값을 가지게 된다.

a는 계단함수를 통해, 0 또는 1로 구분된다. (예를들어 0.5 이상이면 1, 아니면 0)


과일을 분류하는 문제에서, 정확도를 높이는 것이 분류의 목표이다.

즉 우리가 사과 배 감을 분류할 때, 분류한 과일 중 진짜 사과 배 감으로 분류한 비율을 높여야 한다.

분류된 샘플의 비율은 미분 가능하지 않으므로 경사 하강법의 손실 함수로 사용할 수 없다.

그래서 로지스틱 손실 함수를 이용해 목표를 달성해야 한다.

 

손실 함수를 계산하는 것도 중요하지만 가장 중요한 것은

로지스틱 손실 함수의 미분을 통해 로지스틱 손실 함수의 값을 최소로 하는 가중치와 절편을 찾아야한다는 점이다. 

이 과정은 너무 수학적인 부분에 치중되어있으므로 건너뛰도록 하겠다!

만약 궁금하다면 교재 87페이지부터 보면 된다.


훈련 데이터 세트 안에는 모델을 학습 시키는 훈련 세트가 있고 테스트에 사용되는 테스트 세트가 있다.

훈련 세트가 테스트 세트보다 많아야한다.

양성, 음성 클래스가 어느 한쪽에 몰리지 않도록 훈련 세트와 테스트 세트에 골고루 섞여야한다.

 

다음 구현된 로지스틱 회귀를 통해 자세히 알아보자

class LogisticNeuron:
    
    def __init__(self):
        self.w = None
        self.b = None

    def forpass(self, x):                # np.sum()은 각 요소를 모두 더한 값을 반환한다
        z = np.sum(x * self.w) + self.b  # 직선 방정식을 계산합니다
        return z

    def backprop(self, x, err):
        w_grad = x * err    # 가중치에 대한 그래디언트를 계산합니다
        b_grad = 1 * err    # 절편에 대한 그래디언트를 계산합니다
        return w_grad, b_grad

    def activation(self, z):       # 시그모이드 함수
        z = np.clip(z, -100, None) # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a
        
    def fit(self, x, y, epochs=100):      # 훈련하는 메소드
        self.w = np.ones(x.shape[1])      # 가중치를 초기화합니다.
        self.b = 0                        # 절편을 초기화합니다.
        for i in range(epochs):           # epochs만큼 반복합니다
            for x_i, y_i in zip(x, y):    # 모든 샘플에 대해 반복합니다
                z = self.forpass(x_i)     # 정방향 계산
                a = self.activation(z)    # 활성화 함수 적용
                err = -(y_i - a)          # 오차 계산
                w_grad, b_grad = self.backprop(x_i, err) # 역방향 계산
                self.w -= w_grad          # 가중치 업데이트
                self.b -= b_grad          # 절편 업데이트
    
    def predict(self, x):                       # 새로운 샘플에 대한 예측값 계산
        z = [self.forpass(x_i) for x_i in x]    # 정방향 계산
        a = self.activation(np.array(z))        # 활성화 함수 적용
        return a > 0.5

 

 

일반적인 신경망은 입력층, 출력층 뿐만 아니라 가운데 은닉층이 있다. 우리가 앞에서 본 신경망은 은닉층이 없는 신경망이다. 이처럼 은닉층이 없는 신경망을 단일층 신경망이라고 한다. 

 

또한 지금까지 사용한 경사 하강법은 샘플 데이터 1개에 대한 그레디언트를 계산했다. 이를 확률적 경사 하강법이라고 하는데, 이 방법은 가중치가 최적값에 수렴하는 과정이 불안정하다. 그렇다고 모든 샘플 데이터를 사용하면 계산 비용이 많이 든다. 이를 보완한 방법이 미니 배치 경사 하강법이다.

 

미니 배치 경사 하강법이란 전체 샘플 중 몇 개의 샘플을 중복되지 않도록 무작위로 선택해서 그레디언트를 계산하는 것이다. 이러한 기능들을 포함시켜 단일망 신경층을 구현을 해보자.

 

class SingleLayer:
    
    def __init__(self):
        self.w = None
        self.b = None
        self.losses = []

    def forpass(self, x):
        z = np.sum(x * self.w) + self.b  # 직선 방정식을 계산합니다
        return z

    def backprop(self, x, err):
        w_grad = x * err    # 가중치에 대한 그래디언트를 계산합니다
        b_grad = 1 * err    # 절편에 대한 그래디언트를 계산합니다
        return w_grad, b_grad

    def activation(self, z):
        z = np.clip(z, -100, None) # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a
        
    def fit(self, x, y, epochs=100):
        self.w = np.ones(x.shape[1])               # 가중치를 초기화합니다.
        self.b = 0                                 # 절편을 초기화합니다.
        for i in range(epochs):                    # epochs만큼 반복합니다
            loss = 0
            # 인덱스를 섞습니다, 미니 배치를 하기 위해
            indexes = np.random.permutation(np.arange(len(x)))
            for i in indexes:                      # 모든 샘플에 대해 반복합니다
                z = self.forpass(x[i])             # 정방향 계산
                a = self.activation(z)             # 활성화 함수 적용
                err = -(y[i] - a)                  # 오차 계산
                w_grad, b_grad = self.backprop(x[i], err) # 역방향 계산
                self.w -= w_grad                   # 가중치 업데이트
                self.b -= b_grad                   # 절편 업데이트
                # 안전한 로그 계산을 위해 클리핑한 후 손실을 누적합니다
                a = np.clip(a, 1e-10, 1-1e-10)
                loss += -(y[i]*np.log(a)+(1-y[i])*np.log(1-a))
            # 에포크마다 평균 손실을 저장합니다
            self.losses.append(loss/len(y))
    
    def predict(self, x):
        z = [self.forpass(x_i) for x_i in x]     # 정방향 계산
        return np.array(z) > 0                   # 스텝 함수 적용
    
    def score(self, x, y):
        return np.mean(self.predict(x) == y)

 

사이킷런에는 경사 하강법이 구현된 클래스, SGDClassifier가 있기 때문에 이를 이용해서도 로지스틱 회귀 문제를 해결할 수 있다.

 

이렇게 3, 4장에 걸쳐 딥러닝을 위한 핵심 알고리즘을 알아보았다!

 

 

 

[참고자료] Do it! 딥러닝 입문