[Tensorflow] Chapter 3. 텐서플로 2.0 시작하기
본 글은 ‘시작하세요! 텐서플로 2.0 프로그래밍’ 을 바탕으로 작성되었습니다.
Chapter 3. 텐서플로 2.0 시작하기
텐서플로 기초
현재 인공지능(혹은 딥러닝)은 각계에서 매우 각광받고 있는 분야이지만, 인공지능의 겨울이라고 불리는 암흑기를 두 번 겪기도 했습니다. 그 중 첫번째는 신경망을 구성하는 뉴런의 원조격인 퍼셉트론 perceptron의 XOR 문제가 큰 비중을 차지합니다. 이 장에서는 Tensorflow의 기초를 알아보며 신경망 네트워크를 만들어보도록 하겠습니다.
난수 생성
신경망은 많은 숫자로 구성된 행렬이라고 간단히 정의해볼 수 있습니다. 입력을 어떠한 행렬에 넣으면 출력을 얻게 되며 우리는 잘 작동하는 행렬을 만드는 것이 목표입니다. 처음에는 이 행렬을 구성하는 숫자를 랜덤한 값으로 지정해야 합니다. 맨 처음의 초깃값을 잘 지정하는 초기화에 대한 연구도 많이 있지만 지금은 단순하게 무작위로 생성하겠습니다.
tf.Tensor([0.36214244], shape=(1,), dtype=float32)
난수를 생성하는 코드
난수를 생성하는 방법은 위와 같습니다. 1번 행은 같은 결과가 나오기 위해 무작위성을 고정해준 것입니다. 앞으로는 같은 결과가 나올 수 있도록 무작위성을 지닌 모든 코드에 seed를 126으로 부여할 것이지만 글에서는 생략하겠습니다.
tf.random.uniform 함수에서는 균일 분포로 난수을 얻습니다. 첫번째 인자 [1] 은 결과값의 차원 수를 나타내며, 두 번째 인자와 세 번째 인자는 각각 최솟값과 최댓값을 의미합니다.
우리는 이 코드로 무작위 값 0.36214244를 얻었으며, shape는 (1,) 입니다. 파이썬에서는 변하지 않는 값에 대해 튜플 tuple 자료형을 사용합니다. 원소의 개수가 한 개인 경우에도 (1,) 과 같이 자료형이 튜플임을 나타냅니다. 만약 데이터가 [[2],[3]]이라면 shape는 [2,1], [[1,2],[3,6]] 이라면 shape는 [2,2] 를 갖습니다. 바깥쪽부터 안쪽까지 원소의 개수를 표현합니다.
마지막 dtype은 자료형입니다. default값으로 float32를 사용하며 32비트의 부동소수점 수를 의미합니다.
tf.Tensor([-0.6980922 -1.2426193 0.71065265 0.8911458 ], shape=(4,), dtype=float32)
정규 분포로부터 shape [4]의 난수 생성
위 코드는 균일 분포가 아닌 정규 분포 normal distribution로부터 shape [4]의 난수를 추출한 코드입니다. tensor.random.normal의 두 번째 인자와 세 번째 인자는 각각 평균과 표준 편차를 의미합니다.
뉴런 만들기
이제 신경망의 가장 기본적인 구성요소인 뉴런을 만들 것입니다. 뉴런은 입력을 받아 출력을 반환하는 단순한 구조입니다. 신경망은 뉴런이 여러 개 모여 레이어를 구성한 후 이 레이어가 다시 모여 구성된 형태입니다. 가장 기본적인 단위인 뉴런은 다시 입력, 가중치, 활성화함수, 출력으로 구성됩니다. 우리는 이 중에서 가중치를 학습하게 됩니다. 학습이 잘 된다는 것은 가중치를 잘 조정해서 원하는 값에 가까운 출력을 얻어낸다는 것을 의미합니다.
많이 쓰이는 활성화 함수로는 시그모이드 sigmoid나 ReLU가 있습니다. 활성화 함수를 사용하지 않는다면 아무리 신경망을 깊고 복잡하게 만들어도 결국 선형 결합에 지나지 않습니다. 이는 즉 하나의 행렬을 곱해주는 결과가 나온다는 의미입니다. 활성화 함수는 함수에 비선형성을 부여하기 위해 주로 사용됩니다. 활성화 함수에 대해서는 나중에 더 자세히 알아보도록 하고 일단 여기서는 시그모이드 함수를 구현해보겠습니다. 시그모이드 함수는 출력을 0에서 1 사이의 값으로 제한하기 때문에 이번에 구현할 AND, OR, XOR 등의 연산을 다루기에 더 적합합니다.
sigmoid 함수 그래프
시그모이드 함수를 그림으로 표현하면 위와 같습니다. 입력을 받게 되면 그 값에 따라 0에서 1 사이의 범위의 값으로 반환합니다.
sigmoid 함수 구현
위와 같이 math 라이브러리의 math.exp() 를 활용해서 시그모이드 함수를 구현했습니다. 이제 뉴런의 입력과 출력을 정의해보겠습니다.
0.3322353423606972
입력과 출력 정의
입력인 x는 1, 가중치는 정규 분포에서 무작위로 생성한 w를 넣었습니다. 출력으로는 입력과 가중치를 곱한 값이 나옵니다. 출력으로 나온 0.3322와 정답 y의 차이인 $0 - 0.3322$를 에러 라고 부릅니다.
여기서 뉴런이란 결국 w 값입니다. 우리는 에러를 0에 가까워지게 가중치 w를 학습해야 합니다. 여기서 우리는 경사 하강법 Gradient Descent을 사용하게 됩니다.
\[\text{w} = \text{w} + \text{x} \times \alpha \times \text{error}\]가중치 w에 입력과 학습률 learning rate과 에러를 곱한 값을 더해주는 방법입니다. 여기서 학습률은 주로 $\alpha$로 표기합니다. w를 업데이트하는 정도를 의미하는데 큰 값을 설정하면 학습이 빨리 되지만 적정한 수치를 벗어날 수 있고, 너무 작은 값으로 설정하면 학습 속도가 느려지게 됩니다. 여기서는 $\alpha = 0.1$로 설정하겠습니다. 이제 결과를 코드로 확인하겠습니다.
99 -0.08819347662195508 0.08819347662195508
199 -0.04825551451312346 0.04825551451312346
299 -0.03295457590473006 0.03295457590473006
399 -0.024954816654965642 0.024954816654965642
499 -0.020056225492914317 0.020056225492914317
599 -0.016754433264190724 0.016754433264190724
699 -0.014380598734003183 0.014380598734003183
799 -0.012592808694543352 0.012592808694543352
899 -0.011198504379077398 0.011198504379077398
999 -0.010080989000057751 0.010080989000057751
경사하강법 적용
for 반복문을 활용하여 1,000번동안 for 문 안의 내용을 반복합니다. error는 y에서 output을 빼서 구합니다. 이렇게 구한 error를 다시 경사 하강법의 업데이트 식에 넣어 w를 다시 계산합니다. if문을 통해 100번 반복 될 때마다 결과값을 출력합니다. 반복할수록 error값이 0에 점점 더 가까워지고, output 역시 실제 값인 0에 가까워지도록 w가 학습됩니다.
99 0.5 0.5
199 0.5 0.5
299 0.5 0.5
399 0.5 0.5
499 0.5 0.5
599 0.5 0.5
699 0.5 0.5
799 0.5 0.5
899 0.5 0.5
999 0.5 0.5
위 코드는 입력으로 0을 넣었을 때 출력으로 1을 얻는 뉴런입니다. $\text{x} = 0$이기 때문에 w에 더해지는 값이 없어 w가 업데이트 되지 않습니다. 즉 학습이 전혀 되지 않습니다. 우리는 이런 경우를 방지하기 위해 편향 bias을 넣어줄 수 있습니다.
편향은 입력으로 x가 아니라 한 쪽으로 치우친 고정된 값을 받습니다. 편향은 주로 b로 표현합니다. 편향으로 보편적으로 쓰이는 1을 입력으로 넣어주겠습니다. 편향은 w처럼 난수로 초기화되며 뉴런에 더해져서 출력을 계산합니다.
99 0.11439745990457373 0.8856025400954263
199 0.055581794825595776 0.9444182051744042
299 0.03627661815291239 0.9637233818470876
399 0.02683146637354994 0.9731685336264501
499 0.021257275966082623 0.9787427240339174
599 0.017587359972614847 0.9824126400273852
699 0.014991437528453444 0.9850085624715466
799 0.013059631249431769 0.9869403687505682
899 0.011566721691299464 0.9884332783087005
999 0.010378754622208497 0.9896212453777915
편향을 나타내는 b가 추가되었고 output을 계산할 때 w * x에 b를 더해줍니다. w와 b는 각자 학습됩니다. 그 결과 output이 실제 값인 1에 가까워지도록 학습되면서 error는 0에 가까워집니다. 학습이 잘 되고 있음을 확인할 수 있습니다.
네트워크 구현
첫 번째 신경망 네트워크 : AND
앞서 살펴보았던 내용을 활용해 신경망 네트워크를 만들어보도록 하겠습니다.
입력 1 | 입력 2 | AND 연산 |
---|---|---|
True | True | True |
True | False | False |
False | True | False |
False | False | False |
AND 연산은 입력이 모두 참 일 때만 참이 되고 나머지 경우엔 거짓이 됩니다. 파이썬에서는 True가 1, False가 0으로 변환됩니다. 위 표의 값을 입력으로 사용하여 AND 연산자를 수행하는 신경망 네트워크를 만들겠습니다.
\[\text{Y} = \text{X}_1 \ast\text{w}_1 + \text{X}_2 \ast\text{w}_2 + \text{1}\ast\text{b}\]위와 같이 하나의 뉴런은 여러 개의 입력을 받을 수 있습니다. 이와 달리 편향은 한 개만 사용합니다. Y를 구한 후 활성화함수에 넣으면 뉴런의 출력을 계산할 수 있습니다.
199 -0.12080349552026862
399 -0.06924597626624415
599 -0.048460118233432055
799 -0.037159076495028705
999 -0.03007404734492967
1199 -0.02522657918555745
1399 -0.021708534724218496
1599 -0.01904093682279571
1799 -0.01694959652037591
1999 -0.015268364487532552
일단 넘파이라는 모듈을 불러와서 array라는 자료형을 활용했습니다. 파이썬의 리스트보다 빠르며 딥러닝에서도 자주 활용되는 자료형입니다. 여기서는 w를 업데이트하는 부분의 계산을 편리하게 하기 위해 활용했습니다. 파이썬의 기본 자료형인 list에 상수를 곱하면 정수인 경우 그 값만큼 리스트의 원소를 반복하고, 0.01과 같은 값을 곱하면 에러가 발생합니다. 반면 array는 각 원소에 상수를 곱하게 됩니다. 이를 각 원소에 대한 element-wise 연산이라고 합니다.
입력의 수가 4배로 많아졌기 때문에 수렴에 더 많은 연산이 필요합니다. error의 합인 error_sum이 점점 줄어드는 것을 확인할 수 있습니다. 그럼 실제로 이렇게 학습된 네트워크가 어떻게 작동하는지 확인해보겠습니다.
X: [1 1] Y: [1] Output: 0.9643550986660553
X: [1 0] Y: [0] Output: 0.025256390835580837
X: [0 1] Y: [0] Output: 0.02533480842565804
X: [0 0] Y: [0] Output: 2.489388539984249e-05
w: tf.Tensor([6.947768 6.9509487], shape=(2,), dtype=float32)
b: tf.Tensor([-10.600863], shape=(1,), dtype=float32)
이와 같이 출력이 실제 값과 가까이 나오고 있는 것을 알 수 있습니다. 이처럼 AND 연산을 수행하는 네트워크를 학습시켰습니다. 학습된 w와 b의 값도 확인할 수 있습니다.
두 번째 신경망 네트워크 : OR
OR 연산을 수행하는 네트워크의 경우는 AND와 거의 같습니다. y 부분의 실제값 부분만 수정하면 만들 수 있습니다.
입력 1 | 입력 2 | AND 연산 |
---|---|---|
True | True | True |
True | False | True |
False | True | True |
False | False | False |
결과부터 말씀드리면 AND 네트워크와 마찬가지로 잘 학습됩니다. 직접 해보시면 좋을 것 같습니다. 따로 코드를 첨부하지는 않겠습니다.
세 번째 신경망 네트워크 : XOR
문제와 원인
XOR은 OR과 비슷하지만 한 가지 차이가 있습니다. 홀수 개의 입력이 True 일때만 결괏값이 True가 됩니다. 입력이 2개인 경우 입력 2개의 값이 서로 다를 경우 True가 됩니다.
입력 1 | 입력 2 | AND 연산 |
---|---|---|
True | True | False |
True | False | True |
False | True | True |
False | False | False |
이제 여태까지 했던 것과 같이 XOR 연산을 수행하는 네트워크를 구현해보겠습니다.
199 -0.008831319893969791
399 -0.0003592029264150032
599 -1.4606531231309283e-05
799 -5.993775218371411e-07
999 3.7228424787372205e-09
1199 3.722842145670313e-09
1399 3.722842145670313e-09
1599 3.722842145670313e-09
1799 3.722842145670313e-09
1999 3.722842145670313e-09
코드는 역시 y의 실제값을 제외하고는 같습니다. 에러 값이 점점 줄어들다가 어느 순간 변하지 않습니다. 이 네트워크가 어떻게 작동하는 지도 살펴보도록 하겠습니다.
X: [1 1] Y: [0] Output: 0.5128176286712095
X: [1 0] Y: [1] Output: 0.5128176305326305
X: [0 1] Y: [1] Output: 0.4999999990686774
X: [0 0] Y: [0] Output: 0.5000000009313226
w: tf.Tensor([ 5.1281754e-02 -7.4505806e-09], shape=(2,), dtype=float32)
b: tf.Tensor([3.7252903e-09], shape=(1,), dtype=float32)
앞서 학습시켰던 AND 네트워크나 OR 네트워크와는 다른 결과를 보여주고 있습니다. 우선 Y와 출력 사이에 큰 차이가 있어 보입니다. X가 변하더라도 출력은 0.5 근처에서 머물고 있습니다.
학습된 w와 b도 다릅니다. w의 경우 약 0.0512, -0.00000000745 이고 b의 경우 약 0.000000000373으로 매우 작은 값입니다. 앞서 AND 네트워크의 w와 b가 [6.47, 6.50], -10.6 이였던 것과 비교하면 훨씬 작습니다. w에 순차적으로 x가 곱해지기 때문에 첫 번째 입력이 나머지보다 큰 영향을 미치고 있으며, 나머지는 거의 영향력이 없다는 사실을 알 수 있습니다.
XOR 네트워크의 입력과 중간 계산, 출력
AND 네트워크의 입력과 중간 계산, 출력
AND 네트워크의 값들과 XOR 네트워크의 값들을 정리하면 위와 같습니다. AND 네트워크의 가중치들은 비슷하기 때문에 X1과 X2가 비슷한 영향력을 가집니다. 편향값은 큰 음수이기 때문에 중간 계산값을 음수로 보내게 됩니다. 즉 두 가중치가 모두 양수가 나와야 편향값을 이겨내고 1을 반환하게 됩니다.
반면 XOR 네트워크는 각 가중치와 편향의 역할이 명확하지 않습니다. 중간 계산값은 0에 가깝고 시그모이드를 취하면 0.5에 가까울 뿐인 결과를 보여줍니다.
이것이 앞서 말했던 첫 번째 인공지능의 겨울을 불러온 XOR 문제입니다. 하나의 퍼셉트론으로는 간단한 XOR 문제도 해결할 수 없습니다.
해결
그렇다면 해결책은 무엇일까요? 바로 여러 개의 퍼셉트론을 활용하는 것입니다. 여기서는 keras 를 활용해서 구현해보겠습니다.
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_4 (Dense) (None, 2) 6
_________________________________________________________________
dense_5 (Dense) (None, 1) 3
=================================================================
Total params: 9
Trainable params: 9
Non-trainable params: 0
_________________________________________________________________
하나씩 차근차근 살펴보도록 하겠습니다.
-
model
keras에는 딥러닝 계산을 간편하게 하기 위한 추상적인 클래스인 model이 있습니다. 딥러닝 계산을 위한 함수와 변수의 묶음이라고 생각할 수 있습니다. 앞으로 꾸준히 사용하게 됩니다.
-
tf.keras.Sequential
model에서 가장 많이 쓰이는 구조가 tf.keras.Sequential 입니다. 순차적(sequential)으로 레이어를 일직선으로 배치한 것입니다. 시퀸셜 모델의 인수로는 레이어가 차례대로 정의된 리스트를 전달하면 됩니다.
-
tf.keras.layers.Dense
Dense는 가장 기본적인 구조의 레이어로, 이전 층의 레이어와 다음 층의 레이어에 있는 모든 뉴런이 서로 연결되는 레이어입니다.
- units는 레이어를 구성하는 뉴런의 수를 정의합니다.
- activation은 우리가 앞서 시그모이드로 사용했던 활성화 함수를 의미합니다. 여기서도 마찬가지로 시그모이드를 사용했습니다.
- input_shape는 sequential 모델의 첫 번째 레이어에서만 정의합니다. 입력의 차원 수를 의미합니다. 여기서는 입력 데이터가 [1,0] 과 같은 2개의 입력을 받는 1차원 array 이기 때문에 원소의 개수인 2를 명시한 (2,)를 사용했습니다.
XOR 네트워크 도식
이렇게 만든 모델을 그림으로 나타내면 위와 같습니다. 코드 출력 결과에서 Param 를 보시면 첫 번째 레이어에는 6개, 두 번째 레이어에는 3개가 있다고나옵니다. 위 그림의 화살표의 갯수와 같습니다. 파라미터는 weight의 수를 의미한다고 보시면 됩니다. 각 파라미터는 각자 학습됩니다.
-
model.compile
모델이 동작할 수 있도록 준비하는 명령어입니다. 여기서는 optimizer와 loss로 이루어져 있습니다.
-
optimizer 는 딥러닝의 학습 함수를 의미합니다. 앞에서 언급했던 경사 하강법이 그 종류 중 하나입니다. SGD는 확률적 경사 하강법의 약자이며 전체를 계산하지 않고 확률적으로 일부 샘플을 조금씩 나눠서 계산하는 경사 하강법의 한 종류입니다.
-
loss는 앞에서 살펴본 error와 비슷한 개념입니다. 딥러닝은 이 손실을 줄이는 바탕으로 학습하게 되는데 loss를 계산할 식을 정해줍니다. MSE는 평균 제곱 오차의 약자로 실제 값에서 출력을 뺀 뒤 제곱한 값을 평균한 값입니다. \(\text{MSE} = \frac1n\sum^n_{k=1}(y_k-\text{output}_k)^2\)
-
이제 이 네트워크를 실제로 학습시켜 보겠습니다.
Epoch 1/2000
4/4 [==============================] - 1s 2ms/step - loss: 0.1842
Epoch 2/2000
4/4 [==============================] - 0s 2ms/step - loss: 0.1834
Epoch 3/2000
4/4 [==============================] - 0s 2ms/step - loss: 0.1826
...
Epoch 1998/2000
4/4 [==============================] - 0s 997us/step - loss: 0.0037
Epoch 1999/2000
4/4 [==============================] - 0s 1ms/step - loss: 0.0037
Epoch 2000/2000
4/4 [==============================] - 0s 997us/step - loss: 0.0037
model.fit 함수는 앞의 예제의 for 반복문과 같이 epoch에 지정된 횟수만큼 학습시킵니다. batch_size는 한번에 학습시키는 데이터의 수 입니다. x와 y는 각각 입력과 실제 값을 의미합니다.
학습이 끝나면 네트워크를 평가할 수도 있습니다.
array([[0.90535486],
[0.05620937],
[0.05175305],
[0.00188303]], dtype=float32)
model.predict 함수에 입력을 넣으면 네트워크의 출력 결과를 알 수 있습니다. 첫 번째와 네 번째 값은 0에 가깝고 두 번째와 세 번째 값은 1에 가깝습니다. 즉 XOR 네트워크를 잘 학습했습니다. 이제 가중치와 편향을 출력해보겠습니다.
<tf.Variable 'dense/kernel:0' shape=(2, 2) dtype=float32, numpy=
array([[-2.8604188, -2.3092973],
[-1.6776553, -3.252578 ]], dtype=float32)>
<tf.Variable 'dense/bias:0' shape=(2,) dtype=float32, numpy=array([2.5341706, 3.4848986], dtype=float32)>
<tf.Variable 'dense_1/kernel:0' shape=(2, 1) dtype=float32, numpy=
array([[-4.4813437],
[-5.718428 ]], dtype=float32)>
<tf.Variable 'dense_1/bias:0' shape=(1,) dtype=float32, numpy=array([3.4273224], dtype=float32)>
가중치 정보는 model.weights에 저장합니다. 뉴런끼리 연결할 때 사용되는 가중치는 kernel, 편향에 연결된 가중치는 bias로 표현합니다. 값을 보면 아까 위에서 구현했던 XOR 네트워크가 아주 작은 값을 가졌던 것과 다르게 AND 네트워크처럼 큰 값을 가지고 있다는 사실을 알 수 있습니다. 이제 네트워크의 학습이 어떻게 이루어졌는지 그래프를 그려 파악해보겠습니다.
시각화
딥러닝을 학습시킬 때 학습이 잘 되고 있는지 확인하기 위에 이렇게 epoch에 따라 loss가 어떻게 변화하고 있는지를 주로 살펴보게 됩니다. 시각화를 위해 matplotlib 라는 라이브러리를 활용했습니다. plt.plot으로 그래프를 그릴 수 있으며 앞서 history라는 변수에 학습 내역을 저장했으므로 저장된 loss를 불러와서 시각화를 해보았습니다.
댓글남기기