본문 바로가기

프로그래밍/AI

[혼공머신 11기] 3주차 스터디 및 과제

반응형

3주차 후기

지난주에도 새로운 개념이 많았다고 징징대는 후기를 적었는데, 이번주는 더 어려웠다.
특히 확률적 경사 하강법은 책만 보고 이해가 잘 되지 않아서 인터넷 검색을 많이 해봤다. 머신러닝의 개념이 생소해서 그런지 여전히 "딱 이거다!"하는 느낌으로 와닿지 않는 개념이였다.
나중에 혼자 8장, 9장을 학습할 때 이번에 정리한 내용을 바탕으로 복습해봐야 겠다.


기본 미션

04-1. 확인 문제 2번 풀고, 풀이 과정 설명하기

  • 정답 : (1) 시그모이드 함수
  • 설명 : 이진 분류에서 사용하는 시그모이드 함수는 출력값을 0 ~ 1 사이로 변환하고, 이는 0 ~ 100% 확률로 해석할 수 있다.

선택 미션

04-2. 과대적합/과소적합 손코딩 코랩 화면 캡쳐하기


04. 다양한 분류 알고리즘

핵심 키워드

  • 다중 분류
  • 로지스틱 회귀
  • 시그모이드 함수
  • 소프트맥스 함수

예제 문제

  • 내용
    • 7 종류의 생선이 존재하고, 특정 생선이 어떤 종일지 확률을 계산해야 한다.
  • 특성 종류
    • 길이
    • 높이
    • 두께
    • 대각선
  • 접근법
    • k-최근접 이웃 알고리즘은 주변 이웃을 찾는다. 찾은 이웃들의 클래스 비율을 확률이라고 출력한다.

다중 분류 (multi-class classification)

  • 타깃 데이터에 2개 이상의 클래스가 포함된 문제
    • 예제 문제에서는 타깃 데이터에 생성 종류(Species) 7 종류가 포함되어 있다.
  • 클래스 분류 방법
    • 이진 분류에서는 2개의 클래스를 1과 0으로 분류했다.
    • 다중 분류에서도 각 클래스를 숫자로 관리할 수도 있다.
    • 사이킷런에서는 각 클래스를 문자 그대로 사용할 수 있다.
###
# 데이터 준비하기
###
import pandas as pd

# 데이터 불러오기
fish = pd.read_csv('https://bit.ly/fish_csv_data')
print("# 불러온 생선 데이터 확인하기 ")
print(f"생선의 종류 : {pd.unique(fish['Species'])}")
print("처음 5개의 생선 데이터 출력")
print(fish.head())
print()

# 입력 데이터 선정
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
print('# 입력 데이터 첫 5개 행 출력')
print(fish_input[:5])
print()

# 타깃 데이터 선정
fish_target = fish['Species'].to_numpy()

# 훈련 세트와 테스트 세트 준비
from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state=42)

# 표준화 전처리
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)


###
# k-최근접 이웃 분류기를 사용한 확률 예측
###
# 훈련 세트로 훈련
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)

# 결정 계수 측정
print("# 훈련된 k-최근접 이웃 분류기의 결정 계수(R^2)")
print(f"훈련 세트 : {kn.score(train_scaled, train_target)}")
print(f"테스트 세트 : {kn.score(test_scaled, test_target)}\n")

# 다중 분류 클래스 확인
print("# classes_ 속성을 사용한 다중 분류 클래스 확인")
print(f"다중 분류 클래스 목록 : {kn.classes_}\n")

# 테스트 세트에 있는 첫 5개 샘플의 타깃값 예측
import numpy as np
print("# 테스트 세트에 있는 첫 5개 샘플의 타깃값 예측")

predict = kn.predict(test_scaled[:5])
print(f"예측된 타깃값 : {predict}\n")

proba = kn.predict_proba(test_scaled[:5])
proba = np.round(proba, decimals=2)
print("# 예측된 타깃값의 클래스별 확률값")
print(f"분류 : {kn.classes_}")
for index in range(0, 5):
  print(f"{predict[index]} : {proba[index]}")
print()

print("# 네 번째 샘플의 최근접 이웃 클래스 확인")
distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])
print()

print("# 네 번째 샘플 예측 확률 : 약 67% 확률로 Perch 이거나, 약 33% 확률로 Roach 이다.\n")
print("# 결론 : 3개의 최근접 이웃을 사용하기 때문에 가능한 확률이 0/3, 1/3, 2/3, 3/3 이 전부인데, 이것을 확률이라고 하기엔 조금 어색하다.")

