1 项目介绍

1.1数据集内容

本次训练的FGVC—Aircraft数据集来源于kaggle网站,随着航空领域的迅速发展,我们只依托大类别对飞机进行分类存在局限性,这里我们构建针对FGVC-Aircraft数据集的模型主要用于飞机型号的精细识别。

1.2 数据集构成

数据规模:包含10600张图像,涵盖100种飞机型号变体

标注信息:每张图像的飞机都标注了边界框和层次化的飞机型号标签,包括飞机的模型,变体,家族和制造商,这里我们仅对飞机的变体进行识别训练

数据划分:数据集被平均分为了训练、验证和测试三个子集,这一点与之前简单的两划分的数据集有所区别

1.3 难点

该数据集的训练对精细程度有较高的要求,算法需要更精准与强鲁棒性,传统的一些机器学习算法难以胜任,需要引入残差网络等,同时由于图片尺寸较大,计算量多,也需要我们进行模型的迁移学习操作与数据增强操作。

2 数据处理

2.1 数据加载器的构建

class AircraftDataset(Dataset):
    #记住我们这里的这三个参数是创建数据集时需要传入的,要输入数据集路径,加载的数据集种类,是否进行数据变换
    def __init__(self, root_dir,split, transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.split = split
        #找到与存储图像的位置
        self.img_dir = os.path.join(self.root_dir, "data","fgvc-aircraft-2013b","fgvc-aircraft-2013b","data","images")
        #找到我们的标签文件,里面对应着图像的编号与对应的类别
        label_file = os.path.join(root_dir,"data","fgvc-aircraft-2013b","fgvc-aircraft-2013b","data",f"images_variant_{split}.txt")
        #逐行读取我们的标签文件,获取所有的图像名称以及所属类别存储到lines列表中
        with open(label_file,"r") as f:
            lines = f.readlines()
        #准备单独存储图像名与标签的列表,同时也要创建名称对应索引的字典,因为原文件是图像名对应字段,我们不能用字段来表示类别,要用下标索引来代替
        self.filenames = []
        self.labels = []
        self.label_to_dix = {}
        #对读取到的lines列表进行拆分,这里首先将所有的类别名称存储到标签列表内
        all_labels = [line.strip().split(" ",1)[1] for line in lines]
        #将标签先转化为集合去重,再转化为列表进行升序排序,此时,一个下标索引就会代表一个类别
        unique_labels = sorted(list(set(all_labels)))
        #利用枚举函数遍历标签列表中的信息,构建标签-索引的字典,目的是为了快速获取各个类别所代表的索引下标,把类别字符串的展示转化为下标的展示
        self.label_to_dix = {label:idx for idx, label in enumerate(unique_labels)}
        #逐行读取lines,将获取的图像名与标签分别存在两个专属的列表内,此时两者的关系是一一对应的
        for line in lines:
            filename, label = line.strip().split(" ",1)
            #由于储存的仅仅是图片的编号,后面我们在image文件中获取数据时需要完整的文件名,所以这里做了相应处理
            self.filenames.append(f"{filename}.jpg")
            #这里的标签全部转化为对应的索引类别
            self.labels.append(self.label_to_dix[label])
    #类的固定功能,会直接返回数据的数量           
    def __len__(self):
        return len(self.filenames)
    #这是我们构建数据集的关键步骤,目的是要返回我们的图像数据以及索引类别,以便应用到数据加载器中进行配对    
    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.filenames[idx])
        #读取图片以及对应的标签
        img = Image.open(img_path).convert('RGB')
        label = self.labels[idx]
        #一般情况下,我们只对目标进行处理,而不对标签进行处理,也不需要在这里将其转化为tensor,而transform就是我们额外封装的数据处理函数。
        if self.transform:
            img = self.transform(img)
        return img, label

首先我们要顺利地读取文件,不同的数据集内部文件的构造有可能不同,但如果是整理好的,必然会有至少两类数据,一类是训练集,一类是测试集,而这里我们还有额外的验证集,类的继承是来源于定义好的Dataset,我们自己构建最终要返回出图像数据与对应的标签数据。

2.2 数据转化与增强

