CNN-RNN model
首先,將圖片傳送到CNN中仅仆,使用預先訓練的網(wǎng)絡VGG-16或者ResNet入篮。在這個網(wǎng)絡的末尾是一個輸出類別得分的softmax分類器。但我們不是要分類圖像拯腮,我們需要表示該圖像空間信息的一組特征窖式。為了獲取這組特征,刪除圖像分類的全連接層动壤,并查看更早的層級從圖像中提取空間信息萝喘。
現(xiàn)在我們使用CNN作為特征提取器,它會將原始圖像中包含的大量信息壓縮成更小的表示結果琼懊,此CNN通常稱為編碼器(Encoder)阁簸。它會將圖像的內(nèi)容編碼 為更小的特征向量,然后處理這些特征向量哼丈,并將它作為后續(xù)RNN的初始輸入启妹。
可以通過多種方式將CNN的輸出與下個RNN相連,但是在所有的方式中醉旦,從CNN中提取的特征向量都需要經(jīng)歷一些處理步驟才能用作RNN第一個單元的輸入饶米。有時候,在將CNN輸出用作RNN的輸入之前车胡,使用額外的全連接層或線性層解析CNN輸出咙崎。
這與遷移學習很相似,使用過的CNN經(jīng)過預先訓練吨拍,在其末尾添加一個未訓練過的線性層使我們能在訓練整個模型生成圖像說明時褪猛,僅調(diào)整這一層。
然后使用最為RNN輸入羹饰,RNN的作用是解碼處理過的特征向量并將其轉換為自然語言伊滋,這部分通常被稱為解碼器碳却。
圖像字幕模型
我們將創(chuàng)建一個神經(jīng)網(wǎng)絡結構。自動從圖像生成字幕笑旺。我們將使用MS COCO數(shù)據(jù)集
LSTM inputs/Outputs
我們將所有輸入作為序列傳遞給LSTM,序列如下所示:1.首先從圖像中提取特征向量昼浦;2. 然后是一個單詞,下一個單詞等筒主。
嵌入維度(Embedding Dimention)
當LSTM按順序查看輸入時关噪,序列中的每個輸入需要具有一致的大小,因此嵌入特征向量和每個單詞它們都是 embed_size
序列輸入
LSTM按順序查看輸入乌妙,在Pytorch中使兔,有兩種方法可以做到這一點:
- 對于序列中的所有輸入,它將按照圖像藤韵、起始單詞虐沥、下一個單詞、下一個單詞等(直到序列/批次結束)
for i in inputs:
# Step through the sequence one element at a time.
# after each step, hidden contains the hidden state.
out, hidden = lstm(i.view(1, 1, -1), hidden)
- 第二種方法是為LSTM提供整個序列泽艘,并使其產(chǎn)生一組輸出和最后隱藏狀態(tài):
# the first value returned by LSTM is all of the hidden states throughout
# the sequence. the second is just the most recent hidden state
# Add the extra 2nd dimension
inputs = torch.cat(inputs).view(len(inputs), 1, -1)
hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3)) # clean out hidden state
out, hidden = lstm(inputs, hidden)
保持工作區(qū)
from workspace_utils import active_session
with active_session():
# do long-running work here
coco數(shù)據(jù)集
Microsoft C*ommon *Objects in COntext (MS COCO) 數(shù)據(jù)集是用于場景理解的一個大型數(shù)據(jù)集欲险。 該數(shù)據(jù)集通常用于訓練并對目標檢測進行基準測試、分割和標注生成算法匹涮。
你可以在 該網(wǎng)站 或在 該研究論文中查閱有關該數(shù)據(jù)集的更多信息天试。
初始化COCO API
需要打開GPU
import os
import sys
sys.path.append('/opt/cocoapi/PythonAPI')
from pycocotools.coco import COCO
# initialize COCO API for instance annotations
dataDir = '/opt/cocoapi'
dataType = 'val2014'
instances_annFile = os.path.join(dataDir, 'annotations/instances_{}.json'.format(dataType))
coco = COCO(instances_annFile)
# initialize COCO API for caption annotations
captions_annFile = os.path.join(dataDir, 'annotations/captions_{}.json'.format(dataType))
coco_caps = COCO(captions_annFile)
# get image ids
ids = list(coco.anns.keys())
繪制樣本圖像:下來,我們要從數(shù)據(jù)集中隨機選擇一張圖像然低,并為其繪圖秋秤,以及五個相應的標注。 每次運行下面的代碼單元格時脚翘,都會選擇不同的圖像灼卢。
import numpy as np
import skimage.io as io
import matplotlib.pyplot as plt
%matplotlib inline
# pick a random image and obtain the corresponding URL
ann_id = np.random.choice(ids)
img_id = coco.anns[ann_id]['image_id']
img = coco.loadImgs(img_id)[0]
url = img['coco_url']
# print URL and visualize corresponding image
print(url)
I = io.imread(url)
plt.axis('off')
plt.imshow(I)
plt.show()
# load and display captions
annIds = coco_caps.getAnnIds(imgIds=img['id']);
anns = coco_caps.loadAnns(annIds)
coco_caps.showAnns(anns)
探索數(shù)據(jù)加載器
使用 data_loader.py 中的get_loader 函數(shù)對數(shù)據(jù)加載器初始化。
-
transform
- 圖像轉換 具體規(guī)定了應該如何對圖像進行預處理来农,并將它們轉換為PyTorch張量鞋真,然后再將它們用作CNN編碼器的輸入。 -
mode
-'train'
(用于批量加載訓練數(shù)據(jù))或'test'
(用于測試數(shù)據(jù))沃于,二者中的一個涩咖。我們將分別說明數(shù)據(jù)加載器處于訓練模式或測試模式的情況。參照該 notebook 中的說明進行操作時繁莹,請設置mode='train'
檩互,這樣可以使數(shù)據(jù)加載器處于訓練模式。 -
batch_size
- 它是用于確定批次的大小咨演。訓練你的模型時闸昨,它是指圖像標注對的數(shù)量,用于在每個訓練步驟中修改模型權重。 -
vocab_threshold
- 它是指在將單詞用作詞匯表的一部分之前饵较,單詞必須出現(xiàn)在訓練圖像標注中的總次數(shù)拍嵌。在訓練圖像標注中出現(xiàn)少于vocab_threshold
的單詞將被認為是未知單詞。 -
vocab_from_file
- 它是指一個布爾運算(Boolean)循诉,用于決定是否從文件中加載詞匯表横辆。
import sys
sys.path.append('/opt/cocoapi/PythonAPI')
from pycocotools.coco import COCO
!pip install nltk
import nltk
nltk.download('punkt')
from data_loader import get_loader
from torchvision import transforms
# Define a transform to pre-process the training images.
transform_train = transforms.Compose([
transforms.Resize(256), # smaller edge of image resized to 256
transforms.RandomCrop(224), # get 224x224 crop from random location
transforms.RandomHorizontalFlip(), # horizontally flip image with probability=0.5
transforms.ToTensor(), # convert the PIL Image to a tensor
transforms.Normalize((0.485, 0.456, 0.406), # normalize image for pre-trained model
(0.229, 0.224, 0.225))])
# Set the minimum word count threshold.
vocab_threshold = 5
# Specify the batch size.
batch_size = 10
# Obtain the data loader.
data_loader = get_loader(transform=transform_train,
mode='train',
batch_size=batch_size,
vocab_threshold=vocab_threshold,
vocab_from_file=False)
運行上面的代碼單元格時,數(shù)據(jù)加載器會存儲在變量data_loader
中茄猫。
你可以將相應的數(shù)據(jù)集以data_loader.dataset
的方式訪問狈蚤。 此數(shù)據(jù)集是data_loader.py中CoCoDataset
類的一個實例。 如果對數(shù)據(jù)加載器和數(shù)據(jù)集感到陌生划纽,可以查看 此 PyTorch 教程 脆侮。
了解 __getitem__
方法
CoCoDataset類中的getitem方法用于確定圖像標注對在合并到批處理之前應如何進行預處理。 當數(shù)據(jù)加載器處于訓練模式時阿浓,該方法將首先獲得訓練圖像的文件名(path)及其對應的標注(caption)。
Image Pre-Processing(圖像預處理)
# Convert image to tensor and pre-process using transform
image = Image.open(os.path.join(self.img_folder, path)).convert('RGB')
image = self.transform(image)
將訓練文件夾path
中的圖像進行加載后蹋绽,你需要使用與在實例化數(shù)據(jù)加載器時相同的轉換方法(transform_train
)對這些圖像進行預處理芭毙。。
Caption Pre-Processing (標注預處理)
為了生成圖像標注卸耘,我們的目標是創(chuàng)建一個模型退敦,該模型是用于根據(jù)一個句子的前一個token預測下一個token。因此蚣抗,我們要把與所有圖像相關聯(lián)的標注轉換為標記化單詞列表侈百,然后將其轉換為可用于訓練網(wǎng)絡的PyTorch張量。
為了更詳細地了解COCO描述是如何進行預處理的翰铡,我們首先需要看一下CoCoDataset
類的vocab
實例變量钝域。下面的代碼片段是從 CoCoDataset
類中的__init__
方法中提取的:
def __init__(self, transform, mode, batch_size, vocab_threshold, vocab_file, start_word,
end_word, unk_word, annotations_file, vocab_from_file, img_folder):
...
self.vocab = Vocabulary(vocab_threshold, vocab_file, start_word,
end_word, unk_word, annotations_file, vocab_from_file)
...
從上面的代碼片段中,你可以看到锭魔,data_loader.dataset.vocab是vocabulary.py中Vocabulary 類的一個實例例证。
接下來,我們要使用這個實例對COCO描述進行預處理(來自CoCoDataset
類中的__getitem__
方法):
# Convert caption to tensor of word ids.
tokens = nltk.tokenize.word_tokenize(str(caption).lower()) # line 1
caption = [] # line 2
caption.append(self.vocab(self.vocab.start_word)) # line 3
caption.extend([self.vocab(token) for token in tokens]) # line 4
caption.append(self.vocab(self.vocab.end_word)) # line 5
caption = torch.Tensor(caption).long() # line 6
此代碼會將所有字符串值的標注轉換為整數(shù)列表迷捧,然后再將其轉換為PyTorch張量织咧。
為了弄清楚此代碼的工作原理,我們將其應用于下一個代碼單元格中的示例標注漠秋。
sample_caption = 'A person doing a trick on a rail while riding a skateboard.'
在代碼片段的line 1
中笙蒙,標注中的每個字母都轉換為小寫,且nltk.tokenize.word_tokenize
函數(shù)用于獲取字符串值token的列表庆锦。 運行下一個代碼單元格捅位,將其對sample_caption
的影響可視化。
import nltk
sample_tokens = nltk.tokenize.word_tokenize(str(sample_caption).lower())
print(sample_tokens)
在
line 2
和line 3
中,我們初始化一個空列表并附加一個整數(shù)來標記一個圖像標注的開頭绿渣。 我們建議你閱讀的 這篇論文 使用了一個特殊的起始單詞(與一個特殊的結束單詞朝群,我們將在下面查看)來標記一個標注的開頭(和結尾)。這個特殊的起始單詞(
"<start>"
)是在實例化數(shù)據(jù)加載器時確定的中符,并作為參數(shù)(start_word
)傳遞姜胖。 你需要將此參數(shù)保持為其默認值(start_word="<start>"
)。
你將在下面看到淀散,整數(shù)0
始終用于標記一個標注的開頭右莱。
sample_caption = []
start_word = data_loader.dataset.vocab.start_word
print('Special start word:', start_word)
sample_caption.append(data_loader.dataset.vocab(start_word))
print(sample_caption)
在line 4中,我們通過添加與標注中的每個token對應的整數(shù)來繼續(xù)這個列表
sample_caption.extend([data_loader.dataset.vocab(token) for token in sample_tokens])
print(sample_caption)
在
line 5
档插,我們附加了最后一個整數(shù)慢蜓,用來標記該標注的結尾蛙粘。
與上面提到的特殊起始單詞相同瓜挽,特殊結束單詞("<end>"
)會在實例化數(shù)據(jù)加載器時被確定,并作為參數(shù)(end_word
)傳遞购笆。 你需要將此參數(shù)保持為其默認值(end_word="<end>"
)则剃。
你將在下面看到耘柱,整數(shù)1
始終用于標記一個標注的結尾。
end_word = data_loader.dataset.vocab.end_word
print('Special end word:', end_word)
sample_caption.append(data_loader.dataset.vocab(end_word))
print(sample_caption)
最后棍现,在
line 6
中调煎,我們將整數(shù)列表轉換為PyTorch張量并將其轉換為 long 類型。 此外己肮,你可以在 這個網(wǎng)站上閱讀有關不同類型PyTorch張量的更多信息士袄。
import torch
sample_caption = torch.Tensor(sample_caption).long()
print(sample_caption)
總之,所有標注都會轉換為token列表谎僻,其中娄柳, 特殊的開始和結束token用來標記句子的開頭和結尾,如下所示:
[<start>, 'a', 'person', 'doing', 'a', 'trick', 'while', 'riding', 'a', 'skateboard', '.', <end>]
然后將此token列表轉換為整數(shù)列表艘绍,其中西土,詞匯表中的每個不同單詞都具有各自相關聯(lián)的整數(shù)值:
[0, 3, 98, 754, 3, 396, 207, 139, 3, 753, 18, 1]
最后,此列表將轉換為一個PyTorch張量鞍盗。 使用上述lines 1-6
的相同步驟對COCO數(shù)據(jù)集中的所有標注進行預處理
為了將token轉換為其對應的整數(shù)需了,我們將data_loader.dataset.vocab
稱作一個函數(shù)。 你可以在vocabulary.py中Vocabulary
類的__call__
方法中詳細了解此call具體是如何工作的般甲。
def __call__(self, word):
if not word in self.word2idx:
return self.word2idx[self.unk_word]
return self.word2idx[word]
word2idx
實例變量是一個Python 字典 肋乍,它由字符串值鍵索引,而這些字符串值鍵主要是從訓練標注獲得的token敷存。 對于每個鍵墓造,對應的值是token在預處理步驟中映射到的整數(shù)堪伍。
使用下面的代碼單元格查看該字典的子集。
# Preview the word2idx dictionary.
dict(list(data_loader.dataset.vocab.word2idx.items())[:10])
通過遍歷訓練數(shù)據(jù)集中的圖像標注就可以創(chuàng)建一個word2idx字典觅闽。 如果token在訓練集中出現(xiàn)的次數(shù)不小于vocab_threshold次數(shù)帝雇,則將其作為鍵添加到該字典中并分配一個相應的唯一整數(shù)。 之后蛉拙,你可以選擇在實例化數(shù)據(jù)加載器時修改vocab_threshold參數(shù)尸闸。 請注意,通常情況下孕锄,較小的vocab_threshold值會在詞匯表中生成更多的token吮廉。
# Modify the minimum word count threshold.
vocab_threshold = 4
# Obtain the data loader.
data_loader = get_loader(transform=transform_train,
mode='train',
batch_size=batch_size,
vocab_threshold=vocab_threshold,
vocab_from_file=False)
# Print the total number of keys in the word2idx dictionary.
print('Total number of tokens in vocabulary:', len(data_loader.dataset.vocab))
word2idx
字典中還有一些特殊鍵。 通過前面的內(nèi)容畸肆,你已經(jīng)熟悉了特殊的起始單詞("<start>"
)和特殊的結束單詞("<end>"
)宦芦。在這里,還有一個特殊的token轴脐,對應的是未知的單詞("<unk>"
)调卑。 所有未出現(xiàn)在word2idx
字典中的token都被視為未知單詞。 在預處理步驟中大咱,任何未知token都會映射到整數(shù)2
恬涧。
unk_word = data_loader.dataset.vocab.unk_word
print('Special unknown word:', unk_word)
print('All unknown words are mapped to this integer:', data_loader.dataset.vocab(unk_word))
print(data_loader.dataset.vocab('jfkafejw'))
print(data_loader.dataset.vocab('ieowoqjf'))
最后提到的是創(chuàng)建數(shù)據(jù)加載器時提供的
vocab_from_file
參數(shù)。在創(chuàng)建新的數(shù)據(jù)加載器時徽级,詞匯表(data_loader.dataset.vocab
)需要保存為項目文件夾中的 pickle文件气破,文件名為vocab.pkl
聊浅。如果你此刻還在調(diào)整
vocab_threshold
參數(shù)的值餐抢,則必須設置為vocab_from_file=False
,這樣才能使更改生效低匙。但是旷痕,如果你對為
vocab_threshold
參數(shù)選定的值感到滿意,則只需再次使用所選的vocab_threshold
運行數(shù)據(jù)加載器即可顽冶,這樣可以將新詞匯表保存到文件中欺抗。然后,就可以設置vocab_from_file=True
了强重,這樣便于在文件中加載詞匯表并加速數(shù)據(jù)加載器的實例化绞呈。請注意,從零開始構建詞匯表是實例化數(shù)據(jù)加載器過程中最耗時的一部分间景,因此我們強烈建議你盡快設置vocab_from_file=True
佃声。
# Obtain the data loader (from file). Note that it runs much faster than before!
data_loader = get_loader(transform=transform_train,
mode='train',
batch_size=batch_size,
vocab_from_file=True)
使用數(shù)據(jù)加載器獲取批次
數(shù)據(jù)集中的圖像標注長度差異很大,查看一下Python列表data_loader.dataset.caption_lengths
就可以發(fā)現(xiàn)這一點倘要。在這個列表中圾亏,每個訓練標注都有一個entry(其中,值用于存儲相應標注的長度)。
在下面的代碼單元格中志鹃,我們使用此列表輸出每個長度的訓練數(shù)據(jù)中的標注總數(shù)夭问。 接下來你會看到,大多數(shù)標注的長度為10曹铃。同時缰趋,過短與過長的標注非常少見。
from collections import Counter
# Tally the total number of training captions with each length.
counter = Counter(data_loader.dataset.caption_lengths)
lengths = sorted(counter.items(), key=lambda pair: pair[1], reverse=True)
for value, count in lengths:
print('value: %2d --- count: %5d' % (value, count))
為了生成批量的訓練數(shù)據(jù)铛只,我們首先對標注長度進行采樣埠胖。在采樣中,抽取的所有長度的概率需要與數(shù)據(jù)集中具有該長度的標注的數(shù)量成比例淳玩。 然后直撤,我們檢索一批圖像標注對的size
batch_size
,其中蜕着,所有標注都具有采樣長度谋竖。 這種用于分配批次的方法與 這篇文章 中的過程相匹配,并且已被證明在不降低性能的情況下具有計算上的有效性承匣。運行下面的代碼單元格蓖乘,生成一個批次。
CoCoDataset
類中的get_train_indices
方法首先對標注長度進行采樣韧骗,然后對與訓練數(shù)據(jù)點對應的batch_size
indices進行采樣嘉抒,并使用該長度的標注。 這些indices存儲在indices
袍暴。這些indices會提供給數(shù)據(jù)加載器些侍,然后用于檢索相應的數(shù)據(jù)點。該批次中的預處理圖像和標注存儲在
images
和captions
中政模。
import numpy as np
import torch.utils.data as data
# Randomly sample a caption length, and sample indices with that length.
indices = data_loader.dataset.get_train_indices()
print('sampled indices:', indices)
# Create and assign a batch sampler to retrieve a batch with the sampled indices.
new_sampler = data.sampler.SubsetRandomSampler(indices=indices)
data_loader.batch_sampler.sampler = new_sampler
# Obtain the batch.
images, captions = next(iter(data_loader))
print('images.shape:', images.shape)
print('captions.shape:', captions.shape)
# (Optional) Uncomment the lines of code below to print the pre-processed images and captions.
# print('images:', images)
# print('captions:', captions)
使用CNN編碼器
運行下面的代碼單元格岗宣,從model.py中導入EncoderCNN
和DecoderRNN
。
# Watch for any changes in model.py, and re-load it automatically.
% load_ext autoreload
% autoreload 2
# Import EncoderCNN and DecoderRNN.
from model import EncoderCNN, DecoderRNN
在下一個代碼單元格中淋样,我們定義了一個device
耗式,你將使用它將PyTorch張量移動到GPU(如果CUDA可用的話)。 在進行下一步之前趁猴,運行此代碼單元格刊咳。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
運行下面的代碼單元格,在encoder
中實例化CNN編碼器儡司。
然后娱挨,該notebook的 Step 2中批次的預處理圖像會通過編碼器,且其輸出會存儲在features
中枫慷。
# Specify the dimensionality of the image embedding.
embed_size = 256
#-#-#-# Do NOT modify the code below this line. #-#-#-#
# Initialize the encoder. (Optional: Add additional arguments if necessary.)
encoder = EncoderCNN(embed_size)
# Move the encoder to GPU if CUDA is available.
encoder.to(device)
# Move last batch of images (from Step 2) to GPU if CUDA is available.
images = images.to(device)
# Pass the images through the encoder.
features = encoder(images)
print('type(features):', type(features))
print('features.shape:', features.shape)
# Check that your encoder satisfies some requirements of the project! :D
assert type(features)==torch.Tensor, "Encoder output needs to be a PyTorch Tensor."
assert (features.shape[0]==batch_size) & (features.shape[1]==embed_size), "The shape of the encoder output is incorrect."
編碼器使用預先訓練的ResNet-50架構(刪除了最終的完全連接層)從一批預處理圖像中提取特征让蕾。然后將輸出展平為矢量浪规,然后通過 Linear層,將特征向量轉換為與單詞向量同樣大小的向量探孝。
實現(xiàn)RNN解碼器
在model.py中的DecoderRNN 類中編寫init和 forward方法笋婿。
解碼器將會是DecoderRNN類的一個實例,且必須接收下列輸入:
- 包含嵌入圖像特征的PyTorch張量
features
(在 Step 3 中輸出顿颅,當 Step 2 中的最后一批圖像通過編碼器時) - 與 Step 2中最后一批標注(
captions
)相對應的PyTorch張量缸濒。
outputs
應該是一個大小為[batch_size, captions.shape[1], vocab_size]
的PyTorch張量。這樣設計輸出的目的是outputs[i,j,k]
包含模型的預測分數(shù)粱腻,而該分數(shù)表示批次中第 i
個標注中的第j
個token是詞匯表中第k
個token的可能性庇配。
# Specify the number of features in the hidden state of the RNN decoder.
hidden_size = 512
#-#-#-# Do NOT modify the code below this line. #-#-#-#
# Store the size of the vocabulary.
vocab_size = len(data_loader.dataset.vocab)
# Initialize the decoder.
decoder = DecoderRNN(embed_size, hidden_size, vocab_size)
# Move the decoder to GPU if CUDA is available.
decoder.to(device)
# Move last batch of captions (from Step 1) to GPU if CUDA is available
captions = captions.to(device)
# Pass the encoder output and captions through the decoder.
outputs = decoder(features, captions)
print('type(outputs):', type(outputs))
print('outputs.shape:', outputs.shape)
# Check that your decoder satisfies some requirements of the project! :D
assert type(outputs)==torch.Tensor, "Decoder output needs to be a PyTorch Tensor."
assert (outputs.shape[0]==batch_size) & (outputs.shape[1]==captions.shape[1]) & (outputs.shape[2]==vocab_size), "The shape of the decoder output is incorrect."