edwith CS50강의를 보며 정리했습니다.

 


● 기억장치

 

CPU의 용량은 굉장히 제한적이라서 1MB정도의 저장공간만 있다. 한 번에 64bit정도만 처리하면 되기 때문이다. CPU가 두 수를 더한다면 64bit + 64bit 이기 때문에 파일 크기와 관계 없이 적은 양만 CPU로 가져온다. 계산을 하는 동안 데이터를 다른 곳에 저장하는데, 그것을 저장하는 곳이 바로 RAM이다.

RAM은 임의 접근 기억장치다. 메모리는 두 가지 종류가 있는데, RAM과 하드디스크가 있다. 두 메모리는 반대의 성질을 띄는데, 하드디스크는 영구적이고 휘발성이 없는데 반해 RAM은 일시적이고 전원을 끄면 사라지기 때문에 휘발성이 있다. 보통 파일이나 프로그램을 더블클릭하면 도달하는 곳이 RAM이다. 문서를 작성하거나 프로그램을 실행할 때 자료나 프로그램들은 하드디스크에 복사되고 RAM에 일시저장된다. RAM에 저장되는 이유는 하드디스크보다 빠르기 때문이다.

RAM이 더 빠른데도 불구하고 하드디스크를 보통 더 크게(많이) 가지고 있는 이유는 하드디스크에 있는 내용물을 동시에 볼 필요가 없기 때문이다. 우리가 동시에 하드디스크의 모든 프로그램을 실행하지는 않기 때문이다. 또한, CPU에는 어차피 병목현상이 있기 때문에 하드디스크의 용량과 무관하게 훨씬 좁은 파이프라인으로 데이터가 흘러 들어가게 된다.

RAM의 용량이 더 작은 이유는 비싸기도 하고 기술적으로 아직 많은 용량을 담을 수 없기 때문이다. RAM이 더 동적이라고도 말할 수 있는데, RAM이 여러가지를 한꺼번에 처리하는 반면에 하드디스크는 정적인 상태라고 말할 수 있다. 하지만 RAM과 하드디스크는 서로 상응하는 관계이기 때문에 계속해서 커질 것이다. 

하드디스크에 있는 프로그램을 더블클릭하면 RAM에 적재되고 이 일이 발생하기 위해서는 컴퓨터가 입력장치(마우스,키보드)에 반응하기 위해 CPU로 비트가 흘러 들어간다. 메모리의 종류에는 L1,L2,L3 등의 캐시 종류가 있는데 L1은 1차, L2는 2차를 의미한다. 

L1 캐시가 이 셋 중에서 가장 작고 빠르며, 중앙처리장치가 재빨리 받아 처리할 수 있도록 몇 킬로바이트의 데이터만을 저장한다. L2 캐시는 L1 캐시보다 조금 크지만, 그만큼 더 느리다. L3 캐시는 보통 몇 메가바이트를 저장할 수 있어 셋 중 가장 크지만 가장 느리다. 그래도 L3 캐시는 RAM보다는 빠르다.

CPU에 가까울수록 더 빨리 데이터를 받을 수 있지만 양이 적다. 기술적 효율성 때문에 이런 순서로 진행이 된다. CPU는 항상 할 일이 있고 비트를 계속 보낼 수 있기 때문에 빨라야 하고, CPU에서 멀어질수록 느려도 된다. 

하드디스크가 크면 더 많은 것들을 저장할 수 있고, RAM이 크면 동시에 더 많은 일을 한꺼번에 수행할 수 있다. 사실 '동시에' 수행되지는 않지만 엄청나게 빠른 속도로 수행되기 때문에 동시에 수행되는 것처럼 느껴지는 것이다. CPU는 한번에 한가지 일밖에 처리할 수는 없지만 인간이 컴퓨터보다 상대적으로 느리기 때문이다.

요즘 운영체제에서는 '가상 기억장치'라는 것을 제공하는데 만약 500MB 정도의 작은 RAM에서 파일을 실행한다면 이 비트들을 RAM에서 하드디스크로 옮긴다. 프로그램을 실행시키기 위한 공간이 필요하기 때문이다. 그래서 우리가 어떠한 프로그램을 실행할 때 버벅거리거나 로딩이 오래걸린다면 그러한 가상 기억장치로의 이동작업이 이루어졌기 때문이다.

이렇게 버벅거리는 동안 하드디스크에 있던 프로그램을 다시 RAM으로 옮기고 RAM에 있던 프로그램을 다시 하드디스크로 옮기는 작업이 이루어진다. RAM이 크면 이러한 과정들이 생략되기 때문에, PC를 구매할 때 램(메모리)가 큰 것에 투자하는 것이 좋다. 물론 CPU나 디스크(하드) 크기가 큰 것도 좋지만 그래도 램이 우선순위가 높지 않을까?

L2, L1 캐시(Cache)는 CPU에 붙어 나오는데, 구매할 때 L2캐시가 많은 것을 구매할 수 있는 것이 아니다. 1.3GHz의 PC가 1.5GHz의 PC보다 빠를 수도 있는데, RAM이나 CPU의 캐시의 차이에 따라 달라질 수 있다.

edwith CS50강의를 보며 정리했습니다.


● 하드웨어

우리가 pc를 구매할 때, 마케팅팀의 눈속임에 따라서 플로우가 흘러가게 된다. nn만원만 더 투자하면 훨씬 성능이 좋은 걸 사게 되도록 가격이 구성되어 있기 때문이다. 이런 눈속임에 홀리지 않고 정말 나에게 필요한 것이 무엇인지 알고 가격대가 합리적인지 캐치하기 위해서는 하드웨어에 대해 알아두면 좋다. 하드웨어는 컴퓨터를 물리적으로 구성하는 요소이며, 다양한 하드웨어의 기능과 차이점에 대하여 잘 알수록 유용하게 활용할 수 있다.

Processor/CPU : Intel Core i7-8700 Processor(3.2GHz, 8GT/s FSB)

위와 같은 하드웨어가 있을 때 인텔 사의 cpu를 쓰고 속도는 3.2GHz 라는 것을 알 수 있다. FSB는 Front Side Bus의 줄임말로, 기억장치의 속도나 메인보드의 부품들의 속도를 볼 때 참고할 수 있다. 이는 중앙처리장치라고도 하는데, 입력 장치에서 받은  명령을 실제로 처리한다. CPU가 1초에 얼마나 많은 연산을 할 수 있는지 속도를 측정하는 단위는 기가헤르츠(GHz)다.

 

Operating system : Genuine Windows 10 Professional 64

위와 같은 운영체제가 적혀있는데 보통 크게 윈도우, MacOS, 리눅스 등의 운영체제가 있고 우리가 어느 나라에 가든 그 나라의 법을 따라야 하는데 운영체제에 따라 사용법이 다르다. 그래서 어떤 방식으로 컴퓨터를 운영할지에 대한 체계라고 생각하면 된다.  운영체제에는 32bit와 64bit가 있는데 같은 버전에서도 다른 bit가 있다. 요즘은 거의다 64bit이고 훨씬 빠르다. 하지만 32bit에서 만든 프로그램이 32bit 전용 프로그램이라면 64bit에서 실행하지 못할 수도 있다.

 

Display : 27.1 "WXGA LED Panel with Wide viewing Angle

디스플레이의 유형인 WXGA 등의 약어는 종류를 쉽게 구분하기 위해 사용하는 것이다.

 

Total memory : Crucial RAM 16GB DDR4 3200MHz CL22 

Hard Drive : Seagate BarraCuda 1TB Internal Hard Drive HDD – 2.5 Inch SATA 6 Gb/s 5400 RPM 128MB Cache

16기가의 램 메모리와 1테라바이트의 하드디스크를 가지고 있는 것임을 알 수 있다.

기억장치는 입력된 명령이나 데이터가 저장되는 공간으로, 주기억장치보조기억장치로 나누어진다. 주기억장치에는 RAM이 있다. RAM은 기억된 정보를 읽어내기도 하고 다른 정보를 기억시킬 수 있는 메모리로서, 응용 프로그램을 일시적으로 불러오거나 데이터를 일시적으로 저장하는데 사용되는 임의 접근 기억 장치이다. RAM이 메모리에 얼마나 많은 양의 정보를 저장할 수 있는지 측정하는 단위는 보통 기가바이트(GB)가 사용된다.

하드드라이브(C:)는 영구적으로 데이터를 저장한다. 이런 하드드라이브를 보조기억장치라고 하는데, 많이 쓰이는 하드디스크(Hard Disk Drive, HDD)는 원판 모양의 플래터를 회전시켜 드라이브에 데이터를 읽고 쓰는 원리다. 하드드라이브는 다양한 용량이 존재하는데 보통 기가바이트(GB)나 테라바이트(TB) 단위가 쓰인다.

 

입력장치출력장치를 통틀어 입출력장치라고 부른다. 컴퓨터의 입력장치로는 마우스, 키보드, 스캐너 등이 있다. 입력장치사용자가 입력한 자료를 컴퓨터가 이해할 수 있는 형태로 변환하는 장치다. 컴퓨터의 출력장치로 대표적인 것은  모니터와 프린트를 들 수 있다. 흔히 모니터는 이야기 할 때  크기와 해상도와 크기를 언급하는데 모니터의 크기는 보통 대각선 끝과 끝의 길이를 인치로 표시한다.

 

해상도는 우리가 이미지를 볼 때 얼마나 선명하게 볼 수 있는가를 숫자로 나타낸다. 화면에 이미지를 확대해 보면 하나의 작은 점으로 나타나는데 이 하나의 작은 점을 픽셀이라고 한다. 즉 픽셀의 개수가 해상도가 된다. 픽셀의 개수가 많으면 많을수록 해상도는 높아지고 우리는 선명한 이미지를 볼 수 있다.

1/31 선자령

 

1. 스터디에 치여 사는 삶

가장 롱텀으로 이어지고 있는 스터디는 독일어speaking, 독서, free talking으로 셋 다 1년반이 넘었다. 작년 당근에서 새로 구한 독일어 문법/writing 스터디도 주2-3회 만남을 하며 잘 이어가고 있다.
개발 관련 스터디는 선형대수, 스탠포드 ML, 파이썬, CV논문, 혼공학습단 외에 ML project, friedberg.
추가적으로 다른 스터디는 부동산, 주식, NFT, 공인중개사, finance, english 정도.

15-6개 정도 되는 것 같은데 스터디가 날 잡아먹는 것 같아서 좀 줄일까 생각중이지만.. 이중에 내가 부추긴(?) 스터디도 있어서.. 책임감을 가지고 열심히 따라가보려 한다.

 

2. 건강은 실력이다.

수영,헬스,런닝으로 키워 온 지구력 덕분인지 생각보다 덜 지치고 있다. ADHD 마냥 집중도가 많이 떨어졌는데 task들이 많이 생기다보니까 시간 낭비하는 일이 없고 집중도도 높아졌다. 한달전 까지만 해도 휴대폰 보는 시간이나 게임하는 시간을 줄이는 것이 목표였는데, 자동적으로 해결되었다. task 뿐만 아니라 운동을 하며 키워온 집중도도 한 몫 했다 생각한다. 지난주 운동을 5일정도 쉬었더니 몸이 근질근질했다. 운동을 하고 나서의 성취감과 맑은 정신은 무엇을 해도 얻을 수 없어서 계속 하게 된다. 

또 먹는 것도 자극적이지 않은 음식 위주로 먹고 자기 전, 기상 후 따뜻한 물을 마시니 훨씬 혈액순환이 잘 된다. 커피도 많이 줄였음에도 불구하고 집중도가 떨어지지 않았다. 시간이 없다고 대충 먹고 운동도 안하던 내가 시간 내서 운동하고 시간 내서 먹으니 건강과 함께 삶의 질도 같이 업그레이드 됐다.

 

3. 기록의 중요성

기록을 하지 않던 사람이지만 기록의 중요성과 효용성을 깨닫고 기록광공이 되었다. 생각이 정리되지 않는 것도 기록을 하니 더 잘 정리가 되고, 이점이 참 많은 습관이다. 면접 볼 때 내 깃과 블로그를 내 눈앞에서 확인하는 모습을 보고 더욱 기록의 필요성을 느꼈다.

 

