AI => Pytorch與Tersorflow2.0簡單對比

前言

目前一些模型API還沒有遷移到TF20中。 eg: CRF,Seq2Seq等
若是退回TF10,有些傷。
倒不如轉至Torch。
Pytorch的大部分思想和TF20大體類似。python

至於安裝,GPU我前面說過TF20。這裏不贅述。
官檔安裝:https://pytorch.org/get-started/locally/#start-locally網絡

注意

本文幾乎通篇以代碼案例 和 註釋標註 的方式解釋API。(模型的訓練效果不作考慮。只看語法)
你若是懂Tensorflow2.0(Stable),那麼你看本文必定不費勁。
Torch和TF20 很像!!!
所以一些地方,我會列出 TF20 與 Torch的細節對比。app

開門案例1-MNIST

模塊導入

import torch
from torch import nn, optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

數據預處理

data_preprocess = transforms.Compose([  # 頂預約數據處理函數,相似map()裏的函數句柄
    transforms.Resize(28,28),           # 變形
    transforms.ToTensor(),              # numpy 轉 Tensor
])

trian_dataset = datasets.MNIST(         # TF20在keras.datasets中,未歸一化(0-255)
    '.',                                # 下載至當前目錄, (圖片0-1,已經被歸一化了)
    train=True,                         # train=True, 表明直接給你切出 訓練集
    download=True,                      # True,若未下載,則先下載
    transform=data_preprocess,          # 指定數據預處理函數。第一行咱們指定的
)

test_dataset = datasets.MNIST(
    '.', 
    train=False, # False表明測試集       # 就說下這裏, False表明 給你切出測試集
    download=True,
    transform=data_preprocess,
)

train = DataLoader(     # 對應TF20中的 tf.data.Dataset對數據二次預處理(分批,亂序)
    trian_dataset,      # 把上面第一次預處理的數據集 加載進來
    batch_size=16,      # mini-batch
    shuffle=True,       # 亂序,加強模型泛化能力
)

test = DataLoader(
    test_dataset,
    batch_size=16,
    shuffle=True,
)

MNIST模型(定義-訓練代碼)

# 模型定義部分
class MyModel(nn.Module):             # TF20是 tk.models.Model
    def __init__(self):               # TF20 也是 __init__()
        super().__init__()

        self.model = nn.Sequential(   # tk.models.Sequential , 而且 TF裏面 須要加一個 []
            nn.Linear(28*28, 256),    # tk.layers.Dense(256)
            nn.ReLU(),                # tk.layers.Relu()

            nn.Linear(256, 128),      # tk.layers.Dense(128)
            nn.ReLU(),               

            nn.Linear(128, 10),       # tk.layers.Dense(10)            
        ) 
    def forward(self, x):  # TF20是 __call__()
        x = x.view( x.size(0), 28*28 )      # x.view ==> tf.reshape   x.size ==> x.shape[0]
        y_predict = self.model(x)

        return y_predict
# -------------------------------華麗分割線---------------------------------     
# 模型訓練部分
def main():
    vis = visdom.Visdom()
    model = MyModel()
    loss_ = nn.CrossEntropyLoss()     # 會將 y_predict自動加一層 softmax
    optimizer = optim.Adam(model.parameters())     # TF20: model.trainable_variables
    
    # visdom可視化
    # 這步是初始化座標點,下面loss會用這個直接更新
    vis.line(
        [0],                    # x座標
        [0],                    # y座標
        win='loss',             # 窗口名稱
        opts={'title': 'loss'}, # 窗口標題
    )
    
    for epoch in range(10):   # epochs
        for step, (x, y_true) in enumerate(train):
            y_predict = model(x)
            loss = loss_(y_predict, y_true)
            optimizer.zero_grad()           #  優化器清零 
            loss.backward()                 #  梯度計算
            optimizer.step()                #  梯度降低更新 tp.gradient(loss, variables)。 
            
            # 在上面的定義的基礎上更新追加畫點-連成線
            vis.line(
                [loss.item()],
                [step],
                win='loss',
                update='append', # 追加畫點,而不是更新覆蓋
            )
        print(loss.item())                  #  .item()  => 至關於 tensorflow 的 numpy()
        
        if epoch % 2 == 0:
            total_correct_samples = 0       # 用於記錄(預測正確的樣本的 總數量)
            total_samples = 0               # 用於記錄(樣本的 總數量)

            for x_test, y_test in test:
                y_pred = model(x_test)
                y_final_pred = y_pred.argmax(dim=1)     # TF20的座標軸參數是 axis
                
                 # 每一批是 batch_size=16,咱們要把它們都加在一塊兒
                total_correct_samples += torch.eq(y_final_pred, y_test).float().sum().item()
                
                # 這裏提一下 eq() 和 equal() 的返回值的區別, 本身看,咱們一般用 eq
                # print( torch.equal( torch.Tensor([[1,2,3]]), torch.Tensor([[4,5,6]] ) ) ) 
                #結果:  False
                # print( torch.eq( torch.Tensor([[1,2,3]]), torch.Tensor([[4,5,6]] ) ) ) 
                #結果:  tensor([[0, 0, 0]], dtype=torch.uint8)

                per_sample = x_test.size(0)   # 再說一次, size(0) 至關於TF xx.shape[0]
                # 獲取每批次樣本數量, 雖然咱們知道是 16
                # 可是最後一個batch_size 可能不是16,因此要準確獲取。
                total_samples += per_sample

            acc = total_correct_samples / total_samples
            print(f'epoch: {epoch}, loss: {loss}, acc: {acc}')   
            
            # 測試部分
            vis.line(
                [acc],
                [step],
                win='acc',
                update='append', # 追加畫點,而不是更新覆蓋
            )

            x, label = iter(test).next()
            target_predict = model(x).argmax(dim=1)
            
            # 畫出測試集圖片
            viz.images(x, nrow=16, win="test_x", opts={'title': "test_x"}) 
            vis.text(    # 顯示預測標籤文本
                str(target_predict.detach().numpy() ),
                win = 'target_predict',
                opts = {"title": target_predict}
            )
            vis.text(    # 顯示真值文本
                str(label.detach().numpy() ),
                win = 'target_true',
                opts = {"target_true": target_predict}
            )
