IT/AI\ML

[python/tensorflow2.x] Tensorflow2.x버전 작성법(1)

개발자 두더지 2021. 9. 11. 00:31
728x90

TensorFlow의 세 종류/ 여섯 가지 모델 작성법


세 종류의 API

 크게 아래의 세 종류의 API가 존재한다.

- Sequential 모델 : 컴팩트하고 간단한 작성법

- Funtional API : 복잡한 모델을 정의할 수 있는 유연한 작성법

- Subclassing 모델 : 난이도가 살짝 높긴하지만, 자유자재로 커스터마이즈할 수 있는 작성법

 한편, Sequential 모델과 Funtional API는 "선언형"을 의미하므로 Symbolic API로 불리기도하고, Subclassing 모델은 "명령형"을 의미하므로 Imperative API라고도 불리기도 한다. 선언형이란 모델의 레이어를 선언적으로 정의하는 것(Define-and-Run)을 일컫는 것이며, 명령형이란 순전파의 실행 포워드 패스를 명령적으로 기재하는 것(Define-by-Run)을 일컫는다.

 

여섯 가지의 모델 작성법

 방금 설명했던 세 종류의 작성법은 크게 분류한 경우이다. TensorFlow2.x(2.0이후의 버전)에 있어서 최신 작성법은 더욱 세분화되어 여섯 가지로 나뉜다.

 <Sequential 모델>

(1) tf.keras.models.Sequential 클래스의 컨스트럭터의 이용:

  컴팩트하고 간단한 작성법. 뉴럴 네트워크를 학습시키고 싶은 초심자에게 추천한다.

(2) tf.keras.models.Sequential 옵션의 add 메소드로 추가:

 심플하지만, 보다 유연한 작성법. 초심자용. 위의 컨스트럭터로 대처할 수 없는 경우에 사용한다.

<Funtional API>

(3) tf.keras.Model 클래스의 컨스트럭터의 이용:

 유연하게 작성할 수 있지만, 코드양이 는다. 다음에 설명할 서브 클래스화가 더욱 알기쉬운 코드이다. 초중급자용. 유연성과 편리성 두 마리 토끼를 잡고싶을 때 사용 

<Subclassing 모델> 

(4) tf.keras.Model 클래스의 서브클래스화:

 계산 라이브러리 Numpy를 사용하여 기재하는 방법으로, 유연성과 확장성이 우수하다. 라이브러리 PyTorch에 가까운 작성이 가능하므로, 양쪽의 라이브러리를 능숙하게 다르고 싶은 사람이나, TensorFlow를 일상적으로 다루는 초중급자이상의 사람에게 최고로 추천한다.  

(5) TensorFlow저수준API으로 커스텀 구현:

 위의 서브 클래스화의 레이어나 활성화함수, 손실함수, 평가함수 등을 독자적으로 코드로 작성하는 방법이다. Keras의 기본 기능만으로는 부족한 상급자용이다.

<작성된 Estimators>

 (6) tf.estimator.Estimator가 제공하는 작성된 모델(Pre-made Estimators)의 이용:

 목적마다 준비된 DNNRegressor(회귀)/DNNClassifier(분류) 클래스등을 사용하여 모델을 정의할 수 있지만, 융통성음에도, 코드가 길어져버리는 경우가 있다. 호환성때문에 비추천.

 한편, Tensorflow2.0버전 이후에는 Keras가 표준 고수준의 API가 됐다. 작성된 Estimators도 고수준 API이지만, 위에서 언급했듯, 새로운 모델을 작성할 경우에 비추천이므로, tf.keras가 Tensorflow로 유일한 고수준 API이다. (5)의 커스텀 구현도 활성화 함수, 손실함수, 평강 함수 등 TensorFlow의 저수준API를 사용하여 작성하지만, 그 모델이나 레이어는 (기본적으로) tf.keras를 서브클래스화하여 사용하게 된다(서브클래스화하지 않고 구현하는 것이 불가능하지는 않지만, 들일 수고를 생각하면 추천하지 않는다).

 이번 포스팅에서는 Sequential 모델과 Functional API의 내용만 다루도록 하겠다.

 

 

코드를 실행하기 위한 준비


