본문 바로가기

카테고리 없음

[ML 세션] 혼공머신 Chapter 01. 나의 첫 머신러닝 & Chapter 02. 데이터 다루기

Chapter 01. 나의 첫 머신러닝

01-1 인공지능과 머신러닝, 딥러닝

인공지능이란

인공지능은 사람처럼 학습하고 추론할 수 있는 지능을 가진 컴퓨터 시스템을 만드는 기술입니다.인공일반지능 or 강인공지능은 사람과 구분하기 어려운 지능을 가진 컴퓨터 시스템입니다. 약인공지능은 아직까지는 특정 분야에서 사람의 일을 도와주는 보조 역할만 가능합니다.(ex. 음성 비서, 자율 주행 자동차, 음악 추천, 기계 번역 등) 

 

머신러닝이란

머신러닝은 규칙을 일일이 프로그래밍하지 않아도 자동으로 데이터에서 규칙을 학습하는 알고리즘을 연구하는 분야입니다. 인공지능의 하위 분야 중에서 지능을 구현하기 위한 소프트웨어를 담당하는 핵심 분야입니다.

머신러닝은 통계학과 깊은 관련이 있습니다. 하지만 최근 머신러닝의 발전은 통계나 수학 이론보다 경험을 바탕으로 발전하는 경우도 많습니다. 컴퓨터 과학 분야가 이런 발전을 주도하고 있습니다. 컴퓨터 과학 분야의 대표적인 머신러닝 라이브러리는 사이킷런입니다. 사이킷런 라이브러리는 파이썬 API(배우기 쉽고 컴파일 X)를 사용합니다. 

 

새로운 머신러닝 알고리즘을 많은 연구자들이 검증하고 사용해 본 다음 장단점을 파악합니다. 일정 시간이 지나 알고리즘이 유익하다고 증명되어 널리 사용하게 되면 사이킷런 라이브러리 개발자들이 이 알고리즘을 라이브러리에 추가합니다. 따라서 머신러닝 라이브러리에 포함된 알고리즘들은 안정적이며 성능이 검증되어 있습니다.

 

사이킷런이 있기 전까지 머신러닝 기술은 대부분 폐쇄적인 코드와 라이브러리로 통용되었습니다. 하지만 사이킷런과 같은 오픈소스 라이브러리의 발전 덕분에 파이썬 코드를 다룰 수 있다면 누구나 머신러닝 알고리즘을 무료로 손쉽게 제품에 활용할 수 있게 되었습니다.

딥러닝이란

많은 머신러닝 알고리즘 중에 인공 신경망을 기반으로 한 방법들을 통칭하여 딥러닝이라고 부릅니다.

 

인공 신경망이 이전과 다르게 놀라운 성능을 달성하게 된 원동력

- 복잡한 알고리즘을 훈련할 수 있는 풍부한 데이터

- 컴퓨터 성능의 향상

- 혁신적인 알고리즘 개발

 

대표적인 딥러닝 라이브러리

- 구글의 텐서플로(TensorFlow)

- 페이스북의 파이토치(PyTorch)

-> 공통점 : 인공 신경망 알고리즘을 전문으로 다루고 있음, 파이썬 API를 제공함.

 

01-2 코랩과 주피터 노트북

구글 코랩

구글 코랩은 웹 브라우저에서 무료로 파이썬 프로그램을 테스트하고 저장할 수 있는 서비스입니다. 머신러닝은 컴퓨터 사양이 중요한데, 구글 코랩을 사용하면 컴퓨터 성능과 상관없이 프로그램을 실습해 볼 수 있습니다. 코랩은 웹 브라우저에서 텍스트와 프로그램 코드를 자유롭게 작성할 수 있는 온라인 에디터입니다. 이런 코랩 파일을 노트북 혹은 코랩 노트북이라고 부릅니다.

텍스트 셀

은 코랩에서 실행할 수 있는 최소 단위입니다. 텍스트 셀에서 HTML과 마크다운을 혼용해서 사용할 수 있습니다.

코드 셀

코드 셀로 이동하면 코드와 결과가 함께 선택됩니다.

노트북

코랩은 구글이 대화식 프로그래밍 환경인 주피터를 커스터마이징한 것입니다. 주피터 프로젝트의 대표 제품이 바로 노트북입니다. 흔히 주피터 노트북이라고 부릅니다.

 

