resnet 又叫深度殘差網絡
圖像識別准確率很高,主要作者是國人哦
深度網絡的退化問題
深度網絡難以訓練,梯度消失,梯度爆炸,老生常談,不多說
resnet 解決了這個問題,並且將網絡深度擴展到了最多152層。怎么解決的呢?
殘差學習
結構如圖
在普通的卷積過程中加入了一個x的恆等映射(identity mapping)
專家把這稱作 skip connections 或者 shortcut connections
殘差結構的理解
為什么要這樣呢?下面我從多個角度闡述這個問題。
生活角度
每學習一個模型,我都希望能用日常的生活去解釋為什么模型要這樣,一是加深對模型的理解,二是給自己搭建模型尋找靈感,三是給優化模型尋找靈感。
resnet 無疑是解決很難識別的問題的,那我舉一個日常生活中人類也難以識別的問題,看看這個模型跟人類的識別方法是否一致。
比如人類識別杯子里的水燙不燙
一杯水,我摸了一下,燙,好,我的神經開始運轉,最后形成理論杯子里的水燙,這顯然不對
又一杯水,我一摸,不燙,好嘛,這咋辦,認知混亂了,也就是無法得到有效的參數,
那人類是怎么辦呢?
我們不止是摸一摸,而且在摸過之后還要把杯子拿起來仔細看看,有什么細節可以幫助我們更好的識別,這就是在神經經過運轉后,又把x整體輸入,
當然即使我們拿起杯子看半天,也可能看不出任何規律來幫助我們識別,那人類的作法是什么呢?我記住吧,這種情況要小心,這就是梯度消失了,學習不到任何規律,記住就是恆等映射,
這個過程和resnet是一致的。
網絡結構角度
當梯度消失時,f(x)=0,y=g(x)=relu(x)=x,怎么理解呢?
1. 當梯度消失時,模型就是記住,長這樣的就是該類別,是一個大型的過濾器
2. 在網絡上堆疊這樣的結構,就算梯度消失,我什么也學不到,我至少把原來的樣子恆等映射了過去,相當於在淺層網絡上堆疊了“復制層”,這樣至少不會比淺層網絡差。
3. 萬一我不小心學到了什么,那就賺大了,由於我經常恆等映射,所以我學習到東西的概率很大。
數學角度
可以看到 有1 的存在,導數基本不可能為0
那為什么叫殘差學習呢
可以看到 F(x) 通過訓練參數 得到了 H(x)-x,也就是殘差,所以叫殘差學習,這比學習H(x)要簡單的多。
等效映射 identity mapping
上面提到殘差學習中需要進行 F(x)+x,在resnet中,卷積都是 same padding 的,當通道數相同時,直接相加即可,
但是通道數不一樣時需要尋求一種方法使得 y=f(x)+wx
實現w有兩種方式
1. 直接補0
2. 通過使用多個 1x1 的卷積來增加通道數。
網絡結構
block
block為一個殘差單元,resnet 網絡由多個block 構成,resnet 提出了兩種殘差單元
左邊針對的是ResNet34淺層網絡,右邊針對的是ResNet50/101/152深層網絡,右邊這個又被叫做 bottleneck
bottleneck 很好地減少了參數數量,第一個1x1的卷積把256維channel降到64維,第三個又升到256維,總共用參數:1x1x256x64+3x3x64x64+1x1x64x256=69632,
如果不使用 bottleneck,參數將是 3x3x256x256x2=1179648,差了16.94倍
這里的輸出通道數是根據輸入通道數確定的,因為要與x相加。
整體結構
1. 與vgg相比,其參數少得多,因為vgg有3個全連接層,這需要大量的參數,而resnet用 avg pool 代替全連接,節省大量參數。
2. 參數少,殘差學習,所以訓練效率高
結構參數
Resnet50和Resnet101是其中最常用的網絡結構。
我們看到所有的網絡都分成5部分,分別是:conv1,conv2_x,conv3_x,conv4_x,conv5_x
其結構是相對固定的,只是通道數根據輸入確定。
注意,Resnet 最后的 avg_pool 是把每個 feature map 轉換成 1 個特征,故池化野 size 為 feature map size,如 最后輸出位 512x7x7,那么池化野size 為 7
pytorch-resnet34
from torch import nn import torch as t from torch.nn import functional as F class ResidualBlock(nn.Module): ### 殘差單元 def __init__(self, inchannel, outchannel, stride=1, shortcut=None): ### 卷積 super(ResidualBlock, self).__init__() self.left = nn.Sequential( nn.Conv2d(inchannel, outchannel, 3, stride, 1, bias=False), nn.BatchNorm2d(outchannel), nn.ReLU(inplace=True), nn.Conv2d(outchannel, outchannel, 3, 1, 1, bias=False), nn.BatchNorm2d(outchannel) ) self.right = shortcut def forward(self, x): ### 先恆等映射,然后加上卷積后的out再relu out = self.left(x) residual = x if self.right is None else self.right(x) out += residual return F.relu(out) class ResNet34(nn.Module): def __init__(self, num_classes=1000): super(ResNet34, self).__init__() ### 先做 7x7 卷積 self.pre = nn.Sequential( nn.Conv2d(3, 64, 7, 2 ,3, bias=False), ### 輸入 3 通道,輸出 64 通道,卷積核7x7,步長2,padding 3 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(3, 1, 1) ### inchannel,outchannel,padding ) ### 共32層 self.layer1 = self._make_layer(64, 128, 3) ### 3 個 64 通道的殘差單元,輸出 128通道,共6層 self.layer2 = self._make_layer(128, 256, 4, stride=2) ### 4 個 128通道的殘差單元,輸出 256通道,共8層 self.layer3 = self._make_layer(256, 512, 6, stride=2) ### 6 個 256通道的殘差單元,輸出 512通道,共12層 self.layer4 = self._make_layer(512, 512, 3, stride=2) ### 3 個 512通道的殘差單元,輸出 512通道,共6層 ### fc,1層 self.fc = nn.Linear(512, num_classes) def _make_layer(self, inchannel, outchannel, block_num, stride=1): ### 1x1 卷積 改變通道數 shortcut = nn.Sequential( nn.Conv2d(inchannel, outchannel, 1, stride, bias=False), nn.BatchNorm2d(outchannel) ) layers = [] layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut)) ### 先來一個殘差單元,主要是改變通道數 ### 再接幾個同樣的殘差單元,通道數不變 for i in range(1, block_num+1): ### block_num layers.append(ResidualBlock(outchannel, outchannel)) return nn.Sequential(*layers) def forward(self, x): ### 第1層 x = self.pre(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) ### 注意 resnet 最后的池化是把一個 feature map 變成一個特征,故池化野大小等於最后 x 的大小 x = F.avg_pool2d(x, 2) ### 這里用的 cifar10 數據集,此時的 x size 為 512x2x2,所以池化野為2 x = x.view(x.size(0), -1) return self.fc(x)
tensorflow-resnet50
注意我標注的層數
class ResNet50(object): def __init__(self, inputs, num_classes=1000, is_training=True, scope="resnet50"): self.inputs =inputs self.is_training = is_training self.num_classes = num_classes with tf.variable_scope(scope): # construct the model ### 1層 net = conv2d(inputs, 64, 7, 2, scope="conv1") # -> [batch, 112, 112, 64] net = tf.nn.relu(batch_norm(net, is_training=self.is_training, scope="bn1")) net = max_pool(net, 3, 2, scope="maxpool1") # -> [batch, 56, 56, 64] ### 每個bottleneck3層, 16 * 3 = 48層 net = self._block(net, 256, 3, init_stride=1, is_training=self.is_training, scope="block2") # -> [batch, 56, 56, 256] net = self._block(net, 512, 4, is_training=self.is_training, scope="block3") # -> [batch, 28, 28, 512] net = self._block(net, 1024, 6, is_training=self.is_training, scope="block4") # -> [batch, 14, 14, 1024] net = self._block(net, 2048, 3, is_training=self.is_training, scope="block5") # -> [batch, 7, 7, 2048] net = avg_pool(net, 7, scope="avgpool5") # -> [batch, 1, 1, 2048] net = tf.squeeze(net, [1, 2], name="SpatialSqueeze") # -> [batch, 2048] # 1層 self.logits = fc(net, self.num_classes, "fc6") # -> [batch, num_classes] self.predictions = tf.nn.softmax(self.logits) def _block(self, x, n_out, n, init_stride=2, is_training=True, scope="block"): with tf.variable_scope(scope): h_out = n_out // 4 out = self._bottleneck(x, h_out, n_out, stride=init_stride, is_training=is_training, scope="bottlencek1") for i in range(1, n): out = self._bottleneck(out, h_out, n_out, is_training=is_training, scope=("bottlencek%s" % (i + 1))) return out def _bottleneck(self, x, h_out, n_out, stride=None, is_training=True, scope="bottleneck"): """ A residual bottleneck unit""" n_in = x.get_shape()[-1] if stride is None: stride = 1 if n_in == n_out else 2 with tf.variable_scope(scope): # 3層 h = conv2d(x, h_out, 1, stride=stride, scope="conv_1") h = batch_norm(h, is_training=is_training, scope="bn_1") h = tf.nn.relu(h) h = conv2d(h, h_out, 3, stride=1, scope="conv_2") h = batch_norm(h, is_training=is_training, scope="bn_2") h = tf.nn.relu(h) h = conv2d(h, n_out, 1, stride=1, scope="conv_3") h = batch_norm(h, is_training=is_training, scope="bn_3") if n_in != n_out: shortcut = conv2d(x, n_out, 1, stride=stride, scope="conv_4") shortcut = batch_norm(shortcut, is_training=is_training, scope="bn_4") else: shortcut = x return tf.nn.relu(shortcut + h)
最新進展
殘差單元被進一步更新
個人經驗
1. 卷積層包含大量的卷積計算,如果想降低時間復雜度,減少卷積層
2. 全連接層包含大量的參數,如果想降低空間復雜度,減少全連接層
參考資料:
https://blog.csdn.net/lanran2/article/details/79057994
https://zhuanlan.zhihu.com/p/31852747