戰(zhàn):從零自研輕量級(jí)神經(jīng)網(wǎng)絡(luò)全流程)
1. 項(xiàng)目緣起與整體設(shè)計(jì)思路1.1 為什么要在普通顯卡上自研神經(jīng)網(wǎng)絡(luò)先說背景。我手頭只有一張消費(fèi)級(jí)顯卡顯存8GB還是幾年前的老架構(gòu)。每次看到那些動(dòng)輒需要A100集群的模型訓(xùn)練方案心里就癢——難道普通人就玩不了神經(jīng)網(wǎng)絡(luò)了嗎這個(gè)項(xiàng)目就是在這種不甘心的驅(qū)動(dòng)下開始的。核心目標(biāo)很明確用一張普通顯卡從零搭建一個(gè)能跑通、能訓(xùn)練、能出結(jié)果的神經(jīng)網(wǎng)絡(luò)。不依賴任何云端算力不租用任何遠(yuǎn)程服務(wù)器所有代碼和數(shù)據(jù)都在本地完成閉環(huán)。這個(gè)定位決定了整個(gè)項(xiàng)目的設(shè)計(jì)思路——輕量化、模塊化、可復(fù)現(xiàn)。為什么強(qiáng)調(diào)“自研”而不是直接調(diào)庫因?yàn)檎{(diào)庫雖然快但你對(duì)網(wǎng)絡(luò)內(nèi)部到底發(fā)生了什么始終隔著一層紗。自己寫一遍前向傳播、反向傳播、參數(shù)更新哪怕只是一個(gè)小型網(wǎng)絡(luò)你對(duì)梯度消失、學(xué)習(xí)率震蕩、過擬合這些概念的理解會(huì)完全不一樣。這就像學(xué)做菜看一百遍視頻不如自己下廚炒一盤。這個(gè)項(xiàng)目適合誰三類人一是剛?cè)腴T深度學(xué)習(xí)、想通過手寫代碼加深理解的學(xué)生二是手頭算力有限、但又想跑自己想法的小型開發(fā)者三是想給孩子或?qū)W生做演示、需要一套能在普通電腦上跑起來的教學(xué)案例的人。不需要你有分布式訓(xùn)練經(jīng)驗(yàn)也不需要你懂CUDA底層優(yōu)化只要會(huì)Python基礎(chǔ)、知道矩陣乘法是怎么回事就能跟著走下來。1.2 整體架構(gòu)選型與取舍邏輯整個(gè)項(xiàng)目我拆成了四個(gè)獨(dú)立模塊數(shù)據(jù)管道、網(wǎng)絡(luò)定義、訓(xùn)練循環(huán)、評(píng)估與可視化。為什么這么拆因?yàn)槠胀@卡的顯存和算力都有限任何一個(gè)環(huán)節(jié)出問題都會(huì)導(dǎo)致整個(gè)訓(xùn)練崩潰。模塊化之后每個(gè)部分可以單獨(dú)調(diào)試比如數(shù)據(jù)管道可以先在CPU上跑通確認(rèn)沒問題再接GPU。網(wǎng)絡(luò)結(jié)構(gòu)上我沒有一上來就搞Transformer或者ResNet這種大塊頭。原因很簡單——普通顯卡跑不動(dòng)而且對(duì)于理解原理來說過大的網(wǎng)絡(luò)反而會(huì)掩蓋很多細(xì)節(jié)。我選擇的是一個(gè)三層全連接網(wǎng)絡(luò)加兩個(gè)卷積層的混合結(jié)構(gòu)參數(shù)量控制在50萬以內(nèi)。這個(gè)規(guī)模在8GB顯存上可以輕松跑batch size 64甚至128訓(xùn)練速度也能接受。框架選擇上我用的是PyTorch但刻意避開了高層封裝。比如我沒有用nn.Sequential一把梭而是手動(dòng)定義每一層的forward邏輯。優(yōu)化器也是手動(dòng)實(shí)現(xiàn)SGD加動(dòng)量而不是直接調(diào)torch.optim.SGD。這么做的好處是每一行代碼你都知道它在干什么出了問題也能快速定位。注意手動(dòng)實(shí)現(xiàn)優(yōu)化器時(shí)參數(shù)更新那一步一定要用torch.no_grad()包裹否則PyTorch會(huì)自動(dòng)構(gòu)建計(jì)算圖顯存會(huì)爆炸。這個(gè)坑我踩過訓(xùn)練到第二個(gè)batch就OOM了。數(shù)據(jù)方面我用的是一個(gè)公開的小型圖像數(shù)據(jù)集大概一萬張圖片分辨率統(tǒng)一縮放到32x32。為什么選這么小的分辨率因?yàn)槠胀@卡的瓶頸往往在顯存帶寬而不是算力小分辨率意味著更小的特征圖顯存占用直線下降。實(shí)測(cè)下來32x32的輸入在8GB卡上可以開到batch size 256訓(xùn)練速度比128x128快將近四倍。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 數(shù)據(jù)管道的輕量化處理技巧數(shù)據(jù)管道是整個(gè)訓(xùn)練流程的“供水系統(tǒng)”如果這里堵了后面再好的網(wǎng)絡(luò)也跑不起來。我的做法是預(yù)先把所有圖片解碼、縮放、歸一化然后打包成一個(gè)二進(jìn)制文件。為什么不在訓(xùn)練時(shí)實(shí)時(shí)讀取因?yàn)槠胀@卡的算力本來就緊張如果每個(gè)batch都去讀硬盤、解碼JPEGCPU會(huì)成為瓶頸GPU利用率可能連30%都不到。具體操作寫一個(gè)預(yù)處理腳本遍歷所有圖片用Pillow讀取后縮放到32x32轉(zhuǎn)成numpy數(shù)組減去均值除以標(biāo)準(zhǔn)差最后用np.save存成一個(gè)大的.npy文件。這個(gè)文件大概幾百M(fèi)B一次性加載到內(nèi)存里訓(xùn)練時(shí)直接切片取batch。實(shí)測(cè)下來GPU利用率能從40%提升到85%以上。import numpy as np from PIL import Image import os def preprocess_data(image_dir, output_path): images [] labels [] for idx, filename in enumerate(os.listdir(image_dir)): img Image.open(os.path.join(image_dir, filename)).convert(RGB) img img.resize((32, 32)) arr np.array(img, dtypenp.float32) / 255.0 arr (arr - 0.5) / 0.5 images.append(arr) labels.append(idx % 10) # 假設(shè)10分類 np.save(output_path _images.npy, np.array(images)) np.save(output_path _labels.npy, np.array(labels))提示歸一化時(shí)減0.5除0.5是為了把像素值映射到[-1, 1]區(qū)間這對(duì)訓(xùn)練穩(wěn)定性幫助很大。如果你用ReLU激活函數(shù)其實(shí)映射到[0, 1]也可以但實(shí)測(cè)[-1, 1]收斂更快。還有一個(gè)細(xì)節(jié)數(shù)據(jù)增強(qiáng)不要放在訓(xùn)練循環(huán)里做。我一開始在__getitem__里隨機(jī)裁剪、翻轉(zhuǎn)結(jié)果每個(gè)epoch要多花好幾分鐘在CPU上。后來改成預(yù)處理階段就生成增強(qiáng)后的副本比如每張圖翻轉(zhuǎn)一次數(shù)據(jù)集直接翻倍。雖然占更多內(nèi)存但訓(xùn)練速度提升明顯。2.2 網(wǎng)絡(luò)定義中的參數(shù)初始化與激活函數(shù)選擇網(wǎng)絡(luò)定義這部分最容易出問題的是參數(shù)初始化。我試過全零初始化結(jié)果所有神經(jīng)元的梯度完全一樣網(wǎng)絡(luò)根本學(xué)不到東西。也試過標(biāo)準(zhǔn)差0.1的高斯初始化結(jié)果深層網(wǎng)絡(luò)的激活值越來越小到第三層就接近零了梯度傳不回去。最后采用的是He初始化也就是標(biāo)準(zhǔn)差為sqrt(2 / fan_in)的高斯分布其中fan_in是輸入連接數(shù)。這個(gè)初始化方法專門針對(duì)ReLU激活函數(shù)設(shè)計(jì)能保證每一層的輸出方差大致相等。實(shí)測(cè)下來用He初始化后網(wǎng)絡(luò)在第三個(gè)epoch就開始明顯收斂而之前用普通高斯初始化要到第十個(gè)epoch才有動(dòng)靜。def he_init(layer): fan_in layer.weight.shape[1] * layer.weight.shape[2] * layer.weight.shape[3] \ if len(layer.weight.shape) 4 else layer.weight.shape[1] std np.sqrt(2.0 / fan_in) layer.weight.data.normal_(0, std) if layer.bias is not None: layer.bias.data.zero_()激活函數(shù)我選的是LeakyReLU而不是普通ReLU。為什么因?yàn)槠胀≧eLU在負(fù)半軸梯度為零一旦某個(gè)神經(jīng)元輸出變成負(fù)數(shù)它就“死”了再也活不過來。LeakyReLU在負(fù)半軸給一個(gè)很小的斜率比如0.01這樣即使輸出為負(fù)梯度也能傳回去。這個(gè)改動(dòng)讓我的網(wǎng)絡(luò)在訓(xùn)練后期少了很多“突然不收斂”的情況。注意LeakyReLU的負(fù)斜率不要設(shè)太大0.01到0.1之間比較合適。我試過0.3結(jié)果網(wǎng)絡(luò)變得很不穩(wěn)定loss震蕩得厲害。2.3 訓(xùn)練循環(huán)中的學(xué)習(xí)率調(diào)度與梯度裁剪訓(xùn)練循環(huán)是整個(gè)項(xiàng)目最核心的部分也是最容易翻車的地方。我總結(jié)下來學(xué)習(xí)率調(diào)度和梯度裁剪是兩個(gè)必須做的操作。學(xué)習(xí)率我一開始設(shè)的是0.01結(jié)果loss直接飛了變成NaN。后來降到0.001能收斂但很慢。最后采用的是階梯式衰減初始學(xué)習(xí)率0.005每訓(xùn)練10個(gè)epoch乘以0.5。這樣前期收斂快后期穩(wěn)定。實(shí)測(cè)下來20個(gè)epoch就能達(dá)到85%以上的準(zhǔn)確率。def adjust_learning_rate(optimizer, epoch, initial_lr0.005): lr initial_lr * (0.5 ** (epoch // 10)) for param_group in optimizer.param_groups: param_group[lr] lr return lr梯度裁剪也是必須的。普通顯卡上跑有時(shí)候一個(gè)異常batch會(huì)導(dǎo)致梯度爆炸整個(gè)網(wǎng)絡(luò)參數(shù)全變成NaN。我的做法是計(jì)算所有參數(shù)梯度的L2范數(shù)如果超過閾值就等比例縮小。閾值設(shè)多少我試過1.0、5.0、10.0最后發(fā)現(xiàn)5.0比較合適。太小會(huì)限制正常梯度太大會(huì)讓裁剪失效。def clip_gradients(model, max_norm5.0): total_norm 0.0 for p in model.parameters(): if p.grad is not None: param_norm p.grad.data.norm(2) total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 clip_coef max_norm / (total_norm 1e-6) if clip_coef 1: for p in model.parameters(): if p.grad is not None: p.grad.data.mul_(clip_coef)提示梯度裁剪要在loss.backward()之后、optimizer.step()之前調(diào)用。順序錯(cuò)了就沒效果。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境搭建與依賴安裝的極簡方案環(huán)境搭建這塊我走的是極簡路線。不需要裝CUDA Toolkit不需要編譯任何東西只要顯卡驅(qū)動(dòng)正常直接裝PyTorch的預(yù)編譯版本就行。具體命令pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy pillow matplotlib為什么指定cu118因?yàn)檫@是目前兼容性最好的CUDA版本從老卡到新卡都能跑。如果你的顯卡比較老比如GTX 10系列cu118也支持。裝完之后用torch.cuda.is_available()驗(yàn)證一下返回True就說明GPU可用。注意不要用conda install pytorchconda的依賴解析有時(shí)候會(huì)裝成CPU版本而且體積巨大。pip安裝更干凈也更容易控制版本。驗(yàn)證環(huán)境是否正常跑一段小代碼import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) x torch.randn(1000, 1000).cuda() y torch.mm(x, x) print(y.mean().item())如果最后輸出一個(gè)正常的數(shù)值說明GPU計(jì)算沒問題。如果報(bào)錯(cuò)大概率是驅(qū)動(dòng)版本太老去顯卡官網(wǎng)更新一下驅(qū)動(dòng)就行。3.2 完整訓(xùn)練流程的代碼實(shí)現(xiàn)與參數(shù)說明訓(xùn)練流程我寫成了一個(gè)單獨(dú)的腳本核心邏輯大概一百行。下面把關(guān)鍵部分拆開講。首先是數(shù)據(jù)加載。我用的是自己寫的Dataset類繼承torch.utils.data.Dataset然后配合DataLoader使用。num_workers設(shè)成2pin_memory設(shè)成True。為什么設(shè)2因?yàn)槲业腃PU只有4核設(shè)多了反而搶資源。pin_memory開啟后數(shù)據(jù)從CPU傳到GPU的速度會(huì)快一些。from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, images, labels): self.images torch.from_numpy(images).permute(0, 3, 1, 2).float() self.labels torch.from_numpy(labels).long() def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.images[idx], self.labels[idx] train_loader DataLoader(MyDataset(train_images, train_labels), batch_size128, shuffleTrue, num_workers2, pin_memoryTrue)網(wǎng)絡(luò)定義部分我手動(dòng)寫了每一層。輸入是32x32x3先過兩個(gè)卷積層每個(gè)后面接LeakyReLU和最大池化然后展平接三個(gè)全連接層最后輸出10個(gè)類別。import torch.nn as nn class MyNet(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(3, 16, 3, padding1) self.conv2 nn.Conv2d(16, 32, 3, padding1) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(32 * 8 * 8, 256) self.fc2 nn.Linear(256, 64) self.fc3 nn.Linear(64, 10) self.leaky nn.LeakyReLU(0.01) def forward(self, x): x self.pool(self.leaky(self.conv1(x))) x self.pool(self.leaky(self.conv2(x))) x x.view(x.size(0), -1) x self.leaky(self.fc1(x)) x self.leaky(self.fc2(x)) x self.fc3(x) return x訓(xùn)練循環(huán)里每個(gè)epoch做四件事前向傳播、計(jì)算loss、反向傳播、更新參數(shù)。loss用的是交叉熵優(yōu)化器是手動(dòng)實(shí)現(xiàn)的SGD加動(dòng)量。model MyNet().cuda() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.005, momentum0.9) for epoch in range(20): model.train() total_loss 0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() clip_gradients(model, 5.0) optimizer.step() total_loss loss.item() adjust_learning_rate(optimizer, epoch) print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})實(shí)測(cè)下來20個(gè)epoch在8GB卡上大概跑15分鐘最終訓(xùn)練準(zhǔn)確率能到92%左右驗(yàn)證準(zhǔn)確率85%上下。這個(gè)結(jié)果對(duì)于一個(gè)小型網(wǎng)絡(luò)來說已經(jīng)不錯(cuò)了。3.3 顯存優(yōu)化與訓(xùn)練加速的實(shí)戰(zhàn)技巧普通顯卡最大的瓶頸就是顯存。我總結(jié)了幾條實(shí)戰(zhàn)技巧每一條都實(shí)測(cè)有效。第一條用混合精度訓(xùn)練。PyTorch自帶的torch.cuda.amp可以讓部分計(jì)算用float16顯存占用直接減半速度還能提升20%到30%。用法很簡單scaler torch.cuda.amp.GradScaler() for images, labels in train_loader: with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()注意用了混合精度之后梯度裁剪要在scaler.scale(loss).backward()之后、scaler.step()之前做而且要先scaler.unscale_(optimizer)把梯度還原成float32再裁剪。第二條及時(shí)釋放不用的張量。Python的垃圾回收不是實(shí)時(shí)的有時(shí)候中間變量占著顯存不放。我習(xí)慣在關(guān)鍵位置手動(dòng)加del和torch.cuda.empty_cache()。比如每個(gè)epoch結(jié)束后del outputs, loss torch.cuda.empty_cache()第三條batch size不要貪大。我試過batch size 512結(jié)果顯存直接爆了。后來降到128訓(xùn)練速度反而更快因?yàn)轱@存交換少了。找到合適的batch size有個(gè)簡單方法從64開始每次翻倍直到出現(xiàn)OOM然后退回上一檔。第四條用torch.backends.cudnn.benchmark True。這行代碼會(huì)讓cuDNN自動(dòng)尋找最適合當(dāng)前網(wǎng)絡(luò)結(jié)構(gòu)的卷積算法實(shí)測(cè)能提速10%左右。但注意如果你的輸入尺寸經(jīng)常變就不要開因?yàn)槊看味家匦滤阉鳌?. 常見問題與排查技巧實(shí)錄4.1 訓(xùn)練不收斂的五大原因與解決方案訓(xùn)練不收斂是新手最常遇到的問題。我把踩過的坑整理成一張速查表現(xiàn)象可能原因排查方法解決方案loss變成NaN學(xué)習(xí)率太大打印每步loss降低學(xué)習(xí)率10倍loss震蕩劇烈batch size太小觀察loss曲線增大batch size或加動(dòng)量loss下降但準(zhǔn)確率不漲過擬合對(duì)比訓(xùn)練/驗(yàn)證準(zhǔn)確率加Dropout或數(shù)據(jù)增強(qiáng)loss完全不降參數(shù)初始化不當(dāng)檢查初始輸出分布改用He初始化訓(xùn)練后期突然崩潰梯度爆炸打印梯度范數(shù)加梯度裁剪我印象最深的一次是loss一直卡在2.3左右不降查了半天發(fā)現(xiàn)是數(shù)據(jù)標(biāo)簽沒對(duì)齊——圖片和標(biāo)簽的索引錯(cuò)位了。這種問題最隱蔽因?yàn)閘oss看起來正常只是不降。后來我加了一個(gè)檢查步驟隨機(jī)抽幾張圖可視化確認(rèn)標(biāo)簽正確。提示訓(xùn)練前一定要做一次“ sanity check”——用少量數(shù)據(jù)比如10張圖過擬合如果網(wǎng)絡(luò)能把這10張圖全部正確分類說明網(wǎng)絡(luò)結(jié)構(gòu)和訓(xùn)練流程沒問題。如果連10張圖都過擬合不了那肯定是代碼有bug。4.2 顯存不足的應(yīng)急處理與長期優(yōu)化顯存不足報(bào)錯(cuò)CUDA out of memory是家常便飯。應(yīng)急處理很簡單把batch size減半或者把圖片分辨率降一檔。但長期來看需要系統(tǒng)性優(yōu)化。我的做法是梯度累積。比如你想用batch size 256但顯存只夠128那就跑兩次128的batch把梯度累加起來再更新一次參數(shù)。效果等價(jià)于batch size 256但顯存占用不變。accumulation_steps 2 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()還有一個(gè)技巧是動(dòng)態(tài)調(diào)整輸入尺寸。訓(xùn)練前期用16x16的小圖快速收斂后期用32x32的精調(diào)。這樣前期顯存占用少可以開大batch size后期雖然顯存占用多但網(wǎng)絡(luò)已經(jīng)接近收斂需要的epoch少。4.3 模型評(píng)估與可視化的實(shí)操心得訓(xùn)練完了不算完還得知道模型到底學(xué)到了什么。我習(xí)慣做三件事混淆矩陣、特征圖可視化、錯(cuò)誤樣本分析?;煜仃囉胹klearn的confusion_matrix一行搞定能直觀看到哪些類別容易混淆。比如我的模型總是把“貓”和“狗”搞混說明這兩個(gè)類別的特征在淺層網(wǎng)絡(luò)上區(qū)分度不夠。特征圖可視化稍微麻煩一點(diǎn)需要把卷積層的輸出取出來。我的做法是注冊(cè)一個(gè)forward hookfeatures {} def get_features(name): def hook(model, input, output): features[name] output.detach() return hook model.conv1.register_forward_hook(get_features(conv1))然后跑一張測(cè)試圖把features[conv1]的每個(gè)通道畫出來。你會(huì)發(fā)現(xiàn)有些通道專門檢測(cè)邊緣有些檢測(cè)紋理。這個(gè)過程對(duì)理解卷積網(wǎng)絡(luò)的工作原理非常有幫助。錯(cuò)誤樣本分析就是把分錯(cuò)的圖片挑出來看。我經(jīng)常發(fā)現(xiàn)分錯(cuò)的圖片要么是標(biāo)注本身就有問題要么是圖片質(zhì)量太差人眼都難分辨。把這些樣本清理掉之后模型準(zhǔn)確率能提升兩三個(gè)百分點(diǎn)。注意可視化的時(shí)候記得把張量轉(zhuǎn)到CPU上并且用.detach()切斷計(jì)算圖。直接在GPU上畫圖會(huì)報(bào)錯(cuò)而且占用顯存。5. 項(xiàng)目擴(kuò)展與個(gè)人經(jīng)驗(yàn)分享5.1 從單機(jī)到多卡普通玩家的擴(kuò)展路徑如果你后來換了一張更好的顯卡或者又加了一張這個(gè)項(xiàng)目可以平滑擴(kuò)展到多卡訓(xùn)練。PyTorch提供了nn.DataParallel一行代碼就能把模型放到多張卡上model nn.DataParallel(model)但DataParallel有個(gè)缺點(diǎn)它會(huì)在主卡上匯總所有梯度導(dǎo)致主卡顯存占用比副卡多。如果兩張卡顯存不一樣比如一張8GB一張12GB主卡很容易爆。這時(shí)候可以用DistributedDataParallel但配置復(fù)雜很多需要改數(shù)據(jù)加載器和啟動(dòng)腳本。我的建議是如果兩張卡顯存一樣用DataParallel就夠了如果顯存不一樣還是老老實(shí)實(shí)單卡跑或者把大卡當(dāng)主卡。實(shí)測(cè)下來兩張8GB卡用DataParallel訓(xùn)練速度大概提升1.6倍不是線性的因?yàn)橥ㄐ庞虚_銷。5.2 我踩過的三個(gè)印象最深的坑第一個(gè)坑是數(shù)據(jù)加載器的num_workers設(shè)成0。當(dāng)時(shí)為了調(diào)試方便把num_workers設(shè)成0結(jié)果訓(xùn)練速度慢得離譜一個(gè)epoch要跑十分鐘。后來改成2直接降到兩分鐘。原因很簡單num_workers0意味著數(shù)據(jù)加載在主進(jìn)程里做GPU算完一個(gè)batch要等CPU讀下一個(gè)batchGPU利用率只有20%左右。第二個(gè)坑是忘記加model.train()和model.eval()。PyTorch的Dropout和BatchNorm在訓(xùn)練和推理時(shí)的行為不一樣。我有一次評(píng)估時(shí)忘了加model.eval()結(jié)果Dropout還在隨機(jī)丟棄神經(jīng)元準(zhǔn)確率比正常低了十個(gè)百分點(diǎn)。查了半天才發(fā)現(xiàn)是這個(gè)原因。第三個(gè)坑是保存模型時(shí)只保存了參數(shù)沒保存優(yōu)化器狀態(tài)。后來想繼續(xù)訓(xùn)練發(fā)現(xiàn)學(xué)習(xí)率調(diào)度和動(dòng)量都丟了只能從頭開始。正確的做法是保存一個(gè)字典torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: loss, }, checkpoint.pth)5.3 這個(gè)項(xiàng)目后續(xù)還能怎么玩這個(gè)項(xiàng)目本身是一個(gè)基礎(chǔ)框架但擴(kuò)展性很強(qiáng)。我后來在這個(gè)基礎(chǔ)上做了幾個(gè)變體一是換成回歸任務(wù)。把最后的分類層改成輸出一個(gè)數(shù)值loss換成MSE就能做簡單的回歸預(yù)測(cè)。比如預(yù)測(cè)房價(jià)、預(yù)測(cè)溫度數(shù)據(jù)換成表格數(shù)據(jù)就行。二是加入注意力機(jī)制。在卷積層后面加一個(gè)簡單的通道注意力模塊參數(shù)量只增加幾千個(gè)但準(zhǔn)確率能提升兩三個(gè)百分點(diǎn)。代碼大概二十行效果立竿見影。三是做成Web演示。用Gradio或者Streamlit寫一個(gè)簡單界面上傳圖片就能看到分類結(jié)果。這個(gè)對(duì)于做演示或者教學(xué)特別有用別人不需要懂代碼就能體驗(yàn)?zāi)愕哪P汀K氖橇炕渴?。用PyTorch的量化工具把模型轉(zhuǎn)成int8體積縮小四倍推理速度提升兩三倍。雖然準(zhǔn)確率會(huì)掉一兩個(gè)百分點(diǎn)但對(duì)于部署到邊緣設(shè)備來說非常劃算。我個(gè)人在實(shí)際操作中的體會(huì)是普通顯卡完全能玩轉(zhuǎn)神經(jīng)網(wǎng)絡(luò)關(guān)鍵是要控制好規(guī)模。不要一上來就搞大模型從小網(wǎng)絡(luò)開始把每一個(gè)環(huán)節(jié)都吃透后面再擴(kuò)展就順理成章了。另外多動(dòng)手寫代碼少復(fù)制粘貼。自己寫一遍反向傳播比看十篇教程都管用。最后再分享一個(gè)小技巧訓(xùn)練前先用小數(shù)據(jù)集跑通全流程確認(rèn)沒有bug之后再上全量數(shù)據(jù)這樣能省下大量等待時(shí)間。