데이터분석(AI학습 24)

이유진·2024년 7월 8일

--06.분류.iptnb--

분류 (classification)

여러개의 종류(class 라고 부릅니다) 중 하나를 구별해 내는 문제를 분류 (classification) 라고 부릅니다.

만약 2개의 클래스중 하나를 고르는 문제를 이진 분류(binary classification) 이라고 함

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

Fish 데이터 준비

base_path=r'/content/drive/MyDrive/dataset'

file_path = os.path.join(base_path, 'fish.csv')

fish_df = pd.read_csv(file_path)
fish_df

fish_df.info()

fish_df.Species.unique()

fish_df.Species.value_counts()

"""

  • Perch '농어'
  • Bream '도미'
  • Roach '로치, Common Roach 잉어과 담수어'
  • Pike '강꼬치고기, Northern pike'
  • Smelt '빙어'
  • Parkki '청돔'
  • Whitefish '송어'
    """
    None

Fish(생선) 분류 (classification) 문제

  • 생선의 길이(length)무게 (weight) 만으로 생선의 종류(species) 를 자동으로 분류 가능할까?
  • 분류할 생성
    • Bream '도미'
    • Smelt '빙어'

도미와 빙어 데이터 준비

도미 35마리의 데이터

길이

bream_length = fish_df[fish_df.Species == 'Bream']['Length'].to_list()
print(bream_length)

#무게
bream_weight = fish_df[fish_df.Species == 'Bream']['Weight'].to_list()
print(bream_weight)

빙어 14마리의 데이터

길이

smelt_length = fish_df[fish_df.Species == 'Smelt']['Length'].to_list()
print(smelt_length)

#무게
smelt_weight = fish_df[fish_df.Species == 'Smelt']['Weight'].to_list()
print(smelt_weight)

feature

  • 데이터의 특징/특성
  • 데이터를 표현하는 성질

'길이'와 '무게'는 생선을 데이터의 특징(feature)

산점도를 통해 두 변수(feature)의 관계를 시각화

