李宏毅课程作业五 CNN Explaination


本文主要是对课程里的代码加上自己的注解,记录下点滴知识

一、代码作业

1.环境设置

代码如下(示例):

# 下載並解壓縮訓練資料
!gdown --id '19CzXudqN58R3D-1G8KeFWk8UDQwlb8is' --output food-11.zip
!unzip food-11.zip
# 下載 pretrained model,這裡是用助教的 model demo,寫作業時要換成自己的 model
!gdown --id '1CShZHsO8oAZwxQkMe7jRtEgSNb2w_OZu' --output checkpoint.pth
# 安裝lime套件
# 這份作業會用到的套件大部分 colab 都有安裝了,只有 lime 需要額外安裝
!pip install lime==0.1.1.37

2.引入库

代码如下(示例):

import os
#os模块提供了多数操作系统的功能接口函数。
#当os模块被导入后,它会自适应于不同的操作系统平台,根据不同的平台进行相应的操作,
#在python编程时,经常和文件、目录打交道,这时就离不了os模块
import sys
#Python的sys模块提供访问由解释器使用或维护的变量的接口,并提供了一些函数用来和解释器进行交互,操控Python的运行时环境。
import argparse
# argparse 是 Python 内置的一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,
# argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息。
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.optim import Adam
from torch.utils.data import Dataset
import torchvision.transforms as transforms
from skimage.segmentation import slic#图像分割
from lime import lime_image#图像解释器库
from pdb import set_trace#pdb自带调试器

3.参数分析

args = {
      'ckptpath': './checkpoint.pth',
      'dataset_dir': './food-11/'
}
args = argparse.Namespace(**args)

4.定义模型

class Classifier(nn.Module):
  def __init__(self):
    super(Classifier, self).__init__()

    def building_block(indim, outdim):
      return [
        nn.Conv2d(indim, outdim, 3, 1, 1),#(in_channel, out_channel, kernel_size, stride, padding)
        nn.BatchNorm2d(outdim),
        nn.ReLU(),
      ]
    def stack_blocks(indim, outdim, block_num):
      layers = building_block(indim, outdim)
      for i in range(block_num - 1):
        layers += building_block(outdim, outdim)
      layers.append(nn.MaxPool2d(2, 2, 0))
      #class torch.nn.MaxPool1d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
      return layers

    cnn_list = []
    cnn_list += stack_blocks(3, 128, 3)
    cnn_list += stack_blocks(128, 128, 3)
    cnn_list += stack_blocks(128, 256, 3)
    cnn_list += stack_blocks(256, 512, 1)
    cnn_list += stack_blocks(512, 512, 1)
    self.cnn = nn.Sequential( * cnn_list)#Sequential定义训练框架
##############
#   self.cnn = nn.Sequential{

#}
##############
    dnn_list = [
      nn.Linear(512 * 4 * 4, 1024),
      nn.ReLU(),
      nn.Dropout(p = 0.3),
      nn.Linear(1024, 11),
    ]
    self.fc = nn.Sequential( * dnn_list)
    #########
#   self.fc = nn.Sequential{
#    nn.Linear(512*4*4, 1024)
#    nn.ReLU(),
#    nn.Dropout(p=0.3),
#    nn.Linear(1024,11)
#}
#########
  def forward(self, x):
    out = self.cnn(x)
    out = out.reshape(out.size()[0], -1)
    return self.fc(out)
model = Classifier().cuda()
checkpoint = torch.load(args.ckptpath)
model.load_state_dict(checkpoint['model_state_dict'])
# 基本上出現 <All keys matched successfully> 就是有載入成功,但最好還是做一下 inference 確認 test accuracy 沒有錯。

5.定义创建数据集