데이터

 이번 포스팅에 사용할 입력 데이터로써 좌표점(X좌표, Y좌표)의 데이터를 사용한다. 좌표점의 결과가 파란색(1.0)인가, 오렌지색(-1.0)을 학습하여 예측판정할 수 있도록 할 예정이다. 

 데이터는 아래의 커맨드로 취득할 수 있다.

!pip install playground-data

 

뉴럴 네트워크의 설계

- 입력의 수(INPUT_FEATURES)는 X1와 X2 두 가지

- 은닉 레이어의 수는 2개

 은닉층에 있는 첫 번째 뉴런의 수(LAYER1_NEURONS)는 3개

 은닉층에 있는 두 번째 뉴런의 수(LAYER2_NEURONS)는 3개

- 출력층의 뉴런 수(OUTPUT_RESULTS)는 1개

위와 같이 모델을 정의하면 다음과 같다.

# 라이브러리「TensorFlow」의 tensorflow패키지를「tf」이라는 이름으로 임포트
import tensorflow as tf
from tensorflow.keras import layers       # 레이어 모듈의 임포트

# 정수(모델 정의시에 필요한 숫자)
INPUT_FEATURES = 2  # 입력(특징)의 수: 2
LAYER1_NEURONS = 3  # 뉴런의 수: 3
LAYER2_NEURONS = 3  # 뉴런의 수: 3
OUTPUT_RESULTS = 1  # 출력 결과 수: 1

 tf.kresa로 모델을 설계할 때에, 레이어용의 tf.keras.layers 이름 공간을 자주 사용하므로 여기서 임포트해두는 것으로 한다. 더욱이, 평가함수로써는 def tanh_accuracy(y_true, y_pred)이라는 함수를 정의해둔다.

 

 

Sequential 클래스의 컨스트럭터 이용 


 이 방법의 전체 코드는 다음과 같다.

model = tf.keras.models.Sequential([    # 모델의 생성

  # 은닉층:첫 번째 레이어
  layers.Dense(                 # 전결합층
      input_shape=(INPUT_FEATURES,),       # 입력의 형태(=입력층)
      name='layer1',                       # 표시용으로 이름을 붙인다.
      kernel_initializer='glorot_uniform', # 가중치의 초기화(균일분포의 랜덤값)
      bias_initializer='zeros',            # 바이어스의 초기화(0)
      units=LAYER1_NEURONS,                # 유닛의 수
      activation='tanh'),                  # 활성화 함수

  # 은닉층: 두 번째 레이어
  layers.Dense(                 # 전결합층
      name='layer2',                       # 표시용으로 이름을 붙인다.
      kernel_initializer='glorot_uniform', # 가중치의 초기화
      bias_initializer='zeros',            # 바이어스의 초기화
      units=LAYER2_NEURONS,                # 유닛의 수
      activation='tanh'),                  # 활성화 함수

  # 출력층
  layers.Dense(                 # 전결합층
      name='layer_out',                    # 표시용으로 이름을 붙인다.
      kernel_initializer='glorot_uniform', # 가중치의 초기화
      bias_initializer='zeros',            # 바이어스의 초기화
      units=OUTPUT_RESULTS,                # 유닛의 수
      activation='tanh'),                  # 활성화 함수

], name='sequential_constructor'           # 모델에 이름을 붙인다.
)

# 모델의 설계를 완료
model.summary()                         # 모델의 내용을 출력

 이 경우에는 (기본적으로는), 가중치는 'glorot_uniform"(Xavier Glorot와 연속 균등 분포에 의한 랜덤 값 : 최소 ~ 최대 범위를 입력 유닛 수와 출력 유닛 수에서 자동 계산)으로, 바이어스는 "zero(0)"이 된다.

 이번에는 tf.keras.layers.Dense 클래스의 컨스트럭터 (밀접하게는 __init()__메소드)의 인수 kernel_initializer에 "glorot_uniform"이라는 가중치를, 인수 bias_initializer에 "zeros"이라는 바이어스를 문자열로 지정한다. 파라미터의 초기화를 하는 initializer로써는 아래의 문자열로 지정할 수 있다.

- glorot_uniform (Xavier Glorot와 연속 균등 분포에 의한 랜덤 값)

- glorot_normal (Xavier Glorot의 정규분포에 의한 랜덤 값)

- he_uniform (He의연속 균등 분포에 의한 랜덤 값)

