로지스틱 회귀를 위한 뉴런 만들기
훈련된 모델이 실전에서 얼마나 좋은 성능을 내는지 어떻게 알 수 있을까. 우리가 만든 모델의 성능을 평가하지 않고 실전에 투입하면 잘못된 결과를 초래할 수도 있으니 위험하다. 또 훈련 데이터 세트로 학습된 모델을 다시 훈련 데이터 세트로 평가하면 어떨까. 만약 모델이 훈련 데이터 세트를 몽땅 외워버렸다면 평가를 해도 의미가 없을 것이다. 모델을 만들기 전에 성능 평가에 대해 잠시 알아보겠다.
훈련 세트와 테스트 세트
훈련된 모델의 실전 성능을 일반화 성능(generalization performance)이라고 부른다. 그런데 앞에서 말한 것처럼 모델을 학습시킨 훈련 데이터 세트로 다시 모델의 성능을 평가하면 그 모델은 당연히 좋은 성능이 나올 것이다. 이런 성능 평가를 '과도하게 낙관적으로 일반화 성능을 추정한다'고 말한다. 그러면 올바르게 모델의 성능을 측정하려면 어떻게 해야 할까. 훈련 데이터 세트를 두 덩어리로 나누어 하나는 훈련에, 다른 하나는 테스트에 사용하면 된다. 이때 각각의 덩어리를 훈련 세트(training set)와 테스트 세트(test set)라고 부른다.
훈련 데이터 세트를 훈련 세트와 테스트 세트로 나눌 때는 다음 2가지 규칙을 지켜야 한다.
- 훈련 데이터 세트를 나눌 때는 테스트 세트보다 훈련 세트가 더 많아야 한다.
- 훈련 데이터 세트를 나누기 전에 양성, 음성 클래스가 훈련 세트나 테스트 세트의 어느 한쪽에 몰리지 않도록 골고루 섞어야 한다.
위 과정은 사이킷런에 준비되어 있는 도구를 사용하면 편리하게 진행할 수 있다. 물론 우리가 직접 훈련 데이터 세트를 섞고 나눠도 되지만 사이킷런에서 제공하는 편리한 도구를 사용하지 않을 이유가 없다.
훈련 세트와 테스트 세트로 나누기
위에 설명했듯이 훈련 데이터 세트를 훈련 세트와 테스트 세트로 나눌 때는 양성, 음성 클래스가 훈련 세트와 테스트 세트에 고르게 분포하도록 만들어야 한다. 예를 들어 cancer 데이터 세트를 보면 양성 클래스(악성 종양)와 음성 클래스(정상 종양)의 샘플 개수가 각각 212, 357개이다. 이 클래스 비율이 훈련 세트와 테스트 세트에도 그대로 유지되어야 한다. 만약 훈련 세트에 양성 클래스가 너무 많이 몰리거나 테스트 세트에 음성 클래스가 너무 많이 몰리면 모델이 데이터에 있는 패턴을 올바르게 학습하지 못하거나 성능을 잘못 측정할 수도 있다.
지금부터 양성 클래스와 음성 클래스의 비율을 일정하게 유지하면서 훈련 데이터 세트를 훈련 세트와 테스트 세트로 나누어보겠다.
1. train_test_split() 함수로 훈련 데이터 세트 나누기
먼저 sklearn.model_selection 모듈에서 train_test_split() 함수를 임포트한다. 사이킷런의 train_test_split() 함수는 기본적으로 입력된 훈련 데이터 세트를 훈련 세트 75%, 테스트 세트 25%의 비율로 나눠준다.
그런 다음 train_test_split() 함수에 입력 데이터 x, 타깃 데이터 y와 그 밖의 설정을 매개변수로 지정하면 된다.
다음은 매개변수 설정에 대한 설명이다.
1. stratify=y
stratify는 훈련 데이터를 나눌 때 클래스 비율을 동일하게 만든다. train_test_split() 함수는 기본적으로 데이터를 나누기 전에 섞지만 일부 클래스 비율이 불균형한 경우에는 stratify를 y로 지정해야 한다.
2. test_size=0.2
앞에서 이야기했듯이 train_test_split() 함수는 기본적으로 훈련 데이터 세트를 75:25비율로 나눈다. 하지만 필요한 경우 이 비율을 조절하고 싶을 때도 있다. 그럴 때느느 test_size 매개변수에 테스트 세트의 비율을 전달하면 비율을 조절할 수 있다. 여기에서는 입력된 데이터 세트의 20%를 테스트 세트로 나누기 위해 test_size에 0.2를 전달했다.
3. random_state=42
train_test_split() 함수는 무작위로 데이터 세트를 섞은 다음 나눈다. 여기서는 섞은 다음 나눈 결과가 항상 일정하도록 random_state 매개변수에 난수 초깃값 42를 지정했다. 여기서는 실험 결과를 똑같이 재현하기 위해 random_state 매개변수를 사용했는데, 실전에서는 사용할 필요가 없다.
2. 결과 확인
훈련 데이터 세트가 잘 나누어졌는지 훈련 세트와 테스트 세트의 비율을 확인해 보겠다. shape 속성을 이용해 확인해 보니 각각의 훈련 세트와 테스트 세트는 4:1의 비율(455, 114)로 잘 나누어졌다.
3. unique() 함수로 훈련 세트의 타깃 확인
또 넘파이의 unique() 함수로 훈련 세트의 타깃 안에 있는 클래스의 개수를 확인해 보니 전체 훈련 데이터 세트의 클래스 비율과 거의 비슷한 구성이다.
로지스틱 회귀 구현
로지스틱 회귀는 정방향으로 데이터가 흘러가는 과정인 정방향 계산과 가중치를 업데이트하기 위해 역방향으로 데이터가 흘러가는 과정인 역방향 계산을 구현해야 한다. 정방향 계산부터 역방향 계산까지 순서대로 구현해 보겠다.
__init__() 메서드를 보면 입력 데이터의 특성이 많아 가중치를 미리 초기화하지 않았다. 가중치는 나중에 입력 데이터를 보고 특성 개수에 맞게 결정한다.
훈련하는 메서드 구현
훈련을 수행하는 fit() 메서드를 구현해 보겠다.
fit() 메서드의 기본 구조는 이전에 포스팅한 선형 회귀의 Neuron클래스와 같다. 다만 활성화 함수(activation())가 추가된 점이 다르다. 역방향 계산에는 로지스틱 손실 함수의 도함수를 적용한다. 앞에서 초기화하지 않은 가중치는 np.ones()함수를 이용하여 간단히 1로 초기화하고 절편은 0으로 초기화한다.
activation() 메서드에는 시그모이드 함수가 사용되어야 한다. 시그모이드 함수는 자연상수의 지수 함수를 계산하는 넘파이의 np.exp() 함수를 사용하면 간단히 만들 수 있다.
예측하는 메서드 구현
이전 선형 회귀 포스팅에서 새로운 샘플에 대한 예측값을 계산할 때 forpass() 메서드를 사용했다. 여러 개의 샘플을 한꺼번에 예측하려면 forpass() 메서드를 여러 번 호출하게 되는데 이 작업은 번거롭다. 분류에서는 활성화 함수와 임계 함수도 적용해야 하므로 새로운 샘플에 대한 예측값을 계산해 주는 메서드인 predict() 메서드를 만들어보겠다.
predict() 메서드의 매개변수 값으로 입력값 x가 2차원 배열로 전달된다고 가정하고 구현하겠다. 예측값은 입력값을 선형 함수, 활성화 함수, 임계 함수 순서로 통과시키면 구할 수 있다. 앞에서 forpass()와 activation() 메서드를 이미 구현했으니 predict() 메서드는 다음과 같이 간단하게 구현할 수 있다.
로지스틱 회귀 모델 훈련시키기
이제 준비한 데이터 세트를 사용하여 로지스틱 회귀 모델을 훈련해 보고 정확도도 측정해 보겠다.
LogisticNeuron 클래스의 객체를 만든 다음 훈련 세트와 함께 fit() 메서드를 호출한다.
위 코드를 통해 훈련이 끝난 모델에 테스트 세트를 사용해 예측값을 넣고 예측한 값이 맞는지 비교한다.
predict() 메서드의 반환값은 True나 False로 채워진 (m, )크기의 배열이고 y_test는 0 또는 1로 채워진 (m, ) 크기의 배열이므로 바로 비교할 수 있다. np.mean() 함수는 매개변수 값으로 전달한 비교문 결과의 평균을 계산한다. 즉, 계산 결과 0.82...는 올바르게 예측한 샘플의 비율이 된다. 이를 정확도(accuracy)라고 한다.
사실 우리가 구현한 LogisticNeuron 클래스의 성능은 좋은 편이 아니다. 실전에서는 사이킷런과 같은 안정적인 패키지 사용을 권한다.