파이썬 제어문과 반복문

제어구문이란?

제어 구문이란 입력이나 출력 등 어떤 값에 의해서 프로그램의 흐름을 바꿀 수 있는 명령어를 의미합니다. 쉽게 말하면 프로그램의 흐름이 코딩 순서대로 흘러가다가 어떤 조건(Condition)에 의해서 프로그램의 흐름이 달라지는 것을 의미합니다. 실제로 이런 일은 프로그램을 하면서 매우 자연스러운 것입니다.

이런 조건문을 말로 풀어 본다면 “만일 000 라면 000 하라” 라고 쓸 수 있습니다. 이 안에 여러가지를 넣어서 문장을 만들 수 있습니다. “만일 성적이 90점 이상이라면 A를 주어라” 라고 한다면 입력되는 점수에 따라서 그 성적이 바뀔 수 있을 것입니다. 조건을 하나 더 붙인다면 “만일 90점 이상이라면 A를 주고 80점 이상이라면 B를 주고 70점 이상이라면 C를 주고 60점 이상이라면 D를 주고 그 이하라면 F를 주어라” 라고 조건을 여러개 주어서 프로그램을 만들 수도 있습니다.

IF 조건문

파이썬에서 이런 조건에 따라서 수행할 코드를 분기 할때에 사용하는 것이 if 조건문입니다. if 조건문은 굉장히 간단한 구조로 되어 있기 때문에 쉽게 이해하고 사용할 수 있습니다.

if 조건식:
    # 조건식이 참(True)일 경우에 실행
else:
    # 조건식이 거짓(False)일 경우에 실행

if 조건문에 들어가는 조건식에는 주로 하나 이상의 변수를 사용해서 조건식을 만듭니다. 예를 들어서 하나의 조건문이 True/False 인지를 확인할 수 있고 두개의 변수를 서로 비교해서 같거나 다르거나 크거나 같거나를 표현 할 수도 있습니다.
그것을 정리하면 아래의 표와 같습니다.

a == b # 두개의 변수 a와 b가 같다. 맞으면 True 틀리면 False
a != b # 두개의 변수 a와 b는 같지 않다. 
a > b  # 두개의 변수 a는 b보다 크다.
a >= b  # 두개의 변수 a는 b보다 크거나 같다.
a < b # 두개의 변수 a는 b보다 작다.
a <= b # 두개의 변수 a는 b보다 작거나 같다.

이 두개의 변수는 숫자값이 들어가도 되지만 문자열도 입력할 수 있습니다. 문자열의 경우에는 문자의 비교만 가능합니다. 예를 들어서 a=”apple”, b=”pear” 일때 a == b 조건은 당연히 False가 됩니다. 이런 방법으로 문자가 같은지를 확인하는 조건식을 만들고 활용할 수 있습니다.
주의 해야 할 것은 a와 b가 같은지를 볼때에 ‘=’ 하나를 쓰는 것이 아니라 ‘==’ double equal을 사용한다는 것입니다. 하나만 사용할 경우에는 해당 변수에 값을 대입하는 것으로 인식하기 때문에 조건문을 사용할 때에 이 부분을 조심하셔야 합니다.

이 조건문을 사용해서 이전에 간단히 설명했던 성적코드를 표시하는 프로그램을 작성해보면 아래와 같이 할 수 있습니다. 여기서 보면 “if 조건문 elif 조건문 else…”의 형태가 나오는데 여기서 나오는 “elif”는 새로운 것이 아니라 “else if”의 축약된 형태로 조건문이 False일 경우에 다른 조건문을 사용하는 간단히 말하면 중첩된 조건문에서 사용하는 코드입니다. “if … elif…”를 사용하지 않고 “if … else …” 형태를 반복해서 사용할 수 있지만 이렇게 작성된 코드는 코딩이 길어지기 때문에 간결하지 않고 또 간결하지 않은 코드는 수정이 쉽지 않다는 단점이 있습니다.

그렇기 때문에 가급적이면 간단하게 코딩하는 습관을 기르는 것이 좋습니다.

score = 88

if score > 90:
    print('A')
elif score > 80:
    print('B')
elif score > 70:
    print('C')
elif score > 60:
    print('D')
else:
    print('F')

위의 코드를 실행하면 출력값은 ‘B’가 출력됩니다.

아래의 코드는 문자열 값을 입력해서 비교하는 코드입니다. 아래와 같은 경우는 출력값이 False가 됩니다.

a = "apple"
b="pear"

if a == b:
    print('True')
else:
    print('False')

그리고 비교하는 변수는 꼭 두개가 있어야 하는 것은 아닙니다. 아래와 같이 flag를 bool 자료형으로 선언하고 True 값을 주면 해당 값의 조건에 따라서 True / False를 출력합니다.

flag = True

if flag:
    print('True')
else:
    print('False')

또 아래와 같이 조건식을 여러개 함께 사용할 수도 있습니다. 그렇게 되면 이전에 논리연산자를 함께 사용해야 합니다. 논리 연산자는 ‘and’,’or’ 연산자입니다. ‘and’의 경우에는 조건 모두를 만족해야 참(True)이고 하나라도 만족하지 않으면 거짓(False)가 됩니다. 그러나 ‘or’의 경우에는 조건 중 하나만 만족해도 참(True)이 됩니다.

아래의 조건문의 경우에는 a==b가 False이지만 score > 80이 True 임으로 출력값은 True를 표시하게됩니다.

if (a==b) or score > 80:
    print('True')
else:
    print('False')

반복문

제어문과 마치 한쌍과 같이 사용되는 것이 바로 반복문입니다. 이 반복문은 리스트와 같은 어떤 연속된 자료를 반복해서 처리하면서 사전에 정의된 조건문을 처리합니다. 가장 많이 사용되는 반복문은 for나 while과 같은 명령어입니다. 이 두 반복문에도 역시 반복에 필요한 조건들이 들어갑니다.

만약 조건이 들어가지 않는다면 해당 반복문들은 무한히 명령어를 실행하게됩니다. 이것을 무한루프라고 합니다. 시스템이 무한루프에 들어가면 무한정 리소스를 사용하기 때문에 결국 응용프로그램이 에러를 출력하게됩니다.

WHILE 루프

while은 for 루프와 함께 파이썬의 대표적인 반목문입니다. 어떤 조건을 주고 그 조건이 참일때까지 해당 명령어를 수행하게됩니다. 아래의 예와 같이 사용하는 방법도 굉장히 간단합니다.

while 조건식:
    #반복하면서 처리할 코드

이제 while 루프를 사용해서 간단한 코딩을 해보겠습니다. 일단 score라는 리스트를 선언하고 해당 리스트에 성적을 입력합니다. 그리고 리스트의 크기만큼 루프를 반복하며 성적을 출력해보는 예제입니다. 여기서 len() 함수는 리스트의 크기를 리턴해줍니다. 그러므로 아래의 예제는 i라는 변수에 0을 입력하고 i가 리스트의 크기보다 작은 조건을 만족할 때까지 while 루프안에 있는 print 구문을 수행하고 i의 값에 1을 계속적으로 더해줍니다. 결국 i는 0-4까지의 조건을 만족하겠고 5가 되는 순간 리스트의 크기와 같아지면서 해당 조건을 만족하지 않기 때문에 while 루프에서 벗어나게 됩니다.

실제로 아래 예제를 수행해보면 100부터 87까지의 값을 출력하고 해당 루프를 빠져나가는 것을 확인할 수 있습니다.

score = [100,80,99,92,87]
i = 0
while i < len(score):
    print(score[i])
    i += 1

만약 while의 조건식 부분에 True를 입력하게 되면 어떻게 될까요?
해당 구문은 무한루프로 들어가게됩니다. 언제 이런 구문을 사용하는가 생각해보면 사용자가 값을 계속 입력하는 상황을 생각해 볼 수 있겠습니다. 그러다가 마지막으로 입력을 종료하고 싶으면 약속된 키를 입력하면 종료되는 상황이 있을 수 있습니다.

my_score = []
while True:
    s = input('Score?')
    if s == 'q': break
    my_score.append(s)

print('입력하신 스코어는 {}입니다.'.format(','.join(my_score)))

::: 출력 형태 :::
Score? 100
Score? 90
Score? 50
Score? 60
Score? 80
Score? q
입력하신 스코어는 100,90,50,60,80입니다.

해당 코드를 실행하면 위와 같이 q라는 값이 들어오기 전까지 while 안에 있는 명령어를 계속 실행하게 되고 q 값이 입력되면 break 문이 실행되면서 while 루프를 벗어나게 됩니다. 만약 break 문이 없다면 해당 코드를 강제로 종료해야 하는 일이 생기게됩니다.

FOR 루프

for 루프는 while과 함께 대표적인 반복문입니다. while 루프도 많이 사용하지만 아마도 가장 많이 사용하는 것이 for 문법이라고 생각합니다. for는 사용하기가 while 비해서는 약간 복잡하게 느껴질 수도 있지만 다양한 표현이 가능하다는 것은 많은 부분에서 사용할 수 있다는 의미도 됩니다.

몇가지 예제를 통해서 그 쓰임을 알아보겠습니다.

