관리 메뉴

클라이언트/ 서버/ 엔지니어 "게임 개발자"를 향한 매일의 공부일지

다양한 분류 알고리즘 1 - 로지스틱 회귀 1 : 로지스틱 회귀 알고리즘으로 클래스 확률 예측하기 본문

인공지능/머신러닝

다양한 분류 알고리즘 1 - 로지스틱 회귀 1 : 로지스틱 회귀 알고리즘으로 클래스 확률 예측하기

huenuri 2024. 9. 29. 08:05

이제 새로운 단원 학습을 시작해 본다. 로지스틱 회귀 모델은 지난번 수업 시간에 공부했던 내용이기도 하지만 아직 기초가 부족하니 이 학습을 통해 많은 것을 배울 수 있을 것 같다.


 

 

 

학습 목표

로지스틱 회귀 알고리즘을 배우고 이진 분류 문제에서 클래스 확률을 예측하기

 

 

시작하기 전에

혼공머신은 이제 럭키 백에 포함된 생선이 무엇인지 확률을 구하는 문제를 해결해야 한다.

 

 

머신러닝으로 럭키백의 생선이 어떤 타깃에 속하는지 확률을 구할 수 있을까?


 

 

 

 

럭키백의 확률

김팀장은 혼공머신에게 럭키백에 들어갈 수 있는 생선은 7개라고 알려주었다. 이벤트를 잘 마치려면 럭키백에 들어간 생선의 크기, 무게 등이 주어졌을 때 7개 생선에 대한 확률을 출력해야 한다. 이번에는 길이, 높이, 두께 외에도 대각선 길이와 무게도 사용할 수 있다.

 

 

 

혼공머신은 최근접 이웃 모델을 사용하면 이 문제를 해결할 수 있지 않을까 생각한다. 샘플 X 주위에 가까운 이웃 10개를 표시했다. 이웃한 샘플의 클래스를 확률로 삼는다면 샘플 X가 사각형일 확률은 30%, 삼각형은 50%, 원은 20%이다.

이제 데이터를 준비하고 k-최근접 이웃 분류기로 럭키백에 들어간 생선의 확률을 계산해 보겠다.


 

 

 

데이터 준비하기

인터넷에서 직접 CSV 데이터를 읽어 들인다. 판다스의 read_csv() 함수로 CSV 파일을 데이터프레임으로 변환한 다음 head() 메서드로 처음 5개 행을 출력했다.

 

 

 

 

왼쪽의 0, 1, 2, ..와 값을 숫자는 행 번호이다. 맨 위에 쓰인 Species, Weight 등은 열 제목이다. 그리고 데이터 프레임은 판다스에서 제공하는 2차원 표 형식의 주요 데이터 구조이다. 데이터프레임은 넘파이 배열과 비슷하게 열과 행으로 이루어져 있다. 그리고 통계와 그래프를 위한 메서드를 풍부하게 제공한다. 데이터프레임은 넘파이로 상호 변환이 쉽고 사이킷런과도 잘 호환된다.

 

어떤 종류의 생선이 있는지 알기 위해 Species 열에서 고유한 값을 추출한다. 이 데이터프레임에서 Species 열을 타깃으로 만들고 나머지 5개 열은 입력 데이터로 사용한다. 데이터프레임에서 열을 선택하는 방법은 원하는 열을 리스트로 나열하면 된다. Species 열을 빼고 나머지 5개를 선택했다.

이제 데이터를 훈련 세트와 데이터 세트로 나눈다. 그런 다음 앞에서 배운 대로 훈련 세트와 테스트 세트를 표준화 전처리한다. 여기서도 훈련 세트의 통계값으로 테스트 세트를 변환해야 한다. 그런 다음 k-최근접 이웃 분류기로 테스트 세트에 들어있는 확률을 예측해 보겠다.


 

 

 

k-최근접 이웃 분류기의 확률 예측

사이킷런의 KNeighborsClassifier 클래스 객체를 만들고 훈련 세트로 모델을 훈련한 다음 훈련 세트와 테스트 세트의 점수를 확인해 본다. 최근접 이웃 개수인 k를 3으로 지정하여 사용한다. 여기서는 클래스 확률을 배우는 것이 목적이므로 훈련 세트와 테스트 세트 점수에 대해서는 잠시 잊기로 하겠다.

타깃 데이터를 만들 때 fish['Species']를 사용해 만들었기 때문에 훈련 세트와 테스트 세트의 타깃 데이터에도 7개의 생선 종류가 들어있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류라고 부른다.

 

 

이진 분류를 사용했을 때는 양성 클래스와 음성 클래스를 각각 1과 0으로 지정하여 타깃 데이터를 만들었다. 디중 분류에서도 타깃값을 숫자로 바꾸어 입력할 수 있지만 사이킷런에서는 편리하게도 문자로 된 타깃값을 그대로 사용할 수 있다.

타깃값을 그대로 사이킷런 모델에 전달하면 순서가 자동으로 알파벳 순으로 매겨진다.

 

