Keras和PyTorch的視覺識別與遷移學習對比

編譯:yxy

出品:ATYUN訂閱號

在上一篇文章中,我們簡述了Keras和PyTorch的區別,旨在幫助你選擇更適合你需求的框架。現在,我們進行實戰進行。我們將讓Keras和PyTorch互相較量以展示他們的優劣。我們使用的問題是:區分異形和鐵血戰士。

圖像分類,是計算機視覺任務之一。由於在大多數情況下從頭開始訓練很難實施(因爲它很需要數據),我們使用在ImageNet上預訓練的ResNet-50進行遷移學習。我們儘可能貼合實際地展示概念差異和慣例。同時,我們的代碼保持簡約,使其清晰、易於閱讀和重用。

那麼,什麼是遷移學習?爲什麼使用ResNet-50?實際上,很少有人從頭開始訓練整個卷積網絡(使用隨機初始化),因爲足夠大小的數據集相對罕見的。因此,通常在非常大的數據集(例如ImageNet,其包含具有1000個類別的120萬個圖像)上預訓練ConvNet,然後使用ConvNet作爲自己任務的初始化或固定特徵提取器(出自Andrej Karpathy,CS231n)。

遷移學習是對在給定任務上訓練的網絡進行微小調整以執行另一個類似任務的過程。在我們的案例中,我們使用經過訓練的ResNet-50模型對ImageNet數據集中的圖像進行分類。這足以學習很多可能在其他視覺任務中有用的紋理和模式,甚至可以辨別異形大戰鐵血戰士中的異形。這樣,我們使用更少的計算能力來取得更好的結果。

在我們的例子中,我們以最簡單的方式做到:

  • 保持預訓練的卷積層(即,所謂的特徵提取器),保持它們的權重不變。
  • 刪除原始稠密層,並用我們用於訓練的新稠密層替換。

那麼,應該選擇哪個網絡作爲特徵提取器?

ResNet-50是很流行的ImageNet圖像分類模型(AlexNet,VGG,GoogLeNet,Inception,Xception也很流行的模型)。它是一種基於殘餘連接的50層深度神經網絡架構,殘連接差是爲每層增加修改的連接(注意,是修改)。

讓比賽開始吧!

我們通過七個步驟完成Alien vs. Predator任務:

  1. 準備數據集
  2. 導入依賴項
  3. 創建數據生成器
  4. 創建網絡
  5. 訓練模型
  6. 保存並加載模型
  7. 對樣本測試圖像進行預測

我們在Jupyter Notebooks(Keras-ResNet50.ipynb,PyTorch-ResNet50.ipynb)中使用Python代碼補充了這篇博客文章。這種環境比裸腳本更便於原型設計,因爲我們可以逐個單元地執行它並將峯值輸出到輸出中。

好的,我們走吧!

0.準備數據集

我們通過谷歌搜索“alien”和“predator”來創建數據集。我們保存了JPG縮略圖(大約250×250像素)並手動過濾了結果。以下是一些例子:

我們將數據分爲兩部分:

  • 訓練數據(每類347個樣本) – 用於訓練網絡。
  • 驗證數據(每類100個樣本) – 在訓練期間不使用,以檢查模型在以前沒有看過的數據上的性能。

Keras要求以下列方式將數據集放在文件夾中:

|-- train
    |-- alien
    |-- predator
|-- validation
    |-- alien
    |-- predator

如果要查看將數據組織到目錄中的過程,可以查看data_prep.ipynb文件。

Kaggle下載數據集:https://www.kaggle.com/pmigdal/alien-vs-predator-images

1.導入依賴項

我們假設你有Python 3.5+,Keras 2.2.2(帶有TensorFlow 1.10.1後端)和PyTorch 0.4.1。具體需求可查看requirements.txt文件。

首先,我們需要導入所需的模塊。我們將Keras,PyTorch和他們共有的代碼(兩者都需要)分開。

共有

import numpy as np
import matplotlib.pyplot as plt
from PILimport Image
%matplotlib inline

KERAS

import keras
from keras.preprocessing.imageimport ImageDataGenerator
from keras.applicationsimport ResNet50
from keras.applications.resnet50import preprocess_input
from kerasimport Model, layers
from keras.modelsimport load_model, model_from_json

PYTORCH

import torch
from torchvisionimport datasets, models, transforms
import torch.nn as nn
from torch.nnimport functional as F
import torch.optim as optim

我們可以分別鍵入keras .__ version__ 和torch .__ version__來檢查框架的版本。

2.創建數據生成器

通常,圖像不能一次全部加載,因爲這樣內存會不夠。並且,我們希望通過一次處理少量圖像來從GPU中受益。因此,我們使用數據生成器分批加載圖像(例如,一次32個圖像)。每次遍歷整個數據集都稱爲一個訓練週期(epoch,或者說一次迭代)。

