【基于 CNN 的食物图片分类:数据增强、最优模型保存与学习率调整实战】

目录

一、项目概述

二、核心技术点

三、完整实现代码

1.无数据增强:

2.数据增强:

3.最优模型保存

4.调整学习率函数

四、项目总结


一、项目概述

本项目基于 ** 卷积神经网络(CNN)** 实现食物图片分类任务,核心完成三大实验需求:

  • 对比有无数据增强对模型训练效果的影响
  • 实现最优模型自动保存与加载
  • 加入动态学习率调整优化训练过程

二、核心技术点

  • CNN 网络结构:3 组卷积 + 池化层提取图片特征,全连接层完成 20 分类
  • 数据增强:随机旋转、翻转、裁剪、颜色抖动,提升模型泛化能力
  • 最优模型保存:实时监测测试准确率,保存性能最好的模型
  • 学习率调整:使用 StepLR 动态衰减学习率,让模型收敛更稳定
  • 结果可视化:绘制准确率 / 损失曲线,直观对比训练效果

三、完整实现代码

1.无数据增强:

python 复制代码
import torch
from torch.utils.data import Dataset,DataLoader
import numpy as np
from PIL import Image
from torchvision import transforms


data_transforms = {
    'train': transforms.Compose([
        transforms.Resize([256, 256]),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'valid': transforms.Compose([
        transforms.Resize([256, 256]),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
}

#做了数据增强不代表 训练效果一定会变好,只能说大概率上会变好
class food_dataset(Dataset):    #food_dataset是自己创建的类名称,可以改为你需要的名称
    def __init__(self, file_path,transform=None): #类的初始化
        self.file_path = file_path
        self.imgs = []
        self.labels = []
        self.transform = transform
        with open(self.file_path) 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):  #类实例化对象后,可以使用len函数测量对象的个数
        return len(self.imgs)

    def __getitem__(self, idx): #关键,可通过索引的形式获取每一个图片数据及标签
        image = Image.open(self.imgs[idx])   #
        if self.transform:
            image = self.transform(image)

        label = self.labels[idx]
        label = torch.from_numpy(np.array(label,dtype = np.int64))
        return image, label

training_data = food_dataset(file_path = './train.txt',transform = data_transforms['train'])
test_data = food_dataset(file_path = './test.txt',transform = data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)  # 64张图片为一个包,
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

# '''展示训练数据集中的图片'''
# from matplotlib import pyplot as plt
# image, label = iter(train_dataloader).__next__()        #iter是一个迭代器函数。__next__()用于获取下一个数据
# sample = image[2]       #image
# sample = sample.permute((1, 2, 0)).numpy()  #tensor数据的维度转换
# plt.imshow(sample)
# plt.show()
# print('Label is: {}'.format(label[2].numpy()))


'''-------------cnn卷积神经网络部分----------------------'''
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")

''' 定义神经网络  '''
from torch import nn

class CNN(nn.Module):
    def __init__(self):         # 输入大小 (3, 288, 288)
        super(CNN, self).__init__()
        self.conv1 = nn.Sequential(  #将多个层组合成一起。
            nn.Conv2d(          #2d一般用于图像,3d用于视频数据(多一个时间维度),1d一般用于结构化的序列数据
                in_channels=3,  # 图像通道个数,1表示灰度图(确定了卷积核 组中的个数),
                out_channels=16,# 要得到几多少个特征图,卷积核的个数
                kernel_size = 5,  # 卷积核大小,5*5
                stride=1,       # 步长
                padding=2,      # 一般希望卷积核处理后的结果大小与处理前的数据大小相同,效果会比较好。那padding改如何设计呢?建议stride为1,kernel_size = 2*padding+1
            ),                  # 输出的特征图为 (16, 288, 288)
            nn.ReLU(),  # relu层
            nn.MaxPool2d(kernel_size=2),  # 进行池化操作(2x2 区域), 输出结果为: (16, 144, 144)
        )

        self.conv2 = nn.Sequential(  #输入 (16, 144, 144)
            nn.Conv2d(16, 32, 5, 1, 2),  # 输出 (32, 144, 144)
            nn.ReLU(),  # relu层
            nn.Conv2d(32, 32, 5, 1, 2), # 输出 (32, 144, 144)
            nn.ReLU(),
            nn.MaxPool2d(2),  # 输出 (32, 72, 72)
        )

        self.conv3 = nn.Sequential(  #输入 (32, 72, 72)
            nn.Conv2d(32, 128, 5, 1, 2),
            nn.ReLU(),  # 输出 (128, 72, 72)
        )

        self.out = nn.Linear(128 * 64 * 64, 20)  # 全连接层得到的结果

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)# 输出 (64,64, 32, 32)
        x = x.view(x.size(0), -1)  # flatten操作,结果为:(batch_size, 64 * 32 * 32)
        output = self.out(x)
        return output

model = CNN().to(device)
print(model)

def train(dataloader, model, loss_fn, optimizer):
    model.train()
#pytorch提供2种方式来切换训练和测试的模式,分别是:model.train() 和 model.eval()。
# 一般用法是:在训练开始之前写上model.trian(),在测试时写上 model.eval() 。
    batch_size_num = 1

    for X, y in dataloader:                 #其中batch为每一个数据的编号
        X, y = X.to(device), y.to(device)   #把训练数据集和标签传入cpu或GPU
        pred = model.forward(X)             #自动初始化 w权值
        loss = loss_fn(pred, y)             #通过交叉熵损失函数计算损失值loss
        # Backpropagation 进来一个batch的数据,计算一次梯度,更新一次网络
        optimizer.zero_grad()               #梯度值清零
        loss.backward()                     #反向传播计算得到每个参数的梯度值
        optimizer.step()                    #根据梯度更新网络参数

        train_loss = loss.item()                  #获取损失值
        print(f"loss: {train_loss:>7f}  [number:{batch_size_num}]")
        batch_size_num += 1

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()    #
    test_loss, correct = 0, 0
    with torch.no_grad():   #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候。这可以减少计算所用内存消耗。
        for X, y in dataloader:#一个批次的循环
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item() #
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            a = (pred.argmax(1) == y)  #dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
            b = (pred.argmax(1) == y).type(torch.float)
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
    acc_s.append(correct)
    loss_s.append(test_loss)
loss_fn = nn.CrossEntropyLoss() #创建交叉熵损失函数对象,因为手写字识别中一共有10个数字,输出会有10个结果
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)#创建一个优化器,SGD为随机梯度下降算法??

'''训练模型'''
epochs = 10
acc_s = []#用来保存测试集每一轮的acc
loss_s = []#用来报错测试集每一轮的loss
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)

