李哥考研复试班--食物分类实战

def seed_everything(seed):
    # torch.manual_seed 是 PyTorch 提供的一个函数,用于设置 CPU 生成随机数的种子。这样在使用 PyTorch 进行张量初始化、随机采样等涉及随机数的操作时,只要种子相同,每次生成的随机数序列都是一样的。
    torch.manual_seed(seed)
    # torch.cuda.manual_seed 用于设置当前 GPU 生成随机数的种子。如果代码在 GPU 上运行,这个函数可以确保在当前 GPU 上的随机操作是可重复的。
    torch.cuda.manual_seed(seed)
    # torch.cuda.manual_seed_all 用于设置所有可用 GPU 生成随机数的种子。当使用多个 GPU 进行训练时,这个函数可以保证所有 GPU 上的随机操作都是可重复的。
    torch.cuda.manual_seed_all(seed)
    # torch.backends.cudnn.benchmark 是 PyTorch 中用于 CuDNN(NVIDIA 提供的深度神经网络库)的一个设置选项。
    # 当设置为 True 时,CuDNN 会在每次运行时自动寻找最优的卷积算法,以提高计算效率,但这会引入一些随机性。
    # 将其设置为 False 可以确保每次运行时使用相同的卷积算法,从而保证结果的可重复性。
    torch.backends.cudnn.benchmark = False
    # torch.backends.cudnn.deterministic 也是用于 CuDNN 的设置选项。当设置为 True 时,CuDNN 会使用确定性的算法,
    # 避免一些可能引入随机性的操作,进一步保证实验结果的可重复性。
    torch.backends.cudnn.deterministic = True

    random.seed(seed)
    np.random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0)
###############################################

这段代码的主要目的是设置随机种子,以确保深度学习实验的可重复性。在深度学习中,很多操作都涉及到随机数的生成,例如模型参数的初始化、数据的随机打乱等。如果不固定随机种子,每次运行代码时这些随机操作的结果都会不同,从而导致模型的训练结果和性能也会有所差异。通过设置随机种子,可以保证每次运行代码时,随机数的生成序列都是相同的,进而使得实验结果可以复现。

class food_Dataset(Dataset):
    def __init__(self, path, mode="train"):
        self.mode = mode
        # 如果 mode 为 "semi"(半监督模式),调用 read_file 方法读取数据,并将结果保存到 self.X 中。
        # 否则,调用 read_file 方法读取数据和对应的标签,分别保存到 self.X 和 self.Y 中,并将标签转换为 torch.LongTensor 类型。
        if mode == "semi":
            self.X = self.read_file(path)
        else:
            self.X, self.Y = self.read_file(path)
            self.Y = torch.LongTensor(self.Y)  #标签转为长整形\
        # 如果 mode 为 "train",使用 train_transform 作为图像预处理转换;否则,使用 val_transform 作为图像预处理转换。
        if mode == "train":
            self.transform = train_transform
        else:
            self.transform = val_transform

    def read_file(self, path):
        # 如果 mode 为 "semi",获取指定路径下的所有文件列表,并创建一个形状为 (文件数量, HW, HW, 3) 的零数组 xi,用于存储图像数据。
        if self.mode == "semi":
            file_list = os.listdir(path)
            xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
            # 列出文件夹下所有文件名字
            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:
            # 如果 mode 不是 "semi",遍历 11 个类别文件夹,对于每个类别文件夹,获取该文件夹下的所有文件列表,并创建两个零数组 xi 和 yi,分别用于存储图像数据和标签。
            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 = img.resize((HW, HW))
                    xi[j, ...] = img
                    yi[j] = i

                if i == 0:
                    X = xi
                    Y = yi
                else:
                    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]
        else:
            return self.transform(self.X[item]), self.Y[item]

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

这段代码定义了一个名为 food_Dataset 的自定义数据集类,继承自 torch.utils.data.Dataset。该类的主要作用是读取食品图像数据,并根据不同的模式(训练、验证、半监督)对数据进行处理,方便后续使用 DataLoader 进行批量加载。


