【Pytorch教程】迅速入門Pytorch深度學習框架

UnderTurrets發表於2024-08-26

@

目錄
  • 前言
  • 1.tensor基礎操作
    • 1.1 tensor的dtype型別
    • 1.2 建立tensor(建議寫出引數名字)
      • 1.2.1 空tensor(無用資料填充)
        • API
        • 示例
      • 1.2.2 全一tensor
      • 1.2.3 全零tensor
      • 1.2.4 隨機值[0,1)的tensor
      • 1.2.5 隨機值為整數且規定上下限的tensor
        • API
        • 示例
      • 1.2.6 隨機值均值0方差1的tensor
      • 1.2.7 從列表或numpy陣列建立tensor
    • 1.3 tensor常用成員函式和成員變數
      • 1.3.1 轉為numpy陣列
      • 1.3.2 獲得單元素tensor的值item
      • 1.3.3 獲取維度個數
      • 1.3.4 獲取資料型別
      • 1.3.5 獲取形狀
      • 1.3.6 淺複製與深複製
        • detach函式淺複製
        • 深複製
      • 1.3.7 形狀變換
        • 轉置
        • cat堆疊
        • stack堆疊
        • view改變形狀
        • reshape改變形狀
      • 1.3.8 數學運算
      • 1.3.9 使用指定裝置計算tensor
  • 2.線性迴歸模型
    • 2.1 自動求導機制
    • 2.2 nn.Module的繼承(from torch import nn)
      • 2.2.1 概述
      • 2.2.2 例項
    • 2.3 最佳化器類(from torch import optim)
      • 2.3.1 概述
      • 2.3.2 流程
      • 2.3.3 動態學習率(import torch.optim.lr_scheduler)
    • 2.4 代價函式(from torch import nn)
    • 2.5 評估模型
    • 2.6 線性迴歸模型的建立
      • 2.6.1 流程
      • 2.6.2 示例
  • 3.資料集和資料載入器 (from torch.utils.data import Dataset,DataLoader)
    • 3.1 Dataset類的繼承(from torch.utils.data import Dataset)
      • 3.1.1 概述
      • 3.1.2 例項
    • 3.2 DataLoader類
      • 3.2.1 API
      • 3.2.2 示例
  • 4.影像處理:手寫數字識別
    • 4.1 torchvision模組
      • 4.1.1 transforms.ToTensor類(仿函式)
      • 4.1.2 transforms.Normalize類(仿函式)
      • 4.1.3 transforms.Compose類(仿函式)
      • 4.1.4 示例
    • 4.2 網路構建
      • 4.2.1 啟用函式大全
      • 4.2.2 演示程式碼(在gpu上)
  • 5.製作圖片資料集(以flower102為例)
    • 5.1 建立資料集骨架
    • 5.2 建立從名稱到數字標籤的對映
    • 5.3 建立csv資料
    • 5.4 完善成員函式和transform過程
    • 5.5 DataLoader檢驗
  • 6.遷移學習
    • 6.1 現有模型的儲存和載入
      • 6.1.1 儲存(torch.save函式)
        • 把資料載入進網路
        • 把資料載入進最佳化器
      • 6.1.3 示例
    • 6.2 使用預訓練的模型(以resnet50為例)
      • 6.2.1 確定初始化引數
    • 6.3 開始訓練


前言

本文只是對於pytorch深度學習框架的使用方法的介紹,如果涉及演算法中複雜的數學原理,本文將不予闡述,敬請讀者自行閱讀相關論文或者文獻。


1.tensor基礎操作

1.1 tensor的dtype型別

程式碼 含義
float32 32位float
float floa
float64 64位float
double double
float16 16位float
bfloat16 比float範圍大但精度低
int8 8位int
int16 16位int
short short
int32 32位int
int int
int64 64位int
long long
complex32 32位complex
complex64 64位complex
cfloat complex float
complex128 128位complex float
cdouble complex double

1.2 建立tensor(建議寫出引數名字)

建立tensor時,有很多引數可以選擇,為節省篇幅,本文在列舉API時只列舉一次,不列舉過載的API。

1.2.1 空tensor(無用資料填充)

API

@overload
def empty(size: Sequence[Union[_int, SymInt]], *, memory_format: Optional[memory_format]=None, out: Optional[Tensor]=None, dtype: Optional[_dtype]=None, layout: Optional[_layout]=None, device: Optional[Union[_device, str, None]]=None, pin_memory: Optional[_bool]=False, requires_grad: Optional[_bool]=False) -> Tensor: ...

size:[行數,列數]

dtype(deepth type):資料型別

device:選擇運算裝置

requires_grad:是否進行自動求導,預設為False

示例

       gpu=torch.device("cuda")
       empty_tensor=torch.empty(size=[3,4],device=gpu,requires_grad=True)
       print(empty_tensor)

輸出

tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]], device='cuda:0', requires_grad=True)

1.2.2 全一tensor

@overload
def ones(size: _size, *, names: Optional[Sequence[Union[str, ellipsis, None]]], dtype: Optional[_dtype]=None, layout: Optional[_layout]=None, device: Optional[Union[_device, str, None]]=None, pin_memory: Optional[_bool]=False, requires_grad: Optional[_bool]=False) -> Tensor: ...

size:[行數,列數]

dtype(deepth type):資料型別

device:選擇運算裝置

requires_grad:是否進行自動求導,預設為False

1.2.3 全零tensor

@overload
def zeros(size: _size, *, names: Optional[Sequence[Union[str, ellipsis, None]]], dtype: Optional[_dtype]=None, layout: Optional[_layout]=None, device: Optional[Union[_device, str, None]]=None, pin_memory: Optional[_bool]=False, requires_grad: Optional[_bool]=False) -> Tensor: ...

1.2.4 隨機值[0,1)的tensor

@overload
def rand(size: _size, *, generator: Optional[Generator], names: Optional[Sequence[Union[str, ellipsis, None]]], dtype: Optional[_dtype]=None, layout: Optional[_layout]=None, device: Optional[Union[_device, str, None]]=None, pin_memory: Optional[_bool]=False, requires_grad: Optional[_bool]=False) -> Tensor: ...

1.2.5 隨機值為整數且規定上下限的tensor

API

@overload
def randint(low: _int, high: _int, size: _size, *, generator: Optional[Generator]=None, dtype: Optional[_dtype]=None, device: Device=None, requires_grad: _bool=False) -> Tensor: ...

示例

   int_tensor=torch.randint(low=0,high=20,size=[5,6],device=gpu)
   print(int_tensor)

輸出

tensor([[18,  0, 14,  7, 18, 14],
        [17,  0,  2,  0,  0,  3],
        [16, 17,  5, 15,  1, 14],
        [ 7, 12,  8,  6,  4, 11],
        [12,  4,  7,  5,  3,  3]], device='cuda:0')

1.2.6 隨機值均值0方差1的tensor

@overload
def randn(size: _size, *, generator: Optional[Generator], names: Optional[Sequence[Union[str, ellipsis, None]]], dtype: Optional[_dtype]=None, layout: Optional[_layout]=None, device: Optional[Union[_device, str, None]]=None, pin_memory: Optional[_bool]=False, requires_grad: Optional[_bool]=False) -> Tensor: ...

1.2.7 從列表或numpy陣列建立tensor

def tensor(data: Any, dtype: Optional[_dtype]=None, device: Device=None, requires_grad: _bool=False) -> Tensor: ...
  • 如果使用torch.from_numpy(),返回的tensor與ndarray共享記憶體。

1.3 tensor常用成員函式和成員變數

1.3.1 轉為numpy陣列

def numpy(self,*args, **kwargs): # real signature unknown; NOTE: unreliably restored from __doc__ 
	pass
  • 只有在CPU上運算的tensor才可以轉為numpy陣列
  • tensor.requires_grad屬性為True的tensor不能轉為numpy陣列