plt.scatter(bream_length, bream_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
생선의 길이(length)가 길수록 무게가 많이 나가는건 자연스러운 관계입니다.

이와 같이 그래프가 일직선에 가까운 형태로 나타나는 경우를 '선형(linear)'하다 라고 한다.
"""
None

도미와 빙어 데이터 수치를 눈으로 보자

print(bream_length)
print(bream_weight)
print('-' * 20)
print(smelt_length)
print(smelt_weight)

두 생선의 데이터를 한번에 확인.

plt.scatter(bream_length, bream_weight)
plt.scatter(smelt_length, smelt_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
주황색이 빙어의 산점도 데이터다.

빙어는 도미에 비해 길이도 무게도 매우 작습니다.
그러나, 그보다 더 중요하게 바라보아야 하는것은 연관정도입니다.

빙어도 도미와 비슷하게 길이와 무게가 비례하지만, 늘어나는 정도가 조금 다릅니다.
빙어의 산점도도 선형적이긴 하지만, 무게가 길이에 영향을 덜 받는다고 볼 수 있다.

이제 위 두 생성의 데이터로 생선을 분류(classification) 하기 위한 머신러닝 프로그램을 작성해보자.
"""
None

머신러닝의 입력(input) 데이터 준비

  • feature vector 형태로 준비

k-최근접 이웃 (k-Nearest Neighbors, KNN) 알고리즘을 사용해 도미와 빙어 데이터를 구분해보자

도미와 빙어데이터를 하나의 데이터로 합친다

length = bream_length + smelt_length
weight = bream_weight + smelt_weight

print(length)

print(weight)

"""
scikit-learn 에선 'data'를
각 '특성들' 의 '배열' 형태로 만들어야 한다 -> 2차원 배열

길이 무게
[ ↓ ↓
[25.4, 242.0],
[26.3, 290.0],
[26.5, 340.0],
...
[15.0, 19.9]
]

  • 이를 '입력(input)' 이라고도 하고 'feature vector' 라고도 함. 이는 scikit-learn 뿐 아니라,
    대부분의 프레임워크에서도 이러한 방식의 input 을 사용한다.
    """
    None

fish_data = [
[l, w]
for l, w in zip(length, weight)
]
print(fish_data)

머신러닝의 답안 준비

  • 지도학습 (supervised learning) 에선 입력데이터에 대한 답안 이 필요하다
  • 이를 label(레이블) 혹은 target(타겟) 이라 한다
  • 머신러닝 알고리즘은 이를 계산 가능한 '숫자' 형태로 알려주어야 한다

"""
'데이터'는 준비되었다.
이제, 머신 러닝 알고리즘에게 이 데이터가 '어떤 생선' 인지도 '답안지' 도 알려주어야 합니다.
그래야 학습(지도학습) 을 할수 있습니다.

그러면 각각의 데이터에 '도미', '빙어' 라고 알려주면 될까요?
머신러닝 알고리즘은 '문자를 이해 못함' 이들 데이터도 '숫자' 로 표현하여 알려주어야 함

도미는 1 로, 빙어는 0 으로 표현된 '답안'을 준비해보겠습니다

★ 이러한 답안을 label(레이블), 혹은 'target(타켓)' 이라 합니다
"""
None

"""
머신러닝 쪽에서는

data 를 'x' 로, target 을 'y' 로 표기하는 경우가 많다
특히 data 는 'feature 들의 벡터' 라서 대문자 'X' 로 표기하곤 한다.
"""
None

도미를 1로 표현하고, 빙어를 0으로 표현하는 target 만들기

fish_target = [1] 35 + [0] 14
print(fish_target)

머신러닝의 모델 준비

모델(model) 이란
머신러닝 알고리즘을 구현한 프로그램,

혹은, 프로그램은 아니더라도 알고리즘을 (수식등으로) 구체화 하여 표현한 것.

from sklearn.neighbors import KNeighborsClassifier

모델 객체 생성

kn = KNeighborsClassifier()

classifier라고 함

KNN(k-최근접 이웃, k-Neareset Neighbors 알고리즘) 이란?

어떤 데이터에 대한 답을 구할때 '주변의 다른 데이터'를 보고 '다수를 차지하는 것을 정답'으로 사용함.

plt.scatter(bream_length, bream_weight)
plt.scatter(smelt_length, smelt_weight)
plt.scatter(30, 600, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

모델 학습하기

  • 이 과정을 training(훈련) 혹은 fitting (적합) 한다 라고도 함.
  • 주어진 모델에, 주어진 데이터를 통해 규칙을 찾아가는 과정
  • 모델 알고리즘을 구성하는 각종 파라미터(parameter) 값들이 입력된 데이터를 학습하면서 최적의 값으로 변화되어 간다

fish_data

print(fish_target)

모델 객체 kn이 학습되었다.

kn.fit(fish_data, fish_target)

학습한 결과 : 정확도 (accuracy)

얼마나 잘 학습했는지 점수 확인!

score(data, target) => 0.0 ~ 1.0 사이값 1.0 은 100% 맞춘다는 뜻.

이를 accuracy(정확도) 라고 함.

kn.score(fish_data, fish_target)

학습된 모델을 통해 예측(predict)하기

predict()

  • 학습한 모델에 새로이 '관측'된 데이터를 넣어 예측 결과 리턴
  • 예측에는 target 값 필요 없다
  • predict() 의 입력데이터도 'feature 들의 배열'로 넣어주어야 한다
  • 리턴값은 '예측값들의 배열'

kn.predict([[30, 600]])

"""
array([1])

'1' 값을 예측함. '1' 은 '도미' 니까 정확히 예측한것

"""
None

kn.predict([[30, 600],[12, 0.5]])

KNN 모델의 속성 들여다보기

"""
KNN 의
장점: 데이터만 있으면 된다.
단점: 데이터가 너무 많으면 사용하기 힘들다 (많은 메모리, 많은 거리 계산, ...)

"""
None

KNeighborsClassifier 의 속성중

_fit_X : 전달한 data 들을 가지고 있다

_y : 전달한 target 값들을 가지고 있다.

print(kn._fit_X)

print(kn._y)

"""
실제로 KNN 알고리즘은 딱히 무엇가 훈련(train) 되는게 없는 셈이다.
fit() 에 전달한 데이터를 저장하고 있다가 새로운 데이터가 등장하면 가장 '가까운 데이터들'을 참고하여
어떤 생선인지만 구분하는 겁니다.

그러면 '가까운 몇개의 데이터'를 참고할게 될까?
이는 n_neighbors 값으로 정해줄수 있다. (기본값 5)
"""
None

kn49 = KNeighborsClassifier(n_neighbors=49) # 인접하는 참고 데이터 49개로 설정

fish_data 49개중 도미가 35개를 차지하므로 어떤 데이터를 넣어도 무조건 '도미' 로 예측하게 된다.

오히려 정확도 (accuracy) 가 떨어지게 된다.

kn49.fit(fish_data, fish_target)
kn49.score(fish_data, fish_target)

"""
KNeighborsClassifier 의 매개변수들

  • n_neighbors: 이웃의 개수

  • p : 매개변수 거리 재는 방법.
    1 - 맨해튼 거리
    2 - 유클리디안 거리 (디폴트)

  • n_jobs: 사용할 CPU 코어 지정. 이웃간의 거리 계산속도를 높일수는 있지만 fit() 메소드에는 영향이 없다
    1 - 기본값
    -1 - 모든 CPU 코어 사용
    """
    None

과연 n_neighbors가 몇개일 때 부터 정확도(acuracy)가 100%에 미치지 못하게될까?

kn = KNeighborsClassifier()
kn.fit(fish_data, fish_target)

for n in range(5,50) :

최근접 이웃 개수 설정

kn.n_neighbors = n

점수 계산

score = kn.score(fish_data, fish_target)
print(n, score)

100% 즉 (1,0)에 미치지 못하는 경우 출력후 종료

if score < 1:
break


훈련 세트 와 테스트 세트

주어진 '문제'와 '답안지' 로 열심히 공부해서 문제집 문제집 100점 맞으면 그 학생은 잘한다 할수 있나? 진짜 잘하는지는 문제집에 없던 문제들을 풀어도 점수가 좋아야 한다.

그래서 data 와 target을 학습(training) 하는데 다 집어 넣는 것이 아니라, 훈련세트와 테스트세트를 나누어서 모델 훈련 및 검증에 사용한다

  • 훈련 (training) 세트 : 모델 훈련 용 데이터 셋
  • 테스트 (test) 세트 : 모델 평가(검증) 용 데이터 셋

도미 : 35마리, 빙어 14 마리

fish_data[4] # 하나의 데이터를 'sample(샘플)' 이라고 함.

fish_data[0:5] # 이것도 샘플이라 할 수 있다.

fish_data[44:]

"""

데이터의 처음 35개를 train 세트 (훈련세트), 나머지 14개를 test 세트 (테스트 세트)로 사용해보도록 해보자

[
[25.4, 242.0], ─┐
[26.3, 290.0], │ train 세트 35개
[26.5, 340.0], │
... ─┘
... ─┐
... │ test 세트 14개
[15.0, 19.9] ─┘
]

"""
None

train_input = fish_data[:35]
train_target = fish_target[:35]

test_input = fish_data[35:]
test_target = fish_target[35:]

kn.fit(train_input, train_target)
kn.score(test_input, test_target)

"""
↑ 정확도 가 0.0 이 나왔다. 하나도 맞추지 못하고 있다?! 왜 그럴까?

바로 '샘플링 편향 (sampling bias)' 문제다

train 세트에 도미 만 잔뜩 있어서 이를 훈련시키고
test 세트에는 빙어 만 잔뜩 있었으니 결과는 0 이 나온거다

해결 -> 골고루 섞어야 한다
"""
None

numpy를 사용하여 샘플들을 섞어보자

input_arr = np.array(fish_data)
target_arr = np.array(fish_target)

input_arr.shape

random index 생성

np.random.seed(42)
index = np.arange(49)
np.random.shuffle(index)

print(index)

train 세트

train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]

만들어진 index 의 첫번째 값은 13이다,

따라서 train_input 의 [0]번째 원소와 input_arr 의 [13] 원소는 같을 것이다.

input_arr[13], train_input[0]

test 세트

test_input = input_arr[index[35:]]
test_target = target_arr[index[35:]]

train 세트와 test 세트가 적절히 잘 섞였는지 확인 (시각화)

plt.scatter(train_input[:, 0], train_input[:, 1])
plt.scatter(test_input[:, 0], test_input[:, 1])
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

train 데이터로 학습하기

kn = KNeighborsClassifier()
kn.fit(train_input, train_target)

kn.score(test_input, test_target)

kn.predict(test_input)

test_target

데이터 전처리 (preprocess)

문제점 발견!

길이 25cm, 무게 150g 도미(1)라고 생각됨.

kn.predict([[25,150]])

하지만 결과는 빙어(0)이 나옴.

plt.scatter(bream_length, bream_weight)
plt.scatter(smelt_length, smelt_weight)
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
↑ 흠... 저 정도면... 도미(1) 로 분류되어야 하는거 아닐까?
도미에 더 가깝잖아?
"""
None

이런 일이 왜 일어나는지 살피기 위해 몇가지 학습하고 진행하자

numpy 의 column_stack() 함수

전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결.

np.column_stack(([1, 2, 3], [4, 5, 6]))

도미 35, 빙어 14 마리의 feature 들을 다 합쳐놓기

fish_length = bream_length + smelt_length
fish_weight = bream_weight + smelt_weight

fish_data = np.column_stack((fish_length, fish_weight))

초반 5개만 확인

print(fish_data[:5])

비슷한 방법으로 target 데이터도 만들기

fish_target = np.concatenate((np.ones(35), np.zeros(14)))

fish_target

사이킷럿으로 train세트와 test세트 나누기

from sklearn.model_selection import train_test_split

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, random_state=42)

주어진 data 와 target 을 무작위로 섞은뒤 train:test 를 3:1 로 분리 (디폴트)

학습 예제시 동일 결과를 내기 위해 ransom_state 값을 줌

print(train_input.shape, test_input.shape)

print(train_target.shape, test_target.shape)

print(train_target)

print(test_target)

"""
[1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

↑ 잘 섞인듯? 하지만, 빙어의 비율이 조금 모잘라 보인다
도미:빙어 = 35:14 이므로 이는 2.5:1 되어야 한다.

그러나 위 결과는 3.3:1 이다. (샘플링 편향: sampling bias 발생!)

train:test 의 비율이 일정하지 않다면 모델이 일부 샘플을 올바르게 학습할수 없을것이다.

train_test_split() 에 stratify 매개변수에 target 을 전달하면 클래스 비율에 맞게 데이터를 나눈다.
train 데이터가 작거나 특정 클래스의 샘플 개수가 적을 때 특히 유용.
"""
None

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, random_state=42, stratify=fish_target)

print(test_target)

"""
↑ 빙어(0) 이 하나 더 늘었다. 이제 test 세트 비율이 2.25:1 이 되었다. 꽤 비슷한 비율이 되었다.
"""
None

학습

kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

kn.predict([[25, 150]]) # 여전히 빙어(0)로 결과가 나온다.

왜 그런지 시각화로 확인해보자

plt.scatter(bream_length, bream_weight)
plt.scatter(smelt_length, smelt_weight)
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
분명히 (25, 150) 은 빙어(0) 보다 도미(1) 에 가까이 있다.
그런데 왜 모델은 멀리 떨어진 왼쪽 아래에 낮게 깔린 빙어 데이터에 가깝다고 판단한걸까?

KNN 은 주변의 샘플 중에서 '다수' 인 클래스를 예측으로 사용합니다.
이 샘플(25, 150)의 주변샘플은 어떠한 것들이 있는지 들여다 봅시다.

kneighbors() 메소드를 통해 할수 있다
"""
None

이웃까지의 거리(들)과 이웃 샘플에 대한 인덱스(들)을 리턴함.

distances, indexes = kn.kneighbors([[25, 150]])

현재 n-neighbors 는 기본값 5이기 때문에 5개의 neighbor 들이 리턴된다.

distances

indexes

위 인덱스들은 어떤 데이터(들)인지 시각화하여 확인해보자

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes, 0], train_input[indexes, 1], marker='D') # neighbor 들을 따로 찍어보자 (마름모)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
5개의 이웃 중에서 도미(1)는 한개, 빙어(0)는 무려 4개?