main()

模型可視化(visdom)

安裝 和 運行 和 使用
安裝
    pip install visdom
運行
    python -m visdom.server  (第一次可能會有點慢)
    
# 語法和Tensorboard很像

使用
    import visdom
    見上代碼 vis.xxxxx

案例2-CIFAR10+CNN

說明

模塊導入和數據預處理部分和案例1的 MNIST如出一轍。
只要稍稍修改 datasets.MNIST ==> datasets.CIFAR10 便可, 簡單的不忍直視~~dom

代碼以下:

模型定義部分:ide

class MyModel(nn.Module):        # 舒適提示, 這是 Mmodule, 不是model
    def __init__(self):
        """
            先註明一下: 
                TF中輸入圖片形狀爲       (樣本數, 高,寬,圖片通道)
                PyTorch中輸入圖片形狀爲  (樣本數, 圖片通道,高,寬)
        """

        super().__init__()
        self.conv = nn.Sequential( # 再強調一遍,沒有 []
            nn.Conv2d(
                in_channels=3,    # 對應TF  圖片通道數(或者上一層通道)
                out_channels=8,   # 對應TF  filters, 卷積核數量
                kernel_size=3,    # 卷積核大小
                stride=1,         # 步長, TF 是 strides,  特別注意
                padding=0,        # no padding, 默認
            ),
            nn.ReLU(),
            nn.MaxPool2d(
                kernel_size=3,    # 滑動窗口大小
                stride=None,      # 默認爲None, 意爲和 kernel_size相同大小
            ),

            nn.Conv2d(
                in_channels=8,    # 對應TF  圖片通道數(或者上一層通道)
                out_channels=16,   # 對應TF  filters, 卷積核數量
                kernel_size=3,    # 卷積核大小
                stride=1,         # 步長, TF 是 strides,  特別注意
                padding=0,        # no padding, 默認
            ),
            nn.ReLU(),
            nn.MaxPool2d(
                kernel_size=2,    # 滑動窗口大小
                stride=None,      # 默認爲None, 意爲和 kernel_size相同大小
            ),
        )
        self.dense = nn.Sequential(
            nn.Linear(16*4*4, 128),    # 對應TF Dense
            nn.Linear(128, 64),
            nn.Linear(64, 10),
        )
    def forward(self, x):
        conv_output = self.conv(x)
        # (16, 16, 4.4)
        conv_output_reshape = conv_output.view(-1, 16*4*4)
        dense_output = self.dense(conv_output_reshape)

        return dense_output

模型訓練(模型調用+模型訓練的定義)函數

