李的深度学习四:图片分类

步骤:

1.卷积

        这个操作我简单理解为一片固定大小的区域,在一张图片数据上一点一点滑动,每次滑动都会把这片被区域圈住的数据作为一个新的小块数据,整个图片数据被扫过一遍以后,得到新的图片数据。卷积得到的图片数据叫特征图

        卷积核是一个多维矩阵(通常是2D或3D),其元素是‌可学习的权重参数‌。在图像处理中,常见的卷积核尺寸为3×3、5×5等。通过在输入数据上滑动(或扫描),卷积核与局部区域进行‌点乘运算‌,生成对应的输出特征图(Feature Map),从而提取特定模式(如边缘、纹理等)。

         上图的7怎么得到的?在7这个值,是卷积核提取的第一个区域内一一对应的元素逐个相乘得到的结果。

        为了多次卷积,提取更稳定的特征信息,需要填充(padding)0值在特征图周围,一般是能让新的特征图和卷积前特征图尺寸一样的份量。换句话说能进行无数次卷积特征图所需要的填充。

        如下图,5*5的特征图,3*3的卷积核,如果不添加填充,就是无填充卷积(vaild padding),得到一个3*3的新特征图。

        如果添加填充,而且得到的新特征图尺寸和原来的特征图尺寸一样,就是等尺寸填充(same padding),得到的新特征图的尺寸是5*5。

        减少计算量,还会用到步长(stride),卷积核不会只移动一个像素距离,而是根据设定的步长一次移动多个像素的距离。

        padding和stride一起调整网络的深度,可以根据需要自由调整。

2.池化

        这一步可以简化卷积后得到的特征图的特征信息。

3.全连接

         展平池化后的特征图变成一维向量。输入到全连接层中,对一个7*7*1024的特征图,展平以后得到的就是50176这么长的向量

        进行y=w*x+b这种形式的运算。权重矩阵w和偏置项b对展平后的特征进行线性组合,x为展平后的输入向量,y为输出向量‌。

        作用:将高维特征映射到低维空间,提取全局语义信息‌,消除特征的空间位置依赖性,增强模型对输入平移、旋转的鲁棒性‌。

        输出向量到分类层。

4.分类

        用专门的softmax()这个函数对输入的向量进行概率转换,例如对猫狗分类任务,输出一个2维向量 [3.2,−1.5][3.2,−1.5],分别表示“猫”和“狗”的原始得分。

        softmax概率转换‌:

               

        最终输出:模型认为输入图像有98%的概率是“猫”。

对李哥项目的理解:

        一个对食物的图片进行分类的项目。

        有11类食物。
        其中带标签的数据:280 *11 不带标签的训练数据:6786 验证集30*11
        测试集3347。

        李哥说在模型训练中,有的时候得到一个不错的模型,需要保存这个随机种子。

def seed_everything(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True
    random.seed(seed)
    np.random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0)
###############################################

 处理图片数据

  李哥创建一个food_Dataset的类,继承Dataset包。

class food_Dataset(Dataset):

  在这个类中,李哥先定义了一个读取文件的函数read_file,

    def read_file(self, path):

这个函数中有一个分支功能如下

            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)

1.遍历类别目录‌

i = 5
path = "/%02d" % i  # 输出 "/05"

循环处理11个类别(i从0到10),每个类别对应一个以两位数字命名的子目录(如/00、/01)。
‌2.加载图片数据‌
    读取每个子目录下的所有图片文件。
    Image.open加载图片,并统一缩放到指定尺寸(HW x HW)。
    将图片转换为NumPy数组,存储为uint8类型(RGB通道的数据都是整数,所以不能是浮点类型)。
‌3.构建标签数据‌
    为每个图片分配类别标签,标签值为当前子目录的编号i(0到10)。
‌4.合并数据集‌
    将每个类别的图片数据(xi)和标签(yi)纵向堆叠,最终合并为完整的训练集X和标签Y。

        首次迭代(i=0)‌:直接初始化X = xi和Y = yi。
        ‌后续迭代(i≥1)‌:通过np.concatenate将新的xiyi沿‌样本轴(axis=0)‌合并到X和Y中。
