圖片標註問題image_caption

數據集:

  • 數據集中的訓練集使用的是coco train 2014,82783張圖片,測試集使用的是 val 2017 ,5000張圖片,對應的caption是captions_train2014.json,和captions_val2017.json。
    該文件中是以字典的形式包含其內容信息,key值由“info”,”licenses“,”images“,”annotations“組成。
    info:其中包括數據集建立的時間。下載地址,版本號等。
    licenses:是數據集使用條款。
    images:包括圖片的filename,height,width ,圖片對應的網址,圖片對應的caption的id。
    annotation:包含image_id,對應的caption的id,和每個圖片對應的5句描述

  • 訓練:

    訓練集使用的是coco train 2014中的全部數據。

  • 測試:

    測試集使用的是coco val 2017中的的數據。

  • 數據預處理:

    build_data.py中定義了以下幾個函數:

  1.def load_and_process_metadata(captions_file,image_dir): 
         with open(captions_file, encoding='utf-8') as f:
         lines = f.readline()
         d = json.loads(lines)
         print(d)
         f.close()

d["images"]

d["images"]中包含的內容

[{'license':6,'file_name':'000000492110.jpg','coco_url':'http://images.cocodataset.org/val2017/000000492110.jpg', 'height': 427, 'width': 640, 'date_captured': '2013-11-24 00:43:28', 'flickr_url': 'http://farm9.staticflickr.com/8195/8139005828_fda85b4b72_z.jpg', 'id': 492110}]

d["annotations"]中包含的內容

[{'image_id': 54592, 'id': 562033, 'caption': 'Couple of people walking up the snowy mountain with skis'},
{'image_id': 23034, 'id': 561930, 'caption': 'A man smiles watching a rider approach with two horses.'}]

captions_file:caption_train2014.json和val_2014.json文件的路徑。
image_dir:train_2014和val_2014的路徑。
函數作用:
讀取json文件將image和對應的caption對應,將內容封裝到 ImageMetadata中,這時的一個image_id對應一個filename對應五個captions

效果
描述captions
'Beef and other food on a plate next to a wine glass',
'A finished steak dinner and glass of wine are on a table.',
'some food and a fork is on a plate',
'A plate with fork and steak and a wine glass on placemat with papers in background.',
'a rare steak with a fork and a glass of wine
圖片描述captions
'A blue vase filled with colorful flowers sitting on the ground.',
'A blue vase on table holding red flowers next to door.',
'A bunch of orange flowers in a blue vase on a runner on a table.',
'A blue glass vase with some flowers on a table.',
'A blue glass vase with red and yellow flowers