for 루프 변수 in 리스트(또는 튜플, 문자열):
    # 수행할 명령어
    ...

위의 문법을 보면 for 다음에는 ‘루프 변수’가 입력됩니다. ‘루프 변수’는 쉽게 말해서 in 다음에 표시되는 리스트(또는 튜블, 문자열)의 내용을 담아내는 변수입니다. 다시 말하면 for 루프는 리스트의 크기 만큼 실행되며 리스트의 갯수 하나씩 루프 변수에 담는다는 의미입니다. 말로 풀어서 쓰면 복잡하지만 실제로 사용하는 것은 굉장히 간단하고 직관적입니다.

# case1 숫자 리스트
score = [100,80,99,92,87]
for s in score:
    print(s)

# case2 문자열
message = "Hello, World!"
for c in message:
    print(c)

# case3 범위
for i in range(10):
    print(i)

위의 코드를 실행해보면 case1은 score안에 있는 점수가 하나씩 출력됩니다. case2는 message에 입력된 문자열 데이터가 한글자씩 표시됩니다. case3를 실행해보면 0-9까지의 숫자가 출력됩니다.

여기에서 사용하는 range() 문은 굉장히 많이 사용하는 파이썬 내장함수이기 때문에 좀 더 알아보면 방금 사용했던것처럼 range(10)이라고 하면 0-9까지를 출력하고 range(5,10)으로 선언하면 시작하는 숫자가 5부터 시작하니 5-9까지를 출력하게됩니다. range(0,10,2)로 표시하면 0-9까지 2씩 증가한 값 즉, 0,2,4,6,8을 표시하게됩니다.

이 외에도 앞서 살펴본 while에서 break 문도 역시 for에서 사용할 수 있고 또 continue 문도 사용할 수 있습니다. break 문은 앞서 보았기 때문에 이번에는 continue 문을 어떻게 사용하는지 보겠습니다.

5개의 과목 중에서 90점이 넘는 과목은 A로 표시한다고 하면 아래와 같이 continue를 사용해서 표현할 수 있습니다. 조건을 만족하면 조건문 아래 라인을 실행하지 않고 조건을 만족하지 않으면 아래 라인을 실행하게 됩니다. break문의 경우에는 조건을 만족하면 for문을 빠져나가는데 이와 비슷한 개념이지만 약간의 차이가 있습니다.

# case1 숫자 리스트
score = [100,80,99,92,87]
for s in score:
    if s <= 90:
        continue
    print('{}점은 A입니다.'.format(s))

또 하나 파이썬에서 사용하는 for문의 독특하면서도 강력한 기능 중에 하나는 for 문을 한줄로 나열해서 쓸 수 있다는 것입니다. 이것도 예제를 통해서 살펴보겠습니다.

점수를 가지는 하나의 배열이 있는데 이 배열은 2개의 차원으로 되어있다고 가정합니다.

array2d[0] 번지에는 [100,99,98]의 값이 저장되어 있습니다.
array2d[1] 번지에는 [99,78,89] 값이 저장되어 있습니다.
array2d[2] 번지에는 [98,88,91] 값이 저장되어 있습니다.

이 배열에서 해당 점수를 표시하려면 위의 경우와 같이 ‘for s in array2d’와 같은 형식으로도 사용할 수 있지만 아래와 같이 그냥 한줄로 표시할 수도 있습니다.

array2d = [[100,99,98], [99,78,89], [98,88,91]]
['socre:{},{},{}'.format(arr[0],arr[1],arr[2]) for arr in array2d]

이러한 표현 방법은 numpy이나 pytorch에서 여러 줄의 코드를 간단히 한줄로 표현할 수 있게 해주는 굉장히 유용한 방식입니다.

파이썬 자료형

파이썬 자료형 – 리스트

파이썬에는 여러 개의 값을 하나의 데이터 저장소에 담아서 관리 할 수 있는 리스트(List)라는 자료형이 있습니다. 이 리스트에는 여러개의 값을 하나의 변수에 담을 수 있기 때문에 거의 모든 코드에 사용된다고 할 수 있습니다.

예를 들어서 학생의 10개 과목에 대한 성적을 입력할 때에 국어,영어,수학 등 10여개의 과목을 각각 변수에 담지 않고 하나의 변수명을 선언하고 [99,98,89 …] 와 같이 차례로 성적을 저장하는 방식입니다. 이때 중요한 것이 있다면 저장의 순서입니다. 모든 학생의 과목이 일정한 순서대로 정렬되어야 정상적인 결과를 얻을 수 있습니다.

score_list = [99,98,89,78,80...] #각 과목의 점수를 하나의 변수에 저장

dummy_list = [99,98,89,'A',3.4] #다른 형의 자료를 동일한 리스트에 저장할 수 있음
print([type(n) for n in dummy_list])
결과 : [<class 'int'>, <class 'int'>, <class 'int'>, <class 'str'>, <class 'float'>]

len(dummy_list)
결과 : 5

또 대부분 경우는 하나의 리스트에 같은 자료형을 입력하지만 다른 자료형을 입력하는 것도 가능합니다. 배열에 선언된 값에 접근하기 위해서는 변수[index] 형태로 사용하면 됩니다. 중요한 것은 index는 0부터 시작된다는 것입니다.

배열의 크기를 알기 위해서는 len()이라는 내장함수를 사용해서 확인 할 수 있습니다.

리스트에서 꼭 알아야 할 중요한것 중에 하나는 슬라이싱(Slicing)의 개념입니다. 이것은 리스트에서 원하는 범위를 지정해서 사용할 수 있는 굉장히 편리하고 많이 사용하는 개념입니다.

dl = [0,1,2,3,4,5,6,7,8,9]
print(dl[0:3]) #[0, 1, 2]
print(dl[5:]) #[5, 6, 7, 8, 9]
print(dl[3:-1]) #[3, 4, 5, 6, 7, 8]

cl = 'hello world'
print(cl[0:5]) #hello

위의 예제에서 보다시피 리스트의 인덱스 값을 활용해서 해당 자료를 출력합니다.
알아두면 좋은 것은 이것이 리스트 배열에서만 사용하는 것이 아니라 문자 배열에서도 동일하게 사용할 수 있다는 것입니다. 다른 언어의 substring과 같은 개념입니다. 앞에 -(minus) 기호를 붙이면 마지막에서부터 카운트를 하기 때문에 이를 응용해서 다양한 활용이 가능합니다.

파이썬 자료형 – 딕셔너리

리스트와 함께 또 하나의 중요한 데이터 저장소는 딕셔너리(Dictionary)입니다.

딕셔너리는 키(Key), 값(Value)의 쌍으로 되어 있는 데이터 저장소입니다. Java에 익숙하신 분은 맵(Map)이라는 객체를 생각하시면 되겠습니다. 딕셔너리도 선언과 활용이 간단합니다. 해당 내용을 출력하기 위해서는 키(Key) 값을 입력하면 그에 해당하는 값(Value)가 출력되는 구조입니다.

score_dict = {'kor':98, 'eng':90, 'math':100, 'hist':93} #과목을 Key로 선언하고 성적을 Value로 정의
print(score_dict['kor']) #98

print(score_dict.keys()) #dict_keys(['kor', 'eng', 'math', 'hist'])
print(sorted(score_dict.keys())) #['eng', 'hist', 'kor', 'math']
print(score_dict.values()) #dict_values([98, 90, 100, 93])

이전에 리스트에서 성적을 입력할 때에 순서가 중요했다면 딕셔너리에서는 Key가 존재하기 때문에 순서는 중요하지 않습니다. 딕셔너리에서 하나 주의해야 할 것은 Key 가 중복되지 않도록 관리해야 한다는 것입니다.

딕셔너리는 Key 값을 추출한다거나 이를 정렬하는 기능들을 제공하기 때문에 이를 활용해서 다양한 데이터를 처리할 수 있습니다.

파이썬 자료형 – 튜플

튜플(Tuple)은 리스트와 유사하지만 차이가 있다면 내용을 바꿀 수 없다는 점에 큰 차이가 있습니다. tutple로 선언한 변수에 어떤 다른 값을 적용하고자 하면 ‘tuple’ object does not support item assignment 라는 에러 메세지를 표시합니다.

dummy_tuple = (99,88,87,94,92)
print(dummy_tuple[0]) #99

dummy_tuple[0] = 100 #TypeError: 'tuple' object does not support item assignment

파이썬 자료형 – 집합

집합(Set)도 리스트와 유사한 개념입니다. 그러나 각 요소간의 순서는 존재하지 않으며 각 요소간의 중복된 값을 허용하진 않습니다. 또 집합의 수학적인 연산이 가능하다는 특징이 있습니다.

dummy_set0 = set(list(score_dict.keys()))
dummy_set1 = set(['sci','kor'])

print(dummy_set0 | dummy_set1) # 합집합 {'math', 'eng', 'sci', 'kor', 'hist'}
print(dummy_set0 - dummy_set1) # 차집합 {'math', 'eng', 'hist'}
print(dummy_set0 & dummy_set1) # 교집합 {'kor'}

