코파일럿과 함께 구현하는 뉴럴넷

코파일럿과 함께 구현하는 뉴럴넷

깃허브가 제공하는 코파일럿을 사용하면 편리하게 코드를 구현할 수 있다. 사용자가 의도하는 코드를 스스로 알아서 뒷 부분의 코드를 제안한다. 코멘트에 내용을 적으면 그에 해당하는 코드를 제안하기도 한다. 이렇게 코드 작성을 도와주는 부분은 딥러닝 구현에도 적용할 수 있다. 여기서는 숫자 예측 예제를 코파일럿과 함께 구현하는 방법을 익힌다. 


1. 비주얼 스튜디오 코드 환경을 이용한 실습


코파일럿을 이용한 잭스를 이용한 예제 구현은 Visual studio code에서 Copilot 확장자를 설치해서 작성한 사례이다. 코파일럿의 표준 사용 방법 중에 한가지이고 기능도 잘 되어 있어 아래에서는 이 환경에서 구현하는 방법을 설명한다.  


① 잭스 패키지에서 필요한 모듈을 임포트한다.


코파일럿은 한두개의 단어만 적어도 동작하기도 한다. 따라서 아래와 같이 코멘트를 입력하고 코파일럿이 문장을 제안하는지 여부를 보기 위해 잠시 기다린다. 


# Importing 


이렇게만 코멘트를 입력해도 아래와 같이 이후 문장을 제안하기 시작한다. 코파일럿은 사용자의 특성을 이해하고 사용자마다 다른 문장을 제안하니 여기 예제와 다른 제안을 할 수 있음을 주의할 필요가 있다. 따라서 여기서는 제안한 내용보다는 어떻게 제안을 받을 수 있는지 그 방법에 초점을 맞추어 설명을 할 것이다. 


이제 아래 노란 형광색 바탕의 내용과 같이 코파일럿이 다음에 나올 코멘트 문장을 제안을 하게 된다. 


# Importing Libraries 


위에서 Libraries를 코파일럿이 제안한 문장이다. 이때 Tab을 치게되면 이 문장의 사용을 승인하게 된다. Tab을 친 이후는 다음과 같이 변경된다. 

 

# Importing Libraries


이제 잭스에 있는 라이브러리를 임포트하는 문장의 앞부분을 작성한다. 


from jax import grad


앞부분의 from jax만 입력했는데 코파일럿이 뒷 부분인 import grld를 제안해서 Tab을 입력해 제안을 수용했다.


그 다음 줄에 잭스의 numpy 모듈을 불러오기 위해 역시 앞에 문장만 입력하고 나니 뒷 부분을 자동으로 제안한다. 이렇게 제안하는 내용은 일반적으로 잭스 사용자들이 많이 사용하는 문장 또는 현재 사용자의 이전 코드에서 사용했던 문장을 보고 코파일럿이 미리 보여주는 것이다. 


from jax import numpy as jnp


② 학습과 검증에 사용할 데이터를 준비한다. 


여기서 학습과 검증에 사용할 데이터는 1.3절에서 했듯이 수식을 통해 간단히 만든다. 먼저 코멘트에 데이터를 준비한다라고 적기 위해 #를 입력하자마자 다음 문장들이 코파일럿에 의해 제안되었다. 


# Preparing data

 

그 문장을 적고나면 바로 다음 문장을 자동으로 제안한다. 개발자들은 이미 작성한 코드들이  유사하다면 동일한 코멘트 아래에서는 이전에 작성한 코드와 동일한 내용을 작성한다는 전제를 이용하는 것으로 보인다. 


x = jnp.array([1.0, 2.0, 3.0])
y = 2.0*x + 1.0


제안을 받아 드린 뒤에 수정도 가능하다. 주어진 제안 코드를 수정해서 입력 데이터를 좀 더 추가하도록 문장을 수정해 보자. 더 추가하기 위해 3.0 다음에 콤마를 입력하자 말자 다른 코파일럿은 잽싸게 아래와 같이 다른 제안을 하게 된다. 


x = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
y = 2.0*x + 1.0


