深度學習實現自動生成圖片字幕

介紹

圖片描述

本次項目使用深度學習自動生成圖像字幕。如上圖,模型自動生成「The person is riding a surfboard in the ocean」字幕。咱們具體該如何實現呢?git

圖片描述

如圖所示,咱們須要分別使用CNNRNN模型來實現。json

CNN模型segmentfault

利用卷積網絡對圖像特徵提取的強大能力,來提取特徵信息。咱們的CNN模型須要有強大的識別能力,所以該模型須要使用過大量,多類別的訓練集進行訓練,而且識別準確率較高。本次,咱們利用遷移學習使用Inception模型實現此功能。
經過遷移學習實現OCT圖像識別 文章中有遷移學習的相關介紹。網絡

RNN模型:
對於文本序列數據,目前咱們最好的選擇依然是RNN模型。爲了提高模型預測能力,咱們使用注意力機制實現文本預測。
注意力機制實現機器翻譯 文章中有注意力機制的相關介紹。數據結構

對模型的細節要求咱們將在對應代碼實現裏進行介紹。app

數據集介紹

咱們使用MS-COCO數據集進行訓練,爲方便理解,簡單介紹下數據格式。COCO數據有5種類型,分別是: object detection, keypoint detection, stuff segmentation, panoptic segmentation,image captioning。基礎數據結構以下圖所示:dom

clipboard.png

具體樣例(部分):函數

clipboard.png

本次項目使用的是Image Captioning其中,每張照片很多於5個字幕:post

clipboard.png

數據下載處理

import tensorflow as tf
# 開啓eager模式
tf.enable_eager_execution()
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import re
import numpy as np
import os
import time
import json
from glob import glob
from PIL import Image
import pickle

annotation_zip=tf.keras.utils.get_file(
    # cache_dir(默認值): `~/.keras`
    # cache_subdir: `datasets`,
    # ~/.keras/datasets/captions.zip
    fname='captions.zip',
    cache_subdir=os.path.abspath('.'),
    origin='http://images.cocodataset.org/annotations/annotations_trainval2014.zip',
    # 解壓
    extract=True
)

# 返回文件夾名,實現:split(file)[0]
annotation_file = os.path.dirname(annotation_zip)+'/annotations/captions_train2014.json'
name_of_zip='train2014.zip'
if not os.path.exists(os.path.abspath('.')+"/"+name_of_zip):
    image_zip=tf.keras.utils.get_file(
        fname=name_of_zip,
        cache_subdir=os.path.abspath('.'),
        origin='http://images.cocodataset.org/zips/train2014.zip',
        extract=True
    )
    PATH=os.path.dirname(image_zip)+'train2014/'
else:
    PATH=os.path.abspath('.')+'/train2014/'

讀取字幕和圖片:學習

# 讀取註釋json文件
with open(annotation_file,'r') as f:
    annotations=json.load(f)

# 保存所有字幕
all_captions=[]

# 保存所有圖片
all_img_name_vecotr=[]

# json格式參考COCO數據集官網
for annot in annotations['annotations']:
    
    # 添加開始和結束標記
    caption='<start>'+annot['caption']+'<end>'
    # 獲取圖片名字
    image_id=annot['image_id']
    # 參考文章開始給出的「具體樣例」
    full_coco_image_path=PATH+'COCO_train2014_'+'%012d.jpg'%(image_id)

    all_img_name_vecotr.append(full_coco_image_path)
    all_captions.append(caption)

# random_state 隨機種子,確保每次數據一致
train_captions,img_name_vector=shuffle(
        all_captions,
        all_img_name_vecotr,
        random_state=1
    )

    # 使用訓練集前30000樣本
    num_examples=30000
    train_captions=train_captions[:num_examples]
    img_name_vector=img_name_vector[:num_examples]

重訓練InceptionV3:

簡單介紹下InceptionV3模型:

