数据集

训练集有带标签的数据和不带标签的数据,一共11钟食物,每一种280张图片

验证集每一种30张,无标签

训练集3327张

数据预处理——从文件夹里读出图片

1.训练集有标签数据,遍历11个类别文件夹,得到各280张图片的完整路径

2.用image的函数将目标路径的图片转换为rgb图片形式

3.创建两个numpy数组,将每一个224*224*3的图片以及其标签分别存入数组,一共11个280张图片数组竖向拼接成一个大的数组

# 读取文件的图片
def read_file(path):
    for i in tqdm(range(11)):  # 遍历一共11个文件夹,tqdm就是运行的时候能用进度条显示读到哪里了
        file_dir = path + "/%02d" % i  # 每个类别文件夹的路径名
        file_list = os.listdir(file_dir)  # 读每个类别的每张图片名,这里每个文件280张,os.listdir就是取每个图片成一个列表
        xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)  # NumPy 函数,用于创建一个指定大小和形状的数组,并将所有元素初始化为 0,存取所有图片
        yi = np.zeros((len(file_list)), dtype=np.uint8)  # 数组,用于存每个图片的标签0-10
        for j, each in enumerate(file_list):  # 遍历当前类别的每张图片
            img_path = file_dir + "/" + each  # 每个类别的每张图片的完整路径
            img = Image.open(img_path)  # Image.open(img_path) 时,它会读取指定路径的图像文件,并将其转换为一个rgb图像对象
            img = img.resize((HW, HW))  # 更改图片尺寸为224*224
            xi[j, ...] = img  # 将图片存入数组
            yi[j] = i  # 将图片的标签存入数组
        # XY记录当前文件夹的数组,后面的每一轮都在此基础叠加。
        # axis=0表示在第一个轴,即竖着拼接,比如第一轮的280张图片下面继续拼接第二轮的280张,即560张224*224*3的图片
        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


# 数据集
class fooddataset(Dataset):
    def __init__(self, path):
        super(fooddataset, self).__init__()
        self.X, self.Y = read_file(path)
        self.Y = torch.LongTensor(self.Y)
        self.transformer = train_transformer

    def __getitem__(self, item):
        return self.transformer(self.X[item]), self.Y[item]     # 取一批数据时进行增广

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

数据增广

我们给出具体尺寸的数据集,但是一旦图片缩小,或者翻转,计算机就不认识了,所以要将训练集的原始图片进行数据增广(随机旋转,放缩),让计算机提前见过各种图片。

当使用 DataLoader 来迭代数据集时,DataLoader 会在内部自动调用 __getitem__ 方法来获取批次中的数据,数据增广就是在取一批训练集数据时进行的

# 训练集的数据增广。
train_transform = transforms.Compose([
    transforms.ToPILImage(),    # 转换PILImage格式
    transforms.RandomResizedCrop(224),  # 图片随机放大,再取其224的切片
    transforms.RandomRotation(50),  # 图片随机旋转
    transforms.ToTensor()   # 转为张量
])

模型

定义一个自己的模型(比较垃圾,最好迁移学习)

先用卷积层和池化层完成特征图尺寸的缩小:目标 3 * 224 * 224  ->  512 * 7 * 7

再拉直后为25088个参数,进入全连接层,最后得到11维向量

其中,前面的四个整体都可以封装成一个layer对象,一共4个layer,每一个包括一个卷积层Conv2d,再进行批量归一化BatchNorm2d,再经过relu激活函数,最后是一个最大池化层MaxPool2d

