Convolution 구현해보기

먼저, 3x3 필터를 정의합니다. 어디선가 많이 보셨을 법한 필터입니다.(가로선을 검출하는)

filter = [[-1, 0, 1], 
        [-2, 0, 2], 
        [-1, 0, 1]]

 

벡터화(Vectorization)을 수행하지 않는 컨볼루션의 구현은 매우 간단합니다. for-loop 2개로 구현할 수 있습니다.

컨볼루션은 이미지의 영역과 컨볼루션 필터를 element-wise 곱한 뒤, 전부 합칩니다. 다음 코드를 보면 알 수 있습니다.
패딩을 사용하지 않기 때문에 시작 지점은 이미지의 (1, 1) 좌표입니다.

x_shape = img.shape[0]
y_shape = img.shape[1]

for x in range(1,x_shape-1):
  for y in range(1,y_shape-1):
      convolution = 0.0
      convolution = convolution + (img[x - 1, y-1] * filter[0][0])
      convolution = convolution + (img[x, y-1] * filter[0][1])
      convolution = convolution + (img[x + 1, y-1] * filter[0][2])
      convolution = convolution + (img[x-1, y] * filter[1][0])
      convolution = convolution + (img[x, y] * filter[1][1])
      convolution = convolution + (img[x+1, y] * filter[1][2])
      convolution = convolution + (img[x-1, y+1] * filter[2][0])
      convolution = convolution + (img[x, y+1] * filter[2][1])
      convolution = convolution + (img[x+1, y+1] * filter[2][2])

      if(convolution < 0):
        convolution = 0
      elif(convolution > 255):
        convolution = 255
  • x_shape와 y_shape는 이미지의 크기를 나타냅니다.
  • for-loop 끝의 if문은 이미지 값의 범위 0 ~ 255를 위해 사용합니다.
  • 아래 테이블에서 위의 코드를 좀 더 편하게 볼 수 있습니다.
img[x-1, y-1] * filter[0][0] img[x, y-1] * filter[0][1] img[x+1, y-1] * filter[0][2]
img[x-1, y] * filter[1][0] img[x, y] * filter[1][1] img[x+1, y] * filter[1][2]
img[x-1, y+1] * filter[2][0] img[x, y+1] * filter[2][1] img[x+1, y+1] * filter[2][2]

 

MaxPooling 구현해보기

흔히 사용하는 (2,2)의 MaxPooling을 구현해보도록 합니다.

new_x_shape = int(x_shape / 2)
new_y_shape = int(y_shape / 2)

# 풀링이 적용될 이미지를 만듭니다.
pooled_img = np.zeros((new_x_shape, new_y_shape))

# 2는 stride를 나타냅니다.
for x in range(0, x_shape, 2):
	for y in range(0, y_shape, 2):
    	pixels = []
        pixels.append(img[x, y])
        pixels.append(img[x + 1, y])
        pixels.append(img[x, y + 1])
        pixels.append(img[x + 1, y + 1])
        # 최댓값만 저장합니다.
        pooled_img[int(x/2), int(y/2)] = max(pixels)
img[x, y] img[x + 1, y]
img[x, y + 1] img[x + 1, y + 1]

 

 

os.rename()을 사용한다.

import os

os.rename('./test.txt', './rename.txt')

파싱할 xml은 다음과 같습니다.

ml_string = '''<?xml version="1.0"?> 
<data> 
  <tool name="Keras"> 
      <rank>1</rank> 
      <content>good</content>
      <merge name="TensorFlow" year="2020"/>
  </tool> 
  <tool name="TensorFlow"> 
      <rank>1</rank>
      <content>nice</content>
  </tool>
  
  <tool2 name="PyTorch"> 
      <rank>2</rank> 
      <content>research</content>
  </tool2> 
  <tool2 name="MXNet"> 
      <rank>3</rank> 
      <content>well</content>
  </tool2> 
</data> 
'''

ElementTree를 통해 xml 파일을 지정하고 root에 접근하는 방법은 다음과 같습니다.

tree = elemTree.parse('ml.xml')
root = tree.getroot()

* 여기서는 예제를 위해 string을 사용하므로 다음을 사용합니다. 파일을 통한 파싱은 위 코드로 해야합니다.
  아래 코드는 관련 없습니다.

root = ET.fromstring(ml_string)

 

태그와 태그가 가지고 있는 속성은 다음과 같이 확인할 수 있습니다.

print(root.tag, root.attrib)
  • data {}

 

ElementTree에서는 대표적으로 find(), findall(), iter() 함수를 많이 사용합니다.