코랩 노트북은 구글 클라우드의 가상 서버를 사용합니다. 이 노트북은 구글 클라우드의 컴퓨트 엔진에 연결되어 있습니다. 이 서버의 메모리는 약 12기가이고 디스크 공간은 100기가입니다. 구글 계정만 있으면 코랩 노트북을 사용해 무료로 가상 서버를 활용할 수 있습니다. 하지만 무료라 부담 없는 반면에 제한 사항도 있습니다. 코랩 노트북으로 동시에 사용할 수 있는 구글 클라우드의 가상 서버는 최대 5개입니다. 또한 1개의 노트북을 12시간 이상 실행할 수 없습니다.

 

 

01-3 마켓과 머신러닝

생선 분류 문제

 

캐글은 2010년에 설립된 전 세계에서 가장 큰 머신러닝 경연 대회 사이트입니다. 대회 정보뿐만 아니라 많은 데이터와 참고 자료를 제공합니다.

 

문제 풀이

1. 생선 길이가 30cm 이상이면 도미

if fish_length <= 30:
	print("도미")

-> 하지만 30cm보다 큰 생선이 무조건 도미라고 말할 수 없습니다. 머신러닝은 누구도 알려주지 않는 기준을 찾아서 일을 합니다. 머신러닝은 기준을 찾을 뿐만 아니라 이 기준을 이용해 생선이 도미인지 아닌지 판별할 수도 있습니다.

 

도미 데이터 준비하기

* 이진 분류

머신러닝에서 여러 개의 종류(클래스) 중 하나를 구별해 내는 문제를 분류라고 부릅니다. 특히 2개의 클래스 중 하나를 고르는 문제를 이진 분류라고 합니다. 여기에서 클래스는 파이썬 프로그램의 클래스와는 다릅니다.

 

bream_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0]
bream_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0]

 

특성은 데이터의 특징입니다.

두 특성을 숫자로 보는 것보다 그래프로 표현하면 데이터를 더 잘 이해할 수 있고 앞으로 할 작업에 대한 힌트를 얻을 수도 있습니다. 길이를 x축으로 하고 무게를 y축으로 정하겠습니다. 그다음 각 도미를 이 그래프에 점으로 표시해 보죠. 이런 그래프를 산점도라고 부릅니다.

 

파이썬에서 과학계산용 그래프를 그리는 대표적인 패키지는 맷플롯립입니다. 이 패키지를 임포트하고 산점도를 그리는 scatter() 함수를 사용해 보겠습니다. 임포트란 따로 만들어둔 파이썬 패키지(함수 묶음)를 사용하기 위해 불러오는 명령입니다.

 

* 코랩에서의 패키지와 as

패키지는 기능을 구현한 함수를 특정 기능별로 묶어둔 것입니다. 보통은 이런 패키지를 따로 설치해야 하지만 코랩에서는 맷플롯립 같은 패키지를 따로 설치할 필요가 없습니다. 파이썬 프로그래머들은 패키지를 임포트할 때 as 키워드로 패키지 이름을 줄여서 쓰는 것을 좋아합니다. 대표적인 파이썬 패키지들은 이미 널리 사용되는 줄임말이 있습니다. 코드를 읽기 쉽게 만들려면 널리 사용되는 스타일을 따르는 것이 좋습니다.

import matplotlib.pyplot as plt

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

 

이렇게 산점도 그래프가 일직선에 가까운 형태로 나타나는 경우를 선형적이라고 말합니다. 머신러닝에서는 선형이란 단어가 종종 등장합니다.

 

빙어 데이터 준비하기

