[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🛠 기타/Data & AI

단층 퍼셉트론 - 회귀분석 구현

inu 2020. 7. 15. 21:10
반응형
  • 데이터를 기반으로 전복의 고리수를 예측하는 단층 퍼셉트론을 구현해보자.
  • 주어진 데이터는 'Sex', 'Length', 'Diameter', 'Height', 'Whole weight', 'Shucked weight', 'Viscera weight', 'Shell weight', 그리고 결과치가 되는 'Rings' 이상 9개이다.
  • 데이터의 80%는 학습 데이터로, 20%는 테스트 데이터로 활용된다.
  • 수업에서 사용된 코드를 분석한 자료입니다.

파이썬 모듈 불러오기

import numpy as np
import csv
import time

np.random.seed(1234)
def randomize(): np.random.seed(time.time())
  • 필요한 모듈을 불러온다.
  • 추후 활용될 랜덤에 대비하여 미리 random함수의 seed와 randomize라는 seed 재설정함수를 만들어 놓는다.

하이퍼 파라미터 정의

RND_MEAN = 0
RND_STD = 0.0030

LEARNING_RATE = 0.001
  • 학습이 진행될 동안 변하지 않는 제어값인 하이퍼 파라미터들을 정의한다.
  • RND_MEAN과 RND_STD는 추후 정규분포로 랜덤난수를 가져올 때 평균값과 표준편차값으로 활용된다.
  • 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) # 학습 및 테스트 수행 함수
  • 에폭횟수와 미니배치크기, 보고주기를 파라미터로 갖는 메인 실험함수이다. 본 함수를 구동하여 훈련 및 테스트를 진행한다.
  • load_abalone_dataset() : abalone 관련 데이터를 불러온다.
  • init_model() : 모델을 초기화한다.
  • train_and_test() : 파라미터로 받아온 에폭횟수와 미니배치크기, 보고주기를 사용하여 학습 및 테스트를 수행하도록 한다.

데이터 불러오는 함수

def load_abalone_dataset():
    with open('abalone.csv') as csvfile: # csv 파일 데이터 rows 리스트에 저장
        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:]
  • kaggle에서 다운로드 받은 abalone dataset을 활용해 데이터를 전역변수로 저장한다.
  • csv 모듈을 활용해 먼저 rows 리스트에 모든 데이터정보를 저장한다.
  • next는 csv데이터 내부에 첫번째 행에 존재하는 칼럼명 부분을 스킵하는 역할을 한다.
  • input_cnt, output_cnt은 입력벡터와 출력벡터의 데이터 개수를 의미한다.
  • data는 csv에서 얻어온 데이터들의 개수만큼 행 개수를 지정하고, input_cnt + output_cnt의 칼럼 개수를 지정하여 0으로 이루어진 numpy배열을 생성한다.
  • 제일 위에도 적혀있듯이, 결과치가 되는 Rings(고리수)를 제외한 데이터 개수는 총 8개인데, input_cnt를 10으로 설정했다. 그 이유는 '원 핫 벡터'를 사용했기 때문이다. 'Sex(성별)'값을 유충, 수컷, 암컷으로 나누어야하는데 이러한 비선형 데이터를 단순히 0,1,2와 같은 값으로 처리한다면 컴퓨터가 제대로 처리하지 못할 것이다. (해당 값을 곱하는 등의 과정이 반복될텐데, 그 과정에서 0,1,2가 여전히 의미를 가지긴 힘들다) 따라서 유충, 수컷, 암컷의 컬럼을 각각 따로 만들고, 데이터별로 해당하는 컬럼의 값을 1로, 해당하지 않는 칼럼의 값을 0으로 설정하여 활용한다.
  • 맨 아래의 for문은 그러한 원 핫 벡터를 처리하는 반복문이다.

모델 초기화 함수

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는 가중치이다. 기존에 정의한 하이퍼 파라미터 RND_MEAN, RND_STD과 데이터를 불러오는 함수(load_abalone_dataset)에서 정의한 input_cnt와 output_cnt를 사용하여 정규분포를 갖는 랜덤난수 numpy배열을 생성하여 각 데이터를 가중치로 활용한다.
  • bias는 편향치이다. output_cnt만큼만 있으면 되므로 그를 활용해 0으로 이루어진 numpy배열을 생성하고 초기엔 편향을 모두 0으로 설정한다.
  • 두 값 모두 전역변수로 선언하여 다른 함수에서도 활용할 수 있도록 했다.

학습 및 테스트 수행 함수

