본문 바로가기
Programming/Machine Learning

머신러닝 프로그램 만들어 보기 - 생선 분류 문제

by IT learning 2021. 3. 30.
728x90

본 내용은 '혼자 공부하는 머신러닝 + 딥러닝' 교재를 이용하여 배운 것을 토대로 작성합니다.

 

위 머신러닝 공부를 할때 사용하는 IDE는 '구글 코랩' 입니다. 

코랩 사용법을 익히고 오시길 바랍니다.

 

또한 파이썬의 기초적인 지식을 알아야 합니다.

위 코드들은 각자의 파일이 아닌 하나의 파일에 작성된 코드들입니다.

 

오늘은 간단한 머신러닝 프로그램을 만들어보겠다.

사실 프로그램이라고 하기에도 간단한 것이라..아무튼 시작해보자!

 

시작하기 전에

시작하기 전에 간단한 내용을 얘기해보며 시작하겠다. 한 마켓은 앱 마켓 최초로 살아있는 생선을 판매하기 시작했다.

고객이 온라인으로 주문하면 가장 빠른 물류 센터에서 신선한 생선을 곧바로 배송한다.  하지만, 한 가지 문제가 발생했다.

물류 센터에서 생선을 고르는 직원이 생선 이름을 못외운다.  항상 물어보면서 분류를 하기 일쑤였다.

그런 직원을 본 팀장은 생선이름을 자동으로 알려주는 머신러닝을 만들라고 맡겼다.

 

생선 분류 문제

자, 그럼 생선들을 어떻게 구분하면 될까? 생선의 특징을 알면 쉽게 구분할 수 있을 것 같다. 

도미의 경우 생선 길이가 30cm 이상이면 도미라고 알려져있다. 그렇다면 이렇게 만들면 될 듯 하다!

if fish_data >= 30:
	print("도미")

이러면 도미를 알아낼 수 있다!...그렇지만 생각을 다시 해보자. 30cm 보다 큰 생선이 무조건 도미라고 말할 수는 없다.

그렇다고 도미가 다 30cm 이상이라는 보장도 없다.

 

그럼 이 문제를 머신러닝으로 어떻게 해결할 수 있을까? 보통 프로그램은 누군가 정해준 기준대로 일을 한다.

하지만, 머신러닝은 누구도 알려주지 않는 기준을 찾아서 일을 한다. 누가 말해주지 않아도 30~40cm 길이의 생선은 도미이다 라는 기준을 찾는 것이다. 그럼 만들어보자!

 

# 이어질 프로그램은 도미와 빙어 두 종류의 생선을 구분하는 머신러닝 프로그램입니다.

 

도미 데이터 준비하기

머신러닝은 여러 개의 도미 생선을 보면 스스로 어떤 생선이 도미인지를 구분할 기준을 찾는다. 그렇다면 여러개의 도미 생선을 많이 준비해야겠다.

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]

필자는 35마리의 도미를 준비했다. 길이무게를 준비한 파이썬 리스트이다.

리스트에서 첫 번째 도미의 길이는 25.4cm , 무게는 242.0g 이다. 각 도미의 특징을 길이와 무게로 표현한 것이다.

필자는 이런 특징을 특성이라고 부르겠다.

 

두 특성을 숫자로 보는 것보다 그래프로 표현하면 데이터를 잘 이해할 수 있고 앞으로 할 작업에 대한 힌트를 제공할 수 있다.

이러한 그래프를 산점도(Scatter Plot) 라고 부른다. 그럼 길이를 x축, 무게를 y축으로 한 그래프를 그려보자.

import matplotlib.pyplot as plt # matplotlib의 pylot 함수를 plt 로 줄여서 사용

plt.scatter(bream_length, bream_weight)
plt.xlabel('length')            # x축은 길이
plt.ylabel('weight')            # y축은 무게
plt.show()

위 코드의 그래프이다. 구글 코랩을 이용했다.

위 코드를 살펴보면, 과학계산용 그래프를 그리는 대표적인 패키지인 맷플롯립(matplotlib)을 import 했다.

