?開頭摘要??:
本文將深入探討如何使用PyTorch實現基于Softmax回歸的MNIST手寫數字識別系統。從多分類問題的核心概念出發,詳細解析??One-Hot編碼??技術如何將類別標簽向量化,剖析??交叉熵損失函數??的數學原理及其在訓練中的優化機制。通過完整代碼實戰,展示數據加載、網絡構建、模型訓練與評估的全流程,并重點介紹??TensorBoard可視化??工具的應用技巧。本教程涵蓋模型保存加載、預測結果可視化等實用功能,為初學者提供從理論到實踐的全面指導,幫助快速掌握分類任務的核心技術棧。
文章目錄
- 多分類
- one_hot編碼
- 編碼原理
- 交叉熵損失函數
- 示例
- 實戰案例基于mnnist的手寫識別
- TensorBoard的使用
- 部分代碼解釋
- 總體實現
- 核心收獲
- 代碼
多分類
- 輸出是每個類別的概率,要求有多少個分類最終的輸出層就有多少個神經元
- 各分類輸出概率的總和是1(使用softmax歸一化)
one_hot編碼
One-Hot 編碼是機器學習中處理??分類數據??的核心技術,它將離散類別轉換為向量表示,使其能被神經網絡處理。
一句話:在一個樣本中:n個分類,結果是第k個,則yone-hot=[0,?,1?第k位,?,0]y_{\text{one-hot}} = [0, \cdots, \underbrace{1}_{\text{第k位}}, \cdots, 0]yone-hot?=[0,?,第k位1??,?,0]
編碼原理
對于一個包含 C 個類別的特征:
- 創建長度為 C 的零向量
- 對第 k 類數據,將其向量中第 k 個位置設為 1
- 其余位置保持為 0
yone-hot=[0,?,1?第k位,?,0]y_{\text{one-hot}} = [0, \cdots, \underbrace{1}_{\text{第k位}}, \cdots, 0]yone-hot?=[0,?,第k位1??,?,0]
假設動物分類的類別為:[“狗”, “貓”, “鳥”]
類別 One-Hot 編碼
狗 [1, 0, 0]
貓 [0, 1, 0]
鳥 [0, 0, 1]
交叉熵損失函數
一句話:在H(y,pi)=?∑xylog?piH(y,p_i)=-\sum_{x}y\log p_iH(y,pi?)=?∑x?ylogpi?中,只有一條1*log(pi)為最終交叉熵損失函數值,其它都是0*log(pi)=0
交叉熵損失函數與邏輯回歸中的損失函數效果相同,都是為如何調整參數指明方向,即通過求取梯度,調整參數使損失函數的值逼近0,只是交叉熵損失函數用在多分類中
L(y^,y)=?ylog?(y^)?(1?y)log?(1?y^)L(\hat{y}, y) = - y \log(\hat{y}) - (1 - y) \log(1 - \hat{y}) L(y^?,y)=?ylog(y^?)?(1?y)log(1?y^?)
交叉熵損失函數
H(y,pi)=?∑xylog?piH(y,p_i)=-\sum_{x}y\log p_iH(y,pi?)=?x∑?ylogpi?
yi:是真實標簽(真實分布),通常采用one?hot編碼(真實類別為1,其余為0)y_i:是真實標簽(真實分布),通常采用one-hot編碼(真實類別為1,其余為0)yi?:是真實標簽(真實分布),通常采用one?hot編碼(真實類別為1,其余為0)
(乘的時候按類,分開乘了,0*log or 1*log ,單個樣本,最終結果取決于那個唯一的1*log的值)
pi:是預測概率(模型輸出的概率分布)p_i:是預測概率(模型輸出的概率分布)pi?:是預測概率(模型輸出的概率分布)
log?(pi):是預測概率的對數值\log(p_i):是預測概率的對數值log(pi?):是預測概率的對數值
整體計算:是真實標簽yi與預測概率對數log?(pi)的乘積再求和取負整體計算:是真實標簽y_i與預測概率對數\log(p_i)的乘積再求和取負整體計算:是真實標簽yi?與預測概率對數log(pi?)的乘積再求和取負
示例
- L=0.357 表示當前預測不夠準確(理想值應接近0)
- Log的底數無所謂,經過訓練,任何底數的結果都是相同的
log(x)
實戰案例基于mnnist的手寫識別
TensorBoard的使用
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(comment='test_tensorboard') # 用于記錄要可視化的數據
#writer = SummaryWriter(存放的地址)
如果不指定絕對路徑,PyTorch 默認創建runs在當前文件夾下
在你安裝TensorBoard的虛擬py環境中運行以下代碼即可
tensorboard --logdir="這個event文件所在目錄的絕對地址"
部分代碼解釋
import torch
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from torchvision.datasets import MNIST
import matplotlib.pyplot as plt
import os
import numpy as np
from datetime import datetime
import os
log_dir = "runs"
os.makedirs(log_dir, exist_ok=True)
創建一個名為runs的文件夾
import matplotlib.pyplot as plt
writer = SummaryWriter(log_dir=log_subdir)
記錄數據便于后續畫圖
3.定義網絡基本框架
class SoftmaxRegression(torch.nn.Module):#括號中的 torch.nn.Module 表示你的 SoftmaxRegression 類 繼承自 PyTorch 的 Module 基類def __init__(self): #self 指代 當前類的實例對象(即正在創建的具體模型)super().__init__()# 單層網絡結構:784輸入 -> 10輸出self.linear = torch.nn.Linear(28 * 28, 10)#定義了這個網絡的基本結構,有784個輸入特征,10個輸出def forward(self, x):# 應用log_softmax到線性層輸出return torch.nn.functional.log_softmax(self.linear(x), dim=1)
log_softmax(self.linear(x), dim=1)相當于兩步
softmax_output = exp(z_i) / sum(exp(z_j)) # 轉換為概率分布
log_softmax = log(softmax_output) # 取自然對數
torch.nn.functional.log_softmax(self.linear(x), dim=1),dim=1
im=0:跨樣本操作(通常不需要)(每個樣本的第n類概率加起來為1)
dim=1:跨類別操作(分類任務的標準做法)(每個樣本的n個類的各個概率加起來為1)
在這個模型中沒有隱藏層,這是一個單層神經網絡(也稱為 Softmax 回歸或多元邏輯回歸),是直接從784個輸出特征到,輸出層的十個輸出神經元
4.加載訓練數據
def get_data_loader(is_train, batch_size=128):transform = transforms.Compose([transforms.ToTensor(),#張量化圖像transforms.Normalize((0.1307,), (0.3081,)) # MNIST的均值和標準差])dataset = MNIST(root='./data', train=is_train, download=True, transform=transform)return DataLoader(dataset, batch_size=batch_size, shuffle=is_train, pin_memory=True)
dataset = MNIST( # 創建MNIST數據集對象
root=‘./data’, # 指定數據存儲路徑為當前目錄下的data文件夾
train=is_train, # 確定加載訓練集(True)還是測試集(False)
download=True, # 如果本地沒有數據集則自動下載
transform=transform # 應用指定的數據預處理轉換
)
return DataLoader( # 返回數據加載器
dataset, # 使用的數據集對象
batch_size=batch_size, # 指定每次加載的數據批大小
shuffle=is_train, # 決定是否打亂數據順序
pin_memory=True # 內存優化選項(GPU訓練時使用)
)
5.返回正確率
#test_data是測試數據
#net 是一個 ??已經實例化并訓練好的神經網絡對象??"""評估模型準確率"""
def evaluate(test_data, net): net.eval()correct = 0total = 0with torch.no_grad():for images, labels in test_data:images, labels = images.to(device), labels.to(device)outputs = net(images.view(-1, 28 * 28))_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()return correct / total
6.保存訓練結果
def save_model(net, filename='softmax_model.pth'):#當前目錄下"""保存模型"""torch.save(net.state_dict(), filename)#提取模型的所有可學習參數print(f"模型已保存至 {filename}")n
#net 是一個 ??已經實例化并訓練好的神經網絡對象??
7.加載之前的訓練結果
def load_model(net, filename='softmax_model.pth'):"""加載模型"""if os.path.exists(filename):net.load_state_dict(torch.load(filename, map_location=device))#map_location=device是為了指定模型加載到哪個設備上(CPU或GPU)print(f"模型已從 {filename} 加載")else:print(f"警告: 未找到模型文件 {filename}")return net
device = torch.device(“cuda” if torch.cuda.is_available() else “cpu”)
8.可視化
def visualize_predictions(model, test_loader, num_images=12):"""可視化模型預測結果"""model.eval()images, labels = next(iter(test_loader))images, labels = images.to(device), labels.to(device)with torch.no_grad():outputs = model(images.view(-1, 28 * 28))_, predictions = torch.max(outputs, 1)plt.figure(figsize=(12, 8))for i in range(num_images):plt.subplot(3, 4, i + 1)img = images[i].cpu().numpy().squeeze()plt.imshow(img, cmap='gray')plt.title(f"預測: {predictions[i].item()} (真實: {labels[i].item()})")plt.axis('off')plt.tight_layout()plt.savefig('softmax_predictions.png', dpi=150)plt.show()# 將圖像添加到TensorBoard(使用PIL圖像替代)from PIL import Imagefrom torchvision.utils import save_image# 創建臨時圖像文件temp_img_path = "temp_grid.png"save_image(images[:num_images], temp_img_path, nrow=4)# 讀取并添加到TensorBoardimg = Image.open(temp_img_path)img_array = np.array(img)writer.add_image('predictions', img_array, dataformats='HWC')
總體實現
import torch
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from torchvision.datasets import MNIST
import matplotlib.pyplot as plt
import os
import numpy as np
from datetime import datetime# 解決Matplotlib中文顯示問題
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑體顯示中文
plt.rcParams['axes.unicode_minus'] = False # 正常顯示負號# 檢查CUDA設備是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用設備: {device}")# 確保日志目錄存在
log_dir = "runs"
os.makedirs(log_dir, exist_ok=True)# 創建TensorBoard記錄器
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_subdir = os.path.join(log_dir, f"softmax_mnist_{timestamp}")
writer = SummaryWriter(log_dir=log_subdir) #用來畫圖的數據就放在log_dir=log_subdir里class SoftmaxRegression(torch.nn.Module):#括號中的 torch.nn.Module 表示你的 SoftmaxRegression 類 繼承自 PyTorch 的 Module 基類"""簡單的Softmax回歸模型"""def __init__(self): #self 指代 當前類的實例對象(即正在創建的具體模型)super().__init__()# 單層網絡結構:784輸入 -> 10輸出self.linear = torch.nn.Linear(28 * 28, 10)def forward(self, x):# 應用log_softmax到線性層輸出return torch.nn.functional.log_softmax(self.linear(x), dim=1)def get_data_loader(is_train, batch_size=128):"""獲取數據加載器"""transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,)) # MNIST的均值和標準差])dataset = MNIST(root='./data', train=is_train, download=True, transform=transform)return DataLoader(dataset, batch_size=batch_size, shuffle=is_train, pin_memory=True)def evaluate(test_data, net):"""評估模型準確率"""net.eval()correct = 0total = 0with torch.no_grad():for images, labels in test_data:images, labels = images.to(device), labels.to(device)outputs = net(images.view(-1, 28 * 28))_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()return correct / totaldef save_model(net, filename='softmax_model.pth'):"""保存模型"""torch.save(net.state_dict(), filename)print(f"模型已保存至 {filename}")def load_model(net, filename='softmax_model.pth'):"""加載模型"""if os.path.exists(filename):net.load_state_dict(torch.load(filename, map_location=device))print(f"模型已從 {filename} 加載")else:print(f"警告: 未找到模型文件 {filename}")return netdef visualize_predictions(model, test_loader, num_images=12):"""可視化模型預測結果"""model.eval()images, labels = next(iter(test_loader))images, labels = images.to(device), labels.to(device)with torch.no_grad():outputs = model(images.view(-1, 28 * 28))_, predictions = torch.max(outputs, 1)plt.figure(figsize=(12, 8))for i in range(num_images):plt.subplot(3, 4, i + 1)img = images[i].cpu().numpy().squeeze()plt.imshow(img, cmap='gray')plt.title(f"預測: {predictions[i].item()} (真實: {labels[i].item()})")plt.axis('off')plt.tight_layout()plt.savefig('softmax_predictions.png', dpi=150)plt.show()# 將圖像添加到TensorBoard(使用PIL圖像替代)from PIL import Imagefrom torchvision.utils import save_image# 創建臨時圖像文件temp_img_path = "temp_grid.png"save_image(images[:num_images], temp_img_path, nrow=4)# 讀取并添加到TensorBoardimg = Image.open(temp_img_path)img_array = np.array(img)writer.add_image('predictions', img_array, dataformats='HWC')def main():# 獲取數據加載器train_loader = get_data_loader(is_train=True, batch_size=128)test_loader = get_data_loader(is_train=False, batch_size=512)# 創建模型model = SoftmaxRegression().to(device)print(f"模型參數量: {sum(p.numel() for p in model.parameters()):,}")# 嘗試加載現有模型model = load_model(model)# 評估初始準確率init_acc = evaluate(test_loader, model)print(f"初始準確率: {init_acc:.4f}")writer.add_scalar('Accuracy/test', init_acc, 0)# 使用Adam優化器optimizer = torch.optim.Adam(model.parameters(), lr=0.01)# 訓練循環total_step = 0for epoch in range(10):model.train()total_loss = 0correct = 0total = 0for i, (images, labels) in enumerate(train_loader):images, labels = images.to(device), labels.to(device)# 前向傳播outputs = model(images.view(-1, 28 * 28))loss = torch.nn.functional.nll_loss(outputs, labels)# 反向傳播和優化optimizer.zero_grad()loss.backward()optimizer.step()# 統計信息total_loss += loss.item()_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()# 每100個batch記錄一次if (i + 1) % 100 == 0:train_acc = correct / totalavg_loss = total_loss / (i + 1)print(f"Epoch [{epoch + 1}/10], Step [{i + 1}/{len(train_loader)}], "f"Loss: {avg_loss:.4f}, Accuracy: {train_acc:.4f}")# 記錄到TensorBoardwriter.add_scalar('Loss/train', avg_loss, total_step)writer.add_scalar('Accuracy/train', train_acc, total_step)total_step += 1# 每個epoch結束后評估測試集test_acc = evaluate(test_loader, model)print(f"Epoch [{epoch + 1}/10], 測試準確率: {test_acc:.4f}")writer.add_scalar('Accuracy/test', test_acc, epoch)# 訓練后保存模型save_model(model)# 最終評估final_acc = evaluate(test_loader, model)print(f"最終測試準確率: {final_acc:.4f}")# 可視化預測結果visualize_predictions(model, test_loader)# 在TensorBoard中添加模型圖dummy_input = torch.randn(1, 784).to(device)writer.add_graph(model, dummy_input)# 關閉TensorBoard寫入器writer.close()print(f"TensorBoard日志保存在: {log_subdir}")print("使用命令查看TensorBoard: tensorboard --logdir=runs")if __name__ == '__main__':main()
核心收獲
1. 多分類問題本質
○ 輸出層神經元數=分類數
○ 使用Softmax確保概率歸一化:σ(z)j=ezj∑k=1Kezk\sigma(z)_j = \frac{e^{z_j}}{\sum_{k=1}^K e^{z_k}}σ(z)j?=∑k=1K?ezk?ezj??
2. 數據編碼技術
○ One-Hot編碼:y貓=[0,1,0]y_{\text{貓}} = [0,1,0]y貓?=[0,1,0]
○ 數據標準化:Normalize((0.1307,),(0.3081,))Normalize((0.1307,), (0.3081,))Normalize((0.1307,),(0.3081,))
3. 損失函數優化
○ 交叉熵損失:L=?∑yilog?(pi)L = -\sum y_i \log(p_i)L=?∑yi?log(pi?)
○ Adam優化器自適應調整學習率