‌        合并顺序‌:严格保持类别顺序

5.‌输出结构‌
    X的维度为(总样本数, HW, HW, 3),包含所有图片的像素数据。
    Y的维度为(总样本数),包含所有图片对应的类别标签。

        上图只是一个部分,最终大小会随着循环从280到560,最后到280*11。

图片和标签各自存放在一个数组里,位置顺序一一对应。

函数功能分支的另一个走向:

        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

1. ‌模式检查‌
    仅在 self.mode 为 "semi"(半监督模式)时执行读取操作。
2. ‌读取文件列表‌
    使用 os.listdir(path) 获取指定路径下的所有文件名,生成 file_list。
3. ‌初始化存储数组‌
    创建形状为 (len(file_list), HW, HW, 3) 的 numpy 数组 xi,用于存储缩放后的图像数据。

HW = 224

长宽为224*224,深度为3

4. ‌遍历处理每张图像‌
    ‌通过 Image.open(img_path) 加载图像。
    ‌使用 img.resize((HW, HW)) 将图像缩放至固定尺寸 HW x HW。
    ‌将缩放后的图像直接存入 xi 数组。
5. ‌返回结果‌
    返回包含所有图像的数组 xi,并打印读取数量。

图片增广

        为了让一张图片的不同角度,颜色,大小等情况都可以识别出来,要将所有这些情况的图片都收集起来。需要使用transforms包。

        下面李哥首先编写一个数据变换的操作,对训练集的图片进行的变换。

train_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224 将图片转化为模型能够接受的格式
        transforms.RandomResizedCrop(224),#将图片随机放大裁切以后的样子
        transforms.RandomRotation(50),#图片在50度的范围以内随机旋转
        transforms.ToTensor()#最后将图片转化为张量
    ]
)

        变换好的数据可以使用了,在food_Dataset类的初始化函数__init__,取函数__getitem__,长度函数__len__里就可以使用转化好的数据。

    def __init__(self, path, mode="train"):
        self.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 __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-11_sample这个路径的图片,以学习交流为目的。实际上会使用的路径应该是food-11这个路径的图片,但是这样读取速度对大部分学生的设备来说会非常慢,所以选择food-11_sample

# path = r"D:\P_projects\4\food_classification\food-11\training\labeled"
# train_path = r"D:\P_projects\4\food_classification\food-11\training\labeled"
# val_path = r"D:\P_projects\4\food_classification\food-11\validation"
train_path = r"D:\P_projects\4\food_classification\food-11_sample\training\labeled"
val_path = r"D:\P_projects\4\food_classification\food-11_sample\validation"

train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")

        原本的图片集已经按照一类分在一起了,需要打乱,为了加快读取速度,以步长16抽取图片,然后打乱。

train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)

        之后李哥同理对验证集也进行处理,如上方代码展示。

        李哥说在验证的时候,不用进行乱七八糟的变换,因为验证的图片相当于随机的形状,所以验证集的图片数据直接转为张量使用。

val_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224 将图片转化为模型能够接受的格式
        transforms.ToTensor()    #转为张量
    ]
)

        所以在初始化函数__init__中根据模式选择不一样的分支操作——训练模式和验证模式的操作

if mode == "train":
            self.transform = train_transform
        else:
            self.transform = val_transform

 模型

        一个模型类myModel,继承了nn这个包

class myModel(nn.Module):

        随后李哥定义一个初始化函数__init__准备用于全连接分类,这里包含了卷积池化操作。

    def __init__(self, num_class):

    初始卷积块‌:
        conv1:3通道输入,64通道输出,3x3卷积核(stride=1,padding=1),保持特征图尺寸不变。
        bn1:对64通道进行批归一化,加速训练并稳定学习过程。
        ReLU:引入非线性激活。
        pool1:2x2最大池化,特征图尺寸减半。

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

    ‌中间卷积块(layer1, layer2, layer3)‌:
        每层通过nn.Sequential堆叠卷积、批归一化、ReLU和池化。
        ‌layer1‌:64→128通道,3x3卷积,池化后尺寸再减半。
        ‌layer2‌:128→256通道,结构同前(需注意代码中可能缺少闭合括号的语法错误)。
        ‌layer3‌:256→512通道,同上。

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
        )

    ‌额外池化层(pool2)‌:
        对layer3的输出进行2x2最大池化,进一步压缩特征图。

