【项目004】 数据规约与数据增广

隐藏答案 | 返回首页

作者:欧新宇(Xinyu OU)
当前版本:Release v1.0
开发平台:Paddle 2.3.2
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。

最后更新:2025年3月24日


【实验目的】

  1. 理解数据规约与数据增广的基本原理和方法
  2. 学会使用paddle.io.Dataset类将数据集组织成Paddle内置数据格式
  3. 学会使用paddle.vision.transforms类实现对数据进行数据规约与数据增广
  4. 理解训练集、验证集和测试集在进行数据预处理时的区别,并学会使用Pathon实现编码
  5. 能够对编写的代码进行简单的测试和验证

【实验内容】

垃圾分类数据集Garbage 是一个包含有40个类别,14802张图像的数据集。该数据集已经事先实现了训练(train)测试(test)的分割,其中测试集没有类别标签。数据集没有给出验证集的划分建议,因此在进行数据列表生成的时候,可以自行按照一定的比列来将官方提供的训练集看作是trainval进行二次划分,即将原来的 train 文件夹划分为训练集train和验证集val。使用train和val完成训练之后,再按照训练获得超参数,对整个训练验证集trainval进行训练,完成后直接输出 测试集结果 到平台进行评估。注意,本数据集给出了类别的标签字典,因此在生成和划分数据子集的时候,建议考虑基于该类别标签字典而非遍历文件夹的方式来完成。

垃圾分类数据集Garbage

【评分及建议】

  1. 所有作业均在AIStudio上进行提交,提交时包含源代码和运行结果
  2. 评分建议:本项目总分150分,其中标准分100分,附加分50分。
  3. 数据集下载地址:https://aistudio.baidu.com/aistudio/datasetdetail/71361

【Tips】

  1. 由于JupyterLab和VSCode对当前目录的理解有所差异,为避免出错,建议文件路径尽量使用绝对路径。
  2. 特别注意 AIStudio的内核是Linux,在设置路径和中文字体方面和Windows有一定的差异。
  3. 最后的样本图像显示部分,由于进行了数据预处理,因此数值可能会脱离图像的像素值,导致产生警告。

【实验一】训练数据的增广和规约(60分+30分)

# pcodes00401_train_initialization
import os
import cv2
import json
import numpy as np
import matplotlib.pyplot as plt
import paddle
import paddle.vision.transforms as T
import paddle.vision.transforms.functional as F
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']  # Windows中指定绘图区域的字体为“微软雅黑”
plt.rcParams['font.size'] = 14
# from matplotlib.font_manager import FontProperties       # Linux中指定绘图区域的字体为“微软雅黑”
# fontpath = '/home/aistudio/work/msyh.ttc'
# myFont = FontProperties(fname=fontpath, size=14)
paddle.vision.set_image_backend('cv2')                   # 设置vision图像处理的背板为OpenCV

# 1. 定义数据集基本信息
dataset_name      = 'Garbage'
dataset_path         = 'D:/Workspace/ExpDatasets/'     # Windows: D:/Workspace/ExpDatasets/, Linux: /home/aistudio/work/ExpDatasets
dataset_root_path = os.path.join(dataset_path, dataset_name)
dataset_info_path = os.path.join(dataset_root_path, 'dataset_info.json')
# 2. 图像基本信息
args = {
'input_size': [3, 227, 227],             # 定义图像输入模型时的尺寸
'mean_value': [0.485, 0.456, 0.406],     # Imagenet均值
'std_value': [0.229, 0.224, 0.225],      # Imagenet标准差
}

【关于中文字体的设置说明】

在使用 python 的绘图包 matplotlib 绘制图形时,若要使用中文标签,需要手动指定中文字体。这个操作在Windows和Linux中略有不同。

  1. 在Windows中,只需要使用 matplotlib 自带的字体设置命令指定系统自带的字体即可。
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['font.size'] = 14
  1. 在Linux中,由于很多系统没有内置中文字体文件,所以需要手动进行关联。在本例中,我们提供了一个中文字体“微软雅黑”,并保存于 work 目录,你可以和直接对它进行引用。此外,在使用该字体的位置,还需要手动指定字体的配置信息。
# 1. 初始化部分设置
from matplotlib.font_manager import FontProperties
fontpath = '/home/aistudio/work/msyh.ttc'
myFont = FontProperties(fname=fontpath, size=14)

