CNN-RNN model
首先,將圖片傳送到CNN中,使用預先訓練的網絡VGG-16或者ResNet。在這個網絡的末尾是一個輸出類別得分的softmax分類器。但我們不是要分類圖像,我們需要表示該圖像空間信息的一組特徵。爲了獲取這組特徵,刪除圖像分類的全連接層,並查看更早的層級從圖像中提取空間信息。
現在我們使用CNN作爲特徵提取器,它會將原始圖像中包含的大量信息壓縮成更小的表示結果,此CNN通常稱爲編碼器(Encoder)。它會將圖像的內容編碼 爲更小的特徵向量,然後處理這些特徵向量,並將它作爲後續RNN的初始輸入。
可以通過多種方式將CNN的輸出與下個RNN相連,但是在所有的方式中,從CNN中提取的特徵向量都需要經歷一些處理步驟才能用作RNN第一個單元的輸入。有時候,在將CNN輸出用作RNN的輸入之前,使用額外的全連接層或線性層解析CNN輸出。 這與遷移學習很相似,使用過的CNN經過預先訓練,在其末尾添加一個未訓練過的線性層使我們能在訓練整個模型生成圖像說明時,僅調整這一層。 然後使用最爲RNN輸入,RNN的作用是解碼處理過的特徵向量並將其轉換爲自然語言,這部分通常被稱爲解碼器。
圖像字幕模型
我們將創建一個神經網絡結構。自動從圖像生成字幕。我們將使用MS COCO數據集
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提供整個序列,並使其產生一組輸出和最後隱藏狀態:
# 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)
保持工作區
from workspace_utils import active_session with active_session(): # do long-running work here
coco數據集
Microsoft C*ommon *Objects in COntext (MS COCO) 數據集是用於場景理解的一個大型數據集。 該數據集通常用於訓練並對目標檢測進行基準測試、分割和標註生成算法。
你可以在 該網站 或在 該研究論文中查閱有關該數據集的更多信息。
初始化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())
繪製樣本圖像:下來,我們要從數據集中隨機選擇一張圖像,併爲其繪圖,以及五個相應的標註。 每次運行下面的代碼單元格時,都會選擇不同的圖像。
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)
探索數據加載器
使用 data_loader.py 中的get_loader 函數對數據加載器初始化。
-
transform
- 圖像轉換 具體規定了應該如何對圖像進行預處理,並將它們轉換爲PyTorch張量,然後再將它們用作CNN編碼器的輸入。 -
mode
-'train'
(用於批量加載訓練數據)或'test'
(用於測試數據),二者中的一個。我們將分別說明數據加載器處於訓練模式或測試模式的情況。參照該 notebook 中的說明進行操作時,請設置mode='train'
,這樣可以使數據加載器處於訓練模式。 -
batch_size
- 它是用於確定批次的大小。訓練你的模型時,它是指圖像標註對的數量,用於在每個訓練步驟中修改模型權重。 -
vocab_threshold
- 它是指在將單詞用作詞彙表的一部分之前,單詞必須出現在訓練圖像標註中的總次數。在訓練圖像標註中出現少於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)
運行上面的代碼單元格時,數據加載器會存儲在變量data_loader
中。
你可以將相應的數據集以data_loader.dataset
的方式訪問。 此數據集是data_loader.py中CoCoDataset
類的一個實例。 如果對數據加載器和數據集感到陌生,可以查看 此 PyTorch 教程 。
瞭解 __getitem__
方法
CoCoDataset類中的getitem方法用於確定圖像標註對在合併到批處理之前應如何進行預處理。 當數據加載器處於訓練模式時,該方法將首先獲得訓練圖像的文件名(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
中的圖像進行加載後,你需要使用與在實例化數據加載器時相同的轉換方法(transform_train
)對這些圖像進行預處理。。
Caption Pre-Processing (標註預處理)
爲了生成圖像標註,我們的目標是創建一個模型,該模型是用於根據一個句子的前一個token預測下一個token。因此,我們要把與所有圖像相關聯的標註轉換爲標記化單詞列表,然後將其轉換爲可用於訓練網絡的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
此代碼會將所有字符串值的標註轉換爲整數列表,然後再將其轉換爲PyTorch張量。 爲了弄清楚此代碼的工作原理,我們將其應用於下一個代碼單元格中的示例標註。
sample_caption = 'A person doing a trick on a rail while riding a skateboard.'
在代碼片段的line 1
中,標註中的每個字母都轉換爲小寫,且nltk.tokenize.word_tokenize
函數用於獲取字符串值token的列表。 運行下一個代碼單元格,將其對sample_caption
的影響可視化。
import nltk sample_tokens = nltk.tokenize.word_tokenize(str(sample_caption).lower()) print(sample_tokens)
在line 2
和line 3
中,我們初始化一個空列表並附加一個整數來標記一個圖像標註的開頭。 我們建議你閱讀的 這篇論文 使用了一個特殊的起始單詞(與一個特殊的結束單詞,我們將在下面查看)來標記一個標註的開頭(和結尾)。
這個特殊的起始單詞("<start>"
)是在實例化數據加載器時確定的,並作爲參數(start_word
)傳遞。 你需要將此參數保持爲其默認值(start_word="<start>"
)。
你將在下面看到,整數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對應的整數來繼續這個列表
sample_caption.extend([data_loader.dataset.vocab(token) for token in sample_tokens]) print(sample_caption)
在line 5
,我們附加了最後一個整數,用來標記該標註的結尾。
與上面提到的特殊起始單詞相同,特殊結束單詞("<end>"
)會在實例化數據加載器時被確定,並作爲參數(end_word
)傳遞。 你需要將此參數保持爲其默認值(end_word="<end>"
)。
你將在下面看到,整數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
中,我們將整數列表轉換爲PyTorch張量並將其轉換爲 long 類型。 此外,你可以在 這個網站上閱讀有關不同類型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列表轉換爲整數列表,其中,詞彙表中的每個不同單詞都具有各自相關聯的整數值:
[0, 3, 98, 754, 3, 396, 207, 139, 3, 753, 18, 1]
最後,此列表將轉換爲一個PyTorch張量。 使用上述lines 1-6
的相同步驟對COCO數據集中的所有標註進行預處理
爲了將token轉換爲其對應的整數,我們將data_loader.dataset.vocab
稱作一個函數。 你可以在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在預處理步驟中映射到的整數。
使用下面的代碼單元格查看該字典的子集。
# Preview the word2idx dictionary. dict(list(data_loader.dataset.vocab.word2idx.items())[:10])
通過遍歷訓練數據集中的圖像標註就可以創建一個word2idx字典。 如果token在訓練集中出現的次數不小於vocab_threshold次數,則將其作爲鍵添加到該字典中並分配一個相應的唯一整數。 之後,你可以選擇在實例化數據加載器時修改vocab_threshold參數。 請注意,通常情況下,較小的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
字典中還有一些特殊鍵。 通過前面的內容,你已經熟悉了特殊的起始單詞("<start>"
)和特殊的結束單詞("<end>"
)。在這裏,還有一個特殊的token,對應的是未知的單詞("<unk>"
)。 所有未出現在word2idx
字典中的token都被視爲未知單詞。 在預處理步驟中,任何未知token都會映射到整數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'))
最後提到的是創建數據加載器時提供的vocab_from_file
參數。在創建新的數據加載器時,詞彙表(data_loader.dataset.vocab
)需要保存爲項目文件夾中的 pickle文件,文件名爲vocab.pkl
。
如果你此刻還在調整vocab_threshold
參數的值,則必須設置爲vocab_from_file=False
,這樣才能使更改生效。
但是,如果你對爲vocab_threshold
參數選定的值感到滿意,則只需再次使用所選的vocab_threshold
運行數據加載器即可,這樣可以將新詞彙表保存到文件中。然後,就可以設置vocab_from_file=True
了,這樣便於在文件中加載詞彙表並加速數據加載器的實例化。請注意,從零開始構建詞彙表是實例化數據加載器過程中最耗時的一部分,因此我們強烈建議你儘快設置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)
使用數據加載器獲取批次
數據集中的圖像標註長度差異很大,查看一下Python列表data_loader.dataset.caption_lengths
就可以發現這一點。在這個列表中,每個訓練標註都有一個entry(其中,值用於存儲相應標註的長度)。
在下面的代碼單元格中,我們使用此列表輸出每個長度的訓練數據中的標註總數。 接下來你會看到,大多數標註的長度爲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))
爲了生成批量的訓練數據,我們首先對標註長度進行採樣。在採樣中,抽取的所有長度的概率需要與數據集中具有該長度的標註的數量成比例。 然後,我們檢索一批圖像標註對的sizebatch_size
,其中,所有標註都具有采樣長度。 這種用於分配批次的方法與 這篇文章 中的過程相匹配,並且已被證明在不降低性能的情況下具有計算上的有效性。
運行下面的代碼單元格,生成一個批次。 CoCoDataset
類中的get_train_indices
方法首先對標註長度進行採樣,然後對與訓練數據點對應的batch_size
indices進行採樣,並使用該長度的標註。 這些indices存儲在indices
。
這些indices會提供給數據加載器,然後用於檢索相應的數據點。該批次中的預處理圖像和標註存儲在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層,將特徵向量轉換爲與單詞向量同樣大小的向量。
實現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]
包含模型的預測分數,而該分數表示批次中第 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."