namedtuple("ImageMetadata",["image_id","filename",'captions']

1.讀取json文件使用json中的json.loads()方法。
2.索取json中的信息就直接使用字典中的方法就好。

      2.def process_caption(caption):
              tokenized_caption=[FLAGS.start_word]
              tokenized_caption.extend(nltk.tokenize.word_tokenize(caption.lower()))
              tokenized_caption.extend(FLAGS.end_word)

函數作用:將”start_word“和”end_word“加載到每句話中的開始和結束,作用類似於起始密碼子,終止密碼子,蛋白質合成的起始,和終止。並把caption變成小寫,目的不讓同一個單詞在vocabulary字典中出現兩次節省資源。
1.使用方法是用列表中的entend方法

     3.def create_vocab(captions):
             counter = Counter()
             for c in captions:
                  counter.update(c)
             word_counts = [x for x in counter.items() if x[1] >= 4]
             word_counts.sort(key=lambda x: x[1], reverse=True)

函數作用:統計caption中的所有單詞,和出現的頻率,並把單詞以及單詞出現的頻率
寫入到txt文件中。
1.使用到的方法有collections 中 Counter,Counter是一個計數器,將單詞輸入其中可
直接將單詞的出現頻率顯示出來。並將重複的單詞去除。並只取單詞中頻率大於4的。並從小到大排序。然後創建一個字典將排序好的單詞取出,將單詞從0~vocab_size給單詞賦一個id。

 4.class Vocabulary(object):
      def __init__(self, vocab, unk_id):

vocab:是單詞帶有對應id的字典。
unk_id:是對應vocab_size,也就是單詞的長度,如果單詞沒在製作的dict中就返回
一個空的字符。

word_to_id(self, word):

給定caption中的單詞,如果單詞在dict中返回對應的word_id,不在就返回unk_id。

5. def process_dataset()

函數作用:這時的一個image仍然是對應一個caption。將image遍歷,使每一個image對應一個caption。然後將caption,和image讀入tfrecord中。

images = [ImageMetadata(image.image_id, image.filename, [caption])
for image in images for caption in image.captions]
random.seed(12345)
random.shuffle(images)

使用random的方法將image_caption的順序打亂。

  • tfrecord的使用方法

定義寫入tfrecord文件地writer
writer=tf.python_io.TFRecordwriter(filename) # filename:tfrecord保存的地址
文件名隊列在上述的images中已經構造好了
for i in range(len(images))
image=images[i]:
獲取每個image對應的圖片地址+文件名
image_filename=image.filename
獲取每句image的描述
caption=image.captions
獲得每句caption的id
caption_id=word_to_id(caption)

  • 讀入image,並解碼

    with tf.gfile.FastGFile(image.filename, "rb") as f:
    encoded_image = f.read()

解碼:
1. tf.decode_csv() 解碼文件文本內容
2.tf.image.decode_jpeg(countent) 將jpeg編碼的圖像解碼爲uint8張量
3.tf.image.decode_png(content) 將png編碼的圖像解碼爲uint8張量
4.tf.decode_raw(value,uint8)解碼二進制內容

構造example:

def _int64_feature(value):
  """將int 64特性插入序列中的包裝器"""
  return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
def _bytes_feature(value):
  """將字節特性插入序列示例Proto的包裝器"""
  value = tf.compat.as_bytes(value)
  return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
def _int64_feature_list(values):
  """將int 64 FeatureList插入序列的包裝器-示例Proto"""
  return tf.train.FeatureList(feature=[_int64_feature(v) for v in values])
def _bytes_feature_list(values):
  """將字節FeatureList插入序列示例Proto的包裝器"""
  return tf.train.FeatureList(feature=[_bytes_feature(v) for v in values])
 context = tf.train.Features(feature={     
      "image/image_id":_int64_feature(image.image_id),
      "image/data": _bytes_feature(encoded_image),
  })
feature_lists = tf.train.FeatureLists(feature_list={
      "image/caption": _bytes_feature_list(caption),
      "image/caption_ids": _int64_feature_list(caption_ids)
  })

那麼什麼樣的數據需要映射爲FeatureList或Feature?
我的理解是數字表示,二分類就是0或1,那麼就class=0映射爲tf.train.Feature(tf.train.Int64List(value=[0])), 只要這個字段包含的數據維度是固定的,就可以封裝爲 Feature。
對於長度不固定的字段類型,映射爲FeatureList。比如NLP樣本有一個特徵是一句話,那麼一句話的長度是不固定的,NLP中一般是先分詞,然後把每個詞對應爲該詞在字典中的索引,一句話就用一個一維整形數組來表示 [2, 3, 5, 20, ...],這個數組的長度是不固定的,我們就映射爲
這裏將caption通過上述的函數word_to_id中將caption轉化爲caption_id。
sequence_example = tf.train.SequenceExample(
context=context, feature_lists=feature_lists)

將caption的feature和images的feature保存到tf.train.SequenceExample中,
將序列化後的example寫入文件

wtiter.write(example.SerializeToString())
write.close()

讀取tfrecord
  • 構造文件名讀取隊列

1.將需要讀取的文件名放入到文件名隊列中

tf.train.string_input_producer(data_file,shuffle=True)
data_file:tfrecord的文件名
shuffle=True:默認文件名隨機打亂

  • 讀取

使用TFRecorder的方法讀入
_,serialized_example=reader.read(filename_queue)

  • 解析example
context, sequence = tf.parse_single_sequence_example(example_serialized,
                    context_features={
                        image_feature: tf.FixedLenFeature([], dtype=tf.string)},
                    sequence_features={
                        caption_feature: tf.FixedLenSequenceFeature([], dtype=tf.int64) })
self.encoded_image = context[image_feature]
self.caption = sequence[caption_feature]
  • 解碼

解碼仍然使用上述中的解碼方法
tf.image.decode_jpeg(self.encoded_image)
使用tf.cast()方法將其轉換爲tf.float32,由於每張圖片的大小可能不是統一大小的,爲了方便後面的cnn提取特徵,將圖片統一大小。使用tf.image.resize_images(image,size=[])

在圖片處理時將圖片進行數據歸一化,優點是
1.歸一化後加快梯度下降求最優解的速度
2.歸一化後有可能提高精度
3.使用梯度法求最優解時,歸一化往往非常有必要,否則模型很難收斂,甚至不能收斂

方法1
image = tf.image.convert_image_dtype(image, dtype=tf.float32) # 將圖片像素值變爲[0,1]
image = tf.subtract(image, 0.5) # 圖片像素值域由[0, 1]變換爲[-0.5,0.5]
image = tf.multiply(image, 2.0) # 圖片像素值域由[0,0.5]變爲[-1,1]

方法2
def image_to_float(image)
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
return (image/127.5) - 1.0

方法3
在人臉識別中用到的直接將特徵值除以255的簡單縮放法

圖片處理

在對圖片處理時要進行以下操作,目的是爲了防止過擬合,過擬合是指我們訓練的模型“實在是太好了”對與訓練的數據分類預測效果都特別好,而面對新的數據時變現的效果可能就沒那麼好了。我們訓練數據的目的是讓模型既不過擬合,也不是欠擬合,而是處於過擬合與欠擬合之間的一種狀態。欠擬合是指無論是對於測試集還是訓練集的效果,表現得都不是很好。就比如我們複習數學吧,我們只把課本上的題會做理解,而考試面對新的題,可能只是修改個數我們就做的不太好,這種狀態稱爲過擬合。而我們連書的內容看都不看的話,啥題都不會的情況就是欠擬合。

 image = tf.image.random_flip_left_right(image)
調整圖片的隨機亮度
image = tf.image.random_brightness(image, max_delta=32 / 255)
 調整圖片飽和度
 image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
 隨機調整RGB圖像的色調
 image = tf.image.random_hue(image, max_delta=0.032)
 在某範圍隨機調整圖片對比度
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
輸入一個張量image,把image中的每一個元素的值都壓縮在min和max之間。小於min的讓它等於min,大於max的元素值等於max
image = tf.clip_by_value(image, 0, 1.0)
  • 對caption構造input_sequence,target_sequence
       images_and_captions = []
       images_and_captions.append([image, self.caption])
       enqueue_list = []
        for image, caption in images_and_captions:
            # 獲取每個句子的長度
            caption_length = tf.shape(caption)[0]
            # 將句子id向後移動一
            input_length = tf.expand_dims(tf.subtract(caption_length, 1), 0)
            # 真正句子的id
            input_seq = tf.slice(caption, [0], input_length)
            # 句子向有移動後的句子id目的是爲了預測下一個單詞
            target_seq = tf.slice(caption, [1], input_length)
            indicator = tf.ones(input_length, dtype=tf.int32)
            enqueue_list.append([image, input_seq, target_seq, indicator])
  • 構造批出理隊列

有兩種方法:tf.train.batch和tf.train.batch_join的區別,一般來說,單一文件多線程,那麼選用tf.train.batch(需要打亂樣本,有對應的tf.train.shuffle_batch);而對於多線程多文件的情況,一般選用tf.train.batch_join來獲取樣本(打亂樣本同樣也有對應的tf.train.shuffle_batch_join使用)。

第一種
tf.train.batch_join(enqueue_list,
            batch_size=self.batch_size,
            capacity=queue_capacity,
            dynamic_pad=True,
            name="batch_and_pad"))
