深度學習與計算機視覺(一) 深度圖片分類器

CV和深度學習

前幾篇博客講了一些深度學習常用結構的原理, 計算方法和實現. 大致有仿射網絡層, 卷積池化層, 時序運算RNN層, 以及BN, Dropout和激活函數, 損失函數等. 我也嘗試了用這些自己寫的結構搭建深度網絡解決一些實際問題.

線性層,激活函數,損失函數
卷積層, 池化層
BPTT
Dropout, Batch Normalization
今天, 深度學習是一個熱門的計算機科學研究方向, 而它最主要的應用場景是CV和NLP. 反觀常規的應用場景, 如推薦系統和數據分析, 反而不是很適合用DL. 最主要的原因是, 深度學習太過追求end to end, 雖然免去了人爲建模和特徵工程的步驟, 但也讓模型的可解釋性變得很差. 這樣的模型落地時, 不但可能在實際場景中遇到無法預測的重大錯誤, 還不容易寫出漂亮的PPT拿去融資(^^). 客戶需要的是清晰的高萃取度特徵(from Xgboost), 以及比決策樹更簡單易懂的模型(邏輯迴歸), 深度網絡這種不穩定的模型自然不適合落地. 但是打打kaggle還是沒問題的.
但是CV領域有些不同的地方, CV處理的數據是圖像數據, 從圖像數據中提取特徵的最有力手段就是深度卷積, 使用其他傳統方法得到的特徵其可解釋度不一定比深度學習更好, 所以深度學習能在CV領域大行其道. 到了今天, 已經是imagenet的時代, 一般的需求直接拿預訓練模型fine tune一下就能得到不錯的效果.
這裏就先簡單講一下CV中最基礎的圖像n分類問題的解法, 之前的博客我也實現過簡單的CNN, 不過要想得到更好的效果, CNN需要一些其他的設計技巧.

VGG

VGG基於Alexnet改動, 比起Alex它具有更深的深度. 它使用小的,多層的卷積核代替原本大的卷積核, 比如5x5, 7x7和11x11的卷積核都可以用多層3x3的卷積核來代替. 池化仍然是使用2x2, 2步長的max pooling. 它使用的參數並不比alex多很多, 雖然計算開銷上要更大一些(每一層都是獨立的矩陣運算), 但是效果卻也遠遠蓋過了alex, 他也證明了深度可以提升模型的性能.
在這裏插入圖片描述
另一點VGG的設計, 在於卷積特徵提取結束後的分類全連接層, 它使用size非常大的卷積核(和特徵圖大小一致)代替全連接層. 我們可以想象, 用KCxCxHxW的卷積核去卷積CxHxW的特徵圖, 會得到KC個輸出. 這樣的運算和"把特徵圖展開成1d, 再做全連接dense層"是完全相同的. 這就允許我們一定程度上用卷積層代替線性全連接層. 這樣做的好處是我們可以接收任意尺寸的圖片, 只要它不小於卷積核, 那就都可以運算. 這就解除了對圖片維度的限制.

Inception

GoogLeNet中使用了一種叫做Inception的技術, 它旨在讓通道數變得不那麼多, 從而減少運算量. 具體的做法, 是把當前M的通道的特徵圖, 它們每一個位置對應的像素值通過一個矩陣乘法進行線性變換, 變成比M小的N通道. 這個過程也就是用一個1x1的卷積核, 0填充卷積特徵圖. 卷積層的通道數N要小於M, 這就是Inception的運算.
比如,一個3x3的卷積核,如果其輸入和輸出的通道數均爲512,那麼需要的計算量爲9x512x512。在卷積操作中,輸出特徵圖上某一個位置,其是與所有的輸入特徵圖是相連的,這是一種密集連接結構。GoogLeNet基於這樣的理念:在深度網路中大部分的激活值是不必要的(爲0),或者由於相關性是冗餘。因此,最高效的深度網路架構應該是激活值之間是稀疏連接的,這意味着512個輸出特徵圖是沒有必要與所有的512輸入特徵圖相連。如果我們使用Inception, 就可以先通過很少運算量的1x512x512把特徵圖壓縮到n通道, 再用3x3的卷積核處理輸出成512通道, 即只需要1x512x512+9xnx512的計算量. 這樣實現了一種間接的剪枝. inception可以隨心所欲改變通道數, 但一般不會增加通道數, 不談數學只談直覺, 我們也能知道, 降低通道數信息量不一定縮減, 但增加通道數信息量一定不會增加.
在這裏插入圖片描述
GoogLeNet的另一個創新之處在於使用了average pooling, 它對最終輸出的特徵圖再做平均池化, 直接把HxW的特徵圖壓縮成1x1. 這樣全連接層接收的信息更少, 使用的參數更少. 這是有利於訓練和部署的, 實踐時GoogleNet的速度和準確率都超過了VGG.

爲什麼需要殘差

