@浙大疏錦行https://blog.csdn.net/weixin_45655710知識點回顧:
- tensorboard的發展歷史和原理
- tensorboard的常見操作
- tensorboard在cifar上的實戰:MLP和CNN模型
作業:對resnet18在cifar10上采用微調策略下,用tensorboard監控訓練過程。
核心:
-
數據加載和模型創建:復用之前的函數,保持模塊化。
-
SummaryWriter
初始化:創建TensorBoard的寫入器,并自動處理日志目錄,避免覆蓋。 -
train_and_evaluate
函數:創建一個總控函數,封裝了完整的“凍結-解凍”訓練循環,并在其中集成了TensorBoard的各種日志記錄功能。 -
TensorBoard日志記錄:
- 模型圖譜 (Graph):在訓練開始前,記錄模型的計算圖。
- 標量 (Scalars):實時記錄訓練集和測試集的損失(Loss)與準確率(Accuracy),以及學習率(Learning Rate)的變化。
- 圖像 (Images):記錄輸入的樣本圖像和每個epoch結束時預測錯誤的樣本。
- 直方圖 (Histograms):定期記錄模型各層權重(Weights)和梯度(Gradients)的分布,用于診斷訓練狀態。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter # 導入TensorBoard的核心類
import matplotlib.pyplot as plt
import os
import time
from tqdm import tqdm
import torchvision # 確保torchvision被導入以使用make_grid# --- 步驟 1: 準備數據加載器 (保持不變) ---
def get_cifar10_loaders(batch_size=128):"""獲取CIFAR-10的數據加載器,包含數據增強"""train_transform = transforms.Compose([transforms.RandomResizedCrop(224), # ResNet通常在224x224的圖像上預訓練transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet的標準化參數])test_transform = transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)test_dataset = datasets.CIFAR10(root='./data', train=False, transform=test_transform)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2, pin_memory=True)return train_loader, test_loader# --- 步驟 2: 模型創建與凍結/解凍函數 (保持不變) ---
def create_resnet18(pretrained=True, num_classes=10):"""創建并修改ResNet18模型"""model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None)in_features = model.fc.in_featuresmodel.fc = nn.Linear(in_features, num_classes)return modeldef set_freeze_state(model, freeze=True):"""凍結或解凍模型的特征提取層"""print(f"--- {'凍結' if freeze else '解凍'} 特征提取層 ---")for name, param in model.named_parameters():if 'fc' not in name: # 只訓練最后的全連接層param.requires_grad = not freeze# --- 步驟 3: 封裝了TensorBoard的訓練與評估總控函數 ---
def train_with_tensorboard(model, device, train_loader, test_loader, epochs, freeze_epochs, writer):"""使用TensorBoard監控的完整訓練流程"""# 初始化優化器和損失函數criterion = nn.CrossEntropyLoss()# 初始只優化未凍結的參數optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=2, factor=0.5, verbose=True)# --- TensorBoard初始記錄 ---print("正在記錄初始信息到TensorBoard...")dataiter = iter(train_loader)images, _ = next(dataiter)writer.add_graph(model, images.to(device)) # 記錄模型圖img_grid = torchvision.utils.make_grid(images[:16]) # 取16張圖預覽writer.add_image('CIFAR-10 樣本圖像', img_grid)print("? 初始信息記錄完成。")# 開始訓練global_step = 0for epoch in range(1, epochs + 1):# --- 解凍控制 ---if epoch == freeze_epochs + 1:set_freeze_state(model, freeze=False)# 解凍后需要為優化器加入所有參數optimizer = optim.Adam(model.parameters(), lr=1e-4) # 使用更小的學習率進行全局微調print("優化器已更新以包含所有參數,學習率已降低。")# --- 訓練部分 ---model.train()train_loss, train_correct, train_total = 0, 0, 0loop = tqdm(train_loader, desc=f"Epoch [{epoch}/{epochs}] Training", leave=False)for data, target in loop:data, target = data.to(device), target.to(device)optimizer.zero_grad()output = model(data)loss = criterion(output, target)loss.backward()optimizer.step()train_loss += loss.item() * data.size(0)_, pred = output.max(1)train_correct += pred.eq(target).sum().item()train_total += data.size(0)writer.add_scalar('Train/Batch_Loss', loss.item(), global_step)global_step += 1loop.set_postfix(loss=loss.item())loop.close()# 記錄Epoch級訓練指標avg_train_loss = train_loss / train_totalavg_train_acc = 100. * train_correct / train_totalwriter.add_scalar('Train/Epoch_Loss', avg_train_loss, epoch)writer.add_scalar('Train/Epoch_Accuracy', avg_train_acc, epoch)# --- 評估部分 ---model.eval()test_loss, test_correct, test_total = 0, 0, 0with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)output = model(data)loss = criterion(output, target)test_loss += loss.item() * data.size(0)_, pred = output.max(1)test_correct += pred.eq(target).sum().item()test_total += data.size(0)# 記錄Epoch級測試指標avg_test_loss = test_loss / test_totalavg_test_acc = 100. * test_correct / test_totalwriter.add_scalar('Test/Epoch_Loss', avg_test_loss, epoch)writer.add_scalar('Test/Epoch_Accuracy', avg_test_acc, epoch)# 記錄權重和梯度的直方圖 (每個epoch記錄一次)for name, param in model.named_parameters():writer.add_histogram(f'Weights/{name}', param, epoch)if param.grad is not None:writer.add_histogram(f'Gradients/{name}', param.grad, epoch)# 更新學習率調度器scheduler.step(avg_test_loss)writer.add_scalar('Train/Learning_Rate', optimizer.param_groups[0]['lr'], epoch)print(f"Epoch {epoch} 完成 | 訓練準確率: {avg_train_acc:.2f}% | 測試準確率: {avg_test_acc:.2f}%")# --- 步驟 4: 主執行流程 ---
if __name__ == "__main__":# --- 配置 ---EPOCHS = 15FREEZE_EPOCHS = 5 # 先凍結訓練5輪,再解凍訓練10輪BATCH_SIZE = 64DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")# --- TensorBoard 初始化 ---log_dir = "runs/resnet18_finetune_cifar10"version = 1while os.path.exists(f"{log_dir}_v{version}"):version += 1log_dir = f"{log_dir}_v{version}"writer = SummaryWriter(log_dir)print(f"TensorBoard 日志將保存在: {log_dir}")# --- 開始實驗 ---train_loader, test_loader = get_cifar10_loaders(batch_size=BATCH_SIZE)model = create_resnet18(pretrained=True).to(DEVICE)set_freeze_state(model, freeze=True) # 初始凍結print("\n--- 開始使用ResNet18微調模型 ---")print("訓練完成后,在終端運行 `tensorboard --logdir=runs` 來查看可視化結果。")train_with_tensorboard(model, DEVICE, train_loader, test_loader, EPOCHS, FREEZE_EPOCHS, writer)writer.close() # 關閉writerprint("\n? 訓練完成,TensorBoard日志已保存。")
解析
1.數據預處理適配 (get_cifar10_loaders
):
圖像尺寸:ResNet
系列是在224x224
的ImageNet圖像上預訓練的。雖然它們也能處理32x32
的CIFAR-10圖像,但為了更好地利用預訓練權重,一個常見的做法是將小圖像放大到224x224
。我們在transforms
中加入了transforms.RandomResizedCrop(224)
和transforms.Resize(256)
/ transforms.CenterCrop(224)
來實現這一點。
標準化參數:使用了ImageNet數據集的標準化均值和標準差,這是使用在ImageNet上預訓練的模型的標準做法。
2.模塊化訓練流程 (train_with_tensorboard
):
將整個包含“凍結-解凍”邏輯的訓練循環封裝成一個函數,使得主程序非常簡潔。
該函數接收一個writer
對象作為參數,所有TensorBoard的日志記錄都在這個函數內部完成。
3.TensorBoard全面監控:
- 模型圖 (
add_graph
):在訓練開始前,將模型的結構圖寫入日志,方便在GRAPHS標簽頁查看。 - 圖像 (
add_image
):將一批原始訓練樣本寫入日志,可以在IMAGES標簽頁直觀地看到輸入數據。 - 標量 (
add_scalar
) :
Batch級:記錄了每個訓練批次的損失(Train/Batch_Loss
),可以觀察到最細粒度的訓練動態。
Epoch級:記錄了每個輪次結束后的訓練和測試的損失與準確率,以及學習率的變化。這能讓我們在同一個圖表中清晰地對比訓練集和測試集的性能曲線,判斷過擬合。
-
直方圖 (
add_histogram
):每個輪次結束后,記錄模型所有可訓練參數的權重分布和梯度分布。這對于高級調試非常有用,可以幫助判斷是否存在梯度消失/爆炸,或者權重是否更新正常。
4.清晰的執行邏輯:
if __name__ == "__main__":
中,代碼邏輯非常清晰:設置參數 -> 初始化TensorBoard寫入器 -> 準備數據 -> 創建模型 -> 調用總控函數開始訓練 -> 結束并關閉寫入器。