smelt_length = [9.8, 10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
smelt_weight = [6.7, 7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

 

맷플롯립에서 2개의 산점도를 한 그래프로 그리는 것은 아주 간단합니다. 다음처럼 scatter() 함수를 연달아 사용하면 됩니다.

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

 

주황색 : 빙어, 파란색 : 도미

첫 번째 머신러닝 프로그램

가장 간단하고 이해하기 쉬운 k-최근접 이웃 알고리즘을 사용

 

도미와 빙어 데이터를 하나의 데이터로 합치겠습니다.

length = bream_length + smelt_length
weight = bream_weight + smelt_weight

아주 간단하게 두 리스트를 하나로 합쳤습니다. 이제 length와 weight 리스트는 다음과 같습니다.

사이킷런 패키지를 사용하려면 다음처럼 각 특성의 리스트를 세로 방향으로 늘어뜨린 2차원 리스트를 만들어야 합니다.

이렇게 만드는 가장 쉬운 방법은 파이썬의 zip() 함수와 리스트 내포 구문을 사용하는 것입니다. zip() 함수는 나열된 리스트 각각에서 하나씩 원소를 꺼내 반환합니다. zip() 함수와 리스트 내포 구문을 사용해 length와 weight 리스트를 2차원 리스트로 만들어 보겠습니다.

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

* zip() 함수

zip() 함수는 나열된 리스트에서 원소를 하나씩 꺼내주는 일을 합니다.

 

for 문은 zip() 함수로 length와 weight 리스트에서 원소를 하나씩 꺼내어 l과 w에 할당합니다. 그러면 [l, w]가 하나의 원소로 구성된 리스트가 만들어집니다.

print(fish_data)
[[25.4, 242.0], [26.3, 290.0], [26.5, 340.0], [29.0, 363.0], [29.0, 430.0], [29.7, 450.0], [29.7, 500.0], [30.0, 390.0], [30.0, 450.0], [30.7, 500.0], [31.0, 475.0], [31.0, 500.0], [31.5, 500.0], [32.0, 340.0], [32.0, 600.0], [32.0, 600.0], [33.0, 700.0], [33.0, 700.0], [33.5, 610.0], [33.5, 650.0], [34.0, 575.0], [34.0, 685.0], [34.5, 620.0], [35.0, 680.0], [35.0, 700.0], [35.0, 725.0], [35.0, 720.0], [36.0, 714.0], [36.0, 850.0], [37.0, 1000.0], [38.5, 920.0], [38.5, 955.0], [39.5, 925.0], [41.0, 975.0], [41.0, 950.0], [9.8, 6.7], [10.5, 7.5], [10.6, 7.0], [11.0, 9.7], [11.2, 9.8], [11.3, 8.7], [11.8, 10.0], [11.8, 9.9], [12.0, 9.8], [12.2, 12.2], [12.4, 13.4], [13.0, 12.2], [14.3, 19.7], [15.0, 19.9]]

이런 리스트를 2차원 리스트 혹은 리스트의 리스트라고 부릅니다.

 

마지막으로 준비할 데이터는 정답 데이터입니다. 혼공머신은 머신러닝 알고리즘이 생선의 길이와 무게를 보고 도미와 빙어를 구분하는 규칙을 찾기를 원합니다. 그렇게 하려면 적어도 어떤 생선이 도미인지 방어인지를 알려 주어야 합니다.

 

머신러닝은 물론이고 컴퓨터 프로그램은 문자를 직접 이해하지 못합니다. 대신 도미와 빙어를 숫자 1과 0으로 표현해 보겠습니다. 정답 리스트는 1이 35번 등장하고 0이 14번 등장하면 됩니다. 곱셈 연산자를 사용하면 파이썬 리스트를 간단하게 반복시킬 수 있습니다.

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

<머신러닝에서 2개를 구분하는 경우 찾으려는 대상을 1로 놓고 그 외에는 0으로 놓습니다.>

 

이제 사이킷런 패키지에서 k-최근접 이웃 알고리즘을 구현한 클래스인 KNeighborsClassifier를 임포트합니다.

 

* from ~ import 구문

파이썬에서 패키지나 모듈 전체를 임포트하지 않고 특정 클래스만 임포트하려면 from ~ import 구문을 사용합니다. 이렇게 하면 다음과 같이 클래스 이름을 길게 사용하지 않아도 됩니다.

import sklearn
model = sklearn.neighbors.KNeighborsClassifier()

 

임포트한 KNeighborsClassifier 클래스의 객체를 먼저 만듭니다.

kn = KNeighborsClassifier()

이 객체에 fish_data와 fish_target을 전달하여 도미를 찾기 위한 기준을 학습시킵니다. 이런 과정을 머신러닝에서는 훈련이라고 부릅니다. 사이킷런에서는 fit() 메서드가 이런 역할을 합니다.

kn.fit(fish_data, fish_target)

❓❓fit() 메서드는 주어진 데이터로 알고리즘을 훈련시킨 뒤 훈련합니다.❓❓

 

이제 객체(또는 모델) kn이 얼마나 잘 훈련되었는지 평가해 보겠습니다. 사이킷런에서 모델을 평가하는 메서드는 score() 메서드입니다. 이 메서드는 맞춘 비율에 따라 0에서 1 사이의 값을 반환합니다.

 

* 머신러닝에서의 모델

머신러닝 알고리즘을 구현한 프로그램을 모델이라고 부릅니다. 또는 프로그램이 아니더라도 알고리즘을 (수식 등으로) 구체화하여 표현한 것을 모델이라고 부릅니다.

kn.score(fish_data, fish_target)
1.0

이 값을 정확도라고 부릅니다.

 

k-최근접 이웃 알고리즘

k-최근접 이웃 알고리즘은 어떤 데이터에 대한 답을 구할 때 주위의 다른 데이터를 보고 다수를 차지하는 것을 정답으로 사용합니다.

kn.predict([[30, 600]])
-> array([1])

predict() 메서드는 새로운 데이터의 정답을 예측합니다. 이 메서드도 앞서 fit() 메서드와 마찬가지로 리스트의 리스트를 전달해야 합니다. 그래서 삼각형 포인트를 리스트로 2번 감쌌습니다. 반환되는 값은 1입니다. 즉 삼각형은 도미입니다.

 

k-최근접 이웃 알고리즘을 위해 준비해야 할 일

- 데이터를 모두 가지고 있는 것

단점 : 데이터가 아주 많은 경우 사용하기 어려움 -> 데이터가 크기 때문에 메모리가 많이 필요하고 직선거리를 계산하는 데도 많은 시간이 필요합니다.

 

가까운 거리의 데이터를 참고하지만 참고하는 데이터의 개수는 정하기 나름입니다. KNeighborsClassifier 클래스의 기본값은 5입니다. 이 기준은 n_neighbors 매개변수로 바꿀 수 있습니다.

kn49 = KNeighborsClassifier(n_neighbors=49)

참고 데이터를 49개로 한 kn49 모델에 fish_data를 적용하면 fish_data에 있는 모든 생선을 사용하여 예측하게 됩니다. 다시 말하면 fish_data의 데이터 49개 중에 도미가 35개로 다수를 차지하므로 어떤 데이터를 넣어도 무조건 도미로 예측할 것입니다.

kn49.fit(fish_data, fish_target)
kn49.score(fish_data, fish_target)
-> = 35/49

 

* 결괏값은 왜 한 번만 출력될까요?

코드 셀은 마지막 실행 코드의 반환값만을 자동 출력합니다. 모든 코드를 한 셀에 넣으면 중간의 반환값은 출력하지 않습니다.

Chapter 02. 데이터 다루기

02-1 훈련 세트와 테스트 세트

지도 학습과 비지도 학습

머신러닝 알고리즘은 크게 지도 학습 비지도 학습으로 나눌 수 있습니다. 지도 학습 알고리즘은 훈련하기 위한 데이터와 정답이 필요합니다. 지도 학습에서는 데이터와 정답을 입력 타깃이라고 하고, 이 둘을 합쳐 훈련 데이터라고 부릅니다.

반면 비지도 학습 알고리즘은 타깃 없이 입력 데이터만 사용합니다. 이런 종류의 알고리즘은 정답을 사용하지 않으므로 무언가를 맞힐 수가 없습니다. 대신 데이터를 잘 파악하거나 변형하는 데 도움을 줍니다.

훈련 세트와 테스트 세트

머신러닝 알고리즘의 성능을 제대로 평가하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 합니다. 이렇게 하는 가장 간단한 방법은 평가를 위해 또 다른 데이터를 준비하거나 이미 준비된 데이터 중에서 일부를 떼어 내어 활용하는 것입니다. 일반적으로 후자의 경우가 많습니다. 평가에 사용하는 데이터를 테스트 세트, 훈련에 사용되는 데이터를 훈련 세트라고 부릅니다.

 

먼저 1장에서처럼 도미와 빙어의 데이터를 합쳐 하나의 파이썬 리스트로 준비합니다.

fish_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0, 9.8, 
                10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
fish_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0, 6.7, 
                7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

이제 두 파이썬 리스트를 순회하면서 생선의 길이와 무게를 하나의 리스트로 담은 2차원 리스트를 만들겠습니다.

fish_data = [[l, w] for l, w in zip(fish_length, fish_weight)]
fish_target = [1]*35 + [0]*14

이때 하나의 생선 데이터를 샘플이라고 부릅니다. 전체 데이터는 49개의 샘플이 있습니다. 사용하는 특성은 길이와 무게 2개입니다. 이 데이터의 처음 35개를 훈련 세트로, 나머지 14개를 테스트 세트로 사용하겠습니다.

 

먼저 사이킷런의 KNeighborsClassifier 클래스를 임포트하고 모델 객체를 만듭니다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()

이제 전체 데이터에서 처음 35개를 선택해야 합니다. 일반적으로 리스트처럼 배열의 요소를 선택할 때는 배열의 위치, 즉 인덱스를 지정합니다. 파이썬 리스트는 인덱스 외에도 슬라이싱이라는 특별한 연산자를 제공합니다. 예를 들어 첫 번째부터 다섯 번째까지의 샘플을 선택해 보겠습니다.

print(fish_data[0:5])

슬라이싱을 사용할 때는 마지막 인덱스의 원소는 포함되지 않는다는 점을 주의해야 합니다.

 

만약 '0:5'와 같이 처음부터 시작되는 슬라이싱의 경우 0을 생략하고 쓸 수 있습니다. 이와 비슷하게 마지막 원소까지 포함할 경우 두 번째 인덱스를 생략할 수 있습니다.

print(fish_data[:5])
print(fish_data[44:])
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_input = fish_data[:35]
# 훈련 세트로 타깃값 중 0부터 34번째 인덱스까지 사용
train_target = fish_target[:35]
# 테스트 세트로 입력값 중 35번째부터 마지막 인덱스까지 사용
test_input = fish_data[35:]
# 테스트 세트로 타깃값 중 35번째부터 마지막 인덱스까지 사용
test_target = fish_target[35:]

슬라이싱 연산으로 처음 35개 샘플을 훈련 세트로 선택했고, 나머지 14개 샘플을 테스트 세트로 선택했습니다. 데이터를 준비했으니 훈련 세트로 fit() 메서드를 호출해 모델을 훈련하고, 테스트 세트로 score() 메서드를 호출해 평가해 보겠습니다.

kn = kn.fit(train_input, train_target)
kn.score(test_input, test_target)
#-> 0.0 (훈련 세트에는 도미만, 테스트 세트에는 빙어만 들어가 있음)

샘플링 편향

일반적으로 훈련 세트와 테스트 세트에 샘플이 골고루 섞여 있지 않으면 샘플링이 한쪽으로 치우쳤다는 의미로 샘플링 편향이라고 부릅니다.

넘파이

넘파이는 파이썬의 대표적인 배열 라이브러리입니다. 넘파이는 고차원의 배열을 손쉽게 만들고 조작할 수 있는 간편한 도구를 많이 제공합니다.

보통의 xy 좌표계와는 달리 시작점이 왼쪽 아래가 아니고 왼쪽 위에서부터 시작합니다. 배열의 시작점을 이렇게 놓으면 편리한 점이 많습니다.

 

그럼 생선 데이터를 2차원 넘파이 배열로 변환해 보겠습니다. 먼저 넘파이 라이브러리를 임포트합니다.

import numpy as np

파이썬 리스트를 넘파이 배열로 바꾸기는 정말 쉽습니다. 넘파이 array() 함수에 파이썬 리스트를 전달하면 끝입니다.

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

넘파이는 친절하게 배열의 차원을 구분하기 쉽도록 행과 열을 가지런히 출력합니다.

눈으로 확인하는 것 외에도 넘파이 배열 객체는 배열의 크기를 알려주는 shape 속성을 제공합니다.

print(input_arr.shape)
#-> (49, 2)

이제 생선 데이터를 넘파이 배열로 준비했으므로 이 배열에서 랜덤하게 샘플을 선택해 훈련 세트와 테스트 세트로 만들 차례입니다. 여기에서는 배열을 섞은 후에 나누는 방식 대신에 무작위로 샘플을 고르는 방법을 사용하겠습니다. 한 가지 주의할 점은 input_arr과 target_arr에서 같은 위치는 함께 선택되어야 한다는 점입니다.

아예 인덱스를 섞은 다음 input_arr과 target_arr에서 샘플을 선택하면 무작위로 훈련 세트를 나누는 셈이 됩니다.

넘파이 arange() 함수를 사용하면 0에서부터 48까지 1씩 증가하는 인덱스를 간단히 만들 수 있습니다. 그다음 이 인덱스를 랜덤하게 섞습니다.

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

넘파이 arange() 함수에 정수 N을 전달하면 0에서부터 N-1까지 1씩 증가하는 배열을 만듭니다. 넘파이 random 패키지 아래에 있는 shuffle() 함수는 주어진 배열을 무작위로 섞습니다.

print(index)
'-> [13 45 47 44 17 27 26 25 31 19 12  4 34  8  3  6 40 41 46 15  9 16 24 33
 30  0 43 32  5 29 11 36  1 21  2 37 35 23 39 10 22 18 48 20  7 42 14 28
 38]'

 

넘파이는 슬라이싱 외에 배열 인덱싱이란 기능을 제공합니다. 배열 인덱싱은 1개의 인덱스가 아닌 여러 개의 인덱스로 한 번에 여러 개의 원소를 선택할 수 있습니다.

print(input_arr[[1,3]])
'-> [[ 26.3 290. ]
 [ 29.  363. ]]'