高效的CNN經典架構有很多, 比如上面的兩種. 但是不管是哪種技術, 在網絡比較深層時都會面臨梯度消失問題, 從而讓訓練很困難. 即使是VGG和googlenet也只是十幾層而已, 但是當我們想要幾十層的網絡, 事情就變得不簡單了. 我們之前說過的batch-norm可以一定程度上解決這個問題, 但是仍然有些治標不治本. 爲此kaiming大神的團隊在15年提出了resnet的方法, 它允許網絡的跨層連接, 從而梯度能夠更有效傳播到上層.
y=f(x)+x y = f(x)+x
在這裏插入圖片描述
從前向傳播的角度來看, resnet讓映射更像恆等映射. 當理想映射極接近於恆等映射時,殘差映射易於捕捉恆等映射的細微波動。也就是, 我們認爲學習一個映射t(x)=f(x)+xt(x) = f(x)+x比起直接學習t(x)=f(x)t(x) = f(x)要更簡單, 實踐也告訴我們的確如此.

殘差模塊

我們使用在跨層時使用張量加法, 而如果跨層的兩者維度不同, 那麼這個相加是無法進行的. 因此跨層的部分應該不改變特徵圖的長寬, 通道數也應該是一樣的. 這點和VGG很像, 所以實踐時我們一般會用這兩種設計思想來設計網絡.
在這裏插入圖片描述

殘差模塊實現

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

# 3x3 convolution
def conv3x3(in_channels, out_channels, stride=1):
    return nn.Conv2d(in_channels, out_channels, kernel_size=3, 
                     stride=stride, padding=1, bias=False)

# Residual block
class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        """
        Initializes internal Module state, shared by both nn.Module.
        """
        super(ResidualBlock, self).__init__()
        self.block = nn.Sequential(
            conv3x3(in_channels, out_channels,stride = stride),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(),
            conv3x3(out_channels, out_channels),
            nn.BatchNorm2d(out_channels)
        )
        
        self.downsample = downsample 
                
    def forward(self, x):
        """
        Defines the computation performed at every call.
        x: N * C * H * W
        """
        # if the size of input x changes, using downsample to change the size of residual
        residual = x

        out = self.block(x)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = F.relu(out)

        return out

我們在兩次卷積中可能會使輸入的tensor的size與輸出的tensor的size不相等,爲了使它們能夠相加,所以輸出的tensor與輸入的tensor size不同時,我們使用downsample(由外部傳入)來使保持size相同

殘差網絡實現

在這裏插入圖片描述
上圖是ResNet18架構, 我們嘗試照着這種架構搭建殘差網用於cifar-10分類.

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=10):
        """
        Initializes internal Module state, shared by both nn.Module and ScriptModule.
        """
        super(ResNet, self).__init__()
        self.in_channels = 64
        
        #part1
        self.conv_in = nn.Sequential(
            conv3x3(3, self.in_channels),
            nn.BatchNorm2d(self.in_channels),
            nn.ReLU()
        )
        
        #part2 
        self.layer1 = self.make_layer(block, 64, num_blocks=layers[0])
        self.layer2 = self.make_layer(block, 128, num_blocks=layers[1], stride=2)
        self.layer3 = self.make_layer(block, 256, num_blocks=layers[2], stride=2)
        self.layer4 = self.make_layer(block, 512, num_blocks=layers[3], stride=2)
        
        #part3
        self.dropout10 = nn.Dropout(0.1)
        self.dropout50 = nn.Dropout(0.5)
        self.avgpool = nn.AvgPool2d(4, 4)
        self.fc = nn.Linear(512, 10)
        
    def make_layer(self, block, out_channels, num_blocks, stride=1):
        """
        make a layer with num_blocks blocks.
        """
        
        downsample = None
        if (stride != 1) or (self.in_channels != out_channels):
            # use Conv2d with stride to downsample
            downsample = nn.Sequential(
                conv3x3(self.in_channels, out_channels, stride=stride),
                nn.BatchNorm2d(out_channels))
        
        # first block with downsample
        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        
        self.in_channels = out_channels
        # add num_blocks - 1 blocks
        for i in range(1, num_blocks):
            layers.append(block(out_channels, out_channels))
            
        # return a layer containing layers
        return nn.Sequential(*layers)
    
    def forward(self, x):
        """
        Defines the computation performed at every call.
        """
        out = self.conv_in(x)
        out = self.layer1(out)
        out = self.dropout10(out)
        out = self.layer2(out)
        out = self.dropout10(out)
        out = self.layer3(out)
        out = self.dropout10(out)
        out = self.layer4(out)
        out = self.avgpool(out)
    
        # view: here change output size from 4 dimensions to 2 dimensions
        out = out.view(out.size(0), -1)
        out = self.dropout50(out)
        out = self.fc(out)
        return out
resnet = ResNet(ResidualBlock, [2, 2, 2, 2])  #ResNet18

訓練

def train(model, train_loader, loss_func, optimizer, device):
    """
    train model using loss_fn and optimizer in an epoch.
    model: CNN networks
    train_loader: a Dataloader object with training data
    loss_func: loss function
    device: train on cpu or gpu device
    """
    model.train()
    total_loss = 0
    # train the model using minibatch
    for i, (images, targets) in enumerate(train_loader):
        images = images.to(device)
        targets = targets.to(device)

        # forward
        outputs = model(images)
        loss = loss_func(outputs, targets)

        # backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        total_loss += loss.item()
        
        # every 100 iteration, print loss
        if (i + 1) % 100 == 0:
            print ("Step [{}/{}] Train Loss: {:.4f}"
                   .format(i+1, len(train_loader), loss.item()))
    return total_loss / len(train_loader)

