【图像分类案例】(2) DenseNet 天气图片四分类(权重迁移学习),附Tensorflow完整代码
各位同學(xué)好,今天和大家分享一下使用 Tensorflow 構(gòu)建 DenseNet 卷積神經(jīng)網(wǎng)絡(luò)模型,并使用預(yù)訓(xùn)練模型的權(quán)重,完成對四種天氣圖片的分類。
完整代碼在我的 Gitee 中,有需要的自取:
https://gitee.com/dgvv4/image-classification/tree/master
1. DenseNet
1. 網(wǎng)絡(luò)介紹
DenseNet 采用密集連接機(jī)制,即互相連接的所有層,每個層都會與前面所有層在通道維度上堆疊(layers.Concate),實現(xiàn)特征重用,作為下一層的輸入。這樣,不但緩解了梯度消失的現(xiàn)象,也使其可以在參與計算量更少的情況下實現(xiàn)比 ResNet 更優(yōu)的性能。
DenseNet 的密集連接方式需要特征圖大小保持一致,所以 DenseNet 網(wǎng)絡(luò)中使用 DenseBlock + Transition 的結(jié)構(gòu)。DenseBlock 是包含很多層的模塊,每個層特征圖大小相同,層與層之間采用密集連接方式。Transition模塊是連接兩個相鄰的DenseBlock,并且通過池化層完成下采樣。
1.2 DenseBlock代碼復(fù)現(xiàn)
假定輸入層的特征圖的通道數(shù)為 k0,且DenseBlock中各層卷積輸出特征圖的通道數(shù)為 k。那么第L層的輸入特征圖的通道數(shù)是 k0+(L-1)k,我們將k成為網(wǎng)絡(luò)的增長率(growth_rate)
因為每一層都接收前面的所有層的特征圖,即特征傳遞方式是直接將前面所有層的特征圖堆疊(layers.Concate)傳到下一層。
DenseBlock 采用激活函數(shù)在前,卷積層在后的順序,即BN+ReLU+Conv的順序,在DenseNet中該排序方法網(wǎng)絡(luò)的性能較好。
DenseBlock 內(nèi)部采用了1*1卷積減少參數(shù)量,將前面層的堆疊的通道數(shù)下降為4*k個,降低特征輸了,提高計算效率。
代碼展示:
#(1)構(gòu)建一個dense單元
def DenseLayer(x, growth_rate, dropout_rate=0.2):# BN+激活+1*1卷積降維x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)x = layers.Conv2D(filters = growth_rate*4, # 降低特征圖數(shù)量kernel_size = (1,1),strides = 1,padding = 'same')(x)# BN+激活+3*3卷積x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)x = layers.Conv2D(filters = growth_rate,kernel_size = (3,3),strides = 1,padding = 'same')(x)# 隨機(jī)殺死神經(jīng)元x = layers.Dropout(rate = dropout_rate)(x)return x#(2)構(gòu)建DenseBlock的多個卷積組合在一起的卷積塊
def DenseBlock(x, num, growth_rate):# 重復(fù)執(zhí)行多少次DenseLayerfor _ in range(num):conv = DenseLayer(x, growth_rate)# 將前面所有層的特征堆疊后傳到下一層x = layers.Concatenate()([x, conv])return x
1.3 Transition 代碼展示
Transition 模塊主要連接兩個相鄰的 DenseBlock,并下采樣降低特征圖的size,壓縮模型。Transition層包含一個1*1卷積層和2*2平均池化層。
#(3)Transition層連接兩個相鄰的DenseBlock
def Transition(x, compression_rate=0.5):# 1*1卷積下降一半的通道數(shù)out_channel = int(x.shape[-1] * compression_rate)# BN+激活+1*1卷積+2*2平均池化x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)x = layers.Conv2D(filters = out_channel, # 輸出通道數(shù)kernel_size = (1,1),strides = 1,padding = 'same')(x)x = layers.AveragePooling2D(pool_size = (2,2),strides = 2, # 下采樣padding = 'same')(x)return x
1.4 構(gòu)造主干網(wǎng)絡(luò)
網(wǎng)絡(luò)結(jié)構(gòu)圖如下,我以 DenseNet121 為例構(gòu)建網(wǎng)絡(luò)骨干。?
#(4)主干網(wǎng)絡(luò)架構(gòu)
def densenet(input_shape, classes, growth_rate, include_top):# 構(gòu)造輸入層[224,224,3]inputs = keras.Input(shape=input_shape)# 卷積下采樣[224,224,3]==>[112,112,64]x = layers.Conv2D(filters = 2*growth_rate, # 輸出特征圖個數(shù)為兩倍增長率kernel_size = (7,7),strides = 2,padding = 'same')(inputs)x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)# 最大池化[112,112,64]==>[56,56,64]x = layers.MaxPooling2D(pool_size = (3,3), strides = 2, padding = 'same')(x)# [56,56,64]==>[56,56,64+6*32]x = DenseBlock(x, num=6, growth_rate=growth_rate)# [56,56,256]==>[28,28,128]x = Transition(x)# [28,28,128]==>[28,28,128+12*32]x = DenseBlock(x, num=12, growth_rate=growth_rate)# [28,28,512]==>[14,14,256]x = Transition(x)# [14,14,256]==>[14,14,256+24*32]x = DenseBlock(x, num=24, growth_rate=growth_rate)# [14,14,1024]==>[7,7,512]x = Transition(x)# [7,7,512]==>[7,7,512+16*32]x = DenseBlock(x, num=16, growth_rate=growth_rate)# 導(dǎo)入模型時,是否包含輸出層if include_top is True:# [7,7,1024]==>[None,1024]x = layers.GlobalAveragePooling2D()(x)# [None,1024]==>[None,classes]x = layers.Dense(classes)(x) # 不經(jīng)過softmax層轉(zhuǎn)換成概率# 構(gòu)造模型model = Model(inputs, x)return model#(5)接收網(wǎng)絡(luò)模型
if __name__ == '__main__':model = densenet(input_shape=[224,224,3], # 輸入圖像的shapeclasses = 1000, # 分類數(shù)growth_rate = 32, # 設(shè)置增長率,即每個dense模塊的輸出通道數(shù)include_top = True) # 默認(rèn)包含輸出層model.summary() # 查看網(wǎng)絡(luò)架構(gòu)
2. 網(wǎng)絡(luò)訓(xùn)練
2.1 加載數(shù)據(jù)集
函數(shù) tf.keras.preprocessing.image_dataset_from_directory() 構(gòu)造數(shù)據(jù)集,
分批次讀取圖片數(shù)據(jù),參數(shù) img_size 會對讀進(jìn)來的圖片resize成指定大小;參數(shù) label_mode 中,'int'代表目標(biāo)值y是數(shù)值類型索引,即0, 1, 2, 3等;'categorical'代表onehot類型,對應(yīng)正確類別的索引的值為1,如圖像屬于第二類則表示為0,1,0,0,0;'binary'代表二分類。
#(1)加載數(shù)據(jù)集
def get_data(height, width, batchsz):# 訓(xùn)練集數(shù)據(jù)train_ds = keras.preprocessing.image_dataset_from_directory(directory = filepath + 'train', # 訓(xùn)練集圖片所在文件夾label_mode = 'categorical', # onehot編碼image_size = (height, width), # 輸入圖象的sizebatch_size = batchsz) # 每批次訓(xùn)練32張圖片# 驗證集數(shù)據(jù)val_ds = keras.preprocessing.image_dataset_from_directory(directory = filepath + 'val', label_mode = 'categorical', image_size = (height, width), batch_size = batchsz) # 返回劃分好的數(shù)據(jù)集return train_ds, val_ds# 讀取數(shù)據(jù)集
train_ds, val_ds = get_data(height, width, batchsz) #(2)查看數(shù)據(jù)集信息
def check_data(train_ds): # 傳入訓(xùn)練集數(shù)據(jù)集# 查看數(shù)據(jù)集有幾個分類類別class_names = train_ds.class_namesprint('classNames:', class_names)# 查看數(shù)據(jù)集的shape, x代表圖片數(shù)據(jù), y代表分類類別數(shù)據(jù)sample = next(iter(train_ds)) # 生成迭代器,每次取出一個batch的數(shù)據(jù)print('x_batch.shape:', sample[0].shape, 'y_batch.shape:', sample[1].shape)print('前五個目標(biāo)值:', sample[1][:5])# 是否查看數(shù)據(jù)集信息
if checkData is True:check_data(train_ds)#(3)查看圖像
def plot_show(train_ds):# 生成迭代器,每次取出一個batch的數(shù)據(jù)sample = next(iter(train_ds)) # sample[0]圖像信息, sample[1]標(biāo)簽信息# 顯示前5張圖for i in range(5):plt.subplot(1,5,i+1) # 在一塊畫板的子畫板上繪制1行5列plt.imshow(sample[0][i]/255.0) # 圖像的像素值壓縮到0-1plt.xticks([]) # 不顯示xy坐標(biāo)刻度plt.yticks([])plt.show()# 是否展示圖像信息
if plotShow is True:plot_show(train_ds)
天氣圖片如下:
2.2 預(yù)處理改進(jìn)
本篇使用遷移學(xué)習(xí)的方法,使用 DenseNet121 預(yù)訓(xùn)練模型的權(quán)重訓(xùn)練網(wǎng)絡(luò),大體流程和上一篇文章的非遷移學(xué)習(xí)訓(xùn)練方法類似,這里只說明一下需要改動的地方。可見上一篇案例:https://blog.csdn.net/dgvv4/article/details/123714507
首先,既然使用了別人訓(xùn)練好的權(quán)重繼續(xù)訓(xùn)練,那就要使用和別人一樣的數(shù)據(jù)預(yù)處理方法。
在imgnet中數(shù)據(jù)的預(yù)處理方法是,輸入圖像的各個通道的像素值都減去其均值,不使用像素值歸一化的方法。
# 數(shù)據(jù)預(yù)處理不使用歸一化,調(diào)整RGB三通道顏色均值
_R_MEAN = 123.68
_G_MEAN = 116.78
_B_MEAN = 103.94#(4)數(shù)據(jù)預(yù)處理
def processing(x,y): # 定義預(yù)處理函數(shù)x = tf.cast(x, dtype=tf.float32) # 圖片轉(zhuǎn)換為tensor類型y = tf.cast(y, dtype=tf.int32) # 分類標(biāo)簽轉(zhuǎn)換成tensor類型# 對圖像的各個通道減去均值,不進(jìn)行歸一化x = x - [_R_MEAN, _G_MEAN, _B_MEAN]return x,y# 對所有數(shù)據(jù)預(yù)處理
train_ds = train_ds.map(processing).shuffle(10000) # map調(diào)用自定義預(yù)處理函數(shù), shuffle打亂數(shù)據(jù)集
val_ds = val_ds.map(processing)
2.3 加載模型和權(quán)重
其次,加載網(wǎng)絡(luò)模型及權(quán)重,我們只需要網(wǎng)絡(luò)的特征提取層,不需要網(wǎng)絡(luò)的輸出層(原網(wǎng)絡(luò)輸出一般都是1000分類)。通過設(shè)置 model.trainable = False 就能凍結(jié)網(wǎng)絡(luò)特征提取層的所有權(quán)重,正反向傳播過程中只有輸出層的權(quán)重會發(fā)生優(yōu)化更新。建議前10輪的訓(xùn)練凍結(jié)特征提取層,后面的訓(xùn)練再設(shè)置?model.trainable = True 網(wǎng)絡(luò)所有權(quán)重都能更新。
自定義輸出層,由于原網(wǎng)絡(luò)的輸出層一般都是1000分類,而我們自己的網(wǎng)絡(luò)一般是幾個分類,因此輸出層的網(wǎng)絡(luò)結(jié)構(gòu)需要我們自己寫。通過 keras.Sequential() 容器堆疊網(wǎng)絡(luò)各層。
model = densenet(input_shape=input_shape, # 網(wǎng)絡(luò)的輸入圖像的sizeclasses=classes, # 分類數(shù)growth_rate = 32, # 設(shè)置增長率,即每個dense模塊的輸出通道數(shù)include_top=False) # 不載入輸出層# 報錯解決:You are trying to load a weight file containing 242 layers into a model with
# 添加參數(shù):by_name=True
model.load_weights(pre_weights_path, by_name=True)# False凍結(jié)模型的所有參數(shù),只能更新輸出層的參數(shù)
model.trainable = True# 自定義輸出層,不經(jīng)過softmax激活,有利于網(wǎng)絡(luò)的穩(wěn)定性
model = keras.Sequential([model, # [7,7,1024]layers.GlobalAveragePooling2D(), # ==>[None,1024]layers.Dropout(rate=0.5),layers.Dense(512), # ==>[None,512]layers.Dropout(rate=0.5),layers.Dense(classes)]) #==>[None,classes]
2.4 訓(xùn)練結(jié)果
和非遷移學(xué)習(xí)的訓(xùn)練方法相比需要改進(jìn)的部分只有上面兩點,接下來開始訓(xùn)練,設(shè)置回調(diào)函數(shù)將驗證集損失最低的一次迭代的權(quán)重保存下來。
#(7)保存權(quán)重文件
if not os.path.exists(weights_dir): # 判斷當(dāng)前文件夾下有沒有一個叫save_weights的文件夾os.makedirs(weights_dir) # 如果沒有就創(chuàng)建一個#(8)模型編譯
opt = optimizers.Adam(learning_rate=learning_rate) # 設(shè)置Adam優(yōu)化器model.compile(optimizer=opt, #學(xué)習(xí)率loss=keras.losses.CategoricalCrossentropy(from_logits=True), # 交叉熵?fù)p失,logits層先經(jīng)過softmaxmetrics=['accuracy']) #評價指標(biāo)#(9)定義回調(diào)函數(shù),一個列表
# 保存模型參數(shù)
callbacks = [keras.callbacks.ModelCheckpoint(filepath = 'save_weights/densenet.h5', # 參數(shù)保存的位置save_best_only = True, # 保存最佳參數(shù)save_weights_only = True, # 只保存權(quán)重文件monitor = 'val_loss')] # 通過驗證集損失判斷是否是最佳參數(shù)#(10)模型訓(xùn)練,history保存訓(xùn)練信息
history = model.fit(x = train_ds, # 訓(xùn)練集validation_data = val_ds, # 驗證集epochs = epochs, #迭代30次callbacks = callbacks,initial_epoch=0)
訓(xùn)練完成后,history中保存了訓(xùn)練信息,繪制損失曲線和準(zhǔn)確率曲線。
#(11)獲取訓(xùn)練信息
history_dict = history.history # 獲取訓(xùn)練的數(shù)據(jù)字典
train_loss = history_dict['loss'] # 訓(xùn)練集損失
train_accuracy = history_dict['accuracy'] # 訓(xùn)練集準(zhǔn)確率
val_loss = history_dict['val_loss'] # 驗證集損失
val_accuracy = history_dict['val_accuracy'] # 驗證集準(zhǔn)確率#(12)繪制訓(xùn)練損失和驗證損失
plt.figure()
plt.plot(range(epochs), train_loss, label='train_loss') # 訓(xùn)練集損失
plt.plot(range(epochs), val_loss, label='val_loss') # 驗證集損失
plt.legend() # 顯示標(biāo)簽
plt.xlabel('epochs')
plt.ylabel('loss')#(13)繪制訓(xùn)練集和驗證集準(zhǔn)確率
plt.figure()
plt.plot(range(epochs), train_accuracy, label='train_accuracy') # 訓(xùn)練集準(zhǔn)確率
plt.plot(range(epochs), val_accuracy, label='val_accuracy') # 驗證集準(zhǔn)確率
plt.legend()
plt.xlabel('epochs')
plt.ylabel('accuracy')
訓(xùn)練過程中的準(zhǔn)確率和損失
Epoch 1/10
99/99 [==============================] - 64s 354ms/step - loss: 1.8832 - accuracy: 0.5543 - val_loss: 117.8199 - val_accuracy: 0.4000
Epoch 2/10
99/99 [==============================] - 16s 140ms/step - loss: 1.5482 - accuracy: 0.6619 - val_loss: 1.3441 - val_accuracy: 0.6800
Epoch 3/10
99/99 [==============================] - 16s 138ms/step - loss: 2.0702 - accuracy: 0.6129 - val_loss: 5.8502 - val_accuracy: 0.5289
Epoch 4/10
99/99 [==============================] - 16s 137ms/step - loss: 1.2101 - accuracy: 0.7477 - val_loss: 14.2739 - val_accuracy: 0.5956
Epoch 5/10
99/99 [==============================] - 15s 137ms/step - loss: 0.7785 - accuracy: 0.7687 - val_loss: 0.4700 - val_accuracy: 0.8356
Epoch 6/10
99/99 [==============================] - 16s 138ms/step - loss: 1.1095 - accuracy: 0.7349 - val_loss: 0.8931 - val_accuracy: 0.8089
Epoch 7/10
99/99 [==============================] - 16s 138ms/step - loss: 0.8705 - accuracy: 0.7835 - val_loss: 0.2821 - val_accuracy: 0.8978
Epoch 8/10
99/99 [==============================] - 16s 138ms/step - loss: 0.9680 - accuracy: 0.7760 - val_loss: 0.6360 - val_accuracy: 0.8533
Epoch 9/10
99/99 [==============================] - 16s 139ms/step - loss: 0.9522 - accuracy: 0.8041 - val_loss: 0.3987 - val_accuracy: 0.8844
Epoch 10/10
99/99 [==============================] - 16s 138ms/step - loss: 1.0262 - accuracy: 0.7649 - val_loss: 0.4000 - val_accuracy: 0.8578
3. 預(yù)測階段
以對整個測試集的圖片預(yù)測為例,test_ds 存放測試集的圖片和類別標(biāo)簽,對測試集進(jìn)行和訓(xùn)練集相同的預(yù)處理方法,將圖片的每個通道像素值減去每個通道的均值。
model.predict(img) 返回的是每張圖片屬于每個類別的概率,需要找到概率最大值所對應(yīng)的索引 np.argmax(result),該索引對應(yīng)的分類名稱就是最終預(yù)測結(jié)果。
import tensorflow as tf
from tensorflow import keras
from DenseNet import densenet
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt# 報錯解決:NotFoundError: No algorithm worked! when using Conv2D
from tensorflow.compat.v1 import ConfigProto
from tensorflow.compat.v1 import InteractiveSession
config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)# ------------------------------------ #
# 預(yù)測參數(shù)設(shè)置
# ------------------------------------ #
im_height = 224 # 輸入圖像的高
im_width = 224 # 輸入圖像的高
# 分類名稱
class_names = ['cloudy', 'rain', 'shine', 'sunrise']
# 權(quán)重路徑
weight_dir = 'save_weights/densenet_new.h5'
# ------------------------------------ #
# 數(shù)據(jù)預(yù)處理,調(diào)整RGB三通道顏色均值
# ------------------------------------ #
_R_MEAN = 123.68
_G_MEAN = 116.78
_B_MEAN = 103.94
# ------------------------------------ #
# 單張圖片預(yù)測
# ------------------------------------ #
# 是否只預(yù)測一張圖
single_pic = False
# 圖像所在文件夾的路徑
single_filepath = 'D:/deeplearning/test/....../001.jpg'
# 指定某張圖片
picture = single_filepath + 'rain94.jpg'# ------------------------------------ #
# 對測試集圖片預(yù)測
# ------------------------------------ #
test_pack = True
# 驗證集文件夾路徑
test_filepath = 'D:/deeplearning/test/數(shù)據(jù)集/...../test/'#(1)載入模型,不載入輸出層
model = densenet(input_shape=[224,224,3], classes=4, growth_rate = 32, include_top=False)
print('model is loaded')#(2)構(gòu)造輸出層
model = keras.Sequential([model,keras.layers.GlobalAveragePooling2D(), # ==>[None,1024]keras.layers.Dropout(rate=0.5),keras.layers.Dense(512), # ==>[None,512]keras.layers.Dropout(rate=0.5),keras.layers.Dense(len(class_names)), #==>[None,classes] keras.layers.Softmax()]) # 這里需要經(jīng)過得到softmax分類概率#(3)載入權(quán)重.h文件
model.load_weights(weight_dir, by_name=True)
print('weights is loaded')#(4)只對單張圖像預(yù)測
if single_pic is True:# 加載圖片img = Image.open(picture)# 改變圖片sizeimg = img.resize((im_height, im_width))# 展示圖像plt.figure()plt.imshow(img)plt.xticks([])plt.yticks([])# 預(yù)處理,圖像像素值減去均值img = np.array(img).astype(np.float32) # 變成tensor類型img = img - [_R_MEAN, _G_MEAN, _B_MEAN]# 輸入網(wǎng)絡(luò)的要求,給圖像增加一個batch維度, [h,w,c]==>[b,h,w,c]img = np.expand_dims(img, axis=0)# 預(yù)測圖片,返回結(jié)果包含batch維度[b,n]result = model.predict(img)# 轉(zhuǎn)換成一維,擠壓掉batch維度result = np.squeeze(result)# 找到概率最大值對應(yīng)的索引predict_class = np.argmax(result)# 打印預(yù)測類別及概率print('class:', class_names[predict_class], 'prob:', result[predict_class])plt.title(f'{class_names[predict_class]}')plt.show()#(5)對測試集圖像預(yù)測
if test_pack is True:# 載入測試集test_ds = keras.preprocessing.image_dataset_from_directory(directory = test_filepath, label_mode = 'int', # 不經(jīng)過ont編碼, 1、2、3、4、、、 image_size = (im_height, im_width), # 測試集的圖像resizebatch_size = 32) # 每批次32張圖# 測試集預(yù)處理#(2)數(shù)據(jù)預(yù)處理def processing(image, label): image = tf.cast(image, tf.float32) # 修改數(shù)據(jù)類型label = tf.cast(label, tf.int32)# 對圖像的各個通道減去均值,不進(jìn)行歸一化image = image - [_R_MEAN, _G_MEAN, _B_MEAN]return (image, label)test_ds = test_ds.map(processing) # 預(yù)處理test_true = [] # 存放真實值test_pred = [] # 存放預(yù)測值# 遍歷測試集所有的batchfor imgs, labels in test_ds:# 每次每次取出一個batch的一張圖像和一個標(biāo)簽for img, label in zip(imgs, labels):# 網(wǎng)絡(luò)輸入的要求,給圖像增加一個維度[h,w,c]==>[b,h,w,c]image_array = tf.expand_dims(img, axis=0)# 預(yù)測某一張圖片,返回圖片屬于許多類別的概率prediction = model.predict(image_array)# 找到預(yù)測概率最大的索引對應(yīng)的類別test_pred.append(class_names[np.argmax(prediction)])# label是真實標(biāo)簽索引test_true.append(class_names[label])# 展示結(jié)果print('真實值: ', test_true[:10])print('預(yù)測值: ', test_pred[:10])# 繪制混淆矩陣from sklearn.metrics import confusion_matriximport seaborn as snsimport pandas as pdplt.rcParams['font.sans-serif'] = ['SimSun'] #宋體plt.rcParams['font.size'] = 15 #設(shè)置字體大小# 生成混淆矩陣conf_numpy = confusion_matrix(test_true, test_pred)# 轉(zhuǎn)換成DataFrame表格類型,設(shè)置行列標(biāo)簽conf_df = pd.DataFrame(conf_numpy, index=class_names, columns=class_names)# 創(chuàng)建繪圖區(qū)plt.figure(figsize=(8,7))# 生成熱力圖sns.heatmap(conf_df, annot=True, fmt="d", cmap="BuPu")# 設(shè)置標(biāo)簽plt.title('Confusion_Matrix')plt.xlabel('Predict')plt.ylabel('True')
總結(jié)
以上是生活随笔為你收集整理的【图像分类案例】(2) DenseNet 天气图片四分类(权重迁移学习),附Tensorflow完整代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【图像分类案例】(1) ResNeXt
- 下一篇: 【目标检测】(5) YOLOV1 目标检