# 助教 training 時定義的 dataset
# 因為 training 的時候助教有使用底下那些 transforms,所以 testing 時也要讓 test data 使用同樣的 transform
# dataset 這部分的 code 基本上不應該出現在你的作業裡,你應該使用自己當初 train HW3 時的 preprocessing
class FoodDataset(Dataset):
    def __init__(self, paths, labels, mode):
        # mode: 'train' or 'eval'
        
        self.paths = paths
        self.labels = labels
        #数据预处理
        trainTransform = transforms.Compose([
            transforms.Resize(size=(128, 128)),#把图片调整至合适大小
            transforms.RandomHorizontalFlip(),#翻转图片
            transforms.RandomRotation(15),#按角度旋转图像。
            #torchvision.transforms.RandomRotation(degrees, resample=False, expand=False, center=None)
            #degrees(sequence 或float或int) -要选择的度数范围。
            #resample 可选的重采样过滤器
            #expand(bool,optional) - 可选的扩展标志。
            #如果为true,则展开输出以使其足够大以容纳整个旋转图像。如果为false或省略,则使输出图像与输入图像的大小相同
            #center(2-tuple ,optional) - 可选的旋转中心。原点是左上角。默认值是图像的中心。
            transforms.ToTensor(),
        ])
        evalTransform = transforms.Compose([
            transforms.Resize(size=(128, 128)),
            transforms.ToTensor(),
        ])
        self.transform = trainTransform if mode == 'train' else evalTransform

    # 這個 FoodDataset 繼承了 pytorch 的 Dataset class
    # 而 __len__ 和 __getitem__ 是定義一個 pytorch dataset 時一定要 implement 的兩個 methods
    def __len__(self):
        return len(self.paths)

    def __getitem__(self, index):
        X = Image.open(self.paths[index])
        X = self.transform(X)
        Y = self.labels[index]
        return X, Y

    # 這個 method 並不是 pytorch dataset 必要,只是方便未來我們想要指定「取哪幾張圖片」出來當作一個 batch 來 visualize
    def getbatch(self, indices):
        images = []
        labels = []
        for index in indices:
          image, label = self.__getitem__(index)#返回的x,y值
          images.append(image)
          labels.append(label)
        return torch.stack(images), torch.tensor(labels)

# 給予 data 的路徑,回傳每一張圖片的「路徑」和「class」
def get_paths_labels(path):
    imgnames = os.listdir(path)
    imgnames.sort()
    imgpaths = []
    labels = []
    for name in imgnames:
        imgpaths.append(os.path.join(path, name))
        labels.append(int(name.split('_')[0]))
    return imgpaths, labels
train_paths, train_labels = get_paths_labels(os.path.join(args.dataset_dir, 'training'))

# 這邊在 initialize dataset 時只丟「路徑」和「class」,之後要從 dataset 取資料時
# dataset 的 __getitem__ method 才會動態的去 load 每個路徑對應的圖片
train_set = FoodDataset(train_paths, train_labels, mode='eval')

6.显著性图片

我們把一張圖片丟進 model,forward 後與 label 計算出 loss。 因此與 loss 相關的有:

image
model parameter
label
通常的情況下,我們想要改變 model parameter 來 fit image 和 label。因此 loss 在計算 backward 時我們只在乎 loss 對 model parameter 的偏微分值。但數學上 image 本身也是 continuous tensor,我們可以計算 loss 對 image 的偏微分值。這個偏微分值代表「在 model parameter 和 label 都固定下,稍微改變 image 的某個 pixel value 會對 loss 產生什麼變化」。人們習慣把這個變化的劇烈程度解讀成該 pixel 的重要性 (每個 pixel 都有自己的偏微分值)。因此把同一張圖中,loss 對每個 pixel 的偏微分值畫出來,就可以看出該圖中哪些位置是 model 在判斷時的重要依據。

實作上非常簡單,過去我們都是 forward 後算出 loss,然後進行 backward。而這個 backward,pytorch 預設是計算 loss 對 model parameter 的偏微分值,因此我們只需要用一行 code 額外告知 pytorch,image 也是要算偏微分的對象之一。

def normalize(image):#标准化,归一化
  return (image - image.min()) / (image.max() - image.min())
#特征缩放是最重要的数据转换之一.
#可以用线性函数归一化(Normalization,减去最小值,除以最大值与最小值的差值,sklearn中的MinMaxScaler)
#和标准化(Standardization,减去平均值,除以方差,sklearn中的StandardScaler)来实现.