def train_and_test(epoch_count, mb_size, report):
    step_count = arrange_data(mb_size) 
    # mb_size를 기반으로 배치가 총 몇개의 미니배치로 나눠져 학습이 진행되어야 하는지 파악
    test_x, test_y = get_test_data() # 테스트 데이터를 얻는다.

    for epoch in range(epoch_count): 
    # epoch_count를 기반으로 반복횟수를 결정함
        losses, accs = [], [] 
        # 매 에폭마다 손실과 정확도를 저장

        for n in range(step_count): 
        # 위 arrange_data에서 구한 미니배치 덩어리 수 만큼 반복
            train_x, train_y = get_train_data(mb_size, n) 
            # n번째 미니배치에 대한 학습진행
            loss, acc = run_train(train_x, train_y)  
            # 해당 학습에 대한 손실함수값과 정확도 리턴            
            losses.append(loss)
            accs.append(acc)

        if report > 0 and (epoch+1) % report == 0: 
        # 보고 주기가 0보다 크고, epoch값이 보고주기에 이르러야함
            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만큼 학습을 진행한다. 각 epoch별로도 배치 통채로 학습을 하지 않고 미니배치의 크기로 나눠서 학습을 진행한다. (cf. epoch은 학습 데이터 전체를 모델에 한 번 학습시키는 것이다. 즉, epoch_count는 모델에 학습 데이터를 몇번 학습시킬지를 뜻하는 것이다.)
  • 각 에폭마다 미니배치만큼의 사이즈로 학습을 진행한다. get_train_data로 학습 데이터를 얻고, run_train으로 학습을 수행한다.
  • 사용자가 현 상황을 확인할 수 있도록 보고주기의 에폭마다 현 상황(몇번째 에폭인지, 미니배치들의 결과로 생긴 손실도들의 평균은 무엇인지, 미니배치들의 결과로 생긴 정확도들의 평균은 무엇인지, 현 상황에서 테스트 데이터에 대한 정확도는 얼마인지)을 출력한다.

학습 및 평가 데이터 획득 함수

def arrange_data(mb_size):
    global data, shuffle_map, test_begin_idx
    shuffle_map = np.arange(data.shape[0]) # 데이터의 행크기의 전역 np배열 shuffle_map생성
    np.random.shuffle(shuffle_map) # 해당 배열을 무작위로 섞는다
    step_count = int(data.shape[0] * 0.8) // mb_size 
    # 전체데이터 크기의 80%를 기준으로 미니배치 사이즈에 대한 미니배치 개수 출력 (1에폭 수행횟수)
    test_begin_idx = step_count * mb_size
    # 전역변수 test_begin_idx 설정해놓음 (추후 사용)
    return step_count
  • 미니배치의 사이즈 및 총 데이터 중 어디까지 학습 데이터로 활용하고, 어디부터 테스트 데이터로 활용할지 결정한다. 그리고 미니배치의 개수를 리턴한다.
  • 전역변수 shuffle_map에는 '0 ~ 데이터행개수'의 값을 가지는 numpy 배열을 저장한다. 해당 배열은 shuffle로 무작위로 섞어서 사용된다. 해당 numpy 배열은 추후 데이터들을 단순히 배열순서로 사용하지 않고 섞어서 활용하기 위해 사용된다.
  • 위 코드에서는 데이터의 80% 정도를 학습 데이터로 활용한다. (data.shape[0] * 0.8)
  • 따라서 그 개수(데이터 개수의 80%)에 미니배치 사이즈를 나눠서 미니배치크기의 학습을 총 몇번 수행해야 1 에폭이 되는지 파악한다.
  • 그리고 그 횟수를 step_count에 저장하고, 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:] 
    # 테스트 데이터의 독립변수(인풋 데이터), 종속변수(결과) 분할
  • 테스트 데이터가 어디까지인지 파악하고, 그를 리턴한다. 단, 독립변수(인풋 데이터)와 종속변수(결과)로 데이터를 나눠서 처리한다.
  • 앞선 arrange_data 함수에서 처리한 shuffle_map에 test_begin_idx를 활용해 fancy indexing을 하여 테스트 데이터를 찾는다.
  • shuffle_map이 섞여있는 덕분에 주어진 데이터 순서대로가 아닌 섞인 데이터를 얻어올 수 있다.
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)]] 
    # n번째 미니배치부터 다음 미니배치까지의 데이터 파악하고 학습 데이터로 리턴
    return train_data[:, :-output_cnt], train_data[:, -output_cnt:] 
    # 테스트 데이터의 독립변수(인풋 데이터), 종속변수(결과) 분할
  • 파라미터로 미니배치 사이즈와 순번을 받는다.
  • 만약 현재가 처음 미니배치에 돌입하는 것이라면 shuffle_map에서 학습 데이터 부분만을 찾아 셔플한다. (에폭마다 다른 결과를 이끌기 위함?)
  • 그리고 shuffle_map의 현 미니배치에 해당하는 부분을 슬라이싱하여 그를 기반으로 데이터를 fanxy indexing하여 학습 데이터로 명시한다.
  • 마찬가지로 독립변수(인풋 데이터)와 종속변수(결과)로 데이터를 나눠서 처리한다.