이웃의 데이터 학인
"""
None

print(train_input[indexes])

"""
[[[ 25.4 242. ] <- 요건 도미 구..
[ 15. 19.9] <- 다음 네개는 확실히 빙어다..
[ 14.3 19.7] <- 빙어
[ 13. 12.2] <- 빙어
[ 12.2 12.2]]] <- 빙어

↓ 이는 target 데이터로 보면 더 명확하다
"""
None

print(train_target[indexes])

"""
↑ 길이 25cm, 무게 150g 인 생선의 가장 가까운 이웃에 빙어가 압도적으로 많다.
그래서 모델은 빙어라고 예측한거다.

왜일까? scatter plot 에서 보면 직관적으로 '도미' 에 더 가깝게 보이는데?

이를 확인해보기 위해선 kneighbors() 에서 리턴한 distances 배열을 들여다 보면 알수 있다. <-- 이웃간의 거리가 담겨있다
"""
None

print(distances)

"""
위 scatter plot 을 살펴보면, 첫번째 샘플과의 거리가 92이고, 그외 가까운 샘플들은 모두 130, 138 이다.
그런데... 그래프에 보여진 거리 비율이 이상하지 않나요?
어림짐작으로 보아도 92 보다 족히 몇배는 되어 보이는데, 겨우 거리가 130이라니? 수상하다?