비슷한 방식으로 리스트 대신 넘파이 배열을 인덱스로 전달할 수도 있습니다.

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

이번에는 나머지 14개를 테스트 세트로 만들어 보겠습니다.

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

훈련 세트와 테스트 세트에 도미와 빙어가 잘 섞여 있는지 산점도로 그려 봅시다.

import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(test_input[:,0], test_input[:,1])
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
#❓잘 모르겠음❓

파란색이 훈련 세트이고 주황색이 테스트 세트입니다. 양쪽에 도미와 빙어가 모두 섞여 있네요.

두 번째 머신러닝 프로그램

앞서 만든 훈련 세트와 테스트 세트로 k-최근접 이웃 모델을 훈련시켜 봅시다. fit() 메서드를 실행할 때마다 KNeighborsClassifier 클래스의 객체는 이전에 학습한 모든 것을 잃어버립니다. 이전 모델을 그대로 두고 싶다면 KNeighborsClassifier 클래스 객체를 새로 만들어야 합니다.

kn = kn.fit(train_input, train_target)

인덱스를 섞어 만든 train_input과 train_target으로 모델을 훈련시켰습니다. 다음은 test_input과 test_target으로 이 모델을 테스트할 차례입니다.

kn.score(test_input, test_target)
#-> 1.0