1.3.2 獲得單元素tensor的值item

def item(self): # real signature unknown; restored from __doc__
    ...
  • 如果tensor只有一個元素,就返回它的值
  • 如果tensor有多個元素,丟擲ValueError

1.3.3 獲取維度個數

def dim(self): #real signature unknown; restored from __doc__
    return 0
  • 返回一個int表示維度個數

1.3.4 獲取資料型別

dtype = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default

1.3.5 獲取形狀

def size(self,dim=None): # real signature unknown; restored from __doc__
    pass
  • 使用.shape效果相同

1.3.6 淺複製與深複製

detach函式淺複製

假設有模型A和模型B,我們需要將A的輸出作為B的輸入,但訓練時我們只訓練模型B. 那麼可以這樣做:

input_B = output_A.detach()

它可以使兩個計算圖的梯度傳遞斷開,從而實現我們所需的功能。

返回一個新的tensor,新的tensor和原來的tensor共享資料記憶體,但不涉及梯度計算,即requires_grad=False。修改其中一個tensor的值,另一個也會改變,因為是共享同一塊記憶體。

   sequence_tensor=torch.tensor(np.array([[[1,2,3],
                                            [4,5,6]],

                                           [[9,8,7],
                                            [6,5,4]]]),
                                 dtype=torch.float,device=gpu,)
   sequence_tensor_shallowCp=sequence_tensor.detach()
   sequence_tensor_shallowCp+=1
   print(sequence_tensor)
   print(sequence_tensor_shallowCp.requires_grad)

輸出

tensor([[[ 2.,  3.,  4.],
         [ 5.,  6.,  7.]],

        [[10.,  9.,  8.],
         [ 7.,  6.,  5.]]], device='cuda:0')
False

深複製

  • 法一:.clone().detach()
  • 法二:.new_tensor()

1.3.7 形狀變換

轉置

向量或矩陣轉置

def t(self): # real signature unknown; restored from __doc__
    """
    t() -> Tensor
  
    See :func:`torch.t`
    """
    return _te.Tensor(*(), **{})
  • 返回值與原tensor共享記憶體!

指定兩個維度進行轉置:

def permute(self, dims: _size) -> Tensor: 
    r"""
    permute(*dims) -> Tensor
  
    See :func:`torch.permute`
    """
    ...
  • 返回值與原tensor共享記憶體!
  • 對矩陣來說,.t()等價於.permute(0, 1)

多維度同時轉置

def permute(self, *dims): # real signature unknown; restored from __doc__
    """
    permute(*dims) -> Tensor
  
    See :func:`torch.permute`
    """
    return _te.Tensor(*(), **{})
  • 把要轉置的維度放到對應位置上,比如對於三維tensor,x、y、z分別對應0、1、2,如果想要轉置x軸和z軸,則輸入2、1、0即可
  • 返回值與原tensor共享記憶體!

cat堆疊

cat可以把兩個或多個tensor沿著指定的維度進行連線,連線後的tensor維度個數不變,指定維度上的大小改變,非指定維度上的大小不變。譬如,兩個shape=(3,)行向量按dim=0連線,變成1個shape=(6,)的行向量;2個3階方陣按dim=0連線,就變成1個(6, 3)的矩陣。

cat在使用時對輸入的這些tensor有要求:除了指定維度,其他維度的大小必須相同。譬如,1個shape=(1, 6)的矩陣可以和1個shape=(2, 6)的矩陣在dim=0連線。

例子可以參考下面的定義和註釋。

def cat(tensors: Union[Tuple[Tensor, ...], List[Tensor]], dim: _int = 0, *, out: Optional[Tensor] = None) -> Tensor: 
    r"""
    cat(tensors, dim=0, *, out=None) -> Tensor
  
    Concatenates the given sequence of :attr:`seq` tensors in the given dimension.
    All tensors must either have the same shape (except in the concatenating
    dimension) or be a 1-D empty tensor with size ``(0,)``.
  
    :func:`torch.cat` can be seen as an inverse operation for :func:`torch.split`
    and :func:`torch.chunk`.
  
    :func:`torch.cat` can be best understood via examples.
  
    .. seealso::
  
        :func:`torch.stack` concatenates the given sequence along a new dimension.
  
    Args:
        tensors (sequence of Tensors): any python sequence of tensors of the same type.
            Non-empty tensors provided must have the same shape, except in the
            cat dimension.
        dim (int, optional): the dimension over which the tensors are concatenated
  
    Keyword args:
        out (Tensor, optional): the output tensor.
  
    Example::
  
        >>> x = torch.randn(2, 3)
        >>> x
        tensor([[ 0.6580, -1.0969, -0.4614],
                [-0.1034, -0.5790,  0.1497]])
        >>> torch.cat((x, x, x), 0)
        tensor([[ 0.6580, -1.0969, -0.4614],
                [-0.1034, -0.5790,  0.1497],
                [ 0.6580, -1.0969, -0.4614],
                [-0.1034, -0.5790,  0.1497],
                [ 0.6580, -1.0969, -0.4614],
                [-0.1034, -0.5790,  0.1497]])
        >>> torch.cat((x, x, x), 1)
        tensor([[ 0.6580, -1.0969, -0.4614,  0.6580, -1.0969, -0.4614,  0.6580,
                 -1.0969, -0.4614],
                [-0.1034, -0.5790,  0.1497, -0.1034, -0.5790,  0.1497, -0.1034,
                 -0.5790,  0.1497]])
    """
    ...
  • 返回值與原tensor不共享記憶體

stack堆疊

stackcat有很大的區別,stack把兩個或多個tensor在dim建立一個全新的維度進行連線,非指定維度個數不變,建立的維度的大小取決於這次連線使用了多少個tensor。譬如,3個shape=(3,)行向量按dim=0連線,會變成一個shape=(3, 3)的矩陣;兩個3階方陣按dim=-1連線,就變成一個(3, 3, 2)的tensor。

def stack(tensors: Union[Tuple[Tensor, ...], List[Tensor]], dim: _int = 0, *, out: Optional[Tensor] = None) -> Tensor: 
    r"""
    stack(tensors, dim=0, *, out=None) -> Tensor
  
    Concatenates a sequence of tensors along a new dimension.
  
    All tensors need to be of the same size.
  
    .. seealso::
  
        :func:`torch.cat` concatenates the given sequence along an existing dimension.
  
    Arguments:
        tensors (sequence of Tensors): sequence of tensors to concatenate
        dim (int, optional): dimension to insert. Has to be between 0 and the number
            of dimensions of concatenated tensors (inclusive). Default: 0
  
    Keyword args:
        out (Tensor, optional): the output tensor.
  
    Example::
  
        >>> x = torch.randn(2, 3)
        >>> x
        tensor([[ 0.3367,  0.1288,  0.2345],
                [ 0.2303, -1.1229, -0.1863]])
        >>> x = torch.stack((x, x)) # same as torch.stack((x, x), dim=0)
        >>> x
        tensor([[[ 0.3367,  0.1288,  0.2345],
                 [ 0.2303, -1.1229, -0.1863]],
  
                [[ 0.3367,  0.1288,  0.2345],
                 [ 0.2303, -1.1229, -0.1863]]])
        >>> x.size()
        torch.Size([2, 2, 3])
        >>> x = torch.stack((x, x), dim=1)
        tensor([[[ 0.3367,  0.1288,  0.2345],
                 [ 0.3367,  0.1288,  0.2345]],
  
                [[ 0.2303, -1.1229, -0.1863],
                 [ 0.2303, -1.1229, -0.1863]]])
        >>> x = torch.stack((x, x), dim=2)
        tensor([[[ 0.3367,  0.3367],
                 [ 0.1288,  0.1288],
                 [ 0.2345,  0.2345]],
  
                [[ 0.2303,  0.2303],
                 [-1.1229, -1.1229],
                 [-0.1863, -0.1863]]])
        >>> x = torch.stack((x, x), dim=-1)
        tensor([[[ 0.3367,  0.3367],
                 [ 0.1288,  0.1288],
                 [ 0.2345,  0.2345]],
  
                [[ 0.2303,  0.2303],
                 [-1.1229, -1.1229],
                 [-0.1863, -0.1863]]])
    """
    ...
  • 返回值與原tensor不共享記憶體