# 2. 显示中文的代码处进行指定
ax.set_title('中文标题', fontproperties=myFont)
plt.title('中文标题', fontproperties=myFont) 
plt.ylabel('y轴标签', fontproperties=myFont)

2. 数据准备

在调用数据集的样本前需要先手动解压数据集到工作目录,

此外,完成数据集的解压后,还需要生成数据列表,此处可以直接使用目录下的 generate_annotation.py 文件来实现,但注意需要将文件中的默认路径 dataset_path 修改为当前数据集的保存路径。

注意,以上生成过程,只需要执行一次即可。

在AIStudio上,可以使用以下命令对数据集进行解压

!unzip /home/aistudio/data/data71361/Garbage.zip -d /home/aistudio/work/Garbage/

# !python /home/aistudio/work/Garbage/generate_annotation.py
!python D://Workspace/ExpDatasets/Garbage/generate_annotation.py  #

图像列表已生成, 其中训练验证集样本14402,训练集样本11504个, 验证集样本2898个, 测试集样本400个, 共计14802个。

Q1.1 数据集定义 (30分) ([Your codes 1]~[Your codes 3])

class DatasetGarbage(paddle.io.Dataset):
    """定义十二生肖Zodiac数据集"""
    # args: 数据集定义所使用的相关超参数
    # transforms_type=[0|1|2]: 定义Transforms类型,选择是否使用数据增广和数据归约
    # 0:仅使用必要的数据规约,包括尺度变换、数据格式和数据类型变换。用于不使用图像变换的特殊场景。
    # 1:使用完整的数据归约和数据增广。用于train和trainval
    # 2:仅使用数据规约,包括尺度变换、数据格式和数据类型变换、均值消除。用于test和val,以及部分train(原图送入模型)
    def __init__(self, dataset_root_path, mode='test', args=None, transforms_type=None):
        self.data = []                                         # 定义数据序列,用于保存数据的路径和标签
        self.args = args                                       # 定义超参数列表
        self.transforms_type = transforms_type                 # 定义数据预处理类型

        # 1.2 根据模式,选择对应的数据预处理类型
        if mode in ['train', 'trainval']:
            self.transforms_type = 1
        elif mode in ['val', 'test'] and transforms_type != 0:
            self.transforms_type = 2

        # [Q1-1] 读取数据列表文件,将每一行都按照路径和标签进行拆分成两个字段的序列,并将序列依次保存至data序列中
        # 1) 若列表信息长度为2,则表示包含路径和标签信息。
        # 2) 若列表信息长度为1,则表示只包含路径,不包含标签。一般正式的测试文件都只包含路径,不包含标签。
        # [Your codes 1]
        with open(os.path.join(dataset_root_path, mode+'.txt'), encoding='utf-8') as f:
            for line in f.readlines():
                info = line.strip().split('\t')                # 拆分从列表文件中读取到数据信息
                image_path = info[0].strip()                   # 信息的[0]位置为路径
                if len(info) == 2:                             # 判断信息的长度,若包含标签则写入image_label
                    image_label = info[1].strip()
                elif len(info) == 1:                           # 判断信息的长度,若不包含标签,则用"-1"表示
                    image_label = -1
                self.data.append([image_path, image_label])    # 将路径和标签写入[data]容器

        # [Q1-2] 对训练数据和验证、测试数据采用不同的数据预处理方法
        # 1) train和trainval:执行随机裁剪,并完成标准化预处理
        # 2) train和trainval:直接执行尺度缩放,并完成标准化预处理
        # [Your codes 2]
        inputSize = self.args['input_size'][1:3] if len(self.args['input_size'])==3 else self.args['input_size']   # 获取输入图片的尺寸,若输入尺寸为3维(彩色图),则取后两个维度,否则取整个尺寸(灰度图)

        prob = np.random.random()                              # 生成一个随机数,用于判断是否执行数据增广
        if self.transforms_type == 0:
            self.transforms = T.Compose([                      # 0) 输出原始数据,仅作必要数据规约
                T.Resize(inputSize),                           # 直接尺度缩放
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
            ])
        elif self.transforms_type == 1 and prob <= 0.5:
            self.transforms = T.Compose([                      # 1) 训练数据预处理,包含数据增广(trainval, train)
                T.Resize((256, 256)),                          # 直接尺度缩放
                T.RandomResizedCrop(inputSize),                # 随机裁剪
                T.RandomHorizontalFlip(prob=0.5),              # 水平翻转
                T.RandomVerticalFlip(prob=0.5),                # 垂直翻转(谨慎使用)
                T.RandomRotation(15),                          # 随机旋转
                T.ColorJitter(brightness=0.4,                  # 色彩扰动:亮度、对比度、饱和度和色度
                              contrast=0.4,
                              saturation=0.4,
                              hue=0.4),
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
                T.Normalize(mean=self.args['mean_value'],      # Z-Score标准化
                            std=self.args['std_value'])
            ])
        else:
            self.transforms = T.Compose([                      # 2) 基本数据预处理,不含数据增广(val, test, 不参与增强的train)
                T.Resize(inputSize),                           # 直接尺度缩放
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
                T.Normalize(mean=self.args['mean_value'],      # Z-Score标准化
                            std=self.args['std_value'])
            ])

    # [Q1-3] 定义数据获取函数,返回单条数据,并对图像执行数据预处理,返回样本数据和对应的标签
    # [Your codes 3]
    def __getitem__(self, index):
        image_path, label = self.data[index]            # 根据索引,从列表中取出指定[index]图像,并将数据拆分成路径和列表
        img = cv2.imread(image_path, 1)                 # 使用cv2进行数据读取,0为灰度模式,1为彩色模式
        img = self.transforms(img)                      # 执行数据预处理        
        label = np.array(label, dtype='int64')          # 将标签转换为64位整型
        
        return img, label
                
    # 获取数据集的样本总数   
    def __len__(self): 
        # 返回self.data的长度
        return len(self.data)

