文章目录
前言
上一篇文章我们把垃圾分类项目搭建起来后发现,有很多问题和需要改进的地方。
首先问题在于:
一、我们数据集只有四十类,但是在使用时我们用了五十类,为什么程序依然可以运行?
二、为什么单照片预测时总是不准,但是程序显示的准确值却很高?
三、我们如何精确的分析清楚直观的看到模型的效果?
一、我们数据集只有四十类,但是在使用时我们用了五十类,为什么程序依然可以运行?
import torch
import torch.nn as nn
import torch.nn.functional as F
class simpleconv3(nn.Module):
def __init__(self, num_classes):
super(simpleconv3, self).__init__()
self.conv1 = nn.Conv2d(3, 12, 3, 2) # 输入图片大小为3*48*48,输出特征图大小为12*23*23,卷积核大小为3*3,步长为2
self.bn1 = nn.BatchNorm2d(12)
self.conv2 = nn.Conv2d(12, 24, 3, 2) # 输入图片大小为12*23*23,输出特征图大小为24*11*11,卷积核大小为3*3,步长为2
self.bn2 = nn.BatchNorm2d(24)
self.conv3 = nn.Conv2d(24, 48, 3, 2) # 输入图片大小为24*11*11,输出特征图大小为48*5*5,卷积核大小为3*3,步长为2
self.bn3 = nn.BatchNorm2d(48)
self.fc1 = nn.Linear(48 * 5 * 5, 1200) # 输入向量长为48*5*5=1200,输出向量长为1200
self.fc2 = nn.Linear(1200, 128) # 输入向量长为1200,输出向量长为128
self.fc3 = nn.Linear(128, num_classes) # 输入向量长为128,输出向量长为num_classes,等于类别数
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
x = x.view(x.size(0), -1) # 动态调整批次大小
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
我们在net中定义了一个 simpleconv3 模型,且在train文件指定了 num_classes=50,但数据集只有四十类。程序依然可以运行:
1.模型架构:尽管你的模型定义了 50 个输出类(在最后一层 fc3 中),模型依然可以处理数据。这是因为模型的结构允许它生成 50 维的输出向量。(定义的层数由训练的参数确定
2.数据集标签:如果数据集中的标签范围是 [0, 39],那么在训练和评估过程中,模型的最后一层仍会产生一个 50 维的向量,尽管只有前 40 个维度有实际的标签对应。剩下的 10 个维度并不会被使用或对损失计算产生影响。
3.损失函数:交叉熵损失函数 nn.CrossEntropyLoss 可以处理这种情况。它会忽略没有标签对应的类别(即 40-49 类),因为在计算损失时,它只会考虑与数据集标签对应的预测值。
4.梯度更新:在训练过程中,梯度只会更新与实际标签相关的权重,即使模型有更多的输出类别。未使用的输出类别(即 40-49 类)不会影响模型的权重更新。
你的代码中定义了一个 simpleconv3 模型,且指定了 num_classes=50,但数据集只有四十类。程序依然可以运行的原因有以下几点:
模型架构:尽管你的模型定义了 50 个输出类(在最后一层 fc3 中),模型依然可以处理数据。这是因为模型的结构允许它生成 50 维的输出向量。
数据集标签:如果数据集中的标签范围是 [0, 39],那么在训练和评估过程中,模型的最后一层仍会产生一个 50 维的向量,尽管只有前 40 个维度有实际的标签对应。剩下的 10 个维度并不会被使用或对损失计算产生影响。
损失函数:交叉熵损失函数 nn.CrossEntropyLoss 可以处理这种情况。它会忽略没有标签对应的类别(即 40-49 类),因为在计算损失时,它只会考虑与数据集标签对应的预测值。
梯度更新:在训练过程中,梯度只会更新与实际标签相关的权重,即使模型有更多的输出类别。未使用的输出类别(即 40-49 类)不会影响模型的权重更新。
虽然程序可以运行,但使用 num_classes=50 会导致模型多余的计算和参数,这不是最佳实践。应将 num_classes 设置为数据集的实际类别数以优化计算资源并避免混淆。当然,问题很好解决,将类别改为40,结果出现了下面的错误。
具体报错:
train_model(model, train_dataloader, valid_dataloader, num_epochcheckpoint interval=1, final _model_path=“final_model.pth”)File “E:llajifenleillajifenleiltrain.py”, line 53, in train_modelrunning_loss += loss.item()RuntimeError:CUDA error:device-side assert triggeredCUDA kernel errors might be asynchronously reported at some other APcall,so the stacktrace below might be incorrect点For debugging consider passing CUDA_LAUNCH_BLOCKING=1.立Compile with TORCH USE CUDA DSA to enable device-side assertions.
RuntimeError: CUDA error: device-side assert triggered 可能是由于以下原因之一导致的:
1.标签超出范围:这通常是由于标签超出了模型输出的类别范围。例如,如果你的模型输出的类别数为 40,但标签中的某些值大于或等于 40,就会引发此错误。
2.数据不一致:数据集中的图像或标签存在不一致或损坏的情况。
为了探问题的原因,我使用了下面这段代码首先查看是否是标签范围的问题:
from dataset.dataset import train_dataloader, valid_dataloader, test_dataloader
for images, labels in train_dataloader:
print(labels) # 打印标签值以检查
break
def get_all_labels(txt_file):
labels = set()
with open(txt_file, 'r') as file:
for line in file:
_, label = line.strip().split('\t')
labels.add(int(label))
return labels
train_labels = get_all_labels(r'E:\lajifenlei\lajifenlei\dataset\test.txt')
valid_labels = get_all_labels(r'E:\lajifenlei\lajifenlei\dataset\valid.txt')
print("Train labels:", train_labels)
print("Valid labels:", valid_labels)
最后输出的结果:
D:\anacondalenvslyolov8\python.exe E:lajifenleillajifenleil123.
pytensor([24,4,19,14,34,10,4,30,12,2,4.15,25,28. 4,20,39,206,19,12,4,40,8,36,17.15.28.37.6.38.41)
Train labels:{1,2 3 4 5, 6 7,8 9,10 11,12,13,14,15 16 17,18,19,,32,,33,34,35,36,37,38,39,40}
Valid labels::{1,2 3 4 5, 6 7,8 9,10 11,12,13,14,15 16 17,18,19,,32,,33,34,35,36,37,38,39,40}
果然,四十种类标签应该是0-39,而不是1-40,这说明在划分数据集时出现了问题,也与后面的准确率不低却总是预测错误的原因。
二、为什么单照片预测时总是不准,但是程序显示的准确值却很高?
这个问题和上面的标签值超值问题相关联
我们的原代码是这样的:
import os
import random
train_ratio = 0.9
rootdata = "D:/Administrator/Desktop/all/all"
train_list, test_list = [], []
class_flag = 0 # 从0开始标记类别
# 遍历根目录
for root, dirs, files in os.walk(rootdata):
# 遍历每个类别文件夹中的所有文件
for file in files:
file_path = os.path.join(root, file)
# 根据训练和测试比例划分数据
if random.random() < train_ratio:
train_list.append(f"{file_path}\t{class_flag}\n")
else:
test_list.append(f"{file_path}\t{class_flag}\n")
# 处理完一个文件夹中的所有文件后,切换到下一个类别
class_flag += 1
# 打乱列表顺序
random.shuffle(train_list)
random.shuffle(test_list)
# 将训练和测试数据写入文件
with open('train.txt', 'w', encoding='UTF-8') as f:
f.writelines(train_list)
with open('test.txt', 'w', encoding='UTF-8') as f:
f.writelines(test_list)
我们从这段代码:
class_flag = 0 # 从0开始标记类别
#遍历根目录
for root, dirs, files in os.walk(rootdata):
# 遍历每个类别文件夹中的所有文件
for file in files:
file_path = os.path.join(root, file)
# 根据训练和测试比例划分数据
if random.random() < train_ratio:
train_list.append(f"{file_path}\t{class_flag}\n")
else:
test_list.append(f"{file_path}\t{class_flag}\n")
# 处理完一个文件夹中的所有文件后,切换到下一个类别
class_flag += 1
发现,在遍历每个类别文件夹中的所有文件时,我们想当然的认为他是按照目录的顺序开始读取的,这就出现了问题,实际情况是电脑在读取文件时是随机而不按照秩序的(但我们的标签即种类就是文件夹名即标号),这也是我们出错的原因(并且最后类别的+1导致了我们标签范围的超值)。
修改的代码如下:
import os
import random
# 定义训练集、验证集和测试集比例
train_ratio = 0.8
valid_ratio = 0.1
rootdata = r"D:\Administrator\Desktop\all\all"
train_list, valid_list, test_list = [], [], []
# 获取所有类别目录并排序
all_dirs = sorted([d for d in os.listdir(rootdata) if os.path.isdir(os.path.join(rootdata, d))])
# 遍历根目录下的每个类别目录
for dir_name in all_dirs:
dir_path = os.path.join(rootdata, dir_name)
files = os.listdir(dir_path)
# 遍历每个类别文件夹中的所有文件
for file in files:
file_path = os.path.join(dir_path, file)
rand_val = random.random()
# 根据训练、验证和测试比例划分数据
if rand_val < train_ratio:
train_list.append(f"{file_path},{dir_name}\n")
elif rand_val < train_ratio + valid_ratio:
valid_list.append(f"{file_path},{dir_name}\n")
else:
test_list.append(f"{file_path},{dir_name}\n")
# 打乱列表顺序
random.shuffle(train_list)
random.shuffle(valid_list)
random.shuffle(test_list)
# 将训练、验证和测试数据写入文件
train_file_path = 'train.txt'
valid_file_path = 'valid.txt'
test_file_path = 'test.txt'
with open(train_file_path, 'w', encoding='UTF-8') as f:
f.writelines(train_list)
with open(valid_file_path, 'w', encoding='UTF-8') as f:
f.writelines(valid_list)
with open(test_file_path, 'w', encoding='UTF-8') as f:
f.writelines(test_list)
三、我们如何精确的分析清楚直观的看到模型的效果?
想要清晰且直观的看到模型的训练效果和结构,就要用到 tensorboard这个库。
TensorBoard 是一个用于可视化 TensorFlow 或 PyTorch 等深度学习框架训练过程的工具。它提供了多种图表和工具来帮助用户理解和优化模型。
主要功能
1.可视化训练过程中的指标
标量(Scalars):如损失(loss)和准确率(accuracy)等,可以随时间变化进行记录和显示。
图像(Images):训练过程中生成的图像数据。
直方图(Histograms):权重、偏差、梯度等随时间的变化。
文本(Text):可以记录任意文本信息。
图(Graphs):显示计算图,帮助了解模型结构。
嵌入(Embeddings):可视化高维数据的低维表示。
比较实验结果
2.可以同时加载多个实验的结果,方便对比不同超参数、模型架构等的效果。
我们首先在此环境终端下下载tensorboard:
pip install tensorboard
然后修改训练代码,使得显示我们模型在每一轮的训练集的损失率,训练集/验证集的准确率。
import torch
from torch import nn
import torch.optim as optim
from model.net import simpleconv3
from dataset.dataset import train_dataloader, valid_dataloader, test_dataloader
from torch.utils.tensorboard import SummaryWriter
from sklearn.metrics import accuracy_score
# 评估模型在给定数据加载器上的准确率
def evaluate_model(model, dataloader):
model.eval()
all_labels = []
all_preds = []
device = next(model.parameters()).device # 获取模型所在的设备
with torch.no_grad():
for images, labels in dataloader:
images, labels = images.to(device), labels.to(device) # 将数据移动到 GPU
outputs = model(images)
_, preds = torch.max(outputs, 1)
all_labels.extend(labels.cpu().numpy())
all_preds.extend(preds.cpu().numpy())
accuracy = accuracy_score(all_labels, all_preds)
return accuracy
# 更新 train_model 函数
def train_model(model, train_dataloader, valid_dataloader, num_epochs=5, checkpoint_interval=1, final_model_path="final_model.pth"):
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
writer = SummaryWriter(log_dir='./runs')
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for images, labels in train_dataloader:
images, labels = images.to(device), labels.to(device) # 将数据移动到 GPU
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
avg_loss = running_loss / len(train_dataloader)
print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {avg_loss:.4f}")
# 记录训练损失
writer.add_scalar('Loss/train', avg_loss, epoch)
# 计算验证集准确率
val_accuracy = evaluate_model(model, valid_dataloader)
writer.add_scalar('Accuracy/valid', val_accuracy, epoch)
# 计算训练集准确率
train_accuracy = evaluate_model(model, train_dataloader)
writer.add_scalar('Accuracy/train',train_accuracy, epoch)
# 保存检查点
#if (epoch + 1) % checkpoint_interval == 0:
# save_model(model, f"model_checkpoint_epoch_{epoch + 1}.pth")
save_model(model, final_model_path)
print("Training Finished")
writer.close()
def save_model(model, path):
torch.save(model.state_dict(), path)
print(f"Model saved to {path}")
def load_model(model, path):
model.load_state_dict(torch.load(path))
model.eval()
print(f"Model weights loaded from {path}")
# 示例用法
if __name__ == "__main__":
model = simpleconv3(num_classes=40)
train_model(model, train_dataloader, valid_dataloader, num_epochs=100, checkpoint_interval=1, final_model_path="final_model.pth")
然后我们在终端运行代码,打开可视化界面:
tensorboard --logdir=./runs
结果如下图所示(还未跑完)