[목차]
9-1 ) 세가지 주요 컴퓨터 비전 작업
9-2 ) 이미지 분할 예제
9-3 ) 최신 컨브넷 아키텍쳐
9-4 ) 컨브넷이 학습한 것 해석하기
9-1 ) 세가지 주요 컴퓨터 비전 작업
1) 이미지 분류
: 이미지에 하나 이상의 레이블을 할당하는 작업 (단일 레이블 분류 / 다중 레이블 분류)
ex) 다중 레이블 분류: 구글 포토 앱 키워드 검색
2) 이미지 분할
: 이미지를 서로 다른 영역으로 나누거나 분할하는 작업, 각 영역은 일반적으로 하나의 범주를 나타냄
ex) 구글 미트 속 지정 배경을 출력하는 기능 (인물과 배경 분리)
3) 객체 탐지
: 이미지에 있는 관심 객체 주변으로 바운딩 박스를 그리는 작업 (각 사각형은 하나의 클래스와 연결)
ex) 자율주행 자동차의 보행자/ 자동차/ 표지판 등을 감지하는 기능
* 이 외의 작업
유사도 평가: 두 이미지가 얼마나 시각적으로 유사한지 평가
키포인트 감지: 이미지에서 관심 속성을 정확하게 짚어내기
포즈 추정, 3D 메시 추정 등...
9-2 ) 이미지 분할 예제
이미지 분할: 이미지를 서로 다른 영역으로 나누거나 분할하는 작업
>> 모델을 사용하여 이미지 안의 각 픽셀에 클래스를 할당하는 것
종류
1) 시맨틱 분할 : 각 픽셀이 독립적으로 하나의 의미를 가진 범주로 분류됨
즉, 의미가 같을 시 같은 클래스로 분류함
2) 인스턴스 분할 : 이미지 픽셀을 범주로 분류하는 것 뿐만 아니라 개별 객체 인스턴스 구분지음
즉, 각 객체 단위의 분류를 의미함
* 분할 마스크: 이미지 분할에서 레이블에 해당 (입력 이미지와 동일한 크기 이미지, 컬러 채널 1개)
*시맨틱 분할 예제
데이터: 다양한 품종의 강아지/고양이 데이터(각 사진의 전경-배경 분할 마스크 포함)
타깃(분할 마스크): 1(전경), 2(배경), 3(윤곽)
import numpy as np
import random
img_size = (200, 200) // 크기 변환
num_imgs = len(input_img_paths)
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_paths) // 정렬 섞음
def path_to_input_image(path):
return img_to_array(load_img(path, target_size=img_size))
def path_to_target(path):
img = img_to_array(
load_img(path, target_size=img_size, color_mode="grayscale"))
img = img.astype("uint8") - 1 // 레이블 0, 1, 2로 변환
return img
input_imgs = np.zeros((num_imgs,) + img_size + (3,), dtype="float32") // 전체 이미지 각각 입력과 타깃 배열에 로딩함
targets = np.zeros((num_imgs,) + img_size + (1,), dtype="uint8")
for i in range(num_imgs):
input_imgs[i] = path_to_input_image(input_img_paths[i])
targets[i] = path_to_target(target_paths[i])
num_val_samples = 1000
train_input_imgs = input_imgs[:-num_val_samples] // 훈련 셋, 검증 셋 쪼개기
train_targets = targets[:-num_val_samples]
val_input_imgs = input_imgs[-num_val_samples:]
val_targets = targets[-num_val_samples:]
from tensorflow import keras
from tensorflow.keras import layers
def get_model(img_size, num_classes):
inputs = keras.Input(shape=img_size + (3,))
x = layers.Rescaling(1./255)(inputs) // 입력을 0/1 범위로 조정
x = layers.Conv2D(64, 3, strides=2, activation="relu", padding="same")(x) // strides=2로 다운샘플링
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(256, 3, strides=2, padding="same", activation="relu")(x)
x = layers.Conv2D(256, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same", strides=2)(x) // strides=2 사용하여 업샘플링
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same", strides=2)(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same", strides=2)(x)
// padding="same" : 패딩이 특성 맵 크기에 영향 미치지 않도록 함
합성곱 층마다 스트라이드를 추가하여 다운샘플링
** 정보 공간상 위치 중요한 이미지 분류의 경우
최대풀링 사용 시 풀링 윈도우 내 정보 삭제됨
>> 위치 정보를 유지하면서 특성 맵을 다운샘플링하는 스트라이드 사용
마지막 합성곱 층의 활성화 출력: (25, 25, 256)
최종 출력은 입력 = 타깃 마스크와 동일한 크기의 (200, 200, 3) 이어야 함
>> Conv2DTranspose() 층 사용하여 업샘플링
outputs = layers.Conv2D(num_classes, 3, activation="softmax", padding="same")(x) // 3개의 범주로 분류
model = keras.Model(inputs, outputs)
return model
model = get_model(img_size=img_size, num_classes=3)
model.summary()
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
callbacks = [
keras.callbacks.ModelCheckpoint("oxford_segmentation.keras", // 훈련 중 손실 변화에 대한 체크포인트 설정함
save_best_only=True)
]
history = model.fit(train_input_imgs, train_targets,
epochs=50,
callbacks=callbacks,
batch_size=64,
validation_data=(val_input_imgs, val_targets)) // 모델 컴파일, 훈련 후 손실 정보 저장
epochs = range(1, len(history.history["loss"]) + 1)
loss = history.history["loss"]
val_loss = history.history["val_loss"]
plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend() // 저장한 손실 정보 시각화
최종 분류 결과 중 한 예시 시각화
>> 어느 정도의 잡음으로 인한 오류를 제외하고 대체로 잘 작동하는 것 확인 가능
9-3 ) 최신 컨브넷 아키텍쳐
모델의 '아키텍쳐': 모델을 만드는 데에 사용된 일련의 선택 (사용할 층, 층의 설정, 층을 연결하는 방법 등)
>> 이러한 선택이 모델의 가설 공간 정의함
*가설 공간: 경사 하강법이 검색할 수 있는 가능한 함수의 공간 (파라미터: 모델의 가중치)
> 좋은 가설 공간은 현재 문제와 솔루션에 대한 사전 지식을 인코딩함
*아키텍쳐 선택의 중요성
아키텍쳐 적절하지 않을 시 훈련시켜도 성능이 개선되지 않을 수 있음
좋은 아키텍쳐 선택은 학습을 가속화하고 훈련 데이터를 효율적으로 사용 가능케 함 > 대규모 데이터셋 필요성 줄임
>> 좋은 모델 아키텍쳐란? : 탐색 공간의 크기를 줄이거나 탐색 공간의 좋은 위치에 쉽게 수렴할 수 있도록 하는 구조
* 컨브넷 아키텍쳐의 모법 사례
1) 잔차 연결 (residual connection)
2) 배치 정규화 (batch normalization)
3) 분리 합성곱 (separable convolution)
1) 모듈화, 계층화, 그리고 재사용
'아키텍쳐' / 케라스 등 모든 시스템 구조의 기초
복잡한 구조를 모듈화 > 모듈을 계층화 > 같은 모듈을 적절히 재사용
모듈화: 동일한 것을 두번 이상 구현할 필요성 없앰
>> 딥러닝: 경사 하강법을 통한 연속적인 최적화에 이러한 방법 적용
딥러닝 모델 아키텍쳐: 반복되는 층 그룹(모듈)들이 피라미드 구조(계층적 구조)를 이룸
특정 구조 새로 만들지 않고 재사용할 수 있음
계층 구조 > 깊을수록 특성 재사용과 이로 인한 추상화 장려함
> 일반적으로 작은 층을 깊게 쌓은 모델이 큰 층 얇게 쌓은것보다 성능 좋음
* 문제: 그레이디언트 소실(vanashing gradient) > 층을 쌓을 수 있는 것에는 한계 있음
2) 잔차 연결 (residual connection)
층 연결할수록 그레이트언트 소실 문제
원인: 정보가 잡음이 있는 채널을 통해 순차적으로 전달될 때 에러 누적 일어남
예시: y= f4(f3(f2(f1(x))))
> f1을 조정하려면 오차 정보가 f2, f3, f4를 통과해야 함
> 그러나, 연속적으로 놓인 각 함수에는 일정량의 잡음이 존재
> 그레이디언트 소실: 함수 연결이 너무 기프면 이 잡음이 그레이디언트 정보를 압도하여 역전파가 잘 동작하지 않게 됨
>> 해결 방법: 이전 입력에 담긴 잡음 없는 정보를 유지시킴
잔차 연결: 층이나 블록의 입력을 출력에 더해줌
>> 이전 층의 오차 그레이디언트 정보가 잡음 없이 네트워크 깊숙히 전파하도록 해줌
잔차 연결 구현
[필터 개수가 변경되는 잔차 블록]
from tensorflow import keras
from tensorflow.keras import layers
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu")(inputs)
residual = x // 잔차를 따로 저장
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x) // 잔차 블록에 해당하는 층
residual = layers.Conv2D(64, 1)(residual) // 투영
x = layers.add([x, residual]) // 블록 출력과 잔차의 크기가 같으므로 이제 더해줌
출력 필터를 32 > 64로 증가시킴, 패딩으로 인한 다운샘플링이 일어나지 않도록 padding="same" 사용
[최대 풀링 층을 가지는 잔차 블록]
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu")(inputs)
residual = x
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.MaxPooling2D(2, padding="same")(x)
residual = layers.Conv2D(64, 1, strides=2)(residual)
x = layers.add([x, residual])
최대 풀링 층으로 인한 다운샘플링에 맞추기 위해 잔차 투영에 strides=2 사용
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Rescaling(1./255)(inputs)
def residual_block(x, filters, pooling=False):
residual = x
x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x)
if pooling:
x = layers.MaxPooling2D(2, padding="same")(x)
residual = layers.Conv2D(filters, 1, strides=2)(residual) // 잔차를 원하는 크기로 투영 위해 스트라이드 합성곱 사용
elif filters != residual.shape[-1]:
residual = layers.Conv2D(filters, 1)(residual)
x = layers.add([x, residual])
return x
x = residual_block(x, filters=32, pooling=True) // 첫 번째 블록
x = residual_block(x, filters=64, pooling=True) // 두 번째 블록, 블록마다 필터 개수 증가
x = residual_block(x, filters=128, pooling=False) // 마지막 블록은 바로 다음 전역 풀링 사용하기 때문에 최대 풀링 필요하지 않음
x = layers.GlobalAveragePooling2D()(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.summary()
3) 배치 정규화 (batch normalization)
배치 정규화: 훈련하는 동안 평균과 분산이 바뀌더라도 현재 배치 데이터의 평균과 분산을 이용하여 이를 정규화하는 방법
> 내부 공변량 변화로 인한 효과 (정확히 왜 도움이 되는지는 모름)
> 잔차 연결과 매우 흡사하게 그레이디언트의 전파를 도와줌
>> 매우 깊은 네트워크라면 여러 개의 배치 정규화 층을 포함해야 훈련할 수 있음
ex)
x= layers.Conv2D(32, 3, use_bias=False)(x)
x= layers.BatchNormalization()(x)
x=layers.Activation("relu")(x)
>> 올바른 사용법: 활성화 층 이전에 배치 정규화 층을 두어야 함
4) 깊이별 분리 합성곱 (depthwise separable convolution)
깊이별 분리 합성곱: 입력 채널별로 따로따로 공간 방향의 합성곱을 수행
* 깊이별 분리 합성곱은 중간 활성화에 있는 공간상의 위치가 높은 상관관계를 가지지만 채널 간에는 매우 독립적이라는 가정에 의존
장점: 일반 합성곱 (Conv2D 층)보다 훨씬 적은 개수의 파라미터를 사용하고 적은 수의 연산을 수행하는데 유사한 표현 능력을 가짐
단점: 일반적인 합성곱과 동일한 수준의 소프트웨어와 하드웨어 최적화 혜택을 받고 있지 못함
** 컨브넷 아키텍쳐 원칙 정리
1) 모델은 반복되는 블록으로 조직되어야 함
2) 특성 맵의 공간 방향이 줄어둚에 따라 층의 필터 개수는 증가해야 함
3) 깊고 좁은 아키텍쳐가 넓고 얕은 것보다 나음
4) 층 블록에 잔차 연결 추가 시 깊은 네트워크 훈련에 도움
5) 배치 정규화 층 추가
6) 일반 층 깊이별 분리 층으로 바꾸기
>> 이를 Xception 유사 모델에 모두 적용하기
inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)
x = layers.Rescaling(1./255)(x)
x = layers.Conv2D(filters=32, kernel_size=5, use_bias=False)(x)
for size in [32, 64, 128, 256, 512]:
residual = x // 잔차 연결
x = layers.BatchNormalization()(x) // 활성화 층 이전에 배치 정규화 층 추가
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x) // 깊이별 분리 합성곱 층으로 일반 Conv2D 층 대체함
x = layers.MaxPooling2D(3, strides=2, padding="same")(x)
residual = layers.Conv2D(
size, 1, strides=2, padding="same", use_bias=False)(residual) // 잔차 크기 조정하기
x = layers.add([x, residual]) // 잔차 더하기
x = layers.GlobalAveragePooling2D()(x) // 전역 풀링으로 출력 크기와 맞추기
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
파라미터 수 줄어듦, 90.8% 정확도 달성
9-4 ) 컨브넷이 학습한 것 해석하기
> 컨브넷이 학습한 것을 시각화하고 컨브넷이 내린 다양한 결정 이해하는 과정 필요
1) 컨브넷 중간층의 출력(중간층에 있는 활성화) 시각화하기
from tensorflow.keras import layers
layer_outputs = []
layer_names = []
for layer in model.layers:
if isinstance(layer, (layers.Conv2D, layers.MaxPooling2D)):
layer_outputs.append(layer.output)
layer_names.append(layer.name)
activation_model = keras.Model(inputs=model.input, outputs=layer_outputs)
모델 입력이 주어졌을 때 층의 출력을 반환하는 모델 만듬
(입력 이미지 1개 > 9개 층마다 원본 모델의 활성화 값 반환)
activations = activation_model.predict(img_tensor)
first_layer_activation = activations[0]
print(first_layer_activation.shape)
images_per_row = 16
for layer_name, layer_activation in zip(layer_names, activations): // 활성화(+ 해당 층 이름) 에 대한 루프 순회
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1] // 총 활성화 크기 (1, size, n_features)
n_cols = n_features // images_per_row
display_grid = np.zeros(((size + 1) * n_cols - 1,
images_per_row * (size + 1) - 1)) //빈 그리드 생성
for col in range(n_cols):
for row in range(images_per_row):
channel_index = col * images_per_row + row
channel_image = layer_activation[0, :, :, channel_index].copy()
if channel_image.sum() != 0:
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype("uint8") // 채널 값 정규화
display_grid[
col * (size + 1): (col + 1) * size + col,
row * (size + 1) : (row + 1) * size + row] = channel_image // 그리드에 채널 행렬 값 저장
scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1],
scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.axis("off")
plt.imshow(display_grid, aspect="auto", cmap="viridis") // 그리드 출력
>> 첫 층에서는 초기 이미지에 있는 거의 모든 정보가 유지
>> 층이 깊어질수록 추상적 (고수준 개념 인코딩하기 때문)
>> 비어 있는 활성화 증가 (층 올라갈수록 활성화되지 않는 필터 생기기 때문)
2) 컨브넷 필터 시각화
각 필터가 반응하는 시각적 패턴 그려 봄 > 경사 상승법 적용
1) 특정 합성곱 층의 한 필터를 최대화하는 손실 함수를 정의
2) 이 활성화 값을 최대화하기 위해 입력 이미지를 변경하도록 확률적 경사 상승법 사용
확률적 경사 상승법을 사용한 손실 최대화
@tf.function
def gradient_ascent_step(image, filter_index, learning_rate):
with tf.GradientTape() as tape:
tape.watch(image)
loss = compute_loss(image, filter_index)
grads = tape.gradient(loss, image)
grads = tf.math.l2_normalize(grads)
image += learning_rate * grads
return image
필터 시각화 생성 함수
img_width = 200
img_height = 200
def generate_filter_pattern(filter_index):
iterations = 30
learning_rate = 10.
image = tf.random.uniform(
minval=0.4,
maxval=0.6,
shape=(1, img_width, img_height, 3))
for i in range(iterations):
image = gradient_ascent_step(image, filter_index, learning_rate)
return image[0].numpy()
텐서를 이미지로 변환하기 위한 유틸리티 함수
def deprocess_image(image):
image -= image.mean()
image /= image.std()
image *= 64
image += 128
image = np.clip(image, 0, 255).astype("uint8")
image = image[25:-25, 25:-25, :]
return image
3) 클래스 활성화에 대한 히트맵을 이미지에 시각화
클래스 활성화 맵: 어느 부분이 컨브넷의 최종 분류 결정에 기여하는지 이해하는 데에 유용
>> 분류에 실수 있을 경우 컨브넷의 결정 과정을 디버깅하는 데에 도움
마지막 합성곱 층을 반환하는 모델
last_conv_layer_name = "block14_sepconv2_act"
classifier_layer_names = [
"avg_pool",
"predictions",
]
last_conv_layer = model.get_layer(last_conv_layer_name)
last_conv_layer_model = keras.Model(model.inputs, last_conv_layer.output)
마지막 합성곱 출력 위에 있는 분류기에 적용하기 위한 모델 만들기
classifier_input = keras.Input(shape=last_conv_layer.output.shape[1:])
x = classifier_input
for layer_name in classifier_layer_names:
x = model.get_layer(layer_name)(x)
classifier_model = keras.Model(classifier_input, x)
최상위 예측 클래스의 그레이디언트 계산하기
import tensorflow as tf
with tf.GradientTape() as tape:
last_conv_layer_output = last_conv_layer_model(img_array)
tape.watch(last_conv_layer_output)
preds = classifier_model(last_conv_layer_output)
top_pred_index = tf.argmax(preds[0])
top_class_channel = preds[:, top_pred_index]
grads = tape.gradient(top_class_channel, last_conv_layer_output)
그레이디언트를 평균하고 채널 중요도 가중치 적용하기
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)).numpy()
last_conv_layer_output = last_conv_layer_output.numpy()[0]
for i in range(pooled_grads.shape[-1]):
last_conv_layer_output[:, :, i] *= pooled_grads[i]
heatmap = np.mean(last_conv_layer_output, axis=-1)
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)
plt.show()
원본 이미지
클래스 활성화 히트맵
이 결과를 기반으로 왜 네트워크가 어떤 객체가 이미지에 존재한다고 판단했는지,
그리고 해당 객체가 이미지 어디에 존재하는지를 확인해 학습 결과를 해석할 수 있음.