第二種
tf.train.batch(enquene_list,
                    batch=self.batch_size,
                    capacity=queue_capacity,
                    name="batch_and_pad") 
  • 構造image_embedding
    將解碼後的圖片經過CNN的全連接層後生成[batch_size,512]的向量
  • 構造caption_embedding

將輸入的seq變成詞向量,詞向量才能在lstm中進行訓練計算

原理就是
image.png

這裏假設詞表中有“我,愛,河,南,科,技”6個字,在類比one-hot編碼的時候就形成了一個[6,6]的矩陣emdedding-map,然後我如果想取出其中的兩個單詞“河”,“南”那麼就需要查emdedding-map。進行類似的矩陣相乘得到兩個詞的詞向量。

embedding_map=tf.get_variable(name="map",shape[vocab_size,512],initializer=self.initializer)
seq_embeddings = tf.nn.embedding_lookup(embedding_map, self.input_seqs)
返回的是tensor 的 shape是 [batch_size,word_size,512]

模型:

1.結構

  • CNN

1,cnn使用的是inceptionv3模型:主要提取圖片的特徵,
inceptionv3具有很少的參數量,節省計算時間加快學習速度;5x5的卷積核用兩個3x3的 卷積核代替增強表達能力,爲減少計算複雜度將卷積轉化爲稀疏連接以下展示cnn的結構圖;