#通常数据处理操作需要先构建操作字典,这样在调用的时候会很便捷
​
data_transform = {
        'train': Compose([
            Resize((224, 224)),
            RandomCrop((224, 224), padding=20),
            RandomHorizontalFlip(),
            ToTensor(),
            #这里我们进行归一化操作,这里的参数是在ImagNet数据集上经过大量训练得来的,要更准确的话需要自行去计算所有图片像素归一化的平均值,这通常很麻烦,且要耗费大量的时间,所以我们采用经验值来替代
            Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ])
        , 'val': Compose([
            Resize((224, 224)),
            ToTensor(),
            Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ])
        , 'test': Compose([
            Resize((224, 224)),
            ToTensor(),
            Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ])
    }
​

2.3 数据的读取

#实例化数据加载器,可以看到我们需要传入的一系列数据,这里以训练集数据的加载为例
train_dataset = AircraftDataset(root_dir=root_dir, split='train', transform=data_transform['train'])
#当我们完成数据加载器的构建后,就可以顺利从内部迭代出用于训练的数据了
train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)

3 模型构建

3.1 策略选择

刚才提到了,由于计算量大,所以我们需要采用迁移学习的办法,在图像识别领域,用于分类的常用的模型是resnet网络模型,同时,其也能在很大程度上缓解梯度消失的问题,增加模型训练的深度和效果,resnet网络模型又分为不同的层数,这里我们选择在实际业务中常用的resnet50来进行迁移学习。

3.1 ResNet50结构