view改變形狀

view先把資料變成一維陣列,然後再轉換成指定形狀。變換前後的元素個數並不會改變,所以變換前後的shape的乘積必須相等。詳細例子如下:

def view(self, *shape): # real signature unknown; restored from __doc__
    """
    Example::
  
        >>> x = torch.randn(4, 4)
        >>> x.size()
        torch.Size([4, 4])
        >>> y = x.view(16)
        >>> y.size()
        torch.Size([16])
        >>> z = x.view(-1, 8)  # the size -1 is inferred from other dimensions
        >>> z.size()
        torch.Size([2, 8])
  
        >>> a = torch.randn(1, 2, 3, 4)
        >>> a.size()
        torch.Size([1, 2, 3, 4])
        >>> b = a.transpose(1, 2)  # Swaps 2nd and 3rd dimension
        >>> b.size()
        torch.Size([1, 3, 2, 4])
        >>> c = a.view(1, 3, 2, 4)  # Does not change tensor layout in memory
        >>> c.size()
        torch.Size([1, 3, 2, 4])
        >>> torch.equal(b, c)
        False 
    """
    return _te.Tensor(*(), **{})
  • 返回值與原tensor共享記憶體

reshape改變形狀

reshapeview的區別如下:

  • view只能改變連續(.contiguous())的tensor,如果已經對tensor進行了permute、transpose等操作,tensor在記憶體中會變得不連續,此時呼叫view會報錯。且view方法與原來的tensor共享記憶體。
  • reshape再呼叫時自動檢測原tensor是否連續,如果是,則等價於view;如果不是,先呼叫.contiguous(),再呼叫view,此時返回值與原來tensor不共享記憶體
    def reshape(self, shape: Sequence[Union[_int, SymInt]]) -> Tensor: 
        ...

1.3.8 數學運算

在這裡插入圖片描述

    def mean(self, dim=None, keepdim=False, *args, **kwargs): # real signature unknown; NOTE: unreliably restored from __doc__ 
        ...
    def sum(self, dim=None, keepdim=False, dtype=None): # real signature unknown; restored from __doc__
        ...
    def median(self, dim=None, keepdim=False): # real signature unknown; restored from __doc__
        ...
    def mode(self, dim=None, keepdim=False): # real signature unknown; restored from __doc__
        ...
    def dist(self, other, p=2): # real signature unknown; restored from __doc__
        ...
    def std(self, dim, unbiased=True, keepdim=False): # real signature unknown; restored from __doc__
        ...
    def var(self, dim, unbiased=True, keepdim=False): # real signature unknown; restored from __doc__
        ...
    def cumsum(self, dim, dtype=None): # real signature unknown; restored from __doc__
        ...
    def cumprod(self, dim, dtype=None): # real signature unknown; restored from __doc__
        ...

在這裡插入圖片描述

1.3.9 使用指定裝置計算tensor

to可以把tensor轉移到指定裝置上。

    def to(self, *args, **kwargs): # real signature unknown; restored from __doc__
        """
        Example::
      
            >>> tensor = torch.randn(2, 2)  # Initially dtype=float32, device=cpu
            >>> tensor.to(torch.float64)
            tensor([[-0.5044,  0.0005],
                    [ 0.3310, -0.0584]], dtype=torch.float64)
      
            >>> cuda0 = torch.device('cuda:0')
            >>> tensor.to(cuda0)
            tensor([[-0.5044,  0.0005],
                    [ 0.3310, -0.0584]], device='cuda:0')
      
            >>> tensor.to(cuda0, dtype=torch.float64)
            tensor([[-0.5044,  0.0005],
                    [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0')
      
            >>> other = torch.randn((), dtype=torch.float64, device=cuda0)
            >>> tensor.to(other, non_blocking=True)
            tensor([[-0.5044,  0.0005],
                    [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0')
        """
        return _te.Tensor(*(), **{})

2.線性迴歸模型

2.1 自動求導機制

  • 在pytorch中,如果設定一個 tensor 的屬性 requires_grad 為 True,那麼它將會追蹤對於該張量的所有操作。當完成計算後可以透過呼叫 tensor.backward 函式,來自動計算所有的梯度。這個張量的所有梯度將會自動累加到 grad 屬性。
  • 由於是累加,因此在進行線性迴歸模型的計算時,每輪都要用 tensor.zero_ 函式清空一次 grad 屬性

示例

   sequence_tensor=torch.tensor(np.array([[[1,2,3],
                                            [4,5,6]],

                                           [[9,8,7],
                                            [6,5,4]]]),
                                 dtype=torch.float,device=gpu,requires_grad=True)
   multi_tensor=sequence_tensor*3+1
   multi_tensor_mean=multi_tensor.mean()
   multi_tensor_mean.backward()
   print(sequence_tensor.grad)

輸出

tensor([[[0.2500, 0.2500, 0.2500],
         [0.2500, 0.2500, 0.2500]],

        [[0.2500, 0.2500, 0.2500],
         [0.2500, 0.2500, 0.2500]]], device='cuda:0')

2.2 nn.Module的繼承(from torch import nn)

2.2.1 概述

nn.Module是torch.nn提供的一個類,是pytorch中定義網路的必要的一個父類,在這個類中定義了很多有用的方法,使我們非常方便地計算。在我們進行網路的定義時,有兩個地方需要特別注意:

  • 在定義成員變數時必須呼叫super函式,繼承父類__init__引數,即,在__init__中必須呼叫super(<the name of the variable>,self)函式
  • 通常還會在__init__中定義網路的結構
  • 必須定義forward函式,表示網路中前向傳播的過程

2.2.2 例項

    class lr(nn.Module):
        def __init__(self):
            super(lr,self).__init__()
            self.linear=nn.Linear(1,1)

        def forward(self,x):
            y_predict=self.linear(x)
            return y_predict

其中,nn.Linear函式的引數為:輸入的特徵量,輸出的特徵量。

2.3 最佳化器類(from torch import optim)

2.3.1 概述

最佳化器(optimizer),用來操縱引數的梯度以更新引數,常見的方法有隨機梯度下降(stochastic gradient descent)(SGD)等。

  • torch.optim.SGD(引數,float 學習率)
  • torch.optim.Adam(引數,float 學習率)

2.3.2 流程

  • 呼叫Module.parameters函式獲取模型引數,並定義學習率,進行例項化
  • 用例項化物件調同 .zero_grad 函式,將引數重置為0
  • 呼叫tensor.backward函式反向傳播,獲得梯度
  • 用例項化物件呼叫 .step 函式更新引數

2.3.3 動態學習率(import torch.optim.lr_scheduler)

lr_scheduler允許模型在訓練的過程中動態更新學習率,且提供了許多種策略可供選擇,以下列舉一些常用的:

指數衰減:在訓練的過程中,學習率以設定的gamma引數進行指數的衰減。

class ExponentialLR(LRScheduler):
    """Decays the learning rate of each parameter group by gamma every epoch.
    When last_epoch=-1, sets initial lr as lr.

    Args:
        optimizer (Optimizer): Wrapped optimizer.
        gamma (float): Multiplicative factor of learning rate decay.
        last_epoch (int): The index of last epoch. Default: -1.
        verbose (bool): If ``True``, prints a message to stdout for
            each update. Default: ``False``.
    """

    def __init__(self, optimizer, gamma, last_epoch=-1, verbose=False):
        self.gamma = gamma
        super().__init__(optimizer, last_epoch, verbose)

