본문 바로가기

Data Science/Machine Learning 구현

[ML] Tensorflow를 이용해 Neural Network 구현하기

반응형

🔉해당 자료 내용은 순천향대학교 빅데이터공학과 김정현 교수님의 수업자료에 기반하였으며 수업자료의 저작권 문제로 인해 수업자료를 직접 이용하지 않고 수업자료의 내용을 참고하여 본인이 직접 작성하였으므로 저작권 문제가 발생하지 않음을 필히 알려드립니다. 

 

이번 대학교 4학년 마지막 학기에 '딥러닝과 응용'이라는 수업을 듣게 됬다. 1학기에 들었던 수업인 머신러닝의 내용을 기초로 하지만 딥러닝 관련 모델에 수업 내용이 집중될 것 같다. 딥러닝을 모델하는 여러가지 언어와 툴이 있지만 해당 수업에서는 Python을 이용한 Tensorflow 1.x 버전을 사용하여 진행된다. 앞으로 이 수업에 관련된 포스팅은 이론과 실습 두 가지를 동시에 하거나 번갈아가면서 글을 게시할 예정이다.

 

또한 현재 Tensorflow 2.x 버전이 출시된 상태이며 1.x 버전과 2.x 버전 간의 문법적 차이가 존재한다. 하지만 2.x버전이 출시된지 얼마되지 않아 2.x버전으로 되어 있는 자료가 많이 존재하지 않으며 1.x 버전을 먼저 학습하고 2.x 버전으로 넘어가는게 학습이 용이하다고 한다.

 

이번 포스팅에서는 Tensorflow 1.x 버전의 기본적이면서 간단한 문법과 이를 이용해 기본적인 Neural Network을 구현하는 방법에 대해서 소개한다. Neural Network(이하, ANN)에 대한 이론을 알고싶다면 Neural Network의 기본 단위인 PerceptronANN에 대한 포스팅 2개를 참고하자.

 

출처: Wikipedia

 

텐서플로우를 사용할 때 텐서플로우가 어떤 프로세스로 동작하는지에 대해 알아둘 필요가 있다. 텐서플로우의 Tensor는 쉽게 말해서 다차원의 배열(array)를 의미한다. 텐서플로우는 이 Tensor들로 구조를 만들어 놓고 구조를 실행(run)하는 프로세스로 동작한다. 따라서 어떠한 모델을 만들던지 이 프로세스를 항상 염두에 두자. Tensor에 대해서 더 직관적인 이해를 위해 Scalar, Vector, Matrix, Tensor를 구분하는 그림을 첨부해놓았다. 

 

Tensor는 다차원의 배열이다.

 

우선 텐서플로우의 기본적인 문법에 대해서 간단하게 알고 넘어가자. 이 기본적인 문법을 이해하고 있어야 기본적인 뉴럴 네트워크, 나중에 더 나아가 CNN, RNN과 같이 복잡한 모델을 구현할 수 있기 때문이다. 

 

  • tf.constant : 변할 수 있는 값. 즉, 변수가 아닌 변화하지 않는 고정값을 할당할 때 사용한다.
  • tf.Variable : 변할 수 있는 값. 즉, 변수를 할당 시 사용한다.
  • tf.add : 텐서끼리 더하는 연산을 수행한다. 이 외에 빼기 기타 연산은 이 링크를 참고하자.
  • tf.placeholder : 값은 없지만 값을 담을 수 있는 공간, 틀을 만들어 놓는 역할을 한다. 따라서 이 문법을 사용한다면 결국 이 placeholder에 넣어줄 값을 feed_dict로 할당해주어야 한다.
  • tf.Session : 여러 텐서를 쌓아서 만든 구조를 실행하는 역할을 한다.

이제 어느정도 기본적인 문법을 알았으니 간단한 뉴럴 네트워크를 구현해보자. 먼저 간단한 input, output 데이터를 사용해서 구현해보자.(이 때, output 데이터가 존재하는 건 뉴럴 네트워크는 정답이 존재하는 Supervised Learning에 기반하기 때문이다.)

 

구현하는 코드의 목차는 다음과 같다.

 

1. tf.Variable을 이용해 간단한 뉴럴 네트워크 구현

2. tf.placeholder을 이용해 간단한 뉴럴 네트워크 구현

3. SGD를 Python으로 직접 구현해 뉴럴 네트워크 구현

4. 2차원 이상의 데이터를 이용한 뉴럴 네트워크 구현

1. tf.Variable을 이용해 간단한 뉴럴 네트워크 구현

먼저 tf.Variable을 이용해서 파라미터 초기값은 랜덤값으로 설정해 뉴럴 네트워크를 구현해보자.

 

X = [1, 2, 3]
Y = [1, 2 ,3]