#什么时候可以确定模型训练好了
#1、loss已经稳定在一定区间内震荡
#2、调整参数
#3、改进算法

'''绘制训练效果曲线'''
from matplotlib import pyplot as plt
plt.subplot(1,2,1)
plt.plot(range(0,epochs),acc_s)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.subplot(1,2,2)
plt.plot(range(0,epochs),loss_s)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
print("Done!")
#

运行结果:

2.数据增强:

python 复制代码
import torch
from torch.utils.data import Dataset,DataLoader
import numpy as np
from PIL import Image
from torchvision import transforms


data_transforms = { #机器学习的时候,数据进行归一化??  图片做归一化    ->0~1
    'train':
        transforms.Compose([
        transforms.Resize([300,300]),   #是图像变换大小   opencv   int8   0~
        transforms.RandomRotation(45),#随机旋转,-45到45度之间随机选
        transforms.CenterCrop(256),#从中心开始裁剪[256,256]
        transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转 选择一个概率概率
        transforms.RandomVerticalFlip(p=0.5),#随机垂直翻转
        transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),#参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
        transforms.RandomGrayscale(p=0.1),#概率转换成灰度率,3通道就是R=G=B,
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#标准化,均值,标准差,
    ]),
    'valid':#验证集 不需要对图像进行数据增强
        transforms.Compose([
        transforms.Resize([256,256]),  #
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

#做了数据增强不代表 训练效果一定会变好,只能说大概率上会变好
class food_dataset(Dataset):    #food_dataset是自己创建的类名称,可以改为你需要的名称
    def __init__(self, file_path,transform=None): #类的初始化
        self.file_path = file_path
        self.imgs = []
        self.labels = []
        self.transform = transform
        with open(self.file_path) 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):  #类实例化对象后,可以使用len函数测量对象的个数
        return len(self.imgs)

    def __getitem__(self, idx): #关键,可通过索引的形式获取每一个图片数据及标签
        image = Image.open(self.imgs[idx])   #
        if self.transform:
            image = self.transform(image)

        label = self.labels[idx]
        label = torch.from_numpy(np.array(label,dtype = np.int64))
        return image, label

training_data = food_dataset(file_path = './train.txt',transform = data_transforms['train'])
test_data = food_dataset(file_path = './test.txt',transform = data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)  # 64张图片为一个包,
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

# '''展示训练数据集中的图片'''
# from matplotlib import pyplot as plt
# image, label = iter(train_dataloader).__next__()        #iter是一个迭代器函数。__next__()用于获取下一个数据
# sample = image[2]       #image
# sample = sample.permute((1, 2, 0)).numpy()  #tensor数据的维度转换
# plt.imshow(sample)
# plt.show()
# print('Label is: {}'.format(label[2].numpy()))


'''-------------cnn卷积神经网络部分----------------------'''
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")

''' 定义神经网络  '''
from torch import nn

class CNN(nn.Module):
    def __init__(self):         # 输入大小 (3, 288, 288)
        super(CNN, self).__init__()
        self.conv1 = nn.Sequential(  #将多个层组合成一起。
            nn.Conv2d(          #2d一般用于图像,3d用于视频数据(多一个时间维度),1d一般用于结构化的序列数据
                in_channels=3,  # 图像通道个数,1表示灰度图(确定了卷积核 组中的个数),
                out_channels=16,# 要得到几多少个特征图,卷积核的个数
                kernel_size = 5,  # 卷积核大小,5*5
                stride=1,       # 步长
                padding=2,      # 一般希望卷积核处理后的结果大小与处理前的数据大小相同,效果会比较好。那padding改如何设计呢?建议stride为1,kernel_size = 2*padding+1
            ),                  # 输出的特征图为 (16, 288, 288)
            nn.ReLU(),  # relu层
            nn.MaxPool2d(kernel_size=2),  # 进行池化操作(2x2 区域), 输出结果为: (16, 144, 144)
        )

        self.conv2 = nn.Sequential(  #输入 (16, 144, 144)
            nn.Conv2d(16, 32, 5, 1, 2),  # 输出 (32, 144, 144)
            nn.ReLU(),  # relu层
            nn.Conv2d(32, 32, 5, 1, 2), # 输出 (32, 144, 144)
            nn.ReLU(),
            nn.MaxPool2d(2),  # 输出 (32, 72, 72)
        )

        self.conv3 = nn.Sequential(  #输入 (32, 72, 72)
            nn.Conv2d(32, 128, 5, 1, 2),
            nn.ReLU(),  # 输出 (128, 72, 72)
        )

        self.out = nn.Linear(128 * 64 * 64, 20)  # 全连接层得到的结果

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)# 输出 (64,64, 32, 32)
        x = x.view(x.size(0), -1)  # flatten操作,结果为:(batch_size, 64 * 32 * 32)
        output = self.out(x)
        return output