固定步長衰減:在固定的訓練週期後,以指定的頻率進行衰減。

class StepLR(LRScheduler):
    """Decays the learning rate of each parameter group by gamma every
    step_size epochs. Notice that such decay can happen simultaneously with
    other changes to the learning rate from outside this scheduler. When
    last_epoch=-1, sets initial lr as lr.

    Args:
        optimizer (Optimizer): Wrapped optimizer.
        step_size (int): Period of learning rate decay.
        gamma (float): Multiplicative factor of learning rate decay.
            Default: 0.1.
        last_epoch (int): The index of last epoch. Default: -1.
        verbose (bool): If ``True``, prints a message to stdout for
            each update. Default: ``False``.

    Example:
        >>> # xdoctest: +SKIP
        >>> # Assuming optimizer uses lr = 0.05 for all groups
        >>> # lr = 0.05     if epoch < 30
        >>> # lr = 0.005    if 30 <= epoch < 60
        >>> # lr = 0.0005   if 60 <= epoch < 90
        >>> # ...
        >>> scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
        >>> for epoch in range(100):
        >>>     train(...)
        >>>     validate(...)
        >>>     scheduler.step()
    """

    def __init__(self, optimizer, step_size, gamma=0.1, last_epoch=-1, verbose=False):
        self.step_size = step_size
        self.gamma = gamma
        super().__init__(optimizer, last_epoch, verbose)
  • 用法:建立scheduler的時候繫結optimizer物件,然後在呼叫optimizer.step()後面跟著scheduler.step()即可。

2.4 代價函式(from torch import nn)

在torch.nn中已經定義好了很多代價函式,只需要呼叫它們並且傳入真實值、預測值,就可以返回結果,例如:

  • 均方誤差:nn.MSELoss()
  • 交叉熵誤差:nn.CrossEntropyLoss()

當然,也可以自己定義loss的計算過程。

2.5 評估模型

  • Module.eval()表示設定模型為評估模式,即預測模式
  • Module.train(mdoe=True)表示設定模型為訓練模式

2.6 線性迴歸模型的建立

2.6.1 流程

  1. 定義網路,注意:實現super函式和forward函式
  2. 準備資料
  3. 例項化網路、代價函式、最佳化器
  4. 進行迴圈,呼叫Module.forward函式前向傳播,呼叫代價函式進行計算,呼叫最佳化器類進行引數更新
  5. 使用pyplot進行模型評估

2.6.2 示例

if __name__=="__main__":

    import torch
    import numpy as np
    from torch import nn
    from torch import optim
    from matplotlib import pyplot
    gpu=torch.device("cuda")
    cpu="cpu"

    #定義網路
    class lr(nn.Module):
        def __init__(self):
            #繼承成員變數
            super(lr,self).__init__()
            self.linear=nn.Linear(1,1)

        #定義前向傳播函式
        def forward(self,x):
            y_predict=self.linear(x)
            return y_predict


    #準備資料
    x_train=torch.rand([200,1],device=gpu)
    y_train=torch.matmul(x_train,torch.tensor([[3]],dtype=torch.float32,requires_grad=True,device=gpu))+8

    #例項化
    model_lr=lr().to(gpu)
    optimizer=optim.SGD(model_lr.parameters(),0.02)
    cost_fn=nn.MSELoss()

    #開始計算
    for i in range(1000):
         y_predict=model_lr.forward(x_train)
         cost=cost_fn(y_predict,y_train)
         optimizer.zero_grad()

         cost.backward(retain_graph=True)

         optimizer.step()

         if i%20==0:
             print(cost.item())
             print(list(model_lr.parameters()))

    #進行預測與評估
    model_lr.eval()
    y_predict_numpy=model_lr.forward(x_train).to(cpu).detach().numpy()
    x_train_numpy=x_train.to(cpu).detach().numpy()
    y_train_numpy=y_train.to(cpu).detach().numpy()
    pyplot.scatter(x_train_numpy,y_predict_numpy,c="r")
    pyplot.plot(x_train_numpy,y_train_numpy)
    pyplot.show()

輸出

4.7310328227467835e-05
[Parameter containing:
tensor([[3.0237]], device='cuda:0', requires_grad=True), Parameter containing:
tensor([7.9876], device='cuda:0', requires_grad=True)]

繪製圖
在這裡插入圖片描述

3.資料集和資料載入器 (from torch.utils.data import Dataset,DataLoader)

3.1 Dataset類的繼承(from torch.utils.data import Dataset)

3.1.1 概述

在pytorch中提供了資料集的父類torch.utils.data.Dataset,繼承這個父類,我們可以非常快速地實現對資料的載入,與繼承nn.Module類一樣,我們同樣必須定義一些必要的成員函式

  • __getitem__(self,index),用來進行索引,可以用 [ ]
  • __len__(self),用來獲取元素個數

3.1.2 例項

    SMSData_path="D:\Desktop\PycharmProjects\exercise\SMSSpamCollection"
    #資料來源:http://archive.ics.uci.edu/ml/machine-learning-databases/00228/
    class SMSData(Dataset):
        def __init__(self):
            self.data=open(SMSData_path,"r",encoding="utf-8").readlines()

        def __getitem__(self, index):
            current_line=self.data[index].strip()
            label=current_line[:4].strip()
            content=current_line[4:].strip()
            return [label,content]

        def __len__(self):
            return len(self.data)

    SMSex=SMSData()
    print(SMSex.__getitem__(5))
    print(SMSex.__len__())

輸出

['spam', "FreeMsg Hey there darling it's been 3 week's now and no word back! I'd like some fun you up for it still? Tb ok! XxX std chgs to send, £1.50 to rcv"]
5574

3.2 DataLoader類

3.2.1 API

class DataLoader(Generic[T_co]):
	def __init__(self, dataset: Dataset[T_co], batch_size: Optional[int] = 1,
                 shuffle: Optional[bool] = None, sampler: Union[Sampler, Iterable, None] = None,
                 batch_sampler: Union[Sampler[Sequence], Iterable[Sequence], None] = None,
                 num_workers: int = 0, collate_fn: Optional[_collate_fn_t] = None,
                 pin_memory: bool = False, drop_last: bool = False,
                 timeout: float = 0, worker_init_fn: Optional[_worker_init_fn_t] = None,
                 multiprocessing_context=None, generator=None,
                 *, prefetch_factor: int = 2,
                 persistent_workers: bool = False,
                 pin_memory_device: str = ""):
	#只列出參數列,以下詳細內容不再列出

dataset:以Dataset類為父類的自定義類的例項化物件

batch_size:批處理的個數

shuffle:bool型別,若為True則表示提前打亂資料

num_workers:載入資料時用到的執行緒數

drop_last :bool型別,若為True:這個是對最後的未完成的batch來說的,比如你的batch_size設定為64,而一個訓練集只有100個樣本,那麼訓練的時候後面的36個就被扔掉了。如果為False(預設),那麼會繼續正常執行,只是最後的batch_size會小一點。

timeout:如果是正數,表明等待從worker程序中收集一個batch等待的時間,若超出設定的時間還沒有收集到,那就不收集這個內容了。這個numeric應總是大於等於0,預設為0

3.2.2 示例

import torch
from torch.utils.data import Dataset,DataLoader
import chardet
gpu = torch.device("cuda")
cpu="cpu"
try:
    SMSData_path="SMSSpamCollection"
    #獲取檔案編碼方式
    with open(SMSData_path,"rb") as file:
        file_format=chardet.detect(file.read())["encoding"]
      
    class SMSData(Dataset):
        def __init__(self):
            self.data=open(SMSData_path,"r",encoding=file_format).readlines()
  
        def __getitem__(self, index):
            current_line=self.data[index].strip()
            origin=current_line[:4].strip()
            content=current_line[4:].strip()
            return [origin,content]
  
        def __len__(self):
            return len(self.data)
  
    SMSex=SMSData()
    SMSData_loader=DataLoader(dataset=SMSex,batch_size=2,shuffle=False,num_workers=2)
    if __name__=='__main__':#如果設定多執行緒,一定要加這句話,否則會報錯
        for i in SMSData_loader:
            print("遍歷一:",i)
            break
        for i in enumerate(SMSData_loader):
            print("遍歷二:",i)
            break
        for batch_index,(label,content) in enumerate(SMSData_loader):
            print("遍歷三:",batch_index,label,content)
            break
          
          