학습은 두 개의 입력과 출력 데이터 쌍으로 가능하고 검증은 데이터가 많을수록 평균적인 성능을 확인할 수 있기 때문에 추천한 10개 데이터를 모두 사용하기로 하자. 


이번에는 코멘트를 한글로 적어 보았다. 


# 학습 데이터와 검증 데이터 나누기

  

‘나’까지 적고 나니 맨끝에 ‘누기’를 추가할 것을 제안했다. 영어뿐 아니라 한국어 문장의 제안도 제법 쓸만하다. 


다음 줄로 가기위해 엔터를 치자마자 코파일럿이 N = 5를 제안했다. N = 2를 사용해도 되지만 연습 차원에서 5를 사용하기로 하자. 그리고 다시 엔터를 치면 두 데이터를 분리하는 문장을 제안한다. 이렇게 적합한 코드가 제안되는 것은 코파일럿 소개 자료에 나와 있듯이 코드 위에 작성한 코멘트의 내용에 적합한 코드를 맞추기 위한 것으로 보인다. 


N = 5
x_train, y_train, x_test, y_test = x[:N], y[:N], x[N:], y[N:]


위에서 두 번째 문장은 데이터를 분류하기 전에 상호 섞기를 진행하게 만들기 위해 사이킷런의 train_test_split라는 함수를 이용하기로 하자. 이를 위해서는 앞서 임포트했던 부분에서 관련 함수를 임포트해야 한다. 


# I. Importing Libraries
from jax import grad
from jax import numpy as jnp
from sklearn.model_selection import train_test_split


여기서 한 줄 더 추가해 앞부분을 입력하니 뒷 부분인 import train_test_split을 자동으로 제안하여 탭키를 통해 수용하였다.


그리고 위에서 작성했던 분류하는 다음 문장을 어느 정도 수정하니 뒷 부분을 아래와 같이 제안한다. 


x_train, y_train, x_test, y_test = train_test_split(x, y, test_size=N)


우리는 N을 학습 데이터의 길이로 생각했지만 코파일럿은 나름 검증 데이터의 길이로 인식했다. 코드의 흐름상 상관은 없지만 이런 경우를 그냥두게 되면 개발자와 코파일럿의 이해가 달라 전체 코드의 흐름에 방해가 되거나 나중에 잠재적 오류를 유발할 수 도 있어 보인다. 이를 방지하는 것을 훈련하는 차원에서 N을 개발자의 취지에 맞게 len(x) - N으로 변경한다. 


x_train, y_train, x_test, y_test = train_test_split(x, y,
    test_size=len(x)-N) 


③ 모델 및 손실 함수를 구현한다. 


모델을 함수를 구현하기 위해 함수의 프로토타입까지 입력하고 나니 내부 내용을 코파일럿이 자동으로 작성했다. 


# Build a model architecture
def model(param, x):
    w, b = param
    return w*x + b


손실 함수는 인자를 입력하기 전에 나머지를 코파일럿이 제안했다. 


def loss(param, x, y):
    y_hat = model(param, x)
    return jnp.mean((y_hat - y)**2)


④ 모델의 학습을 위해 가중치와 편향값에 대한 미분 함수를 구한다. 


미분 함수의 결과를 담는 변수명을 적고나면 그 다음 코드는 코파일럿이 제안한다. 


# Obtain derivative of the loss function
dJ_dpram = grad(J)


⑤ 만든 모델의 계수들을 주어진 데이터로 학습시킨다.


학습시키기 위해 필요한 초기값 설정은 코파일럿이 코멘트 이후에 바로 제안했고 탭을 입력해 그대로 사용하기로 한다. 


# Train a model
w, b = 0.0, 0.0
N_epoch = 1000


먼저, 제안 내용을 w, b가 아닌 param을 초기화 하도록 수정했다. 또한 제안은 되지 않았지만 중요한 상수인 학습률은 learning_rate = 0.01로 지정하도록 추가 했다. 수정과 추가를 통해 변경된 코드는 아래와 같다. 


# Train a model

param = 0.0, 0.0
N_epoch = 1000
learning_rate = 0.01