import한 패키지를 이용해 산점도를 그리는 scatter() 함수를 사용했고, xlabel, ylabel 함수를 이용해 x축,y축의 이름을 화면에 표시했다.

마지막으로 show() 함수로 그래프를 보이게 했다. 2개의 특성을 사용해 그린 그래프이기 때문에 2차원 그래프라고 말한다.

 

생선의 길이가 길수록 무게가 많이 나간다고 생각하면 이 그래프 모습은 매우 자연스럽다.

이렇게 산점도 그래프가 일직선에 가까운 형태로 나타나는 경우를 선형(linear)적이라고 말한다. 선형이란 단어는 종종 등장하니 기억하자.

 

도미가 준비되었으니 다른 생선인 빙어 데이터를 준비해보자.

 

빙어 데이터 준비하기

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]

필자가 준비한 빙어 데이터는 총 14마리이다. 아까와 같은 방법으로 길이, 무게로 리스트를 만들었다.

 

숫자를 보고 알 수 있듯, 빙어는 크기도 작고 무게도 가볍다.

빙어 그래프도 그려보자.

# 여기서는 맷플롯립 패키지를 임포트 할 필요가 없다. 이미 위에서 임포트 했기 때문이다.

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

위 코드의 그래프이다.

맷플롯립은 친절하게도 2개의 산점도를 색으로 구분해 나타내준다. 주황색 점이 빙어의 산점도이다.

빙어도 도미와 비슷하게 길이와 무게가 비례하지만, 늘어나는 정도가 조금 다르다. 빙어는 길이가 늘어나더라도 무게가 많이 늘지 않는다.

따라서 빙어의 산점도도 선형적이지만 무게가 길이에 영향을 덜 받는다고 볼 수 있다.

 

이제 두 생선의 산점도도 그렸고 데이터도 준비했다. 그럼 두 데이터를 스스로 구분하는 머신러닝 프로그램을 만들어보자.

 

생선 구분 머신러닝 프로그램

여기서는 가장 간단하고 이해하기 쉬운 k-최근접 이웃 알고리즘(k-Nearest Neighbors)알고리즘을 이용하여 도미와 빙어 데이터를 구분해보겠다. k-최근접 이웃 알고리즘을 사용하기 전에 앞서 준비했던 도미와 빙어의 데이터를 하나의 데이터로 합쳐보겠다.

length = bream_length + smelt_length
weight = bream_weight + smelt_weight

간단하게 두 데이터를 합쳤다.  하지만 여기서 사용하는 머신러닝 패키지는 사이킷런(scikit-learn)이다.

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

이렇게 만드는 가장 쉬운 방법은 파이썬의 zip() 함수리스트 내포 구문을 활용하는 것이다.

zip 함수는 나열된 리스트 각각에서 하나의 원소를 꺼내 반환한다.

 

두 방법을 사용하여 2차원 리스트로 만들어보겠다.

fish_data = [[l,w] for l,w in zip(length, weight)] # 리스트 내포를 이용한 fish_data 2차원 리스트 제작.

for 문은 zip() 함수로 length와 weight 리스트에서 원소를 하나씩 꺼내어 l과 w에 할당한다.

그러면 [l,w]가 하나의 원소로 구성된 리스트가 만들어진다.

 

잘 됐는지 fish_data를 출력해보자.

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]]

위 코드의 출력결과이다. 실제로는 구글 코랩의 실행 결과에서 보일 것이다.

첫 번째 생선의 길이 25.4cm 와 무게 242.0g이 하나의 리스트를 구성하고 이런 리스트가 모여 전체 리스트를 만들었다.

이런 리스트를 2차원 리스트 라고 부른다.

 

자, 생선 데이터 49개를 준비했다. 마지막으로 준비할 데이터는 정답 데이터이다.

즉, 첫 번째 생선은 도미이고, 두 번째 생선도 도미라는 식으로 각각 어떤 생선인지 답을 만드는 것이다. 왜 이게 필요할까?

 

우리가 원했던 것은 머신러닝 알고리즘이 생선의 길이와 무게를 보고 도미와 빙어를 구분하는 규칙을 찾길 원했다. 그렇게 하려면 적어도 어떤 생선이 도미인지 빙어인지를 알려줘야 한다.

 