KNeighborsClassifier에서 정렬된 타깃값은 classes_ 속성에 저장되어 있다.

테스트 세트에 있는 처음 5개의 샘플의 타깃값을 예측해 보았다. 5개 샘플에 대한 예측은 어떤 확률로 만들어졌을까? 사이킷런의 분류 모델은 predict_proba() 메서드로 클래스별 확률값을 반환한다. 테스트 세트에 있는 5개 샘플에 대한 확률을 출력해 보면 이와 같다. round() 함수는 기본적으로 소수점 첫째 자리에서 반올림을 하는데, decimals 매개변수로 유지할 소수점 아래 자릿수를 지정할 수 있다. 이렇게 5개의 샘플이 7개 생선에 대한 확률로 출력되었다.

 

첫 번째 열이 'Bream'에 대한 확률, 두 번째 열이 'Parkki'에 대한 확률이다. 3개의 최근접 이웃을 사용하기 때문에 가능한 확률은 0/3, 1/3, 2/3, 3/3이 전부이다. 이제 다른 더 좋은 방법을 찾아보기로 하자.


 

 

 

로지스틱 회귀

로지스틱 회귀는 이름은 회귀이지만 분류 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다. 여기서에 a, b, c, d, e는 가중치 혹은 계수이다.

 

특성은 늘어났지만 다중 회귀를 위한 선형 방정식과 같다. z는 어떤 값도 가능하다. 하지만 확률이 되려면 0~1(또는 0~100%) 사이값이 되어야 한다. z가 아주 큰 음수일 때 0이 되고, z가 아주 큰 양수일 때 1이 되도록 바꾸는 방법은 다음과 같다. 시그모이드 함수(또는 로지스틱 함수)를 사용하면 가능하다.

 

왼쪽의 식이 시그모이드 함수이다. 선형 방정식의 출력 z의 음수를 사용해 자연 상수 e를 거듭제곱하고 1을 더한 값의 역수를 취한다. 이 식을 계산하면 오른쪽과 같은 그래프가 만들어진다. z가 무한하게 큰 음수일 경우 이 함수는 0에 가까워지고, z가 무한하게 큰 양수가 될 때는 1에 가까워진다. z가 0이 될 때는 0.5가 된다. z가 어떤 값이 되더라도 Ø는 절대로 0~1 범위를 벗어날 수 없다. 

 

넘파이를 사용하면 그래프를 간단히 그릴 수 있다. -5와 5 사이에 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수를 계산한다. 지수 함 수 계산은 np.exp() 함수를 사용한다. 사이킷런에는 로지스틱 회귀 모델인 LogisticRegression 클래스가 준비되어 있다.

훈련하기 전에 간단히 이진 분류를 수행해 보겠다. 이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성 클래스, 0.5보다 작으면 음성 클래스로 분류한다. 그럼 먼저 도미와 빙어 2개를 사용해서 이진 분류를 수행해보겠다.


 

 

 

로지스틱 회귀로 이진 분류 수행하기

넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다. 이름 불리언 인덱싱이라고 한다. 예를 들어, A에서 E까지 5개의 원소로 이루어진 배열이 있다고 할 때, 여기서 A와 C만 골라내는 방법은 다음과 같다. 첫 번째와 세 번째 원소만 True이고 나머지 원소는 모두 False인 배열을 전달하면 된다.

이와 같은 방식을 사용해 훈련 세트에서 도미(Bream)와 빙어(Smelt)의 행만 골라내겠다. 이 비교식은 train_target 배열에서 Bream인 것은 True이고 그 외에는 모두 False인 배열을 반환한다. 이 비교 결과를 비트 or연산자를 사용해 합치면 도미와 빙어에 대한 행만 골라낼 수 있다.

 

 

훈련한 모델을 사용해 train_bream_smelt에 있는 처음 5개의 샘플을 예측해 보았다. 두 번째 샘플을 제외하고는 모두 도미로 예측했다. 샘플마다 2개의 확률이 출력되었다. 첫 번째 열이 음성 클래스(0)에 대한 확률이고 두 번째 열이 양성 클래스(1)에 대한 확률이다. 이 둘 중 어떤 것이 양성 클래스일까? classes_ 속성을 통해 타깃값을 알파벳순으로 정렬한다.

predict_proba() 메서드가 반환한 배열값을 보면 두 번째 샘플만 양성 클래스인 빙어의 확률이 높다. 이제 선형 회귀에서처럼 로지스틱 회귀가 학습한 계수를 확인해 보자.


 

 

 

 

 

로지스틱 회귀로 성공적인 이진 분류를 수행했다. LogisticRegression 모델로 z값을 계산해 볼 수도 있다. LogisticRegression 클래스는 decision_function()에서 메서드로 z값을 출력할 수 있다. train_bream_smelt의 처음 5개 샘플의 z값을 출력했다. 이 z 값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있다. 