4. 요즘 좋아하는 것

철학: 삶은 왜 짐이 되었는가를 읽는 중인데 하이데거가 궁금해졌다.

언어학: NLP를 공부하며 다시 공부하지만 언제나 재미있다. 또 독일어를 공부하면서 언어학적으로 접근을 안 할 수가 없다. 이래서 독일이라는 나라가 온갖 학문이 다 발달한 건가 싶기도 하고

수학: 선대와 행렬,확통은 떼어놓을 수 없는 관계란 사실을 받아들이니 오히려 재미있어졌다.. 3b1b채널 최고. 어렵게만 느껴졌던 것이 생각보다 단순하면서도 기발하고 신기하고 재미있다. 수학자들 존경합니다.

머신/딥러닝: SOTA보단 논문 읽고 구조 파악하며 어떤 의문점을 던지며 연구를 시작하게 됐는지 보는 재미가 있다.

기록하기, 요가: 틈날때마다 생각날때마다 필요할때마다 하는 중

 

1/31 선자령

 

5. 반성할 것

일 벌리기만 좋아하고 데드라인 전까지 할 일을 미리 끝내지 못하는 경향이 있다. 머신러닝 딥러닝도 좋지만 data pipeline이나 architecture , server에 대한 흐름을 함께 공부해야 크게 볼 수 있다. 이 사실을 인지하면서도 우선순위를 자꾸만 미루다가 결국 건드리지도 못한 것을 반성한다. 수박겉핥기 식의 접근방법 때문에 뭘 해도 deep하게 하지 못하는 것 같은 느낌.
방향 >>>>> 속도 라는 것을 다시 명심하고 방향성에 집중하고 꾸준히 전진해보자.

'Think' 카테고리의 다른 글

2022년 8월 회고  (0) 2022.09.01
2022년 상반기 회고  (0) 2022.08.01
2022.1Q 회고  (0) 2022.04.07
2022년 3월 회고  (0) 2022.03.31
2022년 2월 회고  (0) 2022.02.28

지난번 업로드했던 내용이 정리가 잘 되었는지 우수 혼공족으로 선정되었다. 항상 도움 많이받는 한빛미디어! 혼공족장님도 열일해주셔서 너무 감사하고,,백신 후유증은 괜찮으신가요? 사랑하는 한빛미디어 뼈를 묻겠습니다..♡


 

04. 다양한 분류 알고리즘

 

04-1 로지스틱 회귀

  • 시작하기 전에
    럭키백을 판매한다고 할 때, 고객에게 힌트를 주기 위해 확률을 구하는 문제가 있다.

    <럭키백의 확률>


    k-최근접 이웃 분류기를 통해 생선의 확률을 계산할 것이다.

    이웃한 샘플 중 다수의 항목들로 분류될 확률이 크다.
    import pandas as pd
    fish = pd.read_csv('https://bit.ly/fish_csv_data')
    fish.head()
    판다스의 데이터프레임으로 데이터를 읽는다. head() 메서드는 처음 5개 행을 출력해준다.

    첫번째 열만 타겟으로 만들고, 나머지 열을 특성으로 만들면 된다. 
print(pd.unique(fish['Species']))

 unique() 메서드를 통해 고유한 값을 추출해볼 수 있다.

 

# 여러 열 선택하여 넘파이배열로 바꿔 저장하기
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
print(fish_input[:5])

# 타깃 데이터 넘파이배열로 저장하기
fish_target = fish['Species'].to_numpy()
print(fish_target[:5])

넘파이배열을 통해 데이터를 쉽게 정리할 수 있다.

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)

앞의 챕터에서 했던 것처럼 훈련 데이터와 테스트 데이터를 StandardScaler 클래스를 사용해 표준화 전처리해준다.

<k-최근접 이웃 분류기의 확률 예측>

k-최근접 이웃 분류기를 통해 훈련 세트와 테스트 세트의 점수를 확인할 수 있다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)
print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))

앞서 데이터 프레임을 만들 때 7개의 생선 종류가 들어있었기 때문에, 타깃 데이터에도 7개의 종류가 들어가 있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류(multi-class classification)라고 부른다.

이진 분류처럼 모델을 만들 수 있지만, 그렇게 한다면 True/False 값을 1/0으로 반환할 것이다. 다중 분류에서도 숫자로 바꿔 입력할 수 있지만 사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있다. 하지만, 그대로 사이킷런 모델에 전달하면 자동으로 '알파벳' 순으로 매겨지기 때문에 처음 입력했던 순서와 다를 수 있다.

# 정렬된 타깃값
print(kn.classes_)

# 처음5개 샘플 타깃값
print(kn.predict(test_scaled[:5]))

알파벳 순서와, 처음 입력했던 그대로의 순서로 다른 것을 확인할 수 있다. round() 메서드와 decimals 매개변수로 이 타깃값에 대한 확률을 추론해볼 수 있다. predict_proba() 메서드는 클래스의 확률을 출력한다.

import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인하기 위해 네번째 샘플을 선택하여 확인한다. predict() 메서드의 확률과 predict_proba() 메서드의 확률이 같아야 한다.

distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

이 샘플의 이웃은 'Roach'가 한 개로 1/3 , 즉 0.3333이고 'Perch'는 2개 이므로 0.6667이 된다. 앞서 출력한 네 번째 샘플의 클래스 확률과 같은 것으로 확인할 수 있다. 하지만 생각해보니, 어차피 확률은 네 개 중에 한 개이다. 
0/3, 1/3, 2/3, 3/3 이 중에 있지 않을까? 만약 이렇게만 표기를 한다면 확률이라고 말하기 조금 애매한 부분이 있다.

 

<로지스틱 회귀>  

# 기본미션
로지스틱 회귀는 이름은 회귀지만 '분류' 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.

여기서 각 특성의 앞에 붙는 숫자(스칼라)는 가중치(weight) 혹은 계수라고도 말한다. z는 어떤 값도 가능하지만 확률이 되려면 결과값은 0~1 또는 0~100% 사이의 값이 되어야 한다. 그러기 위해서 z가 아주 큰 음수이거나 아주 큰 양수일 때, 0 혹은 1로 바꿔주는 함수가 있는데 이를 활성화 함수(activation function)라고 한다. 주로 시그모이드 함수(sigmoid) 를 사용해서 이를 가능케 한다.

선형 방정식의 출력의 음수를 사용하여 자연 상수 e를 거듭제곱 하고 1을 더한 값의 역수를 취한다. 이런 복잡한 식을 거쳐 우측의 그림과 같은 그래프를 만들 수 있다. z가 무한하게 큰 음수일 경우 0에, 무한하게 큰 양수일 경우 1에 가까워 진다. z가 0일 때는 0.5가 된다. z가 어떤 값을 가지더라도 결과값은 0~1 사이에 있기 때문에 확률적으로 0~100% 의 확률을 보이는 것을 알 수 있다.

import matplotlib.pyplot as plt

z = np.arange(-5, 5, 0.1)
phi = 1 / (1 + np.exp(-z))
plt.plot(z, phi)
plt.xlabel('z')
plt.ylabel('phi')
plt.show()

이를 넘파이를 사용하여 그릴 수 있다. -5와 5 사이에서 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수(sigmoid)를 계산할 수 있다. 지수 함수는 np.exp() 함수를 사용한다. 이를 활용하여 로지스틱 회귀모델을 훈련할 수 있다.

이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성(positive/true), 0.5보다 작으면 음성(negative/false)으로 판단한다.

<로지스틱 회귀로 이진 분류 수행하기>  

넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다. 이를 불리언 인덱싱(boolean indexing)이라고 부르기도 한다.

char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])

위와 같은 방법은 지정한 배열 중 True인 값만 출력하는 것이다.

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(lr.predict(train_bream_smelt[:5]))
print(lr.predict_proba(train_bream_smelt[:5]))
print(lr.classes_)
print(lr.coef_, lr.intercept_)

훈련한 모델을 사용해 처음 5개 샘플을 예측하면 첫번째 출력값이 나온다. 이후 처음 5개 샘플의 예측값을 출력하면 샘플마다 2개의 확률이 출력한다. 첫번째 열이 음성(0)에 대한 확률이고 두번째 열이 양성(1)에 대한 확률이다. 그럼 두 개의 항목('bream', 'smelt' 중 어떤 것이 양성 클래스인지 확인해보면 알파벳 순이기 때문에 'smelt'가 양성임을 확인할 수 있다.

선형회귀와 마찬가지로 위의 계수들을 통해 특성에 대한 가중치(계수)가 위와 같이 설정되는 것을 확인할 수 있다. 로지스틱 회귀 모델로 z값을 계산해 볼수도 있다.

decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)

decision_funtion() 메서드를 사용하여 z값을 출력할 수 있다. 이 값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있는데 사이파이(scipy) 라이브러리의 expit() 메서드를 사용하여 얻을 수 있다.

from scipy.special import expit
print(expit(decisions))

출력된 값을 통해 decision_function() 메서드는 양성 클래스에 대한 z값을 반환하는 것을 알 수 있다. decision_funciont()의 출력이 0보다 크면 시그모이드 함수의 값이 0.5보다 크므로 양성 클래스로 예측한다.

 

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

로지스틱 회귀를 활용한 다중 분류도 이진 분류와 크게 다르지 않다. LogisticRegression 클래스는 max_iter 매개변수를 사용하여 반복 횟수를 지정하며 기본값은 100이다. 만약 반복 횟수가 부족하면 경고가 뜬다. 또한 릿지 회귀와 같이 계수의 제곱을 규제하는데, 이를 L2라고 부른다. L2 규제에서는 alpha를 사용한 규제를 했는데, LogisticRegression에서 규제를 제어하는 매개변수는 C이다. C는 alpha와 반대로 작을수록 규제가 커지고 기본값은 1이다.

lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))

훈련 세트와 테스트 세트에 대한 점수가 높으면서도 과대적합이나 과소적합으로 치우친 것 같지 않다.

proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))

테스트 세트의 5개 샘플에 대한 예측을 하면 위와같이 출력이 된다. 첫번째 샘플의 경우 세번째 열의 3번째가 84.1%로 가장 높다.

 

print(lr.classes_)
print(lr.coef_.shape, lr.intercept_.shape)

이를 확인해보면, 첫번째 샘플은 세번째 열의 확률이 가장 높기 때문에 'perch'를 가장 높은 확률로 예측했고 두번째 샘플은 여섯번째 열의 확률이 높기 때문에 'smelt'를 높게 예측한 것을 알 수 있다. 또한 선형 방정식을 확인하여 계수를 출력해보면 5개의 특성을 사용하기 때문에 coef는 5개의 열이 있지만 행이 7개인 것으로 보인다. 즉, z를 7개를 계산한다는 것이다. 다중 분류는 클래스마다 z값을 하나씩 계산하고, 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다. 

확률을 계산할 때는 이진 분류에서는 시그모이드 함수(sigmoid)를 사용했지만, 다중 분류에서는 소프트맥스(softmax) 함수를 사용한다. 소프트맥스(softmax) 함수란 여러 선형 방정식의 출력값을 0~1 사이로 압축하는 역할을 하는 함수이다. 또한 전체 합이 1이 되도록 만들기 위해 지수 함수를 사용하기 때문에 '정규화된 지수 함수'라고 부르기도 한다.

 

소프트맥스 함수는 각각의 지수함수를 전체 지수함수의 합으로 나눈 다음 그 s1~sn까지의 값을 더한 것이다. 모두 더하게 되면 분자와 분모가 같아지므로 결과값이 1이 되기 때문에 정상적으로 계산된 것을 알 수 있다. 시그모이드 함수(sigmoid)소프트맥스(softmax) 함수는 신경망에서도 나오는 개념이기 때문에 확실히 짚어두고 개념을 아는 것이 중요하다.

 

decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))

from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=1))

이진 분류에서처럼 먼저 z값을 구한 후 softmax 함수로 계산할 수 있다. softmax() 메서드의 axis매개변수는 소프트맥스를 계산할 축을 지정하는 매개변수다. axis=1로 지정한다면 각 행(샘플)에 대한 소프트맥스를 계산한다. 만약 axis 매개변수를 따로 지정하지 않으면 배열 전체에 대한 소프트맥스 계산이 진행된다. 출력 결과를 앞서 구한 proba 배열과 비교하면 정확히 일치하는 것을 확인할 수 있다.