model = CNN().to(device)
print(model)

def train(dataloader, model, loss_fn, optimizer):
    model.train()
#pytorch提供2种方式来切换训练和测试的模式,分别是:model.train() 和 model.eval()。
# 一般用法是:在训练开始之前写上model.trian(),在测试时写上 model.eval() 。
    batch_size_num = 1

    for X, y in dataloader:                 #其中batch为每一个数据的编号
        X, y = X.to(device), y.to(device)   #把训练数据集和标签传入cpu或GPU
        pred = model.forward(X)             #自动初始化 w权值
        loss = loss_fn(pred, y)             #通过交叉熵损失函数计算损失值loss
        # Backpropagation 进来一个batch的数据,计算一次梯度,更新一次网络
        optimizer.zero_grad()               #梯度值清零
        loss.backward()                     #反向传播计算得到每个参数的梯度值
        optimizer.step()                    #根据梯度更新网络参数

        train_loss = loss.item()                  #获取损失值
        print(f"loss: {train_loss:>7f}  [number:{batch_size_num}]")
        batch_size_num += 1

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()    #
    test_loss, correct = 0, 0
    with torch.no_grad():   #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候。这可以减少计算所用内存消耗。
        for X, y in dataloader:#一个批次的循环
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item() #
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            a = (pred.argmax(1) == y)  #dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
            b = (pred.argmax(1) == y).type(torch.float)
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
    acc_s.append(correct)
    loss_s.append(test_loss)