def compute_saliency_maps(x, y, model):
  model.eval()
  x = x.cuda()

  # 最關鍵的一行 code
  # 因為我們要計算 loss 對 input image 的微分,原本 input x 只是一個 tensor,預設不需要 gradient
  # 這邊我們明確的告知 pytorch 這個 input x 需要gradient,這樣我們執行 backward 後 x.grad 才會有微分的值
  x.requires_grad_()
  
  y_pred = model(x)
  loss_func = torch.nn.CrossEntropyLoss()
  loss = loss_func(y_pred, y.cuda())
  loss.backward()

  saliencies = x.grad.abs().detach().cpu()
  # saliencies: (batches, channels, height, weight)
  # 因為接下來我們要對每張圖片畫 saliency map,每張圖片的 gradient scale 很可能有巨大落差
  # 可能第一張圖片的 gradient 在 100 ~ 1000,但第二張圖片的 gradient 在 0.001 ~ 0.0001
  # 如果我們用同樣的色階去畫每一張 saliency 的話,第一張可能就全部都很亮,第二張就全部都很暗,
  # 如此就看不到有意義的結果,我們想看的是「單一張 saliency 內部的大小關係」,
  # 所以這邊我們要對每張 saliency 各自做 normalize。手法有很多種,這邊只採用最簡單的
  saliencies = torch.stack([normalize(item) for item in saliencies])
  return saliencies
# 指定想要一起 visualize 的圖片 indices
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
saliencies = compute_saliency_maps(images, labels, model)

# 使用 matplotlib 畫出來
fig, axs = plt.subplots(2, len(img_indices), figsize=(15, 8))
for row, target in enumerate([images, saliencies]):
  for column, img in enumerate(target):
    axs[row][column].imshow(img.permute(1, 2, 0).numpy())
    # 小知識:permute 是什麼,為什麼這邊要用?
    # 在 pytorch 的世界,image tensor 各 dimension 的意義通常為 (channels, height, width)
    # 但在 matplolib 的世界,想要把一個 tensor 畫出來,形狀必須為 (height, width, channels)
    # 因此 permute 是一個 pytorch 很方便的工具來做 dimension 間的轉換
    # 這邊 img.permute(1, 2, 0),代表轉換後的 tensor,其
    # - 第 0 個 dimension 為原本 img 的第 1 個 dimension,也就是 height
    # - 第 1 個 dimension 為原本 img 的第 2 個 dimension,也就是 width
    # - 第 2 個 dimension 為原本 img 的第 0 個 dimension,也就是 channels

plt.show()
plt.close()
# 從第二張圖片的 saliency,我們可以發現 model 有認出蛋黃的位置
# 從第三、四張圖片的 saliency,雖然不知道 model 細部用食物的哪個位置判斷,但可以發現 model 找出了食物的大致輪廓

7.解释性Filter explaination

這裡我們想要知道某一個 filter 到底認出了什麼。我們會做以下兩件事情:

Filter activation: 挑幾張圖片出來,看看圖片中哪些位置會 activate 該 filter
Filter visualization: 怎樣的 image 可以最大程度的 activate 該 filter
實作上比較困難的地方是,通常我們是直接把 image 丟進 model,一路 forward 到底。如:

loss = model(image)
loss.backward()

我們要怎麼得到中間某層 CNN 的 output? 當然我們可以直接修改 model definition,讓 forward 不只 return loss,也 return activation map。但這樣的寫法麻煩了,更改了 forward 的 output 可能會讓其他部分的 code 要跟著改動。因此 pytorch 提供了方便的 solution: hook,以下我們會再介紹。

model
def normalize(image):
  return (image - image.min()) / (image.max() - image.min())