predict() 메서드로 테스트 세트의 예측 결과와 실제 타깃을 확인해 보겠습니다.

kn.predict(test_input)
#-> array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])
test_target
#-> array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])

predict() 메서드의 출력 결과가 test_target의 출력과 동일하게 array()로 감싸 있는 것을 눈여겨보세요. 이 값은 넘파이 배열을 의미합니다. 즉 predict() 메서드가 반환하는 값은 단순한 파이썬 리스트가 아니라 넘파이 배열입니다. 사실 사이킷런 모델의 입력과 출력은 모두 넘파이 배열입니다.

02-2 데이터 전처리

넘파이로 데이터 준비하기

fish_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0, 9.8, 
                10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
fish_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0, 6.7, 
                7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

파이썬 리스트를 순회하면서 원소를 하나씩 꺼내 리스트 안의 리스트로 직접 구성 -> 넘파이로 간단하게 만들기

import numpy as np

넘파이의 column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결합니다. 연결할 리스트는 파이썬 튜플로 전달합니다.

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

* 튜플이 뭐죠?

파이썬 튜플은 리스트와 매우 비슷합니다. 리스트처럼 원소에 순서가 있지만 한 번 만들어진 튜플은 수정할 수 없습니다. 튜플을 사용하면 함수로 전달한 값이 바뀌지 않는다는 것을 믿을 수 있기 때문에 매개변수 값으로 많이 사용합니다.

 