04-2 확률적 경사하강법

  • 시작하기 전에
    공급이 갑자기 늘면서 항목들이 추가되고, 그에 대한 샘플이 없는 경우에 어떻게 모델을 바꿔야할까? 

<점진적인 학습>

데이터가 추가될 때마다 일정 간격을 두고 샘플을 추려서 새로운 모델을 만들면 되는 걸까? 하지만 시간이 지날수록 데이터는 늘어나리 때문에 서버도 기하급수적으로 늘려야할 수도 있다.

또 다른 방법은 새로운 데이터를 추가할 때 이전 데이터를 버림으로써 훈련 데이터 크기를 일정하게 유지하는 것ㅇ이다. 하지만 데이터를 버릴 때 중요한 데이터가 포함되어있으면 안 된다. 

이렇게 데이터를 버리지 않고 새로운 데이터에 대해서만 훈련할 방법은 없을까? 이러한 훈련 방법을 점진적 학습이라고 부른다. 대표적인 점진적 학습 알고리즘은 확률적 경사 하강법(Stochastic Gradient Descent)이 있다.

 

<확률적 경사 하강법>  

확률적 경사 하강법(Stochastic Gradient Descent)은 최적화 방법 중 하나이다. '확률적' 이라는 말은 random하다는 것과 같다. '경사'는 말 그대로 경사, 기울기, 즉 가중치(weight)를 이야기한다. '하강법'은 '내려가는 방법'이기 때문에 합쳐서 경사를 따라 내려가는 방법을 의미한다. 

만약 우리가 산 꼭대기에서 하산을 하려고 하는데, 지도를 분실하거나 휴대폰의 배터리가 다 했다고 가정한다면 우리는 가장 빠르게 하산하기 위해 어떤 방법을 사용할까? 바로 경사가 가장 가파른 길을 선택할 것이다. 경사 하강법의 목적은 가장 빠르게 원하는 최소 지점에 도달하는 것이다.

 

한 걸음씩 내려올 때, 보폭이 너무 크면 최소 지점을 찾지 못할 수 있다. 또, 보폭이 너무 작다면 시간이 오래 걸릴 수 있다. 그렇기 때문에 가파른 길로 내려오되 적당한 보폭으로 내려와야 한다. 가장 경사가 가파른 길을 찾을 때는 전체 샘플이 아닌 '랜덤한' 샘플을 골라 내려올 것이다. 이처럼 훈련 세트에서 랜덤하게 샘플을 고르는 것이 확률적 경사 하강법이다.

만약 랜덤하게 샘플을 골라서 쓰다가 모든 샘플을 다 사용했는데, 내려오지 못했다면 다시 샘플을 채워넣고 랜덤하게 골라 내려오는 것이다. 이렇게 훈련 세트를 한 번씩 모두 사용하는 과정을 에포크(epoch)라고 부른다. 일반적으로 수십~수백번 이상의 에포크를 수행하는 편이다.

무작위로 내려오다가 실족하는 것이 걱정된다면 어떻게 해야할까? 그렇다면 랜덤 선택 시 1개가 아닌 여러 개의 샘플을 선택해서 내려갈 수 있다. 이렇게 여러 개의 샘플을 사용하는 것을 미니배치 경사 하강법(minibatch gradient descent)이라 한다.

한 번의 이동을 위해 전체 샘플을 사용할 수도 있는데 이를 배치 경사 하강법(batch gradient descent)라고 한다. 이는 가장 안정적이고 리스크가 적으르 수 있지만 그만큼 계산이 오래걸리고 컴퓨터 메모리를 많이 사용할 수 있다. 또, 데이터가 너무 많은 경우에는 모두 읽을 수 없을 수도 있다.

 

 

즉, 데이터의 성질이나 샘플의 갯수에 따라 경사 하강법의 종류가 나눠지며, 그 특성에 맞는 경사 하강법을 사용하면 된다.

 

<손실 함수>  

손실 함수(loss function)는 어떤 문제에서 알고리즘이 얼마나 별로인지, 즉 '나쁜 정도'를 측정하는 함수다. 그래서 값이 작을수록 좋은 것이고, 손실 값이 낮은 쪽으로 경사하강법을 진행하는 것이 좋다. 확률적 경사하강법이 최적화할 대상이라고 이해를 해도 좋다. 

분류모델에서는 정확도(accuracy)를 많이 봤었는데, 이진 분류에서는 예측과 정답이 맞는지 여부에 따라 양성과 음성으로 나뉜다. 위의 경우는 정확도가 0.5, 50%임을 알 수 있다. 이를 손실함수로 표현할 수는 없을까?

 

만약 위의 경우라면 정확도가 0, 0.25, 0.5, 0.75, 1의 다섯가지만 가능하다. 정확도가 이렇게 듬성듬성하다면 경사하강법을 이용해 조금씩 움직일 수 없다. 경사는 연속적이어야하기 때문이다. 조금 더 기술적으로 이야기한다면, 선은 점의 연속으로 되어있고, 점이 조금 더 촘촘해서 선을 이루고 즉 미분이 가능해야 한다. 결론적으로, 손실함수는 미분이 가능해야 한다.

그렇다면 연속적인 손실 함수를 만드는 법은 무엇일까? 

 

<로지스틱 손실 함수>

로지스틱 손실 함수의 경우 예측값과 타깃값을 곱해서 사용할 수 있는데, 타깃이 0(false)인 경우는 곱하면 무조건 0이 되기 때문에 양성 클래스로 바꾸어 계산할 수 있다. 예측값과 타깃값 모두 1에서 빼서 변경이 가능하다. 

예측 확률의 범위는 0~1 사이인데 로그 함수는 이 사이에서 음수가 되므로 최종 손실값은 양수가 된다. 로그함수는 0에 가까울수록 아주 큰 음수가 되기 때문에 손실을 아주 크게 만들어 모델에 큰 영향을 끼칠 수 있다.


즉, 타깃값이 1(true)일때는 음수로 바꾼다음 로그함수 적용을 하고, 타깃값이 0(false)일때는 1에서 뺀 다음 음수로 바꾸고 로그함수를 적용한다.

양성 클래스일때 확률이 1에서 멀어질수록 손실은 큰 양수가 된다.  음성 클래스일 때 확률이 0에서 멀어질수록 손실은 큰 양수가 된다. 이 손실 함수를 로지스틱 손실 함수(logistic loss function) 혹은 이진 크로스엔트로피 손실 함수(binary cross-entropy loss function)이라고 한다. 다중 분류에서 사용하는 손실함수는 크로스엔트로피 손실 함수(cross-entropy loss function)라고 한다.

* 회귀에서 사용하는 손실함수

회귀의 손실함수로는 평균 절댓값 오차를 사용할 수 있다. 타깃에서 예측을 뺀 절댓값을 모든 샘플에 평균한 값이다. 혹은 평균 제곱 오차(mean squared error)를 많이 사용한다. 이는 타깃에서 예측을 뺀 값을 제곱한 다음 모든 샘플에 평균한 값이다. 이 값은 작을수록 좋은 모델이다.

 

<SGD Classifier>

확률적 경사하강법을 활용한 학습을 진행하기 전에, 데이터의 전처리를 진행해야 한다. 특성의 스케일값을 맞춘다.

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)

 

스케일값을 맞춘 다음 사이킷런에서 제공하는 확률적 경사 하강법 분류용 클래스를 사용한다. 객체를 만들 때 2개의 매개변수를 지정한다. loss는 손실함수의 종류를 지정하는 것이고, 여기서는 log로 지정하여 로지스틱 손실함수를 지정했다. max_iter는 에포크 횟수를 지정하는 것이다.

from sklearn.linear_model import SGDClassifier

sc = SGDClassifier(loss='log', max_iter=10, random_state=42)
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

0.77정도의 정확도로 미루어 보아 훈련 에포크 횟수(10회)가 부족한 것으로 유추된다. ConvergenceWarning의 경우 모델이 충분히 수렴하지 않았다는 경고이다. 즉, max_iter의 값을 더 늘려주라는 친절한 안내메시지이다.

확률적 경사하강법은 점진적 학습이 가능하기 때문에 이어서 훈련이 가능하다. partial_fit() 메서드를 통해 1 에포크씩 이어서 훈련을 할 수 있다.

# 추가 학습
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

한번의 에포크동안 학습을 더 실행한 결과 정확도가 향상된 것을 알 수 있다. 하지만 이렇게 무작정 한 번씩 반복할 수는 없으니 얼마나 더 반복해야하는지 횟수를 알 수는 없을까?

 

<에포크와 과대/과소적합>

확률적 경사하강법을 사용한 모델도 에포크 횟수에 따라 과대/과소적합이 발생할 수 있다. 에포크가 너무 많으면 과대적합, 에포크가 너무 적으면 과소적합이 될 수 있다.

위의 그래프는 에포크의 진행에 따른 모델의 정확도이다. 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다. 예제에서 실습을 진행해보려고 한다.

# 선택미션

sc = SGDClassifier(loss='log', 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))

# 산점도 그리기
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

에포크마다 점수를 기록하기 위해 2개의 리스트를 준비한 후, 300번의 에포크를 반복한다. 그 다음 훈련에 대한 점수를 산점도로 나타내면 위와 같은 그래프로 나타난다. 위의 그래프를 통해, 100번째 epoch 이후로는 훈련 세트 점수테스트 세트 점수가 조금씩 벌어지는 것을 확인할 수 있다. 또한, 에포크 초기에는 과소적합으로 점수가 낮은 것을 알 수 있다. 결론적으로 100회가 적절한 epoch로 보인다.

