李宏毅ML_Spring2021HW01學(xué)習(xí)記錄
李宏毅ML_2021Spring_HW1
寫(xiě)在前面
可能會(huì)有一些小錯(cuò)誤,會(huì)持續(xù)檢查和更正的
題目如下

Step1. 導(dǎo)入相關(guān)庫(kù)
# 導(dǎo)入PyTorch相關(guān)庫(kù)
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
# 導(dǎo)入數(shù)據(jù)處理相關(guān)庫(kù)
import numpy as np
import csv
import os
# 導(dǎo)入繪圖相關(guān)庫(kù)
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure
myseed = 42069
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(myseed)
torch.manual_seed(myseed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(myseed)
這里設(shè)置隨機(jī)數(shù)種子是為了保證實(shí)驗(yàn)可復(fù)現(xiàn)性,定義一個(gè)固定的隨機(jī)種子,種子是隨機(jī)數(shù)生成器的初始值,只要種子相同,每次運(yùn)行程序時(shí)生成的隨機(jī)數(shù)序列就會(huì)完全一樣。
torch.backends.cudnn.deterministic = True
- 強(qiáng)制CuDNN使用確定性算法。CuDNN在某些情況下為了追求速度,可能會(huì)使用非確定性的算法(即在相同的輸入下,多次運(yùn)行的結(jié)果可能略有差異)。設(shè)置為
True可以確保結(jié)果的一致性,但可能會(huì)犧牲一點(diǎn)點(diǎn)性能
torch.backends.cudnn.benchmark = False
- 禁用CuDNN的自動(dòng)尋找最優(yōu)卷積算法的功能。當(dāng)輸入數(shù)據(jù)的尺寸固定時(shí),CuDNN可以在第一次運(yùn)行時(shí)測(cè)試多種卷積算法,并選擇最快的一種,但這本身就是一個(gè)隨機(jī)的過(guò)程,會(huì)導(dǎo)致不確定性,將其設(shè)置為
False可以確保每次都使用相同的算法
np.random.seed(myseed)
- 設(shè)置NumPy庫(kù)的隨機(jī)數(shù)生成器種子,影響所有使用
np.random的操作
torch.manual_seed(myseed)
- 設(shè)置PyTorch的隨機(jī)數(shù)生成器種子,影響PyTorch中的隨機(jī)操作
設(shè)置隨機(jī)數(shù)種子也是為了保證可重現(xiàn)性
Step2. 一些函數(shù) (Ulitity不知道咋翻譯)
如果GPU能用,就用GPU,不然就用CPU
def get_device():
return "cuda" if torch.cuda.is_available() else "cpu"
繪制模型在訓(xùn)練過(guò)程中的學(xué)習(xí)曲線,即訓(xùn)練損失(train loss)和驗(yàn)證損失(dev loss)隨訓(xùn)練步數(shù)變化的趨勢(shì)。
def plot_learning_curve(loss_record, title = ''):
total_steps = len(loss_record['train'])
x_1 = range(total_steps)
x_2 = x_1[::len(loss_record['train']) // len(loss_record['dev'])]
figure(figsize = (6, 4))
plt.plot(x_1, loss_record['train'], c = 'tab:red', label = 'train')
plt.plot(x_2, loss_record['dev'], c = 'tav:cyan', label = 'dev')
plt.ylim(0.0, 5.)
plt.xlabel('Training steps')
plt.ylabel('MES loss')
plt.title('Learning curve of{}'.format(title))
plt.legend()
plt.show()
- 它接收一個(gè)名為
loss_record的字典,這個(gè)字典存了兩個(gè)列表:一個(gè)是每一步(batch)訓(xùn)練的損失(train loss),另一個(gè)是每一輪(epoch)訓(xùn)練結(jié)束后的驗(yàn)證損失(dev loss) - 它為訓(xùn)練步驟創(chuàng)建一個(gè)x軸
x_1 x_2 = x_1[::len(loss_record['train'])] // len(loss_record['dev'])是一個(gè)小技巧。因?yàn)轵?yàn)證損失的記錄頻率遠(yuǎn)(一個(gè)epoch)低于訓(xùn)練損失(一個(gè)batch)(訓(xùn)練時(shí)每批數(shù)據(jù)都記,驗(yàn)證時(shí)跑完一整輪才記一次),這行代碼能巧妙地在x軸上找到對(duì)應(yīng)的位置來(lái)畫(huà)驗(yàn)證損失的點(diǎn),確保兩條線能夠?qū)R。
評(píng)估模型的最終預(yù)測(cè)能力,通過(guò)畫(huà)散點(diǎn)圖的方式將模型的“預(yù)測(cè)值”與“真實(shí)值”進(jìn)行比較
def plot_pred(dv_set, model, device, lim = 35, preds = None, targets = None):
if preds is None or targets is None:
model.eval()
preds, targets = [], []
for x, y in dv_set:
x, y = x.to(device), y.to(device)
with torch.no_grad():
pred = model(x)
preds.append(pred.detach().cpu())
targets.append(y.detach().cpu())
preds = torch.cat(preds, dim = 0).numpy()
targets = torch.cat(targets, dim = 0).numpy()
figure(figsize = (5, 5))
plt.scatter(targets, preds, c = 'r', alpha = 0.5)
plt.plot([-0.2, lim], [-0.2, lim], c = 'b') # 直線 y = x
plt.xlim(-0.2, lim)
plt.ylim(-0.2, lim)
plt.xlabel('ground truth value')
plt.ylabel('predicted value')
plt.title('Ground Truth v.s. Prediction')
plt.show()
moedl.eval():將模型切換到"評(píng)估模式",這會(huì)關(guān)閉一些只在訓(xùn)練時(shí)才開(kāi)啟的功能(比如Dropout)with torch.no_grad():告訴PyTorch在這個(gè)代碼塊中不要計(jì)算梯度,因?yàn)槲覀兪窃谧鲱A(yù)測(cè),不是在訓(xùn)練,這樣做可以節(jié)省計(jì)算資源和內(nèi)存,運(yùn)行得更快。不然的話PyTorch會(huì)繼續(xù)構(gòu)建計(jì)算圖累加到原來(lái)的結(jié)果上,就糟糕了- 它會(huì)遍歷驗(yàn)證集
(dv_set)的每一個(gè)批次(batch),用模型(model)去進(jìn)行預(yù)測(cè) x, y = x.to(device), y.to(device)將數(shù)據(jù)和標(biāo)簽移動(dòng)到指定的計(jì)算設(shè)備上preds.append(pred.detach().cpu()): 將預(yù)測(cè)結(jié)果從計(jì)算設(shè)備(GPU)移回 CPU,并從計(jì)算圖中分離(detach()),然后存入列表。targets.append(y.detach().cpu()): 同樣處理真實(shí)標(biāo)簽。torch.cat(..., dim=0).numpy(): 將所有批次的預(yù)測(cè)值和真實(shí)值拼接成一個(gè)大的張量,然后轉(zhuǎn)換為 NumPy 數(shù)組,以便 Matplotlib 繪圖。- 然后,會(huì)用
plt.scatter()繪制散點(diǎn)圖- x軸代表真實(shí)值
- y軸代表模型的預(yù)測(cè)值
- 同時(shí)會(huì)畫(huà)一條呈45度角的藍(lán)色對(duì)角線,這條線代表"完美預(yù)測(cè)"(預(yù)測(cè)值 = 真實(shí)值)
Step3. 預(yù)處理
我們已經(jīng)有了數(shù)據(jù)集了:
train:訓(xùn)練集dev:驗(yàn)證集test:測(cè)試集
Dataset -- 圖書(shū)管理員
class COVID19Dataset(Dataset):
def __init__(self, path, mode = 'train', target_only = False):
self.mode = mode
# 把數(shù)據(jù)讀到NumPy的arrays中
with open(path, 'r') as fp:
data = list(csv.reader(fp))
data = np.array(data[1:])[:, 1:].astype(float)
if not target_only:
feats = list(range(93))
else:
# 使用 40 個(gè)州的特征,以及索引為 57 和 75 的兩個(gè)tested_positive特征
feats = list(range(40)) + [57, 75]
if mode == 'test':
# 測(cè)試數(shù)據(jù) Testing data
# data: 893 x 93 40個(gè)列用one-hot表示州 day1(18), day2(18), day3(17)
data = data[:, feats]
self.data = torch.FloatTensor(data)
else:
# 訓(xùn)練數(shù)據(jù) Training data (train/dev sets)
# data: 2700 x 94 day1(18), day2(18), day3(18)
target = data[:, -1]
data = data[:, feats]
# 分割訓(xùn)練數(shù)據(jù)為訓(xùn)練集和驗(yàn)證集
if mode == 'train':
indices = [i for i in range(len(data)) if i % 10 != 0]
elif mode == 'dev':
indices = [i for i in range(len(data)) if i % 10 == 0]
# 把數(shù)據(jù)變成PyTorch的tensors
self.data = torch.FloatTensor(data[indices])
self.target = torch.FloatTensor(target[indices])
#數(shù)據(jù)歸一化(有的Error Surface梯度下降可能比較困難,歸一化使得訓(xùn)練更加順利)
self.data[:, 40:] = (self.data[:, 40:] - self.data[:, 40:].mean(dim = 0, keepdim = True)) / self.data[:, 40:].std(dim = 0, keepdim = True)
self.dim = self.data.shape[1]
def __getitem__(self, index):
if self.mode in ['train', 'dev']:
# 訓(xùn)練
return self.data[index], self.target[index]
else:
# 測(cè)試
return self.data[index]
def __len__(self):
#
return len(self.data)
注意
Dataset是一個(gè)抽象類(lèi),不能創(chuàng)造實(shí)例,只能用來(lái)繼承
圖書(shū)管理員從來(lái)都不是特定的某個(gè)人,只能得到對(duì)應(yīng)的職權(quán)
讀取和解析csv文件
path:csv文件的路徑
mode:數(shù)據(jù)集的模式,有三種可能的值:train訓(xùn)練集 ,dev驗(yàn)證集,test測(cè)試集,默認(rèn)為train
target_only:一個(gè)布爾值,用于決定是否只使用部分特定的特征,還是使用全部特征
with open(path, 'r') as fp:python打開(kāi)指定文件的方式,with語(yǔ)句可以確保文件在操作結(jié)束后被正確關(guān)閉
data = np.array(data[1:])[:, 1:].astype(float)跳過(guò)第0行和第0列,因?yàn)榈?code>0行是表頭不是數(shù)據(jù),第0列是樣本id不是特征信息,然后讀進(jìn)來(lái)的數(shù)據(jù)是字符類(lèi)型,轉(zhuǎn)換一下方便后續(xù)計(jì)算
特征選擇
如果使用全部特征,就把93個(gè)特征列全列進(jìn)來(lái),不然的話,就按照題目要求使用前40個(gè)州的特征以及索引為57和75的兩個(gè)tested_positive特征
區(qū)分不同模式(訓(xùn)練,驗(yàn)證,測(cè)試)
對(duì)于測(cè)試集
data = data[:, feats]
self.data = torch.FloatTensor(data)
這里我們把需要的列取出來(lái)并把它從np.ndarry轉(zhuǎn)換成tensors
對(duì)于訓(xùn)練/驗(yàn)證集
target = data[:, -1]
data = data[:, feats]
target就是我們理想的輸出值
data同上,選出用于訓(xùn)練的特征列
劃分?jǐn)?shù)據(jù)集(train)和驗(yàn)證集(dev)
if mode == 'train':
indices = [i for i in range(len(data)) if i % 10 != 0]
elif mode == 'dev':
indices = [i for i in range(len(data)) if i % 10 == 0]
self.data = torch.FloatTensor(data[indices])
self.target = torch.FloatTensor(target[indices])
這里沒(méi)有直接分割data數(shù)組,而是通過(guò)生成索引indices列表的方式來(lái)分割數(shù)據(jù)
-
i % 10 != 0:i % 10是求 i 除以 10 的余數(shù)。這個(gè)條件的意思是“如果行號(hào)不能被 10 整除”。所以,行號(hào)為 0-8, 10-18, 20-28... 的數(shù)據(jù)會(huì)被選為訓(xùn)練集(每 10 條里選 9 條)。 -
i % 10 == 0:相反,行號(hào)為 0, 10, 20... 的數(shù)據(jù)會(huì)被選為驗(yàn)證集(每 10 條里選 1 條)。
data[indices]: 最后,用這個(gè) indices 列表一次性地從 data 和 target 中取出所有對(duì)應(yīng)的行,完成數(shù)據(jù)集的切分。
數(shù)據(jù)標(biāo)準(zhǔn)化
-
切片
self.data[:, 40:]:這里的 40:` 表示“從第 40 列開(kāi)始,取到最后一列”。為什么從 40 開(kāi)始?因?yàn)楦鶕?jù)數(shù)據(jù)描述,前 40 列是代表不同州的特征(one-hot 編碼),它們的值只有 0 或 1,不需要標(biāo)準(zhǔn)化。而后面的列是數(shù)值型特征,數(shù)值范圍可能很大,需要標(biāo)準(zhǔn)化。 -
公式
(x - mean) / std:這就是標(biāo)準(zhǔn)的 Z-score 標(biāo)準(zhǔn)化公式。它會(huì)把數(shù)據(jù)的均值變?yōu)?0,標(biāo)準(zhǔn)差變?yōu)?1,使得所有特征都在一個(gè)相似的尺度上。-
.mean(dim=0):沿著列(維度0)的方向計(jì)算每一列的平均值。 -
.std(dim=0):沿著列的方向計(jì)算每一列的標(biāo)準(zhǔn)差。
-
def __getitem__(self, index)
- 作用:根據(jù)
DataLoader傳過(guò)來(lái)的一個(gè)具體的索引號(hào)(index),從數(shù)據(jù)集中取出那一條對(duì)應(yīng)的數(shù)據(jù) - 根據(jù)這個(gè)代碼,
DataLoader會(huì)傳給它一個(gè)數(shù)字index, 如0, 1, 2... - 如果當(dāng)前是
train或者dev模式,模型需要數(shù)據(jù)(問(wèn)題)和標(biāo)簽(答案)來(lái)進(jìn)行學(xué)習(xí)和評(píng)估,所以它會(huì)返回一個(gè)元組(tuple),包含兩條信息self.data[index]和self.target[index](第index條的特征數(shù)據(jù)和第index條對(duì)應(yīng)的目標(biāo)值) - 如果是
test模式,我們只有數(shù)據(jù),沒(méi)有標(biāo)簽,因?yàn)榇鸢甘切枰P腿ヮA(yù)測(cè)的,所以只返回self.data[index]
def __len__(self)
- 在
__init__方法中,我們已經(jīng)把所有處理好的數(shù)據(jù)都存放在self.data這個(gè)變量里了。 len(self.data)就可以返回樣本總數(shù)(也就是行數(shù))
DataLoader --高效的數(shù)據(jù)搬運(yùn)工
Dataset 就像是整個(gè)圖書(shū)館的藏書(shū)清單和圖書(shū)管理員。它知道總共有多少本書(shū) (__len__),并且你告訴它書(shū)號(hào) (index),它就能幫你準(zhǔn)確地把那一本書(shū)取出來(lái) (__getitem__)。
但是,在訓(xùn)練模型時(shí),我們面臨一個(gè)問(wèn)題:我們不希望一本一本地去借書(shū)(效率太低),也不可能一次性把整個(gè)圖書(shū)館的書(shū)都搬過(guò)來(lái)(內(nèi)存會(huì)爆炸)。
這時(shí),DataLoader 就登場(chǎng)了。它就像一個(gè)超級(jí)智能的物流團(tuán)隊(duì),負(fù)責(zé)高效地從圖書(shū)館那里(Dataset)搬運(yùn)書(shū)籍(數(shù)據(jù))給在辦公室里等著工作的你(模型)。
def prep_dataloader(path, mode, batch_size, n_jobs=0, target_only=False):
''' Generates a dataset, then is put into a dataloader. '''
dataset = COVID19Dataset(path, mode=mode, target_only=target_only) # Construct dataset
dataloader = DataLoader(
dataset, batch_size,
shuffle=(mode == 'train'), drop_last=False,
num_workers=n_jobs, pin_memory=True) # Construct dataloader
return dataloader
這個(gè)函數(shù)是一個(gè)"包裝"函數(shù),它做了兩件事情:
-
創(chuàng)建
Dataset對(duì)象:就是利用我們之前定義的COVID19Dataset類(lèi)創(chuàng)建了一個(gè)數(shù)據(jù)集實(shí)例 -
創(chuàng)建
DataLoader對(duì)象:dataloader = DataLoader(), 它接收上一步創(chuàng)建的dataset,并用一系列參數(shù)對(duì)它進(jìn)行配置,把它變成一個(gè)數(shù)據(jù)加載器
DataLoader的核心參數(shù)
datloader = DataLoader(
dateset, # 1. 數(shù)據(jù)集
batch_size, # 2. 批次大小
shuffle = ... # 3. 是否打亂
drop_last = False, # 4. 是否丟棄最后一個(gè)不完整的批次
num_workers = ... # 5. 使用多個(gè)子進(jìn)程加載數(shù)據(jù)
pin_memory = True # 6. 是否鎖頁(yè)內(nèi)存
)
-
dataset- 它是什么:我們傳入的
COVID19Dataset實(shí)例 - 為什么需要:
DataLoader需要知道它的數(shù)據(jù)源頭在哪里,也就是要去哪個(gè)"圖書(shū)館"搬書(shū)
- 它是什么:我們傳入的
-
batch_size(批次大小)- 決定了每次打包多少條數(shù)據(jù)(詳見(jiàn)mini-batch)
-
shuffle = (mode = 'train')是否打亂- 如果為
True,DataLoader會(huì)在每一輪(epoch)訓(xùn)練開(kāi)始前,都將數(shù)據(jù)的順序完全隨機(jī)打亂
- 如果為
小結(jié)
整個(gè)數(shù)據(jù)流:
原始CSV文件 \(\rightarrow\) Daset類(lèi)(定義了如何讀取和處理單條數(shù)據(jù)) \(\rightarrow\) DataLoader(負(fù)責(zé)高效地、批量地、可選地打亂數(shù)據(jù),并將其打包好) \(\rightarrow\) 一個(gè)個(gè)批次(batch)的數(shù)據(jù)(最終送入模型訓(xùn)練)
Step4. 深度神經(jīng)網(wǎng)絡(luò)(DNN)
完整代碼
class NeuralNet(nn.Module):
''' A simple fully-connected deep neural network '''
def __init__(self, input_dim):
super(NeuralNet, self).__init__()
# Define your neural network here
# TODO: How to modify this model to achieve better performance?
self.net = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
# Mean squared error loss
self.criterion = nn.MSELoss(reduction='mean')
def forward(self, x):
''' Given input of size (batch_size x input_dim), compute output of the network '''
return self.net(x).squeeze(1)
def cal_loss(self, pred, target):
''' Calculate loss '''
# TODO: you may implement L2 regularization here
return self.criterion(pred, target)
class NeuralNet(nn.Module) 相當(dāng)于樂(lè)高創(chuàng)意工坊
class NeuralNet(nn.Module):
在PyTorch中,所有自定義的模型都必須繼承自torch.nn.Module這個(gè)類(lèi),它可以:
- 參數(shù)跟蹤:它會(huì)自動(dòng)識(shí)別我們模型中所有需要學(xué)習(xí)的參數(shù),我們不需要手動(dòng)管理
- 設(shè)備轉(zhuǎn)移:我們可以使用
.to('cuda')這樣的命令,吧整個(gè)模型(包括所有參數(shù))搬到GPU上加速運(yùn)算 - 模型保存與加載:提供了方便的
.state_dict()和.load_state_dict()方法來(lái)保存和加載你訓(xùn)練好的模型 - 模式切換: 可以用
.train()和.eval()切換訓(xùn)練模式和評(píng)估模式
__init__(self, input_dim) 準(zhǔn)備樂(lè)高積木
這個(gè)方法是模型的構(gòu)造函數(shù),負(fù)責(zé)定義和初始化我們的神經(jīng)網(wǎng)絡(luò)擁有的所有"積木塊"(層)
def __init__(self, input_dim):
super(NeuralNet, self).__init__() #必須的開(kāi)場(chǎng)白
#--- 把積木搭好 ---#
self.net = nn.Sqeuential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
# --- 準(zhǔn)備好評(píng)分標(biāo)準(zhǔn) ---
self.criterion = nn.MSELoss(reduction='mean')
-
super(NeurlaNet, self).__init__()
這是一句必須在最開(kāi)始調(diào)用的代碼,它會(huì)運(yùn)行父類(lèi)nn.Module的初始化邏輯,確保我們的模型工坊能正常運(yùn)作 -
self.net = nn.Sequential()
nn.Sequential是一個(gè)非常有用的“容器”或者“流水線管道”,我們可以吧一系列樂(lè)高積木塊(層)按順序放進(jìn)去,當(dāng)數(shù)據(jù)從管道一頭進(jìn)去時(shí),會(huì)自動(dòng)地、依次地通過(guò)所有積木塊,最后從另一頭出來(lái),這讓我們的forward方法可以寫(xiě)得很簡(jiǎn)潔 -
流水線里的“積木塊”
nn.Linear(input_dim, 64):全連接層,這個(gè)應(yīng)該都知道了,畢竟接觸到第一個(gè)神經(jīng)網(wǎng)絡(luò)就是這個(gè),它對(duì)輸入數(shù)據(jù)進(jìn)行一次線性變換(y = Wx + b),可以理解為將輸入的特征進(jìn)行加權(quán)、混合,然后提煉出新的特征nn.Linear內(nèi)部已經(jīng)包含了需要學(xué)習(xí)的權(quán)重矩陣W和偏置向量b,并且PyTorch會(huì)自動(dòng)對(duì)它們進(jìn)行隨機(jī)初始化input_dim:輸入特征數(shù)量。比如,我們的數(shù)據(jù)有93個(gè)特征,這里就是9364:輸入神經(jīng)元的數(shù)量,這代表我們希望這個(gè)層提煉出64個(gè)新的特征,這是一個(gè)可以自己調(diào)整的超參數(shù)
nn.ReLU():激活函數(shù)- 給網(wǎng)絡(luò)引入非線性,如果沒(méi)有非線性層,那么無(wú)論我們堆疊多少個(gè)
nn.Linear層,整個(gè)網(wǎng)絡(luò)本質(zhì)上還是線性的,理論上一個(gè)線性層就替代了,(那么堆疊多層純純小丑),學(xué)習(xí)能量非常有限,非線性激活函數(shù)能增加模型復(fù)雜度,提高模型的彈性 nn.Linear(64, 1):輸出層64: 它的輸入數(shù)量必須和上一層的輸出數(shù)量保持一致,這樣才能銜接起來(lái)1:它的輸出數(shù)量是1,因?yàn)槲覀兊娜蝿?wù)是預(yù)測(cè)一個(gè)單獨(dú)的數(shù)值(確診人數(shù)),所以最終只需要一個(gè)輸出結(jié)果
- 給網(wǎng)絡(luò)引入非線性,如果沒(méi)有非線性層,那么無(wú)論我們堆疊多少個(gè)
self.criterion = nn.MSELoss()- 模型的“評(píng)分標(biāo)準(zhǔn)”,也就是損失函數(shù),
nn.MSELoss()是PyTorch內(nèi)置的均方誤差損失函數(shù)
- 模型的“評(píng)分標(biāo)準(zhǔn)”,也就是損失函數(shù),
forward(self, x):前向傳播 說(shuō)明書(shū)
這個(gè)方法定義了數(shù)據(jù)如何流過(guò)我們?cè)?code>__init__中準(zhǔn)備好的積木塊
def forward(self, x):
# 數(shù)據(jù) x 直接通過(guò) self.net 這條搭建好的流水線
return self.net(x).squeeze(1)
x: 代表一批輸入的數(shù)據(jù)(一個(gè)Tensor)self.net(x): 因?yàn)槲覀兪褂昧?code>nn.Sequential,所以這里的代碼異常簡(jiǎn)潔,我們直接吧數(shù)據(jù)x喂給self.net這個(gè)管道,它就會(huì)自動(dòng)地按照Linear -> ReLU -> Linear的順序進(jìn)行計(jì)算,并返回最終結(jié)果.squeeze(1): 這是一個(gè)形狀調(diào)整操作。self.net輸出的形狀是[批次大小, 1],而我們的真實(shí)標(biāo)簽是[批次大小],.squeeze(1)會(huì)擠掉那個(gè)多余的維度1,讓預(yù)測(cè)和標(biāo)簽的形狀匹配,方便后續(xù)計(jì)算損失
小結(jié)
\(NeuralNet\)如何工作?
-
創(chuàng)建模型: 當(dāng)我們寫(xiě)
model = NeuralNet(input_dim=93)時(shí),__init__方法被調(diào)用,模型的所有“積木塊”(層)都被創(chuàng)建并準(zhǔn)備好。 -
進(jìn)行預(yù)測(cè): 在訓(xùn)練循環(huán)中,當(dāng)我們寫(xiě)
prediction = model(data)時(shí),PyTorch會(huì)自動(dòng)調(diào)用forward(data)方法。數(shù)據(jù)會(huì)按照我們定義的路徑流過(guò)整個(gè)網(wǎng)絡(luò),最終得到預(yù)測(cè)結(jié)果。 -
計(jì)算誤差: 接著,我們調(diào)用
loss = model.cal_loss(prediction, target)來(lái)計(jì)算預(yù)測(cè)的好壞。
Step5. Train/Dev/Test
5.1 Train
def train(tr_set, dv_set, model, config, device):
''' DNN training '''
n_epochs = config['n_epochs'] # Maximum number of epochs
# Setup optimizer
optimizer = getattr(torch.optim, config['optimizer'])(
model.parameters(), **config['optim_hparas'])
min_mse = 1000.
loss_record = {'train': [], 'dev': []} # for recording training loss
early_stop_cnt = 0
epoch = 0
while epoch < n_epochs:
model.train() # set model to training mode
for x, y in tr_set: # iterate through the dataloader
optimizer.zero_grad() # set gradient to zero
x, y = x.to(device), y.to(device) # move data to device (cpu/cuda)
pred = model(x) # forward pass (compute output)
mse_loss = model.cal_loss(pred, y) # compute loss
mse_loss.backward() # compute gradient (backpropagation)
optimizer.step() # update model with optimizer
loss_record['train'].append(mse_loss.detach().cpu().item())
# After each epoch, test your model on the validation (development) set.
dev_mse = dev(dv_set, model, device)
if dev_mse < min_mse:
# Save model if your model improved
min_mse = dev_mse
print('Saving model (epoch = {:4d}, loss = {:.4f})'
.format(epoch + 1, min_mse))
torch.save(model.state_dict(), config['save_path']) # Save model to specified path
early_stop_cnt = 0
else:
early_stop_cnt += 1
epoch += 1
loss_record['dev'].append(dev_mse)
if early_stop_cnt > config['early_stop']:
# Stop training if your model stops improving for "config['early_stop']" epochs.
break
print('Finished training after {} epochs'.format(epoch))
return min_mse, loss_record
1. 訓(xùn)練前的準(zhǔn)備工作
n_epochs = config['n_epochs'] # 學(xué)習(xí)輪數(shù)
# --- 定義優(yōu)化器 ---
optimizer = getattr(torch.optim, config['optimizer'])(
model.parameters(), **config['optim_hparas'])
# --- 狀態(tài)跟蹤變量 ---
min_mse = 1000.
loss_record = {'train': [], 'dev': []}
early_stop_cnt = 0
epoch = 0
n_epoches:從配置中讀取“訓(xùn)練輪數(shù)”,也就是我們打算讓模型吧整個(gè)訓(xùn)練數(shù)據(jù)集重復(fù)學(xué)習(xí)多少遍optimizer:優(yōu)化器- 簡(jiǎn)單說(shuō),就是一種優(yōu)化梯度下降的方法,由于有的損失函數(shù)的梯度不是很容易降下來(lái),我們可以利用各種優(yōu)化算法。比如
SGD(隨機(jī)梯度下降),Adam等。這里的代碼getattr(torch.optim, config['optimizer'])是一種非常靈活的寫(xiě)法,可以根據(jù)我們?cè)?code>config字典里設(shè)置的字符串(比如'SGD'或'Adam')來(lái)自動(dòng)選擇并創(chuàng)建對(duì)應(yīng)的優(yōu)化器 model.parameters(): 這是nn.Module帶來(lái)的便利之一,整個(gè)方法會(huì)自動(dòng)返回模型中所有需要學(xué)習(xí)的參數(shù),也就是所有nn.Linear層的權(quán)重和偏置,我們把這些參數(shù)交給優(yōu)化器,優(yōu)化器就知道去更新誰(shuí)了
- 簡(jiǎn)單說(shuō),就是一種優(yōu)化梯度下降的方法,由于有的損失函數(shù)的梯度不是很容易降下來(lái),我們可以利用各種優(yōu)化算法。比如
min_mse,loss_record,early_step_cnt: 用于記錄和監(jiān)控訓(xùn)練過(guò)程的變量min_mse: 記錄驗(yàn)證集上出現(xiàn)過(guò)的最小均方誤差,用來(lái)判斷是否要保存模型loss_record: 記錄每一輪訓(xùn)練和驗(yàn)證的損失值,方便后續(xù)畫(huà)出“學(xué)習(xí)曲線”early_stop_cnt: 用于提前停止,用來(lái)防止模型在沒(méi)有進(jìn)步的情況下繼續(xù)浪費(fèi)時(shí)間訓(xùn)練
2. 主學(xué)習(xí)循環(huán)
while epoch < n_epochs:
# ... 一輪學(xué)習(xí)的完整過(guò)程 ...
epoch += 1
- Epoch : 所有
batch跑一遍,也就是把整個(gè)訓(xùn)練數(shù)據(jù)集從頭到尾學(xué)習(xí)一遍,也就是while循環(huán)執(zhí)行一次,就是一個(gè)Epoch(輪)
3. 批次學(xué)習(xí)循環(huán)
在每一個(gè)Epoch學(xué)習(xí)中,模型會(huì)一小批一小批地(mini-batch)看數(shù)據(jù)
model.train() # 1. 切換到訓(xùn)練模式
for x, y in tr_set: # tr_set 就是我們的 DataLoader
# --- 核心訓(xùn)練五步法 ---
optimizer.zero_grad() # 2. 梯度歸零
x, y = x.to(device), y.to(device) # 3. 數(shù)據(jù)上膛(放到GPU)
pred = model(x) # 4. 前向傳播(做預(yù)測(cè))
mse_loss = model.cal_loss(pred, y) # 5. 計(jì)算損失
mse_loss.backward() # 6. 反向傳播(計(jì)算梯度)
optimizer.step() # 7. 更新權(quán)重
loss_record['train'].append(mse_loss.detach().cpu().item())
model_train(): 這是nn.Module的一個(gè)重要方法。這會(huì)啟用一些只有在訓(xùn)練時(shí)才使用的功能(比如Dropout),與之對(duì)應(yīng)的是model.eval(),在驗(yàn)證集和測(cè)試時(shí)使用for x, y in tr_set:tr_set是我們的DataLoader。這個(gè)循環(huán)會(huì)不斷地從DataLoader中取出打包好的小批次數(shù)據(jù),x是特征,y是標(biāo)簽。
接下來(lái)的幾步是 PyTorch 中最經(jīng)典、最核心的訓(xùn)練流程, 幾乎所有的訓(xùn)練代碼都遵循這個(gè)模式,甚至可以當(dāng)成模板
optimizer.zero_grad()梯度歸零:- PyTorch會(huì)默認(rèn)累積梯度,因此,在計(jì)算新一批數(shù)據(jù)的梯度之前,必須手動(dòng)清空上一批的梯度。否則,梯度會(huì)越加越大,導(dǎo)致錯(cuò)誤的更新
x, y = x.to(device), y.to(device):- 將這一批數(shù)據(jù)和標(biāo)簽都移動(dòng)到之前設(shè)定的設(shè)備(GPU或者CPU)上
pred = model(x)- 把數(shù)據(jù)
x喂給模型,模型會(huì)調(diào)用自己的forward方法,得出一個(gè)預(yù)測(cè)結(jié)果pred
- 把數(shù)據(jù)
mes_loss = model.cal_loss(pred, y)計(jì)算損失- 調(diào)用我們之前定義的
cal_loss方法,用損失函數(shù)(MSE)比較預(yù)測(cè)值pred和真實(shí)值y,得到一個(gè)代表“差距”的數(shù)值mse_loss。
- 調(diào)用我們之前定義的
mes_loss.backward()反向傳播autograd:這是PyTorch的“自動(dòng)求導(dǎo)引擎”,調(diào)用.backward()后,PyTorch會(huì)自動(dòng)計(jì)算出mes_loss相對(duì)于模型中每一個(gè)需要學(xué)習(xí)的參數(shù)的梯度(偏導(dǎo)數(shù)),這個(gè)梯度指明了參數(shù)應(yīng)該朝哪個(gè)方向調(diào)整才能讓損失變小
optimizer.step()更新權(quán)重- 優(yōu)化器
optimizer會(huì)根據(jù)上一步計(jì)算出的梯度,使用它自己的更新規(guī)則(比如SGD或Adam算法)來(lái)微調(diào)模型中所有的參數(shù)(model.parameters()。這一步是模型真正在“學(xué)習(xí)”和“成長(zhǎng)”的時(shí)刻。
- 優(yōu)化器
4. 驗(yàn)證和保存
在一輪(Epoch)的所有批次都學(xué)習(xí)完后,我們需要對(duì)模型進(jìn)行一次驗(yàn)證,看看它學(xué)得怎么樣。
dev_mse = dev(dv_set, model, device) # 在驗(yàn)證集上考試
if dev_mse < min_mse:
# 如果這次考試成績(jī)是歷史最好成績(jī)...
min_mse = dev_mse
print('Saving model ...')
torch.save(model.state_dict(), config['save_path']) # 保存模型
early_stop_cnt = 0 # 重置早停計(jì)數(shù)器
else:
early_stop_cnt += 1 # 否則,沒(méi)進(jìn)步的次數(shù)+1
-
dev(dv_set, model, device): 調(diào)用dev函數(shù)在驗(yàn)證集上進(jìn)行評(píng)估,得到一個(gè)均方誤差dev_mse。 -
if dev_mse < min_mse: 如果這次的驗(yàn)證誤差比歷史最低誤差min_mse還要低,說(shuō)明模型取得了進(jìn)步。 -
torch.save(model.state_dict(), ...): 這是保存模型的標(biāo)準(zhǔn)方法。model.state_dict()會(huì)返回一個(gè)包含模型所有學(xué)習(xí)到的參數(shù)(權(quán)重和偏置)的字典。torch.save將這個(gè)字典保存到文件中,這樣我們以后就可以隨時(shí)加載這個(gè)表現(xiàn)最好的模型了。
5. 早停機(jī)制
if early_stop_cnt > config['early_stop']:
# 如果連續(xù)很多輪考試成績(jī)都沒(méi)進(jìn)步...
break # ...就提前結(jié)束訓(xùn)練
這個(gè)機(jī)制可以防止在模型性能不再提升時(shí)繼續(xù)浪費(fèi)時(shí)間訓(xùn)練,同時(shí)也能有效避免過(guò)擬合。
小結(jié)
train 函數(shù)模擬了學(xué)習(xí)過(guò)程:它讓模型在一輪輪(epoch)的學(xué)習(xí)中,一小批一小批(batch)地看數(shù)據(jù),并通過(guò) 前向傳播 -> 計(jì)算損失 -> 反向傳播 -> 更新權(quán)重 的核心流程來(lái)不斷優(yōu)化自己。同時(shí),它還通過(guò)驗(yàn)證集來(lái)監(jiān)控學(xué)習(xí)效果,只保存最好的模型,并在必要時(shí)提前終止訓(xùn)練。
5.2 Validation
def dev(dv_set, model, device):
model.eval() # 1. 切換到評(píng)估模式
total_loss = 0
for x, y in dv_set: # 2. 遍歷驗(yàn)證數(shù)據(jù)
x, y = x.to(device), y.to(device)
with torch.no_grad(): # 3. 關(guān)閉梯度計(jì)算
pred = model(x) # 4. 做預(yù)測(cè)
mse_loss = model.cal_loss(pred, y) # 5. 計(jì)算損失
total_loss += mse_loss.detach().cpu().item() * len(x)
total_loss = total_loss / len(dv_set.dataset)
return total_loss
def dev(dv_set, model, device) 模擬考試
在每一個(gè)epoch結(jié)束后,用驗(yàn)證集dv_set來(lái)評(píng)估一下模型當(dāng)前的表現(xiàn),得到一個(gè)客觀的分?jǐn)?shù)
5.3 Testing
def test(tt_set, model, device):
model.eval() # 同樣切換到評(píng)估模式
preds = []
for x in tt_set: # 1. 遍歷測(cè)試數(shù)據(jù) (注意這里只有 x)
x = x.to(device)
with torch.no_grad(): # 同樣關(guān)閉梯度計(jì)算
pred = model(x) # 2. 做預(yù)測(cè)
preds.append(pred.detach().cpu()) # 3. 收集預(yù)測(cè)結(jié)果
preds = torch.cat(preds, dim=0).numpy() # 4. 拼接并轉(zhuǎn)換為 NumPy 數(shù)組
return preds
def test(tt_set, model, device) 高考
這個(gè)函數(shù)在所有訓(xùn)練都完成之后才被調(diào)用。它使用我們保存下來(lái)的表現(xiàn)最好的模型,對(duì)從未見(jiàn)過(guò)的測(cè)試集 tt_set 進(jìn)行預(yù)測(cè),并生成最終的結(jié)果文件。
注意
流程與 dev 的異同
-
相同點(diǎn): 同樣需要
model.eval()和with torch.no_grad(),因?yàn)檫@也是一個(gè)評(píng)估過(guò)程。 -
不同點(diǎn):
-
輸入: 遍歷測(cè)試集
tt_set時(shí),DataLoader只返回x,因?yàn)闇y(cè)試集沒(méi)有提供標(biāo)簽y。 -
目的:
test函數(shù)的目的不是計(jì)算損失,而是收集所有預(yù)測(cè)結(jié)果pred。 -
輸出:
dev返回一個(gè)數(shù)字(損失值),而test返回一個(gè)包含所有預(yù)測(cè)值的數(shù)組。
-
-
preds.append(pred.detach().cpu())
在循環(huán)中,我們將每一批的預(yù)測(cè)結(jié)果(一個(gè)Tensor)從GPU上分離并移到CPU,然后存入一個(gè)Python列表preds中 -
preds = torch.cat(preds, dim = 0).numpy()
這個(gè)是循環(huán)結(jié)束后的一個(gè)重要處理步驟-
torch.cat(preds, dim=0):preds現(xiàn)在是一個(gè)Tensor列表[tensor_batch1, tensor_batch2, ...]。torch.cat函數(shù)的作用是將這個(gè)列表中的所有Tensor拼接成一個(gè)大的、完整的Tensor。dim=0指定了沿著第0個(gè)維度(也就是行)進(jìn)行拼接。 -
.numpy(): 將最終的PyTorch Tensor轉(zhuǎn)換成NumPy數(shù)組。這樣做是為了方便后續(xù)的處理,比如保存成CSV文件,或者使用其他非PyTorch的庫(kù)(如Scikit-learn)進(jìn)行分析
-
小結(jié)
| 階段 | 訓(xùn)練 (Training) | 驗(yàn)證 (Validation) | 測(cè)試 (Testing) |
|---|---|---|---|
| 目的 | 學(xué)習(xí)知識(shí),更新權(quán)重 | 檢查學(xué)習(xí)效果,調(diào)整策略 | 最終評(píng)估模型性能 |
| 模式 | model.train() |
model.eval() |
model.eval() |
| 梯度 | 需要 (.backward()) |
不需要 (torch.no_grad) |
不需要 (torch.no_grad) |
| 數(shù)據(jù) | 特征 x 和標(biāo)簽 y |
特征 x 和標(biāo)簽 y |
只有特征 x |
| 輸出 | 無(wú)(只更新模型內(nèi)部參數(shù)) | 損失值(一個(gè)數(shù)字) | 所有預(yù)測(cè)結(jié)果(一個(gè)數(shù)組) |
考慮優(yōu)化
好了,這樣整個(gè)流程就基本結(jié)束了,我們就可以考慮去優(yōu)化我們的神經(jīng)網(wǎng)絡(luò)了,這里作業(yè)給的源碼其實(shí)是可以直接跑的,但是因?yàn)樯窠?jīng)網(wǎng)絡(luò)設(shè)計(jì)得比較簡(jiǎn)單所以最后的結(jié)果會(huì)比較差

說(shuō)明模型的準(zhǔn)確率不夠,也就是說(shuō),模型的學(xué)習(xí)能力比較弱,可能有這幾點(diǎn)原因
- 數(shù)據(jù)量不夠大
- 特征太少
- 結(jié)構(gòu)太簡(jiǎn)單
- 梯度降不下去,損失函數(shù)的值一直很大
作業(yè)中的數(shù)據(jù)集是給定的,我們沒(méi)法改了
我們可以從剩下的方面考慮
--------------------------------------------Waiting for update----------------------------------------------------------

浙公網(wǎng)安備 33010602011771號(hào)