我們還使用數據生成器進行預處理:我們調整圖像大小並將其標準化,以使它們像ResNet-50一樣(224 x 224像素,帶有縮放的顏色通道)。最後但並非最不重要的是,我們使用數據生成器隨機擾動圖像:

執行此類更改稱爲數據增強(data augmentation)。我們用它來告訴神經網絡,哪種變化無關緊要。或者,換句話說,我們通過基於原始數據集生成的新圖像來獲得可能無限大的數據集。

幾乎所有的視覺任務都在不同程度上受益於訓練的數據增加。在我們的案例中,我們隨機剪切,縮放和水平翻轉我們的異形和鐵血戰士。

因此,我們創建生成器的步驟是:

  • 從文件夾加載數據
  • 標準化數據(訓練和驗證)
  • 數據增強(僅限訓練)

KERAS

train_datagen= ImageDataGenerator(
    shear_range=10,
    zoom_range=0.2,
    horizontal_flip=True,
    preprocessing_function=preprocess_input)
train_generator= train_datagen.flow_from_directory(
    'data/train',
    batch_size=32,
    class_mode='binary',
    target_size=(224,224))
validation_datagen= ImageDataGenerator(
    preprocessing_function=preprocess_input)
validation_generator= validation_datagen.flow_from_directory(
    'data/validation',
    shuffle=False,
    class_mode='binary',
    target_size=(224,224))

PYTORCH

normalize= transforms.Normalize(mean=[0.485,0.456,0.406],
                                 std=[0.229,0.224,0.225])
data_transforms= {
    'train':
        transforms.Compose([
            transforms.Resize((224,224)),
            transforms.RandomAffine(0, shear=10, scale=(0.8,1.2)),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            normalize]),
    'validation':
        transforms.Compose([
            transforms.Resize((224,224)),
            transforms.ToTensor(),
            normalize])}
image_datasets= {
    'train':
        datasets.ImageFolder('data/train', data_transforms['train']),
    'validation':
        datasets.ImageFolder('data/validation', data_transforms['validation'])}
dataloaders= {
    'train':
        torch.utils.data.DataLoader(
            image_datasets['train'],
            batch_size=32,
            shuffle=True,
            num_workers=4),
    'validation':
        torch.utils.data.DataLoader(
            image_datasets['validation'],
            batch_size=32,
            shuffle=False,
            num_workers=4)}

在Keras中,你可以使用內置的增強和preprocess_input 方法來標準化圖像,但你無法控制它們的順序。在PyTorch中,必須手動標準化圖像,但你可以以任何你喜歡的方式安排增強。

還有其他細微差別:例如,Keras默認使用邊界像素填充增強圖像的其餘部分(如上圖所示),而PyTorch用黑色。每當一個框架比另一個更好地處理你的任務時,請仔細查看它們是否執行相同的預處理(我幾乎可以肯定他們不同)。

3.創建網絡

下一步是導入預訓練好的ResNet-50模型,這在兩種情況下都是輕而易舉的。我們保持所有ResNet-50的卷積層不變,僅訓練最後兩個完全連接(稠密)層。由於我們的分類任務只有2個類,我們需要調整最後一層(ImageNet有上千個)。

也就是說,我們:

  • 加載預訓練好的網絡,減掉頭部並固定權重,
  • 添加自定義稠密層(我們選擇128個神經元的隱藏層),
  • 設置優化器和損失函數。

KERAS

conv_base= ResNet50(include_top=False,
                     weights='imagenet')
for layerin conv_base.layers:
    layer.trainable= False
x= conv_base.output
x= layers.GlobalAveragePooling2D()(x)
x= layers.Dense(128, activation='relu')(x)
predictions= layers.Dense(2, activation='softmax')(x)
model= Model(conv_base.input, predictions)
optimizer= keras.optimizers.Adam()
model.compile(loss='sparse_categorical_crossentropy',
              optimizer=optimizer,
              metrics=['accuracy'])

PYTORCH

device= torch.device("cuda:0" if torch.cuda.is_available()else "cpu")
model= models.resnet50(pretrained=True).to(device)
for paramin model.parameters():
    param.requires_grad= False
model.fc= nn.Sequential(
    nn.Linear(2048,128),
    nn.ReLU(inplace=True),
    nn.Linear(128,2)).to(device)
criterion= nn.CrossEntropyLoss()
optimizer= optim.Adam(model.fc.parameters())

我們很容易從Keras和PyTorch加載ResNet-50。他們還提供了其他許多有名的預訓練架構。那麼,它們有什麼區別?