find()

print(root.find('tool'))
print(root.find('tool').tag, '|', root.find('tool').attrib)

# 하위 태그를 탐색할 수 있습니다.
for child in root.find('tool'):
    print(child.tag, child.attrib)
  • <Element 'tool' at 0x000001E489FB5AE8>
    tool | {'name': 'Keras'}
    rank {}
    content {}
    merge {'name': 'TensorFlow', 'year': '2020'}
  • root.find('tag_name')은 여러 개의 태그 중에서 가장 첫 번째 태그를 가져옵니다.
  • for-loop를 통해 하위 태그를 확인하고 있습니다.

 

findall()

print(root.findall('tool'))

# <tool/> 태그를 전부 탐색할 수 있습니다.
for root_e in root.findall('tool'):
    print(root_e.tag, root_e.attrib)
  • [<Element 'tool' at 0x000001E489FB5AE8>, <Element 'tool' at 0x000001E489FBE228>]
    tool {'name': 'Keras'}
    tool {'name': 'TensorFlow'}
  • 원하는 태그를 전부 찾아서 리스트 형태로 얻을 수 있습니다. 하지만 하위 태그는 찾아주지 않습니다.

iter()

# <tool2/> 태그를 전부 탐색할 수 있습니다.
for root_e in root.iter('tool2'):
    print(root_e.tag, root_e.attrib)
  • tool2 {'name': 'PyTorch'}
    tool2 {'name': 'MXNet'}
  • find, findall과 다르게 상위 또는 하위 태그를 전부 찾아줍니다.

 

다음은 원하는 두 가지 태그를 찾고, 태그의 text를 변경하여 저장하는 것까지의 예제 코드입니다.

def parse_xml(xml_list):
    for xml_name in xml_list:
        xml = './' + xml_name
        attached_name = 'abc'
        
        tree = elemTree.parse(gml)
        root = tree.getroot()
        
        # 두 개 태그의 text를 수정한다고 가정합니다.
        for attr1, attr2 in zip(root.iter('attr_1'), root.iter('attr_2')):
            attr1_newtext = attached_name + attr1.text
            attr2_newtext = attached_name + attr2.text
            
            attr1.text = attr1_newtext
            attr2.text = attr2_newtext
        
        # 수정한 xml 파일을 저장합니다.
        tree.write('./' + xml_name)

 

파싱할 xml에 namespace가 존재하는 경우

간혹 xml에 다음과 같이 namespace가 존재하는 경우가 있다.

다음과 같이 namespace를 등록하지 않으면, ElementTree가 자동으로 ns0, ns1, ...으로 등록하기 때문에 주의해야 한다.

ET.register_namespace("gml", "http://www.opengis.net/gml")
ET.register_namespace("bldg", "http://www.opengis.net/citygml/building/2.0")
ET.register_namespace("app", "http://www.opengis.net/citygml/appearance/2.0")
ET.register_namespace("core", "http://www.opengis.net/citygml/2.0")

https://www.youtube.com/watch?v=51YtxSH-U3Y&list=PLQY2H8rRoyvzuJw20FG82Lgm2SZjTdIXU&index=7


최근 텐서플로우는 파이토치 때문에 연구에는 불편하다는 인식이 있습니다(개인적인 의견일 수도..).

이번 영상에서는 텐서플로우가 효율적인 연구를 위해 제공하는 기능을 알아보도록 하겠습니다.

 

파라미터의 상태를 제어한다는 것은 연구에서 매우 중요한 작업입니다.
예를 들어, 케라스 Dense layer의 파라미터나 bias는 층에 저장되어 있긴 하지만, 여전히 state를 다루기엔 매우 불편합니다.

더욱 편리한 제어를 위해 tf.variable_creator_scope를 사용합니다.

class FactorizedVariable(tf.Module):
    def __init__(self, a, b):
        self.a = a
        self.b = b

tf.register_tensor_conversion_function(
  FactorizedVariable, lambda x, *a, **k: tf.matmul(x.a, x.b))

def scope(next_creator, **kwargs):
    shape = kwargs['initial_value']().shape
    if len(shape) != 2: return next_creator(**kwargs)
    return FactorizedVariable(tf.Variable(tf.random.normal([shape[0], 2])),
                                         tf.Variable(tf.random.normal([2, shape[1]])))

with tf.variable_creator_scope(scope):
    d = tf.keras.layer.Dense(10)
    d(tf.zeros[20, 10])