sc = SGDClassifier(loss='log', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

SGDClassifier에서 반복횟수를 100회로 지정한 후 훈련을 진행해보았다. SGDClassifier는 일정 에포크동안 성능이 향상되지 않으면 자동으로 멈춘다. tol 매개변수에서 향상될 최솟값을 지정할 수 있다. None으로 지정한다면 자동으로 멈추지 않고 max_iter만큼 무조건 반복된다.

* 확률적 경사하강법을 사용한 회귀모델은 SGDRegressor이다.

SGDClassifier의 loss 매개변수의 기본값은 'hinge'이다. 힌지 손실(hinge loss)서포트 벡터 머신(support vector machine)이라 불리는 알고리즘을 위한 손실 함수이다. 이는 또 다른 머신러닝 알고리즘이다. 서포트 벡터 머신에서는 다른 포스팅에서 딥하게 설명하도록 하겠다. SGDClassifier는 여러 종류의 손실함수를 loss 매개변수에 지정하여 알고리즘에 사용할 수 있다.

# 힌지 손실을 사용한 훈련
sc = SGDClassifier(loss='hinge', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

log함수가 아닌 힌지함수를 사용하여 loss값을 지정해주었더니 점수가 달라졌다.

 

 


● 마무리

다양한 회귀, 분류 모델에 사용되는 알고리즘을 통해 배웠다. 점진적 학습을 할 수 있는 확률적 경사 하강법, 로지스틱 회귀 모델 등으로 요즘처럼 데이터가 큰 빅데이터를 다룬다면 아주 유용할 것이다. 손실함수가 어떤 것인지와 적당한 에포크 횟수를 캐치하는 방법도 학습했다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter04_Various_classification_algorithms.ipynb
0.05MB

Forecasting : Principles and Practice 온라인 교재를 보며 참고하였습니다.


Chapter 1. 

우리는 많은 경우에 예측을 한다. 비단 현대사회뿐만 아니라 수천년전부터 사람들은 예측을 하는 것에 관심이 많았다.  고대 바빌로니아의 예측가는 썩은 양의 간에서 구더기의 분포를 미래를 예언하기도 했다. 이처럼, 계획을 세우는 데 있어 예측은 큰 도움이 된다. 예측가능성은 다음과 같은 요인에 의존하게 된다.

  1. 영향을 주는 요인을 얼마나 잘 이해할 수 있는지
  2. 사용할 수 있는 데이터가 얼마나 많은지
  3. 예측이 우리가 예측하려는 것에 영향을 줄 수 있는지 여부

전기 수요 예측의 경우는 위의 조건이 모두 맞기 때문에 상당히 정확한 편이다. 하지만, 환율 예측의 경우에는 2번의 조건만 만족하기 때문에 예측이 정확하지 않은 경우도 있다. 예를 들어 환율 예측은 환율 자체에 직접적으로 영향을 주기도 한다. 즉, 예측 자체 때문에 예측이 맞는 상황이 되기도 하는 것이다. 이러한 상황을 '효율적인 시장 가설(efficient market hypothesis)' 라고 볼 수 있다. 

좋은 예측이란 과거 데이터에서 존재하는 진짜 패턴과 관계를 잡아낸다. 환경이 변하는 경우에 예측이 불가능할 것이라고 대부분 생각한다. 하지만 모든 환경은 변화고, 좋은 예측 모델이랑 변하는 '방식'을 잡아내는 것이다. 즉, 환경이 변하는 방식이 미래에도 계속될 것이라는 가정 하에 예측을 하는 것이다.

1.2 예측, 계획 그리고 목표

예측은 경영을 함에 있어 큰 도움을 줄 수 있는 업무이다. 상세하게 보면 크게 세 분류로 나눠 정의해 볼 수 있다.

  •  예측(Forecasting) : 주어진 이용가능한 모든 정보를 바탕으로 가능한 한 정확하게 미래를 예측하는 것
  •  목표(Goals) : 발생하길 기대하는 희망사항. 목표가 실현 가능한지에 대한 예측 없이 목표를 세우는 경우도 있다.
  •  계획(Planning) : 예측과 목표에 대한 대응. 예측과 목표를 일치시키는 데 필요한 행동을 결정하는 일을 포함한다.

예측은 용도 및 기간에 따라 세 분류로 나눠볼 수 있다.

  • 단기 예측 : 인사, 생산, 수송 계획, 수요 예측
  • 중기 예측 : 원자재 구입, 신규 채용, 장비나 기계 구입
  • 장기 예측 : 보통 전략적으로 계획을 세우는 데 사용한다. 시장 기회, 환경 요인, 내부 자원을 반드시 고려해야 한다.

 

1.3 어떤 것을 예측할 지 결정하기

예측 프로젝트의 초기 단계에서는 어떤 것을 예측할지 결정해야 한다. 예를 들어 생산 환경에서 물품에 대한 예측이 필요하다면,

  1. 모든 생산 라인에 대한 것인가? 혹은 생산 그룹에 대한 것인가?
  2. 모든 판매점에 대한 것인가? 지역별 판매점 그룹에 대한 것인가? 전체 판매량에 대한 것인가?
  3. 주별 데이터인가? 월별 데이터인가? 연간 데이터인가?

등을 고려해야 할 것이다. 예측 범위, 예측 빈도도 고려해야 한다. 또한 예측값을 만들기 전, 예측값을 사용할 사람과 대화하며 사용자의 필요에 대한 이해도 필요할 것이다. 앞의 과정이 이뤄지고 나서야 예측에 필요한 데이터를 찾아야 한다. 예측을 하는 사람이 가장 시간을 많이 쏟는 부분은 데이터를 찾고 모아서 분석하는 부분이다.

 

1.4 예측 데이터와 기법

어떤 데이터를 사용할 수 있는지에 따라 예측 기법이 달라진다. 만약 이용할 수 있는 데이터가 없거나, 이용할 수는 있지만 예측에 상관 없는 데이터라면, 정량적인 예측 기법(Quantitive Forecasting)을 사용해야 한다.

  •  과거 수치 정보를 사용할 수 있을 때
  •  과거 패턴의 몇 가지 양상이 미래에도 계속될 것이라고 가정하는 것이 합리적일 때

위의 두 가지 조건을 만족할 때 정량적인 예측을 사용할 수 있으며 여러 정량적 예측 기법이 있다. 대부분 정량적 예측 문제는 일정한 간격으로 모은 시계열 (LSTM) 데이터나 특정 시점에서 모은 횡단면(cross-sectional) 데이터를 사용한다.

시계열 예측

  • IBM 일별 주가
  • 월별 강우량
  • Amazon의 분기별 판매 결과
  • Google의 연간 수익

위와 같이 시간에 따라 순차적으로 관측된 데이터를 시계열로 다룰 수 있다. 시계열을 예측할 때, 목표는 관측값이 미래에 계속될 것인지 예측하는 것이다.

호주 맥주의 분기별 생산량이며, 파란색 부분은 다음 2년에 대한 예측값이다. 예측값들이 과거 데이터에서의 패턴을 얼마나 잘 캐치하고 다음 2년에 대해 잘 모사하는지 주목해서 봐야 한다.

어둡게 그늘로 표시한 영역은 80% 예측 구간(prediction interval)을 의미한다. 즉, 미래값이 80%의 확률로 어두운 그늘로 표기한 영역에 들어갈 것으로 예측하는 것이다. 밝은 그늘로 표시한 영역은 95% 예측 구간을 의미한다. 이러한 예측 구간은 예측의 불확실성을 나타낼 때 유용하다.

가장 단순한 시계열 예측 기법은 예측할 변수 정보만 이용하고, 변수의 행동에 영향을 미치는 다른 요인들은 고려하지 않는다. 시계열 예측용 모델에는 분해모델 (decomposition models), 지수평활 (exponential smoothing models) ,ARIMA 모델 등이 있다.

 

예측 변수와 시계열 예측

예측 변수는 시계열을 예측할 때 유용하다. 예를 들어 여름 동안 더운 지역의 시간당 전기 수요(ED, electricity demand)를 예측한다면 예측 변수를 고려해야할 것이다.

현재 기온, 경제 상황, 인구, 시간, 요일, 오차 중 어떤 것이 전기 수요의 변동을 일으키는지 설명할 때 도움이 되기 때문에 이 모델을 설명 모델(explanatory model)이라고 부른다. 

여기에서 설계한 모델의 t는 현재시간, t-1은 한 시간 전, t+1은 한 시간 후로 변수의 과거 값으로 미래 예측을 한다. 

위의 두 모델의 특징을 결합한다면 위와 같이 혼합된 모델링을 할 수 있다. 이러한 모델은 동적 회귀 (dynamic regression) 모델, 패널 데이터(panel data) 모델, 종단(longtudinanl) 모델, 수송 함수(transfer function) 모델, 선형 시스템(linear system) 모델 등 다양한 종류의 모델이 있다.

이러한 모델들은 변수의 과거 값만 다루지 않고 다른 변수에 대한 정보도 포함하기 때문에 유용하게 사용할 수 있다. 하지만 설명 모델이나 혼합된 모델 대신에 시계열 모델을 선택할 수도 있는 다양한 경우가 있다.

  • 변수의 행동에 영향을 주는 관계를 측정하기가 어려운 경우
  • 관심 있는 변수를 예측하려면 다양한 예측 변수의 미래값을 알 필요가 있거나 예측할 필요가 있는 경우
  • 주된 관심이 '왜' 일어나는지가 아니라 '무엇'이 일어나는지에 있는 경우

이처럼 데이터나 모델의 정확도, 사용될 방식에 따라 예측에 사용할 모델이 달라진다.

 

1.6 예측 작업의 기본 단계

  1. 문제 정의
  2. 정보 수집
  3. 예비 분석
  4. 모델 선택 및 
  5. 예측 모델 사용 및 평가

 

1.7 통계적 예측 관점

예측값은 확률 변수(random variable)가 비교적 높은 확률로 취할 수 있는 값들의 범위를 제시하는 예측 구간(prediction interval)을 수반한다. 

우리가 아는 A 라는 값이 주어진 상황에서의 무작위 변수가 가질 수 있는 값은 확률 분포(probability distribution) 라고 하며, 예측을 함께 제시했을 때 예측분포(forecast distribution)라 한다. 

'예측'을 말할 때 보통은 예측 분포의 평균을 가리킨다.

[업데이트 : 2022-01-30]

논문 : Dynamical Isometry and a Mean Field Theory of CNNs: How to Train 10,000-Layer Vanilla Convolutional Neural Networks

 

 

2018년에 발표된

Dynamical Isometry and a Mean Field Theory of CNNs:How to Train 10,000-Layer Vanilla Convolutional Neural Networks

논문의 Summary 겸 리뷰를 적어보려고 합니다.

글을 쓰기에 앞서, 공부를 위해 논문을 보며 요약, 작성한 내용이라 간혹 오역이나 잘못된 내용이 있을 수 있습니다.

핵심 키워드는 highlight를 해두었습니다. 틀린 부분은 댓글로 알려주시면 감사하겠습니다^^ 

편의상 경어체를 빼고 작성합니다.


 

 

Abstract

 

최근의 비전 연구들을 보면, cnn의 계층이 더욱 깊어지는 것을 볼 수 있으며 성공적인 모델들은 수백~수천개의 계층을 가지고 있음을 알 수 있다. 병리학에서 exploding/vanishing gradient 와 같은 문제는 더욱 그렇게 만든다. 잔차처리나 배치 정규화(batch normalization)가 이러한 깊이로 훈련을 하는 동안, 과연 그러한 아키텍쳐들이 deep CNN을 학습하는 데 꼭 필요할까?에 대한 불확실성이 제기되었다. 이 과정에서 우리는 정확한 초기화 과정을 통해 일반적인 CNN이 10개 이하의 간결한 레이어를 사용하는 것이 가능하다는 것을 깨달았다.

우리는 신호 전파(propagation)에 대한 평균 필드 이론(mean field theory)을 개발하고 입력-출력 야코비안(Jacobian) 행렬의 특이값의 평형인 동적 등각도(dynamic isometry) 에 대한 조건을 특성화하여 이 초기화 방식을 이론적으로 유도한다.

Isometry 란 육면체를 투영한 때, 3축의 선분이 각각 120℃로 이루어져 윤곽이 정육각형이 되는 투영을 등각투영이라 한다.  이 경우, 좌표축상의 길이는 실제 길이보다 짧게 되지만, 이것을 실제 길이로 묘사한 것을 등각도라 한다. 하나의 투영도로 입체를 나타낼 수 있는 것으로 설명도(說明圖) 등에 이용된다.
 
이러한 조건은 컨볼루션 연산자가 정상 보존이라는 의미에서 직교 변환(orthogonal Transformation)이어야 한다는 것을 필요로 한다. 우리는 이러한 무작위 초기 orthogonal 컨볼루션 커널을 생성하기 위한 알고리즘을 제시하고 이것이 극도로 깊은 아키텍처의 효율적인 훈련을 가능하게 한다는 것을 경험적으로 보여준다.
 

Introduction

Deep CNN은 딥러닝의 성공에 매우 중요했으며 이를 기반으로 한 아키텍처는 컴퓨터 비전, 음성인식, NLP, 그리고 바둑(알파고)까지 뛰어난 정확도를 달성했다. Deep CNN의 성능은 네트워크가 더 깊어짐에 따라 향상되었으며, 일부 모델은 수백~수천 개의 레이어를 사용했다. 그러나 너무 깊은 아키텍처는 잔여(residual) 연결이나 배치 정규화같은 기술과 사용해야만 훈련할 수 있다.

최근 연구에서는 평균 필드 이론(mean field theory)를 사용하여 랜덤 매개변수가 있는 신경망에 대한 이론적 이해를 구축했다. 이러한 연구는 초기화 시 신호가 전파될 수 있는 최대 깊이를 밝혀냈고, 신호가 모든 경로를 통과할 때 네트워크가 정확하게 학습될 수 있음을 확인했다. 

Fully-connected layer의 초기화 하이퍼파라미터 공간에서 순서대로 정렬됨을 추가적으로 예측할 수 있다. 평균 필드 이론은 "평균"을 포착하지만 경사 하강의 안정성에 중요한 변동의 규모를 정량화하지 않는다.

입출력에서 랜덤 행렬을 사용하여 활성화 함수와 특이값의 분포를 정량화했다. 초기 무작위 가중치 행렬이 그려지는데, 야코비안(Jacobian) 행렬이 잘 조절될 때 네트워크가 가장 효율적으로 훈련될 수 있다. 이미지가 작은 경우에도 채널수가 많다는 한계에서 잘 정의된 평균 필드 이론이 심층신경망에 존대한다는 걸 알 수 있다. 심층 신경망은 Fully-connected layer와 정확히 동일한 순서를 가지며 정렬 단계에서 gradient-vanishing 문제, 그리고 chaos 단계에서 gradient exploding 문제가 발생한다는 것을 알 수 있다. 또한 Fully-connected layer와 마찬가지로 임계값에서 초기화되는 매우 깊은 CNN이 두 단계를 분리하는 것은 상대적으로 쉽게 훈련될 수 있다.

또한 랜덤 행렬(random matrix) 에 대한 분석이 convolution으로 이어지는데, block-circulant 구조가 야코비안 행렬이 신경망 속에서 fine tuning을 할 때 간편하게 할 수 있다. 이 초기화를 사용함으로서 일반적인 컨볼루션 모델보다 훨씬 빠르게 학습할 수 있다.

Fully-connected layer와 심층 신경망의 order-to-chaos는 동일해보이지만, 평균 필드 이론(mean field theory)는 사실 상당히 다르다는 점을 강조한다. 깊은 깊이의 한계에서 신호는 최소한의 공간 구조로 모드를 전파할 수 있다. Delta-orthogonal 초기화에서 orthogonal 커널은 균일하지 않은 분포에서 추출되며 성능 저하 없이 10,000개 이상의 레이어로 구성된 바닐라 CNN을 학습할 수 있다.

 

Theoretical results

 

랜덤 컨볼루션 신경망에서 신호 전파에 대한 평균 필드 이론을 도출할 것이다. 일반적인 방법론을 따르면 야코비안(Jacobian) 행렬의 특이값 분포에 대한 이론에 도달하게 된다. 이 커널은 합성곱 신경망의 motivation을 유도할 수 있도록 해주고, 이러한 커널이 바닐라 CNN에 대한 기존 초기화 방식보다 성능이 좋다는 것을 알 수 있다.

2.1 A mean field theory for CNNs

2.1.1 RECURSION RELATION FOR COVARIANCE

평균 필드 이론은 CNN의 최대 훈련 가능한 깊이를 예측하기 때문에 분산 값이 위와 같은 경우 히트맵은 가중치 분산에 따라 특정 값에 수렴을 하며 깊이의 배수에 따라 달라짐을 알 수 있다.

 

다양한 깊이의 CNN 중 orthogonal 커널을 사용한 곡선들은 결과적으로 accuracy가 100%에 도달하지만 그렇지 않고 일반적인 초기화를 사용한 곡선들은 깊이가 증가함에 따라 성능이 저하됨을 알 수 있다. 따라서 Delta-orthogonal 초기화는 깊이가 증가함에 따라 테스트 성능이 감소하는 문제를 해결한다.

 

Discussion

CNN에서 신호가 전파될 때 평균 필드 이론에 기반하여 전파되는 연구를 진행했고, 신호가 네트워크를 통해 앞뒤로 흐르기 위한 필요 조건을 연구함으로써 바닐라 CNN의 훈련을 쉽게 훈련하는 초기화 방식을 발견했다. 

연구에서 제시한 방법은 동적 등각도(dynamic isometry)이며 즉, 야코비안(Jacobian) 행렬의 네트워크 입출력을 활성화할 때, 랜덤 orthogonal 커널을 생성하여 초기화할 수 있도록 진행했다. Fully-connected layer와 반대로 CNN의 신호는 다차원이다. 이번 연구에서는 이 신호를 푸리에 모드로 분해하는 방법과 신호 전반에 걸쳐 전파를 촉진하는 것을 연구했고, 10,000개 이상의 레이어로 기본 CNN을 훈련하는 것이 가능하다는 것을 발견했다.

위의 행위를 통해 무작위로 vanilla CNN을 훈련할 때 발생하는 모든 장애물을 제거했으며, 딥러닝 커뮤니티에서 나오는 깊이만으로 성능을 향상시킬 수 있는지에 대한 질문를 다룰 수 있기 위한 토대를 마련했다. 초기 결과는 수십~수백 개의 레이어오 특정 깊이를 지나면 vanilla CNN의 아키텍쳐의 성능이 포화되는 것으로 나타났다.

이를 통해 residual 연결이나 배치 정규화같은 기능이 단순히 '학습을 효율적으로 하는 것'보다 '좋은 모델 클래스를 정의하는 것'에 중요한 역할을 할 가능성이 있다는 점을 시사할 수 있다.

03. 회귀 알고리즘과 모델 규제

 

03-1 k-최근접 이웃 회귀

  • 시작하기 전에

농어의 무게를 예측하는 모델을 만들어보려고 한다.

<k- 최근접 이웃 회귀>

지도 학습 알고리즘은 분류와 회귀(regression)로 나뉜다. 분류는 샘플을 클래스 중 하나로 분류하는 문제이고, 회귀는 분류가 아니라 임의의 어떤 숫자를 예측하는 문제이다. 즉, 정해진 클래스, 타깃 값(정답)이 없는 것을 예측하여 임의의 수치를 출력하는 것이다. 회귀는 19세기 통계학자인 프랜시스 골턴이 처음 사용한 단어로, 두 변수 사이의 상관관계를 분석하는 방법회귀라고 불렀다.

k-최근접 이웃 분류 알고리즘은 샘플에 가장 가까운 샘플 k개를 선택한 다음 그들의 클래스를 확인하여 예측하는 것이다.

X를 추측할때 네모가 더 많기 때문에 동그라미보단 네모로 예측하는 것이다.

X의 타깃값을 예측할 때는 이웃 샘플의 타깃값의 평균으로 구할 수 있다. 고로, 80으로 예측하는 것이다.

 

<데이터 준비>

훈련데이터를 깃허브에서 복사한 다음, 데이터의 산점도를 그려볼 수 있다.

import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

하나의 특성을 사용하기 때문에 특성 데이터를 x축에 두고, 타깃 데이터를 y축에 놓고 산점도를 그린 결과다. 결과에 따르면, 농어의 길이가 길어짐에 따라 무게도 늘어나는 것을 알 수 있다.

이제 이 데이터를 훈련 세트와 테스트 세트로 나눌 것이다.

여기서 사용하는 random_state의 시드 값을 설정해줄 때 보통 0이나 1이 아닌 42를 관용적으로 설정해주길래 이유를 찾아봤더니 은하수를 여행하는 히치하이커를 위한 안내서 책에서 "삶과 우주, 그리고 모든 것에 대한 답은 42입니다." 라는 문장이 있어서 그 이후로 밈처럼 사용되었다고 한다...

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

print(train_input.shape, test_input.shape)

여기서 perch_length의 특성이 1차원 배열이고 train_input, test_input도 1차원 배열인데 이를 2차원 배열로 바꾼다면 아래와 같이 진행된다.  reshape() 메서드는 크기를 바꿀 수 있는 메서드로, 넘파이에서 제공하는 메서드다. 즉, 바꾸려는 배열의 크기를 지정할 수 있다. 첫 번째 크기에 나머지 원소 개수로 모두 채우려면 -1, 두 번째 크기를  1로 하려면 train_input.shape(-1, 1)처럼 하면 된다.

 

<결정계수(R²)>

사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 kNeighborsRegressor로 kNeighborsClassifier와 매우 유사하다. fit() 메서드를 통해 회귀 모델을 훈련할 수 있다. 

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.score(test_input, test_target))

여기서 나온 0.992~의 숫자는 결정계수(Coefficient of determination)라고 부른다. R²(알스퀘어)라고도 부르는데, 자세히 설명해 둔 블로그가 있어서 참고하였다. 계산식은 아래와 같다.

즉, 타깃의 평균 정도를 예측하는 수준이라면 R²는 0에 가까워지고, 예측이 타깃에 가까워지면 1에 가까운 값이 된다. 사이킷런의 score()메서드의 값은 높을수록 좋다. 분류 모델에서의 정확도나, 회귀 모델에서의 결정계수 모두 동일하다.

하지만 직관적으로 숫자만 보고 어느정도의 결정계수인지 가늠하기 어렵기 때문에 다른 값으로 치환해서 살펴볼 수도 있다.

from sklearn.metrics import mean_absolute_error

# 테스트 세트에 대한 예측
test_prediction = knr.predict(test_input)

# 테스트 세트에 대한 평균 절댓값 오차 계산
mae = mean_absolute_error(test_target, test_prediction)
print(mae)

mean_absolute_error() 메서드는 타깃과 예측이 절댓값 오차를 평균하여 반환한다. 즉, 이 결과값은 결과에서 예측이 평균 19g정도 타깃값과 다르다는 것이다.

 

<과대적합 vs 과소적합>

위에서는 훈련 세트로 훈련을 하고 테스트 세트로 평가했는데, 만약 훈련 세트로 평가를 한다면 어떤 결과가 나올까?

print(knr.score(train_input, train_target))

보통 테스트 세트를 활용한 모델의 점수가 더 높고 훈련 세트를 활용한 모델의 점수가 더 낮게 나오지만, 이 경우엔 반대로 된 것을 확인할 수 있다. 

훈련 세트에서 점수가 좋았는데 테스트 세트의 점수가 굉장히 나쁘다면 모델이 훈련세트에 과대적합(overfitting) 되었다고 한다. 즉, 훈련 세트에만 잘 맞는 모델이라 테스트 세트 혹은 실전에서 사용할 때 잘 동작하지 않을 수 있다. 이는 복잡도가 높다고 할 수도 있다.

반대로 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수 모두 낮은 경우는 과소적합(underfitting) 되었다고 말한다. 즉, 모델이 너무 단순하여 적절히 훈련되지 않은 경우이다. 이는 복잡도가 낮다고 할 수 있다. 이럴 때는 훈련 세트의 크기가 너무 작거나, 데이터가 중요한 특징을 담고 있지 않은 경우일 수 있다.

위의 경우도 과소적합이다. 이를 해결하기 위해서 모델을 조금 더 복잡하게, 즉 훈련 세트에 더 잘 맞게 만들면 테스트 세트의 점수가 낮아질 것이다. 이웃의 개수인 k를 줄인다면 훈련 세트의 패턴에 더욱 민감해지고, k의 개수를 늘린다면 데이터 전반적인 특징을 따를 것이다. 기본 k값인 5를 3으로 줄여서 실행해보려 한다.

# 이웃의 개수 3개로 설정
knr.n_neighbors = 3

# 모델 재훈련
knr.fit(train_input, train_target)


print(knr.score(train_input, train_target))  # 훈련 세트의 결정계수
print(knr.score(test_input, test_target))  # 테스트 세트의 결정계수

테스트 세트의 점수가 훈련 세틑의 점수보다 낮아졌고, 둘의 차이도 크지 않아 과소적합이 해결된 것을 알 수 있다.

이처럼 최적의 k값을 찾기 위한 과정이 필요하고, 스스로 정의하는 이러한 매개변수를 보통 '하이퍼파라미터'라고 부른다.

파라미터(Parameter)란 모델의 구성요소이자 데이터로부터 학습되는 것이다. 즉, 머신러닝 훈련 모델에 의해 요구되는 변수이다. 머신러닝 훈련 모델의 성능은 파라미터에 의해 결정된다.

그 중에서도 하이퍼파라미터(Hyperparameter)란 최적의 훈련 모델을 구현하기 위해 설정하는 변수이다. 이는 모델 학습 과정에 반영되며, 학습을 시작하기 전에 미리 수동으로 값을 결정하는 것이다. 하이퍼 파라미터는 절대적인 최적값이 존재하지 않기 때문에 사용자가 직접 모델에 대한 이해를 통해 값을 설정하고 결정해야 한다. 좋은 모델을 만들기 위해서는 하이퍼파라미터를 잘 튜닝(컨트롤)해야 한다. 그러기 위해서는 학습 알고리즘, 모델의 구조 등 총체적인 이해가 필요하다.

 

<확인문제>  # 기본미션 

# k-최근접 이웃 회귀 객체 만들기
knr = KNeighborsRegressor()

# 5 ~ 45까지 x 좌표 만들기
x = np.arange(5, 45).reshape(-1, 1)

# n = 1, 5, 10일 때 예측 결과 그래프로 그리기
for n in [1, 5, 10]:

# 모델 훈련하기
  knr.n_neighbors = n
  knr.fit(train_input, train_target)

# 지정한 범위 x에 대한 예측 구하기
  prediction = knr.predict(x)

# 훈련 세트와 예측 결과 그래프로 그리기
  plt.scatter(train_input, train_target)
  plt.plot(x, prediction)
  plt.title('n_neighbors = {}'.format(n))
  plt.xlabel('length')
  plt.ylabel('weight')
  plt.show()

 


03-2 선형회귀

  • 시작하기 전에

print(knr.predict([[50]]))

길이가 50cm인 농어의 무게를 예측했을 때, 예측 결과값은 1,033g이지만 실제로는 1.5kg에 달하는 농어였다. 이렇게 예측값과 실제값의 차이가 많이 나는 이유는 무엇일까?

<k-최근접 이웃의 한계>

훈련데이터를 깃허브에서 복사한다.

perch_length = np.array([8.4, 13.7, 15.0, 16.2, 17.4, 18.0, 18.7, 19.0, 19.6, 20.0, 21.0,
       21.0, 21.0, 21.3, 22.0, 22.0, 22.0, 22.0, 22.0, 22.5, 22.5, 22.7,
       23.0, 23.5, 24.0, 24.0, 24.6, 25.0, 25.6, 26.5, 27.3, 27.5, 27.5,
       27.5, 28.0, 28.7, 30.0, 32.8, 34.5, 35.0, 36.5, 36.0, 37.0, 37.0,
       39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 42.0, 43.0, 43.0, 43.5,
       44.0])
perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

훈련 세트와 테스트 세트로 데이터를 나눈다.

from sklearn.model_selection import train_test_split

# 훈련 세트와 테스트 세트로 나누기
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

# 훈련 세트와 테스트 세트를 2차원 배열로 바꾸기
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

 

최근접 이웃 개수를 3으로 하는 모델을 훈련한다.

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor(n_neighbors=3)

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.predict([[50]]))