Q1.2 数据集测试 (30分+30分) ([Your codes 4]~[Your codes 9])

1.2.1 创建数据读取器及数据迭代读取器并进行测试
# pcodes00403_create_DataReader_and_DataLoader
# 1. 生成数据读取器 [Q1-4]
# [Your codes 4]
dataset_trainval = DatasetGarbage(dataset_root_path, mode='trainval', args=args)
dataset_train = DatasetGarbage(dataset_root_path, mode='train', args=args)
dataset_val = DatasetGarbage(dataset_root_path, mode='val', args=args)
dataset_test = DatasetGarbage(dataset_root_path, mode='test', args=args)

# 2. 生成数据迭代读取器 [Q1-5]
# [Your codes 5]
trainval_reader = paddle.io.DataLoader(dataset_trainval, batch_size=32, shuffle=True)
train_reader = paddle.io.DataLoader(dataset_train, batch_size=32, shuffle=True)
val_reader = paddle.io.DataLoader(dataset_val, batch_size=32, shuffle=True)
test_reader = paddle.io.DataLoader(dataset_test, batch_size=32, shuffle=True)

# 3. 生成测试数据 [Q1-6]
# [Your codes 6]
print(f"trainval: {len(dataset_trainval)}, train: {len(dataset_train)}, val: {len(dataset_val)}, test: {len(dataset_test)}")
for i, (image, label) in enumerate(train_reader):
    if i < 2:
        print(f"训练集batch_{i}的图像形态:{image.shape}, 标签形态:{label.shape}")
    else:
        break
for i, (image, label) in enumerate(val_reader):
    if i < 2:
        print(f"验证集batch_{i}的图像形态:{image.shape}, 标签形态:{label.shape}")
    else:
        break
trainval: 14402, train: 11504, val: 2898, test: 400
训练集batch_0的图像形态:[32, 3, 227, 227], 标签形态:[32]
训练集batch_1的图像形态:[32, 3, 227, 227], 标签形态:[32]
验证集batch_0的图像形态:[32, 3, 227, 227], 标签形态:[32]
验证集batch_1的图像形态:[32, 3, 227, 227], 标签形态:[32]
1.2.2 数据标签获取
# pcodes00404_get_Garbage_labelname
def get_Garbage_labelname_from_labelID_by_json(label_id, dataset_info_path):    
    # 1. 根据标签ID,返回Zodiac数据集的文本标签,标签信息来源于dataset_info.json [Q1-7]
    # [Your codes 7]
    dataset_info = json.load(open(dataset_info_path, 'r', encoding='utf-8'))
    label_dict = dataset_info['label_dict']
    return label_dict[str(label_id)]    

