작성자 | 김윤희 |
소 감 | 오늘은 멀티모달인공지능에서 배웠던 showandtell 모델 코드를 구현하는 실습을 복습하였다. 모각코를 함께하는 친구들 모두 해당 강의를 수강하고 있어 복습하는데 매우 효율적이였으며, 다같이 하니 더 잘 익혀지는 것 같아 의미있었다. |
일 시 | 2025. 5. 2. (금) 18:00 ~ 21:00 |
장 소 | 미래관 306호 소프트웨어융합대학 과방 |
참가자 명단 | 신수민, 임혜진, 배세은, 김윤희 (총 4명) |
사 진 | ![]() |
코드 수정사항
1) encoder, decoder 수정
class CNNEncoder(nn.Module):
def __init__(self):
super(CNNEncoder, self).__init__()
# self.resnet = torchvision.models.resnet50(pretrained=True)
# self.resnet.fc = nn.Linear(self.resnet.fc.in_features, hidden_size)
resnet = torchvision.models.resnet50(pretrained=True)
self.feature_extractor = nn.Sequential(*list(resnet.children())[:-1])
def forward(self, images):
# return self.resnet(images)
features = self.feature_extractor(images)
return features.view(features.size(0), -1)
- 수정전
- 원래는 resnet50의 마지막 fc layer 까지 사용하겠다
- hidden size에 맞게 학습가능한 projection layer로 바꾸겠다
- 수정 후
- resnet의 마지막 fc layer를 제거하고
- 마지막 pooling layer 까지의 순수한 CNN feature만 추출하는 구조로 바뀜
- 이유
- fc를 사용하는 건 clssification에 더 적합
- spatial한 정보가 살아있는 feature map을 사용하는 것이 captioning에 더 적합
class LSTMDecoder(nn.Module):
def __init__(self, embed_size, hidden_size, vocab_size, num_layers=1):
super(LSTMDecoder, self).__init__()
self.embed = nn.Embedding(vocab_size, embed_size)
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, vocab_size)
self.init_h = nn.Linear(2048, hidden_size)
self.init_c = nn.Linear(2048, hidden_size)
def forward(self, features, captions):
embeddings = self.embed(captions[:, :-1])
# h0 = features.unsqueeze(0)
# c0 = torch.zeros_like(h0)
h0 = self.init_h(features).unsqueeze(0)
c0 = self.init_c(features).unsqueeze(0)
hiddens, _ = self.lstm(embeddings, (h0, c0))
return self.linear(hiddens)
- 수정 전
- 원래는 hidden state만 CNN feature 에서 가져오고,
- cell state torch.zero_like(h0)처럼 0으로 초기화했음
- 이유
- encoder는 이미지 feature 추출을 담당, decoder는 feature를 받아 어떻게 문장을 생성할 지 결정
- decoder에 두면, 어떤 방식으로 feature를 활용할 지 스스로 학습하게 됨!
- 스스로 feature를 해석하고, 적절한 초기 상태로 변화하게 됨
2) data transform, loader 수정
# Cell 3: Dataset 및 Dataloader 정의
# TODO
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
batch_size = 16
train_dataset = Flickr8k(root='.', train=True,
transform=transform, tokenizer=tokenizer)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = Flickr8k(root='.', train=False,
transform=transform, tokenizer=tokenizer)
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)
- 수정 후
- image normalization 진행
- test loader의 batch size를 1 로 진행
- 이유
- ImageNet에서 학습된 모델들의 입력 전처리 기준과 동일하게 적용하기 위해
- 학습의 안정성!!
- test 시에는 하나의 이미지에 대해 한 문장을 생성해야하니까 batch 단위가 아닌 1 장씩 처리하는 것이 일반적임
- ImageNet에서 학습된 모델들의 입력 전처리 기준과 동일하게 적용하기 위해
3) encoder와 decoder의 learning rate를 다르게 설정
# Cell 4: Show and Tell 모델 및 optimizer 정의
# TODO : model_sat 변수명으로 모델 선언
embed_size = 512
hidden_size = 512
num_layers = 1
vocab_size = tokenizer.vocab_size
model_sat = ShowandTell(tokenizer, embed_size, hidden_size,
vocab_size, num_layers).cuda()
criterion = nn.CrossEntropyLoss(ignore_index=tokenizer.pad_token_type_id)
# optimizer = optim.Adam(model.parameters(), lr=0.001)
encoder_params = list(model_sat.encoder.parameters())
decoder_params = list(model_sat.decoder.parameters())
optimizer = torch.optim.Adam([
{'params': encoder_params, 'lr': 0.0001},
{'params': decoder_params, 'lr': 0.001}
])
- 이유
- encoder는 ImageNet으로 사전학습된 모델이라 너무 큰 lr을 주게되면 좋은 feature를 망가뜨릴 가능성이 존재 → 작은 lr을 사용
- 학습된 가중치를 보존하며 미세조정을 해야함
- decoder는 우리가 새로 정의한 LSTM 이므로 처음부터 학습을 진행해야함
- 상대적으로 초기에 학습속도가 빨라야하므로 큰 lr을 설정
- encoder는 ImageNet으로 사전학습된 모델이라 너무 큰 lr을 주게되면 좋은 feature를 망가뜨릴 가능성이 존재 → 작은 lr을 사용
Greedy decoding
def generate_caption(self, image, max_len=30):
with torch.no_grad():
# 이미지 feature 추출
feature = self.encoder(image)
# cls 토큰으로 시작
inputs = torch.tensor([[self.tokenizer.cls_token_id]]).cuda()
hiddens = self.decoder.init_h(feature).unsqueeze(0)
cells = self.decoder.init_c(feature).unsqueeze(0)
states = (hiddens, cells)
caption = []
for _ in range(max_len):
embeddings = self.decoder.embed(inputs)
hiddens, states = self.decoder.lstm(embeddings, states)
outputs = self.decoder.linear(hiddens.squeeze(1))
predicted = outputs.argmax(1) # argmax를 사용해서 확률이 가장 높은 인덱스
predicted_id = predicted.item()
if predicted_id == self.tokenizer.sep_token_id: # sep 토큰이면 종료
break
caption.append(predicted_id)
inputs = predicted.unsqueeze(0)
return caption # [249, 03044.. ]
- greedy decoding
- 매 시점마다 가장 확률이 높은 단어 하나를 선택해서 문장을 만들어나가는 방식
- greedy → 앞으로 어떤 단어가 나올지 고려하지않고 현재 상태에서 가장 좋은 선택을 해
BLEU evaluation
generated_captions = []
reference_captions = []
for image, caption in test_loader:
image = image.cuda()
pred_caption_ids = model_sat.generate_caption(image)
pred_caption = tokenizer.decode(pred_caption_ids,
skip_special_tokens=True)
generated_captions.append(pred_caption.split())
ref = [tokenizer.decode([w for w in cap if w != tokenizer.pad_token_id],
skip_special_tokens=True).split()
for cap in caption.squeeze(0)]
reference_captions.append(ref)
bleu1_score = corpus_bleu(reference_captions,
generated_captions,
weights = (1, 0, 0, 0))
print(f'BLEU-1 score : {bleu1_score*100:.2f}')
- BLEU란
- 기계번역이나 이미지 캡셔닝에서 생성된 문장과 정답 문장을 비교해서 얼마나 잘 맞는지 평가하는 대표적인 지표
- BLEU-1 → 1-gram 정확도만 고려 → precision 기반
- 코드 설명
- 모델을 통해 캡션 생성
- 이미지 입력으로 캡션을 greedy decoding 방식으로 예측
- 생성된 토큰 → 단어로 디코딩
- 단어 문자열로 복원
- 정답 캡션을 정제
- 캡션은 여러 정답 캡션을 포함해
- 각 참조 캡션에서 pad_token 제거하고 디코딩
- split으로 단어 리스트 변환
- BLEU-1 계산
- weights = (1, 0, 0, 0) → 1-gram 사용
- 모델을 통해 캡션 생성
- BLEU
- 는 문자열 비교가 아니라 단어 리스트를 비교
Attention 메커니즘 적용
1) CNNGridEncoder
class CNNGridEncoder(nn.Module):
def __init__(self):
super(CNNGridEncoder, self).__init__()
resnet = torchvision.models.resnet50(pretrained=True)
self.feature_extractor = nn.Sequential(*list(resnet.children())[:-2])
def forward(self, images):
features = self.feature_extractor(images) # [B, 2048, 7, 7]
features = features.view(features.size(0), 2048, -1) # [B, 2048, 49]
features = features.permute(0, 2, 1) # [B, 49, 2048]
return features
- 이 전과의 차이점
- 전에는 전체 이미지를 하나의 feature vector로 압축
- 하지만 어떤 위치가 중요한 지는 알 수 없고, 모든 위치의 정보를 평균낸 것이라는 정보손실이 있음
- 이제는 49개의 공간 위치별 feature vector로 중요한 위치에 집중할 수 잇음
2) Attention decoder
class AttentionDecoder(nn.Module):
def __init__(self, embed_size, hidden_size, vocab_size, attention_size):
feature_size = 2048
super(AttentionDecoder, self).__init__()
self.attention = Attention(feature_size, hidden_size, attention_size)
self.embed = nn.Embedding(vocab_size, embed_size)
self.lstm = nn.LSTMCell(embed_size + feature_size, hidden_size)
self.linear = nn.Linear(hidden_size, vocab_size)
self.init_h = nn.Linear(feature_size, hidden_size)
self.init_c = nn.Linear(feature_size, hidden_size)
def forward(self, features, captions):
batch_size, seq_len = captions.size()
embeddings = self.embed(captions[:, :-1])
mean_feat = features.mean(dim=1)
h = self.init_h(mean_feat)
c = self.init_c(mean_feat)
outputs = []
for t in range(seq_len - 1):
context, alpha = self.attention(features, h)
lstm_input = torch.cat([embeddings[:, t], context], dim=1)
h, c = self.lstm(lstm_input, (h, c))
output = self.linear(h)
outputs.append(output.unsqueeze(1))
return torch.cat(outputs, dim=1)
- 이전과의 차이점
- 위치별 feature 7x7 grid vector
- 어텐션을 통해 timestep마다 다른 위치에 집중함
- 이미지의 공간적 위치 정보를 유지
'윤희' 카테고리의 다른 글
[호붕싸 모각코 8차] 컴퓨터네트워크 복습 (0) | 2025.04.11 |
---|---|
[호붕싸 모각코 7차] High-Resolution Image Synthesis with Latent Diffusion Models 논문리뷰 (0) | 2025.04.04 |
[호붕싸 모각코 6차] CA-MoE: Channel-Adapted MoE for Incremental Weather Forecasting 논문 리뷰 (0) | 2025.03.31 |
[호붕싸 모각코 5차] beakjoon 2225. 합분해 (0) | 2025.03.24 |
[호붕싸 모각코 4주차] semi-supervised learni (0) | 2025.03.22 |