2.模型輸入的image尺寸爲299x299X3 ,batch_size=32 數據類型爲float32

shape:[batch_size, height, width, channels]

4.使用batch_normalization.(簡稱BN)

BN是一種解決深度神經網絡層數太多,而沒有辦法有效向前傳遞的問題,因爲每一層的輸出值都會有不同的均值,和方差,所以輸出的數據分佈也不一樣。
優點:
1.他不僅加快模型的收斂速度,而且更重要的是在一定程度上緩解了深度網絡中的“梯度彌散”(就是在靠近輸出層的hidden layer梯度越大,參數更新快,但是靠近輸入層的hidden layer梯度小,參數更新慢,幾乎和初始狀態一樣,隨機分佈。梯度爆炸與之相反)總體來說就是梯度相當不穩定。
2.控制過擬合可以減少或不用dropout
3.降低網絡對初始化權重不敏感
4.允許使用較大的學習率
所以使用BN可以使得模型訓練更加容易和穩定。BN從字面意思來說就是每一批數據進行歸一化,這裏分批在前面的數據準備裏已經進行了分批處理。BN可以在網絡中任意一層進行歸一化處理,V3-inception使用到的優化器是SGD。
沒有使用BN的時候每層的值迅速全部變爲零,也就是說所有的神經元都已經死了,而有BN,relu過後,每層的值都能有一個比較好的分佈效果。


數據如果在梯度很小的區域,那麼學習率就會很慢甚至長時間停滯,但是減均值除方差後,數據就被移到中心區域,如右圖所示,對於大多數激活函數來說,這個區域的梯度都是很大的。這個用來解決梯度消失。但是如果每一層都是這樣做的話,數據的分佈總是隨着變化敏感的區域,相當於不用考慮數據分佈變化了這樣訓練起來更有效率。但是減均值除方差得到的分佈是正態分佈,但是並不是正態分佈就是最好或者最能體現我們訓練樣本的特徵分佈。因爲數據本身很多情況下是不對稱的,或者激活函數未必是對方差爲1的數據最好的效果,就比如sigmoid激活函數,在-1到1之間的梯度變化不大。這樣的話對於非線性變化的作用就不能很好的提現,換言之就是,均方誤差操作後可能會削弱網絡的性能,所以再加一步利用優化變一下方差大小和均值位置,使得新的分佈更切合數據的真實分佈,保證模型的非線性表達能力。

卷積層

作用:通過在原始圖像上平移來提取特徵

tf.nn.conv2(input,fitter,strides,padding)

input:是傳入四個維度的圖片或者上一層卷積得到的tensor
fitter:卷積核相當於權重有初始值有形狀,
strides:步長大小,決定卷積核每次移動的步長,決定了卷積後的圖片大小。
padding:可設置爲‘’SAME“,"VALID"當padding=SAME時下一層的圖片長寬計算new_widh=w/s(向上取整)。padding=VALID時new_width=(w-f+1)/s。w爲圖片的長寬,f是卷積核大小,s是步長

激活函數