머신러닝은 물론이고 컴퓨터 프로그램은 문자를 직접 이해하지 못한다. 대신 도미와 빙어를 숫자로 표현하면 된다. 0과 1로 말이다.

예를 들어 첫 번째 생선은 도미이므로 1이고 마지막 생선은 빙어 이므로 0이 된다.

 

앞서 도미와 빙어를 순서대로 나열했기 때문에 정답 리스트1이 35번, 0이 14번 등장하면 된다.

fish_target = [1] * 35 + [0] * 14 # 도미와 빙어를 구분해주는 target. 1일 경우 도미, 0일 경우 빙어를 나타냄.
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]

위 코드의 출력결과이다. 정상적으로 1이 35개 0이 14개 출력되는것을 볼 수 있다.

 

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

from sklearn.neighbors import KNeighborsClassifier # 사이킷런 패키지에서 k-최근접 이웃 알고리즘을 구현한 클래스

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

kn = KNeighborsClassifier()

객체에 fish_data와 fish_target을 전달하여 도미를 찾기 위한 기준을 학습시켜보자. 이런 과정을 머신러닝에서는 훈련이라고 부른다.

사이킷런에서는 fit() 메소드가 이런 역할을 한다. 이 메소드에 fish_data와 fish_target을 순서대로 전달해보자.

kn.fit(fish_data, fish_target) # fit() 메소드는 학습시키는 역할을 한다.

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

 

이제 객체 kn이 얼마나 잘 훈련되었는지를 평가해보자. 평가는 score() 메소드로 한다. 이 메소드는 0에서 1사이의 값을 리턴한다.

1은 모든 데이터를 정확히 맞혔다는 것을 나타낸다. 예를 들어 0.5가 리턴됐다면 절반만 맞혔다는 의미이다.

print(kn.score(fish_data, fish_target))

위 코드의 출력값을 원한다면 앞에 print() 함수를 씌워야 보인다.

1.0

1.0이다! 모든 fish_data 의 답을 정확히 맞혔다! 이 값을 정확도(accuracy)라고 한다.

이 모델은 정확도가 100% 이며 도미와 빙어를 완벽하게 구분했다라는 말이다. 성공이다 ㅎ

 

처음으로 머신러닝 프로그램을 만들어보았다.

 

k-최근접 이웃 알고리즘

앞에서 사용한 알고리즘은 아까도 말했듯이 k-최근접 이웃 알고리즘이다. 이번에는 이 알고리즘에 대해 더 자세히 알아보겠다.

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

 

예를 들어 다음 그림처럼 삼각형으로 표시된 새로운 데이터가 있다고 가정해보자. 이 삼각형은 도미와 빙어 중 어디에 속할까?

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

중간에 위치한 scatter() 함수로 삼각형으로 표시했다.

위 코드의 그래프이다.

자, 삼각형은 어디에 속할까?

우리가 볼때는 당연히 도미에 속한다고 생각할테다. k-최근접 이웃 알고리즘도 마찬가지이다.

이 삼각형 주위에 도미 데이터가 많으므로 삼각형을 도미라고 판단할 것이다. 실제로 그런지 확인 해보자!

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

predict() 메소드는 새로운 데이터의 정답을 예측한다. 이 메소드도 앞서 fit() 메소드와 마찬가지로 리스트의 리스트전달해야한다.

아까 score의 리턴값을 보기 위했던 것 처럼 predict() 함수를 이용하여 리턴된 값을 알기 위해서는 print() 함수를 덧씌어야 한다.

우리가 앞서 도미는 1, 빙어는 0으로 가정했었다. 반환된 값은 1이다. 즉 삼각형은 도미이다. 예상과 같다.

 

이렇게 생각하면 k-최근접 이웃 알고리즘을 위해 준비해야 할 일은 데이터를 모두 가지고 있는게 전부이다. 새로운 데이터에 대해 예측할 때는 가장 가까운 직선거리에 어떤 데이터가 있는지를 살피기만 하면 된다.

 