로지스틱 회귀 (logistic regression)

  • 이름은 회귀이지만 분류 모델이다.
  • 선형 회귀와 동일하게 선형 방정식을 학습한다.
    • 선형 방정식 예시
      • a, b, c, d, e는 가중치 또는 계수이다.
      • z 값은 확률이기 때문에 0 ~ 1(또는 0 ~ 100%) 사이의 값이 되어야 한다.
      • z 값이 아주 작은 음수 또는 아주 큰 양수인 경우 → 시그모이드 함수(Sigmoid Function) 또는 로지스틱 함수(Logistic Function)을 사용하여 0 또는 1로 보정한다.
  • LogisticRegression 클래스
    • 반복적인 알고리즘 사용 > max_iter 매개변수로 반복 횟수를 지정한다. (기본값 : 100)
    • 릿지 회귀와 같이 계수의 제곱을 규제한다. (L2 규제)
    • 규제 매개변수는 C이고, 릿지 회귀의 alpha와 달리 값이 작을 수록 규제의 강도가 강해진다.
  • 시그모이드 함수(Sigmoid Function)
    • 하나의 선형 방정식의 출력값을 0 ~ 1로 압축한다.
    • 이진 분류에 사용한다.
    • z가 무한하게 큰 음수일 경우 0에 가까워진다.
    • z가 무한하게 큰 양수일 경우 1에 가까워진다.
    • 즉, $\phi$는 0 ~ 1 사이의 범위가 되므로, 0 ~ 100% 확률로 해석할 수 있다.
  • 소프트맥스 함수(Softmax Function)
    • 여러 개의 선형 방정식의 출력값을 0 ~ 1 사이로 압축하고, 전체 합이 1이 되도록 만든다.
    • 다중 분류에 사용한다.
    • 이를 위해 지수 함수를 사용하기 때문에 정규화된 지수 함수라고도 부른다.
###
# 시그모이드 함수 (Sigmoid Function)
###
import numpy as np
import matplotlib.pyplot as plt

z = np.arange(-5, 5, 0.1)

# 시그모이드 함수
phi = 1 / (1 + np.exp(-z))  # np.exp()로 지수 함수 계산

plt.plot(z, phi)
plt.xlabel('z')
plt.ylabel('phi')

print("# 시그모이드 함수 그래프 출력")
plt.show()

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

###
# 로지스틱 회귀 모델로 이진 분류 수행
###
print("""
## 로지스틱 회귀 모델로 이진 분류 수행
   - 도미, 빙어를 사용한 이진 분류
   - 출력값이 0.5보다 크면 양성
   - 출력값이 0.5보다 작으면 음성
   - 출력값이 0.5이면 음성 : 판단 기준은 라이브러리마다 다르지만, 사이킷런에서는 음성으로 판단
""")

# 불리언 인덱싱 (boolean indexing)
char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(f"""
# 불리언 인덱싱 예시 : {char_arr} 중에서 A, C만 출력하기
{char_arr[[True, False, True, False, False]]}
""")

# 도미와 빙어 데이터만 골라내기
bream_smelt_indexes = (train_target == 'Bream') | (train_target == 'Smelt')
train_bream_smelt = train_scaled[bream_smelt_indexes]
target_bream_smelt = train_target[bream_smelt_indexes]

# 로지스틱 회귀 모델 훈련하기
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)

# 로지스틱 회귀 모델 훈련 결과 확인
print("""
# 도미와 빙어 데이터로 훈련된 로지스틱 회귀 모델 결과
   - train_bream_smelt 첫 5개 샘플 데이터 사용
""")
predict = lr.predict(train_bream_smelt[:5])
predict_proba = lr.predict_proba(train_bream_smelt[:5])
print(f"예측값, 예측 확률({lr.classes_})")
for index in range(0, 5):
  print(f"{predict[index]}, {predict_proba[index]}")
print()

# 로지스틱 회귀가 사용한 계수 확인
coefficient = np.round(lr.coef_[0], decimals=3)
intercept = np.round(lr.intercept_, decimals=3)
print(f"""
# 로지스틱 회귀가 사용한 계수 확인
사용한 계수 목록 : {coefficient}, {intercept}
로지스틱 회귀 모델이 학습한 방정식 :

  z = {coefficient[0]} x (Weight) {coefficient[1]} x (Length) {coefficient[2]} x (Diagonal) {coefficient[3]}
      x (Height) {coefficient[4]} x (Width) {intercept[0]}
""")

# 샘플 5개에 대한 z 값 출력
decisions = lr.decision_function(train_bream_smelt[:5])
print(f"""
# 샘플 5개에 대한 z 값
{decisions}
""")

# 시그모이드 함수를 사용한 확률 변환
from scipy.special import expit
print(f"""
# 시그모이드 함수를 사용한 확률 변환
{expit(decisions)}

=> predict_proba() 메서드 출력의 두번째 열의 값과 동일하다.
   즉, decision_function() 메서드는 양성 클래스에 대한 z 값을 반환한다.
""")

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