layer_activations = None
def filter_explaination(x, model, cnnid, filterid, iteration=100, lr=1):
  # x: 要用來觀察哪些位置可以 activate 被指定 filter 的圖片們
  # cnnid, filterid: 想要指定第幾層 cnn 中第幾個 filter
  model.eval()

  def hook(model, input, output):
    global layer_activations
    layer_activations = output
  
  hook_handle = model.cnn[cnnid].register_forward_hook(hook)
  # 這一行是在告訴 pytorch,當 forward 「過了」第 cnnid 層 cnn 後,要先呼叫 hook 這個我們定義的 function 後才可以繼續 forward 下一層 cnn
  # 因此上面的 hook function 中,我們就會把該層的 output,也就是 activation map 記錄下來,這樣 forward 完整個 model 後我們就不只有 loss
  # 也有某層 cnn 的 activation map
  # 注意:到這行為止,都還沒有發生任何 forward。我們只是先告訴 pytorch 等下真的要 forward 時該多做什麼事
  # 注意:hook_handle 可以先跳過不用懂,等下看到後面就有說明了

  # Filter activation: 我們先觀察 x 經過被指定 filter 的 activation map
  model(x.cuda())
  # 這行才是正式執行 forward,因為我們只在意 activation map,所以這邊不需要把 loss 存起來
  filter_activations = layer_activations[:, filterid, :, :].detach().cpu()
  
  # 根據 function argument 指定的 filterid 把特定 filter 的 activation map 取出來
  # 因為目前這個 activation map 我們只是要把他畫出來,所以可以直接 detach from graph 並存成 cpu tensor
  
  # Filter visualization: 接著我們要找出可以最大程度 activate 該 filter 的圖片
  x = x.cuda()
  # 從一張 random noise 的圖片開始找 (也可以從一張 dataset image 開始找)
  x.requires_grad_()
  # 我們要對 input image 算偏微分
  optimizer = Adam([x], lr=lr)
  # 利用偏微分和 optimizer,逐步修改 input image 來讓 filter activation 越來越大
  for iter in range(iteration):
    optimizer.zero_grad()
    model(x)
    
    objective = -layer_activations[:, filterid, :, :].sum()
    # 與上一個作業不同的是,我們並不想知道 image 的微量變化會怎樣影響 final loss
    # 我們想知道的是,image 的微量變化會怎樣影響 activation 的程度
    # 因此 objective 是 filter activation 的加總,然後加負號代表我們想要做 maximization
    
    objective.backward()
    # 計算 filter activation 對 input image 的偏微分
    optimizer.step()
    # 修改 input image 來最大化 filter activation
  filter_visualization = x.detach().cpu().squeeze()[0]#指定维若维度为1,则删去
  # 完成圖片修改,只剩下要畫出來,因此可以直接 detach 並轉成 cpu tensor

  hook_handle.remove()
  # 很重要:一旦對 model register hook,該 hook 就一直存在。如果之後繼續 register 更多 hook
  # 那 model 一次 forward 要做的事情就越來越多,甚至其行為模式會超出你預期 (因為你忘記哪邊有用不到的 hook 了)
  # 因此事情做完了之後,就把這個 hook 拿掉,下次想要再做事時再 register 就好了。

  return filter_activations, filter_visualization
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
filter_activations, filter_visualization = filter_explaination(images, model, cnnid=15, filterid=0, iteration=100, lr=0.1)

# 畫出 filter visualization
plt.imshow(normalize(filter_visualization.permute(1, 2, 0)))
plt.show()
plt.close()
# 根據圖片中的線條,可以猜測第 15 層 cnn 其第 0 個 filter 可能在認一些線條、甚至是 object boundary
# 因此給 filter 看一堆對比強烈的線條,他會覺得有好多 boundary 可以 activate

# 畫出 filter activations
fig, axs = plt.subplots(2, len(img_indices), figsize=(15, 8))
for i, img in enumerate(images):
  axs[0][i].imshow(img.permute(1, 2, 0))
for i, img in enumerate(filter_activations):
  axs[1][i].imshow(normalize(img))
plt.show()
plt.close()
# 從下面四張圖可以看到,activate 的區域對應到一些物品的邊界,尤其是顏色對比較深的邊界

8.Lime

Lime 的部分因為有現成的套件可以使用,因此下方直接 demo 如何使用該套件。其實非常的簡單,只需要 implement 兩個 function 即可。

def predict(input):
    # input: numpy array, (batches, height, width, channels)                                                                                                                                                     
    
    model.eval()                                                                                                                                                             
    input = torch.FloatTensor(input).permute(0, 3, 1, 2)                                                                                                            
    # 需要先將 input 轉成 pytorch tensor,且符合 pytorch 習慣的 dimension 定義
    # 也就是 (batches, channels, height, width)

    output = model(input.cuda())                                                                                                                                             
    return output.detach().cpu().numpy()                                                                                                                              
                                                                                                                                                                             