loss_fn = nn.CrossEntropyLoss() #创建交叉熵损失函数对象,因为手写字识别中一共有10个数字,输出会有10个结果
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)#创建一个优化器,SGD为随机梯度下降算法??

'''训练模型'''
epochs = 10
acc_s = []#用来保存测试集每一轮的acc
loss_s = []#用来报错测试集每一轮的loss
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)

#什么时候可以确定模型训练好了
#1、loss已经稳定在一定区间内震荡
#2、调整参数
#3、改进算法

'''绘制训练效果曲线'''
from matplotlib import pyplot as plt
plt.subplot(1,2,1)
plt.plot(range(0,epochs),acc_s)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.subplot(1,2,2)
plt.plot(range(0,epochs),loss_s)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
print("Done!")
#

运行结果:

  1. 有无数据增强对比

• 无数据增强:准确率波动大,损失容易回升,模型泛化能力差

• 有数据增强:准确率稳步上升,曲线更平滑,有效缓解过拟合

3.最优模型保存

训练过程中自动保存测试准确率最高的模型,避免训练后期模型退化,方便直接用于预测。

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import warnings
warnings.filterwarnings("ignore")

# ===================== 1. 基础配置 =====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 图像预处理
transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.ToTensor(),
    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])

# ===================== 2. 数据集类 =====================
class FoodDataset(Dataset):
    def __init__(self, txt_path, transform=None):
        self.transform = transform
        self.imgs = []
        self.labels = []
        with open(txt_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                line = line.strip()
                if not line:
                    continue
                img_path, label = line.split()
                self.imgs.append(img_path)
                self.labels.append(int(label))

    def __len__(self):
        return len(self.imgs)

    def __getitem__(self, idx):
        img = Image.open(self.imgs[idx]).convert('RGB')
        if self.transform:
            img = self.transform(img)
        label = torch.tensor(self.labels[idx], dtype=torch.long)
        return img, label

# ===================== 3. CNN 模型定义 =====================
class CNN(nn.Module):
    def __init__(self, num_classes=5):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        self.classifier = nn.Linear(64 * 32 * 32, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.flatten(1)
        x = self.classifier(x)
        return x

# ===================== 4. 训练函数(你的原代码) =====================
def train(dataloader, model, loss_fn, optimizer):
    model.train()
    for X, y in dataloader:
        X, y = X.to(device), y.to(device)
        pred = model.forward(X)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# ===================== 5. 测试函数(你的原代码 + 保存最优模型) =====================
best_acc = 0
acc_s = []
loss_s = []

def test(dataloader, model, loss_fn):
    global best_acc
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f}")
    acc_s.append(correct)
    loss_s.append(test_loss)

    # 保存最优模型的2种方法(你的原代码)
    if correct > best_acc:
        best_acc = correct
        # 1、保存模型参数方法
        # torch.save(model.state_dict(), "best2026-01.pth")
        # 2、保存完整模型
        torch.save(model, 'best2026-01.pt')
        print(f"accuracy: {100*best_acc:.1f}%")

# ===================== 6. 训练主循环(你的原代码) =====================
if __name__ == "__main__":
    # 加载数据
    batch_size = 8
    train_dataset = FoodDataset("trainda.txt", transform=transform)
    test_dataset = FoodDataset("testda.txt", transform=transform)
    train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

    # 自动识别类别数
    all_labels = []
    with open("trainda.txt", encoding='utf-8') as f:
        for line in f:
            if line.strip():
                all_labels.append(int(line.strip().split()[-1]))
    num_classes = max(all_labels) + 1

    # 初始化模型、损失函数、优化器
    model = CNN(num_classes).to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=1e-4)

    # 训练模型(你的原代码)
    epochs = 150
    for t in range(epochs):
        print(f"Epoch {t+1}\n-------------------------------")
        train(train_dataloader, model, loss_fn, optimizer)
        test(test_dataloader, model, loss_fn)

    print(f"\nDone!Best accurcy: {100*best_acc:.1f}%")

# ===================== 7. 加载最优模型(你的原代码) =====================
def load_best_model():
    # 方法1:读取参数的方法(需要先定义模型结构)
    # model = CNN().to(device)
    # model.load_state_dict(torch.load("best.pth"))

    # 方法2:读取完整模型的方法(无需提前创建model)
    model = CNN().to(device)
    model = torch.load('best2026-01.pt')
    model.eval()  # 固定模型参数和数据,防止后面被修改
    return model