作用:增加非線性分割能力,對輸入的數據原始空間進行扭曲。

  • relu
    1,有效的解決了梯度小時問題
    2,計算速比較快
    SGD:(批梯度下降)的求解速度遠快於sigmoid和tanh
    sigmoid:採用sigmoid缺點:計算量相對大,而採用relu激活函數,整個過程的計算量節省很多,在深層神經網絡中,sigmoid函數反向傳播時很容易出現梯度消失的情況

池化層

作用:減小學習參數,降低網絡的複雜度
池化層分爲最大池化和平均池化,一般最常用最大池化,最大池化是在卷積核大小的feature_map上取最大值,因爲這個最大值可以反映了這個feature周圍的特徵。
內部的池化窗口和卷積核類似,以及步長,padding

Incpetion V3中將5×5的卷積替換成了兩個3×3的卷積
而且在這個module中還是用了卷積的分解,將一個7×7的卷積拆分成了一個1×7的卷積和一個7×1的卷積,不僅能夠大大節省參數降低模型的過擬合,還能比一個7×7的卷積多一個非線性的變換。另外inceptionv3將卷積網絡進行了拆分,其結果比對稱的拆分爲幾個相同的小卷積核效果更明顯,可以處理更多,更豐富的空間特徵,增加特徵多樣性。

  • LSTM

對於LSTM神經網絡無論是從什麼角度描述,中間做的一堆事情,還是找什麼樣的特徵是最適合做這樣的一個分類任務,最終連接softmax是用於分類。時序t就是每個單詞輸入的先後順序,它結合上一時刻的輸出同時乘以權重當做這一時刻的輸入

定義RNN的基本單元

lstm_cell=tf.contrib.rnn.BasicLSTMCell(num_units=512,state_is_tuple=True)

爲了防止過擬合,在LSTM中也添加了dropout層,分別使input,和output的值控制在0.7

lstm_cell=tf.contrib.rnn.DropoutWrapper(lstm_cell,input_keep_prob=0.7,
output_keep_prob=0.7)

定義圖中的隱狀態:hi,可以把隱狀態視作記憶體,它捕捉了之前時間點上的信息。記憶體在RNN迭代的過程也在不斷地更新內部的記憶,就像人的大腦一樣,不斷地記住新的東西,又在忘記舊的

獲得lstm全零狀態
zero_state=lstm_cell.zero_state(batch_size=batch_size,dtype=tf.float32)
更新lstm狀態,這裏將經過CNN後提取得到的特徵向量輸入到lstm_cell
_,initial_state=lstm_cell(self.image_embeddings,zero_state)

定義RNN循環,在這裏將句子的特徵向量self.seq_embeddings輸入到RNN中.sequence_length:圖片對應的caption的句子長度。
initial_state:更新後的initial_state。

lstm_outputs, _ = tf.nn.dynamic_rnn(cell=lstm_cell, inputs=self.seq_embeddings,
sequence_length=sequence_length,
initial_state=initial_state,
dtype=tf.float32,
scope=lstm_scope)

在lstm最後添加一個全連接層作爲最終的輸出
num_outputs:定義輸出的維度
weights_initializer:設置權重參數,
該函數默認使用relu的激活函數

logits = tf.contrib.layers.fully_connected(
inputs=lstm_outputs,
num_outputs=10000,
activation_fn=None,
weights_initializer=self.initializer,
scope=logits_scope)

2.原理

  • 構造損失函數

損失函數使用softmax的交叉熵損失函數,target是logits經過向右移動一後得到的caption的id,target經過tf.cast(tf.reshape(self.seq_target,[-1]),dtype=tf.float32)後才能傳到交叉熵中計算logits對於target的偏離程度

方法1
losses=tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=targets)

weights:是一個數值全爲1的shape=[batch_size*word_size]的一維數組
因爲每個批次的數據都會產生不同的loss值,將每個批次得到的損失值除以批次大小,得到每個批次的平均損失

batch_loss=tf.div(tf.reduce_sum(tf.multiply(losses,weights)),tf.reduce_sum(weights),name="batch_loss")
tf.losses.add_loss(batch_loss)
total_loss=tf.losses.get_total_loss()

方法2