clipboard.png

  • Inception模型結構中最重要的思想就是卷積核分解。經過上圖可知,5x5的卷積可由2個3x3的卷積代替,3x3卷積可由一個3x1卷積和一個1x3卷積代替,代替的好處是減小了權重參數量,增長了網絡非線性(層增多)。好比,一個5x5卷積的權重參數量和2個3x3卷積的權重參數量分別是(5x5):(3x3)x2。InceptionV3中就將7x7的卷積分解成7x1卷積和1x7卷積。
  • 批標準化(BN)正式提出是在InceptionV2BN經過將輸入分佈轉變成均值爲0,標準差爲1的正態分佈,將值域處於激活函數敏感範圍從而防止梯度消失問題。正由於梯度消失問題的解決,咱們可使用更大的學習率進行訓練從而加快模型收斂。因爲BN有相似Dropout的正則化做用,所以在訓練的時候不使用或少使用Dropout,並減輕L2正則。
  • 使用非對稱卷積,如:1x3卷積,3x1卷積(論文做者指出在feature map的大小12x12~20x20之間效果最好)。
  • 使用Label Smoothing對損失修正。下圖是新損失函數:

clipboard.png

  • 網絡各層信息以下圖所示:

clipboard.png

# 使用inception V3 要求圖片分辨率:299,299
# 輸入值範圍[-1,1]

def load_image(image_path):
    img=tf.image.decode_jpeg(tf.read_file(image_path))
    img_reshape=tf.image.resize_images(img,(299,299))

    # 像素範圍[-1,1]
    # (-255)/255
    img_range=tf.keras.applications.inception_v3.preprocess_input(img_reshape)

    return img_range,image_path

使用遷移學習構建新模型:

# 最後一層卷積輸入shape(8*8*2048),並將結果向量保存爲dict
image_model=tf.keras.applications.InceptionV3(
    # 不使用最後全鏈接層
    include_top=False,
    # inception模型的訓練集是imagenet
    weigths='imagenet'
)

# shape:(batch_size,299,299,3)
new_input=image_model.input

# hidden_layer shape:(batch_size,8,8,2048)
hidden_layer=image_model.layers[-1].output

# 建立新模型
image_features_extract_model=tf.keras.Model(
    new_input,
    hidden_layer
)

保存經過使用InceptionV3得到的特徵:

encode_train=sorted(set(img_name_vector))

# map:能夠並行處理數據,默認讀取的文件具備肯定性順序
# 取消順序能夠加快數據讀取
# 經過設置參數num_parallel_calls實現
image_dataset=tf.data.Dataset.from_tensor_slices(encode_train).map(load_image).batch(16)


for img,path in image_dataset:
    # inception v3獲得的feature
    batch_features=image_features_extract_model(img)
    batch_features=tf.reshape(
        
        # shape:(batch_size,8,8,2048) reshape:(batch_size,64,2048)
        batch_features,shape=(batch_features.shape[0],-1,batch_features[3])
    )

# 保存
for bf,p in zip(batch_features,path):
    path_of_feature=p.numpy().decode('utf-8')

    # 文件後綴.npy
    np.save(path_of_feature,bf.numpy())

文本處理

文本處理方式仍是老規矩,先將文本轉成字典表示而後建立字符轉ID,ID轉字符,最後補長到預設長度。

# 計算最大長度
def calc_max_length(tensor):
    return max(len(t)for t in tensor)
top_k=5000
tokenizer=tf.keras.preprocessing.text.Tokenizer(
    num_words=top_k,

    # 字典中沒有的字符用<unk>代替
    oov_token='<unk>',

    # 須要過濾掉的特殊字符
    filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~'
)

# 要用以訓練的文本列表
tokenizer.fit_on_texts(train_captions)

# 轉爲序列列表向量
train_seqs=tokenizer.texts_to_sequences((train_captions))

tokenizer.word_index['<pad>']=0