# 2. 测试标签获取函数
print(f"标签 15 的名称为:{get_Garbage_labelname_from_labelID_by_json(15, dataset_info_path)}")

标签 15 的名称为:可回收物/包。

1.2.3 可视化数据集图片
import random

test_data_list = {'train': train_reader, 'test': test_reader}
for show in test_data_list:
    for i, (image, label) in enumerate(test_data_list[show]):
        print('测试数据集 batch_{} 的图像形态:{}, 标签形态:{}'.format(i, image.shape, label.shape))
        indices = random.sample(range(0, len(image)), 6)
        imgs = image[indices]   # 假设图像存储在'image'键中
        labels = label[indices] # 假设标签存储在'label'键中
        
        plt.figure(figsize=(14, 2))
        plt.title(f"{show}ing data", pad=25)
        plt.axis('off')
        
        for j in range(6):        
            ax = plt.subplot(1, 6, j+1)
            if show != 'test':
                ax.set_title(get_Garbage_labelname_from_labelID_by_json(int(labels[j]), dataset_info_path), fontsize=12)
            img = imgs[j].transpose((1, 2, 0)) # 如果图像是CHW格式,转为HWC   cv2.cvtColor(imgs[j].numpy(), cv2.COLOR_BGR2RGB)
            img = img.numpy()
            img = img * args['std_value'] + args['mean_value']
            plt.axis('off')
            plt.imshow(img) # 如果图像是CHW格式,转为HWC
        break

测试数据集 batch_0 的图像形态:[32, 3, 227, 227], 标签形态:[32]
测试数据集 batch_0 的图像形态:[32, 3, 227, 227], 标签形态:[32]

【实验二】测试数据的增广和规约(40分+20分)

import os
import cv2
import numpy as np
import random
import matplotlib.pyplot as plt
import paddle
import paddle.vision.transforms as T
import paddle.vision.transforms.functional as F
from matplotlib.font_manager import FontProperties       # Linux中指定绘图区域的字体为“微软雅黑”
fontpath = '/home/aistudio/work/msyh.ttc'
myFont = FontProperties(fname=fontpath, size=14)

# 1. 定义数据集基本信息
dataset_name   = 'Garbage'
root_path      = 'D:/Workspace/ExpDatasets' # Windows: 'D:\\Workspace\\'; Linux '/home/aistudio/'
dataset_root_path = os.path.join(root_path, dataset_name)

# 2. 图像基本信息
args = {
'input_size': [3, 227, 227],             # 定义图像输入模型时的尺寸
'mean_value': [0.485, 0.456, 0.406],     # Imagenet均值
'std_value': [0.229, 0.224, 0.225],      # Imagenet标准差
}

title = ['左上', '右上', '左下', '右下', '中央', '左上翻转', '右上翻转', '左下翻转', '右下翻转', '中央翻转']

Q2.1 数据预处理函数的定义(30分) ([Your codes 10]~[Your codes 12])

2.1.1 十重切割函数的定义

# pcodes00407_function_TenCrop
def TenCrop(img, crop_size=227):
    """AlexNet模型种定义的十重切割法,用于分类预测的测试过程"""
    # 1. 实现从256像素的输入图像中切割出10个224×224的patch
    # input_data: Height x Width x Channel 
    img_size = 256
    img = F.resize(img, (img_size, img_size))
    blob = np.zeros([10, crop_size, crop_size, 3])
    
    # 2. 获取左上、右上、左下、右下、中央及其对应的翻转,共计10个切片样本 [Q2-1]
    # [Your codes 10]
    blob[0] = F.crop(img,0,0,crop_size,crop_size)
    blob[1] = F.crop(img,0,img_size-crop_size,crop_size,crop_size)
    blob[2] = F.crop(img,img_size-crop_size,0,crop_size,crop_size)
    blob[3] = F.crop(img,img_size-crop_size,img_size-crop_size,crop_size,crop_size)
    blob[4] = F.center_crop(img, crop_size)
    blob[5] = F.hflip(blob[0, :, :, :])
    blob[6] = F.hflip(blob[1, :, :, :])
    blob[7] = F.hflip(blob[2, :, :, :])
    blob[8] = F.hflip(blob[3, :, :, :])
    blob[9] = F.hflip(blob[4, :, :, :])

    return blob