# W,b 값 초기화
W = tf.Variable(tf.random_normal([1]), name='weight')
b = tf.Variable(tf.random_normal([1]), name='bias')

# Tensor를 add함
hypothesis = X * W + b
# Cost function 정의
y_train = Y
# reduce_mean 은 평균값 연산 함수
cost = tf.reduce_mean(tf.square(hypothesis - y_train))
# SGD 정의
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)
# 위에서 정의한 SGD로 cost값 최소화하도록 정의
train = optimizer.minimize(cost)

sess = tf.Session()
# 변수 초기화
sess.run(tf.global_variables_initializer())
# Run & update graph
for step in range(2001):
    # cost값 최소하는 SGD 실행
    sess.run(train)
    if step % 20 == 0:
        print(f"##Step: {step}\n##Cost value: {sess.run(cost)}\n##Weight: {sess.run(W)}, Bias: {sess.run(b)}\n")
sess.close()

 

여기서 만든 텐서들을 실행하는 과정에서 with 문을 사용해서 다음과 같이 작성할 수 있다. with문을 사용한다면 위 코드의 마지막 부분처럼 sess.close()로 세션을 닫아줄 필요없이 자동으로 닫힌다.

 

X = [1, 2, 3]
Y = [1, 2 ,3]

# W,b 값 초기화
W = tf.Variable(tf.random_normal([1]), name='weight')
b = tf.Variable(tf.random_normal([1]), name='bias')

# Tensor를 add함
hypothesis = X * W + b
# Cost function 정의
y_train = Y
# reduce_mean 은 평균값 연산 함수
cost = tf.reduce_mean(tf.square(hypothesis - y_train))
# SGD 정의
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)
# 위에서 정의한 SGD로 cost값 최소화하도록 정의
train = optimizer.minimize(cost)

# with 구문통해서 sess.close() 안쓰고 run, update하기
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    for step in range(2001):
        sess.run(train)
        if step % 20 == 0:
            print(f"##Step: {step}\n##Cost value: {sess.run(cost)}\n##Weight: {sess.run(W)}, Bias: {sess.run(b)}\n")

2. tf.placeholder을 이용해 간단한 뉴럴 네트워크 구현

다음은 placeholder로 값이 비어있는 틀을 먼저 설정하고 추후에 feed_dict로 데이터 값을 부여해주는 코드를 살펴보자. 그리고 Weight 파라미터값에 따라 Cost값이 어떻게 줄어드는지 그래프도 그려보자.

 

import tensorflow as tf
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings(action='ignore')

X = [1,2,3]
Y = [1,2,3]

# W는 값이 비어있는 placeholder로 할당
W = tf.placeholder(tf.float32)

# 선형식 정의
hypothesis = X * W

# cost값 정의
cost = tf.reduce_mean(tf.square(hypothesis - Y))

# SGD 함수 사용하지 않고 직접 python으로 구현
with tf.Session() as sess:
    # plot그리기 위해서 W, b값 계속 축적해놓을 빈 리스트 할당
    W_val = []
    cost_val = []
    for i in range(-30, 50):
        # 반복문 돌려서 새로운 W값 반복적으로 설정
        feed_W = i * 0.1
        # 2개 이상 객체를 할당할 때 run함수에 리스트로 묶어주기, W는 현재 값이 비어있는 placeholder이기 때문에 feed_dict로 W에 값 할당
        curr_cost, curr_W = sess.run([cost, W], feed_dict={W : feed_W})
        W_val.append(curr_W)
        cost_val.append(curr_cost)
plt.figure(figsize=(10,10))
plt.plot(W_val, cost_val)
plt.title('Plot of Cost & Weight value', fontsize=20, color='blue')
plt.xlabel('Weight', fontsize=17)
plt.ylabel('Cost', fontsize=17)
plt.show()

 

결과 그래프는 다음과 같다.

 

Weight값에 따른 Cost 값의 변화

3. SGD를 Python으로 직접 구현해 뉴럴 네트워크 구현

다음은 SGD(Stochastic Gradient Descent) 즉, 경사하강법을 텐서플로우에서 한 번에 제공하는 메소드를 사용하지 않고 Python과 텐서플로우의 일부 문법을 이용해서 직접 구현해 뉴럴 네트워크를 구현해보자.

 

import tensorflow as tf

x_data = [1,2,3]
y_data = [10,12,14]

# 1차원의 벡터로 랜덤한 값을 변수로 할당
W = tf.Variable(tf.random_normal([1]), name='weight')
X = tf.placeholder(tf.float32)
Y = tf.placeholder(tf.float32)

hypothesis = X * W
cost = tf.reduce_mean(tf.square(hypothesis - Y))