# 若是沒有指定最大長度,pad_sequences會自動計算最大長度
cap_vector=tf.keras.preprocessing.sequence.pad_sequences(
   sequences=train_seqs,
   # 後置補長
   padding='post'
)
max_length=calc_max_length(train_seqs)

模型訓練參數

拆分訓練集,驗證集:

img_name_train,img_name_val,cap_trian,cap_val=train_test_split(
    img_name_vector,
    cap_vector,

    # 驗證數據集佔20%
    test_size=0.2,
    # 確保每次數據一致
    random_state=0
# 最好是2的次冪,更適合GPU運算(加快二進制運算)
BATCH_SIZE=64
# shuffle 緩衝區大小
BUFFER_SIZE=1000
# 詞嵌入維度
embedding_dim=256
units=512
vocab_size=len(tokenizer.word_index)

# 後面會將(8,8,2048)轉爲(64,2048)
# 維度必定要一致
feature_shape=2048
attention_features_shape=64

# 加載保存的以前feature文件
def map_func(img_name,cap):
    img_tensor=np.load(img_name.decode('utf-8')+'.npy')
    return img_tensor,cap
dataset=tf.data.Dataset.from_tensor_slices((img_name_train,cap_trian))

# num_parallel_calls 根據本身的CPU而定
dataset=dataset.map(lambda item1,item2:tf.py_func(
    map_func,[item1,item2],[tf.float32,tf.int32]
),num_parallel_calls=4)

# prefetch 能夠合理利用CPU準備數據,GPU計算數據之間的空閒時間,加快數據讀取
dataset=dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE).prefetch(1)

建立模型

編碼器模型:

# 一層使用relu的全鏈接層
class CNN_Encoder(tf.keras.Model):
    def __init__(self,embedding_dim):
        super(CNN_Encoder, self).__init__()

        # fc shape:(batch_size,64,embedding_dim)
        self.fc=tf.keras.layers.Dense(embedding_dim)
    def __call__(self,x):
        x=self.fc(x)
        x=tf.nn.relu(x)

        return x

注意力層:
詳細介紹能夠查看文章開始給出的連接,這裏給出計算方程式:

clipboard.png

class BahdanauAttention(tf.keras.Model):
    def __init__(self,units):
        super(BahdanauAttention, self).__init__()

        self.W1=tf.keras.layers.Dense(units)
        self.W2=tf.keras.layers.Dense(units)
        self.V=tf.keras.layers.Dense(1)

    def __call__(self, features,hidden):
        # 參考注意力機制計算的方程
        # feature shape:(batch_size,64,embedding_dim)
        # hidden_state shape:(batch_size,hidden_size)
        hidden_with_time_axis=tf.expand_dims(hidden,1)

        # score shape:(batch_size,64,hidden_size)
        score=tf.nn.tanh(self.W1(features)+self.W2(hidden_with_time_axis))

        # attention_weights shape:(batch_size,64,1)
        attention_weights=tf.nn.softmax(self.V(score),axis=1)
        context_vector=tf.reduce_sum(attention_weights*features,axis=1)

        return context_vector,attention_weights

解碼器中的GRU:

# 相比LSTM由於減小了一個門,參數少,收斂快
def gru(units):
    if tf.test.is_gpu_available():
    
        # 使用GPU加速計算
        return tf.keras.layers.CuDNNGRU(
            units=units,
            return_state=True,
            return_sequences=True,
            
            # 循環核的初始化方法
            # glorot_uniform是sqrt(2 / (fan_in + fan_out))的正態分佈產生
            # 其中fan_in和fan_out是權重張量的扇入扇出(即輸入和輸出單元數目)
            recurrent_initializer='glorot_uniform'
        )
    else:
        return tf.keras.layers.GRU(
            return_sequences=True,
            return_state=True,

            # 默認:hard_sigmoid <= -1 輸出0,>=1 輸出1 ,中間爲線性
            recurrent_activation='sigmoid',
            recurrent_initializer='glorot_uniform'
        )