이진 분류를 위해 2개의 생선 샘플을 골라냈고 이를 사용해 로지스틱 회귀 모델을 훈련했다. 이진 분류일 경우 predict_proba() 메서드는 음성 클래스와 양성 클래스에 대한 확률을 출력한다 


 

 

 

로지스틱 회귀로 다중 분류 수행하기

이제 7개의 생선을 분류하는 다중 분류 문제로 넘어가 보겠다. LogisticRegression 클래스는 기본적으로 반복적인 알고리즘을 사용한다. max_iter 매개변수에서  반복 횟수를 지정하며 기본값은 100이다. 충분하게 훈련시키기 위해 반복 횟수를 1000으로 늘리겠다. 또 LogisticRegression은 기본적으로 릿지 회귀와 같이 계수의 제곱을 규제한다. 이런 규제를 L2 규제라고도 부른다. LogisticRegression에서 규제를 제어하는 매개변수는 C이다. 하지만 C는 alpha와 반대로 작을수록 규제가 커진다. C의 기본값은 1이다. 여기서는 규제를 조금 완화하기 위해 20으로 늘리겠다.

 

 

 

훈련 세트와 테스트 세트에 대한 점수가 높고 과대적합이나 과소적합으로 치우친 것 같지 않다. 이제 5개의 샘플에 대한 예측 확률을 출력했다. 첫 번째 샘플을 보면 세 번째 열의 확률이 가장 높다. 세 번째 열이 농어에 대한 확률인지 확인해 보자. 이진 분류는 샘플마다 2개의 확률을 출력하고 다중 분류는 샘플마다 클래스 개수만큼 확률을 출력한다. 여기서는 7개이다. 이 중에서 가장 높은 확률이 예측 클래스가 된다.

 

다중 분류일 경우 선형 방정식은 어떤 모습일까? coef_와 intercept_의 크기를 출력해 보았다. 이 데이터는 5개의 특성을 사용하므로 coef_ 배열의 행렬은 5개이다. intercept_도 7개나 있다. 이 말은 z를 7개나 계산한다는 의미이다. 다중 분류는 클래스마다 z 값을 하나씩 계산한다. 가장 높은 z 값을 출력하는 클래스가 예측 클래스가 된다. 그럼 확률을 어떻게 계산한 것일까?

이진 분류에서는 시그모이드 함수를 사용해 z를 0과 1 사이의 값으로 변환했다. 다중 분류는 이와 달리 소프트맥스 함수를 사용하여 7개의 z값을 확률로 변환한다.


 

 

 

 

먼저 7개의 z값의 이름을 z1에서 z7이라고 붙이겠다. 이를 지수함수를 사용해 모두 더한다. 그런 다음 e_sum으로 나누어주면 된다. s1에서 s7까지 모두 더하면 분자와 분모가 같아지므로 1이 된다. 7개 생선에 대한 확률의 합은 1이 되어야 하므로 잘 맞다.

 

그럼 이진 분류에서처럼 decision_funciton() 메서드로 z1~z7까지의 값을 구한 다음 소프트맥스 함수를 사용해 확률로 바꾸어보겠다. 테스트 세트의 처음 5개 샘플에 대한 z1~z7의 값을 구했다. 사이파이는 소프트맥스 함수도 제공한다. 

앞서 구한 decision 배열을 softmax() 함수에 전달했다. softmax()의 axis 매개변수는 소프트맥스를 계산할 축을 지정한다. 여기서는 axis=1로 지정하여 각 행, 즉 각 샘플에 대해 소프트맥스를 계산한다.

 

만약 axis 매개변수를 지정하지 않으면 배열 전체에 대해 소프트맥스를 계산한다. 출력 결과를 앞서 구한 proba 배열과 비교해 보라. 결과가 정확하게 일치한다. 혼공머신은 로지스틱 회귀를 사용해 7개의 생선에 대한 확률을 예측하는 모델을 훈련했다.

 


 

 

학습을 마치고

강의를 들으며 내용을 정리해 보았다. 처음에는 책에 나오는 모든 코드를 자료로 만드셨던 강사님도 나중에는 귀찮아졌는지 빠진 코드들이 많아서 그것까지 정리하느라 시간이 전보다 더 많이 걸렸다. 그리고 점점 더 어려워지기도 했다. 무슨 말인지 모르는 것들이 많아서 한 50%밖에 이해하지 못한 것 같다. 이전 단원도 이해도가 그리 높지 않았다.

아마 처음 학습하는 것이니 그럴 것이다. 하지만 반복되는 코드가 많으니 계속 공부하다 보면 나중에는 충분히 이해할 수 있으리라 믿는다.

 

1시간 만에 끝내고 7시 반에는 운동을 가고 싶었으나 오늘도 8시가 다 되고 말았다. 30분만 일찍 일어날걸 하는 후회도 든다. 하지만 오늘부터는 진짜 아침에 운동을 꼭 하려고 한다. 쉽지 않겠지만 말이다.