이전에 사용했던 score_dict의 키 값을 모아서 하나의 리스트로 만든 후에 이것을 다시 집합으로 선언합니다. 그리고 또 하나의 집합을 생성합니다. 그리고 이 집합을 활용해서 집합 연산을 수행해봅니다.

집합 연산은 합집합, 차집합, 교집합이 있습니다.
합집합은 ‘+’ 연산자를 사용하지 않고 ‘|’ 연산자를 사용합니다. 집합은 중복을 허용하지 않기 때문에 두 집합의 요소가 하나씩 존재하게됩니다.
차집합은 공통된 요소를 삭제합니다. 교집합은 공통된 요소만 추출합니다.

파이썬 기초

문자화된 언어를 다른 언어로 변환하는 것을 번역(Translate)라고 합니다. 반면 언어를 다른 언어로 실시간으로 전달하는 것을 통역(Interpret)이라고 합니다. 비슷하지만 이렇게 차이가 있죠.
번역은 글로된 전체 문장을 번역하는 것이기 때문에 시간이 많이 걸리지만 통역은 실시간으로 말을 듣고 말로 전하기 때문에 실시간으로 빠른 처리가 가능합니다.

컴퓨터에서도 이러한 일들이 비슷하게 일어납니다. 어떤 언어는 번역을 하듯이 전체 프로그램 코드를 일괄적으로 한번에 기계어로 변환하고 어떤 언어는 실시간으로 Line By Line으로 한줄 한줄 실행해나갑니다.

전자의 언어를 Compile 언어라고 하고 Java와 C 언어가 대표적입니다. 반면 파이썬은 한줄 한줄 읽고 바로 실행하기 때문에 Interpret 언어라고 합니다.

인터프리트 언어의 장점은 컴퓨터와 대화하듯이 코딩을 할 수 있다는 장점이 있습니다. 예를 들어서 프로그래머가 “1+2″는 뭐지? 라고 물어보면 컴퓨터는 “3”이라는 결과를 바로 보여주는 식입니다. 이것을 파이썬 코드로 표현하면 어떻게 될까요?

명령어 입력라인에 아래와 같이 입력하면 됩니다. 이전에 java나 C언어를 경험하신 분은 이런 형식의 코드가 얼마나 간결하다는 것을 쉽게 이해할 수 있을 것입니다.

1+2
결과 : 3

이처럼 파이썬 코딩은 굉장히 직관적이고 간단합니다. 또 파이썬은 인터프리트 방식으로 대화식 프로그래밍이 가능합니다. 다음으로는 몇가지 기본적인 산술 연산에 대해서 알아보겠습니다.
참고로 결과라고 쓴 부분은 코드를 실행한 결과가 표시되는 값을 나타낸 것입니다. 실제로 코드를 실행하면 “결과”라는 단어는 나오지 않습니다.

이제부터 파이썬에서 많이 사용하는 중요한 몇가지 기초적인 코드를 설명하고자 합니다. 처음 파이썬을 접하시는 분을 대상으로 최대한 간단하게 그리고 많이 쓰는 것들 위주로 설명하겠습니다. 몇가지 기본적인 코드의 문법만 이해해도 제가 블로그에 작성한 예제 코드를 이해하는데 문제가 없으리라 생각됩니다.

파이썬 사칙연산

2-3
결과 : -1
4*3
결과 : 12
6/4
결과 : 1.5
5**2
결과 : 25

프로그램에서 *는 곱셈, /은 나눗셈, **은 제곱을 의미합니다. 참고로 나눗셈을 표시하는 기호가 /라고 말씀드렸습니다. 그러나 //을 사용하는 경우도 있습니다. 이것은 **과 같이 제곱을 의미하는 것이 아니라 나눈 결과 값에서 소수점 이하를 버린다는 것을 의미합니다. 예를 들어서 6/4의 결과가 1.5로 표시되는데 6//4로 수행하면 결과값은 1로 표시됩니다. 또 가끔 % 연산자를 만나기도 합니다. 해당 연산자는 나눈 값의 나머지를 출력합니다.
6/3 하면 나머지가 0이 됩니다. 그러나 5/3하면 나머지가 2개 생기는데 % 연산자는 이 값을 출력해줍니다.

파이썬 자료형

type(1)
결과 : int
type(3.14)
결과 : float
type('Hello World')
결과 : str

프로그래밍 언어는 각각 자료형(Data Type)이 있습니다. 자료형은 말 그대로 자료(Data)의 형태를 의미합니다. 예를 들어서 위에서 표현한것과 같이 정수형, 실수형, 문자형의 자료 형태가 있고 type()이라는 내장함수로 데이터의 자료형을 알아 볼 수 있습니다.

참고로 내장함수라는 것은 중요하고 자주 사용하는 기능들은 사용자가 별도로 개발하지 않도록 프로그램 언어가 기본적으로 제공하는 함수(function) 입니다. print(), len(), range() 등 많은 내장함수가 있습니다. 이러한 함수들을 중요하고 또 너무나 많이 사용하기 때문에 다른 라이브러리들과는 달리 import 하지 않고 바로 사용할 수 있는 특징이 있습니다.

내장 함수를 모두 외울 필요는 없지만 관심있게 내용을 살펴볼 필요는 있습니다. 아래의 링크에 파이썬 내장함수가 잘 설명된 페이지를 링크합니다.

https://docs.python.org/ko/3/library/functions.html

파이썬 변수

변수(變數)는 Variable이라고 합니다. 말 그대로 변하는 수입니다. 이는 상수(常數)와 반대되는 개념입니다.
변수를 사용하는 가장 큰 이유는 코드의 재활용성과 가독성을 높여 주기 때문입니다. 이는 코드의 복잡성을 낮춰 줄 수 있고 이는 이후의 기능 수정과 변경에 용이한 특징이 있습니다.

a = 10
b = 10
a + b
출력 : 20

여기서 말하는 a, b와 같은 것이 바로 변수입니다. 즉 변할 수 있는 수라는 의미입니다. 변수는 숫자나 문자만 넣을 수 있는 것은 아닙니다. 거의 모든 것을 담을 수 있습니다.

변수는 마치 번지수와 같다고 할 수 있습니다. 예를 들어 10이라는 값은 메모리 영역에 한 부분에 저장됩니다. 그리고 그 주소는 id(a)로 표시해보면 “4521043376’와 같은 숫자 형태의 메모리 주소가 표시됩니다. 그러나 이것은 사람이게 친절한 표기법은 아닙니다. 인간에게는 a,b,c… 혹은 조합된 단어와 같은 인간의 언어코드로 표시하는게 훨씬 이해하기 쉽습니다.

예를 들어서 사각형의 높이를 구하는 공식은 가로×세로 입니다. 이러한 결과 값을 위해서 각 가로와 세로의 값을 곱해줄 수도 있지만 그렇지 않고 변수를 사용해서 변수간의 곱을 할 수도 있습니다. 이렇게 하는 것이 코드를 더 쉽게 이해할 수 있을 것입니다.

# 변수를 사용하지 않음
5 * 5
출력 : 35

# 변수를 사용
width = 5
height = 5
width * height
출력 : 35 

위와 같이 변수를 사용해서 프로그램을 하는 것이 훨씬 이해하기 쉽습니다. 변수 없이 숫자만으로 나열된 코드를 본다는 것은 생각하기도 힘든 일일 것입니다. 보통 변수로 사용되는 것은 알파벳 대소문자와 숫자입니다. 그러나 숫자로 시작하는 변수는 사용할 수 없습니다.

파이썬 아나콘다와 주피터 랩

파이썬을 개발 환경을 준비하는 방법 중에 가장 대중적인 것은 아나콘다를 활용하는 법입니다. 아나콘다는 패키지 관리와 디플로이를 효율적으로 할 수 있도록 하는 파이썬과 프로그래밍 언어의 자유-오픈 소스 배포판입니다. 개발에 사용하는 패키지 버전들은 패키지 관리 시스템 conda를 통해 관리됩니다.

예를 들어 내 컴퓨터에서 개발을 위해 여러 개발환경을 구축한다고 생각해봅니다. 때로 RNN 프로젝트나 Django를 활용한 웹개발, CNN을 활용한 Classification 문제를 해결한다고 생각해보면 각 프로젝트마다 python의 버전이나 활용하는 패키지가 있을 것입니다.

또 다른 사람이 만들었던 개발환경을 내 컴퓨터에 그대로 복사하고 싶을 때가 있습니다. 이미 내 컴퓨터에 있는 환경과 같다면 크게 문제될 것이 없겠지만 다른 것이 많이 있다면 좀 난감한 상황이겠죠.

아나콘다는 이러한 환경에 독립적인 프로젝트를 만들 수 있도록 도와줍니다. 이러한 툴이 없다면 개발 환경마다 python 버전과 의존성 있는 패키지들을 수정해야 하는 번거로운 작업들을 해야합니다. 그만큼 개발하는데 많은 시간이 소요되겠죠.

아나콘다 홈페이지에는 해당 제품들을 소개하며 모든 데이터 사이언티스트를 위한 도구라고 소개하고 있습니다. 그만큼 사용하기 쉽고 범용적이고 대중적인 도구라는 의미가 되겠습니다.

