● 텐서플로우(Tensorflow)
- 강력한 수치 계산용 라이브러리, 딥러닝 최적화가 되어있다.
- 핵심 구조는 넘파이(Numpy)와 비슷하지만 GPU를 지원한다.
- 분산 컴퓨팅을 지원한다.
- 계산 그래프를 이용하여 함수 최적화 및 중립적 포맷을 유지한다.(리눅스 python 환경에서 훈련 후 안드로이드 자바에서 사용 가능)
- 자동 미분 기능 제공, 고성능 옵티마이저 제공
- 텐서 플로 허브 등에서 다수의 만들어진 모델을 다운 가능하다. (커뮤니티가 잘 되어 있고 사용자도 많다.)
목차
1. 텐서 플로우 연산(numpy처럼 사용하기)
2. 사용자 정의로 모델 만들기
- 손실 함수, 활성화 함수, 규제, 지표, 층 등 사용자 정의로 모델의 구성요소를 만들어 보기
3. 텐서 플로우 함수 만들기
1. 텐서 플로우 연산
● 텐서 : 텐서 플로우에서 사용하는 객체
- numpy ndarray와 비슷한 구조
- 스칼라 값부터 다차원 배열을 가질 수 있다.
▶ 텐서 만들기
import tensorflow as tf
tf.constant([[1,2,3],[4,5,6]])
- 인덱스로 자르기, 덧셈, 곱셈, 행렬곱 등 ndarray와 똑같이 작동한다.
- dtype을 갖는다. float과 int가 동시에 존재할 수 없다. 신경 쓰자
▶ 텐서와 넘파이
- 텐서와 넘파이 배열은 함께 사용할 수 있다.
- 주의) 넘파이는 기본 64비트를 사용하지만 텐서플로우는 32비트를 사용한다. 넘파이 배열을 텐서로 사용하고 싶다면 32로 바꾸자
a = np.array([2.,4.,5.]) # 넘파이 배열
t = tf.constant(a) # 텐서로 변환
t.numpy(), np.array(t) # 넘파이 배열로 변환
tf.square(a) # 텐서플로우 함수로 넘파이 배열 연산 가능, 출력은 텐서로
np.squeare(t) # 텐서를 넘파이 함수로 연산가능, 출력은 넘파이 배열
▶ 타입 변환
- 타입 변환은 성능을 크게 감소시킬 수 있다. 따라서 텐서플로우에서는 자동으로 타입을 변환하지 않는다.
- int와 float은 물론, float32와 float64 끼리도 연산이 불가능하다.
- 타입 변환이 필요한 경우 tf.cast() 함수를 사용한다.
t_float32 = tf.constant(40.)
t_float64 = tf.cast(t_float32, tf.float64)
▶ 변수
- 위에서 살펴본 텐서들은 변경 불가능
- 모델의 가중치를 훈련하거나 옵티마이저에서 과거 그레디언트를 갱신하기 위해서는 변하는 텐서가 필요하다.
v = tf.Variable([[1.,2.,3.],[4.,5.,6.]]) # 변수 생성
- 기본적은 동작은 텐서와 비슷하다.
- 변수를 갱신하는 방법
v # 결과 [[1.,2.,3.],[4.,5.,6.]]
v.assign(2*v) # 결과 [[2.,4.,6.],[8.,10.,12.]]
v[0,1].assign(42) # 결과 [[2.,42.,6.],[8.,10.,12.]]
v[:,2].assign([0.,1.]) # 결과 [[2.,42.,0.],[8.,10.,1.]]
v.scatter_nd_update(indices=[[0,0],[1,2]], updates=[100.,200.])# 결과 [[100.,42.,0.],[8.,10.,200.]]
- assign_add, assign_sub의 함수로 원하는 만큼 값을 증가, 감소시킬 수 있다.
- scatter_update, scatter_nd_update 등의 함수도 사용할 수 있습니다.
- tf.Variable은 tf.Tensor로 저장됩니다. 따라서 변수를 갱신하면 새로운 텐서가 만들어진다.
▶ 다른 여러 가지 데이터 구조
- 희소 텐서(tf.SparseTensor) : 대부분 0으로 이루어진 텐서를 효율적으로 나타내는 구조, tf.sparse 패키지로 희소 텐서 연산 가능
- 텐서 배열(tf.TensorArray) : 텐서의 리스트, 모든 텐서의 크기와 테이터 타입이 일치해야 한다.
- 래그드 텐서(tf.RaggedTensor) : 리스트의 리스트, 데이터 타입은 일치해야 하지만 길이는 달라도 된다. tf.ragged 패키지 사용
- 문자열 텐서 : tf.string 타입, 유니코드가 아닌 바이트 문자열로 표시, tf.strings 패키지로 유니코드, 바이트 사이의 변환 가능
- 집합 : 일반적인 텐서로 나타난다. ex) tf.constant([[1,2],[3,4]]) 에서 {1,2}, {3,4} 두 개의 집합이 나타난다. tf.sets 패키지 사용
- 큐 : 단계별로 텐서를 저장, tf.queue 패키지에 여러 가지 큐가 들어있다.
2. 사용자 정의 모델
● 사용자 정의 손실 함수 : 원하는 함수를 만들어 손실 함수로 사용하기
- 예시로 후버 손실 함수를 직접 만들어 사용한다. (사실 tf.keras.losses.Huber 클래스가 존재한다.)
- 후버 손실 함수
효율적인 코드를 위해서 tf. 함수를 이용한다.
def huber_fn(y_true, y_pred):
error = y_ture - y_pred
is_small_errer = tf.abs(error) < 1
squared_error = tf.square(error)/2
linear_error = tf.abs(error) - 0.5
return tf.where(is_small_error, squared_error, linear_error)
- 정의한 함수 훈련하기
model.compile(loss=huber_fn, optimizer='nadam')
model.fit(...)
- 사용자 정의를 갖는 모델 저장하기, 로드하기
keras는 모델 저장 시 함수 이름을 저장한다. 따라서 로드할 때 실제 이름과 함수를 연결시킨다.
model.save('my_custom_model.h5') # 모델저장
model = tf.keras.models.load_model('my_custom_model.h5'
,custom_object = {'huber_fn':huber_fn}) # 모델 로드
- 매개변수가 있는 함수를 사용할 때
keras.losses.Loss 상속을 받아 활용할 수 있다.
class HuberLoss(tf.keras.losses.Loss):
def __init__(self, threshold=1.0, **kwargs):
self.threshold = threshold
super().__init__(**kwargs)
def call(self,y_ture,y_pred):
error = tf.abs(y_true-y_pred)
is_small_error = error < self.threshold
squared_error = error **2
linear_error = 2 * self.threshold * error - self.threshold ** 2
return tf.where(is_small_error,squared_error,linear_error)
def get_config(self):
base_config = super().get_config()
return {**base_config, 'threshold':self.threshold}
get_config 함수를 통해 매개변수도 함께 저장이 가능하다.
model.compile(loss=HuberLoss(2.), optimizer='nadam') # 매개변수 threshold = 2.0
model.save('my_custom_model.h5')
model = tf.keras.models.load_model('my_custom_model.h5'
,custom_objects={'HuberLoss':HuberLoss})
● 활성화 함수, 초기화, 규제, 제한을 커스터마이징 하기
- softpuls, 글로럿 초기화, L1규제, 양수 가중치로 제한을 예시로 사용한다.
# 사용자 정의로 각각을 만들어 사용해보기
def my_softpuls(z): # softplus 활성화 함수
return tf.math.log(tf.exp(z) +1.0)
def my_glorot_initializer(shape, detype=tf.float32): # 글로럿 초기화
stddv = tf.sqrt(2./(shape[0] + shape[1]))
return tf.random.normal(shape, stddev=stddev,dtype=dtype)
def my_l1_regularizer(weights): # L1 규제
return tf.reduce_sum(tf.abs(0.01*weights))
def my_positive_weights(weights): # 가중치 범위 양수로 제한
return tf.where(weight<0.,tf.zeros_like(weights),weights)
위 조건을 이용한 layer
layer = tf.keras.layers.Dense(30,activation=my_softpuls
,kernel_initializer = my_glorot_initializer
,kernel_regularizer = my_l1_regularizer
,kernel_constraint = my_positive_weights)
손실 함수와 마찬가지로 저장해야 할 매개변수가 있다면 각각 class를 만들어 get_config 함수를 이용한다.
아래 예시는 L1 규제의 예시다.
활성화 함수는 keras.layers.Layer, 초기화는 keras.initializers.Initializer, 제한은 keras.constrains.Constrain을 상속한다.
class MyL1Regularizer(tf.keras.regularizers.Regularizer):
def __init__(self,factor):
self.factor = factor
def __call__(self, weights):
return tf.reduce_sum(tf.abs(self.factor * weights))
def get_config(self):
return {'factor':self.factor}
● 사용자 정의 지표
▶ 손실과 지표
- 손실과 지표는 개념적으로 모델을 평가한다는 점에서 같다.
- 손실은 훈련에 사용되는 값으로 미분 가능, 그레디언트가 0이 아니어야 한다. 는 특징이 있고 지표의 경우 사람이 모델을 평가할 때 사용하
기 때문에 직관적으로 이해가 쉬워야 한다.
- 앞에서 만든 후버 함수를 지표로 사용한 예시
model.compile(loss='mse', optimizer='nadam', metrics=[huber_fn])
▶ 스트리밍 지표
지표는 배치마다 계산되고 그 평균을 기록하다. 대부분 문제없이 사용이 가능하다.
하지만 총배치를 바탕으로 연산이 필요한 경우도 있다. 이때 지표를 스트리밍 지표라고 한다.
정밀도를 지표로 사용하는 경우
배치 1 - 예측 양성: 5, 진짜 양성:4, 정밀도 0.8
배치 2 - 예측 양성: 3 진짜 양성 0, 정밀도 0
단순 평균으로 계산한 정밀도 지표 : (0.8 + 0) /2 = 0.4
스트리밍 지표를 이용하여 전체 결과를 바탕으로 계산한 정밀도 지표 : (4+0) / (5+4) = 0.5
- 스트리밍 지표 만들기
keras.metrics.Metric을 상속받아 update_state 함수를 만든다.
class HuberMetric(tf.keras.metrics.Metric):
def __init__(self, threshold=1.0, **kwargs):
super().__init__(**kwargs) # 기본 매개변수 처리
self.threshold = threshold
self.huber_fn = create_huber(threshold)
self.total = self.add_weight('total', initializer='zeros')
self.count = self.add_weight('count', initializer='zeros')
def update_state(self, y_true, y_pred, sample_weight=None):
metric = self.huber_fn(y_true,y_pred)
self.total.assign_add(tf.reduce_sum(metric)) # 전체 값 저장
self.count.assign_add(tf.cast(tf.size(y_true), tf.float32)) # 전체 개수 저장
def result(self):
return self.total/self.count
def get_config(self):
base_config = super().get_config()
return {**base_config, 'threshold': self.threshold}
● 사용자 층 만들기
- 정의되지 않은 새로운 층을 만들거나 반복되는 층을 묶어 하나의 층으로 만들 때 사용할 수 있는 방법이다.
▶ 가중치가 없는 층
- Flatten과 ReLU와 같은 층은 가중치가 없다.
- keras.layers.Lambda를 이용하여 만들 수 있다.
exponential_layer = keras.layers.Lambda(lambda x: tf.exp(x))
▶ 가중치가 있는 층
- keras.layers.Layer를 상속받아 만들 수 있다.
1. 간단한 Dense layer 구현
class MyDense(tf.keras.layers.Layer):
def __init__(self, units, activation=None, **kwargs):
super().__init__(**kwargs)
self.units = units
self.activation = keras.activations.get(activation)
def build(self, batch_input_shape):
self.kernel = self.add_weight(name='kernel', shape=[batch_input_shape[-1], self.unit]
,initializer = 'glorot_normal')
self.bias = self.add_weight(name='bias',shape=[self.units],initializer='zeros')
super().build(batch_input_shape) # 마지막에 호출해야 한다.
def call(self,X):
return self.activation(X @ self.kernel + self.bias)
def compute_output_shape(self, batch_input_shape):
return tf.TensorShape(batch_input_shape.as_list()[:-1]+ [self.units])
def get_config(self):
base_config = super().get_config()
return {**base_config, 'units':self.units
,'activation':tf.keras.activations.serialize(self.activation)}
- units와 activation을 입력받는다. (출력층 수와 활성화 함수)
- build 메서드에서 커널의 가중치를 설정한다.
- call 메소드에서 해당 layer의 연산을 한다.
- compute_output_shape()에서 layer의 출력 크기를 반환한다.
- get_config 메서드에 필요한 매개변수들을 저장한다.
2. 다중 입력, 다중 출력 층
- 2개의 입력과 3개의 출력을 갖는 층 만들기
class MyMultilayer(tf.keras.layers.Layer):
def call(self,X):
X1,X2 = X
return [X1 + X2, X1 * X2, X1/X2]
def compute_output_shape(self, batch_input_shape):
b1, b2 = batch_input_shape
return [b1,b1,b1]
3. 훈련과 테스트에서 다르게 작동하는 층 만들기
- call 메소드에 training 변수를 입력한다.
class MyGaussianNoise(keras.layers.Layer):
def __init__(self, stddev, **kwargs):
super().__init__(**kwargs)
self.stddev = stddev
def call(self,X,training = None): # training option을 부여한다.
if training:
noise = tf.random.normal(tf.shape(X),stddev=self.stddev) #훈련중에만 가우시안 잡음을 추가
return X + noise
else:
return X
def compute_output_shape(self, batch_input_shape):
return bacth_input_shape
● 사용자 정의 모델 만들기
- keras.Model을 상속받아 만든다.
- 아래 그림의 모델을 ResidualBlock과 함께 구현해 본다.
1. RegidualBlock 구현 -> 구성 2개의 Dense layer와 스킵 연결된 값을 더하여 출력
class ResidualBlock(keras.layers.Layer):
def __init__(self, n_layer, n_neurons, **kwargs):
super().__init__(**kwargs)
self.hidden = [keras.layers.Dense(n_neurons, activation='elu'
,kernel_iniitalizer='he_normal') for _ in range(n_layer)]
def call(self, inputs):
Z = inputs
for layer in self.hidden: # 2번 Dense 통과
Z = layer(Z)
return inputs + Z # 스킵연결
2. 그림 순서로 layer를 깔아 모델 제작
class ResidualRegressor(keras.Model):
def __init__(self,output_dim,**kwargs):
super().__init__(**kwargs)
self.hidden1 = keras.layers.Dense(30,activation='elu'
,kernel_initializer='he_normal')
self.block1 = ResidualBlock(2,30)
self.block2 = ResidualBlock(2,30)
self.out = keras.layers.Dense(output_dim)
def call(self, inputs):
Z = self.hidden1(inputs)
for _ in range(1+3):
Z = self.block1(Z)
Z = self.block2(Z)
return self.out(Z)
save 메서드를 이용하여 모델을 저장하려면(또는 load 하려면) 두 class 모두 get_config 메소드를 정의해야 한다.
Model class는 Layer처럼 사용할 수 있고 compile, fit, evaluate, predict 모두 가능하다. (get_layer, save 도 가능)
▶ Model이 Layer 보다 많은 기능을 갖지만 모델로 정의하여 사용하지 않는 이유
- 기술적으로는 가능하다. 단 모델의 구성요소로서 모델과 구분하기 위해 다르게 사용하는 것
● 모델 구성 요소에 기반한 손실과 지표
- 앞에서 정의한 손실과 지표를 모두 y_ture와 y_pred에 기반한다.
- 은닉층의 가중치나 활성화 함수 등에 기반한 손실을 정의해야 할 때가 있다.(ex. 재구성 손실, 17장에서 자세하게 나온다네요)
- 주 손실과 재구성 손실을 더함으로 일반화 성능을 향상시킬 수 있다.(규제처럼)
- call 메서드에서 add_loss()를 이용한다.
class RecondtructingRegressor(keras.Model):
def __init__(self,output_dim, **kwargs):
super().__init__(**kwargs)
self.hidden = [keras.layers.Dense(30, activation='selu'
,kernel_iniitalizer='lecun_normal') for _ in range(5)]
self.out = keras.layers.Dense(output_dim)
def build(self,bacth_input_shape):
n_input = bacth_input_shape[-1]
self.reconstruct = keras.layers.Dense(n_inputs)
super().build(bacth_input_shape)
def call(self, inputs):
Z = inputs
for layer in self.hidden:
Z = layer(Z)
reconstruction = self.reconstruct(Z)
recon_loss = tf.reduce_mean(tf.square(reconstruction - inputs))
self.add_loss(0.05*recon_loss) # 오류에 reconstruct loss 더하기
return self.out(Z)
● 자동 미분을 이용한 그레디언트 계산하기
- 변수(tf.Variable)에 대해 자동으로 기울기를 계산할 수 있다.
- tf.GradientTape()를 이용한다. (너무 큰 값에 대한 미분을 불가능하다. ex. tf.Variable(100.))
def f(w1, w2):
return 3*w1**2 + 2*w1*w2
w1,w2 = tf.Variable(5.), tf.Variable(3.)
with tf.GradientTape() as tape:
z = f(w1,w2)
gradients = tape.gradient(z,[w1,w2]) # 호출 후에 테이프는 사라진다. 2번 호출하면 에러난다.
gradients # 결과 36.0, 10.0
- graient를 2번 이상 호출해야 하는 경우
with tf.GradientTape(persistent=True) as tape: # 여러번 사용하는 방법
z = f(w1,w2)
dz_dw1 = tape.gradient(z, w1)
dz_dw2 = tape.gradient(z, w2)
del tape # 꼭 삭제를 진행하여 리소스를 해제해야 한다.
dz_dw1, dz_dw2 # 36.0, 10.0
- 변수가 아닌 객체에 대한 미분
기본적으로 변수가 아닌 객체에 대한 자동 미분을 불가능하다. 아래 방법으로 강제할 수 있다.
c1,c2 = tf.constant(5.), tf.constant(3.)
with tf.GradientTape() as tape:
z = f(c1,c2)
tape.gradient(z,[c1,c2]) # None, None
with tf.GradientTape() as tape:
tape.watch(c1)
tape.watch(c2)
z = f(c1,c2)
tape.gradient(z,[c1,c2]) # 36.0, 10.0
- 일부분의 그레디언트가 역전파에서 전달되지 못하도록 하는 방법
- 제외하고 싶은 부분을 tf.stop_gradient 함수로 감싼다.
ef f(w1,w2):
return 3*w1**2 + tf.stop_gradient(2 * w1 * w2)
with tf.GradientTape() as tape:
z = f(w1,w2)
gradients = tape.gradient(z,[w1,w2])
gradients # 30.0, None
● 사용자 정의 훈련 반복
- fit() 메서드의 유연성이 충분하지 않을 때 사용 가능한 방법이다.
장점: 완벽하게 훈련을 제어할 수 있다. ex) 2개의 옵티마이저를 사용하는 경우 compile과 fit 메서드로는 불가능하다.
단점: 코드가 길어지고, 코드에 확신이 없을 수 있다. 버그가 발생하기 쉽고 유지보수가 힘들다.
- 간단한 모델을 만든 후 훈련시키는 예시(직접 훈련시키기 때문에 compile이 필요 없다.)
l2_reg = keras.regularizers.l2(0.05) # 규제
# 모델
model = keras.models.Sequential([keras.layers.Dense(30,activation='elu'
,kernel_initializer='he_normal'
,kernel_regularizer=l2_reg)
,keras.layers.Dense(1,kernel_regularizer=l2_reg)])
# 랜덤하게 배치를 추출하는 함수
def random_batch(X,y,batch_size=32):
idx = np.random.randint(len(X), size=batch_size)
return X[idx], y[idx]
# 현재 스탭, 에폭, 손실등 상태를 출력하는 함수
def print_status_bar(iteration, total, loss, metrics=None):
metrics = ' - '.join(['{}: {:.4f}'.format(m.name, m.result())
for m in [loss] + (metrics or [])])
end = '' if iteration < total else '\n'
print('\r{}/{} - '.format(iteration, total) + metrics, end = end)
- 훈련 조건
n_epochs = 5
batch_size = 32
n_steps = len(X_train) // batch_size
optimizer = keras.optimizers.Nadam(lr=0.01)
loss_fn = keras.losses.mean_squared_error
mean_loss = keras.metrics.Mean()
metrics = [keras.metrics.MeanAbsoluteError()]
- 훈련
for epoch in range(1, n_epochs+1):
print('epoch {}/{}'.format(epoch,n_epochs))
for step in range(1, n_steps+1):
X_batch, y_batch = random_batch(X_train_scaled, y_train)
with tf.GradientTape() as tape:
y_pred = model(X_batch, training=True)
main_loss = tf.reduce_mean(loss_fn(y_batch,y_pred))
loss = tf.add_n([main_loss]+model.losses)
gradients = tape.gradient(loss,model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
mean_loss(loss)
for metric in metrics:
metric(y_batch, y_pred)
print_status_bar(step*batch_size, len(y_train), mean_loss, metrics)
print_status_bar(len(y_train), len(y_train), mean_loss, metrics)
for metric in [mean_loss] + metrics:
metric.reset_states()
3. 텐서플로우 함수 만들기
- python으로 정의됨 함수를 텐서플로우 함수로 변형할 수 있다. (결과가 텐서로 반환된다.)
- 텐서플로우가 함수 내부에 사용하지 않는 노드를 제거하고 표현을 단순화하여 최적화한다.
- 파이썬 함수를 빠르게 실행하고 싶다면 텐서플로우 함수로 변환하는 것이 좋다.
- 오토그래프와 트레이싱 방법을 통해 python 함수를 텐서플로우 함수로 변경한다.
def cube(x): # python 함수
return x **3
cube(tf.constant(2.0)) # 텐서를 입력시 텐서로 결과를 반환한다.
tf_cube = tf.function(cube) # 텐서플로우 함수로 변경
# 데코레이터를 이용하여 텐서플로우 함수로 변경
@tf.function
def tf_cube(x):
return x**3
tf_cube.python_function(4) # 텐서플로우 함수에서 python 함수 결과 가져오기
▶ 텐서플로우 함수를 만들 때 규칙
1. 함수 내에 다른 라이브러리 함수가 포함된다면 트레이싱 과정에서 실행된다. 하지만 가능하다면 tf. 함수를 이용하자.
- 난수 생성 함수에서 문제가 될 수 있다.
- 텐서플로우에서 지원하지 않는 연산의 경우 실행이 불가능하다.
2. 그래프 모드에서 처음 함수만 @tf.function으로 감싸주면 된다.
- ex. 사용자 정의 층에서 __call__에만 감싸주면 된다.
3. 변수를 만든다면 처음 호출될 때만 만들어야 한다.
- 일반적으로 함수 밖에서 변수를 만드는 것이 좋다.
- 변수에 새로운 값을 할당하려면 assign() 함수를 이용하자.
4. 파이썬 코드가 텐서플로우에서 돌아가야 한다.
5. 반복문에서 for i in range(x) 대신 for i in tf.range(x)를 사용하여야 한다.
6. 성능면에서 반복문 대신 벡터화된 구현을 사용하는 것이 좋다.
'python > 핸즈온 머신러닝' 카테고리의 다른 글
[핸즈온 머신러닝] 11장. 심층 신경망 훈련하기 (0) | 2021.03.28 |
---|---|
[핸즈온 머신러닝] 3장. 분류 (0) | 2021.01.31 |
[핸즈온 머신러닝] 2장. 머신러닝 프로젝트 처음부터 끝까지 (0) | 2021.01.21 |
[핸즈온 머신러닝] 1장. 한 눈에 보는 머신러닝 (0) | 2021.01.21 |
댓글