assert isinstance(d.kernel, FactorizedVariable)
  • 먼저, 저장하고 싶은 값을 선택하고, tf.Module을 상속받은 클래스를 정의합니다.
    tf.Module은 저장하고 싶은 변수를 자동으로 추적할 수 있도록 도와줍니다.

위의 코드는 매우 간단하지만, 실제로 사용하는 모델에서는 파라미터가 매우 많기 때문에 관리가 힘듭니다. 따라서 tf.variable_creator_scope를 사용하면 자동 추적 및 파라미터의 변화를 확인할 수 있기 때문에 매우 편리합니다.

딥러닝을 연구하는 데에 있어서 계산 속도는 매우 중요합니다. 텐서플로우는 TensorFlow compiler, XLA 등을 통해 빠른 연산 속도를 지원하고 있습니다. 더욱 효과적으로 사용하려면 @tf.function(experimental_compile=True)를 사용하세요.

활성화 함수의 예를 보겠습니다. 활성화 함수에서는 element-wise 연산 때문에 속도 측면에서 부정적인 영향을 줄지도 모릅니다.
다음 예제 코드에서 속도 차이를 볼 수 있습니다.

def f(x):
    return tf.math.log(2*tf.exp(tf.nn.relu(x+1)))

c_f = tf.function(f, experimental_compile=True)
c_f(tf.zeros([100, 100]))

f = tf.function(f)
f(tf.zeros([100, 100]))

print(timeit.timeit(lambda: f(tf.zeros([100, 100])), number = 10))
# 0.007

print(timeit.timeit(lambda: c_f(tf.zeros([100, 100])), number = 10))
# 0.005 -- ~25% faster!
  • tf.function 사용은 동일합니다. 단지, experimental_compile=True를 추가합니다.
  • linear operations가 포함된 함수나 Bert를 포함한 large-scale 모델에서 효과를 볼 수 있습니다.

element-wise 연산은 옵티마이저에서도 매우 빈번하게 일어납니다. @tf.function을 옵티마이저 코드에 추가한다면 효과를 볼 수 있습니다.
다음은 직접 옵티마이저를 정의해서 @tf.function을 사용하는 예제입니다.

class MyOptimizer(tf.keras.optimizers.Optimizer):
    def __init__(self, lr, power, avg):
        super().__init__(name="MyOptimizer")
        self.lrate, self.pow, self.avg = lr, power, avg
        
    def get_config(self): pass
    def _create_slots(self, var_list):
        for v in var_list: self.add_slot(v, "accum", tf.zeros_like(v))
    
    @tf.function(experimental_compile=True)
    def _resource_apply_dense(self, grad, var, apply_state = None):
        acc = self.get_slot(var, "accum")
        acc.assign(self.avg * tf.pow(grad, self.pow) + (1-self.avg) * acc)
        
        return var.assign_sub(self.lrate * grad/tf.pow(acc, self.pow))

 

다음은 Vectorization을 이야기해보겠습니다. 이는 성능 향상을 위해 매우~! 중요한 지표입니다.
머신 러닝 모델을 다루기 위해 Vectorization이 중요하다는 것은 이미 다 알고 있는 사실이지만, 다루기가 어렵습니다.

그래서 텐서플로우는 이를 위해 auto-Vectorization을 제공합니다. 이 기능은 element-wise 연산이나 batch computation에서 성능 향상을 위해 사용될 것입니다.

Jacobian 연산을 수행하는 예제 코드입니다. jacobian은 미분값을 저장해놓은 행렬입니다.
이를 위해선 tf.GradientTape에서 tape.gradient를 무수히 호출해야하고, 다수의 for-loop를 사용하고, Tensor를 쌓아야 합니다.
이러한 과정을 거치는 코드는 언제나 작동하지만, 좀 더 효율적으로 다룰 수 있는 방법을 텐서플로우가 제공합니다.

tf.vectorized_map을 사용하는 것입니다.

x = tf.random.normal([10, 10])

with tf.GradientTape(persistent=True) as t:
    t.watch(x)
    y = tf.exp(tf.matmul(x, x))
    jac = tf.vectorized_map(
                            lambda yi: tf.vectorized_map(
                            lambda yij: t.gradient(yij, x), yi), y)
  • tf.vectorized_map을 사용하면 빠른 속도로 연산을 수행할 수 있습니다. 하지만 코드가 복잡합니다.
  • 텐서플로우는 이를 위해 jacobian을 아예 함수로 제공합니다.
x = tf.random.normal([10, 10])

with tf.GradientTape() as t:
    t.watch(x)
    y = tf.exp(tf.matmul(x, x))