# 示例:加载模型并预测
if __name__ == "__main__":
    model = load_best_model()
    print("模型加载成功!")

运行结果:

4.调整学习率函数

每 5 轮学习率衰减为原来的 1/2,让模型前期快速收敛,后期精细优化,提升最终精度。

python 复制代码
import torch
from torch.utils.data import Dataset,DataLoader
import numpy as np
from PIL import Image
from torchvision import transforms


data_transforms = { #机器学习的时候,数据进行归一化??  图片做归一化    ->0~1
    'train':
        transforms.Compose([
        transforms.Resize([300,300]),   #是图像变换大小   opencv   int8   0~
        transforms.RandomRotation(45),#随机旋转,-45到45度之间随机选
        transforms.CenterCrop(256),#从中心开始裁剪[256,256]
        transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转 选择一个概率概率
        transforms.RandomVerticalFlip(p=0.5),#随机垂直翻转
        transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),#参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
        transforms.RandomGrayscale(p=0.1),#概率转换成灰度率,3通道就是R=G=B,
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#标准化,均值,标准差,
    ]),
    'valid':#验证集 不需要对图像进行数据增强
        transforms.Compose([
        transforms.Resize([256,256]),  #
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

#做了数据增强不代表 训练效果一定会变好,只能说大概率上会变好
class food_dataset(Dataset):    #food_dataset是自己创建的类名称,可以改为你需要的名称
    def __init__(self, file_path,transform=None): #类的初始化
        self.file_path = file_path
        self.imgs = []
        self.labels = []
        self.transform = transform
        with open(self.file_path) 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):  #类实例化对象后,可以使用len函数测量对象的个数
        return len(self.imgs)

    def __getitem__(self, idx): #关键,可通过索引的形式获取每一个图片数据及标签
        image = Image.open(self.imgs[idx])   #
        if self.transform:
            image = self.transform(image)

        label = self.labels[idx]
        label = torch.from_numpy(np.array(label,dtype = np.int64))
        return image, label

training_data = food_dataset(file_path = './train.txt',transform = data_transforms['train'])
test_data = food_dataset(file_path = './test.txt',transform = data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)  # 64张图片为一个包,
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

'''展示训练数据集中的图片'''
# from matplotlib import pyplot as plt
# image, label = iter(train_dataloader).__next__()        #iter是一个迭代器函数。__next__()用于获取下一个数据
# sample = image[2]       #image
# sample = sample.permute((1, 2, 0)).numpy()  #tensor数据的维度转换
# plt.imshow(sample)
# plt.show()
# print('Label is: {}'.format(label[2].numpy()))


'''-------------cnn卷积神经网络部分----------------------'''
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")

''' 定义神经网络  '''
from torch import nn

class CNN(nn.Module):
    def __init__(self):         # 输入大小 (3, 288, 288)
        super(CNN, self).__init__()
        self.conv1 = nn.Sequential(  #将多个层组合成一起。
            nn.Conv2d(          #2d一般用于图像,3d用于视频数据(多一个时间维度),1d一般用于结构化的序列数据
                in_channels=3,  # 图像通道个数,1表示灰度图(确定了卷积核 组中的个数),
                out_channels=16,# 要得到几多少个特征图,卷积核的个数
                kernel_size = 5,  # 卷积核大小,5*5
                stride=1,       # 步长
                padding=2,      # 一般希望卷积核处理后的结果大小与处理前的数据大小相同,效果会比较好。那padding改如何设计呢?建议stride为1,kernel_size = 2*padding+1
            ),                  # 输出的特征图为 (16, 288, 288)
            nn.ReLU(),  # relu层
            nn.MaxPool2d(kernel_size=2),  # 进行池化操作(2x2 区域), 输出结果为: (16, 144, 144)
        )

        self.conv2 = nn.Sequential(  #输入 (16, 144, 144)
            nn.Conv2d(16, 32, 5, 1, 2),  # 输出 (32, 144, 144)
            nn.ReLU(),  # relu层
            nn.Conv2d(32, 32, 5, 1, 2), # 输出 (32, 144, 144)
            nn.ReLU(),
            nn.MaxPool2d(2),  # 输出 (32, 72, 72)
        )

        self.conv3 = nn.Sequential(  #输入 (32, 72, 72)
            nn.Conv2d(32, 128, 5, 1, 2),
            nn.ReLU(),  # 输出 (128, 72, 72)
        )

        self.out = nn.Linear(128 * 64 * 64, 20)  # 全连接层得到的结果

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)# 输出 (64,64, 32, 32)
        x = x.view(x.size(0), -1)  # flatten操作,结果为:(batch_size, 64 * 32 * 32)
        output = self.out(x)
        return output

