# Data Generationnp.random.seed(42)x = np.random.rand(100, 1)y = 1 + 2 * x + .1 * np.random.randn(100, 1)# Shuffles the indicesidx = np.arange(100)np.random.shuffle(idx)# Uses first 80 random indices for traintrain_idx = idx[:80]# Uses the remaining indices for validationval_idx = idx[80:]# Generates train and validation setsx_train, y_train = x[train_idx], y[train_idx]x_val, y_val = x[val_idx], y[val_idx我們知道a = 1 b = 2,但是現在讓我們看看如何使用梯度下降和訓練集中的80個點來接近真實值的。關於梯度下降的內部運行機制,前面有篇文章來專門說明。這裡只簡單介紹梯度下降的四個基本步驟。步驟1:計算損失對於回歸問題,損失由均方誤差(MSE)給出,即標籤(y)和預測(a + bx)之間所有平方誤差的平均值。值得一提的是,如果我們使用訓練集(N)中的所有點來計算損失,我們是在執行批量梯度下降。如果我們每次都用一個點,那就是隨機梯度下降法。在1和n之間的任何其他(n)都是小批量梯度下降的特徵。
步驟2:計算梯度梯度是多元函數的所有偏導數構成的向量,我們有兩個參數,a和b,所以我們必須計算兩個偏導。導數告訴你,當你稍微改變某個量時,這個量的變化量是多少。在我們的例子中,當我們改變兩個參數中的一個時,我們的MSE損失變化了多少?步驟3:更新參數在最後一步,我們使用梯度來更新參數。因為我們試圖最小化我們的損失,所以我們反轉了更新的梯度符號。還需要考慮另一個參數:學習率,用希臘字母eta表示(看起來像字母n),這是我們需要對梯度進行參數更新的乘法因子,在程序裡通常簡化為lr.關於如何選擇合適的學習率,這是一個需要大量實踐的內容,學習率不能太大,也不能太小。現在,我們使用更新的參數返回步驟1並重新啟動流程。對於批量梯度下降,這是微不足道的,因為它使用所有的點來計算損失-一個輪次等於一個更新。對於隨機梯度下降,一個epoch意味著N次更新,而對於小批量(大小為N),一個epoch有N/n次更新。
接下來就是使用Numpy用梯度下降來實驗線性回歸模型的時候了。還沒有到PyTorch,使用Numpy的原因有兩點:展示主要的難點,以便能夠充分理解使用PyTorch的方便之處。參數/權重的隨機初始化(我們只有兩個,a和b)——第3行和第4行;超參數的初始化(在我們的例子中,只有學習速率和epoch的數量)——第9行和第11行; 確保始終初始化您的隨機種子,以確保您的結果的再現性。和往常一樣,隨機的種子是42,是所有隨機種子中最不隨機的:-)計算損失,使用預測和標籤,以及當前任務的適當損失函數——第18行和第20行;更新參數——第27行和第28行; 請記住,如果您不使用批量梯度下降(我們的示例使用),則必須編寫一個內部循環來為每個點(隨機)或n個點(迷你批量)執行四個訓練步驟。稍後我們將看到一個小型批處理示例。# Initializes parameters "a" and "b" randomlynp.random.seed(42)a = np.random.randn(1)b = np.random.randn(1)
print(a, b)
# Sets learning ratelr = 1e-1# Defines number of epochsn_epochs = 1000
for epoch in range(n_epochs): # Computes our model's predicted output yhat = a + b * x_train
# How wrong is our model? That's the error! error = (y_train - yhat) # It is a regression, so it computes mean squared error (MSE) loss = (error ** 2).mean()
# Computes gradients for both "a" and "b" parameters a_grad = -2 * error.mean() b_grad = -2 * (x_train * error).mean()
# Updates parameters using gradients and the learning rate a = a - lr * a_grad b = b - lr * b_grad
print(a, b)
# Sanity Check: do we get the same results as our gradient descent?from sklearn.linear_model import LinearRegressionlinr = LinearRegression()linr.fit(x_train, y_train)print(linr.intercept_, linr.coef_[0])# a and b after initialization[0.49671415] [-0.1382643]# a and b after our gradient descent[1.02354094] [1.96896411]# intercept and coef from Scikit-Learn[1.02354075] [1.96896447]以上是Numpy的做法,接下來我們看一看PyTorch的做法。在深度學習中,張量無處不在。嗯,谷歌的框架被稱為TensorFlow是有原因的,那到底什麼是張量?張量(tensor)是多維數組,目的是把向量、矩陣推向更高的維度。一個標量(一個數字)有0維,一個向量有1維,一個矩陣有2維,一個張量有3維或更多。但是,為了簡單起見,我們通常也稱向量和矩陣為張量。你可能會問:「我們如何從Numpy的數組過渡到PyTorch的張量?」這就是from_numpy的作用。它返回一個CPU張量。如何要使用GPU,那麼它會把張量發送到GPU上面。「如果我想讓我的代碼回退到CPU,如果沒有可用的GPU ?」你可以使用cuda.is_available()來找出你是否有一個GPU供你使用,並相應地設置你的設備。當然還可以使用float()輕鬆地將其轉換為較低精度(32位浮點數)。import torchimport torch.optim as optimimport torch.nn as nnfrom torchviz import make_dot
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Our data was in Numpy arrays, but we need to transform them into PyTorch's Tensors# and then we send them to the chosen devicex_train_tensor = torch.from_numpy(x_train).float().to(device)y_train_tensor = torch.from_numpy(y_train).float().to(device)
# Here we can see the difference - notice that .type() is more useful# since it also tells us WHERE the tensor is (device)print(type(x_train), type(x_train_tensor), x_train_tensor.type())如果比較這兩個變量的類型,就會得到預期的結果第一種代碼用的是numpy.ndarray,第三種代碼用的是torch.Tensor.使用PyTorch的type(),它會顯示它的位置。我們也可以反過來,使用Numpy()將張量轉換回Numpy數組。它應該像x_train_tensor.numpy()一樣簡單,但是…TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
如何區分用於數據的張量(就像我們剛剛創建的那些)和用作(可訓練的)參數/權重的張量?後一個張量需要計算它的梯度,所以我們可以更新它們的值(即參數的值)。這就是requires_grad=True參數的作用。它告訴PyTorch我們想讓它為我們計算梯度。你可能想為一個參數創建一個簡單的張量,然後把它發送到所選擇的設備上,就像我們處理數據一樣,對吧? 但其實沒那麼快……# FIRST# Initializes parameters "a" and "b" randomly, ALMOST as we did in Numpy# since we want to apply gradient descent on these parameters, we need# to set REQUIRES_GRAD = TRUEa = torch.randn(1, requires_grad=True, dtype=torch.float)b = torch.randn(1, requires_grad=True, dtype=torch.float)print(a, b)
# SECOND# But what if we want to run it on a GPU? We could just send them to device, right?a = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)b = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)print(a, b)# Sorry, but NO! The to(device) "shadows" the gradient...
# THIRD# We can either create regular tensors and send them to the device (as we did with our data)a = torch.randn(1, dtype=torch.float).to(device)b = torch.randn(1, dtype=torch.float).to(device)# and THEN set them as requiring gradients...a.requires_grad_()b.requires_grad_()print(a, b)第一個代碼塊為我們的參數、梯度和所有東西創建了兩個很好的張量。但它們是CPU張量。# FIRSTtensor([-0.5531], requires_grad=True)tensor([-0.7314], requires_grad=True)在第二段代碼中,我們嘗試了將它們發送到我們的GPU的簡單方法。我們成功地將它們發送到另一個設備上,但是我們不知怎麼地「丟失」了梯度……# SECONDtensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)在第三塊中,我們首先將張量發送到設備,然後使用requires_grad_()方法將其requires_grad設置為True。在PyTorch中,每個以下劃線(_)結尾的方法都會進行適當的更改,這意味著它們將修改底層變量。
儘管最後一種方法工作得很好,但最好在設備創建時將張量分配給它們。# We can specify the device at the moment of creation - RECOMMENDED!torch.manual_seed(42)a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)print(a, b)tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)容易多了,對吧? 現在我們知道了如何創建需要梯度的張量,讓我們看看PyTorch如何處理它們。那麼,我們如何讓PyTorch完成它的任務並計算所有的梯度呢?這就是backward()的好處。還記得計算梯度的起點嗎?這是loss。因此,我們需要從相應的Python變量中調用backward()方法,比如,loss. backwards()。那麼梯度的實際值呢?我們可以通過觀察張量的grad屬性來考察它們。如果你查看該方法的文檔,就會清楚地看到漸變是累積的。因此,每次我們使用梯度來更新參數時,我們都需要在之後將梯度歸零。這就是zero_()的好處。因此,讓我們拋棄手工計算梯度的方法,同時使用backward()和zero_()方法。就這些嗎? 嗯,差不多…但是,總是有一個陷阱,這一次它與參數的更新有關…lr = 1e-1n_epochs = 1000
torch.manual_seed(42)a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
for epoch in range(n_epochs): yhat = a + b * x_train_tensor error = y_train_tensor - yhat loss = (error ** 2).mean()
# No more manual computation of gradients! # a_grad = -2 * error.mean() # b_grad = -2 * (x_tensor * error).mean()
# We just tell PyTorch to work its way BACKWARDS from the specified loss! loss.backward() # Let's check the computed gradients... print(a.grad) print(b.grad)
# What about UPDATING the parameters? Not so fast...
# FIRST ATTEMPT # AttributeError: 'NoneType' object has no attribute 'zero_' # a = a - lr * a.grad # b = b - lr * b.grad # print(a)
# SECOND ATTEMPT # RuntimeError: a leaf Variable that requires grad has been used in an in-place operation. # a -= lr * a.grad # b -= lr * b.grad
# THIRD ATTEMPT # We need to use NO_GRAD to keep the update out of the gradient computation # Why is that? It boils down to the DYNAMIC GRAPH that PyTorch uses... with torch.no_grad(): a -= lr * a.grad b -= lr * b.grad
# PyTorch is "clingy" to its computed gradients, we need to tell it to let it go... a.grad.zero_() b.grad.zero_()
print(a, b)在第一次嘗試中,如果我們使用相同的更新結構如Numpy代碼,我們會得到下面的奇怪的錯誤,我們再次「失去」梯度而重新分配參數更新結果。因此,grad屬性為None,它會引發錯誤…# FIRST ATTEMPTtensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)AttributeError: 'NoneType' object has no attribute 'zero_'然後,我們稍微更改一下,在第二次嘗試中使用熟悉的就地Python賦值。而且,PyTorch再一次抱怨它並提出一個錯誤。# SECOND ATTEMPTRuntimeError: a leaf Variable that requires grad has been used in an in-place operation.為什麼? !事實證明,這是一個「好事過頭」的例子。罪魁禍首是PyTorch的能力,它能夠從每一個涉及到任何梯度計算張量或其依賴項的Python操作中構建一個動態計算圖。在下一節中,我們將深入討論動態計算圖的內部工作方式。
那麼,我們如何告訴PyTorch「後退」並讓我們更新參數,而不打亂它的動態計算圖呢? 這就是torch.no_grad()。no_grad()的好處。它允許我們對張量執行常規的Python操作,與PyTorch的計算圖無關。最後,我們成功地運行了我們的模型並獲得了結果參數。當然,它們與我們在純numpy實現中得到的那些差不多。# THIRD ATTEMPTtensor([1.0235], device='cuda:0', requires_grad=True)tensor([1.9690], device='cuda:0', requires_grad=True)目前神經網絡框架分為靜態圖框架和動態圖框架,PyTorch 和 TensorFlow、Caffe 等框架最大的區別就是他們擁有不同的計算圖表現形式。 TensorFlow 使用靜態圖,這意味著我們先定義計算圖,然後不斷使用它,而在 PyTorch 中,每次都會重新構建一個新的計算圖。對於使用者來說,兩種形式的計算圖有著非常大的區別,同時靜態圖和動態圖都有他們各自的優點,比如動態圖比較方便debug,使用者能夠用任何他們喜歡的方式進行debug,同時非常直觀,而靜態圖是通過先定義後運行的方式,之後再次運行的時候就不再需要重新構建計算圖,所以速度會比動態圖更快。PyTorchViz包及其make_dot(變量)方法允許我們輕鬆地可視化與給定Python變量關聯的圖。torch.manual_seed(42)a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat = a + b * x_train_tensorerror = y_train_tensor - yhatloss = (error ** 2).mean()如果我們調用make_dot(yhat),我們將得到下面圖中最左邊的圖形:藍方框:這些對應於我們用作參數的張量,也就是我們要求PyTorch計算梯度的張量;灰箱:包含梯度計算張量或其相依關係的Python操作;綠色方框:與灰色方框相同,只是它是漸變計算的起點(假設使用reverse()方法從用於可視化圖形的變量中調用)——它們是從圖形中的自底向上計算的。如果我們為error(中間)和loss(右邊)變量繪製圖形,那麼它們與第一個變量之間的惟一區別就是中間步驟的數量(灰色框)。現在,仔細看看最左邊的綠色方框:有兩個箭頭指向它,因為它將兩個變量a和b*x相加。然後,看一下同一圖形的灰框:它執行的是乘法,即b*x。但是只有一個箭頭指向它!箭頭來自於對應於參數b的藍色方框。為什麼我們沒有數據x的方框呢?答案是:我們不為它計算梯度!因此,即使計算圖所執行的操作涉及到更多的張量,也只顯示了梯度計算張量及其依賴關係。如果我們將參數a的requires_grad設為False,計算圖形會發生什麼變化?不出所料,與參數a對應的藍色框是no more!很簡單:沒有梯度,沒有圖形。動態計算圖最好的地方在於你可以讓它變得像你想要的那樣複雜。甚至可以使用控制流語句(例如,if語句)來控制梯度流(顯然!)到目前為止,我們一直在使用計算出的梯度手動更新參數。這對於兩個參數來說可能很好,但是如果我們有很多參數呢?我們使用PyTorch的一個優化器,比如SGD或Adam。優化器獲取我們想要更新的參數、我們想要使用的學習率(可能還有許多其他超參數!)並通過其step()方法執行更新。此外,我們也不需要一個接一個地將梯度歸零。我們只需調用優化器的zero_grad()方法就可以了! 在下面的代碼中,我們創建了一個隨機梯度下降(SGD)優化器來更新參數a和b。不要被優化器的名字所欺騙:如果我們一次使用所有的訓練數據進行更新——就像我們在代碼中所做的那樣——優化器執行的是批量梯度下降,而不是它的名字。torch.manual_seed(42)a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)print(a, b)
lr = 1e-1n_epochs = 1000
# Defines a SGD optimizer to update the parametersoptimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs): yhat = a + b * x_train_tensor error = y_train_tensor - yhat loss = (error ** 2).mean()
loss.backward()
# No more manual update! # with torch.no_grad(): # a -= lr * a.grad # b -= lr * b.grad optimizer.step()
# No more telling PyTorch to let gradients go! # a.grad.zero_() # b.grad.zero_() optimizer.zero_grad()
print(a, b)讓我們檢查一下之前和之後的兩個參數,以確保一切正常:# BEFORE: a, btensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)# AFTER: a, btensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)PyTorch集成了很多損失函數。在這個例子中我們使用的是MSE損失。注意nn.MSELoss實際上為我們創建了一個損失函數——它不是損失函數本身。此外,你還可以指定一個要應用的reduction method,即如何聚合單個點的結果—你可以對它們進行平均(約簡= ' mean '),或者簡單地對它們求和(約簡= ' sum ')。
然後在第20行使用創建的損失函數,根據我們的預測和標籤計算損失。torch.manual_seed(42)a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)print(a, b)
lr = 1e-1n_epochs = 1000
# Defines a MSE loss functionloss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs): yhat = a + b * x_train_tensor
# No more manual loss!# error = y_tensor - yhat# loss = (error ** 2).mean() loss = loss_fn(y_train_tensor, yhat)
loss.backward() optimizer.step() optimizer.zero_grad()
print(a, b)在PyTorch中,model由一個常規的Python類表示,該類繼承自Module類。__init__(self)定義了組成模型的兩個參數:a和b。模型可以包含其他模型作為它的屬性,所以可以很容易實現嵌套。
forward(self, x):它執行了實際的計算,也就是說,給定輸入x,它輸出一個預測。讓我們為我們的回歸任務構建一個適當的(但簡單的)模型。它應該是這樣的:class ManualLinearRegression(nn.Module): def __init__(self): super().__init__() # To make "a" and "b" real parameters of the model, we need to wrap them with nn.Parameter self.a = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float)) self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
def forward(self, x): # Computes the outputs / predictions return self.a + self.b * x在_init__方法中,我們定義了兩個參數,a和b,使用Parameter()類,告訴PyTorch應該將這些張量視為它們是的屬性的模型參數。我們為什麼要關心這個?通過這樣做,我們可以使用模型的parameters()方法來檢索所有模型參數的迭代器,甚至是那些嵌套模型的參數,我們可以使用它們來提供我們的優化器(而不是自己構建參數列表!) 此外,我們可以使用模型的state_dict()方法獲取所有參數的當前值。重要提示:我們需要將模型發送到數據所在的同一設備。如果我們的數據是由GPU張量構成的,我們的模型也必須「活」在GPU內部。
我們可以使用所有這些方便的方法來改變我們的代碼,應該是這樣的:torch.manual_seed(42)
# Now we can create a model and send it at once to the devicemodel = ManualLinearRegression().to(device)# We can also inspect its parameters using its state_dictprint(model.state_dict())
lr = 1e-1n_epochs = 1000
loss_fn = nn.MSELoss(reduction='mean')optimizer = optim.SGD(model.parameters(), lr=lr)
for epoch in range(n_epochs): # What is this?!? model.train()
# No more manual prediction! # yhat = a + b * x_tensor yhat = model(x_train_tensor)
loss = loss_fn(y_train_tensor, yhat) loss.backward() optimizer.step() optimizer.zero_grad()
print(model.state_dict())現在列印出來的語句將是這樣的--參數a和參數b的最終值仍然相同,所以一切正常。OrderedDict([('a', tensor([0.3367], device='cuda:0')), ('b', tensor([0.1288], device='cuda:0'))])OrderedDict([('a', tensor([1.0235], device='cuda:0')), ('b', tensor([1.9690], device='cuda:0'))])在PyTorch中,模型有一個train()方法,有點令人失望的是,它沒有執行訓練步驟。其唯一目的是將模型設置為訓練模式。為什麼這很重要?有些模型可能使用Dropout機制,在訓練和評估階段有不同的行為。在我們的模型中,我們手動創建了兩個參數來執行線性回歸。讓我們使用PyTorch的Linear模型作為我們自己的屬性,從而創建一個嵌套模型。儘管這顯然是一個人為設計的示例,因為我們幾乎是在包裝底層模型,而沒有向其添加任何有用的東西,但它很好地說明了這個概念。在_init__方法中,我們創建了一個包含嵌套線性模型的屬性。在forward()方法中,我們調用嵌套模型本身來執行forward傳遞(注意,我們沒有調用self.linear.forward(x))。class LayerLinearRegression(nn.Module): def __init__(self): super().__init__() # Instead of our custom parameters, we use a Linear layer with single input and single output self.linear = nn.Linear(1, 1)
def forward(self, x): # Now it only takes a call to the layer to make predictions return self.linear(x)現在,如果我們調用這個模型的parameters()方法,PyTorch將以遞歸方式顯示其屬性的參數。您可以使用類似於[*LayerLinearRegression().parameters()]的方法來獲得所有參數的列表。你還可以添加新的線性屬性,即使在前向傳遞中根本不使用它們,它們仍然會在parameters()下列出。我們的模型非常簡單……你可能會想:「為什麼要為它構建一個類呢?」「對於使用普通層的簡單模型,其中一層的輸出按順序作為下一層的輸入,我們可以使用Sequential模型。在我們的例子中,我們將使用單個參數構建一個序列模型,即我們用來訓練線性回歸的線性層。模型應該是這樣的:# Alternatively, you can use a Sequential modelmodel = nn.Sequential(nn.Linear(1, 1)).to(device)到目前為止,我們已經定義了優化器、損失函數和模型。向上滾動一點,快速查看循環中的代碼。如果我們使用不同的優化器,或者損失,甚至模型,它會改變嗎?如果不是,我們如何使它更通用?好吧,我想我們可以說所有這些代碼行執行一個訓練步驟,給定這三個元素(優化器、損失和模型)、特性和標籤。那麼,如何編寫一個函數來獲取這三個元素並返回另一個函數來執行一個訓練步驟,將一組特性和標籤作為參數並返回相應的損失呢?然後,我們可以使用這個通用函數來構建一個train_step()函數,以便在訓練循環中調用。現在我們的代碼應該是這樣的……看到訓練循環有多小?def make_train_step(model, loss_fn, optimizer): # Builds function that performs a step in the train loop def train_step(x, y): # Sets model to TRAIN mode model.train() # Makes predictions yhat = model(x) # Computes loss loss = loss_fn(y, yhat) # Computes gradients loss.backward() # Updates parameters and zeroes gradients optimizer.step() optimizer.zero_grad() # Returns the loss return loss.item()
# Returns the function that will be called inside the train loop return train_step
# Creates the train_step function for our model, loss function and optimizertrain_step = make_train_step(model, loss_fn, optimizer)losses = []
# For each epoch...for epoch in range(n_epochs): # Performs one train step and returns the corresponding loss loss = train_step(x_train_tensor, y_train_tensor) losses.append(loss)
# Checks model's parametersprint(model.state_dict())暫時把注意力放在我們的數據上……到目前為止,我們只是簡單地使用了由Numpy數組轉換而來的PyTorch張量。但我們可以做得更好,我們可以建立一個Pytorch張量數據。在PyTorch中,dataset由一個常規的Python類表示,該類繼承自dataset類。你可以將它的睦作一種Python元組列表,每個元組對應於一個數據點(特性,標籤)。它需要實現的最基本的方法是: __init__(self):它採取任何參數需要建立一個元組列表-它可能是一個名稱的CSV文件,將加載和處理;它可以是兩個張量,一個代表特徵,另一個代表標籤;或者其他的,取決於手頭的任務。不需要在構造函數方法中加載整個數據集。如果數據集很大(例如,成千上萬的圖像文件),立即加載它將是內存效率不高的。建議按需加載它們(無論何時調用了_get_item__)。
_get_item__(self, index):它允許數據集被索引,因此它可以像列表一樣工作(dataset)——它必須返回與請求的數據點對應的元組(特性,標籤)。我們可以返回預先加載的數據集或張量的相應切片,或者,如前所述,按需加載它們(如本例中所示)。__len__(self):它應該簡單地返回整個數據集的大小,這樣,無論什麼時候採樣它,它的索引都被限制在實際大小。讓我們構建一個簡單的自定義數據集,它接受兩個張量作為參數:一個用於特性,一個用於標籤。對於任何給定的索引,我們的數據集類將返回每個張量的對應切片。它應該是這樣的:from torch.utils.data import Dataset, TensorDataset
class CustomDataset(Dataset): def __init__(self, x_tensor, y_tensor): self.x = x_tensor self.y = y_tensor
def __getitem__(self, index): return (self.x[index], self.y[index])
def __len__(self): return len(self.x)
# Wait, is this a CPU tensor now? Why? Where is .to(device)?x_train_tensor = torch.from_numpy(x_train).float()y_train_tensor = torch.from_numpy(y_train).float()
train_data = CustomDataset(x_train_tensor, y_train_tensor)print(train_data[0])
train_data = TensorDataset(x_train_tensor, y_train_tensor)print(train_data[0])再一次,你可能會想「為什麼要在一個類中經歷這麼多麻煩來包裝幾個張量呢?」如果一個數據集只是兩個張量,那麼我們可以使用PyTorch的TensorDataset類,它將完成我們在上面的自定義數據集中所做的大部分工作。你注意到我們用Numpy數組構建了我們的訓練張量,但是我們沒有將它們發送到設備上嗎?所以,它們現在是CPU張量!為什麼?我們不希望我們的全部訓練數據都被加載到GPU張量中,就像我們到目前為止的例子中所做的那樣,因為它佔用了我們寶貴的顯卡RAM中的空間。到目前為止,我們在每個訓練步驟都使用了全部的訓練數據。一直以來都是批量梯度下降。這對於我們的小得可笑的數據集來說當然很好,但是對於一些大的數據集,我們必須使用小批量梯度下降。因此,我們需要小批量。因此,我們需要相應地分割數據集。因此我們使用PyTorch的DataLoader類來完成這項工作。我們告訴它使用哪個數據集(我們在前一節中剛剛構建的數據集)、所需的mini-batch處理大小,以及我們是否希望對其進行洗牌。我們的加載器將表現得像一個迭代器,因此我們可以循環它並每次獲取不同的mini-batch批處理。from torch.utils.data import DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)要檢索一個mini-batch批處理示例,只需運行下面的命令—它將返回一個包含兩個張量的列表,一個用於特徵,另一個用於標籤。重新看一下訓練循環,看一下這些是如何對循環做出改變的,我們來看看。losses = []train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs): for x_batch, y_batch in train_loader: # the dataset "lives" in the CPU, so do our mini-batches # therefore, we need to send those mini-batches to the # device where the model "lives" x_batch = x_batch.to(device) y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch) losses.append(loss)
print(model.state_dict())現在有兩件事不同了:我們不僅有一個內部循環來從DataLoader加載每個mini-batch批處理,而且更重要的是,我們現在只向設備發送一個mini-batch批處理。對於更大的數據集,使用Dataset的_get_item__將一個樣本一個樣本地加載(到一個CPU張量中),然後將屬於同一小批處理的所有樣本一次性發送到你的GPU(設備)是為了充分利用你的顯卡RAM的方法。此外,如果有許多gpu來訓練您的模型,那麼最好保持數據集「不可知」,並在訓練期間將這些批分配給不同的gpu。到目前為止,我們只關注訓練數據。我們為它建立了一個數據集和一個數據加載器。我們可以對驗證數據做同樣的事情,使用我們在這篇文章開始時執行的分割…或者我們可以使用random_split。PyTorch的random_split()方法是執行訓練驗證分離的一種簡單而熟悉的方法。請記住,在我們的示例中,我們需要將它應用到整個數據集(而不是我們在前兩節中構建的培訓數據集)。然後,對於每個數據子集,我們構建一個相應的DataLoader,因此我們的代碼如下:from torch.utils.data.dataset import random_split
x_tensor = torch.from_numpy(x).float()y_tensor = torch.from_numpy(y).float()
dataset = TensorDataset(x_tensor, y_tensor)
train_dataset, val_dataset = random_split(dataset, [80, 20])
train_loader = DataLoader(dataset=train_dataset, batch_size=16)val_loader = DataLoader(dataset=val_dataset, batch_size=20)我們需要更改訓練循環,以包括對模型的評估,即計算驗證損失。第一步是包含另一個內部循環來處理來自驗證加載程序的mini-batch,將它們發送到與我們的模型相同的設備。接下來,我們使用模型進行預測,並計算相應的損失。torch_grad():雖然在我們的簡單模型中沒有什麼不同,但是使用這個上下文管理器來包裝驗證內部循環是一個很好的實踐,這樣可以禁用您可能無意中觸發的任何梯度計算——梯度屬於訓練,而不是驗證步驟;eval():它所做的唯一一件事就是將模型設置為評估模式(就像它的train()對手所做的那樣),這樣模型就可以根據某些操作(比如Dropout)調整自己的行為。losses = []val_losses = []train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):for x_batch, y_batch in train_loader:x_batch = x_batch.to(device)y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)losses.append(loss)
with torch.no_grad():for x_val, y_val in val_loader:x_val = x_val.to(device)y_val = y_val.to(device)
model.eval()yhat = model(x_val)val_loss = loss_fn(y_val, yhat)val_losses.append(val_loss.item())
print(model.state_dict())希望在完成本文中所有的代碼後,你能夠更好地理解PyTorch官方教程,並更輕鬆地學習它。Understanding PyTorch with an example: a step-by-step tutorial