基于表征學(xué)習(xí)的行人重實(shí)別

在本節(jié)中,我們使用表征學(xué)習(xí)進(jìn)行行人的重識別。具體的流程如下:在訓(xùn)練時(shí),我們輸入數(shù)據(jù)進(jìn)入深度神經(jīng)網(wǎng)絡(luò),提取出特征向量,然后連接一個(gè)FC層進(jìn)行softmax分類。這就稱為ID損失,在訓(xùn)練集中行人的ID數(shù)為網(wǎng)絡(luò)的類別數(shù)。而在測試時(shí),我們一般使用到倒數(shù)第二層的特征向量進(jìn)行檢索,將FC層丟棄。

#1 數(shù)據(jù)集
DukeMTMC-reID
在數(shù)據(jù)集方面,我們使用了DukeMTMC-reID數(shù)據(jù)集,這是在杜克大學(xué)內(nèi)采集的,圖像來自8個(gè)不同的攝像頭。該數(shù)據(jù)集提供訓(xùn)練集和測試集。訓(xùn)練集包含16522張圖像,測試集包含1761張圖像。訓(xùn)練集幾乎共有702人,平均每個(gè)人有23.5張訓(xùn)練數(shù)據(jù)。是目前最大的行人重識別數(shù)據(jù)集,并且提供了行人屬性,如性別,長短袖,是否有背包等標(biāo)注。

接下來,我們進(jìn)行數(shù)據(jù)處理,首先,我們先觀察數(shù)據(jù)的命名格式,發(fā)現(xiàn)數(shù)據(jù)的命名是00xx_**_**.jpg。后面的不用管,我們的目的是提取對應(yīng)行人的索引。所以可以對_前的數(shù)值進(jìn)行提取,新建一個(gè)data2txt.py寫入如下代碼:
import osdata_path=r'E:\DataSets\DukeMTMC-reID\DukeMTMC-reID\bounding_box_train'image_paths=[os.path.join(data_path,p)for p in os.listdir(data_path)]nameid=[]for image_path in image_paths:name=image_path.split('\\')[-1].split('_')[0]if name+'\n' not in nameid:nameid.append(name+'\n')with open('name.txt','w',encoding='utf8') as f:f.writelines(nameid)
運(yùn)行程序,我們會(huì)發(fā)現(xiàn)多了一個(gè)name.txt文件,里面存放著我們的標(biāo)簽數(shù)據(jù),接著,我們新建一個(gè)load_data.py文件,寫入如下代碼,用來組合文件路徑以及打亂數(shù)據(jù)集:
import osimport randomimport cv2import numpy as np# 加載文件路徑def load_data():path=r'E:\DataSets\DukeMTMC-reID\DukeMTMC-reID\bounding_box_train'image_names=os.listdir(path)with open('name.txt','r',encoding='utf8') as f:names=f.readlines()data=[]label=[]# 組合路徑for index,name in enumerate(names):n=name.strip('\n')image_paths=[os.path.join(path,i) for i in image_names if i.startswith((n))]for image_path in image_paths:data.append(image_path)label.append(index)# 隨機(jī)打亂random_seed=random.randint(1,1000)random.seed(random_seed)random.shuffle(data)random.seed(random_seed)random.shuffle(label)return data,label
接著,我們需要將數(shù)據(jù)集制作成生成器的形式:
def gan_data(data,label,batch_size,n_classes=702):while True:x=[]y=[]for index,value in enumerate(data):image=cv2.imread(value)# 使用cv2縮放圖片,由于圖片是長方形,cv2.resize的參數(shù)是(寬,高)# 而后續(xù)的reshape的參數(shù)是(高,寬),所以是相反的image=cv2.resize(image,(60,200))x.append(image)lb=label[index]y_=np.zeros((n_classes,))=1.y.append(y_)# 當(dāng)數(shù)據(jù)量等于一個(gè)批次是放回?cái)?shù)據(jù)if len(x)==batch_size:x=np.array(x).reshape(-1,200,60,3)/255.y=np.array(y)# print(y.shape)yield x,yx=[]y=[]
#2 模型搭建
DarkNet
在本節(jié)中,我們使用的模型是DarkNet,事實(shí)上,DarkNet還是一個(gè)小眾的深度學(xué)習(xí)框架,但是我們這里所說的DarkNet是一個(gè)名為DarkNet53的深度神經(jīng)網(wǎng)絡(luò)。它實(shí)際上是有名的yolov3目標(biāo)檢測寬框架的backbone。
完整的yolov3的網(wǎng)絡(luò)結(jié)構(gòu)如下,這里我們只取backbone的部分網(wǎng)絡(luò),也就是虛線中的部分作為我們的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)。