class semiDataset(Dataset):
    def __init__(self, no_label_loder, model, device, thres=0.99):
        # __init__ 是类的初始化方法,接受四个参数:
        # no_label_loder:无标签数据的 DataLoader。
        # model:用于预测伪标签的模型。
        # device:模型所在的设备(如 'cuda' 或 'cpu')。
        # thres=0.99:置信度阈值,只有预测概率大于该阈值的样本才会被选中作为伪标签数据。
        x, y = self.get_label(no_label_loder, model, device, thres)
        if x == []:
            self.flag = False

        else:
            self.flag = True
            self.X = np.array(x)
            self.Y = torch.LongTensor(y)
            self.transform = train_transform
    def get_label(self, no_label_loder, model, device, thres):
        # pred_prob:存储每个样本的预测概率最大值。
        # labels:存储每个样本的预测标签。
        # x:存储符合条件的图像数据。
        # y:存储符合条件的伪标签。
        model = model.to(device)
        pred_prob = []
        labels = []
        x = []
        y = []
        soft = nn.Softmax()
        # 使用 with torch.no_grad() 上下文管理器禁用梯度计算,以节省内存和计算资源。
        # 遍历无标签数据加载器 no_label_loder,对每个批次的图像数据进行预测:
        # 将批次数据 bat_x 移动到设备上。
        # 使用模型 model 进行预测,得到原始输出 pred。
        # - 通过 soft(Softmax 层)将输出转换为概率分布 pred_soft。
        # - 找到每个样本的最大概率值 pred_max 和对应的预测标签 pred_value。
        # - 将 pred_max 和 pred_value 转换为 numpy 数组,并添加到 pred_prob 和 labels 列表中。
        with torch.no_grad():
            for bat_x, _ in no_label_loder:
                bat_x = bat_x.to(device)
                pred = model(bat_x)
                pred_soft = soft(pred)
                pred_max, pred_value = pred_soft.max(1)
                pred_prob.extend(pred_max.cpu().numpy().tolist())
                labels.extend(pred_value.cpu().numpy().tolist())
        # 遍历 pred_prob 列表中的每个概率值:
        # - 如果概率值 prob 大于阈值 thres,则将对应的图像数据和伪标签添加到 x 和 y 列表中。
        # no_label_loder.dataset[index][1] 用于获取原始无标签数据集中第 index 个样本的图像数据(假设原始数据集的 __getitem__ 返回 (图像, 无标签),这里通过索引 1 获取图像数据)。
        for index, prob in enumerate(pred_prob):
            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]
    def __len__(self):
        return len(self.X)

def get_semi_loader(no_label_loder, model, device, thres):
    # no_label_loder:无标签数据的 DataLoader 对象,用于加载无标签的图像数据。
    # model:用于预测无标签数据伪标签的模型。
    # device:模型运行的设备,例如 'cuda' 表示使用 GPU,'cpu' 表示使用 CPU。
    # thres:置信度阈值,用于筛选出模型预测置信度较高的样本作为伪标签数据。
    semiset = semiDataset(no_label_loder, model, device, thres)
    # semiset.flag 是 semiDataset 类中的一个标志属性。如果 flag 为 False,说明在生成伪标签数据的过程中,没有找到预测概率大于阈值的样本,即没有可用的伪标签数据。此时,函数直接返回 None。
    if semiset.flag == False:
        return None
    # 如果 semiset.flag 为 True,说明有可用的伪标签数据。使用 torch.utils.data.DataLoader 创建一个半监督数据加载器 semi_loader。
    # semiset:传入前面创建的半监督数据集对象。
    # batch_size=16:设置每个批次加载的数据样本数量为 16。
    # shuffle=False:设置不打乱数据的顺序。在半监督学习中,通常不需要打乱伪标签数据的顺序。
    # 最后,函数返回创建好的半监督数据加载器 semi_loader,以便后续在训练过程中使用。
    else:
        semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
        return semi_loader

class semiDataset 的作用是:
生成伪标签:利用已有的模型对无标签数据进行预测,筛选出预测置信度较高的样本作为伪标签数据。
数据加载:将伪标签数据包装成 Dataset 格式,方便与 DataLoader 配合使用,用于半监督训练。
数据增强:对伪标签数据应用与训练集相同的数据增强操作(如随机裁剪、旋转),以提高模型的泛化能力
该类通过动态生成伪标签,将无标签数据转化为有监督学习的训练数据,从而提升模型在半监督学习任务中的性能。