이 모델은 무게를 1,033g 정도로 예측했다. 문제점을 찾기 위해 산점도를 표시하는데, kneighbors() 메서드를 사용할 것이다.

import matplotlib.pyplot as plt

# 50cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[50]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 50cm 농어 데이터
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

길이가 50cm이고 무게가 1,033g인 농어는 삼각형으로 표시되고 그 주변의 샘플은 다이아몬드로 표시된다. 산점도에 따르면, 길이가 길어질수록 무게가 증가하는 경향이 있다. 하지만 50cm의 농어에서 가장 가까운 샘플은 45cm의 샘플이기 때문에, k-최근접 이웃 알고리즘에 따르면 이들의 무게의 평균을 예측 무게값으로 출력할 수 밖에 없다. 

print(np.mean(train_target[indexes]))

새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수 있다. 예를 들어 길이가 50cm가 아니라 100cm, 200cm 여도 농어의 무게는 여전히 1,033g으로 예측될 것이다.

다시 한 번 그래프를 통해 확인해보려고 한다.

# 100cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[100]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 100cm 농어 데이터
plt.scatter(100, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

이런 식이면 농어가 아무리 커도 무게는 늘어나지 않을 것이다. k-최근접 이웃이 아닌 다른 알고리즘으로 이 문제를 해결할 수 있을까?

 

<선형 회귀>

선형 회귀(Linear Regression)는 대표적인 회귀 알고리즘 중 하나로, 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘이다. 즉, 그 특성을 가장 잘 나타낼 수 있는 직선을 학습하는 것이다.

사이킷런의 LinearRegression 클래스를 사용하여 선형회귀 알고리즘을 구현할 수 있다. 이 클래스에도 fit(), score(), predict() 메서드가 있다.

from sklearn.linear_model import LinearRegression
lr = LinearRegression()

# 선형 회귀 모델 훈련
lr.fit(train_input, train_target)

# 50cm 농어 예측
print(lr.predict([[50]]))

k-최근접 이웃 회귀와 달리 선형 회귀는 50cm 농어의 무게를 1,241g으로 예측했다. 

직선을 그리려면 기울기와 절편이 있어야 한다. 이 선형회귀 클래스가 이 데이터에 가장 잘 맞는 a와 b를 찾은 것이다. 클래스가 찾은 a와 b는 lr 객체의 coef_와 intercept_ 속성에 저장되어 있다.

 

#선택미션

Q. 모델 파라미터에 대해 설명하기

A. coef_intercept_를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터(model parameter)라 부른다. coef_는 기울기로, 계수(coefficient) 또는 가중치(weight)라고 부른다. 모델 기반 학습에서 모델이 찾은 정보는 모델 파라미터에 저장되고, 선형 회귀에서는 방정식의 계수가 여기에 해당된다.

많은 머신러닝 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같다. 이를 모델 기반 학습이라고 부른다. 앞서 사용한 k-최근접 이웃 알고리즘처럼 모델 파라미터가 없는 경우의 훈련은 사례 기반 학습이라고 부른다.

 

농어의 길이 15~50까지를 직선으로 그릴 때, 앞서 구하나 기울기와 절편을 사용하여 두 점을 잇고, 산점도로 그려볼 수 있다.

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~50 1차방정식 그래프 그리기
plt.plot([15, 50], [15 * lr.coef_ + lr.intercept_, 50 * lr.coef_ + lr.intercept_])

# 50cm 농어 데이터
plt.scatter(50, 1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

이 직선이 선형 회귀 알고리즘이 데이터셋에서 찾은 최적의 직선이다. 길이가 50cm인 농어의 예측값 역시 이 직선의 연장선 상에 있음을 알 수 있다. 훈련 세트와 테스트 세트에 대한 R² 점수도 확인해보겠다.

둘의 결정계수 점수의 차이가 나는 것을 발견할 수 있다. 그럼 이게 훈련 세트에 과적합된걸까? 어디가 문제이니 살펴보니 그래프 왼쪽 아래가 좀 이상해보이기도 한다.

<다항 회귀>

선형 회귀가 만든 직선은 왼쪽 아래로 쭉 뻗어있다. 선형 회귀에 따르면 길이가 짧다면 무게가 0g 이하로 내려갈 것이다. 그래서 최적의 직선을 찾기보다 최적의 곡선을 찾는 것이 더욱 바람직할 것이다.

이런 2차방정식 그래프르르 그리려면 길이를 제곱한 항이 훈련 세트에 추가되어야 한다. 이를 위해서는 넘파이를 사용하면 된다.

train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))