事實(shí)上,DarkNet53的網(wǎng)絡(luò)結(jié)構(gòu)和ResNet很相似,同樣是擁有殘差連接的結(jié)構(gòu),基礎(chǔ)的卷積方式同樣是Conv+BN+激活函數(shù),只不過激活函數(shù)換成了LeakyReLu,并且殘差的位置發(fā)生了改變。以下是DarkNet53的網(wǎng)絡(luò)結(jié)構(gòu)圖,我們可以根據(jù)這一結(jié)構(gòu)圖編寫我們的代碼。

新建一個(gè)model.py文件,先導(dǎo)入依賴庫,然后定義一個(gè)compose()函數(shù):
from tensorflow.keras.layers import *from tensorflow.keras.regularizers import l2from functools import reduceimport tensorflow.keras as kfrom image_recognition.DarkNet.load_data import load_data,gan_datadef compose(*funcs):if funcs:return reduce(lambda f,g:lambda *a, **kw:g(f(*a,**kw)),funcs)else:raise ValueError('Composition of empty sequence not supported.')
這個(gè)函數(shù)的作用是對輸入進(jìn)來的一個(gè)數(shù)據(jù)集合或元組中的所有數(shù)據(jù)進(jìn)行疊加操作,例如下面的代碼中:
# 單個(gè)卷積def conv2D(filter,kernel_size,stride=2,l2_=True,bise=False):padding='valid' if stride==2 else 'same'return Conv2D(filters=filter,kernel_size=kernel_size,padding=padding,kernel_regularizer=l2(5e-4),strides=stride,use_bias=bise)# 卷積+標(biāo)準(zhǔn)化+激活def Conv2D_BN_Leaky(filter,kernel_size,stride=2,l2=True,bise=False):return compose(conv2D(filter,kernel_size,stride,l2,bise),BatchNormalization(),LeakyReLU(alpha=0.1))
Conv2D_BN_Leaky()函數(shù)中的代碼一般情況下我們是這么寫的,使用reduce的話,可以使用和keras類似的函數(shù)式的方式搭建網(wǎng)絡(luò)。
def Conv2D_BN_Leaky(x,filter,kernel_size,stride=2,l2=True,bise=False):x=conv2D(x,filter,kernel_size,stride,l2,bise)x=BatchNormalization()(x)x=LeakyReLU(alpha=0.1)(x)return x
接著是 殘差網(wǎng)絡(luò),DarkNet的殘差結(jié)構(gòu)并不是和ResNet相同,而是和DenseNet類似,下采樣放到了殘差連接的外面,但連接處又使用add操作區(qū)別于DenseNet的concatenate操作。
# 殘差塊def res_block(x,filters,num_res_blocks):x=ZeroPadding2D(((1,0),(1,0)))(x)# 下采樣x=Conv2D_BN_Leaky(filters,kernel_size=3)(x)# 多個(gè)殘差塊for i in range(num_res_blocks):y=Conv2D_BN_Leaky(filters//2,kernel_size=1,stride=1)(x)y=Conv2D_BN_Leaky(filters,kernel_size=3,stride=1)(y)x=Add()([x,y])return x
接著,是主干網(wǎng)絡(luò)的搭建,這一部分比較簡單,直接寫入模型結(jié)構(gòu)中的參數(shù),然后調(diào)用前面的函數(shù)即可,再這里我們進(jìn)行了32倍下采樣,再最后使用全局平均池化展平特征向量,并在最后連接一個(gè)輸出層,是我們數(shù)據(jù)集的類別數(shù)。
def dreaknet53_output(inpt):# 200*60x=Conv2D_BN_Leaky(32,3,stride=1)(inpt)# 100*30x = res_block(x, 64, 1)# 50*15x = res_block(x,128 ,2)# 25*7x = res_block(x ,256, 8)# 12*3x = res_block(x ,512, 8)# 6*1x = res_block(x ,1024, 4)x=GlobalAveragePooling2D()(x)x=Dense(702,activation='softmax')(x)model=k.models.Model(inpt,x)return model
最后,在mian函數(shù)中創(chuàng)建模型結(jié)構(gòu),編譯模型,加載數(shù)據(jù),進(jìn)行訓(xùn)練,這里我們使用了一個(gè)回調(diào)函數(shù)用來保存最優(yōu)模型。
if __name__ == '__main__':model=dreaknet53_output(k.Input((200,60,3)))model.summary()#batch_size=4=load_data()m=k.callbacks.ModelCheckpoint('darknet_reid_{loss:.4f}.h5',monitor='loss',save_best_only=True)='categorical_crossentropy',optimizer=k.optimizers.Adam(lr=3e-5),metrics=['acc'])model.fit(gan_data(data,label,batch_size),steps_per_epoch=len(data)//batch_size,epochs=100,callbacks=[m])model.save('darknet_reid.h5')
#3 康康結(jié)果
Grain Rain
由于篇幅的原因,模型使用以及預(yù)測的代碼將在下一篇中放出,先康康最終效果咯。點(diǎn)個(gè)關(guān)注能夠第一時(shí)間收到更新哈!

