戰(zhàn)筆記:從環(huán)境搭建到訓(xùn)練避坑的完整指南)
簡(jiǎn)介這份PyTorch學(xué)習(xí)課件以PPT形式呈現(xiàn)面向深度學(xué)習(xí)入門者與希望系統(tǒng)了解PyTorch框架的讀者。課件內(nèi)容從人工智能與神經(jīng)網(wǎng)絡(luò)的基本概念展開依次講解深度學(xué)習(xí)原理、Anaconda與PyCharm環(huán)境配置、張量基礎(chǔ)以及FCNN、CNN、RNN、注意力機(jī)制等常見網(wǎng)絡(luò)結(jié)構(gòu)。關(guān)于PyTorch框架重點(diǎn)演示了定義模型類、配置網(wǎng)絡(luò)層和編寫forward前向傳播的完整流程并配有Hello World示例方便零基礎(chǔ)學(xué)習(xí)者對(duì)照操作。資源包共包含1個(gè)PPTX文件大小僅1.17MB內(nèi)容精煉可作為課堂講義或自學(xué)預(yù)習(xí)材料使用。目前已有2991人學(xué)習(xí)過該資源適合希望快速入門PyTorch、理解核心概念并上手實(shí)踐的讀者。1. 搜“pytorch學(xué)習(xí)課件”的人真正缺的是能跑的實(shí)驗(yàn)搜過“pytorch學(xué)習(xí)課件”的人大多下載過一堆 PDF 和 PPT然后在收藏夾里堆到吃灰。真正動(dòng)手寫代碼時(shí)卡住你的從來不是某個(gè)公式?jīng)]看懂而是報(bào)錯(cuò)看不懂、維度對(duì)不上、顯卡驅(qū)動(dòng)不對(duì)勁。課件給你的是知識(shí)圖譜沒人替你處理訓(xùn)練程序的運(yùn)行問題。這篇筆記換個(gè)角度不整理課件目錄而是把 PyTorch 入門拆成環(huán)境、數(shù)據(jù)、模型、訓(xùn)練四條線每條線給你能直接跑起來的最小代碼和參數(shù)解釋再附上我在訓(xùn)練里踩過的高頻坑。適合三類人剛交完機(jī)器學(xué)習(xí)理論課想動(dòng)手的學(xué)生、第一次從別的框架切換過來的開發(fā)者以及準(zhǔn)備拿 PyTorch 做實(shí)驗(yàn)但一直不敢開始的初學(xué)者。反直覺結(jié)論寫在前面學(xué) PyTorch 最快的方式不是把課件看完而是把報(bào)錯(cuò)讀順。2. 環(huán)境先于課件conda 虛擬環(huán)境與第一個(gè)張量測(cè)試我看過不少人的第一個(gè) PyTorch 程序是從課件里復(fù)制粘貼的結(jié)果沒跑通因?yàn)榄h(huán)境沒先解決。課件往往默認(rèn)你已經(jīng)有一臺(tái)能用的機(jī)器不做這個(gè)假設(shè)后面全是空談。所以真正翻開課件講張量、講自動(dòng)求導(dǎo)之前我會(huì)先花十分鐘把環(huán)境裝到“敢隨便刪、敢隨便重建”的狀態(tài)。2.1 為什么課件里不會(huì)幫你解決環(huán)境斷層我一般會(huì)建議用 conda 單獨(dú)建一個(gè)虛擬環(huán)境而不是把包直接裝進(jìn)系統(tǒng) Python。原因不是系統(tǒng) Python 不能用而是你后面會(huì)不停地裝各種依賴某天裝了一個(gè)和 PyTorch 沖突的包系統(tǒng)環(huán)境就廢了。虛擬環(huán)境的好處是給你留了后悔藥環(huán)境壞了直接刪掉重建不碰系統(tǒng)里其他項(xiàng)目。conda create -n pytorch-learn python3.9 -y conda activate pytorch-learn python --version pip --version pip install torch torchvisionpython3.9是我目前的習(xí)慣不是必須。版本號(hào)選一個(gè)相對(duì)成熟的即可沒必要追最新版很多依賴庫對(duì)太新的 Python 版本支持會(huì)慢半拍。conda create -n pytorch-learn里的名字可以隨便改我習(xí)慣用項(xiàng)目名方便之后一眼認(rèn)出。后面兩行是確認(rèn)當(dāng)前環(huán)境里用的到底是哪個(gè) Python 和 pip這一步能避免很多“明明裝了卻找不到包”的玄學(xué)問題。裝完以后別急著關(guān)終端先驗(yàn)證一下安裝位置和版本。常見做法是打印包路徑確認(rèn)你的torch確實(shí)來自剛剛激活的虛擬環(huán)境而不是系統(tǒng)里某個(gè)殘留。這一步不寫進(jìn)課件但能幫你省下一個(gè)晚上的排查時(shí)間。2.2 最小張量測(cè)試先把 dtype、shape、device 三個(gè)概念跑一遍課件里畫張量永遠(yuǎn)是對(duì)的你手里的張量永遠(yuǎn)是 shape 不對(duì)的。所以我建議的下一步不是去看模型代碼而是寫一個(gè)不到十行的張量測(cè)試把 dtype、shape、device 三個(gè)概念親手過一遍。import torch x torch.tensor([[1.0, 2.0], [3.0, 4.0]]) print(x.dtype) # torch.float32 print(x.shape) # torch.Size([2, 2]) print(x.device) # cpu on cpu print(torch.cuda.is_available()) if torch.cuda.is_available(): x x.to(cuda) print(x.device)dtype決定數(shù)據(jù)在內(nèi)存里怎么存默認(rèn)是float32課件里不會(huì)強(qiáng)調(diào)它但真實(shí)訓(xùn)練中凡是報(bào)“expected scalar type Float but found Double”的錯(cuò)基本都是你給了一個(gè)float64的張量。shape是報(bào)錯(cuò)重災(zāi)區(qū)幾乎所有維度不匹配的報(bào)錯(cuò)都能通過打印 shape 快速定位。device更是關(guān)鍵CPU 上的張量和 GPU 上的張量不能直接做運(yùn)算這個(gè)錯(cuò)誤在入門前兩周會(huì)見很多次。再補(bǔ)一個(gè)隨機(jī)數(shù)種子的習(xí)慣。你的模型一旦初始化隨機(jī)種子不同結(jié)果就不同。為了讓實(shí)驗(yàn)“能復(fù)現(xiàn)”我建議在腳本開頭固定它。torch.manual_seed(0) y torch.randn(3, 4) print(y)固定了種子別人才有機(jī)會(huì)復(fù)現(xiàn)你的結(jié)果你自己調(diào)參時(shí)也能確定改動(dòng)來自參數(shù)而不是隨機(jī)波動(dòng)。這一步做完環(huán)境才算真正立住了。接下來進(jìn)入數(shù)據(jù)部分這里才是新手和熟手拉開差距的地方。3. 數(shù)據(jù)管道是第一個(gè)分水嶺從自定義 Dataset 到 DataLoader大部分人看課件看到數(shù)據(jù)讀取就會(huì)跳過因?yàn)檎n件用的都是內(nèi)置數(shù)據(jù)集一行datasets.XXX就完事。真實(shí)項(xiàng)目里你需要處理自己的圖片、自己的 CSV這時(shí)候數(shù)據(jù)管道的設(shè)計(jì)能力直接決定訓(xùn)練到底能不能跑起來。我見過最典型的翻車是把圖片讀取寫在訓(xùn)練循環(huán)里每個(gè) batch 都重新讀一次磁盤訓(xùn)練速度慢十倍。數(shù)據(jù)管道這件事值得單獨(dú)拿出來講清楚。3.1 自定義 Dataset三步補(bǔ)齊__init__、__len__、__getitem__PyTorch 的Dataset不是一個(gè)需要實(shí)現(xiàn)復(fù)雜邏輯的基類它只要求你實(shí)現(xiàn)三個(gè)方法__init__負(fù)責(zé)記錄路徑和標(biāo)簽__len__返回樣本總數(shù)__getitem__根據(jù)索引返回一個(gè)樣本。最常見做法是文件夾里放一批圖片配一個(gè) CSV 文件記錄每張圖的名字和標(biāo)簽。import os from PIL import Image from torch.utils.data import Dataset class MyDataset(Dataset): def __init__(self, img_dir, label_file, transformNone): self.img_dir img_dir self.transform transform self.samples [] with open(label_file, r, encodingutf-8) as f: for line in f: img_name, label line.strip().split(,) self.samples.append((img_name, int(label))) def __len__(self): return len(self.samples) def __getitem__(self, index): img_name, label self.samples[index] img_path os.path.join(self.img_dir, img_name) image Image.open(img_path).convert(RGB) if self.transform is not None: image self.transform(image) return image, label__init__里的加載邏輯盡量只做一次不要在__getitem__里反復(fù)讀取 CSV否則每次取樣本都會(huì)做重復(fù) IO。__getitem__接收的index由 DataLoader 內(nèi)部的 sampler 傳進(jìn)來你不需要自己維護(hù)順序。返回的值一般是一個(gè)元組(數(shù)據(jù), 標(biāo)簽)數(shù)據(jù)可以是張量也可以是 PIL 圖片后面讓 transform 統(tǒng)一處理。Image.open(...).convert(RGB)是為了確保所有圖片都是三通道避免某張灰度圖或者 RGBA 圖在后續(xù)進(jìn)入模型時(shí) shape 不一致。這個(gè)細(xì)節(jié)我在實(shí)際項(xiàng)目里踩過一張透明的 PNG 就能讓整個(gè)訓(xùn)練報(bào)錯(cuò)。3.2 DataLoader 的四參數(shù)權(quán)衡batch_size、shuffle、num_workers、pin_memoryDataset 定義了“怎么取一個(gè)樣本”DataLoader 負(fù)責(zé)“怎么把這些樣本組成一批、并高效送給模型”。新手最容易在這里照抄默認(rèn)值不調(diào)導(dǎo)致訓(xùn)練慢或者直接卡死。from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ) ]) train_loader DataLoader( MyDataset(images, labels.csv, transformtransform), batch_size32, shuffleTrue, num_workers4, pin_memoryTrue )先解釋 transform 這一塊。Resize((224, 224))把圖片統(tǒng)一到模型期望的輸入大小ToTensor()把 PIL 圖片轉(zhuǎn)成[C, H, W]的張量順便把像素值從 0 到 255 縮放到 0 到 1Normalize用均值 0.485、0.456、0.406 和方差 0.229、0.224、0.225 做標(biāo)準(zhǔn)化。這組數(shù)值來自大規(guī)模圖像分類任務(wù)的經(jīng)驗(yàn)統(tǒng)計(jì)值做遷移學(xué)習(xí)時(shí)沿用它們通常是安全的。再說四個(gè)參數(shù)。batch_size32是一個(gè)相對(duì)穩(wěn)的起點(diǎn)顯存小換 16顯存充足換 64常見做法是數(shù)著顯存來。shuffleTrue只能用在訓(xùn)練集驗(yàn)證集和測(cè)試集應(yīng)該設(shè)False否則每輪驗(yàn)證的數(shù)據(jù)順序一直在變指標(biāo)波動(dòng)會(huì)掩蓋真實(shí)改進(jìn)。num_workers4在 Linux 上通常能跑但在 Windows 上經(jīng)常出現(xiàn)“卡住不動(dòng)、子進(jìn)程反復(fù)報(bào)錯(cuò)”的情況我給你的建議是先在 Windows 上設(shè) 0 確認(rèn)能跑再慢慢往上加。pin_memoryTrue配合 GPU 訓(xùn)練值得默認(rèn)打開它讓 CPU 側(cè)的數(shù)據(jù)在傳輸?shù)?GPU 前鎖頁能減少一部分拷貝時(shí)間。到這里數(shù)據(jù)牌局的底層就搭好了。接著模型部分先帶你拆掉一個(gè)叫“黑匣子”的顧慮。4. 模型搭建把 forward 從黑匣子改成你能說清的結(jié)構(gòu)很多人看到nn.Module就覺得是黑匣子覺得只需要把官方代碼貼進(jìn)來就能跑。其實(shí)它只是一層輕封裝核心邏輯是你自己寫的forward。把構(gòu)造和計(jì)算分開想明白模型對(duì)你就不再是黑匣子。4.1 nn.Module 的拆分邏輯構(gòu)造層與 forward 各自管什么拿一個(gè)最簡(jiǎn)的圖像分類模型舉例輸入是 28×28 的灰度圖處理成 784 維的向量后過兩層全連接。import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(784, 128) self.relu nn.ReLU() self.fc2 nn.Linear(128, 10) def forward(self, x): x self.fc1(x) x self.relu(x) x self.fc2(x) return x model SimpleNet() print(model)__init__里做的事是“把層建出來”相當(dāng)于先準(zhǔn)備好積木forward里做的事是“把數(shù)據(jù)流過這些層”相當(dāng)于規(guī)定積木怎么搭。一個(gè)常見誤區(qū)是直接在forward里新建層比如fc nn.Linear(784, 128)然后每次調(diào)用都重新創(chuàng)建一份這樣不僅參數(shù)沒被注冊(cè)每次前向傳播都會(huì)產(chǎn)生新的隨機(jī)參數(shù)模型永遠(yuǎn)學(xué)不會(huì)。正確的做法是在__init__里完成任務(wù)forward里只負(fù)責(zé)調(diào)用。第一層的輸入維度 784 是手算出來的28×28 展平成 784不是自動(dòng)推斷的。Pytorch 的nn.Linear不會(huì)幫你自動(dòng)推算上一層輸出所以你經(jīng)??吹降谝粋€(gè)前向報(bào)錯(cuò)“mat1 and mat2 shapes cannot be multiplied”原因多半是第一層維度寫錯(cuò)。處理圖像數(shù)據(jù)時(shí)常見做法是先在代碼里打印一行x.shape確認(rèn)進(jìn)入全連接層之前的數(shù)據(jù)形狀再寫 Linear 的輸入維度。4.2 parameters() 與 requires_grad讓模型參數(shù)“指哪打哪”模型搭完以后你需要回答一個(gè)問題優(yōu)化器到底在更新哪些參數(shù)答案是model.parameters()它是所有帶requires_gradTrue的參數(shù)的集合。用下面這段可以看得一清二楚for name, param in model.named_parameters(): print(name, param.shape, param.requires_grad)輸出里你會(huì)看到fc1.weight、fc1.bias、fc2.weight、fc2.bias這就是這個(gè)模型的全部家當(dāng)。named_parameters()里的名字和__init__里的屬性名是對(duì)應(yīng)的這也能反過來驗(yàn)證你有沒有把層正確注冊(cè)到模塊里。如果某個(gè)層沒出現(xiàn)在這個(gè)列表里說明它沒有被當(dāng)成參數(shù)管理優(yōu)化器自然也不會(huì)管它。優(yōu)化器接收的也正是這個(gè)optimizer torch.optim.Adam(model.parameters(), lr1e-3)lr1e-3是一個(gè)比較通用的起點(diǎn)。所謂調(diào)參玄學(xué)其實(shí)大部分時(shí)候教你做兩件事一是遇到 loss 不降時(shí)把學(xué)習(xí)率調(diào)小一個(gè)量級(jí)二是明確你要更新哪些參數(shù)。requires_gradFalse可以讓某個(gè)層凍結(jié)不參與訓(xùn)練這在遷移學(xué)習(xí)里非常常用把預(yù)訓(xùn)練骨干網(wǎng)絡(luò)的參數(shù)凍住只微調(diào)最后幾層。這個(gè)操作帶來的速度提升和防止過擬合效果往往比換一個(gè)復(fù)雜模型還明顯。freeze 之后如果你反悔了把param.requires_grad True改回去就行這就是后悔藥。5. 訓(xùn)練階段的避坑筆記三條高頻翻車與排查順序訓(xùn)練循環(huán)本身代碼很固定真正讓人掉頭發(fā)的是“看起來在訓(xùn)練但結(jié)果完全不對(duì)”。這一章我按“現(xiàn)象 → 原因 → 解決”的節(jié)奏寫三條血淚經(jīng)驗(yàn)。排查的時(shí)候我建議先把批次調(diào)小在 CPU 上跑通一次完整前向再換 GPU這個(gè)順序能篩掉一大半低級(jí)錯(cuò)誤。5.1 現(xiàn)象loss 飆升到 NaN原因是學(xué)習(xí)率和輸入分布一起失控最典型的訓(xùn)練事故是前面幾輪 loss 還在下降突然某一步變成nan之后再也回不來了。我見過有人遇到 NaN 后瘋狂改網(wǎng)絡(luò)結(jié)構(gòu)但其實(shí)根源往往不在結(jié)構(gòu)而在數(shù)值穩(wěn)定性。排查順序先看學(xué)習(xí)率。學(xué)習(xí)率太大梯度一步跨過頭權(quán)重更新后輸出直接爆炸幾輪就會(huì)nan。解決方法是把lr從1e-3降到1e-4跑 20 輪看趨勢(shì)。第二步看輸入分布如果圖片沒做 Normalize像素值范圍過大或者標(biāo)簽值不在[0, 類別數(shù)-1]區(qū)間內(nèi)交叉熵很容易算出 NaN。最后可以打印梯度范數(shù)確認(rèn)梯度是否異常放大。# 在 loss.backward() 前后各打一行先定位是 loss 異常還是梯度異常 print(loss:, loss.item()) loss.backward() for name, param in model.named_parameters(): if param.grad is not None: print(name, param.grad.abs().mean().item())loss.item()里那個(gè).item()很重要它把只含一個(gè)值的張量轉(zhuǎn)成普通 Python 數(shù)字避免在打印時(shí)把整個(gè)計(jì)算圖的歷史帶出來。如果 loss 本身已經(jīng)是nan問題多半出在模型輸出或標(biāo)簽構(gòu)造如果 loss 正常但梯度打印出來是nan問題多在反向傳播過程中的數(shù)值溢出。5.2 現(xiàn)象訓(xùn)練集 acc 很高驗(yàn)證集卻一塌糊涂看到這個(gè)現(xiàn)象時(shí)第一反應(yīng)不一定是過擬合先查兩個(gè)更容易犯的錯(cuò)數(shù)據(jù)劃分泄漏和驗(yàn)證流程寫錯(cuò)。數(shù)據(jù)泄漏的常見樣子是你先把整個(gè)數(shù)據(jù)集做了歸一化再切訓(xùn)練集和驗(yàn)證集。這樣驗(yàn)證集的分布已經(jīng)被訓(xùn)練集的信息修正過得到的 acc 虛高換到真實(shí)場(chǎng)景立刻露餡。正確的做法是先洗牌、再劃分、最后再算歸一化參數(shù)而且歸一化參數(shù)只能用訓(xùn)練集算。另一個(gè)非常隱蔽的泄漏是數(shù)據(jù)增強(qiáng)泄漏驗(yàn)證集不能做和訓(xùn)練集一樣的隨機(jī)裁剪、翻轉(zhuǎn)只能做縮放和標(biāo)準(zhǔn)化。驗(yàn)證流程寫錯(cuò)更常見也是最容易被忽略的推理時(shí)忘了model.eval()或者忘了關(guān)梯度。model.eval() with torch.no_grad(): val_logits model(val_data)model.eval()會(huì)切換 Dropout 和 BatchNorm 的行為讓它們不再使用訓(xùn)練期的隨機(jī)丟棄或批次統(tǒng)計(jì)量。torch.no_grad()則讓本次前向不建立計(jì)算圖省顯存也省時(shí)間而且結(jié)果更穩(wěn)定。很多神秘的高訓(xùn)練 acc、低驗(yàn)證 acc有一半的原因是少了這兩行。5.3 現(xiàn)象數(shù)據(jù)加載越跑越慢或者直接 OOM第一種是時(shí)間維度的翻車前幾個(gè) epoch 很快后面越來越慢。這通常不是模型變慢而是數(shù)據(jù)管道堆積。Windows 上尤其容易出現(xiàn)num_workers子進(jìn)程反復(fù)報(bào)錯(cuò)導(dǎo)致主進(jìn)程卡死或者你每輪循環(huán)里無意中創(chuàng)建了新的 Dataset 實(shí)例舊對(duì)象沒有釋放內(nèi)存越占越多。我的建議是 Windows 下先num_workers0跑通再逐步提高到 4 或 8。Linux 下也建議用任務(wù)管理器觀察內(nèi)存確認(rèn)沒有持續(xù)上漲。第二種是顯存維度的翻車CUDA out of memory。常見原因有三個(gè)忘了optimizer.zero_grad()梯度不斷累加圖越掛越大batch_size 設(shè)置過大驗(yàn)證階段沒開no_grad()。標(biāo)準(zhǔn)訓(xùn)練循環(huán)里梯度清零的位置是固定的for epoch in range(epochs): for data, target in train_loader: optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step()zero_grad()必須放在backward()之前不能放在step()之后。如果漏了這一步默認(rèn)行為是梯度累加而不是覆蓋——這在某些特殊場(chǎng)景是需要的但對(duì)大多數(shù)人來說就是 OOM 和訓(xùn)練不穩(wěn)的來源。如果顯存仍然吃緊先把batch_size減半再檢查是否在驗(yàn)證階段用torch.no_grad()包住了前向。這一章的每一條都值得寫進(jìn)你自己的實(shí)驗(yàn)筆記里。最后收尾我想分享三個(gè)我在反復(fù)踩坑后沉淀下來的小習(xí)慣。6. 把課件改造成你自己的調(diào)參記錄三個(gè)值得堅(jiān)持的小習(xí)慣課件是別人的知識(shí)結(jié)構(gòu)你的實(shí)驗(yàn)筆記才是自己的知識(shí)結(jié)構(gòu)。與其把課件從頭到尾抄一遍不如讓它為你的一手記錄服務(wù)。我目前最常用的做法是每看一章課件就把它改造成一個(gè)能獨(dú)立運(yùn)行的最小腳本跑出結(jié)果后再把結(jié)論寫回自己的筆記里。這樣筆記里每一句都對(duì)應(yīng)一段你親手驗(yàn)證過的代碼而不是一段摘抄。第二個(gè)習(xí)慣是給每次實(shí)驗(yàn)做一行記錄。這個(gè)習(xí)慣看起來笨但能救命。記錄項(xiàng)可以非常簡(jiǎn)單時(shí)間模型結(jié)構(gòu)lrbatch_size隨機(jī)種子最終loss備注第1輪實(shí)驗(yàn)SimpleNet1e-33200.42基線第2輪實(shí)驗(yàn)SimpleNet1e-43200.31lr下調(diào)第3輪實(shí)驗(yàn)SimpleNet1e-464420.28加大batch這張表寫滿十行之后你對(duì)“哪個(gè)參數(shù)改壞了”會(huì)變得非常敏感。調(diào)參的后悔藥不是一道命令而是這份記錄。它能讓你在三天之后還能回答“上一個(gè)能跑的組合到底是什么”。第三個(gè)習(xí)慣是訓(xùn)練報(bào)錯(cuò)時(shí)先看最后一行。Python 的 traceback 會(huì)把真正出錯(cuò)的代碼行放在最下面往上翻是調(diào)用棧。新手最常見的做法是從頭讀讀三分鐘發(fā)現(xiàn)根本看不懂。我現(xiàn)在的習(xí)慣是先讀最后一行找到文件名和行號(hào)再往上翻到我的業(yè)務(wù)代碼那一層而不是去看 PyTorch 內(nèi)部的源碼。大多數(shù)錯(cuò)誤不是庫的問題是你的 shape、設(shè)備或數(shù)據(jù)類型的問題。我早期帶 A 同學(xué)做圖像分類作業(yè)時(shí)他就是把課件從頭看到尾三天沒寫出一個(gè)能跑的腳本后來改成先跑通一個(gè)最簡(jiǎn)單的全連接版本再逐步換成卷積網(wǎng)絡(luò)第二個(gè)晚上就出了結(jié)果。那次之后我養(yǎng)成一個(gè)習(xí)慣新學(xué)一個(gè)模塊先寫一個(gè)最小 demo再回到課件補(bǔ)理論。這個(gè)順序幫我省下無數(shù)冤枉時(shí)間也是這篇筆記最想讓你帶走的一件事。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取