이제 학습이 진행되고 경과를 보여주는 코드를 작성할 시점이다. 엔터를 입력하면 다음과 같이 관련 코드를 코파일럿이 제안한다. 참고로 제일 아래에 프린트 문은 문장이 길어서 잘라서 표현하도록 제안한 코드를 약간 수정을 했다. 


for epoch in range(N_epoch):
    for x_i, y_i in zip(x_train, y_train):
        dl_dw, dl_db = dJ_dpram(param, x_i, y_i)
        param[0] -= learning_rate * dl_dw
        param[1] -= learning_rate * dl_db
    l = J(param, x_train, y_train)
    err = J(param, x_test, y_test)
    if epoch % 100 == 0:
        print(f'epoch {epoch}: test loss {err:.3e}, train loss {l:.3e}',
        f'w {param[0]:.3f}, b {param[1]:.3f}')


일단 전체 구조는 비슷하기 때문에 제안한 코드를 탭을 입력해 수락했다. 하지만 아래와 같이 몇 가지는 수정이 필요해 변경을 했다. 


for epoch in range(N_epoch):
    dl_dw, dl_db = dJ_dpram(param, x_train, y_train)
    param[0] -= learning_rate * dl_dw
    param[1] -= learning_rate * dl_db
    l = J(param, x_train, y_train)
    err = J(param, x_test, y_test)
    if epoch % 100 == 0 or epoch == N_epoch-1:
        print(f'epoch {epoch}: train loss {l:.3e}, test loss {err:.3e}',
        f'w {param[0]:.3f}, b {param[1]:.3f}')


우선 개별 인자 단위로 그라디언트를 구하는 반복문은 제거를 했고, 이에 따라 dJ_param()에서 사용하는 입력과 출력에 해당되는 변수도 x_train과 y_train으로 변경했다. 또한 제일 마지막 에폭 결과도 출력할 수 있도록 아랫 쪽에 있는 if 문의 조건에 epoch == N_epoch-1도 추가했다. 


⑥ 코드파일럿이 작성한 코드 검증과 디버깅하기


코드는 짜고 나면 늘 검증과 디버깅 과정이 필요하다. 그러나 이 과정은 좀 다르다. 개발자 본인이 아닌 코파일럿이 작성한 오류가 있기 때문에 예상 외의 검증 과정이 필요하다. 


우선 현재 상태에서 실행을 하자. 실행 후 결과는 정답과는 거리가 멀게 아래처럼 나온다. 


<결과>

epoch 0: train loss 1.112e+01, test loss 4.711e+01 w 0.576, b 0.096

epoch 100: train loss 7.834e+00, test loss 4.502e+01 w 0.354, b 1.884

epoch 200: train loss 6.394e+00, test loss 4.788e+01 w 0.203, b 3.067

epoch 300: train loss 5.762e+00, test loss 5.159e+01 w 0.102, b 3.850

epoch 400: train loss 5.485e+00, test loss 5.485e+01 w 0.036, b 4.369

epoch 500: train loss 5.364e+00, test loss 5.735e+01 w -0.008, b 4.713

epoch 600: train loss 5.311e+00, test loss 5.916e+01 w -0.037, b 4.940

epoch 700: train loss 5.288e+00, test loss 6.043e+01 w -0.057, b 5.091

epoch 800: train loss 5.277e+00, test loss 6.130e+01 w -0.069, b 5.190

epoch 900: train loss 5.273e+00, test loss 6.189e+01 w -0.078, b 5.256

</결과>


학습이 진행되어도 오류가 미비한 수준으로 아주 조끔씩 줄어들고 w, b 값은 기대하는 값과는 다른 방향으로 바뀌어 간다. 2에 가까워져야 할 w는 점점 값이 작아져 0 이하로 가고 있고 1에 가까이 가야할 b 값은 자꾸 커져 5가 넘었다.


코드를 천천히 살펴본 결과, 위에서 코파일럿과 함께 작성한 아래 줄이 문제였다. 


x_train, y_train, x_test, y_test = train_test_split(x, y,
    test_size=len(x)-N)  


출력에 대한 변수들의 순서는 개발자가 정했는데 제안한 코드에서 리턴하는 변수의 순서가 이 순서와도 다름에도 불구하고 그대로 제안했고 우리는 그걸 확인없이 수락했다. 


