import random #导入语句 引入random模块 import torch #导入pytorch库的python命令 import torch.nn as nn #从pytorch库的nn模块中引入一系列构建和训练神经网络的工具 import numpy as np #引入numpy库 import os #用于导入操作系统相关的接口 from PIL import Image #读取图片数据 from torch.utils.data import Dataset, DataLoader #构建和处理数据集的接口 from tqdm import tqdm #实时显示进度 from torchvision import transforms #提供图像变换函数 确保输入数据一致性,增强模型泛化能力 import time #时间相关 暂停程序执行,测量时间间隔,获取当前时间等 import matplotlib.pyplot as plt #绘图
from model_utils.model import initialize_model #这样的代码片段会出现在需要使用预训练模型或特定配置初始化模型的场景中,比如在机器学习或深度学习项目里
def seed_everything(seed): #设置所有相关库的随机种子,以确保实验的可重复性seed(int)随机种子值 torch.manual_seed(seed) #设置ptroch的随机种子 manual手动设置随机种子 torch.cuda.manual_seed(seed) #如果使用GPU还需要设置cuda随机种子 torch.cuda.manual_seed_all(seed) #如果使用多个GPU torch.backends.cudnn.benchmark = False #cudnn NVIDIA开发的深度学习库 专门用于加速卷积神经网络的训练和推理 设置cudnn的禁用基准模式 torch.backends.cudnn.deterministic = True #设置cudnn的确定性模式 random.seed(seed) #设置python内置的随机模块的种子 np.random.seed(seed) #设置numpy的随机种子 os.environ['PYTHONHASHSEED'] = str(seed) #设置操作系统的环境变量 ################################################################# seed_everything(0) #设置所有随机种子为0; ###############################################
HW = 224 #输入图像的高度和宽度均为22像素 train_transform = transforms.Compose( #将多个图像变换操作组合成一个单一的变换管道,以便在训练和测试过程中对图像进行一致的预处理 [ transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224 将pytorch张量或numpy数组转换为PIL(python imaging library image) transforms.RandomResizedCrop(224), #对图像进行随机裁剪并调整大小 transforms.RandomRotation(50), #随机旋转 transforms.ToTensor() #将图像转化为张量 ] ) val_transform = transforms.Compose( [ transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224 transforms.ToTensor() #转换为张量 ] )
class food_Dataset(Dataset): #自定义数据集 def __init__(self, path, mode="train"): #初始化函数,用于设置数据集的路径、转换操作等 根据传入的路径和模式(如 "train"、"val" 或 "test")来加载不同的子集 self.mode = mode #将传入的模式参数赋值给实例的mode属性 if mode == "semi": self.X = self.read_file(path) #读取数据文件 else: self.X, self.Y = self.read_file(path) self.Y = torch.LongTensor(self.Y) #标签转为长整形\ if mode == "train": self.transform = train_transform else: self.transform = val_transform
def read_file(self, path): #从指定路径的文件中读取数据,并根据不同的模式处理标记和未标记的数据
if self.mode == "semi":
file_list = os.listdir(path) #读取指定目录下的所有文件和子目录
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) #用于创建一个 NumPy 数组,该数组将存储从文件列表中读取的所有图像数据。这里假设每个图像都被调整为相同的尺寸
,并且是 RGB 图像(因此最后一个维度是 3)。
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(path, img_name)
img = Image.open(img_path)
img = img.resize((HW, HW))
xi[j, ...] = img
print("读到了%d个数据" % len(xi))
return xi
else:
for i in tqdm(range(11)):
file_dir = path + "/%02d" % i
file_list = os.listdir(file_dir)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
yi = np.zeros(len(file_list), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(file_dir, img_name) #构建图像文件的完整路径
img = Image.open(img_path) #打开路径img_path的图像文件
img = img.resize((HW, HW)) #调整图像大小
xi[j, ...] = img #将处理后的图像数据赋值到numpy xi中的对应位置
yi[j] = i #存储对应标签
if i == 0: #第一次迭代中将xi yi分别赋值个XY 以便在后续迭代中可以将新的数据追加到这些变量中。
X = xi
Y = yi
else: #将新批次的xi yi 追加到XY中
X = np.concatenate((X, xi), axis=0)
Y = np.concatenate((Y, yi), axis=0)
print("读到了%d个数据" % len(Y))
return X, Y
def __getitem__(self, item): #定义如何从数据集中获取单个样本 if self.mode == "semi": return self.transform(self.X[item]), self.X[item] #返回图像 self.x是包含所有图像数据的numpy数组 transform对图像进行处理 else: return self.transform(self.X[item]), self.Y[item] #返回图像和对应标签 def __len__(self): #返回数据集的大小,即样本的数量 return len(self.X)
class semiDataset(Dataset): #创建一个自定义的半监督学习数据集类 def __init__(self, no_label_loder, model, device, thres=0.99): #初始化一个半监督学习的数据集或模型类 x, y = self.get_label(no_label_loder, model, device, thres) #从无标签数据加载器中获取有标签的数据,并将其添加到现有的有标签数据集中 if x == []: #检查从get_label方法返回的新有标签数据是否为空 self.flag = False else: self.flag = True self.X = np.array(x) #转化为一个numpy数组 self.Y = torch.LongTensor(y) #标签转为长整形 self.transform = train_transform def get_label(self, no_label_loder, model, device, thres): model = model.to(device) #将一个深度学习模型移动到指定的计算设备上 pred_prob = [] #初始化一个空列表pred_prob 用来存储模型预测的概率或置信度分数 labels = [] #初始化一个空列表,存储标签 x = [] y = [] soft = nn.Softmax() #创建一个softmax层实例 softmax函数,将原始输入值转换为概率分布 with torch.no_grad(): #pytorch中的一个上下文管理器,用于在推理阶段禁用梯度计算,这可以显著减少内存占用并加速计算 for bat_x, _ in no_label_loder: #假设no_label_loder是一个pytorch的dataloader对象,用于加载未标记的数据,由于这些数据没有标签,因此在迭代过程中用_来忽略标签 bat_x = bat_x.to(device) #将一个批次的数据从cpu移动到指定的计算设备 pred = model(bat_x) #将当前批次的数据传递给模型 pred_soft = soft(pred) #用于将模型的原始输出(通常是未经归一化的logits)通过softmax函数转换为概率分布 pred_max, pred_value = pred_soft.max(1) #从pred_soft中 沿着指定维度(这里是维度1)找到最大值及其对应的索引 pred_prob.extend(pred_max.cpu().numpy().tolist()) #将当前批次中pred_max转换为numpy数组并转换为python列表,然后将其添加到pred_prob列表中 ,这通常用于在推理过程中手机所有样本的预测结果 labels.extend(pred_value.cpu().numpy().tolist()) #将当前批次中每个样本的预测类别索引pred_value转换为 NumPy 数组并进一步转换为 Python 列表,然后将其添加到labels列表中,,这通常用于在推理过程中手机所有样本的预测类别 for index, prob in enumerate(pred_prob): #遍历pred_prob列表中的每一个元素,并同时获取每个元素的索引和值 enumerate可以在迭代过程返回每个元素的索引和对应的值 if prob > thres: #检查是否超过阈值 x.append(no_label_loder.dataset[index][1]) #调用到原始的getitem y.append(labels[index]) #打上伪标签 return x, y def __getitem__(self, item): #用于实现数据集的索引操作 return self.transform(self.X[item]), self.Y[item] #self.transform可选的转换函数或一组转换 def __len__(self): #用于返回数据集大小 return len(self.X)
def get_semi_loader(no_label_loder, model, device, thres): #返回一个新的数据加载器 semiset = semiDataset(no_label_loder, model, device, thres) #创建一个semidataset类的实例,并将这个实例赋值给变量semiset if semiset.flag == False: return None else: semi_loader = DataLoader(semiset, batch_size=16, shuffle=False) #创建一个DataLoader对象,该对象以批次的形式加载由semiset数据集提供的数据 return semi_loader
class myModel(nn.Module): #定义了一个新的类myModel该类继承自 PyTorch 的nn.Module类 def __init__(self, num_class): #定义了一个类的构造函数(即初始化方法)self指向当前实例的引用 num_class是一个用户自定义的参数,表示分类任务中的类别数量 super(myModel, self).__init__() #调用父类(即基类)的构造函数 super() 返回一个代理对象 通过这个代理对象可以调用父类的方法 myModel 是当前类,self 是当前实例 #3 *224 *224 -> 512*7*7 -> 拉直 -》全连接分类 self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 64*224*224 卷积层 输入 输出 卷积核 步长 padding self.bn1 = nn.BatchNorm2d(64) #二维批量归一化层 批量归一化是一种常用的正则化技术,用于加速训练过程并提高模型的稳定性 self.relu = nn.ReLU() #ReLU(Rectified Linear Unit)激活函数层 用于在神经网络中引入非线性特性,从而使得模型能够学习更复杂的模式 self.pool1 = nn.MaxPool2d(2) #64*112*112 定义了一个二维最大池化层 最大池化是一种常用的下采样操作,用于减少特征图的空间尺寸,从而减少计算量和模型参数数量,并在一定程度上增强特征的鲁棒性。 self.layer1 = nn.Sequential( #定义一个顺序容器(Sequential container),它允许你将多个层按顺序组合在一起 nn.Conv2d(64, 128, 3, 1, 1), # 128*112*112 nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2) #128*56*56 ) self.layer2 = nn.Sequential( nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2) #256*28*28 ) self.layer3 = nn.Sequential( nn.Conv2d(256, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(), nn.MaxPool2d(2) #512*14*14 ) self.pool2 = nn.MaxPool2d(2) #512*7*7 self.fc1 = nn.Linear(25088, 1000) #25088->1000 全连接层 将输入特征数为 25088 的数据映射到输出特征数为 1000 的数据。 用于将卷积层提取的特征图展平后传递给分类器。 self.relu2 = nn.ReLU() self.fc2 = nn.Linear(1000, num_class) #1000-11 #全连接层 将输入特征数为1000的数据映射到num_class个输出类别 def forward(self, x): #pytorch中定义神经网络前向传播 x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.pool1(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.pool2(x) x = x.view(x.size()[0], -1) #用于将一个多维张量展平为二维张量 以便传递给全连接层。 x.size()[0]返回张量x的尺寸 [0]获取张量第一个维度的大小 -1表示该维度的大小由pytorch自动推断 x = self.fc1(x) x = self.relu2(x) x = self.fc2(x) return x
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path): model = model.to(device) #将模型移动到指定设备 semi_loader = None #不需要使用半监督学习的数据加载器 plt_train_loss = [] #定义了四个空列表 plt_val_loss = [] plt_train_acc = [] plt_val_acc = [] max_acc = 0.0 for epoch in range(epochs): #循环语句 range生成一个0到1的整数序列 train_loss = 0.0 val_loss = 0.0 train_acc = 0.0 val_acc = 0.0 semi_loss = 0.0 semi_acc = 0.0 start_time = time.time() #为了记录训练和验证过程的总时间,你可以在训练开始前记录当前时间 model.train() #设置模型为训练模式 for batch_x, batch_y in train_loader: 用于遍历训练数据集的每个批次,并将每个批次的数据和标签分别赋值给batch_x, batch_y x, target = batch_x.to(device), batch_y.to(device) #移动到指定的设备 pred = model(x) #神经网络的前向传播 train_bat_loss = loss(pred, target) train_bat_loss.backward() #执行反向传播 optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度 optimizer.zero_grad() train_loss += train_bat_loss.cpu().item() #移动到 CPU,并将其转换为 Python 标量 train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy()) #用于计算当前批次的训练准确率,并将其累加到train_acc plt_train_loss.append(train_loss / train_loader.__len__()) #将当前 epoch 的平均训练损失添加到plt_train_loss plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率, if semi_loader!= None: for batch_x, batch_y in semi_loader: x, target = batch_x.to(device), batch_y.to(device) pred = model(x) semi_bat_loss = loss(pred, target) semi_bat_loss.backward() optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度 optimizer.zero_grad() semi_loss += train_bat_loss.cpu().item() semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy()) print("半监督数据集的训练准确率为", semi_acc/train_loader.dataset.__len__()) model.eval() with torch.no_grad(): #将模型设置为评估(evaluation)模式的命令 for batch_x, batch_y in val_loader: x, target = batch_x.to(device), batch_y.to(device) pred = model(x) val_bat_loss = loss(pred, target) val_loss += val_bat_loss.cpu().item() val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy()) plt_val_loss.append(val_loss / val_loader.dataset.__len__()) plt_val_acc.append(val_acc / val_loader.dataset.__len__()) if epoch%3 == 0 and plt_val_acc[-1] > 0.6: #用于在每第三个 epoch 结束时检查验证集上的准确率是否超过了某个阈值 semi_loader = get_semi_loader(no_label_loader, model, device, thres) #创建一个半监督学习环境下的数据加载器,其中包含了从未标记数据中挑选出来的、由模型预测且满足一定条件的数据样本 if val_acc > max_acc: torch.save(model, save_path) #保存模型 max_acc = val_loss print('[%03d/%03d] %2.2f sec(s) TrainLoss : %.6f | valLoss: %.6f Trainacc : %.6f | valacc: %.6f' % \ (epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1], plt_val_acc[-1]) ) # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。 plt_train_loss[-1]训练集上最新的损失值 plt.plot(plt_train_loss) #绘制训练损失的变化曲线 plt.plot(plt_val_loss) plt.title("loss") #设置图表标题 plt.legend(["train", "val"]) #用于在图表上添加图例的命令 plt.show() #在屏幕上显示你创建的图表 plt.plot(plt_train_acc) plt.plot(plt_val_acc) plt.title("acc") plt.legend(["train", "val"]) plt.show()
# path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled" # train_path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled" # val_path = r"F:\pycharm\beike\classification\food_classification\food-11\validation" train_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\labeled" val_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\validation" no_label_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\unlabeled\00" #表示路径 train_set = food_Dataset(train_path, "train") #实例化一个自定义的数据集类 food_Dataset,并传入两个参数 val_set = food_Dataset(val_path, "val") no_label_set = food_Dataset(no_label_path, "semi") train_loader = DataLoader(train_set, batch_size=16, shuffle=True) #创建一个数据加载器对象 val_loader = DataLoader(val_set, batch_size=16, shuffle=True) no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False) # model = myModel(11) #实例化一个自定义的深度学习模型类 11类别数 model, _ = initialize_model("vgg", 11, use_pretrained=True) #初始化一个预训练的 VGG 模型,并将其分类头调整为适应指定的类别数(在这个例子中是 11 类) use_pretrained=True决定是否使用预训
lr = 0.001 #定义了一个学习率 学习率是优化算法中的一个重要超参数,用于控制模型参数更新的步长大小。 loss = nn.CrossEntropyLoss() #定义了一个损失函数 使用 PyTorch 的nn.CrossEntropyLoss类来创建一个交叉熵损失(Cross-Entropy Loss)对象 交叉熵损失 衡量的是模型预测的概率分布与真实标签之间的差异 optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) #定义了一个优化器 使用 PyTorch 的AdamW优化器来优化模型的参数AdamW
是由 Ilya Loshchilov 和 Frank Hutter 提出的一种优化算法,旨在修正原始 Adam 优化器中权重衰减实现的问题。传统的 Adam 优化器将权重衰减和梯度下降结合在一起,这可能会导致次优的学习行为。而AdamW
将权重衰减从梯度更新步骤中分离出来,使其更接近于标准的 L2 正则化 device = "cuda" if torch.cuda.is_available() else "cpu" #设备 save_path = "model_save/best_model.pth" # 用于保存最佳模型权重的文件路径 epochs = 15 #轮次 thres = 0.99 #阈值 train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path) #用于在一个给定的数据集上训练模型,并在验证集上评估其性能。