def main():
    vis = visdom.Visdom()

    epochs = 100

    device = torch.device('cuda')  # 預約義 GPU 槽位(一會往裏面塞 模型和數據。)

    model = MyModel().to(device)   # 模型轉爲 GPU 計算
    
    # CrossEntropyLoss 會自動把下面的 dense_output ,也就是y_predict 加一層 softmax
    loss_ = nn.CrossEntropyLoss().to(device)  
    optimizer = optim.Adam( model.parameters() )

    for epoch in range(epochs):
        for step, (x_train, y_train) in enumerate(train):
            x_train, y_train = x_train.to(device), y_train.to(device)
            dense_output = model(x_train)

            loss = loss_(dense_output, y_train)

            optimizer.zero_grad()   # 上一個例子提到過,梯度清零
            loss.backward()         # 反向傳播, 並將梯度累加到 optimizer中
            optimizer.step()        # 至關於作了 w = w - lr * 梯度

        print(loss.item())          # item() 意思就是 tensor轉numpy,TF中的 API是 xx.numpy()

        sample_correct_numbers = 0
        sample_total_numbers = 0

        with torch.no_grad():   # 測試部分不須要計算梯度,所以能夠包裹在上下文中。
            for x_test, y_test in test:
                x_test, y_test = x_test.to(device), y_test.to(device)
                
                # softmax 的 y_predict  與  y_test的 one-hot作交叉熵
                y_predict = model(x_test).argmax(dim=1) 
                sample_correct_numbers += torch.eq(y_predict, y_test).float().sum().item()
                sample_total_numbers += x_test.size(0)  # 每批樣本的總數加在一塊兒
            acc = sample_correct_numbers / sample_total_numbers
            print(acc)

main()

案例3:CIFAR10+ResNet-18

結構圖體系:

image.png
上述結構說明:測試

1conv + (2+2+2+2)*2 + 1 fc = 18層
1conv + (3+4+6+3)*2 + 1 fc = 34層
1conv + (3+4+6+3)*3 + 1 fc = 50層
1conv + (3+4+23+3)*3 + 1 fc = 101層
1conv + (3+8+36+3)*3 + 1 fc = 152層

代碼實現

模塊導入優化

import cv2
import torch
from torch import nn, optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import visdom
import torch.nn.functional as F

數據導入預處理ui