print(train_poly.shape, test_poly.shape)

column_stack() 함수는 1차원 배열을 2차원 배열에 열로 쌓는 함수이다. 새로 만든 데이터의 크기가 위와같이 나온다.

train_poly를 사용해 선형 회귀 모델을 다시 훈련한 후, 2차 방정식의 a,b,c값을 추론할 수 있다. 2차 방정식 그래프를 위해 훈련 세트에 제곱 항을 추가했지만 타깃 값은 그대로 사용한다.

lr = LinearRegression()
lr.fit(train_poly, train_target)

print(lr.predict([[50 ** 2, 50]]))

선형회귀에서 예측한 것보다 훨씬 높은 무게가 예측된 것을 확인할 수 있다. 여기서 다시 계수와 절편을 출력할 수 있다.

이 모델은 위와 같은 그래프를 학습한 것이다. 이런 방정식을 다항식(polynomial)이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀(polynomial regression)라 부른다.

여기서 발견한 절편 a,b,c를 통해 산점도를 그려볼 수 있다.

# 구간별 직선을 그리기 위해 15~49 정수배열 만들기
point = np.arange(15, 50)

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~49 2차 방정식 그래프 그리기
plt.plot(point, 1.01 * point ** 2 - 21.6 * point + 116.05)

# 50cm 농어 데이터
plt.scatter(50, 1574, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

무게가 음수로 나올 일도 없고 앞선 모델보다 훨씬 안정적으로 보인다. 결정계수를 확인해보겠다.

아까보단 차이가 많이 줄었지만 여전히 테스트 세트의 점수가 높은 것으로 미루어 보아 과소적합이 있는 것 같다.


03-3 특성 공학과 규제

  • 시작하기 전에

다항 회귀에서 해결하지 못한 과소적합으로 추측되는 문제는, 더 고차항을 넣어서 해결해야 하는 걸까? 아니면 길이와 무게 데이터가 아닌 높이와 두께 데이터를 사용해 볼 수 있을까?

<다중 회귀>

앞서 사용한 선형 회귀는 하나의 특성을 사용했지만, 사실 선형 회귀는 특성이 많을 수록 엄청난 효과를 낸다. 여러 개의 특성을 사용한 선형 회귀를 다중 회귀(multiple regression)라 부른다.

좌측의 그림처럼 1개의 특성을 사용하면 직선을 학습하고, 우측의 그림처럼 2개의 특성을 학습하면 평면을 학습한다. 즉, 타깃값과 함께 3차원 공간을 형성하고 곧 평면이 된다. 특성이 3개인 경우는 어떻게 될까?

우리는 3차원 이상의 공간은 그리거나 상상할 수는 없지만 특성이 적다고 무조건 성능이 안 좋은 것은 아니다. 또, 주어진 특성 외에도 특성들을 연산하여 새로운 특성으로 만들 수도 있다. 이렇게 기존의 특성을 사용하여 새로운 특성을 뽑아내는 작업을 특성 공학(feature engineering)이라 부른다.

<데이터 준비>

농어의 특성이 두께, 길이, 높이 3개로 늘어났고, 복사해서 붙여넣는 방식은 너무 피곤하다. 이럴 경우 판다스를 사용하여 데이터를 내려받아 데이터프레임에 저장할 수 있다. 그 다음 넘파이 배열로 변환하여 사용할 수 있고, 판다스에서는 보통 csv 형태의 파일을 많이 사용한다.

import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
print(perch_full)

 

이렇게 긴 데이터를 판다스를 통해 쉽게 불러올 수 있다. 타깃데이터는 기존처럼 깃허브에서 복사한다. 

perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

그 다음 훈련 세트와 테스트 세트로 나누고 특성 공학을 사용하여 새로운 특성을 만든다.

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_full, perch_weight, random_state=42)

 

<사이킷런의 변환기>

사이킷런에서 제공하는 전처리를 위한 클래스들을 변환기(transformer)라고 부르는데, 이 트랜스포머들은 모두 fit(), transform() 메서드를 제공한다.

이번에는 PolynomialFeatures 사용할 것이다. transform(변환)메서드는 fit(훈련)을 한 이후에 사용할 수 있다. 혹은 두 개를 붙인 fit_transform() 메서드를 사용해도 된다.

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

fit() 메서드는 새롭게 만들 특성 조합을 찾고 transform() 메서드는 실제로 데이터를 변환한다. 변환기는 타깃 데이터 없이 입력 데이터를 변환하기 때문에 fit() 메서드에 입력 데이터만 전달한다.

poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

사실 선형 방정식의 절편을 항상 값이 1인 특성과 곱해지는 계수라 할 수 있기 떄문에, 절편을 위한 항을 제거하고 특성의 제곱과 특성끼리 곱한 항만 사용할 것이다.

 

이제 다시 훈련 데이터를 활용하여 학습에 적용할 수 있다.

poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape)

 

get_feature_names() 메서드는 9개의 특성의 내용을 알려준다. 하지만, 에러가 떠서 찾아보니 사이킷런 블로그에 의하면 get_feature_names_out() 메서드를 대신 사용해달라고 한다.

poly.get_feature_names_out()

'x0'는 첫번째 특성, 'x0^2'는 첫번째 특성의 제곱, 'x0 x1'은 첫번째 특성과 두번째 특성의 곱을 나타낸다고 한다.

 

이제 테스트 세트를 변환하여 다중회귀 모델을 훈련할 것이다.

test_poly = poly.transform(test_input)

 

<다중 회귀 모델 훈련하기>

다중 회귀 모델을 훈련하는 것은 선형 회귀 모델을 훈련하는 것과 같지만 특성의 개수가 여러개일 뿐이다. 

from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))

특성이 증가함으로 인해서 테스트 세트와 훈련 세트 모두의 점수가 높게 나온 것을 확인할 수 있다. 특성을 더 많이 추가해보려고 한다. degree 매개변수를 사용하여 5제곱까지의 특성을 만들어 출력해보려 한다.

