【基于 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. 动态学习率调整让训练更稳定、精度更高
相关推荐
姜太小白1 分钟前
【Linux】CentOS 7 VNC 远程桌面配置
linux·python·centos
Ai.den2 分钟前
Windows 安装 DeerFlow 2.0
人工智能·windows·python·ai
geovindu5 分钟前
go: Model,Interface,DAL ,Factory,BLL using mysql
开发语言·mysql·设计模式·golang·软件构建
weixin_433179335 分钟前
python - 存储数据
python
XiYang-DING8 分钟前
【Java】反射
java·开发语言
意法半导体STM3210 分钟前
【官方原创】STM32 USBx Host HID standardalone移植示例 LAT1449
开发语言·前端·stm32·单片机·嵌入式硬件
阿坤带你走近大数据10 分钟前
数据API接口的数据源和目标源分别是什么?怎么设置?
java·python·api
别退11 分钟前
env_TensorFlow2.20.0_PyTorch2.9.0+cpu
python
若阳安好13 分钟前
【java】任务流批处理平台
java·开发语言
ak啊14 分钟前
Python后端开发准则
python