회귀 분석 - 전복의 고리수 추정 신경망
목적
[파이썬 날코딩으로 알고 짜는 딥러닝] 책에서 첫번째로 나오는 전복의 고리 수 추정 신경망 파이썬 코드를 분석해보고자 한다.
데이터
kaggle의 전복 데이터셋은 4000여마리의 전복에 대해 8가지 특징값과 전복의 고리 수가 들어있다.
단층 퍼셉트론 신경망
https://minchocoin.github.io/alzza-deeplearning/1/
코드 분석
파이썬 모듈 불러들이기
import numpy as np
import csv
import time
np.random.seed(1234)
def randomize(): np.random.seed(time.time())
행렬 연산 등에 사용하는 넘파이 모듈과 데이터셋 csv파일을 읽는데 사용하는 csv모듈, 난수 초기화에 사용하는 time 모듈이 있다. 랜덤시드는 1234로 고정이며, 원한다면 randomize()로 랜덤화할 수도 있다.
하이퍼파라미터값의 정의
RND_MEAN = 0
RND_STD = 0.0030
LEARNING_RATE = 0.001
정규분포 난수값에서 RND_MEAN는 평균이고, RND_STD는 표준편차이다. 즉 저 값으로 설정하고 정규분포 난수값을 무수히 많이 생성한다면, 0이 가장 많이 생성되고, 0에서 멀어질수록 덜 생성되는, 숫자별 확률분포를 그린다면 정규분포(0,0.003)가 나올 것이다. LEARNING_RATE는 학습률이다.
실험용 메인 함수 정의
def abalone_exec(epoch_count=10, mb_size=10, report=1):
load_abalone_dataset()
init_model()
train_and_test(epoch_count, mb_size, report)
위 함수는 먼저 데이터셋을 읽는 함수를 호출하고, 모델의 파라미터를 초기화하는 함수를 호출하고, 마지막으로 매개변수로 받은 epoch_count,mb_size,report를 train_and_test함수에 전달하고 호출하여 실제로 훈련과 테스트를 한다.
데이터 적재 함수 정의
def load_abalone_dataset():
with open('../../data/chap01/abalone.csv') as csvfile:
csvreader = csv.reader(csvfile)
next(csvreader, None)
rows = []
for row in csvreader:
rows.append(row)
global data, input_cnt, output_cnt
input_cnt, output_cnt = 10, 1
data = np.zeros([len(rows), input_cnt+output_cnt])
for n, row in enumerate(rows):
if row[0] == 'I': data[n, 0] = 1
if row[0] == 'M': data[n, 1] = 1
if row[0] == 'F': data[n, 2] = 1
data[n, 3:] = row[1:]
먼저 csv파일을 연다. 그리고 next함수를 이용하여 헤더정보는 읽지 않도록 한다. 그리고 csvreader의 각 행을 rows라는 배열에 넣는다.
그리고 input_cnt와 output_cnt에는 각각 10과 1이 들어가는데, 각각 입력 벡터의 크기와 출력 벡터의 크기이다. data는 입출력벡터 정보를 저장하는 공간이다. rows 배열과의 차이점은 data는 성별 정보가 원-핫 벡터로 표시된다는 점이다.
data의 0번째,1번째,2번째 열을 성별로 사용하는데, 유충이면 0번째열만, 수컷이면 1번째열만 , 암컷이면 2번째열만 1로 설정한다. data의 3번째 열부터는 rows의 데이터를 그대로 붙인다.
파라미터 초기화 함수 정의
def init_model():
global weight, bias, input_cnt, output_cnt
weight = np.random.normal(RND_MEAN, RND_STD,[input_cnt, output_cnt])
bias = np.zeros([output_cnt])
weight 행렬의 크기를 [10,1]로 하고, bias의 크기를 [1]로 하여 각각 초기화한다. weight는 정규분포를 갖는 난숫값으로 초기화하는데, 경사하강법을 시작할 때 다양한 출발지에서 출발하도록 하기 위함이다. 즉 파라미터의 초기값을 여러개 해보는 것이다. bias는 초기에는 오히려 학습에 역효과만 주기때문에 0으로 한다.
학습 및 평가 함수 정의
def train_and_test(epoch_count, mb_size, report):
step_count = arrange_data(mb_size)
test_x, test_y = get_test_data()
for epoch in range(epoch_count):
losses, accs = [], []
for n in range(step_count):
train_x, train_y = get_train_data(mb_size, n)
loss, acc = run_train(train_x, train_y)
losses.append(loss)
accs.append(acc)
if report > 0 and (epoch+1) % report == 0:
acc = run_test(test_x, test_y)
print('Epoch {}: loss={:5.3f}, accuracy={:5.3f}/{:5.3f}'. \
format(epoch+1, np.mean(losses), np.mean(accs), acc))
final_acc = run_test(test_x, test_y)
print('\nFinal Test: final accuracy = {:5.3f}'.format(final_acc))
먼저 epoch_count 만큼 학습을 반복하게 되어있다. 1번 학습시, mb_size만큼 쪼갠 데이터를 step_count(=학습용 데이터의 양 // mb_size)만큼 돌며 순서대로 학습하여 학습용 데이터를 모두 학습하도록 되어있다.
미니배치크기의 데이터 학습은 먼저 get_train_data로 학습용 데이터를 얻어와 run_train() 함수로 학습시키고, 손실과 정확도를 리턴받아 리스트에 넣는다. 만약 보고주기가 되면 run_test()로 테스트데이터를 이용하여 정확도를 측정하여 보여준다.
그리고 최종적으로 run_test()를 돌리고 최종 정확도를 출력한다.
학습 및 평가 데이터 획득 함수 정의
def arrange_data(mb_size):
global data, shuffle_map, test_begin_idx
shuffle_map = np.arange(data.shape[0])
np.random.shuffle(shuffle_map)
step_count = int(data.shape[0] * 0.8) // mb_size
test_begin_idx = step_count * mb_size
return step_count
def get_test_data():
global data, shuffle_map, test_begin_idx, output_cnt
test_data = data[shuffle_map[test_begin_idx:]]
return test_data[:, :-output_cnt], test_data[:, -output_cnt:]
def get_train_data(mb_size, nth):
global data, shuffle_map, test_begin_idx, output_cnt
if nth == 0:
np.random.shuffle(shuffle_map[:test_begin_idx])
train_data = data[shuffle_map[mb_size*nth:mb_size*(nth+1)]]
return train_data[:, :-output_cnt], train_data[:, -output_cnt:]
arrange_data()함수는 train_and_test()함수에서 딱 한 번 호출한다. data.shape()함수는 data 행렬의 (행의 개수, 열의 개수)를 출력한다. 또한 arange함수는 np.arange(시작점(생략 시 0), 끝점(미포함), step size(생략 시 1)) 로 호출하며, 시작점부터 끝점(미포함) 까지 step size간격으로 수열을 생성한다. 따라서 data 개수 만큼 일련번호를 만들어 그것을 무작위로 섞는 함수이다.
그리고 데이터를 학습용과 평가용으로 나누는데, 학습용 데이터를 80%로 잡는다. 또한 학습용 데이터 수를 미니배치 사이즈(mb_size)로 나누어 전체 데이터를 한번 학습하는데 미니배치 사이즈 만큼의 학습을 몇번 해야하는지 계산한다.
또한 test_begin_idx에 테스트용 데이터의 시작점을 저장한다(step_count*mb_size는 학습용 데이터의 대략의 크기이다).
get_test_data()함수는 arrange_data()에서 섞어놓은 shuffle_map에서 test_begin_idx 이후의 값(data 행렬의 행의 번호)을 평가용 데이터로 반환한다.
배열 인덱싱에서 A[-2]는 A 배열의 뒤에서 2번째 열을 의미하고, 행렬 인덱싱에서 B[1:3,4:5]는 B행렬에서 1행부터 2행 그리고 4열을 추출한다는 의미이며 C[:,:-2]는 C의 전체 행, 뒤에서 3번째열까지 추출한다는 의미이다.
따라서 test_data[:,:-output_cnt]는 test_data의 모든 데이터 행(:) 과 뒤에서 output_cnt열까지 추출한다는 의미, 여기서는 output_cnt=1이므로 마지막 열을 빼고 추출한다는 의미이다. 마지막 열은 정답 벡터이고 그 이전 열은 입력 벡터이므로, ‘return …‘에서 알 수 있듯 (입력 벡터, 정답 벡터)를 반환한다.
get_train_data()는 get_test_data()와 비슷한데, 단지, 에포크 시작시(nth=0)shuffle_map의 학습용 데이터 부분(0~ test_begin_idx-1까지)를 한번 섞고 train_data를 mb_size만큼 쪼개어 nth번째 shuffle_map데이터를 입력 벡터와 정답 벡터로 분할해 리턴한다.
학습 실행 함수와 평가 실행 함수 정의
def run_train(x, y):
output, aux_nn = forward_neuralnet(x)
loss, aux_pp = forward_postproc(output, y)
accuracy = eval_accuracy(output, y)
G_loss = 1.0
G_output = backprop_postproc(G_loss, aux_pp)
backprop_neuralnet(G_output, aux_nn)
return loss, accuracy
def run_test(x, y):
output, _ = forward_neuralnet(x)
accuracy = eval_accuracy(output, y)
return accuracy
먼저 run_train()함수는 주어진 입력행렬 x와 정답행렬 y를 이용하여 학습한다.
먼저 forward_neuralnet()함수가 순전파를 수행하여 입력행렬 x로 부터 출력 output을 구하고 forward_postproc()함수가 손실값을 계산 하고, eval_accuracy를 통해 정확도를 측정한다.
그리고 초기 loss값 1.0을 설정하고, backprop_postproc()을 호출하고 backprop_neuralnet를 실행하여 파라미터값을 변화시킨다.
run_test()함수는 입력 x에대한 output의 정확도만 측정하여 반환한다.
순전파 및 역전파 함수 정의
def forward_neuralnet(x):
global weight, bias
output = np.matmul(x, weight) + bias
return output, x
def backprop_neuralnet(G_output, x):
global weight, bias
g_output_w = x.transpose()
G_w = np.matmul(g_output_w, G_output)
G_b = np.sum(G_output, axis=0)
weight -= LEARNING_RATE * G_w
bias -= LEARNING_RATE * G_b
forward_neuralnet(x) 함수는 입력 x에 대해 가중치를 곱하고 bias를 더해 output을 계산하여 반환한다. x는 (N,10) 형태이고, weight는 (10,1)형태이므로 두 행렬을 곱하면 (N,1) 형태이다. bias는 (1)의 형태인데, 파이썬이 알아서 각 행에 bias를 더하게 된다.
backprop_neuralnet는 순전파 출력 output에 대한 손실 기울기 G_output을 받아서 weight와 bias의 손실 기울기를 구한다. weight와 bias에 손실기울기를 빼줌으로서 학습을 수행한다. 앞 글에서 $w_i$의 손실 기울기는 output의 손실 기울기에 $x_i$를 곱하면 된다고 하였다. 예를 들어 학습용 데이터 입력 세트가 총 5(a,b,c,d,e)개 들어왔다고 하자. 그리고 각 입력 세트는 3개의 입력으로 되어있다고 하자. 그러면 출력도 5개가 될 것이다. 각 입력 세트의 output의 손실 기울기를 $G_x$라 하자. 그러면 다음과 같이 가중치 행렬 W의 손실 기울기는 다음과 같다.
\[\begin{pmatrix}Gw_0\\Gw_1\\Gw_2\\Gw_3\\ \end{pmatrix} = \begin{pmatrix}1&1&1&1&1\\a_1&b_1&c_1&d_1&e_1\\a_2&b_2&c_2&d_2&e_2\\a_3&b_3&c_3&d_3&e_3\\ \end{pmatrix}\begin{pmatrix}G_1\\G_2\\G_3\\G_4\\G_5\\ \end{pmatrix}\]여기서 $Gw_x$는 $w_1$의 손실 기울기 의미한다. bias의 경우에는 $w_0x_0$에서 $x_0=1$로 고정이면 $w_0$가 bias 역할을 하므로 위 식에서 $a_0,b_0,c_0,d_0,e_0$가 다 1이면 된다. 따라서 bias의 손실 기울기는 $G_1 + G_2 + G_3 + G_4 + G_5$임을 알 수 있다.
후처리 과정에 대한 순전파 및 역전파 함수 정의
def forward_postproc(output, y):
diff = output - y
square = np.square(diff)
loss = np.mean(square)
return loss, diff
def backprop_postproc(G_loss, diff):
shape = diff.shape
g_loss_square = np.ones(shape) / np.prod(shape)
g_square_diff = 2 * diff
g_diff_output = 1
G_square = g_loss_square * G_loss
G_diff = g_square_diff * G_square
G_output = g_diff_output * G_diff
return G_output
forward_postproc() 함수는 먼저 예측값 output 벡터와 실제값 y벡터를 빼서 각 예측값과 각 정답을 빼서 차이를 구하고, 오차를 제곱한 값을 square에 넣고 오차의 제곱 평균을 loss에 넣는다. output, y, diff, square는 (N,1)의 벡터이지만, loss는 스칼라 값이다.
backprop_postproc() 에서 diff는 (미니배치 크기(A), 출력벡터 크기{B}), 여기서는 (N,1) 크기를 갖는다. 여기서 $diff_{ij} = output_{ij} - y_{ij}$ 이고 $square_{ij} = diff_{ij}^2$ 이라 하고, 평균제곱오차이므로 손실기울기 $L = \frac{\sum square}{AB}$ , G_loss = $\frac{\partial L}{\partial L} = 1$ 에서
\[\frac{\partial L}{\partial square_{ij}} = \frac{1}{AB}\]이고
\[\frac{\partial square_{ij}}{\partial diff_{ij}} = 2diff_{ij}\]이며
\[\frac{\partial diff_{ij}}{\partial output_{ij}} = 1\]이다.
따라서
\[\frac{\partial L}{\partial output_{ij}} =\frac{\partial L}{\partial L}\frac{\partial L}{\partial square_{ij}}\frac{\partial square_{ij}}{\partial diff_{ij}}\frac{\partial diff_{ij}}{\partial output_{ij}}= \frac{2diff_{ij}}{AB}\]이다.
backprop_postproc()은 위 식을 코드로 만든 것이다.
정확도 계산 함수 정의
def eval_accuracy(output, y):
mdiff = np.mean(np.abs((output - y)/y))
return 1 - mdiff
mdiff는 정답 값에 비해 정답 값과 output의 차이가 얼마나 나는지의 평균이고, 이를 오류율이라고 하면 정확도는 1-오류율이라고 할 수 있다.
실행 결과
abalone.ipynb를 실행하는 abalone_test.ipynb에서 abalone_exec()을 실행하였을 때 표1과 같은 결과가 나왔다.
모델1 | 모델2 | 모델3 | 모델4 | 모델5 | 모델6 | 모델7 | 모델8 | 모델9 | 모델10 | |
---|---|---|---|---|---|---|---|---|---|---|
학습률 | 0.1 | 0.1 | 0.1 | 0.01 | 0.01 | 0.1 | 0.01 | 0.2 | 0.2 | 0.3 |
미니배치 | 100 | 100 | 10 | 100 | 100 | 100 | 200 | 200 | 100 | 200 |
에포크 | 100 | 1000 | 100 | 100 | 10000 | 20000 | 10000 | 10000 | 10000 | 10000 |
정확도 | 84.1% | 84.7% | 82.9% | 81.6% | 83.8% | 84.5% | 84.6% | 83.8% | 83.8% | 85.4% |
(표1 : 실험결과)
(사진1 : 실험진행과정)
결과 분석
다음은 하이퍼파라미터 값에 따른 정확도의 변화이다.
에포크값에 의한 변화
(표1)의 모델1, 모델2, 모델6으로 보았을 때 에포크 값이 100에서 1000으로 늘었을 때는 정확도가 증가하였으나 1000에서 20000으로 늘렸을 때는 오히려 감소하는 모습을 보였다.
미니배치에 의한 변화
(표1)의 모델1과 모델3으로 보았을 때 미니배치 값을 100에서 10으로 줄였을 때 정확도가 84.1%에서 82.9%로 감소하였다. 그리고 <표1>의 모델5와 모델7로 보았을 때 미니배치값을 100에서 200으로 늘렸을 때 정확도가 증가하였다.표1>
학습률에 의한 변화
(표1)의 모델1과 모델4로 보았을 때 학습률을 0.1에서 0.01로 줄였을 때 오히려 정확도가 감소하였다. (표1)의 모델7, 모델8, 모델10으로 보았을 때, 0.01에서 0.2로 늘렸을 때 정확도가 감소하였으나, 0.2에서 0.3으로 올렸을 때 정확도가 상승하였다. 그리고 학습률이 0.01, 0.1 일때보다 0.2,0.3 일때 학습 중에 정확도가 더 많이 움직이는 것을 알 수 있다.
결론
코드 분석을 통해 컴퓨터가 어떻게 데이터를 학습하는지 알 수 있었다.
(표1)의 10번의 실험을 통해 에포크, 미니배치사이즈, 학습률 모두 정확도에 영향을 주는 것을 알 수 있다. 영향을 선형적으로 주는 것이 아니라, 즉 클수록, 작을수록 좋은 것이 아니라 적당한 값이 있다. 학습률 0.3, 미니배치사이즈 200, 에포크 10000 일때 85.4%(모델 10)로 가장 정확도가 크게 나타났다. 이 값은 학습률 0.01, 미니배치사이즈 100, 에포크 100 일때 81.6%(모델4)와 3.8%차이이다.
Reference
- 윤덕호 저, [파이썬 날코딩으로 알고 짜는 딥러닝], 한빛미디어, 2019
- https://jimmy-ai.tistory.com/45
댓글남기기