단층 퍼셉트론에 대한 순전파 함수

def forward_neuralnet(x):
    global weight, bias # 전역변수 가중치와 편향 활용
    output = np.matmul(x, weight) + bias 
    # matmul : x와 weight 행렬곱. / 그 결과에 편향 더함
    return output, x
  • 입력 데이터를 받아 해당 데이터 기반 순전파 결과값과 함께 리턴한다.
  • 입력 데이터 (독립 변수)에 가중치 행렬을 곱하고 편향을 더해 결과 행렬을 도출한다.
  • 그렇게 계산된 결과 행렬과 기존의 입력 데이터 행렬을 함께 리턴한다.
def forward_postproc(output, y):
    diff = output - y
    square = np.square(diff)
    loss = np.mean(square) # 손실함수값 리턴
    return loss, diff
  • 순전파 후처리 과정으로, mse(평균 제곱오차)값을 구하고,추후 활용을 위해 단순 오차값인 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
  • 출력층으로부터 받은 손실기울기 G_loss값(단층 퍼셉트론이므로 1을 받음)과 단순 오차값 diff를 받아 역전파에 필요한 값 'G_output'을 리턴한다.
  • G_output은 복잡해보이지만, 결국은 MSE를 기울기 혹은 편차에 대해 편미분했을 때 도출되는 공통적인 부분을 표현한 것이다. (직접 편미분해보면 2(y-y')/size 정도의 공통값이 보일것이다.)
  • 체인룰의 형태를 자세히 보여주기 위해 저런식으로 코드를 구현한 듯 하다. (아마 추후 활용성을 다양하게 하기 위함으로도 보임)
  • 해당 값을 방식으로 학습에 대한 최종 값으로 활용한다.
def backprop_neuralnet(G_output, x):
    global weight, bias # 전역변수 가중치와 편향 활용
    g_output_w = x.transpose() # x의 전치행렬, G_output과 연산이 가능하도록 한다.

    G_w = np.matmul(g_output_w, G_output)
    G_b = np.sum(G_output, axis=0)

    weight -= LEARNING_RATE * G_w # 전역변수 weight 갱신
    bias -= LEARNING_RATE * G_b # 전역변수 bias 갱신
  • 그렇게 얻은 값들을 기반으로 실질적 학습을 진행하는 함수이다.
  • 기울기인 weight는 편미분을 하면 (2(y-y')/size) * x 의 형태가 나타난다. 해당값을 학습률에 곱해 빼준다.
  • 편향인 bias는 편미분을 하면 (2(y-y')/size) 의 형태가 나타난다. 해당값을 학습률에 곱해 빼준다.

학습 실행 함수와 평가 실행함수

def run_train(x, y): 
    # 순전파 및 정확도 추출
    output, aux_nn = forward_neuralnet(x) # 신경망 연산 부분 : 순전파 결과와 기존행렬 x 리턴
    loss, aux_pp = forward_postproc(output, y) # 신경망 후처리 : 손실함수값, 단순 오차값 리턴
    accuracy = eval_accuracy(output, y) # 정확도처리

    # 역전파 과정
    G_loss = 1.0 # 단층 퍼셉트론이기 때문에 초반에 주어지는 loss는 무조건 1로 시작
    G_output = backprop_postproc(G_loss, aux_pp) # 손실함수값에 단순 차이값 기반 역전파 필요값(공통 편미분값) 준비
    # 직접적인 학습이 이뤄지는 부분
    backprop_neuralnet(G_output, aux_nn) # 기본벡터 x와 공통 편미분값을 이용해 가중치와 편향값 업데이트

    return loss, accuracy # 손실함수값과 정확도 리턴
  • 하나의 미니배치를 받아 학습을 수행하는 함수이다. 해당 학습의 손실함수값과 정확도를 리턴한다.
  • 앞서 정의한 순전파와 역전파의 과정이 순서대로 진행된다.
def run_test(x, y):
    output, _ = forward_neuralnet(x) # 신경망 연산 보조정보(기존행렬 x)는 필요없으므로 _처리
    accuracy = eval_accuracy(output, y) # 정확도처리
    return accuracy # 정확도 리턴
  • 주어진 값에 대해 테스트를 진행하고 정확도를 리턴하는 함수이다.
def eval_accuracy(output, y):
    mdiff = np.mean(np.abs((output - y)/y))
    return 1 - mdiff
  • 1에서 diff 절대값의 평균을 빼줘서 정확도를 리턴하는 함수이다.
반응형