- he_normal (He의 정규 분포에 의한 랜덤 값)

- lecun_uniform (LeCun의 연속 균등 분포에 의한 랜덤 값)

- lecun_normal(LeCun의 정규 분포에 의한 랜덤 값)

 또한, 전반적으로 각 name 인수에 이름을 지정해간다. 여기까지의 model.summary()메소드의 출력은 아래와 같이, 모델이나 레이어의 이름이 알기 쉬워진다.

 그리고, 리스트 1-2의 코드에 의해 모델 구성도를 표시하면, 아래의 그림와 같이 표시된다. 여기에도 지정한 이름이 표시되는 것을 알 수 있다.

# 리스트 1-2

tf.keras.utils.plot_model(model, show_shapes=True, show_layer_names=True, to_file='model.png')
from IPython.display import Image
Image(retina=False, filename='model.png')

 

가중치/바이어스의 초기화지정 (응용)

 아까, kernel_initializer/bais_initializer에는 문자열로 지정하였지만, 초기화를 커스터마이즈하고 싶은 경우가 있을 것이다. 예를 들어, "glorot_uniform'이 아닌, 스스로 하한과 상한을 지정한 연속균등분포로 랜덤 값을 얻어내고 싶을 때, tf.keras.RandomUniform 클래스를 사용하여 initializer를 독자적으로 작성하면 좋다.

random_init = tf.keras.initializers.RandomUniform(
    minval=-1.0, maxval=1.0) # 하한과 상한을 지정한 연속균등분포

 이렇게 생성된 initializer를 kernel_initializer = random_init와 같이 지정할 뿐이다.

 

학습과 추론 : 작성법 입문에서 공통된 부분

 모델을 설계해서 인스턴스화 한 후에는 학습 방법을 설정하고, 학습, 추론(예측)하는 흐름이 된다. 또한, playground-data라이브러리로부터 훈련용 데이터를 정밀도 검증용 데이터 (아래의 코드에서는 X_train 변수, y-train 변수 등에 대입하고 있다)의 취득에 대해서는 맨 앞에서 설명했었다.

# 학습 방법을 설정하고, 학습, 추론(예측)한다.
model.compile(tf.keras.optimizers.SGD(learning_rate=0.03), 'mean_squared_error', [tanh_accuracy])
model.fit(X_train, y_train, validation_data=(X_valid, y_valid), batch_size=15, epochs=100, verbose=1)
model.predict([[0.1,-0.2]])

 한편, 모델의 작성법뿐만 아니라, 학습에 관한 코드도 아래와 같이 초중급자용, 전문가용으로 나뉜다.

- 초중급자용 작성법 : 위와 같이 compile()과 fit()메소드를 호출하는 간단한 작성법

- 전문자용 작성법 : 다음 포스팅에서 설명하겠지만, tf.GradientTape 클래스 (자동미분기록 기능)을 사용한 유연하고 확장성이 높은 작성법 (PyTorch에 가까운 작성법)

 

작성이 끝난 모델의 리셋

 방금 model이라는 변수명에 모델을 생성했다. 작성법을 비교하기 쉽도록 이후에도 model이라는 변수명을 사용할 것이다. 일단 모델을 삭제하고 리셋하고 싶은 경우는 아래의 코드를 실행하길 바란다.

tf.keras.backend.clear_session() # 계산 그래프를 파기
del model                        # 변수를 삭제

 

 

Sequential 오브젝트의 add 메소드로 추가 [tf.keras-Sequential API]


앞에서는 tf.keras.models.Sequnatil 클래스의 컨스트럭터를 호출한 인수에, tf.keras.layers.Dense 오브젝트를 리스트형식으로 지정해왔었다. 여기서 설명할 작성법은 일단 빈 상태로 Sequential 객체를 생성하고, 그 객체의 add 메소드를 사용하여 Dense객체를 추가해가는 방식이다.

 아래의 코드를 보면, add 메소드의 인수 지정부분에 Dense 클래스의 컨스트럭터가 호출되어, Dense 객체가 생성, 지정되고 있다.

model = tf.keras.models.Sequential()  # 모델의 생성

# 은닉층:1번째의 레이어
model.add(layers.Dense(        # 전결합층
    input_shape=(INPUT_FEATURES,),    # 입력의 형태(=입력층)
    name='layer1',                   # 표시용으로 이름을 붙임
    units=LAYER1_NEURONS,             # 유닛 수
    activation='tanh'))               # 활성화 함수

