① LB(Large Batch)는 학습 속도도 빠르고, 성능도 좋을 수 있다 ② LB 일수록 더 큰 학습률(Learning Rate)를 사용하면 좋다 (하지만 큰 학습률을 사용하면 수렴이 안되는 문제가?) ③ 하지만 LB보다 SB(Small Batch)가 안정적으로 학습할 수 있어 일반화(Generalization)에 강하다
Gradient Accumulation 방법이 배치 크기와 관련이 있다보니 간단하게 배치 크기 관련 이야기를 해보았는데, 이번 글은 어떤 이유에서든 "비록 가지고 있는 GPU memory가 제한적이지만 일단 LB를 사용해서 학습시키고 싶다."라는 생각입니다.
Gradient Accumulation?
큰 배치 크기를 활용할때 가장 큰 문제는 '성능이 좋아질 수 있다', '안좋아질 수 있다'가 아닐겁니다. 아마도 가장 중요하게 고려하는 것은 지금 보유하고 있는 GPU를 가지고 '1,024, 2,048, ... 처럼 큰 배치 크기를 돌려볼 수 있느냐'입니다.
누구나 다 겪는 문제인데요. 만약 이와 같은 문제에 직면했다면, 이를 해결하기 위한 방법 중 하나로 Gradient Accumulation 방법을 생각해볼 수 있습니다. 가장 좋은 방법은 돈을 들이는 것...
일반적인 학습 방법과 Gradient Accumulation 방법의 차이는 아래 그림에서 바로 확인할 수 있습니다.
General Training vs Training with GA
일반적인 학습 방법이 미니 배치를 통해 gradient를 구한 후, Update를 즉시 진행하는 방법이라면,
Gradient Accumulation 방법을 포함한 학습은 미니 배치를 통해 구해진 gradient를 n-step동안 Global Gradients에 누적시킨 후, 한번에 업데이트하는 방법입니다.
매우 간단합니다. 즉, 핵심 아이디어는 256 mini-batch를 통해 gradient를 업데이트하는 것과 64 mini-batch를 4번(64 * 4 = 256) 누적하여 업데이트하는 것이 비슷한 결과를 가져다 줄 것이라는 생각입니다.
이 예와 같다면, 우리가 보유하고 있는 GPU memory 한계상 64 mini-batch 밖에 사용할 수 없다면, 이 학습 방법을 통해 4번 누적하여 업데이트할 경우 256 mini-batch를 사용하여 학습하는 것과 다름없게 되는 셈이죠.
하지만 이 방법은 memory issue를 중점적으로 해결하고자 하는 방법이지 속도, 성능과는 아직 explicit하게 증명된 바가 없습니다.
아래의 간단한 구현을 통해 위에서 글로서 살펴본 것보다 좀 더 쉽게 이해할 수 있을 거에요.
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.models import Model
import tensorflow as tf
print(tf.__version__)
# 스타일이나 표현하고 싶은 방법에 따라 다름, 여기선 가장 기본적인 문법
# 경우에 따라 특수문자를 표현하기 위해 %, - 등을 집어넣을 수 있다
# ex) r'([\w.%-]+@ 등)'
re.findall(r'(\w+)@(\w+).(\w+)', 'test123@tistory.com')
# [('test123', 'tistory', 'com')]
re.findall(r'(\w+)-(\w+)-(\w+)', '010-1234-5678')
# [('010', '1234', '5678')]
아래 예제에서는 findall로 대부분 예제를 작성했지만, 주로 search, match, findall을 사용합니다.
Pandas 라이브러리는 데이터 분석과 관련한 다양한 함수를 제공해줌과 동시에 NumPy 라이브러리와 같이 빠른 성능을 보여줍니다.
하지만 만약 하고 있는 작업을 NumPy Array를 활용한 연산으로 수행할 수 있다면, 되도록이면 NumPy 연산으로 바꿔보는 것을 추천합니다.
생각치 못한 곳에서 속도 향상이 일어날 수 있고, 이후 처리 작업으로 전환하기에도 매우 편리합니다.
4. 속도가 정말 느린 것 같다면 배치 연산을 활용
전체 데이터에 전처리(preprocessing) 작업을 수행한 후, 처리된 데이터를 stack, concat과 같은 함수를 사용하여 새로운 저장 공간(ex: list or DataFrame etc.)에 쌓는 경우 속도 저하 현상이 발생할 수 있습니다.
이는 전체 데이터를 한번에 쌓고자하는 작업 때문에 병목 현상이 발생한 경우인데 특히, 우리가 자주쓰는 loc, iloc 함수와 같이 인덱싱 기능이 포함된 함수를 사용할 경우 자주 만나볼 수 있는 문제입니다.
속도 향상을 위해 전체를 한번에 쌓는 방법보다 분할하여 쌓은 뒤, 합쳐(merge)주는 순서로 변경해보세요. 신경망 모델 학습 시에 배치 연산을 하는 것처럼 바꿔보면 큰 속도 향상을 기대해볼 수 있습니다. (ex: 1,000개 데이터를 100개씩 나누어 10번 처리한 뒤, 한번에 합쳐주는 방법을 의미)
매우 쉬운 방법이지만 한번에 떠오르기 쉽지 않습니다.
5. Class Config 설정
클래스를 정의할 때 여러 가지 하이퍼파라미터 정의가 필요할 수 있습니다.
__init__함수내 변수로 선언하여 관리할 수 있지만, 다루는 함수가 많아지거나 복잡해질수록 무언가 실수로 하나씩 변경하지 못하는 실수가 발생하기 마련입니다.
이를 방지하고자 관리하는 스타일에 따라 (1) Config 클래스(2) 외부 파일 저장(3) Config dict를 인자로 넘기기 등 방법을 사용할 수 있는데 여기서 볼 것은 (3)번 방법입니다.
**kwargs(dict type)를 사용해서 정의된 하이퍼파라미터를 넘겨준 후, 클래스 변수로 등록하는 과정입니다. (+ *args는 tuple type)
class Test:
def __init__(self):
pass
def set_config(self, **kwargs):
self.add_entries(**kwargs)
def add_entries(self, **kwargs):
for key, value in kwargs.items():
self.__dict__[key] = value
config = {
'num_epochs': 10,
'batch_size': 1024,
'hidden_nums': 16,
}
t = Test()
t.set_config(**config)
# 결과는 __dict__로 확인할 수 있다
# {'num_epochs': 10, 'batch_size': 1024, 'hidden_nums': 16}
print(t.__dict__)
6. Jupyter Notebook으로 수식 쓰기
수학 공식을 표현하고 싶을 때, LaTeX 방법으로 수식을 만들 수 있지만 복잡하고 어렵습니다.
파이썬으로 크롤링을 해야할 때, 스크래피(Scrapy) 등 다양한 방법이 있지만 기본적으로 우리가 사용하는 방법은 BeautifulSoup과 Selenium 라이브러리입니다.
특히, Selenium은 ChromeDriver을 활용해서 뷰(View)를 확인할 수 있는 장점이 있어 명확하고, 몇몇 함수를 사용해 더욱 편리하게 크롤링을 진행할 수 있습니다. 뿐만 아니라 버튼 클릭부터 페이지 변경까지 매우 편리합니다.
그런데 만약 버튼 클릭, 페이지네이션(Pagination), 링크 들어가서 또 링크를 들어가야하는 등 여러 가지 복잡한 액션이 포함된 크롤링을 진행해야할 때는 어떨까요? 뿐만 아니라 파싱해야할 정보량도 많다면? 이때, Selenium만 활용하면 속도가 매우 느리다는 단점을 느낄 수 있습니다. 음... 굳이 별다른 이유를 설명하지 않아도 일단 뷰를 우리에게 제공한다는 점을 생각해보면 일반적으로 납득이 가는 상황입니다.
이에 대한 해결방법으로 BeautifulSoup만 활용해보는 것입니다. BeautifulSoup은 HTTP 통신으로 시각적인 정보를 제공하지 않고 바로 파싱을 진행할 수 있기 때문에 크롤링 속도가 매우 빠르다는 장점이 있습니다.
만약, 여기서 페이지네이션이나 링크를 타고 들어가는 크롤링 과정을 뷰로 확인하고 싶다면, Selenium을 활용하면 되겠죠. 그런데 단점은 속도잖아요. 속도가 걱정된다면?
두 방법을 섞어서 활용해보세요. 여러 가지 액션은 Selenium, 파싱은 BeautifulSoup를 활용하면 빠른 속도로 크롤링을 진행할 수 있습니다.
MLP-Mixer model은 MLP만 사용하는 아키텍처이며, 두 가지 유형의 MLP layer를 포함합니다.
각 이미지 패치를 독립적으로 사용함으로써 per-location feature를 혼합합니다
채널축으로 적용하여 spatial information을 혼합합니다.
이 방법은 어떻게 보면 Xception model에서 대표적으로 사용한 depthwise separable convolution 구조와 유사해보이지만, 차이점으로는 two chained dense transforms, no max pooling, layer normalization instead of batch normalization 사용에 있습니다.
Implement the MLP-Mixer model
class MLPMixerLayer(layers.Layer):
def __init__(self, num_patches, hidden_units, dropout_rate, *args, **kwargs):
super(MLPMixerLayer, self).__init__(*args, **kwargs)
self.mlp1 = keras.Sequential(
[
layers.Dense(units=num_patches),
tfa.layers.GELU(),
layers.Dense(units=num_patches),
layers.Dropout(rate=dropout_rate),
]
)
self.mlp2 = keras.Sequential(
[
layers.Dense(units=num_patches),
tfa.layers.GELU(),
layers.Dense(units=embedding_dim),
layers.Dropout(rate=dropout_rate),
]
)
self.normalize = layers.LayerNormalization(epsilon=1e-6)
def call(self, inputs):
# Apply layer normalization.
x = self.normalize(inputs)
# [num_batches, num_patches, hidden_units] -> [num_batches, hidden_units, num_patches]
x_channels = tf.linalg.matrix_transpose(x)
# mlp1을 채널 독립적으로 적용합니다.
# Dense Layer는 2-D 이상일 경우, 마지막 차원에서 가중치 연산이 일어납니다 -> 일종의 trick으로 사용
mlp1_outputs = self.mlp1(x_channels)
# [num_batches, hidden_dim, num_patches] -> [num_batches, num_patches, hidden_units]
mlp1_outputs = tf.linalg.matrix_transpose(mlp1_outputs)
# Add skip connection.
x = mlp1_outputs + inputs
# Apply layer normalization.
x_patches = self.normalize(x)
# mlp2를 각 패치에 독립적으로 적용합니다.
mlp2_outputs = self.mlp2(x_patches)
# Add skip connection.
x = x + mlp2_outputs
return x
Build, train, and evaluate the MLP-Mixer model
mlpmixer_blocks = keras.Sequential(
[MLPMixerLayer(num_patches, embedding_dim, dropout_rate) for _ in range(num_blocks)]
)
learning_rate = 0.005
mlpmixer_classifier = build_classifier(mlpmixer_blocks)
history = run_experiment(mlpmixer_classifier)
MLP-Mixer 모델은 다른 모델에 비해 사용되는 파라미터 수가 적습니다.
MLP-Mixer 논문에 언급된 바에 따르면, large-dataset에 pre-trained하고 modern regularization 방법을 함께 사용한다면 SOTA 모델들의 score와 경쟁할 수 있는 수준의 score를 얻을 수 있다고 합니다. 임베딩 차원, 블록 수, 입력 이미지 크기를 늘리거나 다른 패치 크기를 사용하거나 모델을 더 오랫동안 학습시키면 더 좋은 성능을 얻을 수 있습니다.
The FNet model
FNet 모델은 Transformer block과 유사한 구조의 블록을 사용합니다. 하지만 FNet 모델은 self-attention layer 대신, 파라미터에서 자유로운 2D Fourier transformation layer를 사용합니다.
이 예제에선 Structured data classification을 위해 Bryan Lim et al.(arxiv.org/abs/1912.09363)에서 제안된 Gated Residual Networks와 Variable Selection Networks를 사용해봅니다. GRN은 필요에 따라 비선형성을 제공할 수 있는 유연성을 제공하고, VSN은 성능에 부정적인 영향을 주는 불필요한 feature를 제거하는 역할을 수행합니다. 또한, 이 두 가지를 함께 사용했을 때 모델의 학습 능력을 주요하게 향상시킬 수 있습니다.
이 예제는 논문에서 제안된 전체 모델을 구현하진 않고, 부분적으로 GRN과 VSN만 구현하게 됩니다.
※ TensorFlow 2.3 이상의 버전에서 정상 작동합니다.
The Dataset
데이터셋은 UC Irvine Machine Learning Repo에서 제공하는 United States Census Income Dataset을 사용합니다. 이진 분류이며, 연소득 50K가 넘는 사람인지를 구분하는 문제입니다.
데이터셋은 41개의 feature(7 numerical + 34 categorical)과 ~300K의 인스턴스로 이루어져 있습니다.
Setup
import math
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
reading, parsing, encoding이 원할하게 수행될 수 있도록 metadata를 정의하겠습니다.
# Target feature name.
TARGET_FEATURE_NAME = "income_level"
# Weight column name.
WEIGHT_COLUMN_NAME = "instance_weight"
# Numeric feature names.
NUMERIC_FEATURE_NAMES = [
"age", "wage_per_hour",
"capital_gains", "capital_losses",
"dividends_from_stocks", "num_persons_worked_for_employer",
"weeks_worked_in_year"
]
# Categorical feature와 feature가 가지고 있는 unique label을 구합니다.
CATEGORICAL_FEATURES_WITH_VOCABULARY = {
feature_name: sorted([str(value) for value in list(data[feature_name].unique())])
for feature_name in CSV_HEADER
if feature_name
not in list(NUMERIC_FEATURE_NAMES + [WEIGHT_COLUMN_NAME, TARGET_FEATURE_NAME])
}
# All features names.
FEATURE_NAMES = NUMERIC_FEATURE_NAMES + list(
CATEGORICAL_FEATURES_WITH_VOCABULARY.keys()
)
# default 값 처리를 위해 categorical은 ["NA"], 나머지는 [0.0]으로 초기화합니다.
COLUMN_DEFAULTS = [
[0.0]
if feature_name in NUMERIC_FEATURE_NAMES + [TARGET_FEATURE_NAME, WEIGHT_COLUMN_NAME]
else ["NA"]
for feature_name in CSV_HEADER
]
Create a tf.data.Dataset for training and evaluation
file을 read and parsing하고 feature, label 변환을 수행하기 위해 tf.data.Dataset을 사용합니다.
from tensorflow.keras.layers.experimental.preprocessing import StringLookup
def process(features, target):
for feature_name in features:
if feature_name in CATEGORICAL_FEATURES_WITH_VOCABULARY:
# Categorical feature이면 string으로 cast합니다.
features[feature_name] = tf.cast(features[feature_name], tf.dtypes.string)
# Get the instance weight.
weight = features.pop(WEIGHT_COLUMN_NAME)
return features, target, weight
def get_dataset_from_csv(csv_file_path, shuffle=False, batch_size=128):
# csv 파일을 읽어오는 method
dataset = tf.data.experimental.make_csv_dataset(
csv_file_path,
batch_size=batch_size,
column_names=CSV_HEADER,
column_defaults=COLUMN_DEFAULTS,
label_name=TARGET_FEATURE_NAME,
num_epochs=1,
header=False,
shuffle=shuffle,
).map(process)
return dataset
Create model inputs
def create_model_inputs():
inputs = {}
for feature_name in FEATURE_NAMES:
if feature_name in NUMERIC_FEATURE_NAMES:
inputs[feature_name] = layers.Input(
name=feature_name, shape=(), dtype=tf.float32
)
else:
inputs[feature_name] = layers.Input(
name=feature_name, shape=(), dtype=tf.string
)
return inputs
Encode input features
Categorical feature는 encoding_size dimension을 가지는 Embedding Layer를 사용해서 인코딩합니다. Numerical feature는 layers.Dense를 사용해서 encoding_size dimension을 가지도록 선형 변환합니다. 따라서, 모든 인코딩된 feature는 동일한 dimension을 가지도록 구성됩니다.
embedding representation으로 변환하는 작업은 아래 테스트 코드로 확인해보세요.
index = StringLookup(
vocabulary=CATEGORICAL_FEATURES_WITH_VOCABULARY['country_of_birth_father'],
mask_token=None, num_oov_indices=0
)
test = train_data['country_of_birth_father'][:32]
# print(index(test))
embedding_ecoder = layers.Embedding(
input_dim=len(CATEGORICAL_FEATURES_WITH_VOCABULARY['country_of_birth_father']),
output_dim=200
)
print(embedding_ecoder(index(test)))
from tensorflow.keras.layers.experimental.preprocessing import CategoryEncoding
from tensorflow.keras.layers.experimental.preprocessing import StringLookup
def encode_inputs(inputs, encoding_size):
encoded_features = []
for feature_name in inputs:
if feature_name in CATEGORICAL_FEATURES_WITH_VOCABULARY:
vocabulary = CATEGORICAL_FEATURES_WITH_VOCABULARY[feature_name]
# Create a lookup to convert a string values to an integer indices.
# String value를 Integer index로 변환합니다.
# 따라서, 동일한 label은 동일한 index로 변환됩니다.
index = StringLookup(
vocabulary=vocabulary, mask_token=None, num_oov_indices=0
)
# String -> Integer index
value_index = index(inputs[feature_name])
# encoding_size dimension을 가지는 Embedding layer를 정의합니다.
embedding_ecoder = layers.Embedding(
input_dim=len(vocabulary), output_dim=encoding_size
)
# index value를 embedding representation으로 변환합니다.
encoded_feature = embedding_ecoder(value_index)
else:
# encoding_size unit을 가지는 Dense layer를 활용해서 numeric feature를 인코딩합니다.
encoded_feature = tf.expand_dims(inputs[feature_name], -1)
encoded_feature = layers.Dense(units=encoding_size)(encoded_feature)
encoded_features.append(encoded_feature)
return encoded_features
StringLookup은 엑셀에서 굳이 비슷한 이름을 가진 함수를 찾아보자면, VLookup, HLookup처럼 인덱스를 찾아주는 함수인데, tensorflow version 2.4부터 tf.keras.layers.experimental.preprocessing.StringLookUp으로 만나볼 수 있습니다.
그런데 TesnorFlow 2.4 이전 버전은 StringLookUp 함수가 제공되지 않기 때문에 다른 방법으로 사용해야 합니다. 물론 다양한 방법이 있겠지만, tensorflow가 제공하는 함수를 활용해볼거라면 tensorflow lookup 모듈을 고려해볼 수 있었습니다.
이를 위해선 두 가지를 사용해야 합니다.
tf.lookup.StaticHashTable
tf.lookup.KeyValueTensorInitializer
tf.lookup.KeyValueTensorInitializer에서 vocab(key)와 data value(value)를 준비해주면, StaticHashTable에 넣어 Table을 만드는 구조인 것으로 보입니다.