# 模型
class myModel(nn.Module):
    def __init__(self, num_class):
        super(myModel, self).__init__()
        # 目标 3 * 224 * 224 -> 512 * 7 * 7

        #  封装成layer对象
        self.layer1 = nn.Sequential(
            nn.Conv2d(3,64,3,1,1),  # 卷积(通道数,卷积核数量,尺寸,stride,padding)
            nn.BatchNorm2d(64),     # 批量归一化
            nn.ReLU(),
            nn.MaxPool2d(2)     # 最大卷积池
        )
        #  输出 64*112*112

        # self.conv2= nn.Conv2d(64,128,3,1,1)
        # self.bn2 = nn.BatchNorm2d(128)
        # self.relu2 = nn.ReLU()
        # self.pool2 = nn.MaxPool2d(2)
        self.layer2 = nn.Sequential(
            nn.Conv2d(64,128,3,1,1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 128*56*56

        self.layer3 = nn.Sequential(
            nn.Conv2d(128,256,3,1,1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )

        # 256*28*28
        self.layer4 = nn.Sequential(
            nn.Conv2d(256,512,3,1,1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 512*14*14
        self.pool1 = nn.MaxPool2d(2)
        # 512*7*7 - 拉直成25088个参数,通过全连接层,num_class = 11
        self.fc1 = nn.Linear(25088, 1000)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(1000, num_class)

    # 前向传播
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.pool1(x)
        x = x.view(x.size()[0], -1)     # view将特征图拉直
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

参数 

使用AdamW优化器, AdamW 是一种自适应学习率优化算法,它是 Adam 优化器的一个变种

  • SGD: 通常使用固定的或者逐步减少的学习率。在SGD中,所有参数通常使用相同的学习率进行更新。
  • Adam: 具有自适应学习率,这意味着每个参数都有自己的学习率,这些学习率会根据参数更新历史动态调整。如果梯度很缓,学习率就大一些,如果梯度很陡,学习率就小一些。而AdamW的w是weight_decay=0.0001: 这是权重衰减的参数,用于正则化(w就是正则项),防止模型过拟合
lr = 0.001
loss = nn.CrossEntropyLoss()    # 传入模型输出和标签,计算交叉熵损失
epochs = 20
# AdamW 是一种自适应学习率优化算法,它是 Adam 优化器的一个变种,weight_decay=0.0001: 这是权重衰减的参数,用于正则化,防止模型过拟合。
optimizer = torch.optim.AdamW(mymodel.parameters(), lr=lr, weight_decay=1e-4)             # AdamW
device = 'cuda' if torch.cuda.is_available() else 'cpu'
save_ = 'model_save/mymodel.pth'

训练模型

训练的过程跟回归任务几乎一模一样,就加了个准确率

# 训练模型
def train_val(model, trainloader, valloader,optimizer, loss, epoch, device, save_):
    # trainloader = DataLoader(trainset,batch_size=batch,shuffle=True)
    # valloader = DataLoader(valset,batch_size=batch,shuffle=True)
    model = model.to(device)                # 模型和数据 ,要在一个设备上。  cpu - gpu
    plt_train_loss = []
    plt_val_loss = []
    plt_train_acc = []
    plt_val_acc = []


    max_val_acc = 0.0               # 记录最大准确度

    for i in range(epoch):                 # 训练epoch 轮
        train_acc = 0.0
        val_acc = 0.0
        start_time = time.time()             # 记录开始时间
        model.train()                         # 模型设置为训练状态      结构
        train_loss = 0.0
        val_loss = 0.0
        for data in trainloader:                     # 从训练集取一个batch的数据
            optimizer.zero_grad()                   # 梯度清0
            x, target = data[0].to(device), data[1].to(device)       # 将数据放到设备上
            pred = model(x)                          # 用模型预测数据
            bat_loss = loss(pred, target)       # 计算loss
            bat_loss.backward()                        # 梯度回传, 反向传播。
            optimizer.step()                            # 用优化器更新模型
            train_loss += bat_loss.detach().cpu().item()             # 记录loss和
            # argmax可以得到最大值所在的下标,即预测类别,再判断是否等于标签,最终得到一轮数据准确的个数
            train_acc += np.sum(np.argmax(pred.cpu().data.numpy(), axis=1) == target.cpu().numpy())

        plt_train_loss.append(train_loss/trainloader.dataset.__len__())   # 记录loss到列表。注意是平均的loss ,因此要除以数据集长度。
        plt_train_acc.append(train_acc / trainloader.dataset.__len__())  # 预测对的个数 / 训练集的长度 = 平均的准确率

        model.eval()                 # 模型设置为验证状态
        with torch.no_grad():                    # 模型不再计算梯度
            for data in valloader:                      # 从验证集取一个batch的数据
                val_x , val_target = data[0].to(device), data[1].to(device)          # 将数据放到设备上
                val_pred = model(val_x)                 # 用模型预测数据
                val_bat_loss = loss(val_pred, val_target)          # 计算loss
                val_loss += val_bat_loss.detach().cpu().item()                  # 计算loss
                val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_target.cpu().numpy())

        plt_val_loss.append(val_loss / valloader.dataset.__len__())  # 记录loss到列表。注意是平均的loss ,因此要除以数据集长度。
        plt_val_acc.append(val_acc / trainloader.dataset.__len__())  # 预测对的个数 / 训练集的长度 = 平均的准确率

        if val_acc < max_val_acc:
            torch.save(model, save_path)               # 如果acc比之前的最小值小, 说明模型更优, 保存这个模型
            max_val_acc = val_acc

        # print("val_acc:",val_acc)

        print('[%03d/%03d] %2.2f sec(s) TrainLoss : %.6f Trainacc : %.6f | valLoss: %.6f valacc: %.6f' % \
              (i, epoch, time.time()-start_time, plt_train_loss[-1], plt_train_acc[-1], plt_val_loss[-1], plt_val_acc[-1])
              )              #打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。

        # print('[%03d/%03d] %2.2f sec(s) TrainLoss : %3.6f | valLoss: %.6f' % \
        #       (i, epoch, time.time()-start_time, 2210.2255411, plt_val_loss[-1])
        #       )              #打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。
    plt.plot(plt_train_acc)              # 画图, 向图中放入训练loss数据
    plt.plot(plt_val_acc)                # 画图, 向图中放入训练loss数据
    plt.title('acc')                      # 画图, 标题
    plt.legend(['train', 'val'])             # 画图, 图例
    plt.show()                                 # 画图, 展示

 迁移学习

数据量很小时,自己的数据集不足以训练好的模型,可以直接使用迁移学习用大佬的模型。卷积神经网络分为特征提取器和分类头,由于不同数据集的类别不同,分类头最后全连接分类成的向量维数也不同,故将大佬模型的除了最后一层分类层全部复制即可

预训练是指,大佬以及在大的数据集上已经训练好了的模型

微调是指,除了预训练好的模型的分类头,其余部分不变,只需要微调分类头部分即可

    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
from model_utils.model import initialize_model
# 使用resnet18模型,将分类头最后的输出维度改为11,use_pretrained为true表示使用模型的框架和参数,如果为false表示只使用框架
model, _ = initialize_model("resnet18", 11, use_pretrained=True)

半监督学习

步骤

  1. 监督学习阶段

    • 使用有标签数据训练神经网络的初始模型。
    • 某一轮的模型训练到一定程度时(比如验证集的准确率大于百分之50时),开始对无标签数据进行训练。
  2. 伪标签生成

    • 使用在上一步骤中训练的模型对无标签数据进行预测,生成伪标签
    • 可以采用置信度阈值(thres = 0.9来选择高质量的伪标签。
    • 将预测的概率(softmax后的概率分布中的最高值)超过置信度的样本及其预测的标签加入一个新的伪标签数据集
  3. 训练阶段

    • 将伪标签数据与原始的有标签数据合并,形成一个新的训练集。(将伪标签数据与原始的有标签数据“合并”通常是指,当伪标签数据集不为空的时候,在训练过程中先后训练原始数据集和伪标签数据集,而不是真正地将它们放在同一个数据集中进行训练。)
    • 可能需要重复这个过程多次,每次都生成新的伪标签并更新训练集。

半监督学习优点

  1. 性能提升:半监督学习使得模型能够从无标签数据中学习到更广泛的特征表示和数据分布,这有助于提高模型在分类、回归或其他预测任务中的准确性和泛化能力

  2. 成本效率:通过减少对标注数据的依赖,半监督学习大幅降低了数据准备的成本无标签的数据成本低很多,半监督学习充分利用了未标注数据的价值,这在数据丰富的时代尤为重要。通过这种方式,可以最大化地利用现有数据资源。

项目改进

1.先读入无标签Unlabel的数据

2.创建一个伪标签数据集类semiDataset,用于存放无标签数据通过模型后,筛选出来的有伪标签的数据集

3.训练有标签的数据,当模型达到验证集准确率高于50%时,开始对无标签数据进行训练

4.当伪标签数据集中有数据了(有大于置信度的概率样本),每轮训练对有标签数据集训练(更新参数后),也同样对伪标签数据集进行训练并进行参数更新,后续每轮如此

# 半监督训练后的伪标签数据集
class semiDataset(Dataset):
    def __init__(self, no_label_lodaer, model, device, thres=0.1):  # thres是置信度
        X, Y = self.data_pred(no_label_lodaer, 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 data_pred(self, no_label_lodaer, model, device, thres):        # 给无标签数据打标签
        model = model.to(device)
        soft = nn.Softmax(dim=1)
        pred_prob = []
        labels = []
        x = []
        y = []
        with torch.no_grad():
            for data in no_label_lodaer:
                data = data[0].to(device)
                pred = model(data)
                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_lodaer.dataset[index][1])
                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.Y)

# 取一批半监督伪标签数据
def get_semi_loader(no_label_lodaer, model, device, thres):
    semi_set = semiDataset(no_label_lodaer, model, device, thres)
    if semi_set.flag == False:
        return None
    semi_lodaer = DataLoader(semi_set,batch_size=32,shuffle=False)
    return semi_lodaer

知识拓展(简单介绍)

无监督学习  Unsupervised Learning

无监督学习是机器学习的一种类型,它与监督学习和半监督学习相对。在无监督学习中,模型试图从未标记的数据中发现模式或结构。由于数据没有附带标签,所以无监督学习算法需要自行找出数据的内在特征和关系

无监督学习通常用于创建预训练的特征提取器,这些特征提取器能够从数据中学习到有用的表示(或特征),这些表示可以捕捉到数据的关键属性和结构,而不需要任何标签信息。这些学习到的特征可以用于各种下游任务,包括但不限于分类、回归、聚类、降维和生成模型。

无监督训练出的模型具有很强的提取特征的能力,相当于一个解码器用于解码encoder

自监督学习(Self-Supervised Learning,简称SSL)

是机器学习中的一种。是一种无监督学习,它通过从数据本身生成伪标签来训练模型,例如,通过预测文本的下一个单词或图像的缺失部分。

例如把图片的一部分遮起来,用已知部分推测未知,文字也类似。

更多推荐