# 은닉층:2번째 레이어
model.add(layers.Dense(        # 전결합층
    name='layer2',                   # 표시용으로 이름을 붙임
    units=LAYER2_NEURONS,             # 유닛 수
    activation='tanh'))               # 활성화 함수

# 출력층
model.add(layers.Dense(        # 전결합층
    units=OUTPUT_RESULTS,             # 유닛의 수
    name='layer_out',                # 표시용으로 이름을 붙임
    activation='tanh'))               # 활성화 함수

# 이것으로 모델 설계가 완료
model.summary()                       # 모델 내용을 출력

# 학습 방법을 설정하고, 학습, 추론(예측)한다.
#model.compile(tf.keras.optimizers.SGD(learning_rate=0.03), 'mean_squared_error', [tanh_accuracy])
#model.fit(X_train, y_train, validation_data=(X_valid, y_valid), batch_size=15, epochs=100, verbose=1)
#model.predict([[0.1,-0.2]])

 그것 이외에는 완전히 동일한 내용이므로 특별히 어렵지 않을 것이라고 생각한다. 모델 내용을 출력하느 행에서도 그림으로 보면 알 수 있듯 앞에서 설명한 내용과 동일하다.

 다음은 Funtional API의 작성법을 살펴보자.

 

 

Model 클래스의 컨스트럭터 이용 [tf.keras-Funtional API]


 이번에 설명한 작성법은 바로 앞에서 설명했던 작성법과 거의 비슷한 부분이 있다. 먼저 아래의 코드의 레이어 정의 부분의 layer1 = layers.Dens(...) 부분을 보자. 이는 방금의 내용과 동일하게, Dense 클래스의 컨스트럭터를 호출해, Dense 객체를 생성, 지정하고 있는 코드이다. 그러나, layer1등의 변수로 되어 있어(즉 작성한 Dense 객체를 변수 layer1등으로 대입하고 있다)는 점이 다르다.

# ### 활성화 함수를 변수 (하이퍼 파라미터로써 정의) ###
# 변수 (모델 정의시에 필요한 값)
activation1 = layers.Activation('tanh' # 활성화 함수(은닉층용): tanh함수(변경가능)
    , name='activation1'               # 활성화 함수에도 이름을 붙임
    )
activation2 = layers.Activation('tanh' # 활성화 함수(은닉층용): tanh함수(변경가능)
    , name='activation2'               # 활성화 함수에도 이름을 붙임
    )
acti_out = layers.Activation('tanh'    # 활성화 함수(은닉층용): tanh함수(변경가능)
    , name='acti_out'                  # 활성화 함수에도 이름을 붙임
    )

# ### 레이어를 정의 ###
# input_shape인수의 대신에 Input클래스를 사용하는 것도 가능
inputs = layers.Input(          # 입력층
    name='layer_in',                 # 표시용으로 이름을 붙임
    shape=(INPUT_FEATURES,))         # 입력의 형태

# 隠れ層:1つ目のレイヤー
layer1 = layers.Dense(          # 전결합층
    #input_shape=(INPUT_FEATURES,),  # ※입력층은 이미 정의했으므로 불필요
    name='layer1',                   # 표시용으로 이름을 붙임
    units=LAYER1_NEURONS)            # 유닛의 층

# 隠れ層:2つ目のレイヤー
layer2 = layers.Dense(          # 전결합층
    name='layer2',                   # 표시용으로 이름을 붙임
    units=LAYER2_NEURONS)            # 유닛의 층

# 出力層
layer_out = layers.Dense(       # 전결합층
    name='layer_out',                # 표시용으로 이름을 붙임
    units=OUTPUT_RESULTS)            # 유닛의 층

# ### 포워드 패스 정의 ###
# 「출력=활성화 함수(제n층(입력))」의 형식으로 기재 
x1 = activation1(layer1(inputs))     # 활성화 함수를 변수로써 정의
x2 = activation2(layer2(x1))         # 위와 동일 동일
outputs = acti_out(layer_out(x2))    # ※활성화함수는「tanh」고정

# ### 모델의 생성 ###
model = tf.keras.Model(inputs=inputs, outputs=outputs
    , name='model_constructor'       # 모델에도 이름을 붙임
)

