
케라스 창시자에게 배우는 딥러닝 - 3장 케라스와 텐서플로 소개
Dobby-HJ
·2023. 7. 28. 08:19
5. 텐서플로 시작하기
- 첫째, 모든 현대적인 머신 러닝의 기초 인프라가 되는 저수준 텐서 연산. 이는 다음과 같은 텐서플로 API로 변환됩니다.
- 텐서(신경망의 상태를 저장하는 특별한 텐서(변수)도 포함)
- 덧셈, relu, matmul 같은 텐서 연산
- 수학 표현식의 그레이디언트를 계산하는 방법인 역전파(텐서플로의 GradientTape 객체를 통해 처리됨)
- 둘째, 고수준 딥러닝 개념. 이는 다음과 같은 케라스 API로 변환됩니다.
- 모델을 구성하는 층
- 학습에 사용되는 피드백 신호를 정의하는 손실 함수
- 정확도처럼 모델의 성능을 평가하는 측정 지표
- 미니 배치 확률적 경사 하강법을 수행하는 훈련 루프
5.1 상수 텐서와 변수
텐서플로에서 어떤 작업을 하러면 텐서가 필요합니다. 텐서를 만드려면 초기값이 필요합니다. 예를 들어 모두 1이거나 0인 텐서를 만들거나.
import tensorflow as tf
x = tf.ones(shape=(2, 1))
print(x)
x = tf.zeros(shape=(2,1))
print(x)
랜덤 텐서
x = tf.random.normal(shape=(3,1), mean=0,stddev=1.)
print(x)
x = tf.random.uniform(shape=(3, 1), minval=0, maxval=1.)
print(x)
넘파이 배열과 텐서플로 텐서 사이의 큰 차이점은 텐서플로 텐서에는 값을 할당할 수 없다는 것입니다.
즉, 텐서플로 텐서는 상수입니다. 예를 들어 넘파이에서 다음과 같이 할 수 있습니다.
import numpy as np
x = np.ones(shape=(2,2))
x[0, 0] = 0.
텐서플로에서 같은 작업을 하면 **“EagerTensor object does not support item assignment”**와 같은 에러가 발생합니다.
x = tf.ones(shape=(2, 2))
x[0, 0] = 0.
모델을 훈련하려면 모델의 상태, 즉 일련의 텐서를 업데이트해야 합니다. 텐서에 값을 할당할 수 없다면 어떻게 이 작업을 할 수 있을까요? 이것이 변수가 있는 이유입니다. tf.Varaibale은 텐서플로에서 수정 가능한 상태를 관리하기 위한 클래스입니다. 2장 끝에서 훈련 루프를 구현할 때 잠깐 보았습니다.
변수를 만들려면 랜덤 텐서와 같이 초깃값을 제공해야 합니다.
v = tf.Variable(initial_value=tf.random.normal(shape=(3, 1)))
print(v)
변수의 상태는 다음과 같이 assign 메서드로 수정할 수 있습니다.
변수의 일부 원소에만 적용할 수도 있습니다. (mutable한 객체라고 함)
assign_add()와 assign_sub()은 각각 +=, -=와 같습니다.
v.assign_add(tf.ones((3,1)))
5.2 텐서 연산 : 텐서플로에서 수학 계산하기
넘파이와 마찬가지로 텐서플로는 수학 공식을 표현하기 위해 많은 텐서 연산을 제공합니다.
a = tf.ones((2,2))
b = tf.square(a) # 제곱을 계산
c = tf.sqrt(a) # 제곱근을 계산
d = b + c # element-wise addition
e = tf.matmul(a, b) # dot product
e *= d
5.3 GradientTape API 다시 살펴보기
지금까지는 텐서플로가 넘파이와 매우 비슷하게 보일 것입니다. 하지만 넘파이가 할 수 없는 것이 있습니다. 미분 가능한 표현이라면 어떤 입력에 대해서도 그레디언트를 계산할 수 있습니다. Graident 블록을 시작하고 하나 또는 여러 입력 텐서에 대해 계산을 수행한 후 입력에 대해 결과의 그레디언트를 구하면 됩니다.
**gradient = tape.gradient(target, sources)**
input_var = tf.Variable(initial_value=3.)
**with tf.GradientTape() as tape:**
result = tf.square(input_var)
gradient = **tape.gradient**(result, input_var)
**gradient = tape.Gradient(loss, weight)**와 같이 가중치에 모델 손실의 그레디언트를 계산하는데 가장 널리 사용된 방법입니다.
지금까지 **tape.Gradient()**의 입력 텐서가 텐서플로 변수인 경우만 보았습니다. 실제로 입력은 어떤 텐서라도 가능합니다. 하지만 텐서플로는 기본적으로 훈련 가능한 변수만 추적합니다. 상수 텐서의 경우 **tape.watch()**를 호출하여 추적한다는 것을 수동으로 알려주어야 합니다.
input_const = tf.constant(3.)
with tf.GradientTape() as tape:
tape.watch(input_const)
result = tf.square(input_const)
gradient = tape.gradient(result, input_const)
왜 이것이 필요할까요? 모든 텐서에 대한 모든 그레디언트를 계산하기 위해 필요한 정보를 미리 앞서서 저장하는 것은 너무 비용이 많이 들기 때문입니디ㅏ. 자원 낭비를 막기 위해 테이프는 감시할 대상을 알아야 합니다. 훈련 가능한 변수는 기본적으로 감시 대상입니다. 훈련 가능한 변수에 대한 손실의 그레디언트를 계산하는 것이 그레디언트 테이프의 주 사용 용도이기 때문입니다.
그레디언트 테이프는 이계도(second-order) 그레디언트도 계산할 수 있습니다.
time = tf.Variable(0.)
with tf.GradientTape() as outer_tape:
with tf.GradientTape() as inner_tape:
position = 4.9 * time ** 2
speed = inner_tape.gradient(position, time)
accleration = outer_tape.gradient(speed, time)
print(position, speed, accleration)
# tf.Tensor(0.0, shape=(), dtype=float32)
# tf.Tensor(0.0, shape=(), dtype=float32)
# **tf.Tensor(9.8, shape=(), dtype=float32)**
6. 신경망의 구조 : 핵심 Keras API 이해하기
6.1 층 : 딥러닝의 구성 요소
신경망의 기보 데이터 구조는 2장에서 소개한 층(layer)입니다. 층은 하나 이상의 텐서를 입력으로 받고 하나 이상의 텐서를 출력학는 데이터 처리 모듈입니다. 어떤 종류의 층은 상태가 없지만, 대부분의 경우 가중치(weight)라는 층의 상태를 가집니다. 가중치는 확률적 경사 하강법으로 학습되는 하나 이상의 텐서이며 여기에 신경망이 학습한 지식이 담겨 있습니다.
Keras의 Layer 클래스
간단한 API는 모든 것이 중심에 모인 하나의 추상화를 가져야 합니다. 케라스에서는 Layer 클래스가 그렇습니다. 케라스에서는 Layer 또는 Layer와 밀접하게 상호 작용하는 것이 전부입니다.
Layer의 서브 클래스(subclass)로 구현한 Dense 층
from tensorflow import keras
class SimpleDense(keras.layers.Layer):
def __init__(self, units, activation=None):
super().__init__()
self.units = units
self.activation = activation
def build(self, input_shape): # build() 메서드에서 가중치를 생성합니다.
input_dim = input_shape[-1]
# add_weight는 가중치를 간편하게 만들 수 있는 메서드입니다. self.W = tf.Variable(tf.random.uniform(w_shape))
# 와 같이 독립적으로 변수를 생성하고 층의 속성으로 할당할 수도 있습니다.
self.W = self.add_weight(shape=(input_dim, self.units),
initializer='random_normal')
self.b = self.add_weight(shape=(self.units,),
initializer="zeros")
def call(self, inputs): # call 메서드에서 정방향 패스 계산을 정의합니다.
y = tf.matmul(inputs, self.W) + self.b
if self.activation is not None:
y = self.activation(y)
return y
my_dense=SimpleDense(units=32, activation=tf.nn.relu)
input_tensor = tf.ones(shape=(2, 784))
output_tensor = my_dense(input_tensor)
print(output_tensor.shape)
# (2, 32)
자동 크기 추론 : 동적으로 층 만들기
레고 블록처럼 호환되는 층만 서로 연결할 수 있습니다. 층 호환(layer compatibility) 개념은 모든 층이 특정 크기의 입력 텐서만 받고, 특정 크기의 출력 텐서만 반환한다는 사실을 의미합니다. 다음 예를 생각해보죠.
from tensorflow.keras import layers
layer = layers.Dense(32, activation="relu") # 32개의 출력 유닛을 가진 밀집 층
케라스를 사용할 때 대부분의 경우 크기 호환성에 대해 걱정할 필요가 없습니다. 모델에 추가하는 층은 앞선 층의 크기에 맞도록 동적으로 만들어지기 때문입니다. 예를 들어 다음과 같은 경우를 생각해 보죠.
from tensorflow.keras import models
from tensorflow.kreas import layers
model = models.Sequential([
layers.Dense(32, activation="relu"),
layers.Dense(32)
])
이 층들은 입력 크기에 대해 어떤 정보도 받지 않습니다. 그 대신 입력 크기를 처음 본 입력의 크기로 추론합니다.
2장에서 만들었던 간단한 Dense 층인 NaiveDense 클래스의 경우 가중치를 만들기 위해 각 층의 입력 크기를 생성자에게 명시적으로 전달했습니다.
SimpleDense 클래스에서는 NaiveDense처럼 생성자에게 가중치를 만들지 않습니다. 그 대신 상태생성을 위한 전용 메서드인 **build()**에서 만듭니다. 이 메서드는 층이 처음 본 입력 크기를 매개변수로 받습니다. build() 메서드는 층이 처음 호출될 때 (**__call__()**메서드를 통해) 자동으로 호출됩니다. 사실 이것이 call()메서드가 아니라 별도의 call() 메서드에서 계산을 정의한 이유입니다. 기본 Layer 클래스의 **__call__()**메서드는 다음과 같습니다.
def __call__(self, inputs):
if not self.built:
self.build(inputs.shape)
self.built = True
return self.call(inputs)
6.2 층에서 모델로
딥러닝 모델은 층으로 구성된 그래프입니다. 케라스에서는 Model 클래스에 해당됩니다. 지금까지 Model의 subClass인 Sequential 모델만 보았습니다. 이 모델은 단순히 층을 쌓은 것이고, 하나의 입력을 하나의 출력에 매핑합니다. 하지만 앞으로 배우다 보면 매우 다양한 종류의 네트워크를 보게 될 것입니다.
- 2개의 가지(two-branch)를 가진 네트워크
- 멀티헤드(multihead) 네트워크
- 잔차 연결(residual connection)
케라스에서 이런 모델을 만드는 방법은 일반적으로 두 가지입니다. 직접 Model 클래스의 서브 클래스를 만들거나, 더 적은 코드로 많은 일을 수행할 수 있는 함수형 API(fucntional API)를 사용할 수 있습니다.
모델의 구조는 가설 공간(hypothesis space)를 정의합니다. 1장에서 머신 러닝을 “사전에 정의된 가능성 있는 공간(space of possibility)” 안에서 피드백의 신호의 도움을 받아 입력 데이터의 유용한 표현을 찾는 것’으로 설명했었습니다. 네트워크 구조를 선택하면 가능성 있는 공간(가설 공간)이 입력 데이터를 출력 데이터로 매핑하는 일련의 특정한 텐서 연산으로 제한됩니다. 그 다음 우리가 찾을 것은 이런 텐서 연산에 관련된 가중치 텐서의 좋은 값입니다.
데이터에서 학습하려면 데이터에 대한 가정을 해야합니다. 이런 가정이 학습할 수 있는 것을 정의합니다. 따라서 가설 공간의 구조, 즉 모델의 구조는 매우 중요합니다. 문제에 대한 가정, 즉 시작할 때 모델이 가지게 될 사전 지식을 인코딩합니다. 예를 들어 활성화 함수 없이 하나의 Dense 층을 가진 모델(순수한 affine transform)로 2개의 클래스를 분류하는 문제를 다룬다면, 두 클래스가 선형적으로 구분될 수 있다고 가정하는 것입니다.
잘 맞는 네트워크 구조를 선택하는 것은 과학보다는 예술에 가깝습니다. 믿을 만한 모범 사례와 우너칙이 있지만 연습을 해야만 올바른 신경망 설계자가 될 수 있습니다. 이어지는 몇 개의 장에서 신경망 구축의 명확한 원리를 이해하고, 특정 문제에 적합한 구조와 그렇지 않은 구조에 대한 직관을 길러보겠습니다. 다양한 종유의 문제에 어떠한 모델 구조가 맞는지, 실제 이런 네트워크를 어떻게 만드는지, 학습을 위해 적절한 설정값을 어떻게 고르는 지, 원하는 결과를 얻을 때까지 모델을 어떻게 조정하는지 자세히 배워보겠습니다.
6.3 ‘컴파일’ 단계 : 학습 과정 설정
- 손실 함수(loss function)(목적 함수(objective function)) : 훈련 과정에서 최소화할 값, 현재 작업에 대한 성공의 척도
- 옵티마이저(optimizer) : 손실 함수를 기반으로 네트워크가 어떻게 업데이트될 지 결정합니다. 특정 종류의 확률적 경사 하강법(SGD)으로 구현됩니다.
- 측정 지표(metric) : 훈련과 검증 과정에서 모니터링할 성공의 척도입니다.
손실, 옵티마이저, 측정 지표를 선택했다면 모델에 내장된 **compile()**과 **fit()** 메서드를 사용하여 모델 훈련을 시작할 수 있습니다. 또는 사용자 정의 루프를 만들 수도 있습니다. 이에 대해서는 7장에서 다루겠습니다.
**compile()**메서드는 훈련 과정을 설정합니다.
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer='rmsprop',
loss = 'mean_squared_error',
metrics=['accuracy'])
앞의 compile()메서드에서 옵티마이저, 손실, 측정 지표의 매개변수 값을 문자열로 지정했습니다. 이런 문자열은 실제로는 편의를 위한 단축어이며, 해당 파이썬 객체로 변환됩니다. 예를 들어 “rmsprop”은 **keras.optimizers.RMSprop()**이 됩니다. 중요한 것은 다음과 같이 매개변수를 인스턴스 객체로 지정할 수도 있다는 것입니다.
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])
이는 사용자 정의 손실이나 측정 지표를 전달하고 싶을 때 유용합니다. 또는 사용할 객체를 상세히 설정하고 싶을 때입니다. 예를 들어 다음과 같이 옵티마이저의 learning_rate 매개변수를 바꿀 수 있습니다.
model.compile(optimizer=keras.optimizers.RMSprop(learning_reate=1e-4),
loss=my_custom_loss,
metrics=[my_custom_metric_1, my_custom_metric_2]
6.4 손실 함수 선택하기
문제에 맞는 올바른 손실 함수를 선택하는 것은 아주 중요합니다. 네트워크가 손실을 최소화하기 위해 편법을 사용할 수 있기 때문입니다.
6.5 fit() 메서드 이해하기
compile()다음에는 fit() 메서드를 호출합니다. fit() 메서드는 훈련 루프를 구현합니다. 다음은 fit() 메서드의 주요 매개변수입니다.
- 훈련할 데이터(입력과 타깃)
- 훈련할 에포크(epoch) 횟수
- 배치 크기
histroy = model.fit(
inputs, # 입력 샘플(넘파이 배열)
targets, # 훈련 타깃
epochs=5, # 훈련 횟수
batch_size=128 # 배치 크김
)
6.6 검증 데이터에서 손실과 측정 지표 모니터링하기
머신 러닝의 목표는 훈련 데이터에서 잘 동작하는 모델을 얻는 것이 아닙니다. 이렇게 하는 것은 쉽습니다. 그레디언트를 따라가기만 하면 됩니다.
새로운 데이터에 모델이 어떻게 동작하는지 예상하기 위해 훈련 데이터의 일부를 검증 데이터(validation data)로 떼어 놓는 것이 표준적인 방법입니다. 검증 데이터에서 모델을 훈련하지 않지만 이 데이터를 사용하여 손실과 측정 지표를 계산합니다. 이렇게 하려면 fit() 메서드의 validation_data 매개변수를 사용합니다. 훈련 데이터처럼 검증 데이터는 넘파이 배열이나 텐서플로 Dataset 객체로 전달할 수 있습니다.
indices_permutation = np.random.permutation(len(inputs))
shuffled_inputs = inputs[indices_permutation]
shuffled_targets = targets[indices_permutation]
num_validation_samples = int(0.3 * len(inputs))
val_inputs = shuffled_inputs[:num_validation_samples]
val_targets = shuffled_targets[:num_validation_samples]
training_inputs = shuffled_inputs[num_validation_samples:]
training_targets = shuffled_targets[num_validation_samples:]
model.fit(
training_inputs,
training_targets,
epochs=5,
batch_size=16,
validation_data=(val_inputs, val_targets)
)
훈련이 끝난 후 검증 손실과 측정 지표를 계산하고 싶다면 evaludate() 메서드를 사용할 수 있습니다.
loss_and_metrics = model.**evaluate**(val_inputs, val_targets, batch_size=128)
6.7 추론:훈련한 모델 사용하기
모델을 훈련하고 나면 이 모델을 사용하여 새로운 데이터에서 예측을 만들게 됩니다. 이를 추론(inference)이라고 부릅니다. 간단한 방법은 모델의 **__call__()**메서드를 호출하는 것입니다.
predictions = model(new_inputs)
추론을 하는 더 나은 방법은 predict() 메서드를 사용하는 것입니다. 이 메서드는 데이터를 작은 배치로 순회하여 넘파이 배열로 예측을 반환합니다. **__call__()** 메서드와 달리 텐서플로 Dataset객체도 처리할 수 있습니다.
predictions = model.predict(new_inputs, batch_size=128
7. 요약
- 텐서플로는 CPU, GPU, TPU에서 실행할 수 있는 컴퓨팅 프레임워크입니다.
- 케라스는 텐서플로에서 딥러닝을 수행하기 위한 표준 API로, 이 책에서 사용하는 라이브러리입니다.
- 텐서플로의 핵심 객체는 텐서, 변수, 텐서 연산, 그레이디언트 테이프입니다.
- 케라스의 핵심 클래스 Layer입니다. 층은 가중치와 연산을 캡슐화합니다. 이런 층을 조합하여 모델을 만듭니다.
- 모델을 훈련하기 전에 옵티마이저, 손실, 측정 지표를 선택하여 model.compile() 메서드에 지정해야 하비다.
- 미니배치 하강법을 실행하는 fit() 메서드로 모델을 훈련할 수 있습니다. 또한, 이 메서드를 사용하여 모델이 훈련 과정에서 본 적 없는 검증 데이터에 대한 손실과 측정 지표를 모니터링할 수 있습니다.
- 모델을 훈련하고 나면 model.predict() 메서드를 사용하여 새로운 입력에 대한 예측을 만듭니다.
알게된 것
- **call()**과 **__call__()**은 역할이 다르다.
질문
외워야 하는 것
tf.random.normal
tf.random.uniform
tf.random.random
tf.ones
tf.zeros
gradient = tape.gradient(target, sources)
tf.Layer를 subclass로 구현할 때, weight와 bias를 초기화할 때는 keras.layers.Layer
의 상속을 받은 뒤, build
메소드에서 가중치값에 add_weight
메소드를 이용해 가중치를 초기화할 수 있음. self.W = tf.Variable(tf.random.uniform(w_shape))
와 같이 독립적으로 변수를 생성하고 층의 속성으로 할당할 수도 있습니다.
tf는 입력 차원에 대한 크기 제한 없이 모델을 만들 수 있는데, 학습을 위해 처음 모델이 호출되면 __call__
메서드가 호출되고, 이때 input.shape
에 맞게 모델 가중치가 초기화 및 생성되어 call()
메서드 실행으로 넘어가게 됩니다.
지금까지 model을 아래와 같이 python의 str
객체를 keyword args로 넣어서 진행했습니다.
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer='rmsprop',
loss = 'mean_squared_error',
metrics=['accuracy'])
다만 'rmsprop'은 keras.optimizers.RMSprop()
객체로 변환해서 input으로 넣어줄 수 있습니다. 즉, 매개변수를 인스턴스 객체로 지정할 수 있다는 뜻입니다.
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])
'DeepLearning > Tensorflow' 카테고리의 다른 글
케라스 창시자에게 배우는 딥러닝 - 9장 컴퓨터 비전을 위한 고급 딥러닝 (0) | 2023.08.05 |
---|---|
케라스 창시자에게 배우는 딥러닝 - 8장 컴퓨터 비전을 위한 딥러닝 (0) | 2023.08.04 |
케라스 창시자에게 배우는 딥러닝 - 6장 일반적인 머신 러닝 워크플로 (0) | 2023.08.01 |
케라스 창시자에게 배우는 딥러닝 - 5. 머신 러닝의 기본 요소 (0) | 2023.08.01 |
케라스 창시자에게 배우는 딥러닝 - 4. 신경망 시작하기 : 분류와 회귀 (0) | 2023.07.28 |