except BaseException as error:
    print(error)

輸出

遍歷一: [('ham', 'ham'), ('Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...', 'Ok lar... Joking wif u oni...')]
遍歷二: (0, [('ham', 'ham'), ('Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...', 'Ok lar... Joking wif u oni...')])
遍歷三: 0 ('ham', 'ham') ('Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...', 'Ok lar... Joking wif u oni...')
  • 可見,DataLoader是一個可遍歷物件,每輪中返回的資料以列表的方式儲存,且列表中每個元素都是一個元組,列表的長度等於Dataset.__getitem__返回的列表長度,元組的長度等於batch_size引數的大小

4.影像處理:手寫數字識別

4.1 torchvision模組

4.1.1 transforms.ToTensor類(仿函式)

class ToTensor:
    def __init__(self) -> None:
        _log_api_usage_once(self)
  • 將原始的PILImage資料型別或者numpy.array資料型別化為tensor資料型別。
  • 如果 PIL Image 屬於 (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1)中的一種影像型別,或者 numpy.ndarray 格式資料型別是 np.uint8 ,則將 [0, 255] 的資料轉為 [0.0, 1.0] ,也就是說將所有資料除以 255 進行歸一化。

4.1.2 transforms.Normalize類(仿函式)

class Normalize(torch.nn.Module):
    def __init__(self, mean, std, inplace=False):
        super().__init__()
        _log_api_usage_once(self)
        self.mean = mean
        self.std = std
        self.inplace = inplace

mean:資料型別為元組,元組的長度取決於通道數

std:資料型別為元組,元組的長度取決於通道數

  • 此函式可以將tensor進行標準化,使其在每個通道上都轉化為均值為mean,標準差為std的高斯分佈。

4.1.3 transforms.Compose類(仿函式)

class Compose:
    def __init__(self, transforms):
        if not torch.jit.is_scripting() and not torch.jit.is_tracing():
            _log_api_usage_once(self)
        self.transforms = transforms

transforms:資料型別為列表,列表中每個元素都是transforms模組中的一個類,如ToTensor和Normalize(隱式構造)。

  • 此函式可以將許多transforms類結合起來同時使用。

4.1.4 示例

import torchvision
if __name__ == '__main__':
    MNIST=torchvision.datasets.MNIST(root="./data",train=True,download=False,transform=None)
    MNIST_normalize=torchvision.transforms.Compose([torchvision.transforms.ToTensor(),torchvision.transforms.Normalize((0),(1))])(MNIST[0][0])
    print(MNIST_normalize)

4.2 網路構建

4.2.1 啟用函式大全

在這裡插入圖片描述

  • 在pytorch中已經實現了上述很多的啟用函式,下面我們將使用ReLU啟用函式進行網路構建。

4.2.2 演示程式碼(在gpu上)

import torchvision
import torch
from torch.utils.data import DataLoader
from torch import nn
from torch import optim
from torch.nn import functional as Activate
from matplotlib import pyplot

# 定義所用網路
class ExNet(nn.Module):
    def __init__(self):
        # super函式呼叫
        super(ExNet, self).__init__()

        # 卷積層1
        self.conv1 = nn.Conv2d(1, 15, 5)
        '''
        輸入通道數1,輸出通道數15,核的大小5,輸入必須為1,輸出可以自定義
        '''
        # 卷積層2
        self.conv2 = nn.Conv2d(15, 30, 3)
        '''
        輸入通道數15,輸出通道數30,核的大小3,輸入必須與上層的輸出一致,輸出可以自定義
        '''

        # 全連線層1
        self.fully_connected_1 = nn.Linear(30 * 10 * 10, 40)
        '''
        MNIST原始影像是1*28*28,輸入為batch_size*1*28*28,經過卷積層1後,變為batch_size*15*24*24
        經過池化層後,變為batch_size*15*12*12
        經過卷積層2後,變為batch_size*30*10*10
        這個全連線層的第一層輸入個數就是這麼來的
        '''

        # 全連線層2
        self.fully_connected_2 = nn.Linear(40, 10)
        '''
        輸入與上層保持一致
        由於要鑑別十個數字,因此輸出層的神經元個數必須是10
        '''

    # 定義前向傳播
    def forward(self, x):
        in_size = x.size(0)  # 在本例中in_size,也就是BATCH_SIZE的值。輸入的x可以看成是batch_size*1*28*28的張量。

        # 卷積層1
        out = self.conv1(x)  # batch*1*28*28 -> batch*15*24*24
        out = Activate.relu(out)  # 呼叫ReLU啟用函式

        # 池化層
        out = Activate.max_pool2d(out, 2, 2)  # batch*15*24*24 -> batch*15*12*12(2*2的池化層會減半)

        # 卷積層2
        out = self.conv2(out)  # batch*15*12*12 -> batch*30*10*10
        out = Activate.relu(out)  # 呼叫ReLU啟用函式

        # flatten處理
        out = out.view(in_size, -1)

        # 全連線層1
        out = self.fully_connected_1(out)
        out = Activate.relu(out)

        # 全連線層2
        out = self.fully_connected_2(out)

        # 歸一化處理,以便進行交叉熵代價函式的運算
        out = Activate.log_softmax(out, dim=1)

        return out

# 開始訓練
def train(the_model, the_device, train_loader, the_optimizer, the_epoch):
  
    # 模型相關設定
    the_model=the_model.to(device=the_device)
    the_model.train(mode=True)

    # 用來繪製影像的變數
    list_times = []
    list_cost = []

    # 每輪迴圈
    for batch_idx, (data, target) in enumerate(train_loader):

        # 轉移到指定裝置上計算
        data = data.to(the_device);target = target.to(the_device)

        # 最佳化器引數重置
        the_optimizer.zero_grad()
      
        # 向前計算
        output = the_model.forward(data)
      
        # 計算誤差
        cost = Activate.nll_loss(output, target)
      
        # 反向傳播
        cost.backward()
      
        # 引數更新
        the_optimizer.step()
      
        # 列印資訊
        if batch_idx % 10 == 0:

            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                the_epoch, batch_idx * len(data), len(train_loader.dataset),
                           100. * batch_idx / len(train_loader), cost.item()))
            print(batch_idx, cost.item())

            list_times.append(batch_idx)
            list_cost.append(cost.item())

    # 繪製影像
    pyplot.scatter(list_times, list_cost)
    pyplot.savefig("costImage.jpg")
    pyplot.show()

    return


def test(the_model, the_device, the_test_loader):
  
    # 設定訓練模式
    the_model=the_model.to(device=the_device)
    the_model.eval()

    # 測試的結果集
    acc_vector = []
    cost_vector = []
  
    #開始測試
    with torch.no_grad():
        for index, (data, target) in enumerate(the_test_loader):
          
            # 轉移到指定裝置上計算
            data = data.to(the_device);target = target.to(the_device)

            # 向前計算
            output = the_model.forward(data)
          
            # 計算誤差
            cost = Activate.nll_loss(output, target)
            cost_vector.append(cost)

            pred = output.max(dim=1)[-1]  # output的尺寸是[batch_size,10],對每行取最大值,返回索引編號,即代表模型預測手寫數字的結果
            cur_acc = pred.eq(target).float().mean()  # 均值代表每組batch_size中查準率
            acc_vector.append(cur_acc)

    # 列印結果
    print("平均查準率:{}".format(sum(acc_vector)/len(acc_vector)))
    print("average cost:{}".format(sum(cost_vector)/len(cost_vector)))
    return