解碼器模型:

# 使用注意力模型
class RNN_Decoder(tf.keras.Model):
    def __init__(self,embedding_dim,units,vocab_size):
        super(RNN_Decoder, self).__init__()
        self.units=units
        
        # 詞嵌入將高維離散數據轉爲低維連續數據,並表現出數據之間的類似性(向量空間)
        self.embedding=tf.keras.layers.Embedding(input_shape=vocab_size,output_dim=embedding_dim)
        self.gru=gru(units)
        self.fc1=tf.keras.layers.Dense(self.units)
        self.fc2=tf.keras.layers.Dense(vocab_size)
        self.attention=BahdanauAttention(self.units)

    def __call__(self,x,features,hidden):
        # 獲取注意力模型輸出
        context_vector,attention_weights=self.attention(features,hidden)

        # x shape:(batch_size,1,embedding_dim)
        x=self.embedding(x)
        
        # 注意力,當前輸入合併
        # 注意力shape:(batch_size,1,hidden) x shape:(batch_size,1,embedding_size)
        # x shape:(batch_size, 1, embedding_dim + hidden_size)
        x=tf.concat([tf.expand_dims(context_vector,1),x],axis=-1)

        output,state=self.gru(x)

        # x shape:(batch_size,max_length,hidden_size)
        x=self.fc1(output)

        # x shape:(batch_size*max_length,hidden_size)
        x=tf.reshape(x,shape=(-1,x.shape[2]))

        # x shape:(batch_size*max_length,vocab_size)
        x=self.fc2(x)

        return x,state,attention_weights
    def reset_state(self, batch_size):
     return tf.zeros((batch_size, self.units))

模型訓練

實例化模型:

encoder = CNN_Encoder(embedding_dim)
decoder = RNN_Decoder(embedding_dim, units, vocab_size)

損失函數,優化器設置:

# InceptionV3模型使用的不是Adam優化器
# 各類優化器之後放到一篇單獨的文章詳細介紹
optimizer=tf.train.AdamOptimizer(learning_rate=0.0001)

def loss_function(real,pred):
    mask=1-np.equal(real,0)
    
    # 帶mask的交叉熵損失
    loss_=tf.nn.sparse_softmax_cross_entropy_with_logits(
        labels=real,
        logits=pred
    )*mask

    return tf.reduce_mean(loss_)

訓練:

  • 將使用InceptionV3模型提取的特徵做爲編碼器輸入
  • 編碼器輸出,hidden_state,字幕文本做爲解碼器輸入
  • 解碼器hidden_state做爲下一次輸入,預測值用於計算模型損失
  • 使用標籤文本做爲解碼器輸入(teacher-forcing模式)
  • 梯度計算及應用
loss_plot=[]

EPOCHS=20

for epoch in range(EPOCHS):
    start=time.time()
    total_loss=0

    for (batch,(img_tensor,target)) in enumerate(dataset):
        loss=0

        # 每迭代一次batch後重置 hidden_state
        hidden=decoder.reset_states(batch_size=target.shape[0])
        
        # input維度是3維
        dec_input=tf.expand_dims([tokenizer.word_index['<start>']*BATCH_SIZE],1)
        
        # eager模式下記錄梯度
        with tf.GradientTape() as tape:
            # inception模式提取的特徵
            features=encoder(img_tensor)

            # 每張照片不止一個captions
            for i in range(1,target.shape[1]):
            
                # attention_weights此處暫不須要
                predictions,hidden,_=decoder(dec_input,features,hidden)
                loss+=loss_function(target[:,i],predictions)

                # teacher forcing 使用標籤數據做爲輸入替代hidden-output
                dec_input=tf.expand_dims(target[:,i],1)
            total_loss+=(loss/int(target.shape[1]))
            
            # 總訓練參數
            variables=encoder.variables+decoder.variables
            
            # 梯度計算及應用
            gradients=tape.gradient(loss,variables)
            optimizer.apply_gradients(zip(gradients,variables))

            if batch%100 == 0:
                print('epoch{},batch{},loss{:.4}'.format(
                    epoch+1,
                    batch,
                    loss.numpy()/int(target.shape[1])
                ))
        loss_plot.append(total_loss/len(cap_vector))

