目录
一、项目概述
本项目基于 ** 卷积神经网络(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!")
#
运行结果:


- 有无数据增强对比
• 无数据增强:准确率波动大,损失容易回升,模型泛化能力差
• 有数据增强:准确率稳步上升,曲线更平滑,有效缓解过拟合
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!")
#

运行结果:

四、项目总结
本项目完整实现了食物图片分类的深度学习流程,掌握三大核心技能:
- 数据增强是提升模型泛化能力的关键手段
- 最优模型保存保证实战中使用最佳性能模型
- 动态学习率调整让训练更稳定、精度更高