def segmentation(input):
    # 利用 skimage 提供的 segmentation 將圖片分成 100 塊                                                                                                                                      
    return slic(input, n_segments=100, compactness=1, sigma=1)                                                                                                              
                                                                                                                                                                             
img_indices = [83, 4218, 4707, 8598]
images, labels = train_set.getbatch(img_indices)
fig, axs = plt.subplots(1, 4, figsize=(15, 8))                                                                                                                                                                 
np.random.seed(16)                                                                                                                                                       
# 讓實驗 reproducible
for idx, (image, label) in enumerate(zip(images.permute(0, 2, 3, 1).numpy(), labels)):                                                                                                                                             
    x = image.astype(np.double)
    # lime 這個套件要吃 numpy array

    explainer = lime_image.LimeImageExplainer()                                                                                                                              
    explaination = explainer.explain_instance(image=x, classifier_fn=predict, segmentation_fn=segmentation)
    # 基本上只要提供給 lime explainer 兩個關鍵的 function,事情就結束了
    # classifier_fn 定義圖片如何經過 model 得到 prediction
    # segmentation_fn 定義如何把圖片做 segmentation
    # doc: https://lime-ml.readthedocs.io/en/latest/lime.html?highlight=explain_instance#lime.lime_image.LimeImageExplainer.explain_instance

    lime_img, mask = explaination.get_image_and_mask(                                                                                                                         
                                label=label.item(),                                                                                                                           
                                positive_only=False,                                                                                                                         
                                hide_rest=False,                                                                                                                             
                                num_features=11,                                                                                                                              
                                min_weight=0.05                                                                                                                              
                            )
    # 把 explainer 解釋的結果轉成圖片
    # doc: https://lime-ml.readthedocs.io/en/latest/lime.html?highlight=get_image_and_mask#lime.lime_image.ImageExplanation.get_image_and_mask
    
    axs[idx].imshow(lime_img)

plt.show()
plt.close()
# 從以下前三章圖可以看到,model 有認出食物的位置,並以該位置為主要的判斷依據
# 唯一例外是第四張圖,看起來 model 似乎比較喜歡直接去認「碗」的形狀,來判斷該圖中屬於 soup 這個 class
# 至於碗中的內容物被標成紅色,代表「單看碗中」的東西反而有礙辨認。
# 當 model 只看碗中黃色的一坨圓形,而沒看到「碗」時,可能就會覺得是其他黃色圓形的食物。

总结

目前还是初学阶段,望各位大佬发现错误时能够对我批评指正,多谢!
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
标签高效的语义分割是指使用扩散模型进行语义分割的方法。语义分割是计算机视觉中的一项重要任务,目标是将图像中的每个像素标记为属于特定类别。传统的语义分割方法通常需要大量的标注数据进行训练,这对于很多场景来说是不可行的,因为标注数据的获取十分耗时和费力。因此,提出了一种使用扩散模型的标签高效语义分割方法。 扩散模型基于自监督学习的思想,通过利用图像自身的结构信息进行训练。具体而言,该方法首先将图像划分为一组超像素或紧密区域,然后根据这些区域的颜色、纹理或形状等特征来推断像素的标签。通过这种方式,可以大大减少对标注数据的依赖。 该方法的关键步骤是利用扩散模型对像素进行标签推断。扩散模型是一种基于概率传播的图像分割方法,它通过计算每个像素与其周围像素之间的相似度来推断像素的标签。这样一来,即使只有部分像素有标注,也可以利用它们与周围像素的相似性来推断未标注像素的标签,从而实现标签高效的语义分割。 与传统的语义分割方法相比,标签高效的语义分割方法在保持较高准确率的同时,显著减少了对标注数据的需求。这对于那些无法获得大量标注数据的场景来说,具有重要的实际意义。因此,标签高效的语义分割方法在计算机视觉和图像处理领域具有广泛的应用前景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值