plt.plot(loss_plot)
plt.xlabel('epochs')
plt.ylabel('loss')
plt.show()

模型預測

模型預測不使用Teacher forcing模式,當遇到預設的結束標記「<end>」時模型結束訓練。

def evaluate(image):
    attention_plot = np.zeros((max_length, attention_features_shape))
    
    # 初始化hidden-state
    hidden = decoder.reset_state(batch_size=1)
    
    # shape:(1,299,299,3)
    temp_input = tf.expand_dims(load_image(image)[0], 0)
    
    # 特徵提取
    img_tensor_val = image_features_extract_model(temp_input)
    
    # shape:(1,8,8,2048) reshape:(1,64,2048)
    img_tensor_val = tf.reshape(img_tensor_val, (img_tensor_val.shape[0], -1, img_tensor_val.shape[3]))
    
    # shape:(1,64,256)
    features = encoder(img_tensor_val)
    
    # 增長batchsize維度
    dec_input = tf.expand_dims([tokenizer.word_index['<start>']], 0)
    result = []

    for i in range(max_length):
        predictions, hidden, attention_weights = decoder(dec_input, features, hidden)

        attention_plot[i] = tf.reshape(attention_weights, (-1, )).numpy()
        
        # 咱們使用softmax歸一化結果,使用argmax查詢最大值
        # 對於分類數量大於2,softmax和sigmoid的區別是
        # 類別之間有相互關係的使用sigmoid,反之使用softmax
        predicted_id = tf.argmax(predictions[0]).numpy()
        
        # ID轉字符,獲取文本結果
        result.append(tokenizer.index_word[predicted_id])
        
        # 判斷是不是預設的結束標記
        if tokenizer.index_word[predicted_id] == '<end>':
            return result, attention_plot
        
        # 將預測值做爲輸入,預測下一個結果(teacher-forcing在這裏使用數據標籤做爲輸入)
        dec_input = tf.expand_dims([predicted_id], 0)

    attention_plot = attention_plot[:len(result), :]
    return result, attention_plot

如下用於可視化注意力機制訓練過程:
此處代碼主要是圖像展現就不作過多介紹了。

def plot_attention(image, result, attention_plot):
    temp_image = np.array(Image.open(image))

    fig = plt.figure(figsize=(10, 10))
    
    len_result = len(result)
    for l in range(len_result):
        temp_att = np.resize(attention_plot[l], (8, 8))
        ax = fig.add_subplot(len_result//2, len_result//2, l+1)
        ax.set_title(result[l])
        img = ax.imshow(temp_image)
        ax.imshow(temp_att, cmap='gray', alpha=0.6, extent=img.get_extent())

    plt.tight_layout()
    plt.show()
rid = np.random.randint(0, len(img_name_val))
image = img_name_val[rid]
real_caption = ' '.join([tokenizer.index_word[i] for i in cap_val[rid] if i not in [0]])
result, attention_plot = evaluate(image)

print ('Real Caption:', real_caption)
print ('Prediction Caption:', ' '.join(result))
plot_attention(image, result, attention_plot)

Image.open(img_name_val[rid])

總結

想要對圖像生成字幕,首先須要提取圖像特徵,本文咱們利用遷移學習使用Inception模型來提取特徵,對於Inception模型,咱們重點理解卷積核分解。至於文本預測部分與使用注意力機制實現機器翻譯大致一致。有一點想說的是,相似這樣的項目維度轉換會比較多,也是很容易出錯的地方,這一點須要格外留意。

本文代碼內容來自 Yash Katariya在此表示感謝。

相關文章
相關標籤/搜索