jac = t.jacobian(y, x)
  • 제공하는 jacobian을 사용하면, 기존 코드보다 10배는 빠르다고 합니다.

마지막으로 데이터에 관한 이야기입니다.
텐서플로우를 사용하는 우리는 항상 매우 커다란 크기의 array를 다루게 됩니다.

 

또, 머신 러닝 모델을 다루다보면 서로 다른 타입의 데이터를 다루기도 합니다. type도 다르고, shape 다르고...
예를 들어, 텐서플로우는 다음과 같은 예를 임베딩 형태로 만들어 줍니다.

텐서플로우는 서로 다른 길이의 데이터를 다루기 위해 ragged tensor 형태를 사용합니다.

data = [['this', 'is', 'a', 'sentence'],
       ['another', 'one'],
       ['a', 'somewhat', 'longer', 'one', ',', 'this']]

rt = tf.ragged.constant(data)
vocab = tf.lookup.StaticVocabularyTable(
    tf.lookup.KeyValueTensorInitializer(
    ['This', 'is', 'a', 'sentence', 'another', 'one', 'somewhat', 'longer'],
    tf.range(8, dtype = tf.int64)), 1)

rt = tf.ragged.map_flat_values(lambda x:vocab.lookup(x), rt)
embedding_table = tf.Variable(tf.random.normal([9, 10]))
rt = tf.gather(embedding_table, rt)
tf.math.reduce_mean(rt, axis = 1)
# Result has shape (3, 10)

길이가 다르고, type이 다르면 tf.ragged를 사용하세요!

https://www.youtube.com/watch?v=v9a240kjAx4&list=PLQY2H8rRoyvzuJw20FG82Lgm2SZjTdIXU&index=4


우리는 프로젝트와 실험 등의 결과를 공유하기 위해 대표적으로 git 또는 그 외의 수단을 활용합니다.

표현하는 수많은 방법 중에서도 머신러닝을 통한 결과 공유는 차트를 활용하면 더욱 효과적으로 공유할 수 있습니다.

결과가 잘못된 경우 깃허브의 issue 기능을 통해 무엇이 잘못되었는지 토의하곤 하는데,
TensorBoard를 활용하면 결과가 왜 잘못되었는지를 명확히 알 수 있습니다.

하지만 Tensorboard를 통해 알 수 있는 결과를 스크린샷으로 공유하는 방법은 효율적이지 않습니다.
그렇기 때문에 다양한 지표를 스크린샷 형태가 아닌 다른 방법으로 표현하고 싶을지도 모릅니다.

어떻게 해야할까요?
우리는 이미 훌륭한 도구인 TensorBoard를 활용할 수 있습니다.
이를 활용하면 좋겠군요!

TensorFlow는 더 많은 실험 결과를 공유할 수 있도록 다양한 기능을 추가하고 있습니다.

작년에 추가한 HParams Dashboard를 볼까요? 대시보드를 통해 다양한 파라미터를 직접 확인할 수 있습니다.

 

처음에 언급하였던 스크린샷 형태로 결과를 효과적으로 제공할 수 없는 문제에 대해 다시 언급하겠습니다.
TensorFlow는 이 문제를 해결하기 위해 Tensorboard dev.를 준비하였습니다.

Tensorboard dev.는 Tensorboard에서 볼 수 있는 실험 결과를 누구에게나, 쉽게 공유할 수 있도록 도와줍니다.
이를 활용하면 large-scale의 실험 결과 또한, 효율적으로 공유할 수 있습니다.

 

최근 TensorFlow를 활용한 연구 논문에서도 이를 활용하고 있습니다. 
기존에 Figure의 그림으로만 제공되던 결과를 TensorFlow dev. 링크 공유를 통해 직접 실험 결과를 확인할 수 있도록 합니다.

 

Tensorboard를 활용하는 방법은 매우 간단합니다.
케라스 콜백을 활용하세요!

model = create_model()
model.compile(optimizer='adam',
                loss='sparse_categorical_crossentropy',
                metrics=['accuracy']
                
log_dir="logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogream_freq=1)

model.fit(x=x_train, y=y_train,
          epochs=5,
          validation_data=(x_test, y_test),
          callbacks=[tensorboard_callback])

 

그리고 다음과 같이 실행하면,

!tensorboard dev upload --logdir ./logs \
  --name "My latest experiments" \
  --description "Simple comparison of several hyperparameters"

 

Tensorboard 화면을 볼 수 있습니다.

오른쪽 상단의 공유버튼을 누르고, 자신의 실험 결과를 공유하세요!