膨脹卷積——《MULTI-SCALE CONTEXT AGGREGATION BY DILATED CONVOLUTIONS》
看這篇論文主要是想了解膨脹卷積,搜出這篇,看起來貌似比deeplab簡單一些,於是以此入手。這篇論文把膨脹卷積的計算原理講的很清楚,但是作用和產生的緣由的話還是deeplab的論文更容易懂,deeplab裡面叫"hole algorithm"。
1. dense prediction
在談膨脹卷積之前想先說一下dense prediction,一開始對這個概念不太理解,看了看別人的解釋後說說自己的理解吧。
在做分類時,我們輸入一整張圖片(假如是(n,n,3)尺寸的),輸出只需要一個class label(尺寸為1);
做目標檢測時,給出物體所在邊框(方框)的4個頂點(尺寸為(1,4));
做物體邊緣檢測時,給出物體的邊緣畫素點,這時候上升到畫素級別,預測實際上是基於每個畫素了(尺寸為(n, n,1));
做語義分割時,實際上是對輸入影象的每個畫素點做類別預測,假如有m類,輸出實際上是每個畫素點被分為各類別的概率(尺寸為(n,n,m));例項分割的話類別就更多了,輸出也更稠密。
按照輸入和輸出的size對比,越往下,輸出相對於輸入的密度越大。從笨妞做過的實驗來看,迭代需要的時間也越長。
作者認為dense prediction有兩個關鍵點多尺度上下文推理和全解析度輸出。“最近的工作研究了兩種處理多尺度推理和全解析度密集預測的衝突需求的方法.一種方法是重複向上卷積,目的是恢復丟失的解析度,同時從下采樣層進行全域性透視。另一種方法包括將影象的多個重新縮放版本作為輸入到網路,並結合為這些多個輸入獲得的預測。” 個人理解就是既要通過前面的多尺度卷積抽取影象的特徵,卷積抽取特徵的一大特點就是特徵的尺寸會變小;同時,由於前面說了dense prediction需要對每個畫素進行預測,輸出需要儲存和原解析度。像FCN就是前面抽特徵的時候先縮,特徵抽完了再放。
膨脹卷積就是為了保持泛化的抽特徵,同時影象的尺寸不縮減。
2. 膨脹卷積
本篇論文中的膨脹卷積平面計算層是這樣的:
deeplab論文裡面的計算細節是這樣的:
當然,為了保持尺寸,通常需要先對影象做padding. padding的數量和膨脹率相關。
一般卷積和膨脹卷積的計算差別:
“膨脹卷積運算元在過去被稱為“帶擴張濾波器的卷積”,它在小波分解演算法a trous中起著關鍵的作用。我們用“擴張卷積”一詞代替了“膨脹濾波器卷積”來說明沒有構造或表示“擴張濾波器”,而是對卷積運算元本身進行了修改,使其能夠以不同的方式使用濾波器引數。擴張卷積運算元可以使用不同的擴張因子在不同的範圍內應用相同的濾波器,我們的定義反映了擴張卷積運算元的適當實現,它不涉及擴張濾波器的構造。”
原理:擴充套件的卷積支援指數擴充套件的接受域,從而不丟失解析度或覆蓋範圍。
論文這個公式笨妞不太能理解,自己算了一下,擴充套件的接受域大概是這樣的:
從膨脹卷積的計算過程可以看到,它既帶有conv卷積濾波功能,同時具有pool層的泛化作用,但與pool層不同,pool只要stride>1,特徵圖的尺寸就會大幅減小,而dilate不會。
3. 多尺度的膨脹卷積
論文中提到的膨脹卷積網路有7層,其中,每一層的核size都是(3,3)每層的膨脹率率分別是(1,1,2,4,8,16,1),但是作者提供的程式中網路更加複雜,在論文講到的膨脹卷積網路前面添加了VGG16。同時,vgg16前4個模組之外維持不變,第5個卷積模組改為膨脹率為2,第一個全連線層改為核尺寸為(7,7),膨脹率為4的卷積。可能這就是作者後面提到的更大的網路吧。
論文的網路結構是這樣的:
上面網路的初始化方法:
後面提到的更復雜網路的初始化方法:
4.keras版本的程式解析
作者額源程式是基於caffe。多年不用caffe,實在不習慣,從github上找了個keras版本的,地址在這裡
程式是載入預訓練模型做預測的,預訓練模型只有theano的,沒有我需要的tensorflow模型。另外,作者採用4個數據集,分別是
cityscapes、pascol voc2012、kitti、camvid,每個資料集的影象尺寸不同,網路略有不同,但主要基本一樣,就以cityscapes對應的網路來看吧。
def get_dilation_model_cityscapes(input_shape, apply_softmax, input_tensor, classes):
if input_tensor is None:
model_in = Input(shape=input_shape)
else:
if not K.is_keras_tensor(input_tensor):
model_in = Input(tensor=input_tensor, shape=input_shape)
else:
model_in = input_tensor
"""""""""""""""""""vgg16""""""""""""""""""""
h = Convolution2D(64, 3, 3, activation='relu', name='conv1_1')(model_in)
h = Convolution2D(64, 3, 3, activation='relu', name='conv1_2')(h)
h = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='pool1')(h)
h = Convolution2D(128, 3, 3, activation='relu', name='conv2_1')(h)
h = Convolution2D(128, 3, 3, activation='relu', name='conv2_2')(h)
h = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='pool2')(h)
h = Convolution2D(256, 3, 3, activation='relu', name='conv3_1')(h)
h = Convolution2D(256, 3, 3, activation='relu', name='conv3_2')(h)
h = Convolution2D(256, 3, 3, activation='relu', name='conv3_3')(h)
h = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='pool3')(h)
h = Convolution2D(512, 3, 3, activation='relu', name='conv4_1')(h)
h = Convolution2D(512, 3, 3, activation='relu', name='conv4_2')(h)
h = Convolution2D(512, 3, 3, activation='relu', name='conv4_3')(h)
h = AtrousConvolution2D(512, 3, 3, atrous_rate=(2, 2), activation='relu', name='conv5_1')(h)
h = AtrousConvolution2D(512, 3, 3, atrous_rate=(2, 2), activation='relu', name='conv5_2')(h)
h = AtrousConvolution2D(512, 3, 3, atrous_rate=(2, 2), activation='relu', name='conv5_3')(h)
h = AtrousConvolution2D(4096, 7, 7, atrous_rate=(4, 4), activation='relu', name='fc6')(h)
h = Dropout(0.5, name='drop6')(h)
h = Convolution2D(4096, 1, 1, activation='relu', name='fc7')(h)
h = Dropout(0.5, name='drop7')(h)
h = Convolution2D(classes, 1, 1, name='final')(h)
"""""""""""""""""""vgg16""""""""""""""""""""
#到上面為止都是vgg16,只是第5個conv模組加入了膨脹, fc6從全連線層變成了conv層,並加入膨脹,fc7和final也變成conv層。
""""""""""""""""""""論文當中的網路模組"""""""""""""""""""""""""
h = ZeroPadding2D(padding=(1, 1))(h) #膨脹卷積之前先padding
h = Convolution2D(classes, 3, 3, activation='relu', name='ctx_conv1_1')(h)
h = ZeroPadding2D(padding=(1, 1))(h)
h = Convolution2D(classes, 3, 3, activation='relu', name='ctx_conv1_2')(h)
h = ZeroPadding2D(padding=(2, 2))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(2, 2), activation='relu', name='ctx_conv2_1')(h)
h = ZeroPadding2D(padding=(4, 4))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(4, 4), activation='relu', name='ctx_conv3_1')(h)
h = ZeroPadding2D(padding=(8, 8))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(8, 8), activation='relu', name='ctx_conv4_1')(h)
h = ZeroPadding2D(padding=(16, 16))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(16, 16), activation='relu', name='ctx_conv5_1')(h)
h = ZeroPadding2D(padding=(32, 32))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(32, 32), activation='relu', name='ctx_conv6_1')(h)
h = ZeroPadding2D(padding=(64, 64))(h)
h = AtrousConvolution2D(classes, 3, 3, atrous_rate=(64, 64), activation='relu', name='ctx_conv7_1')(h) #論文中有7個卷積層,這裡多了一個。
""""""""""""""""""""論文當中的網路模組"""""""""""""""""""""""""
h = ZeroPadding2D(padding=(1, 1))(h)
""""""""""""""""""""類似於全連線層部分"""""""""""""""""""""""""
h = Convolution2D(classes, 3, 3, activation='relu', name='ctx_fc1')(h)
h = Convolution2D(classes, 1, 1, name='ctx_final')(h)
""""""""""""""""""""類似於全連線層部分"""""""""""""""""""""""""
"""""""""""""""""""上取樣模組,只有cityscape對應的網路採用"""""""""""""
# the following two layers pretend to be a Deconvolution with grouping layer.
# never managed to implement it in Keras
# since it's just a gaussian upsampling trainable=False is recommended
h = UpSampling2D(size=(8, 8))(h)
logits = Convolution2D(classes, 16, 16, bias=False, trainable=False, name='ctx_upsample')(h)
"""""""""""""""""""上取樣模組,只有cityscape對應的網路採用"""""""""""""
if apply_softmax:
model_out = softmax(logits) #2維softmax
else:
model_out = logits
model = Model(input=model_in, output=model_out, name='dilation_cityscapes')
return model
預測程式:
#沒有細看這個方法的演算法,大致看來是把反射填充的部分重新算回去,並做zoom
#做zoom的原因在於,除了sityscape對應的網路有最後兩層upsample,部分還原了影象尺寸,其他資料集對應的網路都沒有upsample,需要通過zoom來放大輸出。
def interp_map(prob, zoom, width, height):
zoom_prob = np.zeros((prob.shape[0], height, width), dtype=np.float32)
for c in range(prob.shape[0]):
for h in range(height):
for w in range(width):
r0 = h // zoom
r1 = r0 + 1
c0 = w // zoom
c1 = c0 + 1
rt = float(h) / zoom - r0
ct = float(w) / zoom - c0
v0 = rt * prob[c, r1, c0] + (1 - rt) * prob[c, r0, c0]
v1 = rt * prob[c, r1, c1] + (1 - rt) * prob[c, r0, c1]
zoom_prob[c, h, w] = (1 - ct) * v0 + ct * v1
return zoom_prob
def predict(image, model, ds):
image = image.astype(np.float32) - CONFIG[ds]['mean_pixel']
#輸入影象大多是(500,500)以內的圖片,作者採用的是填充而不是resize同一影象尺寸,
#先對整張圖做同一尺寸的填充,然後再根據每個影象本身的尺寸做影象分割和填充。
conv_margin = CONFIG[ds]['conv_margin']
input_dims = (1,) + CONFIG[ds]['input_shape']
batch_size, num_channels, input_height, input_width = input_dims
model_in = np.zeros(input_dims, dtype=np.float32)
image_size = image.shape
output_height = input_height - 2 * conv_margin
output_width = input_width - 2 * conv_margin
#整體填充
image = cv2.copyMakeBorder(image, conv_margin, conv_margin,
conv_margin, conv_margin,
cv2.BORDER_REFLECT_101) #填充方式為“反射填充”
#計算影象分割數量
num_tiles_h = image_size[0] // output_height + (1 if image_size[0] % output_height else 0)
num_tiles_w = image_size[1] // output_width + (1 if image_size[1] % output_width else 0)
row_prediction = []
for h in range(num_tiles_h):
col_prediction = []
for w in range(num_tiles_w):
offset = [output_height * h,
output_width * w]
#有重疊的分割,並填充到input_size
tile = image[offset[0]:offset[0] + input_height,
offset[1]:offset[1] + input_width, :]
margin = [0, input_height - tile.shape[0],
0, input_width - tile.shape[1]]
tile = cv2.copyMakeBorder(tile, margin[0], margin[1],
margin[2], margin[3],
cv2.BORDER_REFLECT_101)
model_in[0] = tile.transpose([2, 0, 1])
#每張分割圖做一次預測
prob = model.predict(model_in)[0]
col_prediction.append(prob)
col_prediction = np.concatenate(col_prediction, axis=2)
row_prediction.append(col_prediction) #預測圖合併成大圖
prob = np.concatenate(row_prediction, axis=1)
if CONFIG[ds]['zoom'] > 1:
#做zoom,還原影象。
prob = interp_map(prob, CONFIG[ds]['zoom'], image_size[1], image_size[0])
prediction = np.argmax(prob, axis=0)
color_image = CONFIG[ds]['palette'][prediction.ravel()].reshape(image_size)
return color_image
程式只有theano版本的預訓練模型,於是笨妞自己弄了個訓練程式,訓練pascol voc2012,訓練程式如下
import numpy as np
import cv2
from dilation_net import DilationNet
from datasets import CONFIG
from keras.optimizers import SGD
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing import image
from PIL import Image
import random
def binarylab(labels, size, nb_class):
y = np.zeros((size,size,nb_class))
for i in range(size):
for j in range(size):
y[i, j,labels[i][j]] = 1
"""
for k in range(nb_class):
plt.imshow(y[:, :, k])
plt.show()
"""
return y
def load_data(path, size=224, mode=None):
img = Image.open(path)
w,h = img.size
if w < h:
if w < size:
img = img.resize((size, size*h//w))
w, h = img.size
else:
if h < size:
img = img.resize((size*w//h, size))
w, h = img.size
img = img.crop((int((w-size)*0.5), int((h-size)*0.5), int((w+size)*0.5), int((h+size)*0.5)))
if mode=="original":
return img
if mode=="label":
y = np.array(img, dtype=np.int32)
mask = y == 255
y[mask] = 0
y = binarylab(y, size, 21)
#y = np.expand_dims(y, axis=0)
return y
if mode=="data":
X = image.img_to_array(img)
#X = np.expand_dims(X, axis=0)
X = preprocess_input(X)
return X
def generate_arrays_from_file(names, path_to_train, path_to_target, input_size, output_size, batch_size):
while True:
for name in names:
Xpath = path_to_train + "{}.jpg".format(name)
ypath = path_to_target + "{}.png".format(name)
X = []
y = []
for i in range(batch_size):
X.append(load_data(Xpath, input_size, mode="data"))
y.append(load_data(ypath, output_size, mode="label"))
X = np.array(X)
y = np.array(y)
yield (X, y)
if __name__ == '__main__':
ds = 'voc12' # choose between cityscapes, kitti, camvid, voc12
nb_class = 21
# get the model
model = DilationNet(dataset=ds)
sgd = SGD(lr=0.0002)
model.compile(optimizer=sgd, loss='categorical_crossentropy')
model.summary()
input_size = CONFIG[ds]['input_shape'][0]
output_size = 34
nb_class = CONFIG[ds]['classes']
path_to_train = 'VOCtrainval_11-May-2012/VOCdevkit/VOC2012/JPEGImages/'
path_to_target = 'VOCtrainval_11-May-2012/VOCdevkit/VOC2012/SegmentationClass/'
path_to_txt = 'VOCtrainval_11-May-2012/VOCdevkit/VOC2012/ImageSets/Segmentation/train.txt'
with open(path_to_txt, "r") as f:
ls = f.readlines()
names = [l.rstrip('\n') for l in ls]
random.shuffle(names)
nb_data = len(names)
train_gen = generate_arrays_from_file(names, path_to_train, path_to_target, input_size, output_size, batch_size=2)
model.fit_generator(train_gen,
samples_per_epoch=nb_data//2,
nb_epoch=1)
但是發現反向計算梯度的時候記憶體直接溢位了,笨妞的機子實在太爛,16G記憶體,沒有GPU。沒辦法,只能閹割網路,先把vgg16第5個conv模組閹割掉,還是報溢位,沒有辦法,接著把fcn6和fcn7的kernel數量從4096縮減到1024,終於可以正常跑了。1輪訓練要花10個小時,龜速啊!
使用預設學習率(0.001),第二個batch,loss就變nan了,降到0.0001,loss穩著不動,到0.0002,勉強慢慢下降,下降也是很慢。
其實還有一個加快速度的辦法就是前面的vgg16模型直接載入imagenet預訓練權重,並設定為不訓練。同時,也可以在閹割一下,直接用論文中的7層網路。