在Keras中,我們可以僅導入特徵提取層,不加載外來數據(include_top = False)。然後,我們使用基本模型的輸入和輸出以功能性的方式創建模型。然後我們使用 model.compile(…)將損失函數,優化器和其他指標放入其中。

在PyTorch中,模型是一個Python對象。在models.resnet50中,稠密層存儲在model.fc屬性中。我們重寫它們。損失函數和優化器是單獨的對象。對於優化器,我們需要顯式傳遞我們希望它更新的參數列表。

在PyTorch中,我們應該使用.to(device)方法顯式地指定要加載到GPU的內容。每當我們打算在GPU上放置一個對象時,我們都必須編寫它。

凍結層的工作方式與此類似。然而,在 Keras 的批量標準化層中,它被破壞了 (截至當前版本,詳見http://blog.datumbox.com/the-batch-normalization-layer-of-keras-is-broken/)。也就是說,無論如何都會修改一些層,即使 trainable = False。

Keras和PyTorch以不同的方式處理log-loss。

在Keras中,網絡預測概率(具有內置的softmax函數),其內置成本函數假設它們使用概率工作。

在PyTorch中我們更加自由,但首選的方法是返回logits。這是出於數值原因,執行softmax然後log-loss意味着執行多餘的log(exp(x))操作。因此,我們使用LogSoftmax(和NLLLoss)代替使用softmax,或者將它們組合成一個nn.CrossEntropyLoss 損失函數。

4.訓練模型

我們繼續進行最重要的一步 – 模型訓練。我們需要傳遞數據,計算損失函數並相應地修改網絡權重。雖然Keras和PyTorch在數據增強方面已經存在一些差異,但代碼長度差不多。但在訓練這一步,差的就很多了。

在這裏,我們:

  • 訓練模型,
  • 測量損失函數(log-loss)和訓練和驗證集的準確性。

KERAS

history= model.fit_generator(
    generator=train_generator,
    epochs=3,
    validation_data=validation_generator)

PYTORCH

def train_model(model, criterion, optimizer, num_epochs=3):
    for epochin range(num_epochs):
        print('Epoch {}/{}'.format(epoch+1, num_epochs))
        print('-' * 10)
        for phasein ['train','validation']:
            if phase== 'train':
                model.train()
            else:
                model.eval()
            running_loss= 0.0
            running_corrects= 0
            for inputs, labelsin dataloaders[phase]:
                inputs= inputs.to(device)
                labels= labels.to(device)
                outputs= model(inputs)
                loss= criterion(outputs, labels)
                if phase== 'train':
                    optimizer.zero_grad()
                    loss.backward()
                    optimizer.step()
                _, preds= torch.max(outputs,1)
                running_loss+= loss.item()* inputs.size(0)
                running_corrects+= torch.sum(preds== labels.data)
            epoch_loss= running_loss/ len(image_datasets[phase])
            epoch_acc= running_corrects.double()/ len(image_datasets[phase])
            print('{} loss: {:.4f}, acc: {:.4f}'.format(phase,
                                                        epoch_loss,
                                                        epoch_acc))
    return model
model_trained= train_model(model, criterion, optimizer, num_epochs=3)

在Keras中,model.fit_generator執行訓練……然後就沒了!在Keras的訓練就是這麼簡單。正如你在notebook中所看到的,Keras還爲我們提供了進度條和計時功能。但如果你想做任何非標準的事情,那你就有的頭疼了。

PyTorch與此截然不同。這裏一切都是明確的。你需要更多行代碼來構建基本訓練,但你可以隨意更改和自定義你想要的所有內容。

讓我們剖析下PyTorch訓練代碼。我們有嵌套循環,迭代:

  • 迭代次數,
  • 訓練和驗證階段,
  • 批次。

epoch循環很好理解,只是重複裏面的代碼。訓練和驗證階段:

  • 一些特殊的層,如批量標準化(出現在ResNet-50中)和dropout(在ResNet-50中不存在),在訓練和驗證期間的工作方式不同。我們分別通過model.train()和model.eval()設置它們的行爲。
  • 當然,我們使用不同的圖像進行訓練和驗證。
  • 最重要但也很容易理解的事情:我們只在訓練期間訓練網絡。magic命令optimizer.zero_grad(),loss.backward()和 optimizer.step()(按此順序)完成工作。如果你理解什麼是反向傳播,你就會欣賞它們的優雅。

我們負責計算迭代的損失並打印。

5.保存並加載模型

保存

一旦我們的網絡經過訓練,通常這需要很高的計算和時間成本,最好將其保存以備以後使用。一般來說,有兩種類型保存:

  • 將整個模型結構和訓練權重(以及優化器狀態)保存到文件中,
  • 將訓練過的權重保存到文件中(將模型架構保留在代碼中)。

你可以隨意選擇。在這裏,我們保存模型。

KERAS

# architecture and weights to HDF5
model.save('models/keras/model.h5')
# architecture to JSON, weights to HDF5
model.save_weights('models/keras/weights.h5')
withopen('models/keras/architecture.json','w') as f:
    f.write(model.to_json())

PYTORCH

torch.save(model_trained.state_dict(),'models/pytorch/weights.h5')

兩個框架中都有一行代碼就足夠了。在Keras中,可以將所有內容保存到HDF5文件,或將權重保存到HDF5,並將架構保存到可讀的json文件中。另外,你可以加載模型並在瀏覽器中運行它。

目前,PyTorch創建者建議僅保存權重。他們不鼓勵保存整個模型,因爲API仍在不斷髮展。

加載

加載模型和保存一樣簡單。你需要記住你選擇的保存方法和文件路徑。

KERAS

# architecture and weights from HDF5
model= load_model('models/keras/model.h5')
# architecture from JSON, weights from HDF5
withopen('models/keras/architecture.json') as f:
    model= model_from_json(f.read())
model.load_weights('models/keras/weights.h5')

PYTORCH

model= models.resnet50(pretrained=False).to(device)
model.fc= nn.Sequential(
    nn.Linear(2048,128),
    nn.ReLU(inplace=True),
    nn.Linear(128,2)).to(device)
model.load_state_dict(torch.load('models/pytorch/weights.h5'))

在Keras中,我們可以從JSON文件加載模型,而不是在Python中創建它(至少在我們不使用自定義層時不需要這樣)。這種序列化方便了轉換模型。

PyTorch可以使用任何Python代碼。所以我們必須在Python中重新創建一個模型。在兩個框架中加載模型權重比較類似。

6.對測試樣本圖像進行預測

爲了公平地檢查我們的解決方案的質量,我們要求模型預測未用於訓練的圖像中怪物的類型。我們可以使用驗證集或者任何其他圖像。

在這裏,我們:

  • 加載和預處理測試圖像
  • 預測圖像類別
  • 顯示圖像和預測

共有

validation_img_paths= ["data/validation/alien/11.jpg",
                        "data/validation/alien/22.jpg",
                        "data/validation/predator/33.jpg"]
img_list= [Image.open(img_path)for img_pathin validation_img_paths]

KERAS

validation_batch= np.stack([preprocess_input(np.array(img.resize((img_size, img_size))))
                             for imgin img_list])
pred_probs= model.predict(validation_batch)

PYTORCH

validation_batch= torch.stack([data_transforms['validation'](img).to(device)
                                for imgin img_list])
pred_logits_tensor= loaded_model(validation_batch)
pred_probs= F.softmax(pred_logits_tensor, dim=1).cpu().data.numpy()

共有

fig, axs= plt.subplots(1,len(img_list), figsize=(20,5))
for i, imgin enumerate(img_list):
   ax= axs[i]
   ax.axis('off')
   ax.set_title("{:.0f}% Alien, {:.0f}% Predator".format(100*pred_probs[i,0],
                                                         100*pred_probs[i,1]))
   ax.imshow(img)

像訓練一樣,預測也分批進行(這裏我們一批3個,也可以每批1個)。在Keras和PyTorch中,我們需要加載和預處理數據。新手常見的錯誤是忘記了預處理步驟(包括顏色縮放)。也許方法仍然有效,但會導致糟糕的預測(因爲它能有效地看到相同的形狀,但不能有效看到不同的顏色和對比度)。

在PyTorch中還有兩個步驟,因爲我們需要:

  • 將logits轉換爲概率,
  • 將數據傳輸到CPU並轉換爲NumPy(當我們忘記此步驟時,錯誤消息會很明白的告訴你)。

下面就是我們得到的:

成功了!你也可以使用其他圖像。如果你無法想出任何其他(或任何人),可以嘗試使用你同事的照片。

結論

現在你看到了,Keras和PyTorch在如何定義,修改,訓練,評估和導出標準深度學習模型方面的差異。有些部分,它純粹是針對不同的API約定,而其他部分,則涉及抽象級別之間的基本差異。

Keras在更高級別的抽象上運行。它更加即插即用,通常更簡潔,但這是以靈活性爲代價的。

PyTorch提供更明確和詳細的代碼。在大多數情況下,它意味着可調試和靈活的代碼,只需多費一點時間。然而,PyTorch的訓練更加冗長,但有時這會提供很大的靈活性。

github:https://github.com/deepsense-ai/Keras-PyTorch-AvP-transfer-learning

Kaggle kernels:https://www.kaggle.com/pmigdal/alien-vs-predator-images/kernels

Neptune:https://app.neptune.ml/deepsense-ai/Keras-vs-PyTorch

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章