if __name__ == '__main__':
    gpu = torch.device("cuda")
    cpu = "cpu"

    # 準備資料
    transAndNorm = torchvision.transforms.Compose([torchvision.transforms.ToTensor(),
                                                   torchvision.transforms.Normalize((0), (1))])
    MNISTData = torchvision.datasets.MNIST(root="./data", train=True, download=False, transform=transAndNorm)
    MNISTtest = torchvision.datasets.MNIST(root="./data", train=False, download=False, transform=transAndNorm)
    MNISTData_loader = DataLoader(dataset=MNISTData, batch_size=10, shuffle=True)
    MNISTtest_loader = DataLoader(dataset=MNISTtest, batch_size=10, shuffle=True)

    # 例項化網路和最佳化器
    MNISTnet_Ex = ExNet()
    MNIST_optimizer = optim.Adam(MNISTnet_Ex.parameters(), lr=0.001)  # lr(learning rate)是學習率

    for i in range(1,2):
        train(the_model=MNISTnet_Ex, the_device=gpu, train_loader=MNISTData_loader, the_optimizer=MNIST_optimizer, the_epoch=i)
        test(the_model=MNISTnet_Ex, the_device=gpu, the_test_loader=MNISTtest_loader)

輸出

平均查準率:0.9804015159606934
average cost:0.061943911015987396

散點圖
在這裡插入圖片描述

5.製作圖片資料集(以flower102為例)

在剛剛的MNIST手寫數字識別分類任務中,我們使用的資料集是pytorch官方內建的圖片資料集。現在,我們要從零開始,嘗試製作我們自己的資料集。

Oxford 102 Flower 是一個影像分類資料集,由 102 個花卉類別組成。被選為英國常見花卉的花卉。每個類別由 40 到 258 張影像組成。影像具有大尺度、姿勢和光線變化。此外,還有一些類別在類別內有很大的變化,還有幾個非常相似的類別。這裡是flower102資料集的下載地址。解壓後的檔案目錄如下:
在這裡插入圖片描述

5.1 建立資料集骨架

如第三章一樣建立即可,如下:

import torch
from torch.utils.data import Dataset
import os
gpu = torch.device("cuda")
cpu = "cpu"
class flower102(Dataset):
    def __init__(self,root,resize,mode):
        super(flower102,self).__init__()
        pass

    def __len__(self):
        pass

    def __getitem__(self, item):
        pass

5.2 建立從名稱到數字標籤的對映

在這裡插入圖片描述
在訓練集中,這102種花的類別名稱如上圖所示(我這裡是經過重新命名的),我們定義名稱flower1數字標籤1,這樣我們就建立了一個對映。接下來,稍微修改一下建構函式,就可以實現全部的對映。如下:

import csv
import glob
import random
import os
from PIL import Image

import torch
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms

gpu = torch.device("cuda")
cpu = "cpu"

class flower102(Dataset):
    def __init__(self, root, resize, mode):
        super(flower102, self).__init__()

        self.root = root
        self.train_root = os.path.join(self.root, "train")
        self.val_root = os.path.join(self.root, "valid")
        self.test_root = os.path.join(self.root, "test")

        self.resize = resize
        self.mode = mode

        self.mean = [0.485, 0.456, 0.406]
        self.std = [0.229, 0.224, 0.225]

        self.cat2label = {}  # 建立一個空字典,用於儲存對映關係。
        for name in sorted(os.listdir(os.path.join(self.train_root))):  # 遍歷訓練集目錄下的檔案和資料夾,並按照名稱排序。
            if not os.path.isdir(os.path.join(self.train_root, name)):  # 如果遍歷到的是檔案而不是資料夾,則跳過該項繼續遍歷下一項。
                continue
            elif not (name in self.cat2label):
                self.cat2label[name] = len(self.cat2label.keys())  # 將資料夾名稱與類別標籤對應,類別標籤為字典長度(每次迴圈增加1)。
        print(self.cat2label)  # 列印對映關係字典。

    def __len__(self):
       	pass

    def __getitem__(self, idx):
        pass

# 建立資料集例項
db = flower102(r"D:\Desktop\Datasets\flower102\dataset", resize=224, mode="train")

結果如下:

{'flower1': 0, 'flower10': 1, 'flower100': 2, 'flower101': 3, 'flower102': 4, 'flower11': 5, 'flower12': 6, 'flower13': 7, 'flower14': 8, 'flower15': 9, 'flower16': 10, 'flower17': 11, 'flower18': 12, 'flower19': 13, 'flower2': 14, 'flower20': 15, 'flower21': 16, 'flower22': 17, 'flower23': 18, 'flower24': 19, 'flower25': 20, 'flower26': 21, 'flower27': 22, 'flower28': 23, 'flower29': 24, 'flower3': 25, 'flower30': 26, 'flower31': 27, 'flower32': 28, 'flower33': 29, 'flower34': 30, 'flower35': 31, 'flower36': 32, 'flower37': 33, 'flower38': 34, 'flower39': 35, 'flower4': 36, 'flower40': 37, 'flower41': 38, 'flower42': 39, 'flower43': 40, 'flower44': 41, 'flower45': 42, 'flower46': 43, 'flower47': 44, 'flower48': 45, 'flower49': 46, 'flower5': 47, 'flower50': 48, 'flower51': 49, 'flower52': 50, 'flower53': 51, 'flower54': 52, 'flower55': 53, 'flower56': 54, 'flower57': 55, 'flower58': 56, 'flower59': 57, 'flower6': 58, 'flower60': 59, 'flower61': 60, 'flower62': 61, 'flower63': 62, 'flower64': 63, 'flower65': 64, 'flower66': 65, 'flower67': 66, 'flower68': 67, 'flower69': 68, 'flower7': 69, 'flower70': 70, 'flower71': 71, 'flower72': 72, 'flower73': 73, 'flower74': 74, 'flower75': 75, 'flower76': 76, 'flower77': 77, 'flower78': 78, 'flower79': 79, 'flower8': 80, 'flower80': 81, 'flower81': 82, 'flower82': 83, 'flower83': 84, 'flower84': 85, 'flower85': 86, 'flower86': 87, 'flower87': 88, 'flower88': 89, 'flower89': 90, 'flower9': 91, 'flower90': 92, 'flower91': 93, 'flower92': 94, 'flower93': 95, 'flower94': 96, 'flower95': 97, 'flower96': 98, 'flower97': 99, 'flower98': 100, 'flower99': 101}

5.3 建立csv資料

在建立了從名稱到數字標籤的對映後,我們希望有一個csv檔案,裡面儲存了所有的圖片路徑及其數字標籤,接下來,我們將定義一個load_csv函式去完成這件事,如下:

import csv
import glob
import random
import os
from PIL import Image

import torch
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms

gpu = torch.device("cuda")
cpu = "cpu"