아나콘다는 Window, Mac, Linux를 모두 지원하니 사용하시는 OS에 맞춰서 다운로드 받으시면 되겠습니다. 저의 경우에는 Mac을 사용하기 때문에 Mac용 아나콘다를 다운로드 받아서 사용하고 있습니다.

다운 받은 후에 실행하면 아래와 같은 형태의 화면이 표시됩니다. 이중에서 이번 파이썬 코드 리뷰에 사용하는 도구는 Jupyter Lab이기 때문에 간단히 살펴보겠습니다. 사실 Jupyter Lab은 Jupyter Notebook의 확장판이라고 생각하시면 되기 때문에 이전에 Jupyter Notebook을 사용해 보신 분이라면 쉽게 사용하실 수 있습니다. 사용 방식이나 UI 구성은 거의 같습니다.
이 외에도 VS Code라는 훌륭한 개발도구도 있으니 사용해보시는 것도 추천합니다.

Jupyter Lab의 가장 좋은 장점은 Interactive하고 Reproducible한 특징이라고 할 수 있습니다. 마땅히 우리말로 설명하기가 어렵기 때문에 Jupyter Lab에서 소개하는 단어를 그대로 사용하겠습니다.
이러한 특징이 있기 때문에 사용자는 코딩 하고 바로 그 결과를 확인 할 수 있습니다. 무엇보다 무료이고 가볍고 웹 기반의 형식으로 되어 있기 때문에 UI도 따로 설명할 것이 없을 정도로 간단합니다.

그러나 그 기능과 활용도는 매우 강력하다고 할 수 있습니다. 아래의 이미지는 Jupyter Lab 공식 홈페이지에서 제공하는 이미지입니다. 보시는 것처럼 풍성한 시각화 도구를 지원하고 있습니다. 자세한 내용은 아래의 링크로 접속하시면 확인할 수 있습니다.

https://jupyter.org/

이 외에도 Colab(Colaboratory) 이라는 강력한 도구가 있습니다.

줄여서 ‘Colab’이라고도 하는 Colaboratory를 사용하면 브라우저에서 Python을 작성하고 실행할 수 있습니다. 자세한 소개 영상은 아래의 영상을 확인하시기 바랍니다.

Colab을 사용하면서 얻을 수 있는 장점은 별도의 개발환경을 구축하지 않아도 된다는 점, GPU를 제한적이지만 무료로 사용할 수 있다는 점, 그리고 공유가 간편하다는 장점이 있습니다. 조금 더 좋은 환경을 사용하려면 유료 결제를 통해서 Colab Pro 버전을 활용할 수도 있습니다.

저도 Colab Pro 버전을 사용하고 있는데 확실히 Colab 무료 버전 보다는 조금 더 나은 테스트 환경을 제공해줍니다. (많이는 아니지만…)

파이썬 소개

파이썬이 세상에 등장한지도 이미 20년이 넘었습니다. 많은 언어가 등장했다가 역사속으로 사라지기를 반복하지만 파이썬의 경우는 시간이 갈 수록 꾸준히 인기를 얻고 있는 언어입니다. 현재는 인공지능의 발전과 함께 가장 인기있는 언어가 되었습니다.

위의 그래프는 TIOBE에서 발표한 프로그램언어의 순위입니다. JAVA는 아주 오랜기간 C와 함께 가장 많이 사용하는 언어로 군림해왔습니다. 아마 JAVA 만큼 객체지향을 잘 표현한 언어는 없을 것입니다. 그만큼 자바는 객체지향의 대명사로 수십년동안 가장 많이 사용되는 언어였습니다. C언어는 그 역사와 활용도는 언급할 필요도 없이 지금도 강력한 영향력을 미치는 언어입니다.

파이썬은 2002년 당시에는 사용자가 많지 않은 언어였습니다. 그러나 2018년 이후 급격하게 사용자 수가 증가해서 현재는 C, JAVA를 이어서 그 다음으로 인기있는 언어가 되었습니다.

파이썬이 급성장한 이유는 인공지능의 발전과 함께 하기 때문이라는데는 반론의 여지가 없을 것입니다. 그렇다면 파이썬의 어떤 특징으로 인해서 현재 이토록 많은 사람이 사랑하는 언어가 되었을까요?

일단 파이썬은 배우기 쉬운 프로그램 언어입니다. 또 오픈소스이기 때문에 무료로 사용할 수 있고 풍성한 라이브러리를 지원하기 때문에 다양한 곳에 많이 사용됩니다. 또 영어와 유사한 문법으로 되어 있기 때문에 친근하다고 할 수 있습니다. 코드를 작성하기 쉽다는 것은 그만큼 읽기도 쉽다는 뜻이 됩니다. 또 컴파일의 과정도 없이 바로 실행하고 결과를 확인 할 수 있는 구조로 되어 있기 때문에 빠른 응답이 필요한 업무에 굉장히 편리합니다. 물론 그렇다고 컴파일 언어가 매력이 없다는 뜻은 아닙니다. 컴파일 언어는 그 나름대로 장점이 많이 있습니다.

파이썬은 이러한 여러가지 특징 때문에 고등학교, 대학교에서 프로그램을 강의하는데 처음 접하는 언어로 파이썬을 많이 사용하고 있습니다. 그렇다고 초보자만 사용하는 언어는 아닙니다. 인공지능을 개발하는 많은 분야의 전문가들이 파이썬을 사용하고 있습니다.

파이썬은 특히 인공지능이나 기계학습 분야에서 많이 사용됩니다.

그 이유는 파이썬 자체의 특별한 장점도 있지만 거기에 더해서 Numpy, SciPy와 같은 강력한 수치계산과 통계를 지원하는 다양한 라이브러리와 데이터를 다루는데 탁월한 Pandas, 시각화에 최적화되어 있는 matplotlib의 강력한 기능이 파이썬을 더 특별한 언어로 만들어 줍니다.

실제로 현재 가장 많이 사용하는 인공지능 프레임워크인 TensorFlow와 제가 이 블로그에 다양한 예제를 구현하는데 사용하는 PyTorch 같은 훌륭한 오픈소스 인공지능 프레임워크들이 파이썬 코드로 개발되어 있습니다.

또 Django와 같은 웹 어플리케이션 개발을 위한 프레임워크까지 구성되어 있기 때문에 만일 인공지능을 배우고자하는 분이 있다면 파이썬은 반드시 경험하고 배워야할 언어가 되었습니다.

본 예제에서는 파이썬의 간단한 문법을 소개해드리겠습니다.

물론 많은 다양한 블로그에서 다루고 있기 때문에 모든 것을 다 다루지는 않고 제가 그동안 구현하면서 필요하다고 생각되는 많은 사용하는 부분이나 꼭 필요한 부분들을 다뤄가도록 하겠습니다.

K-Means 클러스터

학습의 종류에는 레이블 데이터의 포함 여부에 따라서 지도학습(Supervised Learning)과 비지도학습(Unsupervised Learning)으로 나눌 수 있습니다. 지도학습 알고리즘은 종속변수의 특징에 따라서 분류(Classification)와 회귀(Regression)로 나눌 수 있습니다.
반면 비지도 학습은 레이블 데이터가 없는 형태의 데이터로 클러스터링(Clustering)이 가장 대표적인 기법입니다. 학습 데이터의 Feature를 파악해서 특징을 추출하고 이를 통해서 공통점이 있는 데이터를 묶어주는 형태의 분석기법입니다.

예를 들어서 클러스터링 기법을 활용해서 고객의 구매특징에 따라서 고객군을 묶어 줄 수 있습니다. 그렇게 된다면 고객군을 대상으로하는 맞춤형 마케팅도 가능합니다. 이밖에도 네트워크 유해 트래픽 탐지, 영화나 TV 장면 분류, 권역 설정, 뉴스나 토픽 클러스터링 등 다양한 분야에 활용됩니다. 그리고 이러한 알고리즘은 단독으로 사용되기도 하지만 또 여러 다른 알고리즘과 결합되서 사용되기도 합니다.

이중에서 K-Means 알고리즘은 가장 유명한 클러스터링 알고리즘입니다. “K”는 분석 대상 데이터로부터 클러스터 할 수 있는 수를 의미합니다. 그리고 Means는 각 클러스터의 중심의 평균거리를 의미합니다. 종합해보면 각 클러스터의 중심(Centroid)을 기준으로 주변에 있는 가까운 데이터들을 하나로 묶어주는 과정이라고 할 수 있습니다.

K-Means 알고리즘은 아래와 같은 과정으로 수행됩니다.

  1. Centroid 설정
  2. 각 데이터들을 가까운 Centroid에 속한 그룹에 할당
  3. 2번 과정을 통해서 만들어진 클러스터의 Centroid를 새롭게 지정
  4. 2번,3번의 과정을 Centroid가 변하지 않을때까지 반복 수행

아래의 예제를 통해서 기본적인 컨셉을 알아보겠습니다. 먼저 필요한 라이브러리들을 임포트합니다.

from sklearn.datasets import make_blobs
import pandas as pd
import numpy as np
import math
import scipy as sp
import matplotlib.pyplot as plt