코파일럿은 개발자가 입력한 코드는 손되지 않고 나머지 코드를 제안한다. 그러다보니 간혹, 개발자 코드와 제안한 코드가 맞지 않는 경우가 생기게 된다. 더 영리한 코파일럿이라면 자신이 경험하지 않았더라도 개발자의 코드에 맞게 자신의 코드의 리턴값을 변경하든지 아니면 그런게 복잡하거나 어려운 상황이라면 개발자에게 작성한 코드를 변경하도록 제안해야 되지 않나 생각이 된다.    


위의 코드를 뒤에 나오는 train_test_split()의 리턴 값 출력 순서에 맞게 다시 아래와 같이 수정한다. 


x_train, x_test, y_train, y_test = train_test_split(x, y,
    test_size=len(x)-N)   


수정한 이후에 다시 수행하면 이제 아래와 같이 제대로된 결과가 나온다. 


<결과>

epoch 0: train loss 1.961e+01, test loss 2.805e+01 w 1.328, b 0.212

epoch 100: train loss 4.169e-02, test loss 3.180e-02 w 2.064, b 0.593

epoch 200: train loss 1.555e-02, test loss 1.186e-02 w 2.039, b 0.752

epoch 300: train loss 5.801e-03, test loss 4.424e-03 w 2.024, b 0.848

epoch 400: train loss 2.164e-03, test loss 1.650e-03 w 2.015, b 0.907

epoch 500: train loss 8.072e-04, test loss 6.156e-04 w 2.009, b 0.943

epoch 600: train loss 3.011e-04, test loss 2.296e-04 w 2.005, b 0.965

epoch 700: train loss 1.123e-04, test loss 8.566e-05 w 2.003, b 0.979

epoch 800: train loss 4.189e-05, test loss 3.195e-05 w 2.002, b 0.987

epoch 900: train loss 1.562e-05, test loss 1.191e-05 w 2.001, b 0.992

epoch 999: train loss 5.887e-06, test loss 4.488e-06 w 2.001, b 0.995

</결과>


2. 전체 코드

이 절에서 실습한 코드 전체는 다음과 같다. 


#%% ① 잭스 패키지에서 필요한 모듈을 임포트한다.
# Importing Libraries
from jax import grad
from jax import numpy as jnp
from sklearn.model_selection import train_test_split

#%% ② 학습과 검증에 사용할 데이터를 준비한다.
# Preparing data
x = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
y = 2.0*x + 1.0

# 학습 데이터와 검증 데이터 나누기
N = 5
x_train, x_test, y_train, y_test = train_test_split(x, y,
    test_size=len(x)-N) 

#%% ③ 모델의 학습을 위해 가중치와 편향값에 대한 미분 함수를 구한다.
# Build a model architecture
def model(param, x):
    w, b = param
    return w*x + b

def J(param, x, y):
    y_hat = model(param, x)
    return jnp.mean((y_hat - y)**2)

#%% ④ 파라미터에 대한 미분 함수를 구한다.
# Obtain derivative of the loss function
dJ_dpram = grad(J)

#%% ⑤ 만든 모델의 계수들을 주어진 데이터로 학습시킨다.
# Train a model
param = [0.0, 0.0]
N_epoch = 1000
learning_rate = 0.01
for epoch in range(N_epoch):
    dl_dw, dl_db = dJ_dpram(param, x_train, y_train)
    param[0] -= learning_rate * dl_dw
    param[1] -= learning_rate * dl_db
    l = J(param, x_train, y_train)
    err = J(param, x_test, y_test)
    if epoch % 100 == 0 or epoch == N_epoch-1:
        print(f'epoch {epoch}: train loss {l:.3e}, test loss {err:.3e}',
        f'w {param[0]:.3f}, b {param[1]:.3f}')


깃허브에서 이 절의 예제를 아래 파일명으로 내려받을 수 있다. 

  • 깃허브 위치: https://github.com/jskDr/jax_copilot

  • 파이썬 파일 : ch1_ex3_jax_copilot.py

Comments

Popular posts from this blog

Missing Lester Young~~

Jeff Koons and Ciciolina as Kitsch artists

This is my Google blog.