poly = PolynomialFeatures(degree=5, include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape)

특성이 55개나 만들어진 것이다. 이를 통해 다시 훈련을 해보겠다.

점수가 음수가 나와버렸다... 특성의 개수를 크게 늘리면 선형 모델은 거의 완벽하게 학습할 수 있을 정도로 강력해진다. 하지만 훈련 세트에 너무 과대적합되기 때문에 테스트 세트에서는 형편없는 점수가 나올 수 있다.

 

<규제>

규제(regularization)는 모델이 훈련 세트를 너무 과도하게 학습하지 못하도록(과대적합하지 않도록) 하는 것이다. 즉, 선형 회귀모델의 경우 특성에 곱해지는 계수(기울기)의 크기를 작게 만드는 것이다.

좌측은 훈련 세트를 과도하게 학습했고, 우측은 보다 보편적으로 학습한 것을 알 수 있다.

특성의 스케일이 정규화되지 않으면 곱해지는 계수 값도 차이가 난다. 그렇기 때문에 규제를 적ㅇ굥하기 전에 정규화를 먼저해야 한다. 사이킷런의 StandardScaler 클래스를 이용할 수 있다.

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

 

선형 회귀 모델에 규제를 추가한 모델을 릿지(ridge)라쏘(lasso)라고 부르는데, 규제 방법이 다르다. 릿지는 계수를 제곱한 값을 기준으로 규제하고, 라쏘는 계수의 절댓값을 기준으로 규제한다. 보통 릿지를 더 선호하는 편이고, 두 알고리즘 모두 계수의 크기를 줄인다. 하지만 라쏘는 아예 0으로 만들 수도 있다.

 

<릿지 회귀>

사이킷런에서 제공하는 모델이라 편하게 릿지 회귀 알고리즘을 사용할 수 있다.

from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

 

점수가 정상인 것을 알 수 있다. 릿지와 라쏘 모델을 사용할 때 alpha라는 매개변수를 통해 규제의 강도를 조절할 수 있다. 값이 커질수록 규제의 강도도 세진다. 적절한 alpha 값을 찾는 법은 alpha 값에 대한 결정계수 값의 그래프를 그려보는 것이다. 

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 릿지모델 만들기
    ridge = Ridge(alpha=alpha)
    # 릿지모델 훈련하기
    ridge.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(ridge.score(train_scaled, train_target))
    test_score.append(ridge.score(test_scaled, test_target))

 

이제 그래프를 그려볼 것이다. alpha 값을 10배씩 늘렸기 때문에 이대로 그래프를 그리면 왼쪽이 너무 촘촘해진다. 따라서, 동일한 간격으로 나타내기 위해 log함수로 바꿔서 표현할 것이다. np.log() 혹은 np.log10() 함수를 사용할 수 있다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

위가 훈련세트, 아래가 테스트세트의 그래프이다. 

 

좌측을 보면 두 데이터의 점수차가 아주 큰 것을 통해, 훈련 세트에만 잘 맞는 과대적합의 모습을 볼 수 있다. 반대로 우측을 보면 둘의 점수가 모두 낮아지는 과소적합의 모습을 볼 수 있다. 따라서 적절한 alpha 값은 둘의 거리가 가까우면서도 테스트 세트의 점수가 가장 높은 -1(0.1)임을 알 수 있다.

# 10의 -1제곱인 0.1을 alpha값으로 지정함
ridge = Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

결과값을 통해 균형잡힌 모델임을 알 수 있다.

 

<라쏘 회귀>

라쏘 회귀도 릿지와 구현 방법은 비슷하다.

from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

결과가 나쁘지 않지만, 릿지에서 했던 것처럼 최적의 alpha 값을 찾아보려 한다.

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 라쏘모델 만들기
    lasso = Lasso(alpha=alpha, max_iter=10000)
    # 라쏘모델 훈련하기
    lasso.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(lasso.score(train_scaled, train_target))
    test_score.append(lasso.score(test_scaled, test_target))

경고문구가 뜨지만, 정상작동되는 것이다. 반복 횟수가 부족할 때 경고가 발생할 수 있다.

다시 그래프를 그린다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

좌측은 과대적합, 우측은 과소적합되는 걸로 미루어보아 최적의 alpha 값은 1, 즉 10임을 알 수 있다. 이로 다시 훈련을 해볼 수 있다.

# 10의 1제곱인 10을 alpha값으로 지정함
lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

릿지와 마찬가지로 과대적합을 많이 줄이며 테스트 세트의 성능을 높인 것을 알 수 있다. 라쏘 모델은 계수 값을 0으로 만들 수 있기 때문에 얼마나 0이 되었는지 찾아볼 수 있다.

55개의 특성 중 40개가 0으로 변하고, 15개의 특성만 사용한 것을 알 수 있다. 라쏘 모델은 유용한 특성을 골라내는 데에 사용하기도 좋을 듯 하다.

 


● 마무리

다양한 선형 회귀 알고리즘을 통해 과대적합 문제와 과소적합 문제를 해결할 수 있다. 또한, 특성을 추가할수록 좋은 성능을 내는 선형 회귀 모델에 발맞춰 특성을 만들어보기도 하였다. 랏쏘와 릿지 규제를 사용하여 모델에 제약을 주기도 할 수 있다. 또한, 규제의 양을 조절하는 매개변수인 alpha 값도 직접 찾아볼 수 있다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

 

Chapter03_Regression_algorithms_and_model_regularization.ipynb
0.17MB

[업데이트 : 2022-01-26]

논문 : Understanding the difficulty of training deep feedforward neural networks

 

2010년에 발표된

Understanding the difficulty of training deep feedforward neural Networks

논문의 Summary 겸 리뷰를 적어보려고 합니다.

글을 쓰기에 앞서, 공부를 위해 논문을 보며 요약, 작성한 내용이라 간혹 오역이나 잘못된 내용이 있을 수 있습니다.

핵심 키워드는 highlight를 해두었습니다. 틀린 부분은 댓글로 알려주시면 감사하겠습니다^^ 

편의상 경어체를 빼고 작성하겠습니다.

 


 

Abstract

 

2006년 이전엔 DNN(Deep Neural Networks)이 훈련에 있어서 큰 성과를 내지 못했지만, 그 이후 성공적인 성과를 보였다. 왜 경사하강법(Gradient Descent)이 심층 신경망에서 제대로 수행되지 않는지에 대해서 찾아보려 한다.

 

Deep Neural Networks

딥러닝은 하위 레벨의 feature들로 형성된 상위 수준 계층의 기능을 사용하여 기능 계층을 학습하는 것을 목표로 한다. 여기에는 은닉층(hidden layer)이 있는 신경망과 그래픽 모델 등의 다양한 심층 아키텍처에 대한 학습 방법이 포함된다. 그들의 비전과 NLP에서의 경험적 성공 덕분에 최근 많은 연구들이 진행되고 있다. Ben-gio(2009)가 논의해 온 이론에 따르면, 고차원적인 추상화(비전, 언어 등)를 나타내는 복잡한 추상화를 하기 위해서는 깊은 아키텍처가 필요하다.

 

깊은 아키텍처에 대한 최근 연구의 결과들은 심층 지도 신경망 모델로 얻을 수 있지만, 초기화나 훈련방식은 고전적인 피드포워드(실행 결과가 목표치에서 벗어날 것을 예측하고 미리 필요한 조작을 하는 제어 기능) 방식과 다르다.

새로운 알고리즘은 무작위 초기화나 기울기 기반(gradient-based)의 최적화(optimization)보다 더 잘 작동하는데, 그 이유는 무엇일까? 정답은 바로 비지도학습에 있다. 그것은 로컬의 최소값에 해당하는 최적화 절차 과정에서, "더 나은" 매개변수를 초기화하는 정규화 역할을 한다.

이전 연구에서 순수한 지도학습도 더 나은 결과를 줄 것이란 걸 보여주었다. 그래서 비지도학습이나 준지도학습에서 깊은 아키텍쳐를 가져오는 것보단 기존의 오래된(하지만 깊은) 다층 신경망이 무엇이 잘못되었는지를 분석하는 데 초점을 맞추려고 한다.

 

 

 

정재곤님의 Do it! 안드로이드 앱 프로그래밍 책과 유튜브 영상을 참고하여 공부하며 정리하였습니다. 

 


01. Hello! Android

01.안드로이드란?

구글에서 만든 스마트폰용 운영체제(OS)로, 휴대용 단말기를 위한 운영체제뿐만 아니라 다양한 앱을 설치하여 실행할 수 있는 앱 플랫폼이기도 하다. 아무리 아이폰이 많이 팔린다고 하지만, 안드로이드가 압도적으로 판매량이 높다. 그 이유는 무엇일까?

* 안드로이드의 장점

  1. 오픈소스이다. 그렇기에 전 세계 개발자들의 피드백도 빠른 편이다.
  2. 자바(Java)언어를 사용한다. 전세계 수많은 개발자들이 사용하기 때문에 접근이 손쉽다.
    - 코틀린으로도 안드로이드 앱 프로그래밍을 할 수 있는데, 코틀린은 자바 가상머신(JVM)에서도 동작하기 때문에 훨씬 간단하고 단순하다는 특징을 가지고 있다.
  3. 미리 제공된 컴포넌트를 사용할 수 있어, 플랫폼까지 신경 쓰지 않고 앱을 만들어 배포만 잘 하면 된다. 따라서 기존 오픈 소스가 가지고 있는 취약점은 거의 없다.
  4. 다른 사람이 만든 앱을 쉽게 연동할 수 있다. 예를 들어 앱에 촬영 기능을 포함하려고 한다면 직접 화면을 만들지 않고 카메라 앱을 실행시켜 촬영할 수 있다.
  5. 안드로이드 플랫폼에서 다양한 기능을 지원한다. OS 업그레이드가 빠르고, 리눅스 기반이기 때문에 새로운 하드웨어가 나와도 쉽게 신기술을 접목할 수 있다.
  6. ART라는 런타임이 탑재되어 있다. 런타임은 프로그램을 실행시키는 엔진같은 것인데, 프로그램의 성능에 영향을 주는 가장 중요한 요소 중 하나이다. 예전엔 속도가 느려 성능 문제가 있었지만, 현재는 지속적으로 개선되어 성능에 문제가 거의 없다.


    * 안드로이드의 흐름 살펴보기

2005년, 구글이 안드로이드사를 인수한 후 Android SDK를 처음 출시하며 이후 지속적으로 업데이트해오고 있다. 안드로이드의 특징은 버전별로 디저트 이름을 사용해왔었고, 지금은 버전 숫자를 사용한다.

안드로이드 단말기가 시장 점유율을 높일 수 있던 이유는 에코 시스템을 잘 구성했기 때문이다. 제조사에게는 저비용으로 휴대전화를 만들 수 있다는 장점, 통신사에게는 통신망 사용 수익과 함께 앱 판매 수익에 대한 수수료, 앱 개발자에게는 광고비와 더불어 앱 다운로드에 따른 수익을 가져다 줄 수 있다.

 

02. 개발 도구 설치하기 (Window)

윈도우 외의 운영체제(Mac, Linux 등) 사용자분들은 튜토리얼을 참고 부탁드린다.

 

 

1) 웹사이트에 접속하여 최신 버전의 안드로이드 스튜디오를 설치한다.

2) 다운로드 한다.

 

3) 이제는 대충 느낌상 알겠는 동의 버튼을 누른다.

4) 파일을 실행한다.

 

5) 원하는 경로로 설정을 해준다. 기본 경로가 괜찮다면 그대로 Next, 만약 원하는 경로가 따로 있다면 지정해준다.

 

6) 아이콘을 만들고 싶지 않으면 Do not create shortcus를 체크하고, 만들고 싶으면 그대로 Install을 누른다.

 

7) 설치 후 실행한다.

 

만약 아래와 같은 화면이 뜬다면, 이전에 설치한 이력이 있는 것이다. 그래서 이전의 설정을 가져올 것인지 묻는 것이다.만약 처음 설치하는 것이라고 하면, Do not import settings 그대로 둔 상태로 OK를 누른다.

 

 