class myModel(nn.Module):
    def __init__(self, num_class):
        super(myModel, self).__init__()
        #3 *224 *224  -> 512*7*7 -> 拉直 -》全连接分类
        self.conv1 = nn.Conv2d(3, 64, 3, 1, 1)    # 64*224*224
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU()
        self.pool1 = nn.MaxPool2d(2)   #64*112*112


        self.layer1 = nn.Sequential(
            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
        self.relu2 = nn.ReLU()
        self.fc2 = nn.Linear(1000, num_class)  #1000-11

    def forward(self, x):
        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 = self.fc1(x)
        x = self.relu2(x)
        x = self.fc2(x)
        return x

myModel 是一个简单的卷积神经网络模型,通过卷积层提取图像特征,再通过全连接层进行分类。它的输入是 3x224x224 的 RGB 图像,输出是 num_class 个类别的预测分类类别。


def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path):
    # model:待训练的深度学习模型。
    # train_loader:训练集的数据加载器,用于批量加载有标签的训练数据。
    # val_loader:验证集的数据加载器,用于批量加载验证数据。
    # no_label_loader:无标签数据的数据加载器,用于半监督学习。
    # device:模型运行的设备,如 'cuda' 或 'cpu'。
    # 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):
        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:
            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()
            train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
        plt_train_loss.append(train_loss / train_loader.__len__())
        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():
            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:
            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.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()


HW = 224
# transforms.ToPILImage():将输入数据转换为 PIL(Python Imaging Library)图像对象。在深度学习中,有时候数据可能是以其他格式(如 numpy 数组)存在的,这个操作可以将其转换为 PIL 图像,方便后续的图像处理操作。
# transforms.RandomResizedCrop(224):这是一个数据增强操作。它会随机裁剪输入图像,并将裁剪后的图像调整为 224x224 的大小。随机裁剪的区域和比例是随机选择的,这样可以增加训练数据的多样性,使模型能够学习到不同位置和尺度的特征,从而提高模型的泛化能力。
# transforms.RandomRotation(50):也是一个数据增强操作。它会随机将图像旋转一定的角度,旋转角度的范围是 [-50, 50] 度。通过随机旋转图像,可以让模型学习到不同角度下的图像特征,进一步增强模型对图像旋转的鲁棒性。
# transforms.ToTensor():将 PIL 图像或 numpy 数组转换为 torch.Tensor 类型,同时将图像的像素值归一化到 [0, 1] 范围内。
train_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.RandomResizedCrop(224),
        transforms.RandomRotation(50),
        transforms.ToTensor()
    ]
)
# transforms.ToPILImage():同样是将输入数据转换为 PIL 图像对象。
# transforms.ToTensor():将 PIL 图像或 numpy 数组转换为 torch.Tensor 类型,并将像素值归一化到 [0, 1] 范围内。
val_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.ToTensor()
    ]
)
# 数据增强方面:train_transform 包含了 RandomResizedCrop 和 RandomRotation 两个数据增强操作,而 val_transform 没有这些操作。数据增强主要用于训练阶段,通过对训练数据进行随机变换,可以增加训练数据的多样性,使模型学习到更丰富的特征,从而提高模型的泛化能力。
# 稳定性需求方面:验证集(或测试集)的目的是评估模型的真实性能,因此不需要对数据进行随机变换,以保证评估结果的稳定性和准确性。如果在验证集上使用数据增强,每次评估时输入的数据都不同,会导致评估结果不稳定,无法准确反映模型的真实性能。

train_path = r"E:\深度学习\food_classification\food-11\training\labeled"
val_path = r"E:\深度学习\food_classification\food-11\validation"
no_label_path = r"E:\深度学习\food_classification\food-11\training\unlabeled\00"

train_set = food_Dataset(train_path, "train")
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, _ = initialize_model("vgg", 11, use_pretrained=True)

lr = 0.001
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值