機(jī)器學(xué)習(xí)簡(jiǎn)單來講就是要在數(shù)據(jù)中訓(xùn)練出一個(gè)模型枕稀,能夠?qū)⑤斎胗成涑珊侠淼妮敵鲅病K裕谟?xùn)練模型之前萎坷,我們首先準(zhǔn)備好輸入凹联、輸出對(duì);然后再利用這些輸入哆档、輸出對(duì)來優(yōu)化模型蔽挠,使模型的LOSS(預(yù)測(cè)輸出和實(shí)際輸出的誤差)盡可能小。模型優(yōu)化的基本原理是梯度下降法瓜浸。pytorch為實(shí)現(xiàn)上述任務(wù)提供了一個(gè)很好的框架澳淑,或者說一個(gè)很好的模板,使得做深度學(xué)習(xí)變得非常簡(jiǎn)單插佛,簡(jiǎn)單到一兩個(gè)小時(shí)就能入門杠巡。本文借助一個(gè)簡(jiǎn)單線性回歸的例子,簡(jiǎn)要介紹了Pytorch框架中的數(shù)據(jù)加載及模型訓(xùn)練等雇寇。
生成輸入輸出對(duì)
在訓(xùn)練模型之前氢拥,我們首先生成一些輸入蚌铜、輸出對(duì),作為模型訓(xùn)練數(shù)據(jù)和測(cè)試數(shù)據(jù)兄一。本例通過一個(gè)線性函數(shù)疊加一些噪聲來生成厘线。比如下面這段代碼,取權(quán)重值為2出革,偏置為5的線性函數(shù)造壮,然后疊加一個(gè)標(biāo)準(zhǔn)正態(tài)分布的噪聲,生成100個(gè)數(shù)據(jù)點(diǎn)骂束。生成兩次耳璧,一次用于模型訓(xùn)練,一次用于模型測(cè)試展箱。通過該數(shù)據(jù)訓(xùn)練的線性模型旨枯,我們希望權(quán)重越接近2越好,偏置越接近5越好混驰。
import random
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# 生成數(shù)據(jù)
x = random.sample(range(0, 100), 100)
x = 0.1*np.array(x)
y = x*2+5+np.random.randn(100)
plt.plot(x, y, 'o')
plt.show()
data = [list(x), list(y)]
# 矩陣轉(zhuǎn)置
data = list(zip(*data))
column = ['x', 'y']
# list轉(zhuǎn)換成dataFrame
dataset = pd.DataFrame(data=data, columns=column)
# 將數(shù)據(jù)保存到'.csv'文件中
dataset.to_csv('data/trainData.csv')
# dataset.to_csv('data/testData.csv')
上述代碼生成的數(shù)據(jù)分布如下圖所示攀隔,
模型的設(shè)計(jì)
有了輸入、輸出對(duì)數(shù)據(jù)栖榨,接下來我們?cè)賮碓O(shè)計(jì)模型昆汹。pytorch為我們提供了一個(gè)框架,使得網(wǎng)絡(luò)模型的搭建非常簡(jiǎn)單婴栽,簡(jiǎn)單得像是在搭積木满粗。pytorch不僅提供了搭積木的框架,還提供了大量的積木塊愚争,例如Linear層映皆、卷積層、激活函數(shù)等等轰枝,我們只需要根據(jù)任務(wù)需求將這些積木堆在一起就行了捅彻。我們通過線性回歸這個(gè)最簡(jiǎn)單的例子來學(xué)習(xí)一下pytorch的模型搭建框架。
pytorch的積木搭建框架以及積木塊都在pytorch的nn模塊中狸膏,因此首先要導(dǎo)入nn模塊沟饥。pytorch的積木搭建框架是一個(gè)叫做Module的類,在搭建自己的網(wǎng)絡(luò)模型是需要繼承這個(gè)類湾戳。在這個(gè)類里面,有兩個(gè)函數(shù)為我們搭建積木提供了支撐广料,需要改寫砾脑,一個(gè)是init函數(shù),一個(gè)是forward函數(shù)艾杏。init函數(shù)列出我們需要用的積木塊韧衣,并根據(jù)需要設(shè)置好這些積木塊的相關(guān)參數(shù);在設(shè)置參數(shù)的時(shí)候一定要注意上一層積木的輸出維度和下一層積木的輸入維度匹配。forward函數(shù)將這些積木塊壘在一起畅铭,使輸入能順利地通過一層層的積木塊氏淑,最后輸出。我們這里是一個(gè)簡(jiǎn)單的線性回歸問題硕噩,所以只需要一塊積木假残,那就是nn.Linear,該積木塊提供了線性變換功能炉擅。nn.Linear有三個(gè)參數(shù)辉懒,分別是in_features, out_features和bias,分別代表了輸入的維度谍失、輸出的維度和是否需要偏置(默認(rèn)的情況下偏置保留)眶俩。在我們這個(gè)例子中,輸入和輸出的維度都是1快鱼,需要偏置颠印。模型搭建的代碼如下,代碼保存在model.py中抹竹。
from torch import nn
# 定義模型時(shí)繼承nn.Module
class LinearRegress(nn.Module):
# __init__函數(shù)列出積木塊并設(shè)置積木的參數(shù)线罕,這里的參數(shù)由模型實(shí)例化時(shí)給出
def __init__(self, inputsize, outputsize):
super(LinearRegress, self).__init__()
self.Linear1 = nn.Linear(in_features=inputsize, out_features=outputsize)
# forward函數(shù)搭積木,將積木壘在一塊柒莉,讓輸入依次通過積木塊最后輸出
def forward(self, x):
return self.Linear1(x)
數(shù)據(jù)的加載
為了簡(jiǎn)化訓(xùn)練數(shù)據(jù)和測(cè)試數(shù)據(jù)的加載過程闻坚,pytorch為我們提供了數(shù)據(jù)集模板Dataset以及數(shù)據(jù)加載器DataLoader。我們?cè)谟?xùn)練模型時(shí)需要從數(shù)據(jù)集中摳出輸入兢孝、輸出對(duì)窿凤,Dataset恰好為我們給我們提供了一個(gè)摳輸入、輸出對(duì)的模板跨蟹。我們定義自己的數(shù)據(jù)集時(shí)雳殊,需要繼承Dataset,并改寫三個(gè)函數(shù)窗轩,分別是init, getitem, len夯秃。init一般告訴代碼要加載的數(shù)據(jù)集存在哪個(gè)位置。getitem從文件夾中讀入數(shù)據(jù)集并進(jìn)行一些處理痢艺,返回輸入輸出對(duì)仓洼。這里要注意返回輸入、輸出對(duì)的格式是Tensor堤舒,并且輸入Tensor的維度一定要和模型的輸入維度一致色建,輸出Tensor的維度一定要和模型的輸出維度一致,否則會(huì)出錯(cuò)舌缤。例如mn.Linear輸入箕戳、輸出都是二維Tensor某残,分別是[batchsize,in_features]、[bathchsize,outfeature]陵吸。所以玻墅,在加載了數(shù)據(jù)之后,首先要將輸入壮虫、輸出數(shù)據(jù)都轉(zhuǎn)換成Tensor澳厢,然后將1維Tensor轉(zhuǎn)換成二維Tensor。len函數(shù)返回?cái)?shù)據(jù)集的長(zhǎng)度旨指。
DataLoader提供一些列參數(shù)設(shè)置赏酥,方便我們可以根據(jù)需要靈活的加載數(shù)據(jù)。例如一次加載的數(shù)據(jù)大小batchsize谆构,是否打亂數(shù)據(jù)順序shuffer等裸扶,還有各種參數(shù)可以看和help中對(duì)DataLoader的解釋。下面是代碼搬素,保存在dataProcess.py中呵晨。
import os
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
import pandas as pd
# 將數(shù)據(jù)導(dǎo)入到DataSet
class MyDataSet(Dataset):
# 初始化時(shí)從文件中把數(shù)據(jù)讀進(jìn)來
def __init__(self, dataDir, dataName):
DataPath = os.path.join(dataDir, dataName)
self.data = pd.read_csv(DataPath)
def __getitem__(self, idx):
# 將數(shù)據(jù)轉(zhuǎn)換成二維Tensor
x_tensor = torch.Tensor(self.data['x'].to_list()).reshape(-1, 1)
y_tensor = torch.Tensor(self.data['y'].to_list()).reshape(-1, 1)
return x_tensor[idx], y_tensor[idx]
def __len__(self):
return len(self.data)
#加載訓(xùn)練數(shù)據(jù)
myTrainData = MyDataSet("data", "trainData.csv")
#將batch_size設(shè)置成50,表示每一次迭代取出50個(gè)數(shù)據(jù)熬尺。
myTrainDataLoader = DataLoader(dataset=myTrainData, batch_size=50, shuffle=True)
#加載測(cè)試數(shù)據(jù)
myTestData = MyDataSet("data", "testData.csv")
myTestDataLoader = DataLoader(dataset=myTestData, batch_size=50, shuffle=True)
模型的訓(xùn)練與測(cè)試
模型的訓(xùn)練過程大致如下:
- 從數(shù)據(jù)集中取出一個(gè)btachsize的輸入摸屠、輸出對(duì)。
- 把輸入扔給模型粱哼,得到預(yù)測(cè)輸出
- 計(jì)算預(yù)測(cè)輸出和真實(shí)輸出之間的LOSS
- 反向傳播計(jì)算梯度季二,并優(yōu)化一次模型參數(shù)
- 回到第1步,直到從數(shù)據(jù)集中取出所有數(shù)據(jù)揭措,完成一次完整的訓(xùn)練
- 重復(fù)1-5步epoch次
為了測(cè)試模型的泛化能力胯舷,我們往往在優(yōu)化模型的過程中還會(huì)使用一些測(cè)試數(shù)據(jù)來測(cè)試模型的預(yù)測(cè)效果。這里一定要注意測(cè)試數(shù)據(jù)和訓(xùn)練數(shù)據(jù)不是同一個(gè)數(shù)據(jù)集绊含,提前要把數(shù)據(jù)進(jìn)行分割桑嘶,分成訓(xùn)練數(shù)據(jù)和測(cè)試數(shù)據(jù)。一般每進(jìn)行一次完整的訓(xùn)練后躬充,對(duì)模型進(jìn)行一次測(cè)試逃顶,也就是每一個(gè)epoch,測(cè)試一次模型充甚。測(cè)試的時(shí)候也需要計(jì)算模型預(yù)測(cè)輸出和真實(shí)輸出之間的LOSS以政,只是測(cè)試不用再計(jì)算梯度和優(yōu)化模型了。如果在訓(xùn)練過程中發(fā)現(xiàn)訓(xùn)練的LOSS在不斷減小伴找,但是測(cè)試的LOSS卻在增加妙蔗,這時(shí)候模型發(fā)生了過擬合問題,要提前終止訓(xùn)練疆瑰。
當(dāng)然眉反,我們?yōu)榱丝吹接?xùn)練的效果,往往要畫LOSS隨著迭代次數(shù)的變化曲線穆役,這個(gè)我們可以借助Tensorboard寸五,也可以用一個(gè)list把訓(xùn)練過程的LOSS保存下來,最后用matplotlib.pyplot畫出來耿币。
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from model import *
from dataProcess import *
import matplotlib.pyplot as plt
#加載訓(xùn)練數(shù)據(jù)
myTrainData = MyDataSet("data", "trainData.csv")
#將batch_size設(shè)置成50梳杏,表示每一次迭代取出50個(gè)數(shù)據(jù)。
myTrainDataLoader = DataLoader(dataset=myTrainData, batch_size=50, shuffle=True)
#加載測(cè)試數(shù)據(jù)
myTestData = MyDataSet("data", "testData.csv")
myTestDataLoader = DataLoader(dataset=myTestData, batch_size=50, shuffle=True)
# 創(chuàng)建網(wǎng)絡(luò)模型
myModel = LinearRegress(inputsize=1, outputsize=1)
# 損失函數(shù)
loss_fn = nn.MSELoss()
# 學(xué)習(xí)率
learning_rate = 5e-3
# 優(yōu)化器
optimizer = torch.optim.SGD(myModel.parameters(), lr=learning_rate)
# 總共的訓(xùn)練步數(shù)
total_train_step = 0
#總共的測(cè)試步數(shù)
total_test_step = 0
step = 0
epoch =500
# Tensorboard的writer實(shí)例淹接,用于記錄訓(xùn)練過程中的LOSS變化
writer = SummaryWriter("logs")
train_loss_his = []
test_totalloss_his = []
for i in range(epoch):
print(f"-------第{i}輪訓(xùn)練開始-------")
# 這一部分是模型訓(xùn)練
for data in myTrainDataLoader:
# 注意這里是取了一個(gè)batchsize的數(shù)據(jù)十性,該例batchsize=50,因此取了50個(gè)數(shù)據(jù)
x, y = data
# 把輸入扔給模型塑悼,得到預(yù)測(cè)輸出output
output = myModel(x)
# 計(jì)算預(yù)測(cè)輸出output和真是輸出y之間的LOSS
loss = loss_fn(output, y)
# 將梯度清零劲适,好像這一步必須要
optimizer.zero_grad()
# 反向傳播,計(jì)算梯度
loss.backward()
# 優(yōu)化一次參數(shù)
optimizer.step()
# 總的迭代次數(shù)加1
total_train_step = total_train_step+1
# 將當(dāng)前的LOSS放到LOSS記錄的list中
train_loss_his.append(loss)
# 將當(dāng)前的LOSS記錄到tensorboard的中
writer.add_scalar("train_loss", loss.item(), total_train_step)
print(f"訓(xùn)練次數(shù):{total_train_step}厢蒜,loss:{loss}")
# 下面這段代碼是模型測(cè)試
total_test_loss = 0
# 這里告訴代碼不用求梯度了
with torch.no_grad():
for data in myTestDataLoader:
x, y = data
output = myModel(x)
loss = loss_fn(output, y)
# 這里求一個(gè)epoch的總loss
total_test_loss = total_test_loss + loss
print(f"測(cè)試集上的loss:{total_test_loss}")
test_totalloss_his.append(total_test_loss)
writer.add_scalar("test_loss", total_test_loss.item(), i)
# 輸出線性模型的兩個(gè)參數(shù)霞势,分別是權(quán)重和偏置
for parameters in myModel.parameters():
print(parameters)
writer.close()
# 畫出訓(xùn)練損失變化曲線
plt.plot(train_loss_his)
plt.show()
# 畫出測(cè)試損失變化曲線
plt.plot(test_totalloss_his)
plt.show()
運(yùn)行上述代碼,訓(xùn)練LOSS的變化如下圖斑鸦,
測(cè)試LOSS的變化如下圖愕贡,
線性模型的兩個(gè)參數(shù):權(quán)重為2.1539,偏置為4.1611巷屿」桃裕可以看到訓(xùn)練后的參數(shù)和預(yù)期參數(shù)接近,但也存在一定的偏差嘱巾『┝眨可以嘗試設(shè)置不同學(xué)習(xí)率、采用不同的優(yōu)化器浓冒、使用不同的LOSS函數(shù)等栽渴,會(huì)對(duì)結(jié)果產(chǎn)生很大的影響,這就是無聊的調(diào)參了稳懒。
到此為止闲擦,基于pytorch的深度學(xué)習(xí)框架就入門了,確實(shí)很簡(jiǎn)單场梆!