# ### 以上でモデル設計は完了 ###
model.summary()                      # モデルの内容を出力

# 学習方法を設定し、学習し、推論(予測)する
#model.compile(tf.keras.optimizers.SGD(learning_rate=0.03), 'mean_squared_error', [tanh_accuracy])
#model.fit(X_train, y_train, validation_data=(X_valid, y_valid), batch_size=15, epochs=100, verbose=1)
#model.predict([[0.1,-0.2]])

 또한, 지금까지는 입력층의 지정은 은닉층의 첫 번째에 input_shape = (INPUT_FEATURES)로 지정해왔다. 그러한 방법이 아닌, 레이어 객체로써 입력층을 정의하는 것이 가능하다. 이 방법을 적용한 부분이, inputs = layers.Input(...)이다.

 또한, 이때의 입력층의 형태 지정을 input_shape가 아닌 shape이라는 인수명으로 되어있음을 주의하자.

 이로인해 작성된 inputs/layer1/layer2/layer_out등의 변수를 사용해, 순전파의 실행 패스(= 뉴럴 네트워크의 데이터의 흐름)을 형성할 필요가 있다. 앞에서 설명한 두 가지 작성법에서는 리스트 값이나 add 메소드에 의한 나열순으로 순전파 경로를 결정했다. 이번에는 변수가 각각 다르므로, 이것을 명시적으로 조합할 필요가 있다. 이러한 조합 부분은 아래의 부분이다.

x1 = activation1(layer1(inputs))
  ↓
  x2 = activation2(layer2(x1))
  ↓
  outputs = acti_out(layer_out(x2))

  예를 들어, activation1 / activation2 / acti_out함수는 두고, 

입력된 데이터 inputs > 첫 번째의 은닉층 layer1 > 출력된 데이터 x1

전파된 데이터 x1 > 두 번째 은닉층 layer2 > 출력된 데이터 x2

전파된 데이터 x2 > 출력층 layer_out > 출력된 데이터 ouputs

 이라는 흐름이 되어 있음을 확인해뒀음 한다. 이로 인해 순전파 패스를 형성할 필요가 있다.

 더욱이, 뉴럴 네트워크에서는 각 뉴론에 있어서 데이터의 출력시에 활성화 함수로 데이터를 변환할 필요가 있다. 이러한 역할을 하는 것이 activation1 / activation2 / acti_out함수이다. 각각의 함수는 코드의 맨 앞에 작성되어 있다. 예를 들어, activation 1 = layers.Activation('tanh')이라는 코드에서는 tf.keras.layers.Activation 클래스의 컨스트럭터에 활성화 함수명을 지정하여 Activation 오브젝트를 생성하고 있다. 

 이로 인해, 변수 activation1가 작성되어 있다. 이 부분에 상응하는 부분이 x1 = activation1(...)코드로 함수와 같이 사용되고 있음을 알 수 있다(엄밀히 말하자면 activation1 객체의 __call()__메소드를 호출하고 있다). 

 한편으로, 뉴론 (노드)로의 데이터 입력시에 일어나는 전결합층의 변수는 선형 변환 (linear trnssformation)이라고 불리며, 뉴론의 데이터 출력시에 실행되는 활성화 함수의 변환은 비선형 변환(non-linear transformation)이라고 부른다.

 또한 위 코드에서는 활성화 함수를 함수로써 사용할 수 있도록 잘라냈지만, 앞의 두 가지 작성법과 동일하게, Dense클래스의 컨스트럭터 인수 activation에 문자열로 지정하는 것도 가능하다. 그 경우에는, activation1(...)이라는 함수를 호출하는 것이 아닌, x1 = layer1(inputs) 와 같이 쓸 수 있다. 그러나, 이렇게 작성하면, 순전파의 흐름을 파악하기 쉽지 않아진다. 

 모델 내용의 출력 예는 그림을 살펴보면 알 수 있듯, 앞의 두 가지 작성법과 달리, 입력층 layer_in이나 활성화 함수 activation1가 레이어로써 표시된다.


참고자료

https://atmarkit.itmedia.co.jp/ait/articles/1909/19/news026.html

https://atmarkit.itmedia.co.jp/ait/articles/2002/27/news021.html

 

 

 

728x90