그럼 이제 fish_length와 fish_weight를 합치겠습니다. 방법은 동일합니다.

fish_data = np.column_stack((fish_length, fish_weight))
print(fish_data[:5])
'->[[ 25.4 242. ]
 [ 26.3 290. ]
 [ 26.5 340. ]
 [ 29.  363. ]
 [ 29.  430. ]]'

 

넘파이에는 타깃 데이터도 쉽게 만들 수 있는 방법이 있습니다. 바로 np.ones()와 np.zeros() 함수입니다. 이 두 함수는 각각 원하는 개수의 1과 0을 채운 배열을 만들어 줍니다.

print(np.ones(5))
#->[1. 1. 1. 1. 1.]
print(np.zeros(5))
#->[0. 0. 0. 0. 0.]

이 두 함수를 사용해 1이 35개인 배열과 0이 14개인 배열을 간단히 만들 수 있습니다. 그다음 두 배열을 그대로 연결하면 됩니다. 여기에서는 np.column_stack() 함수를 사용하지 않고 첫 번째 차원을 따라 배열을 연결하는 np.concatenate() 함수를 사용합니다.

그럼 np.concatenate() 함수를 사용해 타깃 데이터를 만들어 보겠습니다. np.column_stack()과 마찬가지로 연결한 리스트나 배열을 튜플로 전달해야 합니다.

