处理数据不平衡问题的方法有多种,以下是一些常用的方法:
过采样(Oversampling):增加少数类样本的数量,使得正样本和负样本的数量更加平衡。过采样的方法包括随机复制样本、SMOTE(Synthetic
Minority Over-sampling Technique)等。欠采样(Undersampling):减少多数类样本的数量,使得正样本和负样本的数量更加平衡。欠采样的方法包括随机删除样本、Cluster
Centroids等。生成合成样本(Synthetic Sample Generation):通过生成合成样本的方式增加少数类样本的数量,而不是简单地复制已有的样本。SMOTE是最常用的合成样本生成方法之一。
集成学习(Ensemble Learning):使用集成学习方法,如Bagging、Boosting等,结合多个模型来处理数据不平衡问题。在不同的子模型中,可以使用不同的采样策略来处理不平衡问题。
代价敏感学习(Cost-sensitive Learning):在训练过程中,给少数类样本赋予更高的权重,以降低模型对多数类样本的偏好,使得模型更加关注少数类样本。
阈值调整(Threshold Adjustment):调整模型的预测阈值,使得模型更容易检测到少数类样本。通常情况下,可以将阈值设置得更低,以增加对少数类样本的检测灵敏度。
基于特征选择的方法(Feature-based Methods):选择更具代表性的特征,或者通过特征工程的方式增加有助于区分少数类样本的特征,从而提高模型对少数类样本的识别能力。
选择哪种方法取决于数据集的特点、任务需求以及计算资源等因素。通常情况下,需要尝试多种方法,并根据模型的性能进行评估和选择。
1.生成合成样本
以下的处理是为了增加训练样本的多样性。提高畸形听小骨的学习效果,使模型更好地适应这类样本,从而提升整体的检测性能。
以一个demo为例。 由于数据集存在数据不平衡的问题,主要体现在学习占比较多的是正常听小骨,而畸形听小骨的样本相对较少,这使得模型在学习畸形听小骨方面的效果不佳。
1.1. 脚本筛选出数据少的数据
为了解决这一问题。我通过脚本筛选出数据集中所有包含畸形听小骨的图像。
以下为筛选并复制畸形听小骨脚本的全部代码:
import os
import shutil
def filter_and_copy_labels(input_folder, output_folder):
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 遍历输入文件夹中的标签文件
for filename in os.listdir(input_folder):
if filename.endswith(".txt"):
input_path = os.path.join(input_folder, filename)
output_path = os.path.join(output_folder, filename)
# 打开输入文件并检查是否有标签值为1的行,如果有则复制整个文件到输出文件夹
with open(input_path, 'r') as input_file:
for line in input_file:
label = int(line.split()[0])
if label == 1:
shutil.copyfile(input_path, output_path)
break # 一旦找到一个标签值为1的行,就复制整个文件并停止循环
if __name__ == "__main__":
input_folder = r"E:\CT\dataset\labels"
output_folder = r"E:\CT\dataset\labels_1"
filter_and_copy_labels(input_folder, output_folder)
1.2. 进行数据增强
并对这些图像进行数据增强,包括缩放,旋转,错切,翻转,增加高斯噪声和 限制对比度自适应直方图均衡等方法进行数据增强等操作。
全部脚本:
import xml.etree.ElementTree as ET
import os
import numpy as np
from PIL import Image
import imgaug as ia
from imgaug import augmenters as iaa
ia.seed(1)
# 读取出图像中的目标框
def read_xml_annotation(root, image_id):
in_file = open(os.path.join(root, image_id))
tree = ET.parse(in_file)
root = tree.getroot()
bndboxlist = []
for object in root.findall('object'): # 找到root节点下的所有country节点
bndbox = object.find('bndbox') # 子节点下节点rank的值
xmin = int(bndbox.find('xmin').text)
xmax = int(bndbox.find('xmax').text)
ymin = int(bndbox.find('ymin').text)
ymax = int(bndbox.find('ymax').text)
bndboxlist.append([xmin, ymin, xmax, ymax])
return bndboxlist # 以多维数组的形式保存
def change_xml_annotation(root, image_id, new_target):
new_xmin = new_target[0]
new_ymin = new_target[1]
new_xmax = new_target[2]
new_ymax = new_target[3]
in_file = open(os.path.join(root, str(image_id) + '.xml'))
tree = ET.parse(in_file)
xmlroot = tree.getroot()
object = xmlroot.find('object')
bndbox = object.find('bndbox')
xmin = bndbox.find('xmin')
xmin.text = str(new_xmin)
ymin = bndbox.find('ymin')
ymin.text = str(new_ymin)
xmax = bndbox.find('xmax')
xmax.text = str(new_xmax)
ymax = bndbox.find('ymax')
ymax.text = str(new_ymax)
tree.write(os.path.join(root, str(image_id) + "_aug" + '.xml'))
def change_xml_list_annotation(root, image_id, new_target, saveroot, id,image_aug):
in_file = open(os.path.join(root, str(image_id) + '.xml')) # 读取原来的xml文件
tree = ET.parse(in_file) # 读取xml文件
xmlroot = tree.getroot()
# 获取图像的宽度和高度信息
img_height, img_width, _ = image_aug.shape
index = 0
# 将bbox中原来的坐标值换成新生成的坐标值
for object in xmlroot.findall('object'): # 找到root节点下的所有country节点
bndbox = object.find('bndbox') # 子节点下节点rank的值
# 注意new_target原本保存为高维数组
new_xmin = new_target[index][0]
new_ymin = new_target[index][1]
new_xmax = new_target[index][2]
new_ymax = new_target[index][3]
xmin = bndbox.find('xmin')
xmin.text = str(new_xmin)
ymin = bndbox.find('ymin')
ymin.text = str(new_ymin)
xmax = bndbox.find('xmax')
xmax.text = str(new_xmax)
ymax = bndbox.find('ymax')
ymax.text = str(new_ymax)
index = index + 1
# 添加图像宽度和高度信息
size = xmlroot.find('size')
width_elem = size.find('width')
height_elem = size.find('height')
width_elem.text = str(img_width)
height_elem.text = str(img_height)
tree.write(os.path.join(saveroot, str(image_id) + "_aug_" + str(id) + '.xml'))
# 处理文件
def mkdir(path):
# 去除首位空格
path = path.strip()
# 去除尾部 \ 符号
path = path.rstrip("\\")
# 判断路径是否存在
# 存在 True
# 不存在 False
isExists = os.path.exists(path)
# 判断结果
if not isExists:
# 如果不存在则创建目录
# 创建目录操作函数
os.makedirs(path)
print(path + ' 创建成功')
return True
else:
# 如果目录存在则不创建,并提示目录已存在
print(path + ' 目录已存在')
return False
if __name__ == "__main__":
# 存储增强前的图片文件夹路径
IMG_DIR = r"E:\CT\Aug_1\images_1"
# 存储增强前的XML文件夹路径
XML_DIR = r"E:\CT\Aug_1\xml_1"
# 存储增强后的影像文件夹路径
AUG_IMG_DIR = r"E:\CT\Aug_1\Aug_1_sample1\images"
mkdir(AUG_IMG_DIR)
# 存储增强后的XML文件夹路径
AUG_XML_DIR = r"E:\CT\Aug_1\Aug_1_sample1\xml"
mkdir(AUG_XML_DIR)
AUGLOOP = 5 # 每张影像增强的数量
boxes_img_aug_list = []
new_bndbox = []
new_bndbox_list = []
# 图片数据增强
seq = iaa.Sequential([
# 选择0到5种方法做变换
iaa.SomeOf((2, 8),
[
# 改变标签文件的数据增强方式,有时需要重新标注 # 注意只要是带有旋转和平移性质的方式都要检查标签是否合适,做人工微调
# 仿射变换
# 包含:平移(Translation)、旋转(Rotation)、放缩(zoom)、错切(shear)。
# 仿设变换通常会产生一些新的像素点,我们需要指定这些新的像素点的生成方法,这种指定通过设置cval和mode两个参数来实现。参数order用来设置插值方法。
# 只有在mode设置为“constant”时,cval的值才有效。
iaa.Affine(
translate_percent={"x": (-0.1, 0.1), "y": (-0.1, 0.1)}, # 平移
scale=(0.8, 0.95), # 图像缩放为80%到95%之间
rotate=(-15, 15), # 旋转±15度之间
shear=(-10, 10), # 错切±15度之间
mode="edge"
),
# 改变图片的尺寸只使用一个
iaa.OneOf([
# 长和宽变成256*256
iaa.Resize({"height": 1500, "width": 1500}),
# 长变成原来的0.5到0.75的随机倍数,宽同理
iaa.Resize({"height": (0.5, 0.75), "width": (0.8, 1)}),
# 长变成原来的0.8到1的随机倍数,且长宽比不变
iaa.Resize({"height": (0.8, 1), "width": "keep-aspect-ratio"})
]),
# 翻转只使用一个
iaa.OneOf([
# 镜像翻转
iaa.Fliplr(1), # 对50%的图片进行水平镜像翻转
iaa.Flipud(1), # 对50%的图片进行垂直镜像翻转
# 中心对称
iaa.Sequential([
iaa.Fliplr(1), # 对50%的图片进行水平镜像翻转
iaa.Flipud(1), # 对50%的图片进行垂直镜像翻转
]),
]),
# 不改变标签文件的数据增强方式
# 添加噪声只使用一个
iaa.OneOf([
# 增加高斯噪声
iaa.AdditiveGaussianNoise(
loc=0, scale=(0.0, 0.05 * 255)
),
# 为每个像素乘以一个值
iaa.MultiplyElementwise((0.8, 1.2)),
# 为每个像素加上一个值
iaa.AddElementwise((-40, 40)),
]),
# 限制对比度自适应直方图均衡(CLAHE算法),本算法与普通的自适应直方图均衡不同地方在于对比度限幅,图像对比度会更自然。
iaa.CLAHE(clip_limit=(1, 15)),
# 不作任何变换
iaa.Noop(),
],
random_order=True # 以随机的方式执行上述扩充
)
], random_order=True)
# 得到当前运行的目录和目录当中的文件,其中sub_folders可以为空
for root, sub_folders, files in os.walk(XML_DIR):
# 遍历每一张图片
for name in files:
bndbox = read_xml_annotation(XML_DIR, name)
for epoch in range(AUGLOOP):
seq_det = seq.to_deterministic() # 固定变换序列,之后就可以先变换图像然后变换关键点,这样可以保证两次的变换完全相同
# 读取图片
img_path = os.path.join(IMG_DIR, name[:-4] + '.jpg')
img = np.array(Image.open(img_path).convert('RGB'), dtype=np.uint8)
# bndbox 坐标增强,依次处理所有的bbox
for i in range(len(bndbox)):
bbs = ia.BoundingBoxesOnImage([
ia.BoundingBox(x1=bndbox[i][0], y1=bndbox[i][1], x2=bndbox[i][2], y2=bndbox[i][3]),
], shape=img.shape)
bbs_aug = seq_det.augment_bounding_boxes([bbs])[0]
boxes_img_aug_list.append(bbs_aug)
# new_bndbox_list:[[x1,y1,x2,y2],...[],[]]
new_bndbox_list.append([int(bbs_aug.bounding_boxes[0].x1),
int(bbs_aug.bounding_boxes[0].y1),
int(bbs_aug.bounding_boxes[0].x2),
int(bbs_aug.bounding_boxes[0].y2)])
# 存储变化后的图片
image_aug = seq_det.augment_images([img])[0]
path = os.path.join(AUG_IMG_DIR, str(name[:-4]) + "_aug_" + str(epoch) + '.jpg')
Image.fromarray(image_aug).save(path)
# 存储变化后的XML
change_xml_list_annotation(XML_DIR, name[:-4], new_bndbox_list, AUG_XML_DIR, epoch,image_aug)
new_bndbox_list = []
print(name[:-4] + ' ' + 'finish')
print('Complete!!!!!')
增强后正常和畸形的标签框数量相仿。这样的处理能够提高畸形听小骨的学习效果,使模型更好地适应这类样本,从而提升整体的检测性能。