學習重點
- torchvision包的使用
代碼實現
導入相關的庫
from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
知識點一 torchvision
很多基於Pytorch的工具集都非常好用,比如處理自然語言的torchtext,處理音頻的torchaudio,以及處理圖像視頻的torchvision。
torchvision包含一些常用的數據集、模型、轉換函數。本實驗由他來加載模型和進行數據加載器。
設置有關參數
data_dir = './' # 設置文件的根目錄
data_transforms = {
# 訓練中的數據增強和歸一化
'train': transforms.Compose([
transforms.RandomResizedCrop(224), # 隨機裁剪
transforms.RandomHorizontalFlip(), # 左右翻轉
transforms.ToTensor(), # 變為Tensor格式
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 均值方差歸一化
]),
# 驗證集不增強,僅進行歸一化
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
知識點2
transforms.Compose函數就是將transforms組合在一起;而每一個transforms都有自己的功能。
transforms.Resize(256)是按照比例把圖像最小的一個邊長放縮到256,另一邊按照相同比例放縮。
transforms.RandomResizedCrop(224,scale=(0.5,1.0))是把圖像按照中心隨機切割成224正方形大小的圖片。
transforms.ToTensor() 轉換為tensor格式,這個格式可以直接輸入進神經網絡了。
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])對像素值進行歸一化處理。
進行數據集的加載和預處理工作
數據集加載器
進行訓練集和驗證集的加載,輸入的第一個參數是圖片的路徑,輸入的第二個參數是轉換的類型(由上所示)
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train','val']}
由於數據集的格式解壓后是這樣的:
這種情況需要我們用datasets.ImageFolder中的方法
前面的dataset只是路徑的集合,需要創建加載器便於加載,其中batch_size:一批輸出的數據,shuffle:是否打亂,這對於用IMageFolder加載的數據是十分有必要的,因為有可能接連幾個數據都屬於同一類,不利於訓練
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train','val']}
儲存有用的常量
dataset_sizes = {x: len(image_datasets[x]) for x in ['train','val']} # 得到數據集的長度
class_names = image_datasets['train'].classes # 得到含有類名稱的列表
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 創建運行方式
class_nums = len(class_names) # 設置待分類的個數
記載預訓練模型並改寫
# 從torchvision中載入resnet18模型,並且加載預訓練
model_conv = torchvision.models.resnet18(pretrained=True)
# 這里的目的是防止預訓練模型的准確結果隨訓練進行變化。
for param in model_conv.parameters():
param.requires_grad = False
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, class_nums)
這里的最后,我們把最后的全連接層改成我們分類需要的全連接層。
自定義模型和損失器
model_conv = model_conv.to(device)
criterion = nn.CrossEntropyLoss()
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
對於exp_lr_scheduler
,有:
- optimizer (Optimizer):要更改學習率的優化器;
- step_size(int):每訓練step_size個epoch,更新一次參數;
- gamma(float):更新lr的乘法因子;
- last_epoch (int):最后一個epoch的index,如果是訓練了很多個epoch后中斷了,繼續訓練,這個值就等於加載的模型的epoch。默認為-1表示從頭開始訓練,即從epoch=1開始。
和optimizer
模型的訓練
這里的一般思路是,每一輪詞都對訓練集進行訓練,對驗證集
看似是訓練模型:
注意細節:
細節一:對於每一輪次設置計時器
since = time.time()
time_elapsed = time.time() - since
細節二:每一輪次設置輸出信息
細節三:注意將輸入的數據加載到gpu上
inputs = inputs.to(device)
labels = labels.to(device)
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
since = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# 每一個epoch都會進行一次驗證
for phase in ['train','val']:
if phase == 'train':
model.train() # 設置模型為訓練模式
else:
model.eval() # 設置模型為驗證模式
running_loss = 0.0
running_corrects = 0
# 迭代所有樣本
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
# 將梯度歸零
optimizer.zero_grad()
# 前向傳播網絡,僅在訓練狀態記錄參數的梯度從而計算loss
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
# 反向傳播來進行梯度下降
if phase == 'train':
loss.backward()
optimizer.step()
# 統計loss值
running_loss += loss.item() * inputs.size(0) #item
running_corrects += torch.sum(preds == labels.data)
if phase == 'train':
# 進行優化器的學習率優化
scheduler.step()
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects.double() / dataset_sizes[phase]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(
phase, epoch_loss, epoch_acc))
# 依據驗證集的准確率來更新最優模型
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
print()
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(
time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
# 載入最優模型
model.load_state_dict(best_model_wts)
return model
保存最好的模型
torch.save(model_ft, 'xybw_model')
最終的結果到達了很高的正確率
進行test集的加載和預測
進行類別轉類別名的測試
id2class = {i:class_names[i] for i in range(len(class_names))}
這里進行轉類別名測試的時候犯了難,如果利用ImageFolder的方式進行圖片讀取,這樣會自動將用PIL格式讀取,導致第64張圖片無法被讀取到,這里我們用dataset自定義數據加載器
from torch.utils import data
import cv2 as cv
class Animals(data.Dataset):
def __init__(self, root):
imgs=os.listdir(root)
self.all_image_paths =[os.path.join(root,k) for k in imgs]
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
self.mean = np.array(mean).reshape((1, 1, 3))
self.std = np.array(std).reshape((1, 1, 3))
def __getitem__(self, index):
img = cv.imread(self.all_image_paths[index])
img = cv.resize(img, (224, 224))
img = img / 255.
img = (img - self.mean) / self.std
img = np.transpose(img, [2, 0, 1])
#label = self.all_image_labels[index]
img = torch.tensor(img, dtype=torch.float32)
return self.all_image_paths[index], img
def __len__(self):
return len(self.all_image_paths)
自己定義的dataset類需要繼承: Dataset
需要實現必要的魔法方法:
-- __init__
魔法方法里面進行讀取數據文件
-- __getitem__
魔法方法進行支持下標訪問
-- __len__
魔法方法返回自定義數據集的大小,方便后期遍歷
之前之所以要設置dataloader的原因,就是要考慮批次(一次讀入幾組數據)的問題,是否打亂等問題,而在本實驗中,最后進行測試集的測試沒必要
實例化數據集
dataset = Animals('./test')
這個函數卡了好久的bug現在梳理一下。。。
img = img.unsqueeze(0).to(device)
這里用unsqueeze,是因為這是應該是因為網絡的接收輸入是一個mini-batch,image unsqueeze后第一個維度是留給batch size的即使要輸入1,也要輸入[[1]]
to(device)原因易知
pred = pred.cpu().numpy()[0]
得到結果是gpu上的Tensor,需要先到cpu上,在轉化成numpy還要脫出一個
def model_predict(img, model, device):
img = img.unsqueeze(0).to(device)
id2class = {i:class_names[i] for i in range(len(class_names))}
model.eval()
outputs = model(img)
_, pred = torch.max(outputs, 1)
pred = pred.cpu().numpy()[0]
return id2class[pred]
最終進行預測
for i in range(len(dataset)):
k, j =dataset[i]
print(k)
print(model_predict(j, model_ft, device))
思考
dataloader與dataset
之前之所以要設置dataloader的原因,就是要考慮批次(一次讀入幾組數據)的問題,是否打亂等問題,而在本實驗中,最后進行測試集的測試沒必要,其實前面學sklearn的時候接觸過,就是最后忘了(
torch.utils.data.Dataset 重要的特性是有__getitem__
和__len__
方法,這意味着可以用 value[index]
的方式訪問內部元素(可以當作列表用)。
torch.utils.data.DataLoader 使用時,我們首先把torch.utils.data.Dataset類當作一個參數傳遞給 torch.utils.data.DataLoader類,得到一個數據加載器,這個數據加載器每次可以返回一個 Batch 的數據供模型訓練使用。
Dataset子類 其實就是一個靜態的數據池,這個數據池支持我們用 索引 得到某個數據,想要讓這個數據池流動起來,源源不斷地輸出 Batch 還需要下一個工具 DataLoader類 。所以我們把創建的 Dataset子類 當參數傳入 即將構建的DataLoader類才是使用Dataset子類最終目。
以上內容來自:https://www.jianshu.com/p/4818a1a4b5bd 作者:夜和大帝,寫得太好了。
model.eval和with torch.no_grad()
在PyTorch中進行validation時,會使用model.eval()切換到測試模式,在該模式下,
主要用於通知dropout層和batchnorm層在train和val模式間切換
在train模式下,dropout網絡層會按照設定的參數p設置保留激活單元的概率(保留概率=p); batchnorm層會繼續計算數據的mean和var等參數並更新。
在val模式下,dropout層會讓所有的激活單元都通過,而batchnorm層會停止計算和更新mean和var,直接使用在訓練階段已經學出的mean和var值。
該模式不會影響各層的gradient計算行為,即gradient計算和存儲與training模式一樣,只是不進行反傳(backprobagation)
而with torch.no_grad()則主要是用於停止autograd模塊的工作,以起到加速和節省顯存的作用,具體行為就是停止gradient計算,從而節省了GPU算力和顯存,但是並不會影響dropout和batchnorm層的行為。