self.pool2 = nn.MaxPool2d(2)    #512*7*7

    ‌全连接层‌:

        fc1:将展平后的25088维特征映射到1000维,接ReLU激活。
        fc2:1000维→num_class输出,得到分类结果。

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)    # 展平为 [B, 512*7*7=25088]
        x = self.fc1(x)
        x = self.relu2(x)
        x = self.fc2(x)
        return x

输入各项参数

       学习率

        控制优化器在参数更新时的步长大小。值过大会导致模型不稳定或无法收敛;值过小会降低训练速度。0.001 是 Adam 系列优化器的常用初始值,适合大多数任务。

lr = 0.001

       损失函数

        结合Softmax和交叉熵,将模型输出的logits转换为概率分布后计算损失。

loss = nn.CrossEntropyLoss()

       权重衰减

        AdamW是Adam的改进版本,解耦权重衰减和梯度更新,避免传统 Adam 中权重衰减与动量冲突的问题。lr与全局学习率一致。weight_decay=1e-4正则化系数,通过惩罚大权重防止过拟合,提升泛化能力。

optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)

       自动选择训练设备

        GPU加速矩阵运算(如卷积),显著提升训练速度;CPU作为备用设备确保兼容性。

device = "cuda" if torch.cuda.is_available() else "cpu"

       保存最佳模型权重

        通常结合早停法(Early Stopping)或定期保存,避免训练中断或过拟合。

save_path = "model_save/best_model.pth"

        训练的总轮数

        15 轮适合中小型数据集或简单模型。

epochs = 15

导入验证集函数

        从上一个项目复制过来的函数

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):
        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()

直接调用这个函数

train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)

train_val函数中,额外记录模型的准确率

    plt_train_acc = []
    plt_val_acc = []

准确率超过了0就可以记录下来

    max_acc = 0.0

在循环中加入的这些准确率结果

train_acc = 0.0
val_acc = 0.0
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())

分离并转换预测张量‌:
    pred.detach():从计算图分离,避免梯度计算。
    cpu():将数据移至CPU(若原在GPU)。
    numpy():转换为NumPy数组。
    np.argmax(axis=1):获取每个样本的预测类别(最大概率对应的索引)。
‌处理真实标签‌:
    target.cpu().numpy():同样移至CPU并转为NumPy数组。
‌计算正确预测数‌:
    比较预测类别与真实标签,生成布尔数组。
    np.sum():统计True的数量(正确样本数)。
‌累加准确率‌:
    将当前批次的正确数累加到train_acc。 

为李哥鼓掌

plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,

预测正确的次数/预测的总次数=准确率

同理验证集

val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())

同理记录准确率

plt_val_acc.append(val_acc / val_loader.dataset.__len__())

如果准确率超过以往准确率就会保存模型

        if val_acc > max_acc:
            torch.save(model, save_path)
            max_acc = val_loss

要画训练集合验证集两个图像

    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()

打印结果

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的浮点数, 后面可以对应。

使用大佬的模型

# model = myModel(11)
model, _ = initialize_model("vgg", 11, use_pretrained=True)

用他们模型的名字就可以用他们的模型了。

有很多优秀的模型,第一个MyModel是李哥的模型。

