TensorFlow2-卷积神经网络
TensorFlow2卷積神經(jīng)網(wǎng)絡(luò)
簡介
卷積神經(jīng)網(wǎng)絡(luò)廣泛使用于計算機視覺領(lǐng)域,主要用于提取圖片的特征圖(feature map)。不同于數(shù)學(xué)中的卷積,這里的卷積指的是對矩陣的一種運算方式(本質(zhì)上就是離散化的數(shù)學(xué)卷積),使用卷積核對圖片矩陣進行操作,可以減少圖片的位置信息,增加圖片的通道信息,從而得到高層語義信息。
卷積操作
在提出卷積的運算方式之前必須清楚,為什么不使用全連接神經(jīng)網(wǎng)絡(luò)對圖片處理,為什么使用卷積操作,為什么可以使用卷積。
- 之所以不使用全連接神經(jīng)網(wǎng)絡(luò)是因為,全連接神經(jīng)網(wǎng)絡(luò)處理圖片這類特征量很大的數(shù)據(jù),即使是淺層神經(jīng)網(wǎng)絡(luò),其參數(shù)量、數(shù)據(jù)量、梯度量都是極為龐大的,如此龐大的數(shù)據(jù)量是很難存儲運算的(在過去很長一段時間內(nèi)是不可能的)。而且,全連接神經(jīng)網(wǎng)絡(luò)在圖片數(shù)據(jù)上的表現(xiàn)并不好。
- 依據(jù)動物的局部感受野機制(每次觀察只注意小部分區(qū)域),卷積神經(jīng)網(wǎng)絡(luò)誕生了。如果說全連接神經(jīng)網(wǎng)絡(luò)每次提取特征面向的是全局(每次全面觀察,這是費力且沒有必要的),卷積神經(jīng)網(wǎng)絡(luò)則是使用卷積核每次提取局部的特征(每次觀察圖片的一小部分,且觀察方式不變),使用不同的卷積核就可以觀察到各種特征信息。通過卷積神經(jīng)網(wǎng)絡(luò)代替全連接神經(jīng)網(wǎng)絡(luò),網(wǎng)絡(luò)的參數(shù)量大大減少了(卷積神經(jīng)網(wǎng)絡(luò)的參數(shù)只存在于卷積核中)。
- 使用卷積核掃描區(qū)域并將卷積核對應(yīng)位置的參數(shù)和掃描區(qū)域數(shù)值相乘求和得到特征值,掃描多個區(qū)域得到多個特征值組合的矩陣就是特征圖(feature map)。需要注意的是,卷積操作是針對矩陣區(qū)域的,對多個通道的同區(qū)域會進行相應(yīng)合并得到一個特征圖(合并一般是分別對每個通道提取特征,然后特征值求和得到該區(qū)域的特征)。之所以使用卷積操作提取特征,這是由來已久的,在計算機視覺的歷史上使用特定的濾波器處理圖片是個常用手段,卷積很早就被提出了,不過那時候?qū)τ诓煌娜蝿?wù)人為設(shè)計不同的濾波器(卷積核),這里只是在深度學(xué)習(xí)的基礎(chǔ)上沿用罷了(卷積核變?yōu)樽詣訉W(xué)習(xí)而不是人為設(shè)計)。
卷積神經(jīng)網(wǎng)絡(luò)
通過上述卷積操作,可以得到尺寸變小的高通道特征圖,如使用16個3*3卷積核提取5*5圖片特征(圖片如第二節(jié)文末)會得到維度為[3, 3, 16]的特征圖,很顯然,圖片的尺寸變小了。當(dāng)然,區(qū)域維度的信息減少,位置維度的信息增加,這就是卷積神經(jīng)網(wǎng)絡(luò)提取特征的目的(這個過程表現(xiàn)為feature map長寬減小,channel增加,所以卷積神經(jīng)網(wǎng)絡(luò)的卷積核數(shù)目逐層增加或不變)。但是無法控制的變化是不必要的,這個尺寸變換的過程在深層網(wǎng)絡(luò)中不容易明確,需要步步計算才能得到,為了解決這個麻煩,提出了Padding方法(填充方法)和Stride方法(步長方法)。
- Padding是指在原圖周圍補上一圈全0的數(shù)值,使得變換后的矩陣尺寸不變。
- 同時,為了調(diào)節(jié)特征圖尺寸,控制stride也是不錯的選擇(一般的深度學(xué)習(xí)框架步長默認(rèn)為1)。
卷積神經(jīng)網(wǎng)絡(luò)的體系逐漸發(fā)展,產(chǎn)生了固定的一些結(jié)構(gòu),如卷積層、池化層、上采樣層等。這些固定的矩陣操作層在TensorFlow2中的keras模塊中得到了封裝。
- 2D卷積的接口如下,指明卷積核數(shù)目,卷積核大小,步長,以及填充方式(same表示自動等尺寸,valid表示不進行padding)等即可。
- 2D池化(下采樣)接口如下,指明步長即可。根據(jù)給定的步長將該區(qū)域的所有值取最大或者平均。
- 上采樣是下采樣的逆操作,有不同的方法,指明采用大小即可。
還有一個很關(guān)鍵的問題在卷積神經(jīng)網(wǎng)絡(luò)中,就是如何求出輸出loss關(guān)于卷積核參數(shù)的梯度。這個計算并不復(fù)雜,根據(jù)鏈?zhǔn)椒▌t可以知道最終Loss關(guān)于某層feature map的梯度為Loss關(guān)于O的梯度乘以O(shè)關(guān)于參數(shù)w的梯度,這個過程不復(fù)雜,TensorFlow是自動完成的。
經(jīng)典網(wǎng)絡(luò)
在80年代后期,SVM幾乎統(tǒng)治了機器學(xué)習(xí)世界,深度學(xué)習(xí)的發(fā)展受阻,直到2012年AlexNet的突破性表現(xiàn),深度卷積神經(jīng)網(wǎng)絡(luò)在計算機視覺的發(fā)展才如火如荼起來,先后出現(xiàn)了VGGNet、GoogLeNet、ResNet、DenseNet等,總體的發(fā)展趨勢圍繞Google和微軟產(chǎn)生了兩種,越來越寬或者越來越深。由于這些網(wǎng)絡(luò)的提出解析都需要大量篇幅介紹,這里只提一下突破性的成就,設(shè)計自己的應(yīng)用網(wǎng)絡(luò)時可以借鑒這些突破點。
- AlexNet
- 由Hinton主導(dǎo)(深度學(xué)習(xí)三駕馬車之一)相比較于之前的LeNet結(jié)構(gòu)類似,不過提出了Max pooling、Relu激活函數(shù)、Dropout正則化,并且第一次使用GPU加速訓(xùn)練。
- 不過,AlexNet由于當(dāng)時顯卡限制,手工進行網(wǎng)絡(luò)的雙卡切分,如今的顯卡以及深度框架會幫助完成這些。
- VGGNet
- 由牛津大學(xué)VGG組提出,主要貢獻是采用多層小卷積核代替大卷積核計算量減少且效果更好。計算量較大。
- GoogLeNet
- 由Google設(shè)計,主要貢獻為使用1*1調(diào)整通道數(shù)目,對同一層使用多種卷積核以獲得不同感受野得到不同信息,使得網(wǎng)絡(luò)更寬。
- ResNet
- 華人學(xué)者何凱明于微軟亞洲研究院提出殘差模塊,解決了過深網(wǎng)絡(luò)難以訓(xùn)練(主要是梯度彌散)的情況。這是深層卷積神經(jīng)網(wǎng)絡(luò)發(fā)展的基礎(chǔ),也是計算機視覺近幾年最突破的成就之一。
- DenseNet
- 每一層都與前面所有層連接,連接極為密集,信息量很大的網(wǎng)絡(luò)設(shè)計,基于ResNet設(shè)計。
Cifar100與VGG13實戰(zhàn)
VGGNet在卷積神經(jīng)網(wǎng)絡(luò)的歷史上地位舉足輕重,其做出了很多新的嘗試并產(chǎn)生很多貢獻。具體的介紹可以查看我關(guān)于VGGNet的博文。這里通過構(gòu)建VGG13網(wǎng)絡(luò)對Cifar100進行分類實戰(zhàn)。具體Pipeline流程為加載數(shù)據(jù)集、構(gòu)建網(wǎng)絡(luò)、訓(xùn)練網(wǎng)絡(luò)、測試網(wǎng)絡(luò)。VGGNet是很簡單粗暴的容易理解的卷積神經(jīng)網(wǎng)絡(luò)之一,它的設(shè)計思路也是按照卷積神經(jīng)網(wǎng)絡(luò)的基本設(shè)計思想。
VGGNet的具體網(wǎng)絡(luò)配置如下表。
具體的Python代碼下面給出。
""" Author: Zhou Chen Date: 2019/11/2 Desc: VGG13 for Cifar100 """ import os import tensorflow as tf from tensorflow.keras import layers, optimizers, datasets, Sequential os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 忽略低級別警告conv_layers = [# block1layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),# block2layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),# block3layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),# block4layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),# block5layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')]def preprocess(x, y):# [0~1]x = tf.cast(x, dtype=tf.float32) / 255.y = tf.cast(y, dtype=tf.int32)return x, y(x, y), (x_test, y_test) = datasets.cifar100.load_data() y, y_test = tf.squeeze(y, axis=1), tf.squeeze(y_test, axis=1) print(x.shape, y.shape, x_test.shape, y_test.shape)train_db = tf.data.Dataset.from_tensor_slices((x, y)) train_db = train_db.shuffle(1000).map(preprocess).batch(128)test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test)) test_db = test_db.map(preprocess).batch(64)sample = next(iter(train_db)) print('sample:', sample[0].shape, sample[1].shape,tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))def main():# [b, 32, 32, 3] => [b, 1, 1, 512]conv_net = Sequential(conv_layers)fc_net = Sequential([layers.Dense(256, activation=tf.nn.relu),layers.Dense(128, activation=tf.nn.relu),layers.Dense(100, activation=None),])conv_net.build(input_shape=[None, 32, 32, 3])fc_net.build(input_shape=[None, 512])optimizer = optimizers.Adam(lr=1e-4)variables = conv_net.trainable_variables + fc_net.trainable_variables # 拼接變量列表# trainingfor epoch in range(50):for step, (x, y) in enumerate(train_db):with tf.GradientTape() as tape:# [b, 32, 32, 3] => [b, 1, 1, 512]out = conv_net(x)out = tf.reshape(out, [-1, 512])# [b, 512] => [b, 100]logits = fc_net(out)y_onehot = tf.one_hot(y, depth=100)# compute lossloss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)loss = tf.reduce_mean(loss)grads = tape.gradient(loss, variables)optimizer.apply_gradients(zip(grads, variables))if step % 100 == 0:print(epoch, step, 'loss:', float(loss))total_num = 0total_correct = 0for x, y in test_db:out = conv_net(x)out = tf.reshape(out, [-1, 512])logits = fc_net(out)prob = tf.nn.softmax(logits, axis=1)pred = tf.argmax(prob, axis=1)pred = tf.cast(pred, dtype=tf.int32)correct = tf.cast(tf.equal(pred, y), dtype=tf.int32)correct = tf.reduce_sum(correct)total_num += x.shape[0]total_correct += int(correct)acc = total_correct / total_numprint(epoch, 'acc:', acc)if __name__ == '__main__':main()Cifar100與ResNet18實戰(zhàn)
盡管VGG曾今取得過矚目的成就,然而過于龐大的計算量在今天如此深層的網(wǎng)絡(luò)下已經(jīng)有些不合適了,因此ResNet的提出解決了這個問題。這里通過ResNet18對Cifar100進行訓(xùn)練測試,對比其與VGGNet的效果。下面給出模型構(gòu)建代碼,訓(xùn)練代碼類似之前,可以在文末的Github地址找到。
""" Author: Zhou Chen Date: 2019/11/2 Desc: ResNet實現(xiàn) """ import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers, Sequentialclass BasicBlock(layers.Layer):def __init__(self, filter_num, stride=1):super(BasicBlock, self).__init__()self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')self.bn1 = layers.BatchNormalization()self.relu = layers.Activation('relu')self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')self.bn2 = layers.BatchNormalization()if stride != 1:self.downsample = Sequential()self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))else:self.downsample = lambda x: xdef call(self, inputs, training=None):# [b, h, w, c]out = self.conv1(inputs)out = self.bn1(out, training=training)out = self.relu(out)out = self.conv2(out)out = self.bn2(out, training=training)identity = self.downsample(inputs)output = layers.add([out, identity])output = tf.nn.relu(output)return outputclass ResNet(keras.Model):def __init__(self, layer_dims, num_classes=100): # [2, 2, 2, 2]super(ResNet, self).__init__()self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1)),layers.BatchNormalization(),layers.Activation('relu'),layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')])self.layer1 = self.build_resblock(64, layer_dims[0])self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)# output: [b, 512, h, w],self.avgpool = layers.GlobalAveragePooling2D()self.fc = layers.Dense(num_classes)def call(self, inputs, training=None):x = self.stem(inputs, training=training)x = self.layer1(x, training=training)x = self.layer2(x, training=training)x = self.layer3(x, training=training)x = self.layer4(x, training=training)# [b, c]x = self.avgpool(x)# [b, 100]x = self.fc(x)return xdef build_resblock(self, filter_num, blocks, stride=1):res_blocks = Sequential()# may down sampleres_blocks.add(BasicBlock(filter_num, stride))for _ in range(1, blocks):res_blocks.add(BasicBlock(filter_num, stride=1))return res_blocksdef resnet18():return ResNet([2, 2, 2, 2])補充說明
本文介紹了卷積神經(jīng)網(wǎng)絡(luò)相關(guān)內(nèi)容以及在TensorFlow2中如何實現(xiàn)CNN模型。具體的代碼同步至我的Github倉庫歡迎star;博客同步至我的個人博客網(wǎng)站,歡迎查看其他文章。如有疏漏,歡迎指正。
總結(jié)
以上是生活随笔為你收集整理的TensorFlow2-卷积神经网络的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TensorFlow2-网络训练技巧
- 下一篇: 卷积神经网络精确率不增反降_深度学习