Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

합성곱 필터

1합성곱 연산

완전연결 계층을 극복하려면 특정한 범위에서의 관계만을 표현할 수 있는 방법이 필요하고, 그러한 방법은 수학적인 도구로 뒷받침되어야 합니다. 그 도구로 17세기 프랑스 수학자가 발명한 합성곱(convolution) 연산이 결과적으로 선택되었습니다.

합성곱은 실수 범위의 값을 발생시키는 두 함수 사이의 연산입니다. 예를 들어 특정 시점 tt의 비트코인 가격을 예측한다고 합시다. 특정 시점의 가격을 f(t)f(t)라고 하면, 가격을 예상할 때 최근의 가격이 먼 시점의 가격보다 더 중요하다는 것은 상식적으로 받아들일 수 있습니다. 그래서 최근일수록 더 중요하게 반영하는 가중치를 g(t)g(t)라고 합시다. 그러면 실제 예측은 지난 모든 가격의 변화가 아니라 최근 가격들의 변화를 주요하게 고려하여 수행됩니다. 즉, g(t)g(t)는 최근 가격들의 관계만을 고려하여 특정 시점의 가격을 발생시킵니다.

비교해서, 완전연결의 방식이라면 f(t)f(t)를 예상하기 위해 최근과 과거의 가격을 동등한 가치로 보고 그 모든 관계를 알아내려 합니다. 비트코인처럼 어느 시점에서 가격이 이전과 비교할 수 없을 만큼 급격하게 바뀌는 경우, 변화 이전과 이후의 가격을 동일하게 놓고 가중치를 학습하려는 완전연결 방식은 적절하지 않습니다.

비트코인 가격의 관측값에 비유한 f(t)f(t)를 합성곱 연산에서는 입력이라 하고, 여기에 적용되어 tt 이전의 일부 구간에 대해서만 한정해 작용하는 g(t)g(t)를 커널(kernel)이라 부릅니다. 커널은 일종의 시야 범위로 비유할 수 있으며, 합성곱 연산의 결과는 특징 맵(feature map)이라고 부릅니다.

합성곱 연산: 입력 f(t)f(t) 와 커널 g(t)g(t) 의 연산으로 특징 맵을 발생시킵니다.

1.1이산 합성곱

합성곱 연산의 원래 형식은 실수 값에 대해 정의되었지만, 이미지와 같은 경우에는 이산 합성곱 연산으로 수행됩니다. 이산 합성곱은 커널의 크기에 해당하는 영역에서 단일 곱셈 누산을 수행합니다. 즉, 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구해 해당 영역의 값으로 삼습니다. 커널은 일정한 간격으로 이동하면서 전체 입력 영역에 적용됩니다. 기본적으로는 1 간격으로 이동하고, 필요에 따라 이동 간격을 설정할 수 있습니다.

이산 합성곱: 커널 크기 영역에서 대응 원소끼리 곱한 뒤 총합을 구해(단일 곱셈 누산) 특징 맵의 한 원소를 만들고, 커널을 일정 간격으로 이동시켜 전체 입력에 적용합니다.

1.2희소 연결과 수용 영역

합성곱 연산은 완전연결과 비교해 희소한 연결이라고 할 수 있습니다. 완전연결 계층은 모든 입력이 모든 출력으로 전달되는 구성입니다. 반면 합성곱 연산은 특정한 출력이 커널 범위의 입력에 대해서만 영향을 받습니다. 이때 영향을 주는 입력의 범위를 수용 영역(receptive field)이라 합니다.

이러한 효과는 입력에 대비하여 커널의 크기를 작게 선택함으로써 발생합니다. 예를 들어 입력 이미지는 수천 혹은 수백만 개의 픽셀로 구성될 수 있지만, 그에 대응하는 커널은 수 개에서 수십 개 정도로 한정되어도 무방합니다. 이렇게 작은 커널을 전체 영역에 공통적으로 적용하여 유용한 특징을 추출하는 것이 가능합니다.

희소 연결: 완전연결과 달리 하나의 출력은 커널 범위의 입력(수용 영역)에 대해서만 영향을 받습니다.

계층적 희소 연결: 개별 계층은 희소하게 연결되지만, 여러 계층을 통하면 h3은 x1~x5 모든 입력으로부터 간접적으로 영향을 받습니다.

1.3매개변수 공유: 윤곽선 추출 예시

합성곱 연산의 핵심은 희소 연결과 매개변수 공유입니다. 입력 전체가 아니라 커널 크기로 지정한 일부 영역끼리만 연산이 수행되므로 희소한 연결이며, 그 연산에 사용되는 커널의 값이 영역마다 다른 것이 아니라 공통의 커널이 전체 영역에 적용되므로 매개변수가 공유됩니다.

구체적인 예로 입력 이미지의 윤곽선을 추출하는 경우를 봅시다. 윤곽선 추출은 오른쪽 픽셀에서 왼쪽 픽셀의 값을 빼면 된다는 것이 알려져 있습니다. 이러한 연산은 두 개의 값으로 구성된 커널을 합성곱으로 적용하면 수행됩니다. 전체 이미지의 크기와 상관없이 이 커널은 임의의 크기의 입력 이미지에 적용될 수 있으므로, 희소 연결과 공유된 매개변수의 효과를 함께 확인할 수 있습니다.