2.1.2 单幅图像预处理函数定义

# pcodes00408_function_SimplePreprocessing
def SimplePreprocessing(img, args=None, isTenCrop=True):
    # 1. 定义数据预处理功能,包含尺度规约、数据类型变换和均值消除 [Q2-2]
    # [Your codes 11]
    transform = T.Compose([
        T.Resize([args['input_size'][1],args['input_size'][2]]),
        T.ToTensor(),
        T.Normalize(mean=args['mean_value'], std=args['std_value']) 
    ])

    # 2. 根据超参数iSTenCrop判断是否对数据进行十重切割处理 [Q2-3]
    # [Your codes 12]
    if isTenCrop:
        fake_data = np.zeros([10] + args['input_size'], dtype=np.float32)    # 初始化一个4D的numpy数组,第一个维度是batch,后面的维度为channel× Height x Width
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        fake_blob = TenCrop(img)
        for i in range(10):
            fake_data[i] = transform(fake_blob[i]).numpy()
    else:
        fake_data = transform(img)

    return fake_data

Q2.2 数据测试 (10分+20分) ([Your codes 13]~[Your codes 14]))

2.2.1 输出数据集统计信息
提示:
数据预处理前的数据形态: (227, 227, 3)
仅十重切割后的数据形态: (10, 227, 227, 3)
数据预处理后的数据形态: (10, 3, 227, 227)

# pcodes00409_print_results
# 1. 从测试集中随机获取一个样本用于统计和可视化测试 [Q2-4]
# [Your codes 13.1]
dataset_test = DatasetGarbage(dataset_root_path, args=args, mode='test', isTransforms=0)
i = random.randrange(0, len(dataset_test))            # 生成一个随机数用于从测试集中获取样本
img = np.transpose(np.array(dataset_test[i][0]), [1,2,0])                             # 根据随机生成的整数,匹配测试集中的样本
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)        # 将图像从BRG转换成RGB

# 2. 对数据执行数据预处理
# 2.1 对数据执行十重切割,用于显示未做预处理的样本
# [Your codes 13.2]
fake_blob = TenCrop(img_rgb)
# 2.2 对数据执行十重切割,并进行简单预处理
# [Your codes 13.3]
fake_data = SimplePreprocessing(img,args=args, isTenCrop=True)      

# 3. 输出预处理后的数据形态
print('数据预处理前的数据形态: {}'.format(img.shape))  
print('仅十重切割后的数据形态: {}'.format(fake_blob.shape))
print('数据预处理后的数据形态: {}'.format(fake_data.shape))

2.2.2 可视化测试样本
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-2.117904..2.4945679].

注意,在以下的图像可视化过程中,可能会出现如上的 [Warning] 信息,主要原因是我们进行预处理后,数据可能会偏离图像像数值的规定取值范围 [0, 1] 或 [0, 255],此时若强制进行可视化,则会出现警告信息,这属于正常现象。

# 1. 可视化仅执行十重切割后的图像切片 [Q2-5.1]
# [Your codes 14.1]
plt.figure(1, figsize=(18, 6))
for i in range(10):
    ax = plt.subplot(2, 5, i+1)
    # ax.set_title(title[i], fontproperties=myFont)  # Linux 需要手动指定字体
    ax.set_title(title[i], fontsize=14)
    plt.imshow(fake_blob[i])
    plt.axis('off')

# 2. 可视化执行数据预处理后的图像切片 [Q2-5.2]
# [Your codes 14.2]
plt.figure(2, figsize=(18, 6))
for i in range(10):
    ax = plt.subplot(2, 5, i+1)
    # ax.set_title(title[i] + 'with均值消除', fontproperties=myFont)   # Linux 需要手动指定字体
    # ax.set_title(title[i] + 'with均值消除') 
    fake_img_pre = np.transpose(fake_data[i], (1,2,0))
    fake_img_pre_rgb = cv2.cvtColor(fake_img_pre, cv2.COLOR_BGR2RGB)
    plt.imshow(fake_img_pre_rgb)
    plt.axis('off') 
仅执行十重切割后的图像切片 执行数据预处理后的图像切片

【项目004】 数据规约与数据增广

【Tips】