그래서인지 단점은 이런 특성 때문에 데이터가 아주 많은 경우 사용하기 어렵다. 데이터가 크기 때문에 메모리가 많이 필요하고, 직선거리를 계산하는 데도 많은 시간이 필요하다.

 

사이킷런의 KNeighborsClassifier 클래스도 마찬가지다.

이 클래스는 _fit_X 속성에 우리가 전달한 fish_data를 모두 가지고 있다. 또 _y 속성에 fish_target을 가지고 있다.

print(kn._fit_X)
print(kn._y)
# _fit_X
[[  25.4  242. ]
 [  26.3  290. ]
 [  26.5  340. ]
 [  29.   363. ]
 [  29.   430. ]
 [  29.7  450. ]
 [  29.7  500. ]
	.
        .
        .
 [  11.8   10. ]
 [  11.8    9.9]
 [  12.     9.8]
 [  12.2   12.2]
 [  12.4   13.4]
 [  13.    12.2]
 [  14.3   19.7]
 [  15.    19.9]]
# _y
[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]

 

흠.. 이렇게 보면 실제로 k-최근접 이웃 알고리즘은 무언가 훈련되는 것이 없는 셈이다. fit() 메소드에 전달한 데이터를 모두 저장하고 있다가 새로운 데이터가 등장하면 가장 가까운 데이터를 참고하여 도미인지 빙어인지를 구분하는 것이다.

 

그럼 과연 가까운 몇 개의 데이터를 참고할까? 사실 이는 정하기 나름이다. KNeighborsClassifier 클래스의 기본 값은 5이다.

이 기준은 n_neighbors 매개변수로 바꿀 수 있다. 예를 들어 다음과 같이 말이다.

kn49 = KNeighborsClassifier(n_neighbors=49)

가장 가까운 데이터 49개를 사용하는 k-최근접 이웃 모델에 fish_data를 적용하면 fish_data에 있는 모든 생선을 사용하여 예측하게 된다.

다시 말하자면, fish_data 의 데이터 49개 중 도미가 35개 로 다수를 차지하므로 어떤 데이터를 넣어도 무조건 도미로 예측할 것이다.

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

학습 시키고 얼마나 학습이 잘 됐는지 확인을 해보면,

0.7142857142857143

kn49 모델은 도미만 올바르게 맞히기 때문에 정확도를 계산하면 이렇게 나온다. 

 

확실히 n_neighbors 매개변수를 49로 두는 것은 좋지 않은 듯 하다. 기본값을 5로 하여 도미를 완벽하게 분류한 모델을 사용해야겠다.

 

도미와 빙어 분류 정리

우리는 도미와 빙어를 구분하기 위해 첫 번째 머신러닝 프로그램을 만들었다.

먼저 도미 35 마리와 빙어 14 마리의 길이와 무게를 측정해 파이썬 리스트로 만들었다.

그 다음 도미와 빙어 데이터를 합쳐 리스트의 리스트(2차원 리스트)로 데이터를 준비했었다.

 

우리가 사용한 알고리즘은 k-최근접 이웃 알고리즘이다. 사이킷런의 k-최근접 이웃 알고리즘은 주변에서 가장 가까운 5개의 데이터를 보고 다수결의 원칙에 따라 데이터를 예측한다. 그리고 우리가 제작한 프로그램은 완벽하게 맞혔다.

 

도미와 빙어를 분류하는 문제를 풀면서 KNeighborsClassifier 클래스의 fit(), score(), predict() 메소드를 사용해보았다.

끝으로 k-최근접 이웃 알고리즘의 특징을 알아보았다!

 

 

마지막으로 위 코드들의 합쳐진 코드인 필자의 코드를 올리며 마치겠다.

github.com/ITlearning/Practice_Machine_Learning/blob/main/03_29/BreamAndSmelt.ipynb

 

ITlearning/Practice_Machine_Learning

사지방에서 공부하는 머신러닝 + 딥러닝. Contribute to ITlearning/Practice_Machine_Learning development by creating an account on GitHub.

github.com

오늘은 이것으로 마치겠다!

728x90

댓글

IT_learning's Commit