data_preprocess = transforms.Compose([
    transforms.Resize(32,32),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

train_dataset = datasets.CIFAR10(
    '.',
    train=True,
    download=True,
    transform=data_preprocess,
)


test_dataset = datasets.CIFAR10(
    '.',
    train=False, # False表明測試集
    download=True,
    transform=data_preprocess,
)


train = DataLoader(
    train_dataset,
    batch_size=16,
    shuffle=True,
)

test = DataLoader(
    test_dataset,
    batch_size=16,
    shuffle=True,
)

基礎塊定義(BasicBlock):spa

class BasicBlock(nn.Module):
    """單個殘差塊 2個卷積+2個BN"""
    def __init__(self, input_channel, output_channel, stride=1):
        super().__init__()
        self.major = nn.Sequential(
            # 第一個Conv的步長爲指定步長,容許降採樣,容許輸出輸出通道不一致
            nn.Conv2d(input_channel,output_channel,kernel_size=3,stride=stride, padding=1),
            nn.BatchNorm2d(output_channel),
            nn.ReLU(inplace=True),
            # 第二個Conv的步長爲定長1, 輸入輸出通道不變(緩衝輸出)
            nn.Conv2d(output_channel, output_channel, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(output_channel),   
            # 第二個Conv就不用ReLU了, 由於一會須要和 x加在一塊兒,最後最一層大的Relu
        )
        # 若輸入通道==輸出通道,且步長爲1,意味着圖片未被降採樣,則殘差網絡課直接爲普通網絡

        self.shortcut = nn.Sequential()
        # 若輸入輸出通道不匹配,這時須要將圖片作一樣的變換,才能加在一塊兒。
        if input_channel != output_channel or stride != 1:
            self.shortcut = nn.Sequential(
                nn.Conv2d(
                    input_channel,
                    output_channel,
                    kernel_size=(1,1),
                    stride = stride
                ),
                nn.BatchNorm2d(output_channel)
            )            

    def forward(self, x):
        major_out = self.major(x)        # 主幹網絡的輸出
        shotcut_out = self.shortcut(x)   # 殘差網絡的輸出
        # 上面這兩個網絡是平行的關係,  由於 它們的輸出不是鏈式的, 而是  都是一樣的 x。

        # 拼接主幹網絡+殘差網絡,F 至關於TF20的 tf.nn 裏面單獨有各類 loss函數
        return F.relu(major_out + shotcut_out)  # 最後在拼接後的網絡外面加一層relu

ResNet+ResBlock定義:

class ResNet(nn.Module):
    def __init__(self, layers):  # layers用來接受,用戶想要指定 ResNet的形狀
        super().__init__()

        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
        )        

        self.res_net = nn.Sequential(
            *self.ResBlock(32,64, layers[0],stride=2),    # 16
            *self.ResBlock(64,128, layers[1],stride=2),   # 8
            *self.ResBlock(128,256, layers[2],stride=2),  # 4
            *self.ResBlock(256,512, layers[3],stride=2),  # 2
        )
        # 由於咱們一會須要展平,裏面填"通道*寬度*高度", "輸出通道"
        self.dense = nn.Linear(512 * 2 * 2, 10)  
        
    def forward(self, x):
        out = self.conv1(x)
        out = self.res_net(out)

        out = out.view(x.size(0), -1)# 卷積展平操做 , torch中沒有flatten因此咱們就得手工
        out = self.dense(out)
        return out

    def ResBlock(self, input_channel, output_channel, block_nums=2, stride=2):
        # 自定義規定,第一個block縮小的(對應通道翻倍),其他block大小不變
        # 通道翻倍,步長*2,特徵減半
        all_block = [BasicBlock(input_channel, output_channel,stride=stride)]   

        for x in range(1,block_nums):
            all_block.append(BasicBlock(output_channel, output_channel,stride=1))
        return all_block

# resnet = ResNet(layers=[2,2,2,2])
# out = resnet(torch.randn(4,3,32,32))
# print(out.shape)

模型訓練:

def main():
    vis = visdom.Visdom()

    epochs = 5

    device = torch.device('cuda')

    model = ResNet(layers=[2,2,2,2]).to(device) 
    
    # 會自動把下面的 dense_output ,也就是y_predict 加一層 softmax,y_true作one-hot
    loss_ = nn.CrossEntropyLoss().to(device)  
    optimizer = optim.Adam( model.parameters(), lr=0.0001)

    for epoch in range(epochs):
        total_loss = 0.0
        for step, (x_train, y_train) in enumerate(train):
            x_train, y_train = x_train.to(device), y_train.to(device)
            dense_output = model(x_train)

            loss = loss_(dense_output, y_train)

            optimizer.zero_grad()   # 上一個例子提到過,梯度清零
            loss.backward()         # 反向傳播, 並將梯度累加到 optimizer中
            optimizer.step()        # 至關於作了 w = w - lr * 梯度
            total_loss += loss.item() # item()就是 tensor轉numpy, TF中的 API是 xx.numpy()
            if step % 50 == 49:
                print('epoch:',epoch, 'loss:', total_loss / step) 

        sample_correct_numbers = 0
        sample_total_numbers = 0

        with torch.no_grad():   # 測試部分不須要計算梯度,所以能夠包裹在上下文中。
            for x_test, y_test in test:
                x_test, y_test = x_test.to(device), y_test.to(device)
                # softmax 的 y_predict  與  y_test的 one-hot作交叉熵
                y_predict = model(x_test).argmax(dim=1) 
                sample_correct_numbers += torch.eq(y_predict, y_test).float().sum().item()
                sample_total_numbers += x_test.size(0)  # 每批樣本的總數加在一塊兒
            acc = sample_correct_numbers / sample_total_numbers
            print(acc)
    torch.save(model, 'model.pkl')  # 保存整個模型
main()

測試數據預處理(我隨便在網上下載下來的 1 張圖片):

# 這是Cifar-10數據的標準標籤
label = ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']

plane = cv2.imread('plane.jpg')              # 我用的opencv
plane = cv2.cvtColor(plane, cv2.COLOR_BGR2RGB) # opencv讀的數據格式是BGR,因此轉爲RGB

plane = (plane - 127.5) / 127.5    # 二話不說,保持模型輸入數據的機率分佈,先作歸一化
plane = cv2.resize(plane, (32,32)) # 圖片縮小到32x32,和模型的輸入保持一致
plane = torch.Tensor(plane)        # 轉換成 tensor
plane = plane.view(1,32,32,3)      # 增長一個維度
plane = plane.repeat(16,1,1,1)     # 我就用一張圖片,爲了知足模型的形狀16,我複製了16次     
plane = plane.permute([0,3,1,2])   # 雖然torch也有 像TF那樣的transpose,可是隻能操做2D

device = torch.device('cuda')      # 先定義一個cuda設備對象
plane = plane.to(device)           # 咱們訓練集用的cuda, 因此預測數據也要轉爲cuda

正式輸入模型預測:

model = torch.load('model.pkl')    # 讀取出 咱們訓練到最後整個模型
# 說明一下,若是你的預測是另外一個腳本中,class ResNet 的代碼定義部分也要複製過來

out = model(plane)                 # 預測結果,形狀爲[16,10] 16個樣本,10個預測機率,
label_indexes = out.argmax(dim=1)  # 取10個機率最大值的索引。 (1軸),形狀爲 [16,1]
print(label_indexes)
for i in label_indexes:            # i爲每一個樣本預測的最大機率值 的 索引位置。
    print(label[i])                # 拿着預測標籤的索引  去 真實標籤中找到真實標籤
相關文章
相關標籤/搜索