以下是我们ResNet50的整体结构,接收3通道的数据,卷积一共有四层,每一层内又嵌套有多个block模块,这里我们看一下源码,因为后期涉及到注意力模块的插入,同时业务又是以迁移学习的形式为主,所以我们需要了解其网络结构。

ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): Bottleneck(
      (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (downsample): Sequential(
        (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): Bottleneck(
      (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (2): Bottleneck(
      (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
  )
  (layer2): Sequential(
    (0): Bottleneck(
      (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): Bottleneck(
      (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (2): Bottleneck(
      (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (3): Bottleneck(
      (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
  )
  (layer3): Sequential(
    (0): Bottleneck(
      (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (downsample): Sequential(
        (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): Bottleneck(
      (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (2): Bottleneck(
      (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (3): Bottleneck(
      (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (4): Bottleneck(
      (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (5): Bottleneck(
      (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
  )
  (layer4): Sequential(
    (0): Bottleneck(
      (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (downsample): Sequential(
        (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): Bottleneck(
      (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
    (2): Bottleneck(
      (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=2048, out_features=1000, bias=True)
)

这里再附上不同ResNet函数的结构模块。以便理解

img

3.2 模型迁移与预训练

def FGCV_moudle(num_classes=100):
    #导入模块后实例化resnet50模型,这里我们要进行预训练,所以要将pretrained参数设置为true
    model = models.resnet50(pretrained=True)
    #这里我们要使用resnet50的模型,要对该模型中的全连接层进行修改,将输出改为类别数量,再进行softmax
    in_features = model.fc.in_features
    model.fc = nn.Sequential(
        nn.Dropout(0.5),
        nn.Linear(in_features, num_classes)
    )
    return model

4 训练与验证、测试环节

4.1 训练与验证

def train():
    #在训练开始之前记得将模型转化到GPU上运行,如果不这样的话,运行效率会很低,训练效果也比较差
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = FGCV_moudle(100).to(device)
    #设置我们模型训练的参数,比如:学习率,损失函数,优化器等
    lr = 0.001
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    num_epochs = 50
    writer = SummaryWriter(log_dir="./data/fgvc-aircraft-2013b/tensorboard")
    #除了将模型迁移到GPU上进行运算,我们还要将模型调整为训练模型,因为模型处于不同的模式有不同的逻辑运算过程,会有不同影响
    for epoch in range(num_epochs):
        model.train()
        #实例化一些可视化需要用到的参数
        running_loss = 0.0
        progress_bar = tqdm(train_dataloader,desc=f"Epoch {epoch+1}/{num_epochs}")
        train_num = 0
        train_batchnum = 0
        correct = 0
        #开始进行训练
        for images, labels in progress_bar:
            #训练的数据需要我们迁移到GPU上去
            images, labels = images.to(device), labels.to(device)
            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()
            train_batchnum += 1
            train_num += len(labels)
            correct += outputs.argmax(dim=1).eq(labels).sum().item()
            progress_bar.set_postfix({"loss":f"{loss.item():.4f}"})
        #进行可视化操作
        print(f"{epoch}loss:{running_loss/train_batchnum}")
        writer.add_scalar("Loss/train", running_loss/train_batchnum , epoch)
        writer.add_scalar("Accuracy/train", correct /train_num, epoch)
        writer.add_scalar("Learning Rate", optimizer.param_groups[0]['lr'], epoch)
    #训练完成后我们需要对模型进行验证,这里专门划分了一个验证数据集,在验证前,我们需要将模型调整为验证模式
    model.eval()
    val_loss = 0.0
    correct = 0
    total = 0
    #为减少计算量,验证过程中不涉及反向传播与梯度计算,这里需要加上此代码保证数据不计算梯度
    with torch.no_grad():
        for images, labels in val_dataloader:
            #验证的数据同样需要迁移到GPU上
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            val_loss += loss.item()
            pred = outputs.argmax(dim=1)
            correct += pred.eq(labels).sum().item()
            total += labels.size(0)
        print(f"Accuracy of the network on the {total} test images: {100 * correct / total}%")
    #保存模型
    torch.save(model.state_dict(), "./data/fgvc-aircraft-2013b/model.pth")
    model.train()
    writer.add_graph(model, input_to_model=torch.randn(1, 3, 224, 224).to(device))
    writer.close()

详细的步骤操作如上显示,这里不同于之前学习的模型构建过程,此处将训练与验证分开进行,用了不同的数据集

4.2 为什么训练与验证过程要使用不同的数据呢?

  • 评估模型的泛化能力:模型的泛化能力是指模型对未知数据的适应和预测能力。使用训练集训练模型,然后用验证集来评估模型在未曾见过的数据上的表现,能够更真实地反映模型在实际应用中的性能。如果使用同一批数据进行训练和验证,模型可能会过度拟合训练数据,即记住了训练数据中的细节和噪声,而不是学习到数据中的一般规律,从而在面对新数据时表现不佳。通过将数据分为训练集和验证集,可以有效避免这种过拟合现象,更好地评估模型的泛化能力。

  • 调整模型超参数:在模型训练过程中,需要调整各种超参数,如学习率、层数、神经元数量等,以获得最佳的模型性能。使用验证集可以帮助我们选择最优的超参数组合。通过在训练集上训练不同超参数组合的模型,并在验证集上评估它们的性能,可以找到在验证集上表现最佳的超参数设置。如果使用同一批数据进行训练和验证,就无法准确判断超参数的调整是否真的提高了模型的泛化能力,因为模型可能只是在训练数据上过拟合,而在新数据上的性能并没有得到提升。

4.3 测试部分

def test():
    #同样测试前需要将模型拉到GPU上
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = FGCV_moudle(100).to(device)
    #加载保存的模型
    model.load_state_dict(torch.load('./data/fgvc-aircraft-2013b/model.pth'))
    model.eval()
    total_correct = 0
    test_num = 0
    total_csv_data = np.empty((0, 102))
    all_preds =[]
    all_labels = []
    #同样不需要去计算相关的梯度
    with torch.no_grad():
        for images, labels in test_dataloader:
            #将数据都放在GPU上跑
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            #构建csv,存储模型测试的信息,以供分析模块使用,文件一般是每一个对象对应的所有分类的概率,再加上预测与真实的标签值
            probability = F.softmax(outputs, dim=1)
            probability = probability.detach().cpu().numpy()
            pred = outputs.argmax(dim=1).cpu().numpy()
            all_preds.extend(pred)
            all_labels.extend(labels.cpu().numpy())
            pred_reshaped = pred.reshape(-1,1)
            labels_deal = labels.cpu().numpy().reshape(-1,1)
            csv_data = np.concatenate((probability,pred_reshaped,labels_deal), axis=1)
            total_csv_data = np.concatenate((total_csv_data,csv_data),axis=0)
            test_num += len(labels)
            total_correct += outputs.argmax(dim=1).eq(labels).sum().item()
            # 生成混淆矩阵
            cm = confusion_matrix(all_labels, all_preds)
​
            # 获取类别标签(按字母顺序排序)
            class_names = sorted(test_dataset.label_to_dix.keys(),
                                 key=lambda x: test_dataset.label_to_dix[x])
            # 绘制混淆矩阵
            plt.figure(figsize=(20, 16))
            sns.heatmap(cm, annot=False, fmt='d', cmap='Blues',
                        xticklabels=class_names,
                        yticklabels=class_names)
            plt.title('Confusion Matrix')
            plt.xlabel('Predicted Label')
            plt.ylabel('True Label')
            plt.xticks(rotation=45, ha='right', fontsize=6)
            plt.yticks(rotation=0, fontsize=6)
​
            # 保存图片
            os.makedirs('./data/fgvc-aircraft-2013b/plots', exist_ok=True)
            plt.savefig('./data/fgvc-aircraft-2013b/plots/confusion_matrix.png',
                        bbox_inches='tight', dpi=300)
            plt.close()
        print(f"test_accaurcy: {100 * total_correct / test_num}%")
        colunms = [*test_dataset.label_to_dix,"prep","target"]
        DF_CF = pd.DataFrame(total_csv_data, columns=colunms)
        DF_CF.to_csv("./data/fgvc-aircraft-2013b/test.csv", index=False)

4.4 为什么测试集不能用来调整超参数呢?

测试集不能用来调整超参数主要是为了保证模型评估的客观性和避免过拟合,从而真实反映模型的泛化能力,具体原因如下:

  • 防止数据窥探偏差:如果使用测试集来调整超参数,就相当于模型在一定程度上 “看到” 了测试集的数据信息,并根据这些信息来优化自身。这会导致模型针对测试集进行了特殊的优化,使得测试集上的性能指标虚高,不能真实反映模型对未知数据的泛化能力。例如,在调整神经网络的层数、学习率等超参数时,如果依据测试集的准确率来不断调整,可能会找到一组超参数使得模型在测试集上表现得非常好,但这只是因为模型记住了测试集的特征,而不是真正学习到了通用的模式,在实际应用中遇到新数据时,性能可能会大幅下降。

  • 保证评估的客观性:测试集的目的是提供一个独立、客观的数据集,用于最终评估模型的性能。只有在模型的训练和超参数调整过程完全不依赖测试集的情况下,才能确保测试集对模型的评估是公正、准确的,能够反映模型在真实场景中的表现。如果测试集被用于超参数调整,那么它就不再是一个纯粹的 “外部” 评估标准,会破坏评估的客观性和可信度。

为了得到可靠的模型和准确的性能评估,应该使用训练集来训练模型,使用验证集来调整超参数,最后使用测试集来对训练好且超参数确定的模型进行最终的性能评估,这样可以最大程度地保证模型的泛化能力和评估结果的可靠性。

5可视化操作

5.1 Tensorboard的使用

首先需要导入所需的包

from torch.utils.tensorboard import SummaryWriter

因为在使用Tensorboard的相关方法记录数据后,需要在终端调用生成的文件运行查看

writer = SummaryWriter(log_dir="./data/fgvc-aircraft-2013b/tensorboard")

因为要绘图记录损失和准确率的变化,所以在每一轮训练结束后都需获取相关信息

writer.add_scalar("Loss/train", running_loss/train_batchnum , epoch)
writer.add_scalar("Accuracy/train", correct /train_num, epoch)
writer.add_scalar("Learning Rate", optimizer.param_groups[0]['lr'], epoch)

要在 TensorBoard 的 GRAPHS 标签页中生成模型的结构图。图中会展示模型的各个层(如卷积层、全连接层等)以及数据在这些层之间的流动方向,还会显示各层的输入输出形状和参数数量等信息。通过这个图,可以直观地了解模型的整体架构和数据流,有助于理解模型的设计和调试模型结构。

writer.add_graph(model, input_to_model=torch.randn(1, 3, 224, 224).to(device))
#关闭显示
writer.close()

5.2 tqdm的使用

导入包

from tqdm import tqdm

使用tqdm包装函数

#构建进度条,desc参数是可选参数,为进度条添加描述
progress_bar = tqdm(train_dataloader,desc=f"Epoch {epoch+1}/{num_epochs}")
#在我们每轮训练时,数据就从包装的tqdm对象中取出
for images, labels in progress_bar:
#这里在一轮训练当中,在进度条的末尾添加一项损失,以便动态显示,通常里面接收的是一个字典
progress_bar.set_postfix({"loss":f"{loss.item():.4f}"})

最后的显示效果如下,损失是动态显示的,能动态追踪运行的情况

5.3 分析报告函数

这里我们直接调用官方的相关代码,主要是针对我们之前储存的关于模型训练的csv进行拆解,也包括混淆矩阵的绘制

from sklearn.metrics import classification_report, accuracy_score, precision_score, recall_score, f1_score,confusion_matrix
def analy():
    # 读取csv数据
    data1 = pd.read_csv("./data/fgvc-aircraft-2013b/test.csv", encoding="GB2312")
    # 新增:显示混淆矩阵图片
    img = plt.imread('./data/fgvc-aircraft-2013b/plots/confusion_matrix.png')
    plt.figure(figsize=(24, 18))
    plt.imshow(img)
    plt.axis('off')
    plt.title('Confusion Matrix')
    plt.show()
    print(type(data1))
    # 整体数据分析报告
    report = classification_report(
        y_true=data1["target"].values,
        y_pred=data1["prep"].values,
    )
    print(report)
    # 准确度 Acc
    print(
        "准确度Acc:",
        accuracy_score(
            y_true=data1["target"].values,
            y_pred=data1["prep"].values,
        ),
    )
    # 精确度
    print(
        "精确度Precision:",
        precision_score(
            y_true=data1["target"].values, y_pred=data1["prep"].values,
            average="macro"
        ),
    )
    # 召回率
    print(
        "召回率Recall:",
        recall_score(
            # 100
            y_true=data1["target"].values,
            y_pred=data1["prep"].values,
            average="macro",
        ),
    )
    # F1 Score
    print(
        "F1 Score:",
        f1_score(
​
    y_true = data1["target"].values,
    y_pred = data1["prep"].values,
    average = "macro",
    ),
    )

6 输出结果

6.1 Tensorboard端输出

可以看到,在训练过程中的准确率已经达到了89.89%,最后一次训练的损失下降到了0.3204

6.2分析报告

但是最终的分析报告显示,模型在最终的测试集上的表现远远不如训练集中精准率与损失,这证明模型出现了过拟合现象

6.3分类矩阵

通过分类的矩阵可以看出,模型在哪些类别上的验证效果不是很好,可以通过后续操作进行改进,如进行针对性的数据增强操作

type precision recall f1-score support

     0.0       0.53      0.55      0.54        33
     1.0       0.70      0.21      0.33        33
     2.0       0.78      0.41      0.54        34
     3.0       0.15      0.06      0.09        33
     4.0       0.75      0.45      0.57        33
     5.0       0.37      0.21      0.26        34
     6.0       0.64      0.55      0.59        33
     7.0       0.28      0.55      0.37        33
     8.0       0.75      0.71      0.73        34
     9.0       0.77      0.52      0.62        33
    10.0       0.23      0.45      0.31        33
    11.0       0.50      0.15      0.23        34
    12.0       0.30      0.21      0.25        33
    13.0       0.25      0.33      0.29        33
    14.0       0.41      0.35      0.38        34
    15.0       0.82      0.82      0.82        33
    16.0       0.58      0.45      0.51        33
    17.0       0.36      0.26      0.31        34
    18.0       0.76      0.67      0.71        33
    19.0       0.26      0.18      0.21        33
    20.0       0.71      0.35      0.47        34
    21.0       0.30      0.52      0.38        33
    22.0       0.42      0.55      0.47        33
    23.0       0.81      0.74      0.77        34
    24.0       0.47      0.52      0.49        33
    25.0       0.56      0.67      0.61        33
    26.0       0.62      0.62      0.62        34
    27.0       0.29      0.36      0.32        33
    28.0       0.33      0.55      0.41        33
    29.0       0.40      0.56      0.46        34
    30.0       0.43      0.67      0.52        33
    31.0       0.83      0.58      0.68        33
    32.0       0.59      0.47      0.52        34
    33.0       0.86      0.55      0.67        33
    34.0       0.69      0.67      0.68        33
    35.0       0.77      0.71      0.74        34
    36.0       0.85      0.88      0.87        33
    37.0       0.89      0.24      0.38        33
    38.0       0.52      0.76      0.62        34
    39.0       0.88      0.67      0.76        33
    40.0       0.74      0.70      0.72        33
    41.0       0.48      0.65      0.55        34
    42.0       0.90      0.55      0.68        33
    43.0       0.27      0.30      0.29        33
    44.0       0.77      0.79      0.78        34
    45.0       0.59      0.48      0.53        33
    46.0       0.61      0.58      0.59        33
    47.0       0.69      0.32      0.44        34
    48.0       0.54      0.64      0.58        33
    49.0       0.57      0.73      0.64        33
    50.0       0.64      0.79      0.71        34
    51.0       0.93      0.82      0.87        33
    52.0       0.50      0.24      0.33        33
    53.0       0.26      0.35      0.30        34
    54.0       0.51      0.61      0.56        33
    55.0       0.39      0.52      0.44        33
    56.0       0.73      0.32      0.45        34
    57.0       0.82      0.42      0.56        33
    58.0       0.61      0.52      0.56        33
    59.0       0.60      0.53      0.56        34
    60.0       0.80      0.61      0.69        33
    61.0       0.83      0.61      0.70        33
    62.0       0.88      0.85      0.87        34
    63.0       0.84      0.94      0.89        33
    64.0       0.83      0.88      0.85        33
    65.0       0.79      0.32      0.46        34
    66.0       0.52      0.88      0.65        33
    67.0       0.76      0.79      0.78        33
    68.0       0.55      0.68      0.61        34
    69.0       0.50      0.67      0.57        33
    70.0       0.58      0.91      0.71        33
    71.0       0.87      0.38      0.53        34
    72.0       0.67      0.88      0.76        33
    73.0       0.45      0.67      0.54        33
    74.0       0.78      0.91      0.84        34
    75.0       0.92      0.73      0.81        33
    76.0       0.68      0.70      0.69        33
    77.0       0.96      0.74      0.83        34
    78.0       0.46      0.79      0.58        33
    79.0       0.93      0.76      0.83        33
    80.0       0.59      0.76      0.67        34
    81.0       0.63      0.79      0.70        33
    82.0       0.76      0.79      0.78        33
    83.0       0.93      0.76      0.84        34
    84.0       0.67      0.67      0.67        33
    85.0       0.43      0.64      0.51        33
    86.0       0.35      0.65      0.45        34
    87.0       0.46      0.39      0.43        33
    88.0       0.57      0.52      0.54        33
    89.0       0.94      0.91      0.93        34
    90.0       0.47      0.88      0.61        33
    91.0       0.47      0.64      0.54        33
    92.0       0.83      0.88      0.86        34
    93.0       0.82      0.82      0.82        33
    94.0       0.90      0.58      0.70        33
    95.0       0.80      0.59      0.68        34
    96.0       0.87      0.61      0.71        33
    97.0       0.91      0.64      0.75        33
    98.0       0.73      0.79      0.76        34
    99.0       0.78      0.76      0.77        33

6.4 混淆矩阵

可以通过混淆矩阵看出,尤其是在最初的20个类别中,分类情况不是很好,容易混淆

6.5 概率分布CSV文件展现

由于此数据集是100分类,所以有很多类别的概率为0

7 简单的模型改进

后续将引入最近所学的混合注意力机制、利用分组卷积、深度可分离技术来进行优化,这里仅进行简单的优化

7.1采用AdamW优化器进行优化

 optimizer = torch.optim.AdamW(model.parameters(), lr=0.001,weight_decay=0.001)
  • 原理基础:AdamW 是在 Adam 算法的基础上改进而来。Adam 算法结合了 Adagrad 善于处理稀疏梯度和 RMSProp 善于处理非平稳目标的优点,为不同的参数计算不同的自适应学习率。但在使用 L2 正则化(权重衰减)时,Adam 算法存在一些问题,会导致训练后的模型泛化能力不佳。AdamW 对权重衰减进行了分离,使其不再依赖梯度,这种解耦的权重衰减方式让优化过程更加合理,有效改善了模型的泛化性能。

  • 优势特点:

    • 良好的泛化性:通过将权重衰减从梯度计算中分离,避免了与自适应梯度算法的不良交互,减少了过拟合风险,提高了模型在新数据上的泛化能力。

    • 适应性强:和 Adam 算法类似,AdamW 能够适应不同的问题和数据集,在处理大规模数据和复杂模型结构时表现出色,对于不同类型的神经网络,如卷积神经网络(CNN)、循环神经网络(RNN)及其变体(LSTM、GRU)等都适用。

    • 收敛速度较快:AdamW 在大多数情况下能快速收敛到较优解,能在相对较少的训练轮次内达到较好的训练效果,节省训练时间和计算资源。

  • 应用场景:广泛应用于各类深度学习任务中,如图像识别、自然语言处理、语音识别等。在图像分类任务中训练深度卷积神经网络时,AdamW 可以帮助模型更快更好地学习图像特征;在自然语言处理的预训练模型,如 BERT 及其衍生模型的训练中,AdamW 也发挥了重要作用,助力模型有效学习文本中的语义信息。

AdamW相比于Adam优化器,进行了正则化上的改进,原来的方法正则化是在损失函数上去进行添加的,这虽然能够控制模型参数,但是也让模型参数与学习率产生一定程度上的对抗,而AdamW方法从根源上解决了这个问题,不在损失函数内使用正则化,而在每一轮反向传播时,直接对参数进行衰减,但是这样优化会产生一个问题,那就是学习率没有办法很好地去进行控制,所以我们引入了第二个方法。

7.2余弦退火算法

这里创建了一个学习率优化器,采用的是余弦退火算法来对学习率进行优化,同时在反向传播的过程当中调用类似梯度更新的scheduler.step()方法来对学习率进行更新,但是建议的是每一轮训练进行一次更新,而不是挨着梯度进行每一个buntch的更新。

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max=50)
​
​
scheduler.step()

在深度学习中,余弦退火算法(Cosine Annealing)主要用于动态调整学习率,以帮助模型更高效地收敛,提升训练效果。以下从原理、特点、优势、应用场景等方面详细介绍:

  • 原理:该算法的核心灵感来源于余弦函数的周期性变化。在训练过程中,学习率会随着训练轮次(epoch)或迭代次数,按照余弦函数的形状进行变化。初始时,学习率被设置为一个较大的值,这有助于模型在参数空间中进行更广泛的探索,快速朝着最优解的大致方向前进。随着训练的推进,学习率逐渐降低,类似于余弦函数从峰值下降的过程,使得模型在接近最优解时能够进行更精细的调整,避免因学习率过大而错过最优解。当学习率降低到一定程度后,又可以重新增大,开始新一轮的调整过程,形成周期性的变化。

  • 特点:

    • 周期性调整:以一定的周期对学习率进行动态调整,这种周期性变化可以使模型在训练过程中既能快速探索参数空间,又能在后期进行精细优化。

    • 无需手动调参:相比固定学习率或一些简单的学习率衰减策略,余弦退火算法不需要手动频繁调整学习率的衰减因子等参数,减少了人工干预,并且能在不同的任务和数据集上表现出较好的适应性。

  • 优势:

    • 提升收敛效果:通过合理地动态调整学习率,有助于模型更快地收敛到更优的解,特别是在处理复杂的深度学习模型和大规模数据集时,能够提高模型的训练效率和最终性能。

    • 避免局部最优:学习率的周期性变化使得模型在训练过程中能够跳出一些较差的局部最优解,增加了找到全局最优解或更优局部最优解的可能性。

    • 缓解梯度消失和梯度爆炸:在训练初期较大的学习率可以加速模型的学习,而在后期逐渐减小的学习率可以缓解因梯度消失或梯度爆炸导致的训练不稳定问题。

  • 应用场景:广泛应用于各种深度学习任务,如计算机视觉领域的图像分类、目标检测、语义分割,自然语言处理中的机器翻译、文本分类、情感分析等。在训练深度神经网络,如卷积神经网络(CNN)、循环神经网络(RNN)及其变体(LSTM、GRU),以及近年来流行的 Transformer 架构等模型时,都可以使用余弦退火算法来调整学习率,提升模型训练效果。

8 改进结果

8.1分析报告

在同等的50次训练下,各项精度上升至少4个百分点,随着轮次的加深,这一效果还会持续放大

8.2 混淆矩阵

8.3Tensorboard显示

可以看每次训练的精准率达到了96.97%

损失值下降到了0.1679

9项目总结

        这次选取的数据集是细分类的著名的Aircraft数据集,该数据集拥有众多更小颗粒度的分类,如飞机的制造商等,具有很大的学习利用价值

        在前期的模型构建中,充分学习了如何从复杂的数据集文件中找到需求的文件并进行合理的分割,同时也利用了迁移学习,利用成熟的模型来进行预训练,大大减少了训练的时间,提升了效果,也采用了一些方法来进行优化,比如余弦退火算法等,但是此数据集做为细分类的经典数据集,我们需要引入注意力机制、可变卷积来进一步进行捕捉图像特征,同时也要引入一些分组卷积的方法来减少参数量,相信通过这些方法,在50轮次的训练下,模型的各项性能会有进一步的提升,这便是下一个章节的主要内容。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