테스트용 데이터를 만들기 위해서 sklearn의 make_blobs() 함수를 사용합니다. 샘플 데이터는 [300 × 2] 행렬입니다. 데이터는 4개의 군집을 가지고 있습니다.

x, y = make_blobs(n_samples=300, centers=4, n_features=2)
df = pd.DataFrame(x, y, columns=['x','y']).reset_index(drop=True)

생성한 데이터에서 4개의 centroid 값을 임의로 추출해봅니다. 샘플 데이터의 그룹과 추출한 값을 붉은색 점으로 표시해보겠습니다. 그림1을 보니 4개의 군집을 이루는 데이터를 확인했습니다. 또 4개의 임의의 점을 표시한 부분을 보니 각 군집의 중앙값과는 상당히 거리가 멀어보입니다. 이제 이러한 학습데이터를 통해서 Clustering을 해보겠습니다.

centroids = df.sample(4)
plt.scatter(x[:,0], x[:,1])
plt.scatter(centroids['x'], centroids['y'], c='r')
그림1. 임의의 Centroid 설정

아래의 함수는 각 테스트 데이터와 4개의 점의 거리를 계산하는 함수입니다. 아래의 함수를 실행하면 [300 × 4]의 행렬이 나오게됩니다. 그 이유는 4개의 중앙값과 샘플 데이터의 거리를 측정하기 때문입니다. 이렇게 측정한 값에서 np.argmin() 함수를 실행하면 4개의 점 중에서 가장 가까운 점의 값을 리턴하게됩니다. 그 데이터를 cluster_num에 입력합니다.

그리고 result라는 데이터셋을 리턴하게됩니다. 그러니까 result 데이터셋은 pandas dataframe의 자료형태를 가지고 있고 컬럼은 기존의 x, y외에 0,1,2,3 중에 하나의 값을 담고 있는 cluster라는 새로운 컬럼을 포함하고 있습니다.

def get_distance(center_df):
    # 각 데이터에 대하여, 각 중심점과의 유클리드 거리 계산
    distance = sp.spatial.distance.cdist(df, center_df, "euclidean")
    cluster_num = np.argmin(distance, axis=1)
    result = df.copy()
    result["cluster"] = np.array(cluster_num)
    return result

이 dataframe에서 cluster로 groupby한 후에 평균값을 계산하면 각 그룹의 x, y값 좌표를 리턴하게됩니다. 그 값을 아래의 scatter 그래프로 표시해보변 그림2와 같은 형태가 표시됩니다.

# cluster별로 묶어서 평균 계산
c = r.groupby("cluster").mean()
r = get_distance(c)
plt.scatter(r['x'], r['y'], c=r['cluster'])

그림2 그래프는 한눈에 봐도 clustering이 안돼보입니다. 이제 4가지 색의 군집의 중앙값을 구한 후에 centroid를 옮겨주고 다시 거리를 계산해봅니다. 이런 과정을 계속해보면 그림3, 그림4와 같이 점점 cluster별로 각기 다른 색으로 분류되는 것을 확인 할 수 있습니다.

그림2. 첫번째 수행
그림3. 두번째 수행
그림4. 3번째 수행

즉, centroid 값을 임의로 정해주고 그 포인트를 중심으로 clustering을 수행한 후에 clustering한 값을 중심으로 다시 centroid를 정하고 다시 clustering을 수행하는 작업을 더이상의 centroid 값이 변화가 없을 때까지 수행하면 군집이 형성되는 것이 바로 군집분석의 기본 알고리즘입니다.

sklearn K-Means 사용

sklearn에는 다른 머신러닝 알고리즘과 마찬가지로 비지도학습을 위한 클러스터링 알고리즘인 K-Means 알고리즘을 패키지 형태로 제공하고 있습니다. sklearn을 사용하면 방금 위에서 했던것과 같은 복잡한 작업을 대신해주기 때문에 편리하게 데이터 분석을 할 수 있습니다.

테스트 데이터로 그동안 사용했던 fitness.csv 데이터를 활용해서 테스트해보겠습니다. 데이터의 수가 많지 않아서 분류 결과가 아쉽게도 좋지 않지만 그래도 수행하는 방법에 대해서 가이드가 될 수 있을듯합니다. 테스트는 먼저 파일을 읽은 다음 해당 데이터셋은 레이블이 없기 때문에 테스트 삼아 임의로 레이블을 만들어보고 클러스터가 어떻게 예측했는지 비교해보겠습니다. 다시 말씀드리지만 정확성을 위해서 수행하는 부분은 아님을 알려드립니다.

dataset = pd.read_csv('./fitness.csv')
dataset.loc[ dataset['age'] < 40, 'ACODE']= 0
dataset.loc[ (dataset['age'] >= 40) & (dataset['age'] <50), 'ACODE']= 1
dataset.loc[ (dataset['age'] >= 50) & (dataset['age'] <60), 'ACODE']= 2
dataset['ACODE'] = dataset['ACODE'].astype('int32')
dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 31 entries, 0 to 30
Data columns (total 8 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   age       31 non-null     int64  
 1   weight    31 non-null     float64
 2   oxygen    31 non-null     float64
 3   runtime   31 non-null     float64
 4   runpulse  31 non-null     int64  
 5   rstpulse  31 non-null     int64  
 6   maxpulse  31 non-null     int64  
 7   ACODE     31 non-null     int32  
dtypes: float64(3), int32(1), int64(4)
memory usage: 1.9 KB

sklearn 패키지에서 KMeans 패키지를 임포트합니다. 사용할 때에 culster가 우리는 3개로 알고 있기 때문에 n_cluster=3으로 설정해줍니다. 해당 알고리즘에서 가장 중요한 부분이 centroid를 초기에 어떻게 설정할것인가에 의한 것인데 init을 정해주지 않는다면 기본적으로 k-means++ 방법을 사용합니다. n_init는 30회 중앙값을 각기 다른 포인트로 설정해주고 그중에 가장 best 값을 활용합니다. 이 외에도 다양한 파라메터가 존재하니 공식 홈페이지를 살펴보시는 것을 추천합니다.

https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html

from sklearn.cluster import KMeans 
kmeans = KMeans(n_clusters=3, n_init=30,)
kmeans.fit(dataset[dataset.columns[:-1]]) # ACODE 제외
dataset['ACODE2'] = kmeans.labels_

예측결과를 ACODE2라는 컬럼에 데이터를 추가해봅니다.

(dataset['ACODE'].values == dataset['ACODE2'].values).sum()/len(dataset)
# 0.3225806451612903

예제로 사용했던 데이터의 경우는 사전에 3개의 클래스를 알고 있었지만 비지도 학습은 이에 대한 정보가 주어지지 않기 때문에 어떻게 군집을 만드는 것이 가장 좋은 케이스인지 알 수 없는 경우가 대부분입니다. 그럴 경우 아래와 같은 방식으로 num_cluster를 체크해볼 필요가 있습니다. 그림5의 경우는 num_cluster를 정하기가 어렵습니다. 그 이유는 데이터가 군집하기 어려운 형태로 분산되어 있기 때문입니다. 굳이 한다면 4,5정도가 좋을 듯합니다.

반면 그림6은 클러스터의 갯수가 명확합니다. 3개 정도가 가장 좋은 케이스라고 여집니다. 이것 역시 데이터에 따라서 차이가 있기 때문에 사전에 확인을 해보는 것도 좋은 방법입니다.

num_cluster = range(1,10)
inertia_ = []

for c in num_cluster:
    model = KMeans(n_clusters=c)
    model.fit(dataset[dataset.columns[:-2]])
    inertia_.append(model.inertia_)

# Plot ks vs inertias
plt.plot(num_cluster, inertia_, '-o')
plt.xlabel('number of clusters, k')
plt.ylabel('inertia_')
plt.xticks(num_cluster)
plt.show()
그림 5 num_cluster
그림 6. num_cluster

그림 6은 health.csv 데이터셋의 주성분분석으로 나타낸 그림입니다. 확인 결과 클러스터링에는 적절치 않은 데이터로 확인되네요. 아마도 데이터의 수가 많지 않기 때문이라고 생각됩니다.

from sklearn.decomposition import PCA
pca = PCA(n_components=2)
dim2 = pca.fit_transform(dataset[dataset.columns[:-2]])
plt.scatter(dim2[:,0], dim2[:,1], c=dataset['ACODE'].values )
그림 6. health cluster

위와 같이 k-Means가 항상 좋은 결과를 낼 수 있는 것은 아닙니다.
각각의 데이터가 잘 모여져있다면 좋은 결과를 내지만 그렇지 않고 그림6과 같이 군집이 약할 경우는 좋은 결과를 얻을 수 없습니다.

그림 7
그림 8

K-Means가 좋은 결과를 얻지 못하는 경우는 그림7과 같은 형태의 데이터일 경우도 좋은 성능을 발휘할 수 없습니다. K-Means는 클러스터의 방향성을 고려하지 않고 무조건 거리가 가까운 데이터를 클러스터로 묶어주기 때문에 위와 같은 형태의 데이터는 잘 반영하지 못합니다. 그림에서와 같이 클러스터를 3개로 분류했지만 한눈에 보기에도 좋은 분류가 아님을 확인할 수 있습니다.

이와 마찬가지로 그림8도 역시 좋은 결과를 얻을 수 없습니다.

랜덤포레스트(Random Forest)

랜덤포레스트는 의사결정트리(Decision Tree)와 닮은 점이 많은 지도학습 예측모델입니다. 두 모델 모두 어떤 질문에 의해서 데이터 셋을 분리하는(가지를 만드는) 기본적인 방식은 닮았지만 의사결정나무가 전체 데이터를 통해서 성능이 좋은 하나의 나무를 만드는데 목적이 있다면 램덤포레스트는 하나의 나무를 만들기 보다는 데이터셋을 랜덤하게 샘플링해서 여러개의 예측 모델을 만들고 그 이러한 모델들을 종합해서 하나의 예측 결과를 리턴하는 방법으로 최종 예측을 수행합니다.

이러한 과정을 Bagging(= Bootstrap + Aggregation)이라고 합니다. 즉, 주어진 하나의 큰 데이터를 여러 개의 부트스트랩 자료(중복허용)를 생성하고 각 부트스트랩 자료를 모델링한 결과를 통합하여 최종의 예측 모델을 산출하는 방법입니다. 이런 예측 모델은 데이터가 변동성이 큰 경우 원자료(Raw Data)로부터 여러번의 샘플링읕 통해서 예측 모델의 정확도를 높이는 기법입니다. 이런 과정을 통해서 n개의 예측 모델이 만들어집니다. 그러나 결국 필요한 것은 하나의 데이터이기 때문에 각각의 모델의 결과 값에 대해서 회귀분석(평균계산)을 하던가 과반 투표(분류 모델)를 통해서 최종 결과 값을 만들게 됩니다.
이러한 방법의 알고리즘 중에 가장 대표적인 모델이 바로 랜덤포레스트(Random Forest)입니다.

이러한 복잡한 알고리즘을 sklearn의 ensemble 패키지의 RandomForestClassifier에서 훌륭하게 구현하고 있습니다.

import numpy as np
import pandas as pd

from sklearn.datasets import make_blobs, make_classification
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

테스트 데이터 생성을 위해서 sklearn의 make_classification를 사용하겠습니다. 데이터의 수는 총 300개, feature는 5개이며이며 각각의 feature는 종속변수와 상관관계가 존재합니다. 해당 데이터 셋의 클러스터는 1이고 데이터의 클래스는 3입니다. 즉, 최종 예측은 0,1,2 이 셋 중에 하나의 값을 가진다는 뜻입니다.

위에 보면 등분산성 데이터를 만들어주는 make_blobs()를 사용한 부분이 있는데 이렇게 하면 데이터가 분류에 너무 최적화 되어 있기 때문에 make_classification()을 사용합니다.

#x, y = make_blobs(n_samples=300, n_features=5, centers=3) 
x, y = make_classification(n_samples=300, n_features=5, n_informative=5, n_redundant=0, n_clusters_per_class=1, n_classes=3)

데이터 셋을 만든 다음 모델의 정확도를 검증하기 위해서 train, test 데이터 셋으로 분리합니다. 분리는 8:2 정도로 하겠습니다.

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.2, random_state=0) # 8:2
print(x_train.shape, x_test.shape)
# (240, 5) (60, 5)