예를 들어 높이와 너비가 모두 640이고 RGB 3채널로 구성된 이미지를 생각해 봅시다. 만약 완전연결이었다면 백이십만여 개의 매개변수가 필요하지만, 합성곱은 단 두 개의 값으로 같은 연산을 수행합니다. 완전연결과 대비하여 합성곱은 비교가 어려울 정도로 효율적이고 효과적인 표현입니다.

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import matplotlib.pyplot as plt
tf.config.set_visible_devices([], 'GPU')
model = keras.applications.xception.Xception(weights="imagenet", include_top=False)
model.summary()
Fetching long content....
# print out all layer names with conv and separable_conv
for layer in model.layers:
    if "conv" in layer.name:
        print(layer.name)
block1_conv1
block1_conv1_bn
block1_conv1_act
block1_conv2
block1_conv2_bn
block1_conv2_act
block2_sepconv1
block2_sepconv1_bn
block2_sepconv2_act
block2_sepconv2
block2_sepconv2_bn
conv2d
block3_sepconv1_act
block3_sepconv1
block3_sepconv1_bn
block3_sepconv2_act
block3_sepconv2
block3_sepconv2_bn
conv2d_1
block4_sepconv1_act
block4_sepconv1
block4_sepconv1_bn
block4_sepconv2_act
block4_sepconv2
block4_sepconv2_bn
conv2d_2
block5_sepconv1_act
block5_sepconv1
block5_sepconv1_bn
block5_sepconv2_act
block5_sepconv2
block5_sepconv2_bn
block5_sepconv3_act
block5_sepconv3
block5_sepconv3_bn
block6_sepconv1_act
block6_sepconv1
block6_sepconv1_bn
block6_sepconv2_act
block6_sepconv2
block6_sepconv2_bn
block6_sepconv3_act
block6_sepconv3
block6_sepconv3_bn
block7_sepconv1_act
block7_sepconv1
block7_sepconv1_bn
block7_sepconv2_act
block7_sepconv2
block7_sepconv2_bn
block7_sepconv3_act
block7_sepconv3
block7_sepconv3_bn
block8_sepconv1_act
block8_sepconv1
block8_sepconv1_bn
block8_sepconv2_act
block8_sepconv2
block8_sepconv2_bn
block8_sepconv3_act
block8_sepconv3
block8_sepconv3_bn
block9_sepconv1_act
block9_sepconv1
block9_sepconv1_bn
block9_sepconv2_act
block9_sepconv2
block9_sepconv2_bn
block9_sepconv3_act
block9_sepconv3
block9_sepconv3_bn
block10_sepconv1_act
block10_sepconv1
block10_sepconv1_bn
block10_sepconv2_act
block10_sepconv2
block10_sepconv2_bn
block10_sepconv3_act
block10_sepconv3
block10_sepconv3_bn
block11_sepconv1_act
block11_sepconv1
block11_sepconv1_bn
block11_sepconv2_act
block11_sepconv2
block11_sepconv2_bn
block11_sepconv3_act
block11_sepconv3
block11_sepconv3_bn
block12_sepconv1_act
block12_sepconv1
block12_sepconv1_bn
block12_sepconv2_act
block12_sepconv2
block12_sepconv2_bn
block12_sepconv3_act
block12_sepconv3
block12_sepconv3_bn
block13_sepconv1_act
block13_sepconv1
block13_sepconv1_bn
block13_sepconv2_act
block13_sepconv2
block13_sepconv2_bn
conv2d_3
block14_sepconv1
block14_sepconv1_bn
block14_sepconv1_act
block14_sepconv2
block14_sepconv2_bn
block14_sepconv2_act
def get_feature_map(model, layer_name, img):
    # preprocess the image
    img = keras.applications.xception.preprocess_input(img)
    # get the feature map of a specific layer
    intermediate_layer_model = keras.Model(inputs=model.input, outputs=model.get_layer(layer_name).output)
    feature_map = intermediate_layer_model.predict(img)
    return feature_map
img = keras.preprocessing.image.load_img('../data/mozzi.jpg', target_size=(224, 224))
X = keras.preprocessing.image.img_to_array(img)
X = np.expand_dims(X, axis=0)
activation = get_feature_map(model, "block3_sepconv1", X)
1/1 [==============================] - 0s 122ms/step
# compute loss
def compute_loss(input_image, layer_name, filter_index):
    activation = get_feature_map(model, layer_name, input_image)
    # remove border
    filter_activation = activation[:, 2:-2, 2:-2, filter_index]
    loss = tf.reduce_mean(filter_activation)
    return loss

# maximize loss with gradient ascent
@tf.function
def gradient_ascent(image, layer_name, filter_index, learning_rate):
    with tf.GradientTape() as tape:
        tape.watch(image)
        loss = compute_loss(image, layer_name, filter_index)
    grads = tape.gradient(loss, image)
    # normalize with L2 norm
    grads = tf.math.l2_normalize(grads)
    # move the image in the direction of the gradient
    image += learning_rate * grads
    return image

# visualize the filter
def visualize_filter(layer_name, filter_index, learning_rate=10, iterations=30):
    # create a random image
    input_image = np.random.random((1, 224, 224, 3)) * 20 + 128.
    for i in range(iterations):
        input_image = gradient_ascent(input_image, layer_name, filter_index, learning_rate)
    img = input_image[0]
    # remove mean
    img -= img.mean()
    # normalize to [0, 1]
    img /= (img.std() + 1e-5)
    img *= 0.1
    # clip to [0, 1]
    img += 0.5
    img = np.clip(img, 0, 1)
    plt.imshow(img)
    plt.show()