###
# 로지스틱 회귀 모델로 다중 분류 수행
###
# 로지스틱 회귀 모델 훈련하기
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(f"""
# 규제 매개변수 C=20, 반복회수 max_iter=1000 설정 시 결정 계수
훈련 세트 : {lr.score(train_scaled, train_target)}
테스트 세트 : {lr.score(test_scaled, test_target)}

=> 훈련 세트와 테스트 세트 모두 점수가 높고, 과대적합 또는 과소적합은 아닌것으로 보인다.\n
""")

# 테스트 세트 처음 5개 샘플에 대한 예측
predict = lr.predict(test_scaled[:5])
predict_proba = np.round(lr.predict_proba(test_scaled[:5]), decimals=3)
print("# 테스트 세트 첫 5개 샘플에 대한 예측 및 확률")
print(f"예측 확률 순서 : {lr.classes_}")
for index in range(0, 5):
  print(f"예측 결과 : {predict[index]}, \t예측 확률 : {predict_proba[index]}")
print("\n=> 7개 생선에 대한 확률 계산이므로 7개의 열이 출력되었다.\n\n")

# 다중 분류 > 선형 방정식
print(f"""# 다중 분류인 경우의 선형 방정식 모형 (행, 열)
coefficient : {lr.coef_.shape} - 7행 5열
intercept : {lr.intercept_.shape} - 7행

=> 다중 분류는 클래스마다 z 값을 모두 계산하고, 그 중에서 가장 높은 값의 클래스를 예측 클래스로 선정한다.\n
""")

# 소프트맥스 함수를 사용하여 확률로 변환
from scipy.special import softmax
decision = lr.decision_function(test_scaled[:5])
proba = softmax(decision, axis=1)
print("# 각 클래스 별 z 값 계산 및 소프트맥스 함수를 사용한 예측 확률 변환")
print(f"순서 : {lr.classes_}\n")
print("샘플 번호, 각 클래스 별 z 값, 소프트맥스 함수로 변환된 각 클래스 별 확률")
for index in range(0, 5):
  print(f"#{index + 1}, {np.round(decision[index], decimals=3)}\t=>\t{np.round(proba[index], decimals=3)}")
print()
print("=> 소프트맥스로 변환한 예측 확률이 predict_proba() 함수를 사용하여 구한 예측 확률과 정확하게 일치한다.")

확률적 경사 하강법

점진적인 학습

  • 앞서 훈련한 모델을 버리지 않고, 새로운 데이터에 대해서만 조금씩 더 훈련하는 방법
  • "온라인 학습"이라고도 부른다.
  • 대표적인 점진적 학습 알고리즘으로 확률적 경사 하강법이 있다.

확률적 경사 하강법

  • 의미
    • 확률적이라는 말은 "무작위하게"의 기술적인 표현이다.
    • 경사는 "기울기"를 의미한다.
    • 하강법은 경사(기울기)를 따라 내려가는 방법을 말한다.
  • 특징
    • 훈련 세트를 사용하여 최적의 장소(?)로 이동하는 알고리즘
    • 훈련 데이터가 모두 준비되어 있지 않고, 지속적으로 업데이트되는 경우에도 계속해서 학습을 할 수 있기 때문에 처음부터 다시 학습할 필요가 없다.
    • 신경망 알고리즘에서 사용한다.
  • SGDClassifier

경사 하강법 종류

무작위로 몇 개의 샘플을 선택하느냐에 따라 구분할 수 있다.

  • 확률적 경사 하강법 (Stochastic Gradient Descent)
    • 무작위로 샘플을 1개씩 선택하여 조금씩 경사를 내려감
  • 미니배치 경사 하강법 (Minibatch Gradient Descent)
    • 무작위로 여러 개의 샘플을 선택하여 조금씩 경사를 내려감
  • 배치 경사 하강법 (Batch Gradient Descent)
    • 전체 샘플을 선택하여 조금씩 경사를 따라 내려감
    • 전체 데이터를 사용하기 때문에 가장 안정적인 방법
    • 컴퓨터 자원을 많이 사용하고, 데이터가 너무 많으면 한 번에 전체 데이터를 읽지 못할 수도 있다.