fish_target = np.concatenate((np.ones(35), np.zeros(14)))
print(fish_target)
'->[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0.]'

이 예제는 데이터가 작기 때문에 큰 차이가 없지만 데이터가 아주 큰 경우에 파이썬 리스트로 작업하는 것은 비효율적입니다. 넘파이 배열은 핵심 부분이 C, C++과 같은 저수준 언어로 개발되어서 빠르고, 데이터 과학 분야에 알맞게 최적화되어 있습니다.

사이킷런으로 훈련 세트와 테스트 세트 나누기

사이킷런은 머신러닝 모델을 위한 알고리즘뿐만 아니라 다양한 유틸리티 도구도 제공합니다. 대표적인 도구가 바로 지금 사용할 train_test_split() 함수입니다. 이 함수는 전달되는 리스트나 배열을 비율에 맞게 훈련 세트와 테스트 세트로 나누어 줍니다. 물론 나누기 전에 알아서 섞어 줍니다!

train_test_split() 함수는 사이킷런의 model_selection 모듈 아래 있으며 다음과 같이 임포트합니다.

from sklearn.model_selection import train_test_split

사용법은 아주 간단합니다. 나누고 싶은 리스트나 배열을 원하는 만큼 전달하면 됩니다. train_test_split() 함수에는 친절하게도 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 있습니다. 다음과 같이 훈련 세트와 테스트 세트를 나눕니다.

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

fish_data와 fish_target 2개의 배열을 전달했으므로 2개씩 나뉘어 총 4개의 배열이 반환됩니다. 차례대로 처음 2개는 입력 데이터(train_input, test_input), 나머지 2개는 타깃 데이터(train_target, test_target)입니다. 랜덤 시드(random_state)는 42로 지정했습니다.

이 함수는 기본적으로 25%를 테스트 세트로 떼어 냅니다.

print(train_input.shape, test_input.shape)
#-> (36, 2) (13, 2)
print(train_target.shape, test_target.shape)
#-> (36,) (13,)

-> 튜플의 원소가 하나면 원소 뒤에 콤마를 추가합니다.

 

도미와 빙어가 잘 섞였는지 테스트 데이터를 출력해 보겠습니다.

print(test_target)
#-> [1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

이처럼 무작위로 데이터를 나누었을 때 샘플이 골고루 섞이지 않을 수 있습니다. train_test_split() 함수는 이런 문제를 간단히 해결할 방법이 있습니다. stratify 매개변수에 타깃 데이터를 전달하면 클래스 비율에 맞게 데이터를 나눕니다. 훈련 데이터가 작거나 특정 클래스의 샘플 개수가 적을 때 특히 유용합니다. 여기에서도 한번 적용해 보겠습니다.

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42)
print(test_target)
#->[0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]

수상한 도미 한 마리

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)
#-> 1.0
#도미 데이터 넣기
print(kn.predict([[25, 150]]))
#-> [0.] : 하지만 빙어로 나왔다.
import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

k-최근접 이웃은 주변의 샘플 중에서 다수인 클래스를 예측으로 사용합니다. 이 샘플의 주변 샘플을 알아보죠. KNeighborsClassifier 클래스는 주어진 샘플에서 가장 가까운 이웃을 찾아 주는 kneighbors() 메서드를 제공합니다. 이 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환합니다.

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

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')
plt.xlabel('length')
plt.ylabel('weight')

marker='D'로 지정하면 산점도를 마름모로 그립니다. 예측 결과와 마찬가지로 가장 가까운 이웃에 도미가 하나밖에 포함되지 않았습니다. 산점도를 보면 직관적으로 도미와 가깝게 보이는데 말이죠.

 

이 문제이 해결 실마리를 찾기 위해 kneighbors() 메서드에서 반환한 distances 배열을 출력해 보겠습니다. 이 배열에는 이웃 샘플까지의 거리가 담겨 있습니다.