# SGD를 python으로 구현하기
learning_rate = 0.1
# gradient: cost값을 미분하여 평균취한 값 - 이때, 상수 2배는 안해주어도 상관없음
gradient = tf.reduce_mean((W * X - Y) * X)
descent = W - learning_rate * gradient
# 업데이트된 W값을 새로운 변수에 할당(.assign함수는 tf.Variable 객체의 함수 종류 중 하나)
update = W.assign(descent)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for step in range(21):
        # W값을 SGD방식으로 업데이트 시켜주면서 X, Y값에 x_data, y_data feed시키기
        sess.run(update, feed_dict={X: x_data, Y: y_data})
        # cost출력시키기 위해서 run시켜주면서 X,Y에 데이터 feed 시키기
        print(step, sess.run(cost, feed_dict={X: x_data, Y: y_data}), sess.run(W))
        print('-'*50)

4. 2차원 이상의 데이터를 이용한 뉴럴 네트워크 구현

마지막으로 1차원의 데이터가 아닌 좀 더 복잡한 2차원의 데이터인 상태에서 뉴럴 네트워크를 구현해보자. 1차원 데이터일 때와 다른 점은 텐서를 쌓을 때 데이터의 차원 형태를 인자로 넣어주어 데이터 형태를 기계가 잘 인식하도록 해주는 것이다.

 

import tensorflow as tf

x_data = [[73, 93, 75],
         [93, 88, 93],
         [89, 91, 90],
         [96, 98, 100],
         [73, 66, 70]]
y_data = [[152], [185], [180], [196], [142]]

# shpae=[None, 3] : 행(데이터) 개수는 유동적으로 바뀌도록, feature(column) 개수는 3개
X = tf.placeholder(tf.float32, shape=[None, 3])
Y = tf.placeholder(tf.float32, shape=[None, 1])

# W값은 3개의 요소로 이루어져 있는 2차원 column vector임. 왜냐하면 1행 3열의 행 벡터의 X와 내적해주려면 W는 3행 1열의 열 벡터이기 때문
# 위 두개의 내적결과 2차원으로 이루어진 요소가 1개인 벡터. Ex. [[15]] 이런식..
W = tf.Variable(tf.random_normal([3, 1]), name='weight')
# 위 W값의 shape인 요소가 1개인 2차원 array와 1차원 array와 더하기는 가능!(내적 시에만 shape를 맞춰줘야 함!)
b = tf.Variable(tf.random_normal([1]), name='bias')

# matmul인자가 바뀌면 안 됨! 행 벡터와 열 벡터 순의 내적이 되어야 요소가 1개인 벡터값으로 도출
hypothesis = tf.matmul(X, W) + b

cost = tf.reduce_mean(tf.square(hypothesis - Y))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1e-5)
train = optimizer.minimize(cost)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    for step in range(2001):
        # train은 출력하진 않지만 run은 시켜야 하므로 _(언더스코어를 사용해) 변수 할당
        cost_val, hy_val, _ = sess.run([cost, hypothesis, train],
                                      feed_dict={X: x_data, Y: y_data})
        if step % 20 == 0:
            print(step, 'Cost: ', cost_val, '\nPrediction:\n', hy_val)
            print('-'*50)

 

필자는 위 코드를 처음 접할 때, W 변수에 초기값을 설정해줄 때 [3, 1] 이 부분이 이해가 되지 않았다. 위 주석에도 설명을 달아놓았지만 혹여나 이해가 안가시는 분들이 있을 수 있기에 다시 한 번 설명해보려 한다.

 

이 부분을 이해하기 위해서는 input 데이터인 X가 파라미터 W가 어떤 형태로 계산되는지 구조를 이해하고 있어야 한다. 다음 그림을 살펴보자.

 

https://kevinthegrey.tistory.com/106

 

위 그림과 같이 X는 [x1, x2, x3]처럼 1행 3열의 행 벡터(Row vector) 형태로, 이 데이터가 여러개로 구성되어 있는 형태로 구성되어 있다. 그런데 이 1행 3열의 행 벡터로 이루어져 있는 input 데이터내적하여 요소가 한 개인 벡터값으로 출력시키려면 3행 1열로 이루어져 있는 열 벡터(Column vector)의 형태로 존재해야 한다. input 데이터인 [x1, x2, x3]내적하려는 값은 W값이며 따라서 W값에 tf.random_normal( [3, 1] )로 shape를 설정해준 것이다. [3, 1]의 3은 행 개수, 1은 열 개수를 의미하며 결국 3행 1열로 이루어진 열 벡터 형태를 의미한다.

(참고로, 위 예시 그림에서 3행 2열로 이루어져있는 그림 속 w값은 신경쓰지 않는 상태로 이해하자. 단지 행 벡터와 열 벡터를 내적해야 하나의 요소를 가진 값으로 출력이 되는 것을 설명하기 위함이다.)

 

반응형