def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
    # 初始化将在此if语句中设置的这些变量。
    # 每个变量都是模型特定的。
    model_ft = None
    input_size = 0
    if model_name =="MyModel":
        if use_pretrained == True:
            model_ft = torch.load('model_save/MyModel')
        else:
            model_ft = MyModel(num_classes)
        input_size = 224

    elif model_name == "resnet18":
        """ Resnet18
        """
        model_ft = models.resnet18(pretrained=use_pretrained)            # 从网络下载模型  pretrain true 使用参数和架构, false 仅使用架构。
        set_parameter_requires_grad(model_ft, linear_prob)            # 是否为线性探测,线性探测: 固定特征提取器不训练。
        num_ftrs = model_ft.fc.in_features  #分类头的输入维度
        model_ft.fc = nn.Linear(num_ftrs, num_classes)            # 删掉原来分类头, 更改最后一层为想要的分类数的分类头。
        input_size = 224
        
    elif model_name == "resnet50":
        """ Resnet50
        """
        model_ft = models.resnet50(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs, num_classes)
        input_size = 224
        
    elif model_name == "googlenet":
        """ googlenet
        """
        model_ft = models.googlenet(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs, num_classes)
        input_size = 224


    elif model_name == "alexnet":
        """ Alexnet
 """
        model_ft = models.alexnet(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "vgg":
        """ VGG11_bn
 """
        model_ft = models.vgg11_bn(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "squeezenet":
        """ Squeezenet
 """
        model_ft = models.squeezenet1_0(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
        model_ft.num_classes = num_classes
        input_size = 224

    elif model_name == "densenet":
        """ Densenet
 """
        model_ft = models.densenet121(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        num_ftrs = model_ft.classifier.in_features
        model_ft.classifier = nn.Linear(num_ftrs, num_classes)
        input_size = 224

    elif model_name == "inception":
        """ Inception v3
 Be careful, expects (299,299) sized images and has auxiliary output
 """
        model_ft = models.inception_v3(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, linear_prob)
        # 处理辅助网络
        num_ftrs = model_ft.AuxLogits.fc.in_features
        model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
        # 处理主要网络
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs,num_classes)
        input_size = 299

    else:
        print("Invalid model_utils name, exiting...")
        exit()

    return model_ft, input_size

李哥说在迁移学习中有线性探测微调方式

线性探测(Linear Probing)‌
仅冻结预训练模型的‌特征提取器‌(如Transformer的编码层),仅训练顶层的‌线性分类头‌(全连接层)。
‌流程‌:
    固定预训练模型的参数;
    仅用新数据集训练分类头,不更新特征提取层;
    适用于特征提取能力较强且新任务数据量较小的场景。

微调(Fine-Tuning)‌
解冻预训练模型的‌部分或全部参数‌,与分类头联合优化,使模型更适应新任务‌14。
‌流程‌:
    初始化预训练模型的权重;
    根据任务需求选择冻结/解冻的层(如仅解冻最后几层);
    使用新数据对模型进行端到端训练。

半监督学习

        半监督学习的特点是少量带标签数据大量无标签数据,目的是为了提升模型的性能。

        多数半监督方法需要‌迭代优化‌
    ‌自训练(Self-training)‌:先用有标签数据训练初始模型,预测无标签数据,选取高置信度预测作为伪标签,重新训练模型。
    ‌协同训练(Co-training)‌:多个模型从不同视角对无标签数据预测,互相提供伪标签。

        其核心价值在于‌以低成本利用无标签数据中的隐含规律‌,突破纯监督学习的数据瓶颈。

大模型的补充:

实现

        首先在food_Dataset这个类的初始函数__init__中设置一个半监督的分支,如果mode="semi"就会进行半监督的数据读取。可以看到和否定分支相比少了Y,是无标签的数据。

    def __init__(self, path, mode="train"):
        self.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)  #标签转为长整形\

设置文件路径

no_label_path = r"D:\P_projects\4\food_classification\food-11_sample\training\unlabeled\00"

no_label_set = food_Dataset(no_label_path, "semi")

        对于半监督的无标签数据,它们的训练用途是在已经被有标签数据训练好的模型上进行验证,所以数据处理的方式和验证集一样,所以在__init__函数里的一个否分支执行的是验证集的数据处理方式。

        if mode == "train":
            self.transform = train_transform
        else:
            self.transform = val_transform    #半监督的验证集数据处理方式
val_transform = transforms.Compose(
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.ToTensor()
    ]
)

        在__getitem__函数中半监督模式中直接返回transform处理的数据和原始的数据。这里transform的数据就像验证集里的数据一样,从模型出来以后得到预测值。

    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]

        原始数据存放在无标签集。 对无标签数据集的图片进行预测,不打乱顺序,因为需要和原始数据的位置一一对应。