class flower102(Dataset):
    def __init__(self, root, resize, mode):
        super(flower102, self).__init__()

        self.root = root
        self.train_root = os.path.join(self.root, "train")
        self.val_root = os.path.join(self.root, "valid")
        self.test_root = os.path.join(self.root, "test")

        self.resize = resize
        self.mode = mode

        self.mean = [0.485, 0.456, 0.406]
        self.std = [0.229, 0.224, 0.225]

        self.cat2label = {}  # 建立一個空字典,用於儲存對映關係。
        for name in sorted(os.listdir(os.path.join(self.train_root))):  # 遍歷訓練集目錄下的檔案和資料夾,並按照名稱排序。
            if not os.path.isdir(os.path.join(self.train_root, name)):  # 如果遍歷到的是檔案而不是資料夾,則跳過該項繼續遍歷下一項。
                continue
            elif not (name in self.cat2label):
                self.cat2label[name] = len(self.cat2label.keys())  # 將資料夾名稱與類別標籤對應,類別標籤為字典長度(每次迴圈增加1)。
        print(self.cat2label)  # 列印對映關係字典。

        if mode == "train":
            self.images, self.labels = self.load_csv("images_train.csv")
        elif mode == "valid":
            self.images, self.labels = self.load_csv("images_valid.csv")
        else:
            raise Exception("invalid mode!", self.mode)

    # 載入CSV檔案並返回影像路徑和標籤列表
    def load_csv(self, filename):
        # 如果CSV檔案不存在,則根據訓練集目錄和對映關係生成CSV檔案
        if not os.path.exists(os.path.join(self.root, filename)):
            images = []
            for name in self.cat2label.keys():
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.png"))
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.jpg"))
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.jpeg"))

            random.shuffle(images)
            with open(os.path.join(self.root, filename), mode="w", newline="") as f:
                writer = csv.writer(f)
                for img in images:
                    label = self.cat2label[img.split(os.sep)[-2]]
                    writer.writerow([img, label])
                print("written into csv file:", filename)

        # 從CSV檔案中讀取影像路徑和標籤
        images = []
        labels = []
        with open(os.path.join(self.root, filename)) as f:
            reader = csv.reader(f)
            for row in reader:
                img, label = row
                label = int(label)

                images.append(img)
                labels.append(label)

        assert len(images) == len(labels)

        return images, labels

    # 反歸一化
    def denormalize(self, x_hat):
		pass

    def __len__(self):
        pass

    def __getitem__(self, idx):
        pass

# 建立資料集例項
db = flower102(r"D:\Desktop\Datasets\flower102\dataset", resize=224, mode="train")

然後,我們獲得了一個如下的csv檔案:
在這裡插入圖片描述

5.4 完善成員函式和transform過程

在完成了load_csv函式後,這個資料集基本製作完成,接下來只需要完善__len__函式和__getitem__函式,並定義transform過程即可。

import csv
import glob
import random
import os
from PIL import Image

import torch
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms

gpu = torch.device("cuda")
cpu = "cpu"

class flower102(Dataset):
    def __init__(self, root, resize, mode):
        super(flower102, self).__init__()

        self.root = root
        self.train_root = os.path.join(self.root, "train")
        self.val_root = os.path.join(self.root, "valid")
        self.test_root = os.path.join(self.root, "test")

        self.resize = resize
        self.mode = mode

        self.mean = [0.485, 0.456, 0.406]
        self.std = [0.229, 0.224, 0.225]

        self.cat2label = {}  # 建立一個空字典,用於儲存對映關係。
        for name in sorted(os.listdir(os.path.join(self.train_root))):  # 遍歷訓練集目錄下的檔案和資料夾,並按照名稱排序。
            if not os.path.isdir(os.path.join(self.train_root, name)):  # 如果遍歷到的是檔案而不是資料夾,則跳過該項繼續遍歷下一項。
                continue
            elif not (name in self.cat2label):
                self.cat2label[name] = len(self.cat2label.keys())  # 將資料夾名稱與類別標籤對應,類別標籤為字典長度(每次迴圈增加1)。
        print(self.cat2label)  # 列印對映關係字典。

        if mode == "train":
            self.images, self.labels = self.load_csv("images_train.csv")
        elif mode == "valid":
            self.images, self.labels = self.load_csv("images_valid.csv")
        else:
            raise Exception("invalid mode!", self.mode)

    # 載入CSV檔案並返回影像路徑和標籤列表
    def load_csv(self, filename):
        # 如果CSV檔案不存在,則根據訓練集目錄和對映關係生成CSV檔案
        if not os.path.exists(os.path.join(self.root, filename)):
            images = []
            for name in self.cat2label.keys():
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.png"))
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.jpg"))
                images += glob.glob(os.path.join(self.root, self.mode, name, "*.jpeg"))

            random.shuffle(images)
            with open(os.path.join(self.root, filename), mode="w", newline="") as f:
                writer = csv.writer(f)
                for img in images:
                    label = self.cat2label[img.split(os.sep)[-2]]
                    writer.writerow([img, label])
                print("written into csv file:", filename)

        # 從CSV檔案中讀取影像路徑和標籤
        images = []
        labels = []
        with open(os.path.join(self.root, filename)) as f:
            reader = csv.reader(f)
            for row in reader:
                img, label = row
                label = int(label)

                images.append(img)
                labels.append(label)

        assert len(images) == len(labels)

        return images, labels

    # 反歸一化
    def denormalize(self, x_hat):

        # x_hat = (x - mean) / std
        # x = x_hat * std + mean
        # x.size(): [c, h, w]
        # mean.size(): [3] => [3, 1, 1]
        mean = torch.tensor(self.mean).unsqueeze(1).unsqueeze(1)
        std = torch.tensor(self.std).unsqueeze(1).unsqueeze(1)
        x = x_hat * std + mean
        return x

    def __len__(self):
        # 返回資料集中樣本的數量
        return len(self.images)

    def __getitem__(self, idx):
        # 根據索引獲取影像和標籤
        img, label = self.images[idx], self.labels[idx]

        # 定義資料的預處理操作
        tf = transforms.Compose([
            lambda x: Image.open(x).convert("RGB"),  # 以RGB格式開啟影像
            transforms.Resize((int(self.resize * 1.25), int(self.resize * 1.25))),  # 調整影像大小為resize的1.25倍
            transforms.RandomRotation(15),  # 隨機旋轉影像(最大旋轉角度為15度)
            transforms.CenterCrop(self.resize),  # 將影像中心裁剪為resize大小
            transforms.ToTensor(),  # 將影像轉換為Tensor型別
            transforms.Normalize(mean=self.mean, std=self.std),  # 歸一化影像
        ])

        # 對影像進行預處理操作
        img = tf(img)
        label = torch.tensor(label)

        return img, label

# 建立資料集例項
db = flower102(r"D:\Desktop\Datasets\flower102\dataset", resize=224, mode="train")

5.5 DataLoader檢驗

if __name__=='__main__' :
    loader = DataLoader(dataset=db, shuffle=True,num_workers=1,batch_size=8)

    import matplotlib.pyplot as plt

    data,target=next(iter(db))
    print(data.shape)

    plt.imshow(transforms.ToPILImage()(db.denormalize(data)))
    plt.show()

成功顯示:
在這裡插入圖片描述

6.遷移學習

6.1 現有模型的儲存和載入

6.1.1 儲存(torch.save函式)

我們要儲存的是:

  • 例項化的網路的資料
  • 例項化的最佳化器的資料
def save(
    obj: object,
    f: FILE_LIKE,
    pickle_module: Any = pickle,
    pickle_protocol: int = DEFAULT_PROTOCOL,
    _use_new_zipfile_serialization: bool = True
) -> None:...
  • 我們只需要把string型別的檔名作為引數輸入即可

把資料載入進網路

  • Module.load_state_dict函式,我們只需要用torch.load函式的返回值作為引數即可

把資料載入進最佳化器

  • optim.load_state_dict函式,我們只需要用torch.load函式的返回值作為引數即可

6.1.3 示例

    torch.save(MNISTnet_Ex.state_dict(),"MNIST.pt")
    torch.save(optimzer.state_dict(),"optimizer.pt")
    MNISTnet_Ex.load_state_dict(torch.load("MNIST.pt"))
    optimzer.load_state_dict(torch.load("optimizer.pt"))

6.2 使用預訓練的模型(以resnet50為例)

pytoch官方提供了不少與訓練的模型可供使用,如下:

model
AlexNet
ConvNeXt
DenseNet
EfficientNet
EfficientNetV2
GoogLeNet
Inception V3
MaxVit
MNASNet
MobileNet V2
MobileNet V3
RegNet
ResNet
ResNeXt
ShuffleNet V2
SqueezeNet
SwinTransformer
VGG
VisionTransformer
Wide ResNet

關於這些模型的詳細用途,可以自行前往pytorch官網查閱相關資料,具體原理本文不再涉及。

6.2.1 確定初始化引數

在使用預訓練模型的過程中,最重要的一步是,確定這個預訓練模型中哪些引數是需要訓練的,哪些引數是不需要訓練的,哪些引數是要修改的

首先,檢視一下resnet50的網路結構:

import torchvision.models as models
print(models.resnet50(pretrained=True))
Resnet(
...
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=2048, out_features=1000, bias=True)
)

看到最後一層是一個1000分類的全連線層,而我們第五章製作的資料集裡,只需要102分類,因此,我們選擇只修改最後一層的引數並訓練。如下所示:

import torchvision.models as models
import torch.nn as nn

def set_parameter_requires_grad(model,need_train):
    if not need_train:
        for para in model.parameters():
            para.requires_grad = False
    return

def initalize_resnet50(num_classes,need_train=False,pretrained=True):
    trained_model=models.resnet50(pretrained=pretrained)
    input_size=224
    set_parameter_requires_grad(trained_model, need_train)
    trained_model.fc = nn.Sequential(
        nn.Linear(trained_model.fc.in_features, num_classes),
        nn.LogSoftmax(dim=1),
    )
    # trained_model.fc = nn.Sequential(
    #     nn.Linear(trained_model.fc.in_features, num_classes),
    #     nn.Flatten(),
    # )

    return trained_model,input_size

resnet50,input_size=initalize_resnet50(num_classes=102,need_train=False,pretrained=True)

6.3 開始訓練

訓練的流程和記錄如第四章所示即可,如下:

import copy  # 匯入copy模組,用於深複製物件
import os.path  # 匯入os.path模組,用於操作檔案路徑
import time  # 匯入time模組,用於計時

def train(model, dataLoader, criterion, optimzer, num_epoch, device, filename):
    """
    訓練函式
  
    Args:
        model: 模型物件
        dataLoader: 資料載入器
        criterion: 損失函式
        optimzer: 最佳化器
        num_epoch: 迭代次數
        device: 計算裝置
        filename: 儲存模型的檔名
  
    Returns:
        model: 訓練後的模型
        train_acc_history: 訓練集準確率歷史
        train_losses: 訓練集損失歷史
        l_rs: 最佳化器學習率歷史
    """

    since = time.time()  # 獲取當前時間
    best_epoch = {"epoch": -1,
                  "acc": 0
                  }  # 儲存最佳模型的epoch和準確率

    model.to(device)  # 將模型移動到計算裝置上

    train_acc_history = []  # 儲存訓練集準確率歷史
    train_losses = []  # 儲存訓練集損失歷史
    l_rs = [optimzer.param_groups[0]['lr']]  # 儲存最佳化器學習率歷史

    best_model_wts = copy.deepcopy(model.state_dict())  # 深複製當前模型的權重作為最佳模型權重

    for epoch in range(num_epoch):  # 迭代訓練
        print("Epoch {}/{}".format(epoch, num_epoch - 1))
        print('*' * 10)

        running_loss = 0.0  # 初始化損失總和
        running_correct = 0.0  # 初始化正確預測的樣本數總和

        for data, target in dataLoader:  # 遍歷資料載入器中的每個批次
            data = data.to(device)  # 將輸入資料移動到計算裝置上
            target = target.to(device)  # 將目標資料移動到計算裝置上

            optimzer.zero_grad()  # 清零梯度

            output = model.forward(data)  # 前向傳播

            loss = criterion(output, target)  # 計算損失

            pred = output.argmax(dim=1)  # 獲取預測結果

            loss.backward()  # 反向傳播

            optimzer.step()  # 更新引數

            running_loss += loss.item() * data.size(0)  # 累加損失
            running_correct += torch.eq(pred, target).sum().float().item()  # 累加正確預測的樣本數

        epoch_loss = running_loss / len(dataLoader.dataset)  # 計算平均損失
        epoch_acc = running_correct / len(dataLoader.dataset)  # 計算準確率

        time_elapsed = time.time() - since  # 計算訓練時間
        print("Time elapsed {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
        print("Loss: {:4f} Acc:{:.4f}".format(epoch_loss, epoch_acc))

        train_acc_history.append(epoch_acc)  # 將準確率新增到歷史列表中
        train_losses.append(epoch_loss)  # 將損失新增到歷史列表中

        if (epoch_acc > best_epoch["acc"]):  # 更新最佳模型資訊
            best_epoch = {
                "epoch": epoch,
                "acc": epoch_acc
            }
            best_model_wts = copy.deepcopy(model.state_dict())  # 深複製當前模型權重作為最佳模型權重
            state = {
                "state_dict": model.state_dict(),
                "best_acc": best_epoch["acc"],
                "optimzer": optimzer.state_dict(),
            }
            torch.save(state, filename)  # 儲存最佳模型的狀態字典到檔案

        print("Optimzer learning rate : {:.7f}".format(optimzer.param_groups[0]['lr']))  # 列印當前最佳化器學習率
        l_rs.append(optimzer.param_groups[0]['lr'])  # 將當前最佳化器學習率新增到歷史列表中
        print()

    time_elapsed = time.time() - since  # 計算總訓練時間
    print("Training complete in {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
    print("Best epoch:", best_epoch)

    model.load_state_dict(best_model_wts)  # 載入最佳模型權重

    return model, train_acc_history, train_losses, l_rs


if __name__ == "__main__":
    import torch
    import Net
    import torch.nn as nn
    import torch.optim as optim

    optimzer = optim.Adam(params=Net.resnet50.parameters(), lr=1e-2)  # 建立Adam最佳化器
    sche = optim.lr_scheduler.StepLR(optimizer=optimzer, step_size=10, gamma=0.5)  # 建立學習率排程器
    criterion = nn.NLLLoss()  # 建立負對數似然損失函式
    #criterion=nn.CrossEntropyLoss()

    import flower102
    from torch.utils.data import DataLoader

    db = flower102.flower102(r"D:\Desktop\Datasets\flower102\dataset", resize=Net.input_size, mode="train")  # 建立資料集物件
    loader = DataLoader(dataset=db, shuffle=True, num_workers=1, batch_size=5)  # 建立資料載入器

    model = Net.resnet50  # 建立模型物件
    filename = "checkpoint.pth"  # 模型儲存檔名

    if os.path.exists(filename):  # 如果存在模型檔案
        checkpoint = torch.load(filename)  # 載入模型狀態字典
        model.load_state_dict(checkpoint["state_dict"])  # 載入模型權重

    model, train_acc_history, train_loss, LRS = train(model=model, dataLoader=loader, criterion=criterion,
                                                      optimzer=optimzer, num_epoch=5,
                                                      device=torch.device("cuda"), filename=filename)

下面是我訓練5輪的結果:

Epoch0/4
**********
Time elapsed 0m 37s
Loss: 11.229704 Acc:0.3515
Optimzer learning rate : 0.0100000
Epoch1/4
**********
Time elapsed 1m 12s
Loss: 8.165128 Acc:0.5697
Optimzer learning rate : 0.0100000
Epoch2/4
**********
Time elapsed 2m 4s
Loss: 7.410833 Acc:0.6363
Optimzer learning rate : 0.0100000
Epoch3/4
**********
Time elapsed 2m 60s
Loss: 6.991850 Acc:0.6822
Optimzer learning rate : 0.0100000
Epoch4/4
**********
Time elapsed 3m 44s
Loss: 6.482804 Acc:0.7128
Optimzer learning rate : 0.0100000
Training complete in 3m 44s
Best epoch: {'epoch': 4, 'acc': 0.7127594627594628}

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章