눈치 채셨나요?
x 축의 범위와 y 축의 범위가 다르다!
x축 (length)의 범위는 (10 ~ 40)
y축 (weight)의 범위는 (0 ~ 1000) x축보다 더 넓다!

따라서 y 축으로 조금만 멀어져도 '거리'가 아주 큰 값으로 계산된다. 그래서 가까이 보이는 도미 샘플이 이웃으로 선택되지 못한거다

이를 눈으로 명확하게 확인하기 위해 x축의 범위도 동일하게 0 ~ 1000으로 맞추어 보자.
"""
None

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')
plt.xlim((0, 1000)) # x 축도 0 ~ 1000 으로 맞추어 보자.
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
산점도가 거의 일직선으로 보인다. 생선의 legnth 는 가장 가까운 이웃을 찾는데 크게 영향을 못미침을 알수 있다.
거의 생선의 weight (y축) 만 거리계산에 크게 영향을 줄것이다.
"""

"""
두 개의 feature(특성), 즉 length 와 weight 의 값이 놓인 범위가 매우 다르다.
이를 두 feature 의 'scale(스케일)이 다르다' 라고도 말합니다.

특성간 스케일이 다른 일은 얼마든지 있을수 있습니다.
어떤 사람이 방의 넓이를 재는데, 세로는 cm 로 가로는 inch 로 쟀다면 정사각형인 방도 직사각형처럼 보일겁니다

데이터를 표현하는 기준이 다르면, 머신러닝 알고리즘이 제대로 학습되고 제대로 예측하기 힘듭니다.
(특히 KNN 과 같이 '거리' 기반이라면 더더욱..)

따라서!
feature 들이 머신러닝에서 서로 동등한 영향력을 행사하도록 할려면 일정한 기준으로 맞추어야 한다
이러한 작업들을 데이터 전처리 (data preprocessing) 이라 하고,
feature 들을 일정한 기준으로 맞추는 작업을 스케일링(scaling) 한다고 함

"""
None