sklearn에 보면 RandomForestClassifier를 만드는데 몇가지 파라메터들이 있습니다. 그중에 예제에서는 n_estimators(The number of trees in the forest.)를 사용합니다. 해당 파라메터는 하나의 나무를 만드는 의사결정나무(Decision-Tree)와는 다르게 여러개의 모델을 만드는 RandomForest의 특징입니다. 나무를 많이 만들면 예측의 정확도는 높아 질 수 있지만 그만큼 많은 자원을 필요로 하고 또 일정 수준 이상으로는 높아지지 않기 때문에 가능하면 테스트를 하면서 수를 늘려가는 방식으로 수행하는 것을 추천합니다.

해당 모델은 데이터셋이 비교적 구분이 잘되어 있는 데이터셋이기 때문에 10으로 셋팅합니다.

from sklearn.ensemble import RandomForestClassifier
randomfc = RandomForestClassifier(n_estimators=10).fit(x_train, y_train)
randomfc
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                       max_depth=None, max_features='auto', max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, n_estimators=10,
                       n_jobs=None, oob_score=False, random_state=None,
                       verbose=0, warm_start=False)

모델의 정확도는 85%로 측정되었습니다. 데이터 셋은 랜덤으로 만들어지기 때문에 만들 때마다 정확도가 다르게 측정됩니다.

predict = randomfc.predict(x_test)
print(randomfc.score(x_test, y_test))
# 0.85

더 자세한 결과를 보기 위해서 confusion_matrix를 체크해봅니다. 2~4개의 오차가 발견되지만결과 관대한 저에게는 대부분 정확하게 예측을 한것으로 보입니다. (다르게 보일 수도 있습니다.)

from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, predict)
# output #
array([[14,  1,  3],
       [ 1, 18,  1],
       [ 3,  0, 19]])

classification_report를 통해서 결과를 더 자세히 살펴보겠습니다. 각각의 용어들은 아래와 같은 의미가 있습니다.

정확도(accuracy) : 예측한 값의 몇개를 맞췄는가?
정밀도(precision) : 예측한 것중에 정답의 비율은?
재현율(recall) : 정답인 것을 모델이 어떻게 예측했는가?
F1 Score : 정밀도와 재현율의 가중조화평균(weight harmonic average)을 F점수(F-score)라고 정의합니다. 즉, F1 Score 값이 높으면 성능이 높다고 할 수 있습니다.

from sklearn.metrics import classification_report
report = classification_report(y_test, predict)
# output #
              precision    recall  f1-score   support

           0       0.78      0.78      0.78        18
           1       0.95      0.90      0.92        20
           2       0.83      0.86      0.84        22

    accuracy                           0.85        60
   macro avg       0.85      0.85      0.85        60
weighted avg       0.85      0.85      0.85        60

이제 랜덤하게 생성된 임의의 데이터가 아닌 load_digits 데이터셋을 활용해보도록 하겠습니다. 해당 데이터셋에 대한 설명은 생략하겠습니다.

from sklearn.datasets import load_digits
digits = load_digits()

x_train, x_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size = 0.2, random_state=0) # 8:2
print(x_train.shape, x_test.shape)
# (1437, 64) (360, 64)
randomfc = RandomForestClassifier(n_estimators=10).fit(x_train, y_train)
print(randomfc.score(x_test, y_test) )
# 0.9472222222222222
predict = randomfc.predict(x_test)
confusion_matrix(y_test, predict)

array([[27,  0,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 0, 34,  0,  0,  0,  1,  0,  0,  0,  0],
       [ 1,  0, 33,  2,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0, 29,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  0, 28,  0,  0,  2,  0,  0],
       [ 0,  0,  0,  0,  0, 39,  0,  0,  1,  0],
       [ 0,  1,  0,  0,  0,  0, 42,  0,  1,  0],
       [ 0,  0,  0,  0,  0,  0,  0, 38,  1,  0],
       [ 0,  2,  0,  2,  0,  0,  0,  0, 35,  0],
       [ 0,  3,  0,  1,  0,  0,  0,  1,  0, 36]])
report = classification_report(y_test, predict)
print(report)

              precision    recall  f1-score   support

           0       0.96      1.00      0.98        27
           1       0.85      0.97      0.91        35
           2       1.00      0.92      0.96        36
           3       0.85      1.00      0.92        29
           4       1.00      0.93      0.97        30
           5       0.97      0.97      0.97        40
           6       1.00      0.95      0.98        44
           7       0.93      0.97      0.95        39
           8       0.92      0.90      0.91        39
           9       1.00      0.88      0.94        41

    accuracy                           0.95       360
   macro avg       0.95      0.95      0.95       360
weighted avg       0.95      0.95      0.95       360

서포트벡터머신(Support Vector Machine)

서포트벡터머신(SVM,Support Vector Machine, 이하 SVM)은 강력한 머신러닝 모델이며 또 다양한 정형 데이터셋에서 좋은 성능을 보여줍니다. SVM은 데이터의 특징이 몇개 되지 않더라도 복잡한 결정경계(Decision Boundary)를 만들어줍니다.

SVM은 저차원, 고차원의 모델에서 좋은 성능을 발휘하지만 샘플이 많을 경우 예를 들어 100,000개 이상의 데이터를 처리하는데는 성능이 좋지 않습니다. 또 데이터 전처리(MinMaxScaler와 같은 작업이 필요)와 매개변수 설정에 좀 더 고민해야 할 부분들이 많이 있고 예측이 어떤 결과를 통해서 이뤄졌는지에 대한 모델 설명이 쉽지 않은 부분이 있습니다.

그럼에도 SVM은 특징이 비슷한 단위이고 샘플 수가 많지 않다면 좋은 결과를 얻을 수 있는 모델입니다.

아래와 같은 예제를 통해서 SVM 모델을 테스트해보겠습니다.

from sklearn.datasets import make_classification
from sklearn.datasets import make_blobs
from sklearn import svm

import numpy as np

import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, GridSearchCV

