Tensorflow 없이 numpy를 이용해서 단층 신경망을 만들어 보자.
우라는 Abalone 데이터를 가지고 전복의 나이를 예측하는 신경망을 구축해 볼 예정이다.
데이터 셋은 Kaggle에 있는 Abalone Dataset 에서 받아서 사용 할 수 있다.
✔︎ 데이터 살펴보기
import pandas as pd
df = pd.read_csv('data/abalone.csv')
df.head()
◼ 변수 설명
독립변수는 Sex ~ Shell weight 까지 8개로 구성되어 있고, 종속변수는 Rings 이다.
데이터는 총 4177개로 준비되어 있다.
📌 단층 신경망을 만들기 위한 설계도
우리는 위의 설계도 순서대로 함수를 하나하나 만들어 볼 것이다.
1. 모듈 불러오기
import numpy as np
import csv
# 실험 결과를 재현하기 위해 난수 발생패턴을 고정시키는 np.random.seed()함수값을 설정
np.random.seed(333)
2. 하이퍼 파라미터 설정
- learning rate 학습률
- 정규분포 난숫값 [ 평균 / 표준편차 ]
RND_MEAN = 0 # 평균
RND_STD = 0.0030 # 표준편차
LEARNING_RATE = 0.001 # 학습률
처음에는 이값을 무작위로 지정할 수 밖에 없다.
그렇다면 무작정 무작위값을 주기 보다, 랜덤하지만 적어도 최소한의 규칙성이 있는 범위 내에서 난수를 지정하는 것 이 더 좋을 것 이다.
이러한 방법은 Xavier 초기화 와 He 초기화라 하며 현재 가장 많이 쓰이는 방법이다.
3. 메인함수 정의
메인 함수 main_exec() 함수를 실행하게 되면
#1 데이터를 불러들이는 load_dataset() 함수,
#2 가중치와 편향을 초기화 해주는 init_model() 함수,
#3 학습 및 신경망 성능 테스트를 위한 train_and_test()함수
이렇게 세가지 함수가 차례로 실행되며, 신경망 모델을 생성하고 학습 전체 과정을 일괄 처리한다.
def main_exec(epoch=10, mb_size=10, report=1, train_rate=0.8):
load_dataset() # 데이터를 불러오는 함수
init_model() # 가중치와 편향을 초기화 해주는 함수
train_and_test(epoch, mb_size, report, train_rate) # 학습 및 신경망 성능 테스트하는 함수
메인 함수에서는 학습 횟수, 미니배치 크기, 중간 보고 주기, 학습 데이터 비율 등 학습과정에 관련된 하이퍼파라미터값들을 epoch_count, mb_size, report, train_rate 의 변수로 지정받아 이 값들을 실제로 이용할 train_and_test()에 전달하게 된다.
4. 데이터를 불러오는 load_dataset() 정의
def load_dataset():
with open('data/abalone.csv') as csvfile:
csvreader = csv.reader(csvfile)
next(csvreader, None) # 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
]) # np.zeros() 함수는 지정해준 크기만큼 0값의 행렬을 생성
# 원 핫 벡터 처리
for n, row in enumerate(rows): # n = index , row = value
if row[0] == 'I': data[n, 0] = 1 # Sex의 값에 따라 원핫인코딩
if row[0] == 'M': data[n, 1] = 1
if row[0] == 'F': data[n, 2] = 1
data[n, 3:] = row[1:] # 나머지 데이터를 넣어줌
# load_dataset() 함수의 주요기능
csv파일 내용을 메모리로 읽어들여 이용할 수 있게 준비해주며, 데이터에 포함되어 있는 비선형 정보를 원-핫 벡터로 표현하여 출력한다.
csv 모듈의 reader()기능을 활용하여 Kaggle에서 가져온 'abalone.csv' 내용을 메모리로 읽어들니다.
Abalone 데이터 셋의 첫 번째 행은 데이터에 대한 설명으로 프로그램을 구축하는데 있어 필요 없다는 것을 알 수 있다.그래서 next() 함수를 통해 파일의 첫 행을 읽지 않고 건너뛰게 만든다. (즉 데이터의 첫 번째 행을 무시하기 위해 사용)
그리고 for 반복문을 활용해 csvreader에 담긴 전복 개체별 정보를 rows빈 리스트에 append()함수로 넣어준다.
그 다음 input_cnt와 output_cnt 변수에 입출력 벡터 크기를 각각 10과 1 로 설정한다.
이 값은 기존 8이었던 입력 벡터 크기가 '원-핫 벡터 표현'을 통해 10으로 증가한 것이며, 이 값을 통해 이후 입출력 벡터 정보를 저장할 data 행렬 생성 및 data의 크기 지정에 활용된다. 또한 이후 전역변수로 선언하여 다른 함수에서 활용될 예정이다.
반복문을 활용하여 rows에 담긴 '범주형 성별 정보'를 '원-핫 벡터로 변환하는 처리', 그리고 나머지 데이터 항목들을 일괄적으로 복제하는 처리를 수행하게 된다.
5. 파라미터 초기화 함수 init_model() 정의
def init_model():
global weight, bias, input_cnt, output_cnt
weight = np.random.normal(RND_MEAN, RND_STD,[input_cnt, output_cnt])
# normal 메서드 : (mean, sd, shape)
bias = np.zeros([output_cnt])
# weight 가중치 행렬은 [10,1] , bias 편향 벡터는 [1]형태
weight는 처음에 설정한 하이퍼파라미터값과 np.random.normal()를 이용해 정규분포를 갖는 난숫값으로 초기화를 진행한다. 그 이유는 𝑙𝑜𝑐𝑎𝑙 𝑚𝑖𝑛𝑖𝑚𝑢𝑚을 피할 가능성을 조금이라도 높이기 위해서 이다.
편향(bias)은 초기에 너무 큰 영향을 주어 학습에 역효과를 불러오지 않도록 0으로 초기화하여 생성하였다.
6. 학습 및 평가 함수 train_and_test() 정의
위의 설계도를 보면 train_and_test() 함수를 정의하기 위해 5개의 함수가 필요하다.
- arrange_data()
- get_train_data()
- get_test_data()
- run_train()
- run_test()
학습과 평가를 위해 데이터 셋을 학습 데이터와 테스트 데이터로 분리해줘야 한다. 여기서는 7:3으로 분리할 예정이다.
그 다음 학습 데이터에 대한 미니배치 처리를 수행하고, 학습 데이터와 테스트 데이터의 독립변수와 종속변수를 나눠줄 것이다.
def train_and_test(epoch_count, mb_size, report, train_rate):
# 미니배치가 볓 스텝으로 쪼개지는지 값을 반환해줌
step_count = arrange_data(mb_size, train_rate)
# 테스트 데이터의 독립, 종속변수 분할
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(
f"Epoch{epoch + 1}: Train - loss = {np.mean(losses):5.3f}, accuracy = {np.mean(accs):5.3f} / Test={acc:5.3f}"
)
final_acc = run_test(test_x, test_y)
print(f'\n 최종 테스트 : final accuracy = {final_acc:5.3f}')
train_and_test 함수는 epoch_count, mb_size, report, train_rate를 인자로 받는다,
6.1 데이터셋을 섞어주고 step_count를 계산해주는 arrange_data()
- mb_size, train_rate 인수값을 받아 전체 데이터셋을 무작위로 섞어준다.
- 학습 데이터에 필요한 1 에폭당 미니배치 스텝 수를 계산하여 step_count에 저장한다.
(step_count = 전체데이터 / 미니배치 사이즈)
def arrange_data(mb_size, train_rate):
# data는 load_dataset()의 전역변수
# shuffle_map, test_begin_index 은 다른 동간에서도 사용할 수 있게 전역변수화
global data, shuffle_map, test_begin_index
# 전체 데이터셋을 무작위로 섞어준다.
shuffle_map = np.arange(data.shape[0])
np.random.shuffle(shuffle_map)
# 미니배치 단위
step_count = int(
data.shape[0] * train_rate) // mb_size # train_rate는 학습데이터의 비율을 말함
test_begin_index = step_count * mb_size # 경계선 / Train_set의 데이터 수
return step_count
6.2 데이터를 분할 해주는 get_test_data(), get_train_data()
# get_test_data()
- 전체 데이터셋에서 테스트에 사용할 데이터 만큼 분리한다.
- 분리된 데이터에서 독립변수와 종속변수를 분할하여 test_x, test_y에 저장한다.
def get_test_data():
global data, shuffle_map, test_begin_index, output_cnt # 전역변수
test_data = data[shuffle_map[test_begin_index:]] # 테스트 데이터 분리
return test_data[:, :-output_cnt], test_data[:, -output_cnt:] # 독립변수, 종속변수 나눠주기
# get_train_data()
- 전체 데이터셋에서 인수값으로 전달 받은 mb_size만큼 미니배치 처리를 수행한다.
- 처리된 미니배치 데이터에서 독립변수와 종속변수로 나눠준다.
- 새로운 epoch이 수행될 때마다의 무작위 표본 추출을 시행한다.
➞ 에폭값이 갱신될 때 학습 데이터가 다시 섞이게 되며, 학습의 효율성이 높아짐을 기대할 수 있게 됩니다.
def get_train_data(mb_size, nth):
global data, shuffle_map, test_begin_index, output_cnt
if nth == 0: # shuffle_map을 epoch이 시작할 때 마다 shuffle 해준다.
np.random.shuffle(shuffle_map[:test_begin_index])
train_data = data[shuffle_map[mb_size * nth : mb_size * (nth + 1)]]
# 미니배치 수 만큼 뽑아서 순차적으로 학습시킴
return train_data[:, :-output_cnt], train_data[:, -output_cnt:] # 독립변수, 종속변수로 나눠준다.
6.3 학습 시키는 함수 run_train()
- 학습 데이터 train_x, train_y를 인자로 전달 받는다.
- 또 다른 다섯가지의 함수가 등장한다.
[ forward_neuralnet(), forward_postproc(), eval_accuracy(), backprop_postproc(), backprop_neuralnet() ] - 순전파와 역전파가 수행되며 학습이 진행, 그에 따른 손실값(loss)과 정확도(acc)를 return 해준다.
- 전달받은 loss와 acc는 append()를 통해 빈 리스트 losses, accs에 보관된다.
def run_train(x, y):
output, aux_nn = forward_neuralnet(x) # axu_nn, aux_pp = 역전파에 필요한 부가정보
loss, aux_pp = forward_postproc(output, y) # output과 실제값 y로 loss를 구해줌
accuracy = eval_accuracy(output, y) # 정확도
G_loss = 1.0
G_output = backprop_postproc(G_loss, aux_pp) # 역전파로 기울기를 구하는 함수
backprop_neuralnet(G_output, aux_nn) # w, b 갱신하는 과정
return loss, accuracy
6.4 테스트를 수행하는 run_test()
- test_x, test_y를 전달받는다.
- 테스트 데이터에 대한 acc를 반환한다.
def run_test(x, y):
output, _ = forward_neuralnet(x) # _: 값이 필요하지 않을 때 사용
accuracy = eval_accuracy(output, y)
return accuracy # 정확도를 반환
학습이 완료된 후에, 최종 모델 정확도를 다시한번 출력하기 위해 한번 더 run_test()를 수행하고,
그 결과를 출력하여 학습 및 평가가 모두 완료되었다고 알려주는 역할을 한다.
7. 순전파 및 역전파 함수 정의
7.1 forward_postproc() : 신경망의 순전파에 따른 mse 구하는 과정
- output과 실제 y값을 인자로 받아 mse를 구해준다.
def forward_postproc(output, y):
diff = output - y # 예측값 - 실제값 (편차)
square = np.square(diff) # 제곱해주는 연산
loss = np.mean(square) # 편차 제곱의 평균 (mse)
return loss, diff # mse와 편차를 return
7.2 backprop_postproc() : mse의 역전파 과정 - 미분을 통한 값의 갱신을 수행
$ \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \times \frac{\partial y}{\partial x} $ 로 역전파가 수행된다.
$\frac{\partial y}{\partial x}$ 의 경우 입력된 파라미터 값에 따른 𝑦 값의 미분을 구하는 과정으로써 이후 가중치와 편향에 따른 손실함수의 기울기를 구하는 과정에서 다뤄볼 것이다.
$ \frac{\partial L}{\partial y} $ 는 출력값 y에 대한 역전파 방법으로 어떤 손실함수로 구할것이냐에 따라 연산과정이 달라진다.
우리가 이번에 다뤄볼 회귀에 맞는 손실함수인 mse의 기울기를 구해보면 다음과 같다.
mse를 구하는 '예측', '편차', '제곱', '평균(손실)' 과정을 각 단계별로 미분하여 진행하면 쉽게 구할 수가 있게 된다.
$\frac{\partial L}{\partial mse} $ 는 1이므로 다음과정인 $ \frac{\partial mse}{\partial square}$ 를 봐보자.
결국 $ \frac{\partial mse}{\partial square}$ 를 수행하면 $ \frac{1}{ MN}$ 이 된다.
그 다음으로 제곱에 대한 $ \frac{\partial square}{\partial diff}$ 를 살펴보면,
2 * diff 라는 간단한 결과가 나온다.
마지막으로 $ \frac{\partial diff}{\partial output}$ 을 보면,
1로 나오는것을 확인 할 수 있다.
def backprop_postproc(G_loss, diff):
shape = diff.shape
G_loss = 1 # delta(L) / delta(mse) 미분
g_loss_square = np.ones(shape) / np.prod(shape) # delta(mse) / delta(square) 미분
g_square_diff = 2 * diff # delta(square) / delta(diff) 미분
g_diff_output = 1 # delta(diff) / delta(output) 미분
G_square = g_loss_square * G_loss # 순차적으로 값을 곱해준다.
G_diff = g_square_diff * G_square
G_output = g_diff_output * G_diff
return G_output
평균제곱오차의 각 단계별 입출력 간 부분적인 기울기를 구해놓고, 손실에 대한 기울기의 연쇄적 계산에 활용하여 최종적으로 mse의 역전파 처리인 G_output을 계산하였다.
7.3 신경망의 순전파 연산과정 forward_neuralnet()
train_x는 x로 전달받아 가중치와 행렬곱을 수행하고, bias와 더해진다.
그리고 이러한 결과는 output으로 저장하고 return 해준다.
또한 순전파 과정을 수행하기위해 전달받은 x 는 이후 역전파과정 수행을 위해 다시 output과 함께 return 해준다.
def forward_neuralnet(x):
global weight, bias
output = np.matmul(x, weight) + bias # 행렬 계산
return output, x # 입력값 x는 역전파를 수행하기 위해 return
7.4 역전파를 수행하는 backprop_neuralnet()
신경망 연산과정을 𝒀 = 𝑿𝑾 + 𝑩 로 하고, 가중치의 손실 기울기에 대하여 구하는 과정을 살펴보자.
가중치의 손실 기울기는 $X^{T}G$ 로 정리할 수 있다.
이때 $G_{ij}$는 $Y_{ij}$ 성분에 대한 손실 함수의 기울기로, 평균제곱오차의 역전파 처리 과정 에서 살펴보았으며, G_output이라는 변수로 저장되어있다.
편향에 관한 손실 기울기는 $G_{ij}$ 의 각 행의 합을 구하는 방법으로 구해진다.
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 # 편향 갱신
8. 신경망 학습 결과 테스트 및 평가함수 정의 eval_accuracy()
정확도 평가의 경우 회귀 분석 문제에서 정확도를 정의하는 다양한 방식이 있지만 그 중, 간단한 방식인 예측값과 실제값과의 차를 실제값으로 나눠주는 방식을 채택하여 오류율을 산출하고 그 값을 1에서 빼줌으로 정확도를 정의한다.
def eval_accuracy(output, y): # y = test_y
mdiff = np.mean(np.abs((output - y) / y)) # 오류율
return 1 - mdiff # 정확도
실행하기
LEARNING_RATE = 0.001
main_exec(epoch=100, mb_size=20, report=20, train_rate=0.80)
new_x = [0, 1, 0, 0.68, 0.54, 0.19, 1.43, 0.67, 0.382, 0.5]
output = forward_neuralnet(new_x)
print(output[0] + 1.5)
새로운 x값을 넣어서 학습을 수행해보면 정확도 81%로 전복의 나이를 13.7세로 예측한다.
weight와 bias를 살펴보면,
암컷 여부와 키가 전복의 고리 수를 추정하는데 큰 영향을 미친 것 을 확인할 수 있다.
하지만 난수 초깃값을 이용했기 때문에 다시 학습을 진행하면 다른 결과가 나올 수 있다.
'DL(Deep-Learning) > 개념' 카테고리의 다른 글
분류 성능 지표 - Precision(정밀도), Recall(재현율) (0) | 2022.01.29 |
---|---|
활성화 함수 activation function (0) | 2022.01.29 |
[DL] 오차 역전파 Error Backpropagation (0) | 2022.01.28 |
[DL] 신경망 Neural Network (0) | 2022.01.28 |
[DL] 퍼셉트론(Perceptron) (0) | 2022.01.28 |