동작 방식

  1. 훈련 세트에서 샘플을 선택한다. 경사 하강법 종류에 따라 샘플을 얼마나 선택할지는 다르다.
  2. 가파른 경사를 조금 내려간다.
  3. 훈련 세트에서 무작위로 또 다른 샘플을 선택한다.
  4. 다시 경사를 조금 내려간다.
  5. 전체 샘플을 모두 사용할 때까지 위 과정을 반복한다.
  6. 모든 샘플을 다 사용했으나, 경사를 모두 내려오지 못한 경우
    • 6-1) 훈련 세트에 모든 샘플을 다시 채워 넣는다.
    • 6-2) 처음부터 다시 시작하고, 만족할만한 위치에 도달할 때까지 계속 경사를 내려간다.
  7. 훈련 세트를 한 번 모두 사용하는 과정을 epoch라고 부르고, 일반적으로 경사 하강법은 수십, 수백 번 이상의 epoch를 수행한다.

손실 함수 (Loss Function)

  • 어떤 문제에서 머신러닝 알고리즘이 얼마나 엉터리인지를 측정하는 기준
  • 확률적 경사 하강법이 최적화 할 대상
  • 손실 함수의 값이 작을수록 좋다.
  • 손실 함수는 미분 가능해야 한다.
  • SGDClassifier는 여러 종류의 손실 함수를 loss 매개변수에 지정한다.

로지스틱 손실 함수 (Logistic Loss Function)

  • 로지스틱 손실 함수는 이진 크로스엔트로피 손실 함수(Binary Cross-Entropy Loss Function)라고도 부른다.
  • 이진 분류는 "로지스틱 손실 함수"를 사용하고, 다중 분류는 "크로스엔트로피 손실 함수"를 사용한다.
  • 손실 함수를 우리가 직접 만들거나 계산하는 일은 거의 없다. 머신러닝 라이브러리가 처리해준다.
# 판다스 데이터프레임 생성
import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')

# 입력 데이터와 타깃 데이터 지정
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_target = fish['Species'].to_numpy()

# 훈련 세트와 테스트 세트 만들기
from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state=42)

# 표준화 전처리
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

###
# 확률적 경사 하강법
###
from sklearn.linear_model import SGDClassifier

sc = SGDClassifier(loss='log_loss', max_iter=10, random_state=42)
sc.fit(train_scaled, train_target)
print(f"""
# 로지스틱 손실 함수와 epoch=10을 지정한 확률적 경사 하강법 정확도
훈련 세트 정확도 : {sc.score(train_scaled, train_target)}
테스트 세트 정확도 : {sc.score(test_scaled, test_target)}
""")

# 점진적 학습
sc.partial_fit(train_scaled, train_target)
print(f"""
# 점진적 학습 이후 확률적 경사 하강법 정확도
훈련 세트 정확도 : {sc.score(train_scaled, train_target)}
테스트 세트 정확도 : {sc.score(test_scaled, test_target)}
""")

print("=> epoch를 한번 더 실행하니 정확도가 향상되었다. 따라서 여러 epoch에서 더 훈련할 필요가 있다. 그런데 얼마나 더 훈련해야 할까?")

epoch와 과대/과소적합

  • 확률적 경사 하강법은 epoch 횟수에 따라 과소적합 또는 과대적합이 될 수 있다.
    • epoch 횟수가 적음 → 훈련 세트를 덜 학습 → 과소적합 가능성
    • epoch 횟수가 많음 → 훈련 세트를 완전히 학습 → 과대적합 가능성
  • 조기 종료 (Early Stopping)
    • 훈련 세트는 epoch가 진행될수록 점수가 올라간다. 반면 테스트 세트 점수는 어느 순간 감소하기 시작한다.
    • 테스트 세트 점수가 감소하는 지점이 과대적합이 시작하는 곳이다.
    • 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료라고 한다.
import numpy as np
sc = SGDClassifier(loss='log_loss', random_state=42)
train_score = []
test_score = []
classes = np.unique(train_target)

for _ in range(0, 300):
  sc.partial_fit(train_scaled, train_target, classes=classes)
  train_score.append(sc.score(train_scaled, train_target))
  test_score.append(sc.score(test_scaled, test_target))

import matplotlib.pyplot as plt
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
print("=> 그래프를 보면 100번째 epoch가 가장 적절해 보인다.")
print()

# epoch=100 설정하여 훈련
sc = SGDClassifier(loss='log_loss', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

print(f"""
# epoch=100을 지정한 확률적 경사 하강법 정확도
훈련 세트 정확도 : {sc.score(train_scaled, train_target)}
테스트 세트 정확도 : {sc.score(test_scaled, test_target)}
""")

# 힌지 손실
sc2 = SGDClassifier(loss='hinge', max_iter=100, tol=None, random_state=42)
sc2.fit(train_scaled, train_target)

print(f"""
# epoch=100을 지정한 확률적 경사 하강법 정확도
훈련 세트 정확도 : {sc2.score(train_scaled, train_target)}
테스트 세트 정확도 : {sc2.score(test_scaled, test_target)}
""")  

반응형