테스트 데이터는 sklearn의 make_classification() 함수를 사용해서 만들었습니다. 그렇기 때문에 이 코드를 통해서 테스트하시는 분들은 다른 형태의 그래프가 그려집니다. 데이터를 계속 새로 만들면서 결정경계가 어떻게 표시되는지 테스트해보시는 것도 좋겠습니다.

저는 2개의 feature가 있는 1,000개의 샘플 데이터를 생성했습니다. 생성한 데이터를 통해서 시각화 해보면 아래와 같은 그래프가 표시됩니다. 이 그래프는 선형으로 분류가 불가능합니다. 두개의 클래스가 겹쳐있기 때문입니다. 그렇기 때문에 차원을 높여서 비선형(Nonlinear) 방식의 분류가 필요합니다.

예제를 수행하기에 앞서 간단히 설명하면 서포트벡터머신(Support Vector Machine)은 선형 데이터 분류와 비선형 데이터 분류 모두 가능합니다. 그 방법은 데이터를 분류할 수 있는 직선을 긋고 직선을 기준으로 데이터를 분류하는 것입니다. 테스트 예제인 그림 1의 경우는 직선으로 분류할 수 없기 때문에 커널(kernel)이라는 형태의 방법이 필요하지만 그림2의 경우는 데이터가 굉장히 명확하게 구분되어 있기 때문에(몇개의 아웃라이어가 있지만…) 하나의 직선으로 두개의 데이터 그룹을 분리 할 수 있습니다. 정리하면 그림1은 비선형, 그림 2는 선형 이라고 하고 SVM은 이 두가지 모두 가능합니다.

그림2에서 좀 더 자세히 설명하면 두개의 데이터 그룹이 있고 한 그룹마다 데이터가 많이 존재하고 있지만 실제로 모든 데이터가 결정경계(Decision Boundary)를 만드는 것은 아니고 일부의 데이터만 결정경계를 만드는데 사용됩니다. 이것은 비교적 적은 연산 과정으로도 결과를 얻을 수 있다는 의미이며 또 속도가 빠르다는 의미도됩니다.
이러한 데이터들을 서포트 벡터(Support Vector)라고 표현합니다. 풀어서 말하면 결정경계를 존재하게 하는 지지 벡터라고 할 수 있습니다. 그런데 왜 점이라고 하지 않고 벡터라고 했을까를 생각해보면 어떤 포인트의 연속된 데이터 형태를 표현하기 위해서 라고 이해할 수 있습니다.

색을 표시하기 위해서 아래와 같은 컬러 코드를 사용했습니다.

red_RGB = (1,0,0)
blue_RGB = (0,0,1)
data_colors = [red_RGB, blue_RGB]

def get_colors(y):
    return [data_colors[label] for label in y]
X,y = make_classification(n_samples=1000, n_features=2, n_informative=1, n_redundant=0, n_clusters_per_class=1)
plt.scatter(X[:,0], X[:,1], c=get_colors(y), s=10)
그림 1
그림 2

이제 그림1의 데이터로 다시 돌아와서 train 데이터와 test 데이터로 나눠줍니다. 분리는 8:2의 형태로 나누겠습니다.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state=0) # 8:2

데이터를 표시하기 위해서 시각화 함수를 정의합니다.

def plot_decision_function_helper(X, y, clf, show_only_decision_function = False):

    colors = get_colors(y)
    plt.axis('equal')
    plt.tight_layout()

    plt.scatter(X[:, 0], X[:, 1], c=colors, s=10, edgecolors=colors)
    ax = plt.gca()
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    # Create grid to evaluate model
    xx = np.linspace(xlim[0], xlim[1], 30)
    yy = np.linspace(ylim[0], ylim[1], 30)
    YY, XX = np.meshgrid(yy, xx)
    xy = np.vstack([XX.ravel(), YY.ravel()]).T # xy.shape = (900, 2)
    Z = clf.decision_function(xy).reshape(XX.shape)

    if  show_only_decision_function:
        ax.contour(XX, YY, Z, colors='k', levels=[0], alpha=0.5,
                 linestyles=['-'])
    else :
        ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,
                 linestyles=['--', '-', '--'])

def plot_decision_function(X_train, y_train, clf):
    colors = get_colors(y_train)
    
    plt.figure(figsize=(8,4), dpi=150)
    plt.title('Support Vector Machine Classifier')
    plot_decision_function_helper(X_train, y_train, clf, False)

SVM은 비선형 데이터를 고차원으로 확장하여 분리할 수 있도록 rbf, linear, poly 커널을 제공합니다. 기본값은 rbf 커널입니다.

clf = svm.SVC() #default kernel ='rbf'    
print(clf)
clf.fit(X_train, y_train)

plot_decision_function(X, y, clf)
print("Accuracy: {}%".format(clf.score(X_test, y_test) * 100 ))
#Accuracy: 92.5%
clf2 = svm.SVC(kernel='linear', C=1)    
clf2.fit(X, y)
plot_decision_function(X, y, clf2)
print("Accuracy: {}%".format(clf2.score(X_test, y_test) * 100 ))
#Accuracy: 91.5%
clf4 = svm.SVC(kernel='poly', C=1)    
clf4.fit(X, y)
plot_decision_function(X, y, clf4)
print("Accuracy: {}%".format(clf4.score(X_test, y_test) * 100 ))
#Accuracy: 90.5%

참고로 결정경계를 만들때 고려해야 할 내용은 마진을 어떻게 결정하는가? 에 대한 내용입니다. 이것은 “C”를 어떻게 설정할 것인가에 따라 달라집니다. C는 기본값은 1이지만 데이터의 특징에 따라 더 많이 설정할 수도 있습니다.

만약 아웃라이어를 최대한 허용하지 않고 모든 데이터를 대상으로 까다롭게(?) 만드는 경우 이것을 하드 마진(hard margin)이라고 부릅니다. 이렇게 되면 서포트 벡터와 결정 경계 사이의 거리가 매우 좁다. 즉, 마진이 매우 작아진다. 이렇게 개별적인 학습 데이터들을 다 놓치지 않으려고 아웃라이어를 허용하지 않는 기준으로 결정 경계를 정해버리면 오버피팅(overfitting) 문제가 발생할 수 있다.

반대로 마진(margin)을 어느 정도 오류값들이 포함되도록 너그럽게 기준을 세운다면 이것을 소프트 마진(soft margin)이라고 부릅니다. 이렇게 너그럽게(?) 잡아 놓으면 서포트 벡터와 결정 경계 사이의 거리가 멀어지는 즉, 마진이 현상이 나타납니다. 그렇게되면 언더피팅(underfitting) 문제가 발생할 수 있다. 그렇기 때문에 훈련 데이터의 오류를 어느 정도는 허용할 것인가에 대한 기준을 잡고 모델을 설계하는 것이 필요합니다.

또 하나 고려해야 할 부분은 gamma 값입니다. gamma는 훈련 샘플이 미치는 영향의 범위를 의미하는 것으로 작은 값은 넓은 영역을 큰 값이라면 미치는 범위가 제한적인 특징이 있습니다.

Reference

https://www.learnopencv.com/svm-using-scikit-learn-in-python/

로지스틱 회귀(Logistic Regression)

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
import matplotlib.pyplot as plt

아래와 같은 데이터가 있다고 생각해보겠습니다. 독립변수 x는 0-9까지의 값이 입력되어 있고 종속변수 y는 연속된 수치 형태의 값이 아닌 0,1의 이항변수로 되어 있습니다. 데이터를 통해 보니 0-4까지는 0, 5-9까지는 1을 리턴하는 형태입니다. 예를 들어서 체력 테스트를 하는데 턱걸이 0-4까지는 0(불합격), 5-9까지는 합격(1) 이라고 가정해보겠습니다.

이러한 문제를 선형회귀의 형태로 분석해본다면 어떻게될까요? 분석이 가능할까요?
가능해보기도 합니다. 먼저 데이터를 시각화 해보겠습니다.

x = [[1],[2],[3],[4],[0],[5],[6],[7],[8],[9]]
y = [0,0,0,0,0,1,1,1,1,1]
line = np.linspace(0,1,num=10)
yline = np.linspace(0.5,0.5,num=10)

plt.scatter(x,y)
plt.plot(line)
plt.plot(yline)

데이터를 시각화 해보면 위와 같은 형태의 그래프가 표시됩니다. 그리고 이러한 데이터라면 선형회귀로도 가능할듯 보입니다. 0-4, 5-9의 데이터를 중간에 주황색 선을 0.5를 기준으로 입력되는 데이터가 0.5 보다 크면 1을 출력하고 작으면 0을 출력하게 하면 될것같습니다.
이러한 기본 컨셉을 가지고 선형회귀를 통해서 해당 내용을 구현해보겠습니다.

from sklearn.linear_model import LinearRegression
linear = LinearRegression().fit(x,y)
predict = linear.predict([[50]])
print(predict)
# 7.39393939

위와 같은 모델을 구현하고 테스트 해보니 0-9까지의 턱걸이 갯수로는 예측이 비교적 잘되는듯 보입니다. 하지만 어떤 사람이 와서 턱걸이를 50회 혹은 그 이상을 하게되면 예측치가 합격(1)/불합격(0)을 벗어서나 7이라는 숫자가 나오게됩니다. 더 큰 숫자를 입력하면 예측치가 더 높게 나오겠죠.