모든 알고리즘이 거리 기반은 아닙니다

가령, 트리 기반 알고리즘은 feature 의 스케일이 다르더라도 잘 동작함.

"""
스케일링의 대표적인 방법 2가지

  • 표준화 (standardazation)
  • 정규화 (normalization)

표준점수 (standard score) 를 사용하여 표준화를 해봅시다
표준점수는 z 점수라고도 합니다
표준점수는 각 feature 값이 평균에서 표준편차의 몇배만큼 떨어져 있는지를 나타냄.
이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교 가능.
"""
None

'분산' 은 데이터에서 평균을 뺀 값을 모두 제곱한 다음 평균을 내는 것

'표준편차' 는 분산의 제곱근 <- 데이터의 분산된 정도 표현

'표준점수' 는 각 데이터가 원점에서 몇 표준편차 만큼 떨어져 있는지 나타내는 값

train_input (36,2)

특성마다 스케일이 다르다, 따라서 평균과 표준편차는 각 특성(feature)별로 계산해야 함.

axis=0을 지정

mean = np.mean(train_input, axis = 0)
std = np.std(train_input, axis = 0)

print(mean, std) # 각 특성마다 평균과 표준편차 구해짐.

원본 데이터에서 평균을 빼고 표준편차로 나누어 표준점수로 변환

