簡介
之前一直使用的是現有人家的數據集,現在我們將使用自己的數據集進行訓練。
基于卷積神經網絡 (CNN) 的 MNIST 手寫數字識別模型
一、訓練自己數據集
1.數據預處理
我們現在有這樣的數據集如下圖:
每一個文件夾里面有著對應的圖片。我們要將這些圖片轉換成數據集的標準格式(也就是x、y標簽)
def train_test_file(root, dir):# 創建存儲圖像路徑和標簽的文本文件file_txt = open(dir + '.txt', 'w')path = os.path.join(root, dir) # 拼接完整路徑(根目錄+訓練/測試文件夾)# 遍歷文件夾結構for roots, directories, files in os.walk(path):# 記錄類別目錄(如"蘋果"、"香蕉"等文件夾名)if len(directories) != 0:dirs = directories # 保存所有類別文件夾名稱else:# 處理圖像文件:獲取當前文件夾名(即類別名)now_dir = roots.split('\\') # 按路徑分隔符拆分for file in files:# 拼接圖像完整路徑path_1 = os.path.join(roots, file)print(path_1) # 打印圖像路徑(調試用)# 寫入格式:圖像路徑 + 空格 + 類別索引(如"apple.jpg 0")file_txt.write(path_1 + ' ' + str(dirs.index(now_dir[-1])) + '\n')file_txt.close() # 關閉文件# 保存類別名稱到class_names.txt(方便后續查看類別對應關系)with open('class_names.txt', 'w', encoding='gbk') as f:f.write('\n'.join(dir))print(f"已生成{dir}.txt,類別列表:{dir}")# 調用函數生成訓練集和測試集文件
train_test_file(r'.\food_dataset\food_dataset', 'train')
train_test_file(r'.\food_dataset\food_dataset', 'test')
- 這個函數遍歷指定目錄,生成圖像路徑和對應標簽的文本文件
- 每個圖像路徑后面跟著它所屬類別的索引(用于訓練時的標簽)
- 同時生成類別名稱文件 class_names.txt
這樣我們就通過代碼生成下面這些文件標簽,模型可以讀取這些文件,前面是圖片的地址,這樣模型就能通過地址去讀取對應的圖片,后面的0等數字就是對應的標簽。
????????對于class_names.txt文件后面我們可以輸入一張自己的圖片進行檢測調用,里面包含了預測的不同食物名稱。
2.定義數據轉換
????????我們的數據集圖片大小不一樣我們要進行將圖片大小統一,如果數據集圖片大小不同意,我們的模型中全鏈接層就不能確定,導致我們的參數的個數都不能確定下來。
data_transforms={ # 字典存儲不同的數據轉換方式'train':transforms.Compose([ # 組合多個轉換操作transforms.Resize([256,256]), # 調整圖像大小為256x256transforms.ToTensor(), # 轉換為Tensor格式]),'valid':transforms.Compose([transforms.Resize([256, 256]),transforms.ToTensor(),]),
}
3. 自定義數據集類
????????這里就是調用dataset類,讓后面的dataloader去通過我們創建的train.txt和 test.txt讀取自己的數據集圖片,然后返回圖片跟標簽類別
class food_dataset(Dataset): # 繼承PyTorch的Dataset類def __init__(self, file_path, transform=None):# 初始化:讀取文件列表并存儲圖像路徑和標簽self.file_path = file_path # 數據文件路徑(如train.txt)self.imgs = [] # 存儲圖像路徑列表self.labels = [] # 存儲標簽列表self.transform = transform # 數據轉換函數# 讀取train.txt/test.txt文件with open(file_path, 'r') as f:# 按行拆分,每行按空格分割為[圖像路徑, 標簽]samples = [x.strip().split(' ') for x in f.readlines()]for img_path, label in samples:self.imgs.append(img_path) # 存儲圖像路徑self.labels.append(label) # 存儲標簽(字符串格式)def __len__(self):# 返回數據集總樣本數(必須實現的方法)return len(self.imgs)def __getitem__(self, idx):# 根據索引獲取單個樣本(必須實現的方法)# 1. 讀取圖像image = Image.open(self.imgs[idx]) # 用PIL打開圖像# 2. 應用預處理if self.transform:image = self.transform(image) # 轉換為張量并調整大小# 3. 處理標簽:轉換為整數張量label = self.labels[idx] # 原始標簽是字符串label = torch.from_numpy(np.array(label, dtype=np.int64)) # 轉換為int64類型張量return image, label # 返回(圖像張量,標簽張量)
- 為什么需要自定義 Dataset:PyTorch 的 DataLoader 需要通過 Dataset 類加載數據,自定義類可靈活適配不同數據格式。
- 核心方法:
__init__
:初始化時讀取文件列表,無需一次性加載所有圖像(節省內存)。__len__
:讓 DataLoader 知道數據集大小,用于迭代。__getitem__
:按需加載單個樣本(延遲加載),避免內存溢出。
4.創建數據加載器(DataLoader)
# 實例化數據集
train_data = food_dataset(file_path='train.txt', transform=data_transforms['train'])
test_data = food_dataset(file_path='test.txt', transform=data_transforms['train'])# 創建數據加載器(批處理、打亂數據)
train_dataloader = DataLoader(train_data, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=32, shuffle=True)
- DataLoader 作用:
- 批量加載數據(
batch_size=32
:每次加載 32 張圖像)。 - 打亂訓練數據(
shuffle=True
:每個 epoch 重新打亂順序)。 - 支持多線程加載(默認參數,加速數據讀取)。
- 批量加載數據(
- 輸出格式:每次迭代返回
(images, labels)
,其中images
形狀為(32, 3, 256, 256)
(批次大小 × 通道數 × 高 × 寬),labels
形狀為(32,)
。
5.選擇計算設備
# 自動選擇最優計算設備
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
- 優先級:GPU (cuda) > 蘋果芯片 GPU (mps) > CPU。
- 作用:將模型和數據遷移到指定設備,加速計算(GPU 比 CPU 快 10~100 倍)
6.定義 CNN 模型結構
class CNN(nn.Module): # 繼承PyTorch的神經網絡基類def __init__(self):super(CNN, self).__init__() # 初始化父類# 第一個卷積塊:卷積+激活+池化self.conv1 = nn.Sequential(nn.Conv2d(in_channels=3, # 輸入通道數(RGB圖像為3)out_channels=32, # 輸出通道數(卷積核數量)kernel_size=5, # 卷積核大小(5×5)stride=1, # 步長(每次滑動1像素)padding=2 # 填充(邊緣補0,保持輸出尺寸與輸入一致)),nn.ReLU(), # 激活函數(引入非線性)nn.MaxPool2d(2) # 最大池化(2×2窗口,輸出尺寸減半))# 第二個卷積塊:卷積+激活+卷積+池化self.conv2 = nn.Sequential(nn.Conv2d(32, 64, 5, 1, 2), # 輸入32通道,輸出64通道nn.ReLU(),nn.Conv2d(64, 64, 5, 1, 2), # 輸入64通道,輸出64通道nn.MaxPool2d(2) # 再次池化,尺寸減半)# 第三個卷積塊:卷積+激活(無池化)self.conv3 = nn.Sequential(nn.Conv2d(64, 128, 5, 1, 2), # 輸入64通道,輸出128通道nn.ReLU())# 全連接層:將特征映射到20個類別self.out = nn.Linear(128 * 64 * 64, 20) # 輸入維度=128通道×64×64特征圖def forward(self, x): # 前向傳播(定義數據流向)x = self.conv1(x) # 經過第一個卷積塊:輸出形狀(32, 128, 128)x = self.conv2(x) # 經過第二個卷積塊:輸出形狀(64, 64, 64)x = self.conv3(x) # 經過第三個卷積塊:輸出形狀(128, 64, 64)x = x.view(x.size(0), -1) # 展平特征圖:(batch_size, 128×64×64)output = self.out(x) # 全連接層輸出:(batch_size, 20)return output
- 模型結構解析:
- 卷積層:通過滑動窗口提取圖像局部特征(如邊緣、紋理)。
- 池化層:降低特征圖尺寸,減少參數數量(如 2×2 池化將尺寸減半)。
- 全連接層:將卷積提取的特征映射到類別空間(20 個類別)。
- 尺寸計算:輸入 256×256 圖像經過兩次池化(每次減半)后,得到 64×64 特征圖,最終展平為
128×64×64=524,288
維向量。
7.訓練函數
def train(dataloader, model, loss_fn, optimizer):model.train() # 切換到訓練模式(啟用 dropout/batchnorm等訓練特有的層)batch_size_num = 1 # 記錄當前批次編號for X, y in dataloader: # 迭代所有批次# 將數據遷移到計算設備X, y = X.to(device), y.to(device)# 1. 前向傳播:計算預測值pred = model.forward(X) # 等價于 model(X)# 2. 計算損失loss = loss_fn(pred, y) # 交叉熵損失:比較預測值與真實標簽# 3. 反向傳播與參數更新optimizer.zero_grad() # 清空歷史梯度(避免累積)loss.backward() # 計算梯度(反向傳播)optimizer.step() # 根據梯度更新參數(梯度下降)# 打印訓練進度(每32個批次)loss = loss.item() # 提取損失值(從張量轉為Python數值)if batch_size_num % 32 == 0:print(f"loss: {loss:>7f} [批次: {batch_size_num}]")batch_size_num += 1
- 核心流程:前向傳播→計算損失→反向傳播→更新參數(標準的深度學習訓練循環)。
- 細節說明:
model.train()
:啟用訓練模式(例如 BatchNorm 層會計算均值和方差)。optimizer.zero_grad()
:必須清空梯度,否則會累積上一輪的梯度。
8.測試函數
def test(dataloader, model, loss_fn):size = len(dataloader.dataset) # 測試集總樣本數num_batches = len(dataloader) # 批次數量model.eval() # 切換到評估模式(關閉 dropout/batchnorm等)test_loss, correct = 0, 0 # 總損失和正確預測數with torch.no_grad(): # 禁用梯度計算(節省內存,加速計算)for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model(X) # 前向傳播(無反向傳播)# 累加損失和正確數test_loss += loss_fn(pred, y).item()# 計算正確預測數:pred.argmax(1)取概率最大的類別索引correct += (pred.argmax(1) == y).type(torch.float).sum().item()# 計算平均損失和準確率test_loss /= num_batches # 平均損失correct /= size # 準確率(正確數/總樣本數)print(f"測試結果:\n 準確率: {(100*correct):>0.1f}%, 平均損失: {test_loss:>8f}")
- 與訓練的區別:
model.eval()
:關閉訓練特有的層(如 Dropout),確保預測穩定。torch.no_grad()
:禁用梯度計算,減少內存占用和計算時間。- 無參數更新:僅計算損失和準確率,不調整模型參數。
9.訓練與評估主流程
# 初始化損失函數、優化器和學習率調度器
loss_fn = nn.CrossEntropyLoss() # 交叉熵損失(適用于分類任務)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Adam優化器(學習率0.001)
# 學習率調度器:每10個epoch學習率乘以0.5(衰減)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)# 訓練20個epoch
epochs = 20
acc_s = [] # 可用于記錄準確率(此處未使用)
loss_s = [] # 可用于記錄損失(此處未使用)
for t in range(epochs):print(f"第{t+1}輪訓練\n-------------------")train(train_dataloader, model, loss_fn, optimizer) # 訓練一輪scheduler.step() # 更新學習率(每輪訓練后調用)
print("訓練完成!")
test(test_dataloader, model, loss_fn) # 最終測試
- 關鍵組件:
- 損失函數:
CrossEntropyLoss
適用于多分類任務,內置 SoftMax 激活。 - 優化器:Adam 是常用的自適應學習率優化器,收斂速度快。
- 學習率調度器:避免學習率過大導致不收斂或過小導致收斂緩慢,這里每 10 輪衰減一半。
- 損失函數:
- 執行流程:重復 20 次「訓練一輪 + 更新學習率」,最后在測試集上評估模型性能。
二、數據增強
????????當然通過我們模型訓練,發現訓練的結果非常的低,損失也很高,這是為什么呢?分析后發現是因為我們的數據集非常的少,那有什么辦法能對數據進行增加呢?我們就可以通過數據增強來使得訓練的數據變多(但是數據集不會變多)
1.整體結構
data_transforms = { # 字典結構,分別存儲訓練集和驗證集的轉換策略'train': transforms.Compose([...]), # 訓練集的數據增強和預處理'valid': transforms.Compose([...]) # 驗證集的預處理
}
- 使用
transforms.Compose
將多個預處理操作組合成一個管道,圖像會按順序依次經過這些操作 - 訓練集和驗證集使用不同的預處理策略:訓練集通常需要數據增強來提高泛化能力,驗證集則只需要基本預處理
2.訓練集轉換 ('train'
)
尺寸調整
transforms.Resize([300, 300]), # 將圖像大小調整為300×300像素
- 先將圖像放大到比最終需要的尺寸更大,為后續的裁剪操作預留空間
隨機旋轉
transforms.RandomRotation(45), # 在-45度到45度之間隨機旋轉圖像
- 數據增強手段:通過隨機旋轉增加樣本多樣性,使模型對圖像旋轉變化更魯棒
- 旋轉角度范圍是 [-45, 45] 度的隨機值
中心裁剪
transforms.CenterCrop(256), # 從圖像中心裁剪出256×256像素的區域
- 在旋轉后進行中心裁剪,得到固定大小的圖像
- 與 Resize 配合使用:先放大再裁剪,避免旋轉后圖像邊緣出現黑邊
隨機水平翻轉
transforms.RandomHorizontalFlip(p=0.5), # 以50%的概率隨機水平翻轉圖像
- 數據增強手段:模擬左右方向變化,例如 "貓" 的圖像左右翻轉后依然是 "貓"
p=0.5
表示有一半的概率會執行翻轉,另一半概率不翻轉
隨機垂直翻轉
transforms.RandomVerticalFlip(p=0.5), # 以50%的概率隨機垂直翻轉圖像
- 數據增強手段:模擬上下方向變化,適用于對垂直方向不敏感的場景
- 注意:某些場景不適合垂直翻轉(如人像),但食物類圖像通常適用
轉換為張量
transforms.ToTensor(), # 將PIL圖像或NumPy數組轉換為PyTorch張量
-
- 轉換后的數據格式為
(C, H, W)
(通道數 × 高度 × 寬度) - 同時會將像素值從 [0, 255] 范圍歸一化到 [0, 1] 范圍
- 轉換后的數據格式為
標準化
transforms.Normalize([0.485, 0.456, 0.486], [0.229, 0.224, 0.225]) # 標準化處理
- 對每個通道進行標準化:
output = (input - mean) / std
- 這里使用的均值和標準差是 ImageNet 數據集的統計值,是計算機視覺中常用的預處理參數
- 標準化的作用:使不同圖像的像素值分布更一致,加速模型收斂
3.驗證集轉換 ('valid'
)
尺寸調整
transforms.Resize([256, 256]), # 直接將圖像調整為256×256像素
- 驗證集不需要數據增強,直接調整到模型輸入需要的尺寸
轉換為張量
transforms.ToTensor(), # 與訓練集相同,轉換為PyTorch張量
標準化
transforms.Normalize([0.485, 0.456, 0.486], [0.229, 0.224, 0.225]) # 使用與訓練集相同的均值和標準差
- 必須使用與訓練集完全相同的標準化參數,否則會導致數據分布不一致,影響模型預測
4.訓練集與驗證集處理差異的原因
- 訓練集:使用多種數據增強技術(旋轉、翻轉等),目的是增加樣本多樣性,防止模型過擬合,提高模型的泛化能力
- 驗證集:只進行必要的預處理(尺寸調整、標準化),不使用數據增強,目的是真實反映模型在測試數據上的表現
完整代碼
data_transforms={ #字典'train':transforms.Compose([ #組合transforms.Resize([300,300]),# 圖像變換大小transforms.RandomRotation(45),#圖片旋轉,45度到-45度之間隨機旋轉transforms.CenterCrop(256),# 從中心開始裁剪transforms.RandomHorizontalFlip(p=0.5),#隨機水平翻轉,設置一個概率transforms.RandomVerticalFlip(p=0.5),#隨機垂直翻轉# transforms.RandomGrayscale(p=0.1),#概率換成灰度值transforms.ToTensor(), #數據轉換成ToTensortransforms.Normalize([0.485,0.456,0.486],[0.229,0.224,0.225])#歸一化,均值,標準差]),'valid':transforms.Compose([transforms.Resize([256, 256]),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.486],[0.229,0.224,0.225])]),}
將上面的data_transforms換成數據增強的就行,其他代碼不變。
通過數據增強,增加訓練次數我們的模型真去了會有所提升(但是還是比較低,這只是一種提升訓練數據量的一種方法,主要的還是要增加數據集)