쇼 엔 텔(Show and Tell)
서론
Kaggle Notebook 확인하기
몇 년 전, 누군가가 우리에게 제시된 풍경을 정확하게 묘사할 수 있는 가상 비서(virtual assistants)를 갖게 될것이라고 주장했다면, 사람들은 그냥 웃어넘겼을 겁니다. 머신러닝이 천천히 딥러닝에 발을 들여놓으면서, 무한한 가능성을 열어놓으며 예전에는 결코 꿈꾸지 못했던 아이디어들이 서서히 가능하다고 여겨지기 시작했습니다. 이러한 아이디어 중의 하나는 Vinyals 등이 발표한 Show and Tell: A Neural Image Caption Generator 에 묘사되어 있습니다. 이 논문에서 저자들은 이미지 캡션 생성기에 대한 엔드 투 엔드(한 끝에서 한 끝을 잇는) 솔루션을 제안했습니다. 이 논문 이전의 경우 이러한 작업을 위해 제안된 모든 내용은 독립적 작업 최적화(비전및 자연어) 및 그 다음 이러한 독립적 작업을 짜깁기하는 것에 그쳤습니다.
본 논문은 인코더(부호기)가 주어진 언어로 시퀀스를 훈련하고, 다른 언어로 시퀀스를 내뱉는 디코더에 대한고정 길이 표현(fixed-length representation)을 생성하는 신경망 기계 번역(Neural Machine Translation)에서 영감을 얻었습니다. 이 아이디어에서 파생되어, 저자들은 비전(시각) 특징 추출기(vision feature extractor)를 인코더로, 시퀀스 모델을 디코더로 사용했습니다.
📜논문 읽기
학술 논문을 이해하게 되는 것은 꽤 매혹적인 일로, 이 논문을 읽어보면 논문에서 제안된 아이디어를 독자분들 스스로 생각해낼 수 있었을지도 모른다고 생각하실 겁니다. 바로 그 순간, 여러분은 아이디어가 단순하면서도 얼마나 강력할 수 있는지 생각하게 될 것입니다. Show and Tell: A Neural Image Caption Generator는 이미지 캡션 생성기에 대한 딥러닝 연구의 문을 열어 준 놀라운 논문 중 하나입니다.
저자들은 기계 번역에 아주 강한 영감을 받았다고 이야기합니다. 이러한 주장은 저희가 로드맵(road-map)의 형태로 기사를 분류하게끔 이끌었으며, 로드맵을 따라가면, 독자들은 저희가 이 논문을 읽었을 때 느꼈던 그 희열을 똑같이 느끼게 되실 겁니다.
로드맵은 다음과 같습니다:
- 작업(Task): 당면한 과제의 핵심 개념을 살펴봅니다.
- 모듈(Modules): 사용된 다양한 아키텍처를 살펴보고 사용하는 이유에 대해 자세하게 설명합니다
- 코드(Code): 어떻게 코드를 나타내지 않고 코드를 구성할 수 있을까요?
- 손실 및 결과(Loss and Results): 사용된 목적 함수는 무엇이고 어떻게 작동하나요?
👨🏫작업
이미지가 주어지면, 이미지를 설명하는 캡션이 필요합니다. 이것은 단순히 인공 에이전트(artificial agent) 이미지가 속할 카테고리를 결정하는 분류(classification)의 문제가 아닙니다. 그리고 인공 에이전트가 분류하는 개체에 경계 상자(bounding box)를 그리는 감지 작업이 아닙니다. 여기서 우리는 이미지의 내용을 해독하고 이미지 내용 간의 관계를 설명하는 단어의 시퀀스를 구성해야 합니다.
생각나는 가장 간단한 아이디어는 작업을 두 개의 다른 작업으로 나누는 것입니다.
- 👁️ ️ 컴퓨터 비전: 이 파트는 제공된 이미지를 다룹니다. 이미지에서 특징을 추출하고, 계층적 특징에서 콘셉트을 구축하며, 데이터 분포를 모델링을 시도합니다. 단순한 합성곱 신경망(convolutional neural network)은 이 목적에 적합할 것입니다. 이미지를 입력하면 CNN 커널(kernels)는 계층적 방식으로 이미지에서 특징을 선택합니다. 이러한 특징들은 제시된 이미지 내용을 압축하여 표현한 것입니다.
DeepLearning by Goodfellow et. al.
- 🗣️ ️단어 생성: 이 작업에서, 훈련할 이미지와 이미지의 캡션이 제공됩니다. 이 캡션은 모델링되어야하며, 이미지를 설명하는 단어의 시퀀스입니다. 캡션 데이터 분포를 모델링하려면 자연어 프로세서가필요합니다. 모델은 단어 분포 및 단어의 문맥(context)을 파악해야 합니다. 여기서 저희는 이 캡션을모델링하고 제공된 캡션 데이터 공간에서 면밀히 샘플링된 단어를 생성할 수 있는 간단한 반복 아키텍처를 사용할 수 있습니다.
여기서 가장 까다로운 부분은 두 영역(realms)을 함께 연결하는 것입니다. 우리는 캡션 데이터 분포에서 단어를 생성하기 위해 자연어 프로세서가 필요할 뿐만 아니라 자연어 프로세스가 고려 중인 이미지를 가져오기를원할지 모릅니다. 이미지의 특징은 이미지 캡션 생성 문제에 있어 중요한 요인입니다. 캡션 생성기는 이미지특징을 선택한 다음 해당 컨텍스트와 함께 캡션 공간에서 단어를 샘플링하고 이미지에 대한 설명을 제공해야합니다. 이 두 영역을 연결하는 것이 이 작업을 매우 흥미롭게 만드는 이유라 할 수 있습니다.
Show and Tell: a Neural Image Caption Generator
제가 아주 흥미롭게 생각하는 작은 통찰력은 바로 숫자의 사용입니다. 우리 인간은 수학이라고 불리는 아름다운 소통(communication) 언어를 생각해냈습니다. 여기서 우리는 콘셉트, 아이디어 등을 숫자와 기호로 나타낼수 있습니다. 당분간 숫자에 초점을 맞추도록 하겠습니다. 컴퓨터 비전 모델은 본질적으로 숫자인 (모델의 가중치와 편향) 이미지에서 중요한 특징을 추출합니다. 유사하게도, 언어 및 단어도 숫자로 묘사될 수 있습니다[단어 임베딩(word embedding)]. 이것은 활용될 경우 작업의 다양한 영역을 연결하는 문제를 해결할 수 있는아이디어입니다. 이미지 캡션 생성 작업이 성공할 방식으로 비전 모델에서 언어 모델로 숫자를 입력해야 합니다
데이터
저희 실험의 경우, 저희는 30,000개의 이미지와 각 이미지에 해당하는 여러 개의 고유 캡션을 담고 있는 Flickr30k 데이터세트를 사용하기로 했습니다. 데이터는 저희 사용 사례를 용이하게 하기 위해 Kaggle 에서 사전 처리 및 호스팅 되었습니다. Flickr30k의 Kaggle 데이터세트와 함께 진행하도록 하겠습니다.
데이터 세트는 각각 캡션에 연결된 이미지 기록이 포함된 CVS
를 포함하고 있습니다.
데이터세트를 잠깐 살펴보자면 다음과 같습니다:
계속 진행하기에 앞서, 독자분들께 <start>
and <end>
에 대해서 알려드리겠습니다. 이는 캡션의 시작과 끝을모델에게 알리고자 할 때 특히 중요합니다. 데이터를 훈련하는 동안에는 필요하지 않는 것 같으나 테스트 시간에의 경우 모델이 캡션의 첫 단어를 생성하기 위해 <start>
토큰을 공급해야 하며, 모델은 <end>
토큰을 생성한 후에는 단어 생성을 중단해야 합니다.
🏋️️모델
이제 작업과 처리해야 할 모델에 대해 상당한 이해도를 갖게 되었습니다. 숫자에 대한 통찰력은 이 섹션에서 아주 유용할 수 있습니다. 저자들이 제안한 모델의 아키텍처부터 시작해서 그 작동에 대해 심층적으로 살펴보겠습니다.
제안된 아키텍처
. 여기서 우리는 당면한 별개의 작업에 대한 두 가지 별개의 모델을 갖고 있습니다. 왼쪽에는 비전 모델이 있으며, 오른쪽에는 단어 생성에 사용되는 반복 모델(recurrent model)이 있습니다. 여기서의 아이디어는 이미지 특징을 반복 모델에 공급하는 것입니다. 이는 단순한 다른 단어였기 때문입니다. 이미지에서 추출된 특징은 숫자의 집합 (벡터)이며, 벡터를 캡션의 임베딩 공간에 플로팅 하는 경우, 명확하게 단어의 표현을 얻게 됩니다. 단어로 변환된 이 이미지 특징은 전체 프로세서의 묘미입니다. 임베딩 공간에 플로팅 된 이미지 특징은 시소러스(thesaurus)에서의 실제 단어를 나타내지 않을 수 있으나 반복 모델이 학습하기에는 충분합니다. 소위 말해서, 이 image-word
는 반복 모델에 대한 초기 입력(input)입니다. 깊은 성찰을 통해 독자는 이 아이디어가 간단하지만 얼마나 효과적인지 깨닫을 것입니다. 같은 특징을 가진 두 사진이 임베딩 공간에 가까이 놓여 있으며, 이러한 특징을 반복 모델에 제공하면 모델은 두 이미지 모두에 대하여 유사한 캡션을 생성하게 됩니다. 독자들은 이제 우리가 임베딩 공간에 벡터 대수(vector algebra)
를 계산할 수 있으며, 단 두 개의 콘셉트를 추가함으로써 새로운 콘셉트를 학습할 수 있기 때문에 image-word
가 중요하다고 생각할 수도 있습니다.
이미지-단어(image-word) 예시
아키텍처를 따라서, 우리는 CNN을 인코더로, 그리고GRU의 스택트 레이어(stacked layer)를 디코더로 사용했습니다. GRU가 LSTM보다 효율적으로 계산하고, 간단한 RNN보다 더 효과적이기 때문에 저희는 LSTM 대신 게이트된 순환 유닛(Gated Recurrent Units, GRU)을 사용하고 있습니다. 대형 데이터세트를 처리하고 있었음을 명심하며, 저희는 일부 계산 효과(computation effectiveness) (기울기 소멸 문제(gradient dissipation problem)) 손실을 대가로 파이프라인의 효율성을 향상을 위해 GRU를 선택했습니다.
👀쇼(Show)
모델의 이 부분은 정보 인코더 역할을 합니다. 저희는 Imagenet
의 데이터에 대하여 사전 훈련된 restnet50
모델을 사용합니다. 여기서 모델의 마지막 레이어를 생략하고 페널티메이트 레이어(penultimate layer)에서 출력을 추출합니다. resnet
출력 위에 글로벌 평균 풀링(Global Average Pooling)과 Dense
레이어를 쌓습니다. GAP
를 사용하여 두 페널티메이트 커널(penultimate kernel) 출력의 평균을 얻고, Dense 레이어를 사용하여 이미지 특징을 단어 임베딩의 모양처럼 동일한 모양으로 몰딩(mold) 하려 합니다.
🗣️️텔(Tell)
모델의 이 부분이 정보 디코더 역할을 합니다. 이 부분을 설명하기 위해 RNN 모델의 근원을 살펴보겠습니다. 반복 아키텍처에 대한 검토가 필요하신 분들은 Under the Hood of RNN을 살펴보시기 바랍니다. 단일 셀 yty_{t}의 출력은 현재 입력 xtx_{t} 과 이전 셀 ht−1h_{t-1}에서의 은닉 상태 활성화(hidden state activation)에 의해 결정됩니다. 첫 RNN 셀이 인코딩된 이미지를 입력으로 가지고 있는 경우, 그것이 생성하는 은닉 상태(hidden sate)는 다음 셀로 넘어가게 됩니다. 이 은닉 상태는 또한 현재 셀의 입력 및 출력에 대한 링크 역할을 하게 되며, 시퀀스의 모든 셀에 대해서 반복됩니다. 요약해서 말하면, 예측되는 각 단어가 주어진 이미지를 기억하여 수행되도록 인코딩된 이미지의 효과는 본질적으로 셀의 시퀀스를 통해 전달됩니다.
점화식 설명
🖥️코드
Kaggle Notebook 확인하기
여기서 코드의 핵심적인 부분을 살펴보겠습니다.
텍스트 처리
다음의 코드 블록(code block)에서 기본 텍스트 처리를 수행하고 사용 가능한 캡션 세트에서 어휘를 생성합니다. 또한 추후에 동일 크기의 모든 문장을 만들 수 있도록 수동으로 ‘pad’ 토큰을 추가합니다.
train_df = df.iloc[:train_size,:]
val_df = df.iloc[train_size+1:train_size+val_size,:]
test_df = df.iloc[train_size+val_size+1:,:]
# Choose the top 5000 words from the vocabulary
top_k = 10000
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
oov_token="<unk>",
filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~')
# build the vocabulary
tokenizer.fit_on_texts(train_df['comment'])
tokenizer.word_index['<pad>'] = 0
tokenizer.index_word[0] = '<pad>'
기본 어휘 생성이 제대로 되었는지 확인하기 위해, 도움 함수(helper function)을 생성합니다.
# This is a sanity check function
def check_vocab(word):
i = tokenizer.word_index[word]
print(f"The index of the word: {i}")
print(f"Index {i} is word {tokenizer.index_word[i]}")
check_vocab("pajama")
다음과 같은 출력을 얻게 됩니다:
다음으로, 토크나이저가 생성한 어휘에 따라서 훈련 데이터를 생성해야 합니다. 동일한 길이를 갖도록 모든 문장을 수동으로 패드(pad)합니다. 그리고 이어서 데이터를 tf.data 파이프라인에 통합합니다.
# Create the tokenized vectors
train_seqs = tokenizer.texts_to_sequences(train_df['comment'])
val_seqs = tokenizer.texts_to_sequences(val_df['comment'])
test_seqs = tokenizer.texts_to_sequences(test_df['comment'])
# If you do not provide a max_length value, pad_sequences calculates it automatically
train_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
val_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(val_seqs, padding='post')
test_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(test_seqs, padding='post')
train_cap_ds = tf.data.Dataset.from_tensor_slices(train_cap_vector)
val_cap_ds = tf.data.Dataset.from_tensor_slices(val_cap_vector)
test_cap_ds = tf.data.Dataset.from_tensor_slices(test_cap_vector)
이미지 처리
이제 flicker30K 데이터세트의 이미지에 대한 tf.data 파이프라인을 생성하겠습니다. 이미지 로딩, 이미지 디코딩, 데이터 유형 변환 및 크기 조정과 같은 기본 작업을 수행합니다.
@tf.function
def load_img(image_path):
img = tf.io.read_file(image_path)
img = tf.image.decode_jpeg(img)
img = tf.image.convert_image_dtype(img, tf.float32)
img = tf.image.resize(img, (224, 224))
return img
train_img_name = train_df['image_name'].values
val_img_name = val_df['image_name'].values
test_img_name = test_df['image_name'].values
train_img_ds = tf.data.Dataset.from_tensor_slices(train_img_name).map(load_img)
val_img_ds = tf.data.Dataset.from_tensor_slices(val_img_name).map(load_img)
test_img_ds = tf.data.Dataset.from_tensor_slices(test_img_name).map(load_img)
데이터 연결
생성된 두 데이터 파이프라인들을 병합하여 네트워크에 직접 공급할 수 있도록 하는 것이 저희의 목적입니다. 데이터 세트 전체는 매우 크기 때문에 여러 배치로 데이터를 처리하고 있습니다.
# prefecth and batch the dataset
AUTOTUNE = tf.data.experimental.AUTOTUNE
BATCH_SIZE = 512
train_ds = tf.data.Dataset.zip((train_img_ds, train_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
val_ds = tf.data.Dataset.zip((val_img_ds, val_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
test_ds = tf.data.Dataset.zip((test_img_ds, test_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
모델
쇼(Show)
앞서 언급한 바와 같이, 쇼(Show)는 이미지를 압축하는 아키텍처의 인코더 부분을 말합니다. ImageNet에서 훈련된 ResNet50은 특징 추출기(feature extractor)의 역할을 하며, GAP이 뒤따릅니다. 마지막으로 완전히 연결된 레이어와 함께 아키텍처의 이 부분을 모읍니다.
class CNN_Encoder(tf.keras.Model):
def __init__(self, embedding_dim):
super(CNN_Encoder, self).__init__()
self.embedding_dim = embedding_dim
def build(self, input_shape):
self.resnet = tf.keras.applications.ResNet50(include_top=False,
weights='imagenet')
self.resnet.trainable=False
self.gap = GlobalAveragePooling2D()
self.fc = Dense(units=self.embedding_dim,
activation='sigmoid')
def call(self, x):
x = self.resnet(x)
x = self.gap(x)
x = self.fc(x)
return x
텔(Tell)
GRU 셀을 수용하고 있는 텔(Tell)은 학습된 캡션과 원본 입력 간의 링크를 설정하는 인코더의 정보를 사용하는 디코더를 말합니다.
class RNN_Decoder(tf.keras.Model):
def __init__(self, embedding_dim, units, vocab_size):
super(RNN_Decoder, self).__init__()
self.units = units
self.embedding_dim = embedding_dim
self.vocab_size = vocab_size
self.embedding = Embedding(input_dim=self.vocab_size,
output_dim=self.embedding_dim)
def build(self, input_shape):
self.gru1 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru2 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru3 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.gru4 = GRU(units=self.units,
return_sequences=True,
return_state=True)
self.fc1 = Dense(self.units)
self.fc2 = Dense(self.vocab_size)
def call(self, x, initial_zero=False):
# x, (batch, 512)
# hidden, (batch, 256)
if initial_zero:
initial_state = decoder.reset_state(batch_size=x.shape[0])
output, state = self.gru1(inputs=x,
initial_state=initial_state)
output, state = self.gru2(inputs=output,
initial_state=initial_state)
output, state = self.gru3(inputs=output,
initial_state=initial_state)
output, state = self.gru4(inputs=output,
initial_state=initial_state)
else:
output, state = self.gru1(inputs=x)
output, state = self.gru2(inputs=output)
output, state = self.gru3(inputs=output)
output, state = self.gru4(inputs=output)
# output, (batch, 256)
x = self.fc1(output)
x = self.fc2(x)
return x, state
def embed(self, x):
return self.embedding(x)
def reset_state(self, batch_size):
return tf.zeros((batch_size, self.units))
훈련
클래스 개체를 생성하고 옵티마이저(optimizer)를 Adam으로 지정합니다. 손실은 희소 범주형 교차 엔트로피(Sparse Categorical Cross entropy)이며, 여기서 원-핫 인코더(one-hot-encoders)를 사용하는 것은 비효율적이기 때문입니다. 또한 시퀀스 모델이 동일한 것에 대해 과적합(overfit)을 학습하지 않도록 마스크(mask)를 사용합니다.
encoder = CNN_Encoder(EMBEDDIN_DIM)
decoder = RNN_Decoder(embedding_dim=EMBEDDIN_DIM,
units=UNITS_RNN,
vocab_size=VOCAB_SIZE)
optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
def loss_function(real, pred):
mask = tf.math.logical_not(tf.math.equal(real, 0))
loss_ = loss_object(real, pred)
mask = tf.cast(mask, dtype=loss_.dtype)
loss_ *= mask
return tf.reduce_mean(loss_)
다음으로, 역전파(backpropagation)를 통해서 기울기를 계산하는 훈련 단계 함수를 작성합니다.
@tf.function
def train_step(img_tensor, target):
# img_tensor (batch, 224,224,3)
# target (batch, 80)
loss = 0
with tf.GradientTape() as tape:
features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
em_words = decoder.embed(target)
x = tf.concat([features,em_words],axis=1)
predictions, _ = decoder(x, True)
loss = loss_function(target[:,1:], predictions[:,1:-1,:])
trainable_variables = encoder.trainable_variables + decoder.trainable_variables
gradients = tape.gradient(loss, trainable_variables)
optimizer.apply_gradients(zip(gradients, trainable_variables))
return loss
@tf.function
def val_step(img_tensor, target):
# img_tensor (batch, 224,224,3)
# target (batch, 80)
loss = 0
features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
em_words = decoder.embed(target)
x = tf.concat([features,em_words],axis=1)
predictions, _ = decoder(x, True)
loss = loss_function(target[:,1:], predictions[:,1:-1,:])
return loss
📉손실 및 결과
Kaggle Notebook 확인하기
목적 함수(objective function)은 생성된 단어의 네거티브 로그 가능도(negative log-likelihood)입니다. 이를좀 더 직관적으로 만들기 위해서, 제공된 아키텍처로 실행되는 피드포워드(feed-forward)를 살펴보겠습니다. 이미지가 CNN으로 제공되는 경우, 이미지는 이미지 특징을 제공합니다. 그 다음 이 특징은 제공된 단어 임베딩과 동일한 모양의 tensor로 인코딩됩니다. 이미지 특징은 첫 번째 시간 단계에서 GRU에 전달됩니다. 이후이 셀은 전체 단어 어휘의 소프트맥스(softmax)를 생성합니다. 이미지를 자세히 설명하는 단어의 가능성을 높이는 것이 작업의 목표입니다. 이 메트릭을 최소화하고 모델을 훈련할 수 있도록 네거티브 로그 가능도(negative log-likelihood)를 취합니다. 다음 시간 단계에서, 캡션의 단어를 입력으로 제공하고 바로 다음 단어의 확률 최대화를 시도합니다.
결론
기계 번역에서 배운 간단한 개념을 사용하여 저자들은 이미지가 주어질 때 자동 캡션을 생성하는 참신한 방법을 고안해냈습니다.본 논문은 자동 캡션 생성 분야의 여러 후속 논문, 특히Show, Attend and Tell 에 큰 영감을 주었으며, 이 논문의 이름에서 추론할 수 있듯이, 이 리포트에 언급된, 학습된 개념과 더불어 어텐션 사용을 채용하고 있습니다.
저자와 소통하기:
이름 | GitHub | |
---|---|---|
Devjyoti Chakrobarty | @Cr0wley_zz | @cr0wley-zz |
Aritra Roy Gosthipaty | @ariG23498 | @ariG23498 |