model = CNN().to(device)
print(model)

def train(dataloader, model, loss_fn, optimizer):
    model.train()
#pytorch提供2种方式来切换训练和测试的模式,分别是:model.train() 和 model.eval()。
# 一般用法是:在训练开始之前写上model.trian(),在测试时写上 model.eval() 。
    batch_size_num = 1

    for X, y in dataloader:                 #其中batch为每一个数据的编号
        X, y = X.to(device), y.to(device)   #把训练数据集和标签传入cpu或GPU
        pred = model.forward(X)             #自动初始化 w权值
        loss = loss_fn(pred, y)             #通过交叉熵损失函数计算损失值loss
        # Backpropagation 进来一个batch的数据,计算一次梯度,更新一次网络
        optimizer.zero_grad()               #梯度值清零
        loss.backward()                     #反向传播计算得到每个参数的梯度值
        optimizer.step()                    #根据梯度更新网络参数

        train_loss = loss.item()                  #获取损失值
        print(f"loss: {train_loss:>7f}  [number:{batch_size_num}]")
        batch_size_num += 1
best_acc = 0.0
acc_s = []
loss_s = []


def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()    #
    test_loss, correct = 0, 0
    with torch.no_grad():   #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候。这可以减少计算所用内存消耗。
        for X, y in dataloader:#一个批次的循环
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item() #
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            a = (pred.argmax(1) == y)  #dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
            b = (pred.argmax(1) == y).type(torch.float)
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
    acc_s.append(correct)
    loss_s.append(test_loss)
loss_fn = nn.CrossEntropyLoss() #创建交叉熵损失函数对象,因为手写字识别中一共有10个数字,输出会有10个结果
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)#创建一个优化器,SGD为随机梯度下降算法??
scheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size=5,gamma=0.5)#调整学习率函数


'''训练模型'''
epochs = 10
acc_s = []#用来保存测试集每一轮的acc
loss_s = []#用来报错测试集每一轮的loss
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    scheduler.step()#在每个epoch的训练中,使用scheduler.step()语句进行学习率更新
    print(best_acc)
    test(test_dataloader, model, loss_fn)

#什么时候可以确定模型训练好了
#1、loss已经稳定在一定区间内震荡
#2、调整参数
#3、改进算法

'''绘制训练效果曲线'''
from matplotlib import pyplot as plt
plt.subplot(1,2,1)
plt.plot(range(0,epochs),acc_s)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.subplot(1,2,2)
plt.plot(range(0,epochs),loss_s)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
print("Done!")
#

运行结果:

四、项目总结

本项目完整实现了食物图片分类的深度学习流程,掌握三大核心技能:

  1. 数据增强是提升模型泛化能力的关键手段
  2. 最优模型保存保证实战中使用最佳性能模型
  3. 动态学习率调整让训练更稳定、精度更高
相关推荐
2501_945424802 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
木井巳2 小时前
【多线程】常见的锁策略及 synchronized 的原理
java·开发语言
代码改善世界2 小时前
【C++初阶】类和对象(二):默认成员函数详解与日期类完整实现
开发语言·c++
专注VB编程开发20年2 小时前
VS2026调试TS用的解析/运行引擎:确实是 ChakraCore.dll(微软自研 JS 引擎)
开发语言·javascript·microsoft
rosmis2 小时前
复杂工程拆解:自顶向下设计,自底向上实现
人工智能·python·机器人·自动化·自动驾驶·硬件工程·制造
njidf2 小时前
Python上下文管理器(with语句)的原理与实践
jvm·数据库·python
郝学胜-神的一滴2 小时前
深入理解Python生成器:从基础到斐波那契实战
开发语言·前端·python·程序人生
2301_764441332 小时前
python与Streamlit构建的旅游行业数据分析Dashboard项目
python·数据分析·旅游
问水っ2 小时前
Qt Creator快速入门 第三版 第6章 事件系统
开发语言·qt