no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)

一个半监督的类

class semiDataset(Dataset):
初始化函数
    def __init__(self, no_label_loder, model, device, thres=0.99):

    no_label_loader: 未标注数据的加载器(DataLoader)
    model: 已预训练(或部分训练)的分类模型,用于生成伪标签
    device: 计算设备(CPU/GPU)
    thres=0.99: 置信度阈值,仅保留模型预测置信度高于该值的样本

x, y = self.get_label(no_label_loder, model, device, thres)

        遍历no_label_loader中的未标注图像,用model预测每张图像的类别概率分布,保留‌最高预测概率超过阈值thres‌的样本,将对应的图像数据存入x,伪标签(预测类别)存入y。

if x == []:
    self.flag = False  # 无有效伪标签数据
else:
    self.flag = True   # 存在有效伪标签数据
    self.X = np.array(x)               # 图像数据转NumPy数组
    self.Y = torch.LongTensor(y)        # 伪标签转长整型张量
    self.transform = train_transform    # 应用数据增强(如裁剪、翻转)

        为什么要使用训练集的数据处理方式?因为半监督学习得到的数据最终也是用于训练模型的,所以和训练集的数据采取相同的处理。 

get_label函数
    def get_label(self, no_label_loder, model, device, thres):
        model = model.to(device)
        pred_prob = []    #预测值的一个列表
        labels = []    #标签的一个列表
        x = []
        y = []         #最终的x和y
        soft = nn.Softmax()    #定义softmax函数
        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())#同理

        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

直观感受如下图

大模型补充

 

这里也就印证了之前取数据的时候为什么不对数据进行打乱,所以shuffle的值应为False 。

随后

    def __getitem__(self, item):
        return self.transform(self.X[item]), self.Y[item]
    def __len__(self):
        return len(self.X)
semi_data_loader函数

        半监督学习的核心是利用模型对无标签数据的预测结果生成伪标签‌,而非直接使用固定标签数据。

训练集/验证集‌:数据标签是预先标注好的,直接读取即可。
‌半监督数据集‌:标签是模型动态预测的(伪标签),需实时生成并筛选。

def get_semi_loader(no_label_loder, model, device, thres):
    semiset = semiDataset(no_label_loder, model, device, thres)
    if semiset.flag == False:
        return None
    else:
        semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
        return semi_loader

        在内部调用get_label生成伪标签,并根据阈值thres筛选高置信度样本。每次调用get_semi_loader,模型可能已更新,生成的伪标签会变化,因此需要重新构建数据集。

避免内存爆炸‌

        直接读取无标签数据并一次性生成伪标签会导致内存占用过高,尤其当数据量极大时。通过 semiDataset的延迟加载(Lazy Loading):
按需生成‌:仅在训练时按批次生成伪标签,减少内存占用。
复用原始数据加载器‌:无需重复存储数据,直接复用no_label_loader的数据流。

        若semiset中没有满足条件的样本,返回None以避免无效训练。

训练模型

        在train_val函数中,有初始化loss和acc

semi_loss = 0.0
semi_acc = 0.0

        如果semi_loader里有东西就拿来训练模型

        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)    #计算loss
                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__())

如何让semi_loader里有东西?

# 条件触发:每隔3个epoch,且最新验证集准确率超过60%
if epoch % 3 == 0 and plt_val_acc[-1] > 0.6:
    # 调用函数生成半监督数据加载器
    semi_loader = get_semi_loader(
        no_label_loader,  # 无标签数据源
        model,            # 当前模型(用于生成伪标签)
        device,           # 计算设备(如GPU)
        thres             # 置信度阈值(过滤低置信度伪标签)
    )

总结

        虽然不能完全理解,但还是写了这些下来,想要加深理解仍然需要自己再照着写一个分类项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值