train_scaled = (train_input - mean) / std # broadcating에 의해 연산

print(train_scaled)

위 데이터와 샘플 (25, 150)을 시각화 해보자

plt.scatter(train_scaled[:, 0], train_scaled[:, 1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
↑ 예상과 다르다.
오른쪽 맨 꼭대기에 수상한 샘플 하나만 덩그러니 떨어져 있다.

이렇게 된이유는?
train 세트를 mean(평균) 으로 빼고 std(표준편차)로 나누어 주었기 때문에
'값의 범위'가 크게 달라졌습니다.
따라서! 샘플 [25, 150] 을 동일한 비율로 변환하지 않으면 이런 현상이 발생한다

명심!
반드시 train 세트의 mean, std 를 이용해서 '샘플도 변환'해야 한다!

"""

None

new = ([25, 150] - mean) / std

plt.scatter(train_scaled[:, 0], train_scaled[:, 1])
plt.scatter(new[0], new[1], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

"""
위 그래프는 앞서 표준편자로 변환하기 전의 산점도와 거의 동일.
크게 달라진 점은 x축 (length) 와 y축(weight) 의 범위가 -1.5 ~ 1.5 사이로 바뀌었다는 점.
train 데이터의 두가지 feature(특성) 이 비슷한 범위를 차지하고 있다.

이제 이 데이터 셋으로 KNN 모델을 다시 훈련해보자
"""
None

kn.fit(train_scaled, train_target)

"""
test 세트로 평가할때도 train 세트의 평균과 표준편차로 변환해야 같은 비율로 산점도를 그릴수 있다.

↓마찬가지로 test 세트도 train 세트의 평균과 표준변차로 변환하자
"""
None

test_scaled = (test_input - mean) / std

kn.score(test_scaled, test_target)

문제의 관측데이터를 예측해보자

kn.predict([new])

KNN에서 과연 어떤 데이터를 neighbor로 측정했는지 시각화 해보자

distances, indexes = kn.kneighbors([new])

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')
plt.scatter(train_scaled[indexes,0], train_scaled[indexes,1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

profile
독해지자

0개의 댓글