이렇게되면 이 모델은 사용할 수가 없습니다.
만약 이럴 때에 턱걸이를 아무리 많이 해도 합격과 불합격으로만 결과치를 표시하고 싶다면 어떻게 하는게 좋을까요?

이럴 때에 사용하는 것이 로지스틱 회귀입니다. 같은 데이터로 sklearn의 로지스틱회귀(LogisticRegression) 패키지를 사용해서 모델을 만들어보면 50이라는 큰 숫자를 입력해도 합격을 의미하는 1이라는 값을 보여주게됩니다.

from sklearn.linear_model import LogisticRegression
logistic = LogisticRegression().fit(x,y)
predict = logistic.predict([[50]])
print(predict)
# 1

이러한 것을 가능하게 해주는 것은 Hypothesis가 기존의 선형회귀 모델과 다른 특별한 형태를 가지기 때문입니다. 이것을 Sigmoid라고 부릅니다.

이 함수에서 보면 z의 값은 선형회귀의 Wx+b의 값, 즉 선형회귀에서의 출력값을 사용합니다. 수식에 따라서 출력은 0-1 사이의 실수 값을 리턴하게 되고 0.5를 기준으로 작으면 0을 크면 1을 출력해줄 수 있게됩니다.

그렇다면 최적화는 어떻게 하게될까? 선형회귀에서는 MSE(Mean Square Error)를 사용하지만 로지스틱에서는 아래와 같은 Cross-Entropy를 사용합니다.

https://towardsdatascience.com/cross-entropy-for-classification-d98e7f974451

위의 식은 두개의 식을 하나로 합친 것입니다.

결정트리(Decision Tree)

결정트리는 결론에 이르기 위해서 “예/아니오”의 답변을 요구하는 질문을 반복하면서 학습을 진행합니다. 마치 스무고개 질문과 비슷하죠. 최소한의 적은 질문으로 어떤 문제를 해결하기 위해서는 질문을 잘해야 하는것처럼 결정트리의 중요한 부분도 “어떻게 질문하는가?” 하는 것 중요합니다.

https://kr.akinator.com/

위의 프로그램은 “akinator”라는 게임입니다. 사용자는 아키네이터의 질문에 따라서 답을 고르게 됩니다. 이런 방법으로 몇차례 질문과 답을 주고 받으면 내가 머리속으로 그려낸 인물에 대해서 정확하게 예측합니다. 이것은 이미 이 프로그램을 통해 게임을 진행했던 사람들의 많은 데이터가 축적되어 있기 때문에 가능한 일입니다. 제가 테스트 했던 인물은 축구선수 손흥민이 이었는데 손흥민선수는 이미 222,308회 선택되어 졌기 때문에 많은 데이터가 축적되어 있었습니다. 그렇기 때문에 몇번 안돼서 아키네이터가 답을 맞췄습니다.

결정트리가 마치 이와 같습니다. 결정트리도 데이터셋에서 질문을 통해서 분류(classification) 작업을 수행합니다. 맨 마지막에 마지막 답이 남을 때까지 데이터를 분류합니다. 이렇게 남는 맨 마지막 노드를 리프(lefa) 노드라고 합니다. 그리고 각 노드들은 엣지(edge)로 연결되어 있습니다.

분류에도 여러 알고리즘들이 있는데 그렇다명 언제 결정트리(Decision Tree)를 사용하면 좋을까요?
예를 들어서 고객이 은행에 대출요청을 했고 은행이 고객에 대한 수락 요청을 거부했을 경우 고객에게 어떤 이유로 대출이 불가한지 설명해야 한다면 바로 이 의사결정트리가 유용한 알고리즘이 될 수있습니다.

해당 알고리즘이 가장 높은 성능을 발휘한다는 보장은 없지만 그래도 어떤 결정에 대해서 설명이 가능하다는 것은 중요한 이유가됩니다. 바로 한눈에 보고 이해할 수 있다는 이러한 정점이 있기 때문에 어떤 결정에 합리적인 판단 기준으로 제시하기 위해 해당 알고리즘을 사용합니다.

의사결정나무은 예측 변수를 기반으로 결과를 분류하거나 예측하는데 이러한 의사결정의 규칙(Decision Rule)을 나타내는 과정이 마치 나무구조와 같다고 해서 붙여진 이름입니다. 해당 알고리즘을 활용하면 분류(classification)나 예측(prediction)도 수행이 가능합니다.

아래의 예제는 붓꽃 데이터를 Decision-Tree 알고리즘을 통해 구성한 결과입니다.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# DecicionTreeClassifier 생성
tree_iris = DecisionTreeClassifier()

# 붓꽃 데이터를 로딩하고, 학습과 테스트 데이터 세트로 분리
iris_data = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris_data.data, iris_data.target, test_size=0.2, random_state=11)

# DecisionTreeClassifier 학습
tree_iris.fit(X_train, y_train)
from sklearn.tree import export_graphviz
export_graphviz(tree_iris, out_file='tree-iris.dot', class_names=['Setosa','Versicolour','Virginica'], feature_names=iris_data.feature_names, impurity=False, filled=True)

분류를 수행하고 그 결과를 ‘tree_iris.dot’이라는 파일로 기록합니다.

digraph Tree {
node [shape=box, style="filled", color="black"] ;
0 [label="petal length (cm) <= 2.45\nsamples = 120\nvalue = [41, 40, 39]\nclass = Setosa", fillcolor="#fffdfd"] ;
1 [label="samples = 41\nvalue = [41, 0, 0]\nclass = Setosa", fillcolor="#e58139"] ;
0 -> 1 [labeldistance=2.5, labelangle=45, headlabel="True"] ...

해당 파일을 읽어보면 위와 같이 코드의 형태로 표시됩니다. 이렇게 만들어진 코드를 바로 이해해도 되겠지만 무리가 있죠. 파이썬의 graphviz라는 패키지를 이용하면 이러한 데이터를 훌륭히 시각화 할 수 있습니다.

# 아래와 같이 패키지를 설치합니다.
conda install python-graphviz

패키지를 설치하고 tree-iris.dot 파일을 읽어보겠습니다.

import graphviz

with open('./tree-iris.dot') as f:
    dot_graph = f.read()
display(graphviz.Source(dot_graph))

위의 그림을 살펴보면 결정트리를 IF(petal length <= 2.45) THEN Setosa ELSE IF(petal width <= 1.55) and IF(petal length <= 5.25) THEN vesicolour … 와 같은 방법으로 표현할 수 있습니다. 그렇기 때문에 어떤 결과를 얻었을 때에 규칙을 통해서 데이터를 얻었다고 이야기 할 수 있는 것입니다.

가장 위에 있는 노드는 Root-Node 라고 하고 Parent-Child Node의 연결이 마치 나무의 구조와 같다고 해서 의사결정나무라고도 부릅니다. 그리고 맨 마지막에 있는 것이 Teminal-Node라고 부릅니다. 각 노드의 연결선을 엣지(Edge) 라고 하고 Yes(True)/No(False)를 의미합니다.

루트노드는 가장 첫번째 질문으로 매우 중요합니다. 이 질문은 가능한 많은 데이터를 분류 할 수 있는 질문이어야 합니다. 위의 경우는 Petal Length <= 2.45를 기준으로 총 120개의 샘플 데이터를 41:79로 분류했습니다. 그리고 41은 전부 “Setosa”라는 종류라는 것을 얻었습니다. 그리고 나머지 79개의 데이터를 다시 Petal Width <= 1.55라는 조건을 통해서 38:41의 데이터로 분류해 냅니다. 이런 과정을 반복하면서 데이터를 마지막가지 분류하게됩니다.

이 결정트리를 그동안 사용했던 fitness.csv를 통해서 적용해 보겠습니다.

dataset = pd.read_csv('./fitness.csv')
dataset.loc[ dataset['age'] < 40, 'ACODE']= 1
dataset.loc[ (dataset['age'] >= 40) & (dataset['age'] <50), 'ACODE']= 2
dataset.loc[ (dataset['age'] >= 50) & (dataset['age'] <60), 'ACODE']= 3

x_data = dataset[dataset.columns[1:-1]].values
y_data = dataset[dataset.columns[-1]].values

fit_tree = DecisionTreeClassifier()
fit_tree.fit(x_data, y_data)

from sklearn.tree import export_graphviz
export_graphviz(fit_tree, out_file='tree-fit.dot', class_names=['30','40','50'], feature_names=dataset.columns[1:-1], impurity=False, filled=True)

with open('./tree-fit.dot') as f:
    dot_graph = f.read()
display(graphviz.Source(dot_graph))

위에서 보시는것처럼 IF maxpulse <= 174 AND oxygen <= 46.723 THEN 50 라는 예측결과를 얻었습니다. 이런 방법으로 조건의 만족 여부를 따라서 이동하다보면 결국 class 값을 얻을 수 있습니다.

위 예제에서는 이지분리(Binary Split)을 사용했지만 알고리즘에 따라서 다지분리(Multi Split)도 가능합니다.