正確的答案,這裏將[batch_size,num_steps]二維數組轉換爲一維數組損失的權重。在這裏所有的權重都爲1,也就是說不同batch和不同時刻的重要程度是一樣的。
 loss=tf.contrib.legacy_seq2seq.sequence_loss_by_example(
 [logits],[tf.reshape(self.targets,[-1][tf.ones([batch_size*num_steps],dtype=tf.float32)] )
  計算得到每個batch的平均損失
  total_cost=tf.reduce_sum(loss)/batch_size

3.實驗

  • 訓練

    • 超參數

      1.learning_rate

      指數衰減法:

      如果staircase=True,代表每decay_step步更新一次learning_rate,如果staircase=False,那就每次迭代都會更新學習率。學習率計算方法
      image

最初設置learning_rate=2.0
tf.train.exponential_decay(learning_rate,global_step,decay_steps=decay_step,decay_rate=0.5 , staircase=True)
1,global_step:用於衰減計算的全局步數。喂入一次batch_size 計爲一次global_step
2,decay_step:衰減速度(num_examples_per_epoch/batch_size)*num_epochs_per_decay
num_example_per_epoch:所有的image_caption的數量
batch_size:batch_size一般設置爲2的n次冪,這裏設置爲32
num_epochs_per_decay設置的是8
3,decay_rate:衰減係數

多項式學習率衰減

polynomial_decay(learning_rate, global_step, decay_steps,end_learning_rate=0.0001, power=1.0,cycle=False, name=None):

在對抗神經網絡中使用的一種學習率衰減方法,特點是確定結束的學習率,學習率更新公式是:

decayed_learning_rate = (learning_rate - end_learning_rate) *(1 - global_step / decay_steps) ^ (power) + end_learning_rate

對於神經網絡中三個參數的概念:

epoch: 訓練時,所有訓練數據都訓練一次,即所有數據的個數
batch:使用訓練集中的一小部分樣本對模型進行一次反向傳播的參數更新,這一小部分樣本稱爲“一批數據”。
batch_size: 在訓練集中選擇一組樣本用來更新權值。一個batch_size包含的樣本數目,通常設爲2的n次冪,常用的包括32,64,128,256,網絡較小的時候使用256,較大時使用32,或者16.

實例:

數據集中有50000張訓練的圖片數量,現在選擇batch_size=256對模型進行訓練。

  • 每個epoch要訓練的圖片數量:50000

  • 訓練集具有的batch數50000/256=196(向上取整)

  • 每個epoch需要完成的batch數196

  • 每個epoch中發生的模型權重更新次數:196

  • 不同次的epoch訓練雖然用的都是訓練集的五萬張圖片,但是對模型的權重跟新值卻是不同的,因爲不同次的模型處於的loss在空間上的不同位置,模型的訓練次數越靠後,越接近谷底,loss也就越小。

優化函數

優化函數使用的是隨機梯度下降SGD:SGD是每一次迭代計算mini-batch的梯度,然後對參數進行更新,計算梯度的方法是求偏導,對於訓練數據集,我們首先將其分成n個batch,每個batch包含m個樣本。我們每次更新都利用一個batch的數據,而非整個訓練集、
優點:
當訓練數據太多時,將訓練集分成一個一個批次,可以減少機器的壓力,並且可以更快地收斂

  • 測試

  • 運行結果

p是概率,因爲生成的圖片描述不是隻有三句,所以三句話的概率和不爲零,只是挑選了三句概率最大的打印出來。

  • 結果分析
效果由於在訓練時loss值沒有下降到最小,所以效果不是太好,圖中是兩個長頸鹿站在草地上,但是在後面兩句將長頸鹿預測成大象。

參考鏈接:

1.在構建讀取隊列時可以開啓多線程:https://zhuanlan.zhihu.com/p/47760620https://zhuanlan.zhihu.com/p/40588218
2.在lstm的構建和理解:https://www.cnblogs.com/hypnus-ly/p/8407905.html
3.caption的詞向量方面:https://www.bilibili.com/video/av35575799/?p=7
4.cnn模型https://blog.csdn.net/loveliuzz/article/details/79135583

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章