def evaluate(model, val_loader, device):
    """
    model: CNN networks
    val_loader: a Dataloader object with validation data
    device: evaluate on cpu or gpu device
    return classification accuracy of the model on val dataset
    """
    # evaluate the model
    model.eval()
    # context-manager that disabled gradient computation
    with torch.no_grad():
        correct = 0
        total = 0
        
        for i, (images, targets) in enumerate(val_loader):
            # device: cpu or gpu
            images = images.to(device)
            targets = targets.to(device)
            
            
            outputs = model(images)
            
            # return the maximum value of each row of the input tensor in the 
            # given dimension dim, the second return vale is the index location
            # of each maxium value found(argmax)
            _, predicted = torch.max(outputs.data, dim=1)
            
            
            correct += (predicted == targets).sum().item()
            
            total += targets.size(0)
            
        accuracy = correct / total
        print('Accuracy on Test Set: {:.4f} %'.format(100 * accuracy))
        return accuracy

def save_model(model, save_path):
    # save model
    torch.save(model.state_dict(), save_path)

import matplotlib.pyplot as plt
def show_curve(ys, title):
    """
    plot curlve for Loss and Accuacy
    Args:
        ys: loss or acc list
        title: loss or accuracy
    """
    x = np.array(range(len(ys)))
    y = np.array(ys)
    plt.plot(x, y, c='b')
    plt.axis()
    plt.title('{} curve'.format(title))
    plt.xlabel('epoch')
    plt.ylabel('{}'.format(title))
    plt.show()

def fit(model, num_epochs, optimizer, device):
    """
     train and evaluate an classifier num_epochs times.
    n and evaluate an classifier num_epochs times.
    We use optimizer and cross entropy loss to train the model. 
    Args: 
        model: CNN network
        num_epochs: the number of training epochs
        optimizer: optimize the loss function    loss_func.to(device)
    loss_func.to(device)

    """
        
    # loss and optimizer
    loss_func = nn.CrossEntropyLoss()
    
    model.to(device)
    loss_func.to(device)
    
    # log train loss and test accuracy
    losses = []
    accs = []
    
    for epoch in range(num_epochs):
        
        print('Epoch {}/{}:'.format(epoch + 1, num_epochs))
        # train step
        loss = train(model, train_loader, loss_func, optimizer, device)
        losses.append(loss)
        
        # evaluate step
        accuracy = evaluate(model, test_loader, device)
        accs.append(accuracy)
        
    
    # show curve
    show_curve(losses, "train loss")
    show_curve(accs, "test accuracy")

from torch.utils.data import Dataset
import torch.utils.data as Data
import torchvision.transforms as transforms
import torchvision

BATCH_SIZE = 100

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

train_data=torchvision.datasets.CIFAR10(
    root='E:/cifar10',
    train=True,
    transform=transform
)
train_loader = Data.DataLoader(
    dataset=train_data,
    batch_size=BATCH_SIZE,
    shuffle=True
)
test_data=torchvision.datasets.CIFAR10(
    root='E:/cifar10',
    train=False,
    transform=transform
)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=BATCH_SIZE,
                                         shuffle=False)


resnet.load_state_dict(torch.load('resnet18.cpk'))
# Hyper-parameters
num_epochs = 5
lr = 0.001
# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# optimizer
optimizer = torch.optim.Adam(resnet.parameters(), lr=lr)

fit(resnet, num_epochs, optimizer, device)

在這裏插入圖片描述
在這裏插入圖片描述
網絡比較深層, 需要比之前更多的迭代epoch次數, 最好用GPU. 這裏跑了15個epoch, 可以觀察到準確率能穩步提升到85以上, 並且還有增長的空間.
反觀我們之前實現的4卷積層簡單網絡, 只能把正確率拉到70以上. 這也證明了深度對性能的影響.

SN殘差

在這裏插入圖片描述
Squeeze-and-Excitation的技巧是比較新的(2017), 用於提升ResNet的殘差模塊性能的一種技巧. 它通過把殘差模塊的輸出x做額外的全局池化, 再做編碼-譯碼以得到"每個通道的重要度打分". 這個重要度應該體現在輸出上, 所以我們把這個分數直接乘在輸出上.
這種思想類似我們後面RNN中常用的attention, 是一種end2end的設計思想. 雖然可能很不可思議, 但是實踐上這個方法是切實能提升模型性能的.

class SELayer(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y.expand_as(x)

class SEResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None, reduction=16):
        super(SEResidualBlock, self).__init__()
        self.res = ResidualBlock(in_channels, out_channels, stride, downsample)
        self.se = SELayer(out_channels, reduction)
        
    def forward(self, x):
        residual = x
        out = self.res(x)
        out = self.se(out)
        out += residual
        return out
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章