실행 시 이런 창이 뜨는데, 말 그대로 내가 사용하는 데이터를 구글에 전송할 건지 묻는 것이다. 전송한다면 Send usage-- 를, 굳이 전송하고 싶지 않다면 Don't send를 클릭한다.

 

 

Welcome Android Studio가 뜨면 설치하기 전에 세팅하는 화면이 뜬다.

 

 

 

다음은 UI 테마를 선택할 수 있다. Dark모드와 Light모드 중 본인이 편한 환경을 선택한다음 Next를 누르면 된다.

 

 

 

 

아래와 같은 화면이 뜬다면 설치가 완료된 것이다.

 

 

Welcome to Android 화면이 보이면 안드로이드 스튜디오 설치가 완료된 것이다.

 

 

하지만, 실제로 앱을 만드는 작업을 하기 위해서는 추가적으로 설치해야할 파일들이 있다.

More actions를 클릭한 다음 SDK Manager를 선택한다.

 

첫 화면에서는 사용 가능한 플랫폼 정보와 이미 설치되어 있는 플랫폼을 확인할 수 있다.

 

두번째 탭인 SDK Tools를 눌러, Android SDK Build-Tools, Android Emulator, Android SDK Platform-Tools 등의 항목이 최신버전으로 설치되어 있는지 확인한다.

추가적으로 'Googld Play Services' 항목도 설치되어있지 않다면 추가로 체크하여 설치한다.

 

설치가 안 된 파일이 있다면 설치가 진행될텐데, 기본 설정이 Decline이라 Accept로 변경해주고 Next를 누른다.

 

Done이라는 메시지가 뜨면 정말 설치가 완료된 것이다.

 

 

 

02. 데이터 다루기

 

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

  • 시작하기 전에


1강에서 사용한 k-최근접 이웃알고리즘은 알고리즘을 찾는다기 보다는 입력한 49개의 데이터에 대한 최근접 이웃을 찾는 것이기 때문에 성공율이 100%일수밖에 없다.

 

<지도학습과 비지도학습>



입력(데이터)과 타깃(정답)이 있는 것이 지도학습, 타깃데이터가 없고 입력만 있는 것이 비지도학습이라고 한다. 강화학습은 모델이 어떤 행동을 수행한 다음 주변의 환경에서 행동의 결과를 피드백을 받아 개선해나가며 수행하는 방식이다.
k-최근접 이웃 알고리즘도 지도학습의 한 종류이다.


<훈련 세트와 테스트 세트>

연습문제와 시험문제가 같다면, 정답률은 100%일수밖에 없다. 따라서 머신러닝 프로그램의 성능을 제대로 평가하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 한다.

평가에 사용하는 데이터를 테스트 세트(test set), 훈련에 사용되는 데이터를 훈련 세트(train set)라고 부른다. 만약 데이터가 부족하다면, 일부를 떼어 내어 훈련시키고 남은 것으로 테스트를 진행해도 된다.

생선의 리스트를 복사하여 사용한다.

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개 데이터 중 35개 데이터를 훈련데이터, 14개 데이터를 테스트데이터로 K최근접이웃알고리즘을 사용할 것이다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()

데이터 중에서 35개를 인덱스와 슬라이싱을 통해 접근한다.

# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_input = fish_data[:35]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_target = fish_target[:35]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
test_input = fish_data[35:]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_target = fish_target[35:]

 연산을 통해 0~34까지의 인덱스( [:35] )를 훈련세트로, 인덱스 35~48까지의 인덱스 ( [35:] )를 테스트 세트로 선택했다. 이제 fit()메서드를 통해 모델을 훈련하고, score()메서드를 통해 평가한다.

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

이게 어찌된 걸까? 정확도가 0%다.. 도대체 어떤 것이 문제인걸까?

 

<샘플링 편향>

훈련/테스트 데이터를 나누려면 두 개의 항목이 골고루 섞이도록 우측 그림처럼 해야한다. 방금 전의 사례처럼, 데이터(샘플링)가 한 쪽으로 치우친 현상을 샘플링 편향(sampling bias)이라고 부른다. 따라서 이를 해결하기 위해서는 데이터를 나누기 전에 데이터를 섞거나 골고루 뽑아야 한다. 이런 작업을 편하게 하기 위해 파이썬 라이브러리인 넘파이를 사용하려고 한다.

 

<넘파이>

넘파이(numpy)는 파이썬의 대표적인 배열 라이브러리다. 넘파이를 활용하여 데이터를 2차원 배열로 변형할 수 있다.

import numpy as np
input_arr = np.array(fish_data)
target_arr = np.array(fish_target)

넘파이의 array() 함수에 파이썬 리스트를 넣고 출력하면 2차원 배열 형태로 출력이 된다.

print(input_arr)

 

shape 속성을 사용하면 배열의 크기를 알 수 있다.

print(input_arr.shape)

49개의 샘플과 2개의 특성이 있음을 알 수 있다.

 

 

입력과 타깃은 다르기 때문에, 하나는 인덱스로, 하나는 랜덤하게 섞어서 사용하기로 한다.

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

arange함수에 정수N을 전달하면 0에서부터 N-1까지 1씩 증가하는 배열을 만든다음, random함수와 shuffle 함수로 무작위로 섞는다.

print(index)
print(input_arr[[1,3]])

 

그다음 인덱스를 통해 제대로 섞였는지 출력해볼 수 있다.

 

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

만든 배열의 처음 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() 메서드를 실행할 때마다 이전에 학습한 모든 것을 잃어버리기 때문에, 앞서 만든 것을 그대로 사용하려고 한다.

kn = kn.fit(train_input, train_target)

 

훈련세트를 훈련시켰으니, 이제 테스트하려고 한다.

kn.score(test_input, test_target)

100%의 정확도를 보이는 것을 알 수 있다. predict() 메서드를 통해 예측 결과와 실제 타깃 확인을 해 볼 수 있다.

kn.predict(test_input)
test_target

예측 결과와 테스트 세트의 정답이 일치하는 것을 확인할 수 있다. predict 메서드가 반환하는 것은 넘파이 배열이다. 사이킷런 모델에서 사용하는 입출력은 모두 넘파이 배열이다.

 

<선택미션>

 


02-2 데이터전처리

  • 시작하기 전에
    길이가 25cm, 무게 150g인 생선이 빙어라고 예측을 하는 사례가 발생했다.

<넘파이로 데이터 준비하기>

다시 데이터를 준비하여 프로그램을 만든다. 앞서 사용했던 생선의 리스트를 복사하여 사용한다.

column_stack 함수를 통해 리스트를 일렬로 세운 다음 나란히 연결한다. 아래처럼 [1,2,3] 리스트와 [4,5,6]을 하나씩 연결하면 하나씩 연결되어 2차원 리스트가 만들어진 것을 확인할 수 있다.

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

 

두 리스트를 연결한다.

fish_data = np.column_stack((fish_length, fish_weight))
print(fish_data[:5])

 

그 다음 타깃데이터를 만드는데, 앞에서처럼 [1], [0]을 곱하지 않고 쉽게 넣을 수 있다. ones() 함수와 zeros() 함수를 사용하면 원하는 개수의 1과 0을 채운 배열을 만들 수 있다.

 

concatenate() 함수를 사용하여 배열을 연결할 수 있다. column_stack() 함수에서 처음 일렬로 배열했던 것처럼 일렬의 리스트 배열을 결과값으로 얻을 수 있다.

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

 

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

앞서 넘파이를 사용하지 않고 사이킷런으로 좀 더 손쉽게 데이터를 나눌 수도 있다. 사이킷런의 train_test_split() 함수를 사용하기 위해서는 먼저 import 해줘야 한다.

from sklearn.model_selection import train_test_split

 

그 다음 나누고 싶은 리스트나 배열을 원하는 만큼 전달하면 된다. random_state 매개변수는 자체적으로 랜덤 시드를 지정할 수 있는 함수이다.

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

 

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

 

잘 나뉘었는지 확인을 하고 잘 섞였는지 확인한다.

print(train_input.shape, test_input.shape)
print(train_target.shape, test_target.shape)
print(test_target)

 

도미와 빙어의 비율이 35:14이기 때문에 2.5:1의 비율을 가지고 있다. 하지만 테스트 세트의 비율은 3.3: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)

빙어가 한 개 증가하여 3개에서 총 4개가 된 것을 확인할 수 있다. 덕분에 비율도 2.25:1로 바뀌었다.

 

<수상한 도미 한 마리>

다시 만든 데이터로 앞서 발생했던 오류를 해결하기 위해 결과를 확인해보려고 한다.

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

 

길이 25cm, 무게 150의 도미를 넣어본다.

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

 

헉! 왜 빙어로 예측을 하는걸까? 이를 확인하기 위해 산점도를 그려보려고 한다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')  # marker 매개변수는 모양을 지정
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

분명 산점도를 보면 도미데이터에 더 가까운데, 왜 빙어로 구분한걸까? k-최근접 이웃은 주변의 샘플 중에서 '다수'인 클래스를 예측으로 사용한다. 여기서 사용하는 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환한다. 기본값이 5이기 때문에 5개의 이웃이 반환된다.

그럼, 이웃 샘플을 다시 구분해서 산점도를 그려보도록 한다.

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')
plt.show()

 

 

5개의 이웃 중 4개가 빙어임을 확인해볼 수 있다. 분명히 육안으로는 도미가 가까운데 왜 그런걸까? 거리를 확인해보기 위해 거리를 출력해본다.

print(distances)

 

<기준을 맞춰라>

빨간색 거리가 92인데, 파란색 거리가 130~138인건 아무리 봐도 이상하다. 왜 그런걸까?

그 이유는 x축은 범위가 좁고(10-40), y축은 범위가 넓기(0~1000) 때문이다. 그래서 y축으로는 조금만 멀어도 거리가 아주 큰 값으로 계산이 되는 것이다. 이를 눈으로 명확히 하기 위해 x축의 범위를 지정하는 xlim() 함수를 사용한다.

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

 

 

x축과 y축의 동일하게 맞췄더니 데이터가 일직선의 모양으로 나타난다. 두 특성(길이/무게)의 값이 놓인 범위가 매우 다르다. 이를 두 특성의 스케일이 다르다고 말한다. 이렇게 기준이 다르면 알고리즘이 올바르게 예측할 수 없기 때문에, 특성값을 일정한 기준으로 맞춰줘야 한다. 보통 이러한 작업을 데이터 전처리라고 한다.

 

가장 널리 사용하는 방법은 표준점수이다. 표준점수는 각 특성값이 0에서 표준편차의 몇 배만큼 떨어져있는지를 나타낸다. 이를 계산하려면 평균을 빼고 표준편차를 나누면 된다. 넘파이의 mean() 함수를 통해 평균을, std() 함수를 통해 표준편차를 계산할 수 있다. axis는 중심선으로, 0을 기준으로 그래프를 그리려면 axis=0으로 해줘야 한다.

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

이제 평균을 빼고 표준점수로 변환하는 작업이 필요하다. 넘파이에서는 이 기능을 제공하는데, 이러한 기능을 브로드캐스팅(broadcasting)이라고 한다.

 

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

앞서 변환한 표준점수를 산점도로 그려본다.

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

 

샘플을 제외한 나머지 데이터가 뭉쳐있는 것을 확인할 수 있다. 훈련 세트를 평균으로 빼고 표준편차로 나눴기 때문에 값의 범위가 달라져서 발생하는 현상이다. 따라서 다시 샘플을 변환하여 산점도를 그려야 한다.

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축의 범위가 바뀐 것을 확인할 수 있다. 다시 이웃모델을 찾아본다.

kn = kn.fit(train_scaled, train_target)
test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)

모든 테스트 샘플을 완벽하게 분류한 것을 알 수 있다. 다시 25cm, 150g의 생선을 제대로 분류하는지 확인한다.

print(kn.predict([new]))

도미로 제대로 분류하는 것을 알 수 있다. 이제 다시 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()

25cm, 150g의 샘플근접 이웃은 제대로 도미로 보이고, 따라서 도미로 예측하는 것을 확인할 수 있다.


● 마무리

데이터를 확인할 때 스케일이 같은지, 다른지를 확인하며 전처리 처리를 해야한다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter02_train_and_test_data.ipynb
0.09MB

+ Recent posts