print(distances)
#-> [[ 92.00086956 130.48375378 130.73859415 138.32150953 138.39320793]]

기준을 맞춰라

어림짐작으로 보아도 92의 거리보다 족히 몇 배는 되어 보이는데 겨우 거리가 130인 게 수상합니다.

x축은 범위가 좁고(10~40), y축은 범위가 넓습니다.(0~1000) 따라서 y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산되겠죠.

 

이를 눈으로 명확히 확인하기 위해 x축의 범위를 동일하게 0~1000으로 맞추어 보겠습니다. 맷플롯립에서 x축 범위를 지정하려면 xlim() 함수를 사용합니다. 비슷하게 y축 범위를 지정하려면 ylim() 함수를 사용합니다.

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))
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

두 특성(길이와 무게)의 값이 놓인 범위가 매우 다릅니다. 이를 두 특성의 스케일이 다르다고도 말합니다. 데이터를 표현하는 기준이 다르면 알고리즘이 올바르게 예측할 수 없습니다. k-최근접 이웃과 같은 거리 기반 알고리즘들은 샘플 간의 거리에 영향을 많이 받으므로 제대로 사용하려면 특성값을 일정한 기준으로 맞춰 주어야 합니다. 이런 작업을 데이터 전처리라고 부릅니다.

 

* 모든 알고리즘은 거리 기반이 아닌가요?

모든 알고리즘이 거리를 기반으로 하는 것은 아닙니다.

 

가장 널리 사용하는 전처리 방법 중 하나는 표준점수입니다.(혹은 z 점수라고도 부릅니다.) 표준점수는 각 특성값이 0에서 표준편차의 몇 배만큼 떨어져 있는지를 나타냅니다. 이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있습니다.

 

표준점수를 계산하려면 평균을 빼고 표준편차를 나누어 주면 됩니다. 넘파이는 편리하게도 이 두 함수를 모두 제공합니다.

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

np.mean() 함수는 평균을 계산하고, np.std() 함수는 표준편차를 계산합니다. train_input은 (36, 2) 크기의 배열입니다. 특성마다 값의 스케일이 다르므로 평균과 표준편차는 각 특성별로 계산해야 합니다. 이를 위해 axis=0으로 지정했습니다. 이렇게 하면 행을 따라 각 열의 통계 값을 계산합니다.

print(mean, std)
#-> [ 27.29722222 454.09722222] [  9.98244253 323.29893931]

이제 표준점수로 변환하겠습니다.

train_scaled = (train_input - mean) / std

넘파이는 train_input의 모든 행에서 mean에 있는 두 평균값을 빼줍니다. 그다음 std에 있는 두 표준편차를 다시 모든 행에 적용합니다. 이런 넘파이 기능을 브로드캐스팅이라고 부릅니다.

전처리 데이터로 모델 훈련하기

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

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

여기에 아주 중요한 점이 있습니다. 바로 훈련 세트의 mean, std를 이용해서 변환해야 한다는 점입니다. 샘플을 변환하고 다시 산점도를 그려보죠.

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축과 y축의 범위가 -1.5~1.5 사이로 바뀌었다는 것입니다. 훈련 데이터의 두 특성이 비슷한 범위를 차지하고 있습니다. 이제 이 데이터셋으로 k-최근접 이웃 모델을 다시 훈련해 보죠.

kn.fit(train_scaled, train_target)

훈련 세트가 평균과 표준편차로 변환됐기 때문에 테스트 세트도 훈련 세트의 평균과 표준편차로 변환해야 합니다.

test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)
print(kn.predict([new]))

마지막으로 kneighbors() 함수로 이 샘플의 k-최근접 이웃을 구한 다음 산점도로 그려 보겠습니다. 특성을 표준점수로 바꾸었기 때문에 가장 가까운 이웃에 변화가 생겼을 것으로 기대할 수 있습니다.

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()

스케일이 다른 특성 처리

대부분의 머신러닝 알고리즘은 특성의 스케일이 다르면 잘 작동하지 않습니다. 이를 위해 특성을 표준점수로 변환했습니다. 사실 특성의 스케일을 조정하는 방법은 표준점수 말고도 더 있습니다. 하지만 대부분의 경우 표준점수로 충분합니다. 또 가장 널리 사용하는 방법입니다. 데이터를 전처리할 때 주의할 점은 훈련 세트를 변환한 방식 그대로 테스트 세트를 변환해야 한다는 것입니다.