1.研究背景与意义
项目参考AAAI Association for the Advancement of Artificial Intelligence
研究背景与意义
近年来,基于骨的学术研究在多个领域中引起了广泛的关注。骨是人体中最重要的组织之一,不仅提供了机械支撑和保护内脏器官的功能,还参与了骨骼系统的生长、修复和代谢等生理过程。因此,对骨的深入研究对于理解人体生理学、疾病发生机制以及开发新的治疗方法具有重要意义。
首先,骨研究对于理解骨骼系统的生长和发育过程至关重要。骨骼系统在人体发育过程中起着至关重要的作用,它不仅决定了人体的身高和体型,还影响了人体的运动能力和生活质量。通过研究骨的生长和发育机制,可以揭示骨骼系统的形成过程,为儿童生长发育问题的解决提供理论依据。
其次,骨研究对于骨损伤和骨疾病的治疗具有重要意义。骨折、骨质疏松症、骨肿瘤等骨疾病是世界范围内广泛存在的健康问题,给患者的生活和工作带来了巨大的困扰。通过研究骨的生理和病理过程,可以深入了解骨疾病的发生机制,为骨疾病的早期诊断和治疗提供科学依据。此外,基于骨的研究还可以为骨损伤的修复和再生提供新的治疗策略,如骨移植、骨替代材料等。
另外,骨研究对于人体整体健康的维护和促进也具有重要意义。骨骼系统不仅与运动功能相关,还参与了人体的代谢调节、免疫功能和内分泌调控等多个生理过程。通过研究骨的生理功能,可以深入了解骨与其他器官系统之间的相互作用,为人体整体健康的维护和促进提供理论基础。
最后,基于骨的研究还具有广泛的应用前景。随着科技的不断进步,基于骨的研究方法和技术不断发展,如骨密度测量、骨组织工程等。这些技术的应用不仅可以为临床医学提供更准确的诊断手段和治疗方法,还可以为药物研发和生物材料的开发提供新的思路和方法。
综上所述,基于骨的学术研究在人体生理学、疾病发生机制和治疗方法等方面具有重要意义。通过深入研究骨的生长发育、骨疾病的发生机制以及骨的生理功能,可以为人体健康的维护和促进提供理论依据,为骨疾病的早期诊断和治疗提供科学依据,同时也为临床医学和生物医学的发展提供新的思路和方法。因此,基于骨的学术研究具有重要的学术和应用价值。
2.图片演示
3.视频演示
基于骨干网络ConvNeXtV2的改进YOLOv5的遥感图像目标检测系统_哔哩哔哩_bilibili
4.数据集的采集&标注和整理
图片的收集
首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集NWPU VHR-10 dataset。
labelImg是一个图形化的图像注释工具,支持VOC和YOLO格式。以下是使用labelImg将图片标注为VOC格式的步骤:
(1)下载并安装labelImg。
(2)打开labelImg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的XML文件。
(6)重复此过程,直到所有的图片都标注完毕。
由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。
下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
import os
classes = [] # 初始化为空列表
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
def convert(size, box):
dw = 1. / size[0]
dh = 1. / size[1]
x = (box[0] + box[1]) / 2.0
y = (box[2] + box[3]) / 2.0
w = box[1] - box[0]
h = box[3] - box[2]
x = x * dw
w = w * dw
y = y * dh
h = h * dh
return (x, y, w, h)
def convert_annotation(image_id):
in_file = open('./label_xml\%s.xml' % (image_id), encoding='UTF-8')
out_file = open('./label_txt\%s.txt' % (image_id), 'w') # 生成txt格式文件
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
for obj in root.iter('object'):
cls = obj.find('name').text
if cls not in classes:
classes.append(cls) # 如果类别不存在,添加到classes列表中
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
bb = convert((w, h), b)
out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
xml_path = os.path.join(CURRENT_DIR, './label_xml/')
# xml list
img_xmls = os.listdir(xml_path)
for img_xml in img_xmls:
label_name = img_xml.split('.')[0]
print(label_name)
convert_annotation(label_name)
print("Classes:") # 打印最终的classes列表
print(classes) # 打印最终的classes列表
整理数据文件夹结构
我们需要将数据集整理为以下结构:
-----data
|-----train
| |-----images
| |-----labels
|
|-----valid
| |-----images
| |-----labels
|
|-----test
|-----images
|-----labels
确保以下几点:
所有的训练图片都位于data/train/images目录下,相应的标注文件位于data/train/labels目录下。
所有的验证图片都位于data/valid/images目录下,相应的标注文件位于data/valid/labels目录下。
所有的测试图片都位于data/test/images目录下,相应的标注文件位于data/test/labels目录下。
这样的结构使得数据的管理和模型的训练、验证和测试变得非常方便。
模型训练
Epoch gpu_mem box obj cls labels img_size
1/200 20.8G 0.01576 0.01955 0.007536 22 1280: 100%|██████████| 849/849 [14:42<00:00, 1.04s/it]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:14<00:00, 2.87it/s]
all 3395 17314 0.994 0.957 0.0957 0.0843
Epoch gpu_mem box obj cls labels img_size
2/200 20.8G 0.01578 0.01923 0.007006 22 1280: 100%|██████████| 849/849 [14:44<00:00, 1.04s/it]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:12<00:00, 2.95it/s]
all 3395 17314 0.996 0.956 0.0957 0.0845
Epoch gpu_mem box obj cls labels img_size
3/200 20.8G 0.01561 0.0191 0.006895 27 1280: 100%|██████████| 849/849 [10:56<00:00, 1.29it/s]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|███████ | 187/213 [00:52<00:00, 4.04it/s]
all 3395 17314 0.996 0.957 0.0957 0.0845
5.核心代码讲解
5.1 engine_finetune.py
class Trainer:
def __init__(self, model, criterion, data_loader, optimizer, device, epoch, loss_scaler, max_norm, model_ema, mixup_fn, log_writer, args):
self.model = model
self.criterion = criterion
self.data_loader = data_loader
self.optimizer = optimizer
self.device = device
self.epoch = epoch
self.loss_scaler = loss_scaler
self.max_norm = max_norm
self.model_ema = model_ema
self.mixup_fn = mixup_fn
self.log_writer = log_writer
self.args = args
def train_one_epoch(self):
self.model.train(True)
metric_logger = utils.MetricLogger(delimiter=" ")
metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))
header = 'Epoch: [{}]'.format(self.epoch)
print_freq = 20
update_freq = self.args.update_freq
use_amp = self.args.use_amp
self.optimizer.zero_grad()
for data_iter_step, (samples, targets) in enumerate(metric_logger.log_every(self.data_loader, print_freq, header)):
# we use a per iteration (instead of per epoch) lr scheduler
if data_iter_step % update_freq == 0:
adjust_learning_rate(self.optimizer, data_iter_step / len(self.data_loader) + self.epoch, self.args)
samples = samples.to(self.device, non_blocking=True)
targets = targets.to(self.device, non_blocking=True)
if self.mixup_fn is not None:
samples, targets = self.mixup_fn(samples, targets)
if use_amp:
with torch.cuda.amp.autocast():
output = self.model(samples)
loss = self.criterion(output, targets)
else: # full precision
output = self.model(samples)
loss = self.criterion(output, targets)
loss_value = loss.item()
if not math.isfinite(loss_value):
print("Loss is {}, stopping training".format(loss_value))
assert math.isfinite(loss_value)
if use_amp:
# this attribute is added by timm on one optimizer (adahessian)
is_second_order = hasattr(self.optimizer, 'is_second_order') and self.optimizer.is_second_order
loss /= update_freq
grad_norm = self.loss_scaler(loss, self.optimizer, clip_grad=self.max_norm,
parameters=self.model.parameters(), create_graph=is_second_order,
update_grad=(data_iter_step + 1) % update_freq == 0)
if (data_iter_step + 1) % update_freq == 0:
self.optimizer.zero_grad()
if self.model_ema is not None:
self.model_ema.update(self.model)
else: # full precision
loss /= update_freq
loss.backward()
if (data_iter_step + 1) % update_freq == 0:
self.optimizer.step()
self.optimizer.zero_grad()
if self.model_ema is not None:
self.model_ema.update(self.model)
torch.cuda.synchronize()
if self.mixup_fn is None:
class_acc = (output.max(-1)[-1] == targets).float().mean()
else:
class_acc = None
metric_logger.update(loss=loss_value)
metric_logger.update(class_acc=class_acc)
min_lr = 10.
max_lr = 0.
for group in self.optimizer.param_groups:
min_lr = min(min_lr, group["lr"])
max_lr = max(max_lr, group["lr"])
metric_logger.update(lr=max_lr)
metric_logger.update(min_lr=min_lr)
weight_decay_value = None
for group in self.optimizer.param_groups:
if group["weight_decay"] > 0:
weight_decay_value = group["weight_decay"]
metric_logger.update(weight_decay=weight_decay_value)
if use_amp:
metric_logger.update(grad_norm=grad_norm)
if self.log_writer is not None:
self.log_writer.update(loss=loss_value, head="loss")
self.log_writer.update(class_acc=class_acc, head="loss")
self.log_writer.update(lr=max_lr, head="opt")
self.log_writer.update(min_lr=min_lr, head="opt")
self.log_writer.update(weight_decay=weight_decay_value, head="opt")
if use_amp:
self.log_writer.update(grad_norm=grad_norm, head="opt")
self.log_writer.set_step()
# gather the stats from all processes
metric_logger.synchronize_between_processes()
print("Averaged stats:", metric_logger)
return {k: meter.global_avg for k, meter in metric_logger.meters.items()}
@torch.no_grad()
def evaluate(self):
criterion = torch.nn.CrossEntropyLoss()
metric_logger = utils.MetricLogger(delimiter=" ")
header = 'Test:'
# switch to evaluation mode
self.model.eval()
for batch in metric_logger.log_every(self.data_loader, 10, header):
images = batch[0]
target = batch[-1]
images = images.to(self.device, non_blocking=True)
target = target.to(self.device, non_blocking=True)
# compute output
if use_amp:
with torch.cuda.amp.autocast():
output = self.model(images)
if isinstance(output, dict):
output = output['logits']
loss = criterion(output, target)
else:
output = self.model(images)
if isinstance(output, dict):
output = output['logits']
loss = criterion(output, target)
torch.cuda.synchronize()
acc1, acc5 = accuracy(output, target, topk=(1, 5))
batch_size = images.shape[0]
metric_logger.update(loss=loss.item())
metric_logger.meters['acc1'].update(acc1.item(), n=batch_size)
metric_logger.meters['acc5'].update(acc5.item(), n=batch_size)
# gather the stats from all processes
metric_logger.synchronize_between_processes()
print('* Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f} loss {losses.global_avg:.3f}'
.format(top1=metric_logger.acc1, top5=metric_logger.acc5, losses=metric_logger.loss))
return {k: meter.global_avg for k, meter in metric_logger.meters.items()}
这个程序文件是用于进行模型微调的。它包含了两个主要的函数:train_one_epoch
和evaluate
。
train_one_epoch
函数用于训练一个epoch的模型。它接受模型、损失函数、数据加载器、优化器、设备、当前epoch等参数。在每个数据迭代步骤中,函数会根据当前的迭代步骤和总迭代步骤数来调整学习率。然后,它将样本和目标数据移动到设备上,并根据需要进行Mixup操作。接下来,根据是否使用混合精度训练,计算模型的输出和损失。如果损失值不是有限的,训练将停止。然后,根据是否使用混合精度训练,计算梯度并更新模型参数。最后,记录一些指标,如损失、准确率、学习率等,并将它们返回。
evaluate
函数用于在测试集上评估模型的性能。它接受数据加载器、模型、设备和是否使用混合精度训练等参数。在每个批次中,函数将图像和目标数据移动到设备上,并计算模型的输出和损失。然后,计算模型的准确率,并记录一些指标,如损失、准确率等。最后,将这些指标返回。
整个程序文件还包含了一些辅助函数和导入的模块,用于调整学习率、计算准确率等。
5.2 engine_pretrain.py
封装为类后的代码如下:
class Trainer:
def __init__(self, model, optimizer, device, loss_scaler, args=None):
self.model = model
self.optimizer = optimizer
self.device = device
self.loss_scaler = loss_scaler
self.args = args
def train_one_epoch(self, data_loader, epoch, log_writer=None):
self.model.train(True)
metric_logger = utils.MetricLogger(delimiter=" ")
metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))
header = 'Epoch: [{}]'.format(epoch)
print_freq = 20
update_freq = self.args.update_freq
self.optimizer.zero_grad()
for data_iter_step, (samples, labels) in enumerate(metric_logger.log_every(data_loader, print_freq, header)):
# we use a per iteration (instead of per epoch) lr scheduler
if data_iter_step % update_freq == 0:
utils.adjust_learning_rate(self.optimizer, data_iter_step / len(data_loader) + epoch, self.args)
if not isinstance(samples, list):
samples = samples.to(self.device, non_blocking=True)
labels = labels.to(self.device, non_blocking=True)
loss, _, _ = self.model(samples, labels, mask_ratio=self.args.mask_ratio)
loss_value = loss.item()
if not math.isfinite(loss_value):
print("Loss is {}, stopping training".format(loss_value))
sys.exit(1)
loss /= update_freq
self.loss_scaler(loss, self.optimizer, parameters=self.model.parameters(),
update_grad=(data_iter_step + 1) % update_freq == 0)
if (data_iter_step + 1) % update_freq == 0:
self.optimizer.zero_grad()
torch.cuda.empty_cache() # clear the GPU cache at a regular interval for training ME network
metric_logger.update(loss=loss_value)
lr = self.optimizer.param_groups[0]["lr"]
metric_logger.update(lr=lr)
loss_value_reduce = utils.all_reduce_mean(loss_value)
if log_writer is not None and (data_iter_step + 1) % update_freq == 0:
""" We use epoch_1000x as the x-axis in tensorboard.
This calibrates different curves when batch size changes.
"""
epoch_1000x = int((data_iter_step / len(data_loader) + epoch) * 1000)
log_writer.update(train_loss=loss_value_reduce, head="loss", step=epoch_1000x)
log_writer.update(lr=lr, head="opt", step=epoch_1000x)
metric_logger.synchronize_between_processes()
print("Averaged stats:", metric_logger)
return {k: meter.global_avg for k, meter in metric_logger.meters.items()}
这样,你可以通过创建一个Trainer
对象,并调用其train_one_epoch
方法来训练模型的一个epoch。
这个程序文件名为engine_pretrain.py,它是一个用于训练模型的脚本。该脚本包含了一个名为train_one_epoch的函数,用于训练一个epoch的数据。该函数接受模型、数据加载器、优化器、设备、当前epoch等参数,并返回训练过程中的统计指标。
在train_one_epoch函数中,首先将模型设置为训练模式,并初始化一个MetricLogger对象用于记录训练过程中的指标。然后设置打印频率和更新频率,并将优化器的梯度清零。
接下来,通过迭代数据加载器中的数据,进行训练。在每个数据迭代步骤中,根据更新频率调整学习率,并将样本和标签移动到指定的设备上。然后使用模型对样本和标签进行前向传播,并计算损失值。
如果损失值不是有限的(finite),则输出错误信息并停止训练。否则,将损失值除以更新频率,并使用损失缩放器(loss_scaler)对模型的参数进行更新。如果达到了更新频率,将优化器的梯度清零,并清除GPU缓存。
在训练过程中,更新MetricLogger对象中的损失值和学习率,并将损失值进行平均汇总。如果提供了日志写入器(log_writer),则将损失值和学习率写入日志。
最后,同步MetricLogger对象的统计指标,并返回全局平均值。
总之,这个程序文件是一个用于训练模型的脚本,其中的train_one_epoch函数用于训练一个epoch的数据,并记录训练过程中的指标。
5.3 model.py
import torch
import timm
from thop import clever_format, profile
class ModelProfiler:
def __init__(self, model_name, input_shape):
self.model_name = model_name
self.input_shape = input_shape
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model = None
def load_model(self):
self.model = timm.create_model(self.model_name, pretrained=False, features_only=True)
self.model.to(self.device)
self.model.eval()
def print_model_info(self):
print(self.model.feature_info.channels())
for feature in self.model(self.dummy_input):
print(feature.size())
def profile_model(self):
flops, params = profile(self.model.to(self.device), (self.dummy_input,), verbose=False)
flops, params = clever_format([flops * 2, params], "%.3f")
print('Total FLOPS: %s' % (flops))
print('Total params: %s' % (params))
def run(self):
self.load_model()
self.print_model_info()
self.profile_model()
model_name = 'convnext_base'
input_shape = (1, 3, 640, 640)
profiler = ModelProfiler(model_name, input_shape)
profiler.run()
这个程序文件名为model.py,主要功能是使用timm库中的模型来进行推理和计算模型的FLOPS和参数数量。
首先,程序会导入torch、timm和thop库。然后,使用timm.list_models()函数列出所有可用的模型,并打印出来。
接下来,程序会判断当前是否有可用的GPU,如果有则使用cuda设备,否则使用cpu设备。然后,创建一个大小为(1, 3, 640, 640)的随机输入张量dummy_input,并将其移动到设备上。
接着,程序会使用timm.create_model()函数创建一个名为’convnext_base’的模型,pretrained参数设置为False,features_only参数设置为True。然后,将模型移动到设备上,并设置为评估模式。
接下来,程序会打印出模型的特征通道数(model.feature_info.channels())和每个特征的大小(model(dummy_input))。
最后,程序会使用thop库的profile函数计算模型的FLOPS和参数数量,并使用clever_format函数将其格式化为字符串。然后,打印出总的FLOPS和参数数量。
总结起来,这个程序主要是用来展示如何使用timm库中的模型进行推理,并计算模型的FLOPS和参数数量。
5.4 optim_factory.py
try:
from apex.optimizers import FusedNovoGrad, FusedAdam, FusedLAMB, FusedSGD
has_apex = True
except ImportError:
has_apex = False
class LayerDecayValueAssigner(object):
def __init__(self, values, depths=[3,3,27,3], layer_decay_type='single'):
self.values = values
self.depths = depths
self.layer_decay_type = layer_decay_type
def get_scale(self, layer_id):
return self.values[layer_id]
def get_layer_id(self, var_name):
if self.layer_decay_type == 'single':
return get_num_layer_for_convnext_single(var_name, self.depths)
else:
return get_num_layer_for_convnext(var_name)
def get_parameter_groups(model, weight_decay=1e-5, skip_list=(), get_num_layer=None, get_layer_scale=None):
parameter_group_names = {}
parameter_group_vars = {}
for name, param in model.named_parameters():
if not param.requires_grad:
continue # frozen weights
if len(param.shape) == 1 or name.endswith(".bias") or name in skip_list or \
name.endswith(".gamma") or name.endswith(".beta"):
group_name = "no_decay"
this_weight_decay = 0.
else:
group_name = "decay"
this_weight_decay = weight_decay
if get_num_layer is not None:
layer_id = get_num_layer(name)
group_name = "layer_%d_%s" % (layer_id, group_name)
else:
layer_id = None
if group_name not in parameter_group_names:
if get_layer_scale is not None:
scale = get_layer_scale(layer_id)
else:
scale = 1.
parameter_group_names[group_name] = {
"weight_decay": this_weight_decay,
"params": [],
"lr_scale": scale
}
parameter_group_vars[group_name] = {
"weight_decay": this_weight_decay,
"params": [],
"lr_scale": scale
}
parameter_group_vars[group_name]["params"].append(param)
parameter_group_names[group_name]["params"].append(name)
print("Param groups = %s" % json.dumps(parameter_group_names, indent=2))
return list(parameter_group_vars.values())
def create_optimizer(args, model, get_num_layer=None, get_layer_scale=None, filter_bias_and_bn=True, skip_list=None):
opt_lower = args.opt.lower()
weight_decay = args.weight_decay
# if weight_decay and filter_bias_and_bn:
if filter_bias_and_bn:
skip = {}
if skip_list is not None:
skip = skip_list
elif hasattr(model, 'no_weight_decay'):
skip = model.no_weight_decay()
parameters = get_parameter_groups(model, weight_decay, skip, get_num_layer, get_layer_scale)
weight_decay = 0.
else:
parameters = model.parameters()
if 'fused' in opt_lower:
assert has_apex and torch.cuda.is_available(), 'APEX and CUDA required for fused optimizers'
opt_args = dict(lr=args.lr, weight_decay=weight_decay)
if hasattr(args, 'opt_eps') and args.opt_eps is not None:
opt_args['eps'] = args.opt_eps
if hasattr(args, 'opt_betas') and args.opt_betas is not None:
opt_args['betas'] = args.opt_betas
opt_split = opt_lower.split('_')
opt_lower = opt_split[-1]
if opt_lower == 'sgd' or opt_lower == 'nesterov':
opt_args.pop('eps', None)
optimizer = optim.SGD(parameters, momentum=args.momentum, nesterov=True, **opt_args)
elif opt_lower == 'momentum':
opt_args.pop('eps', None)
optimizer = optim.SGD(parameters, momentum=args.momentum, nesterov=False, **opt_args)
elif opt_lower == 'adam':
optimizer = optim.Adam(parameters, **opt_args)
elif opt_lower == 'adamw':
optimizer = optim.AdamW(parameters, **opt_args)
elif opt_lower == 'nadam':
optimizer = Nadam(parameters, **opt_args)
elif opt_lower == 'radam':
optimizer = RAdam(parameters, **opt_args)
elif opt_lower == 'adamp':
optimizer = AdamP(parameters, wd_ratio=0.01, nesterov=True, **opt_args)
elif opt_lower == 'sgdp':
optimizer = SGDP(parameters, momentum=args.momentum, nesterov=True, **opt_args)
elif opt_lower == 'adadelta':
optimizer = optim.Adadelta(parameters, **opt_args)
elif opt_lower == 'adafactor':
if not args.lr:
opt_args['lr'] = None
optimizer = Adafactor(parameters, **opt_args)
elif opt_lower == 'adahessian':
optimizer = Adahessian(parameters, **opt_args)
elif opt_lower == 'rmsprop':
optimizer = optim.RMSprop(parameters, alpha=0.9, momentum=args.momentum, **opt_args)
elif opt_lower == 'rmsproptf':
optimizer = RMSpropTF(parameters, alpha=0.9, momentum=args.momentum, **opt_args)
elif opt_lower == 'novograd':
optimizer = NovoGrad(parameters, **opt_args)
elif opt_lower == 'nvnovograd':
optimizer = NvNovoGrad(parameters, **opt_args)
elif opt_lower == 'fusedsgd':
opt_args.pop('eps', None)
optimizer = FusedSGD(parameters, momentum=args.momentum, nesterov=True, **opt_args)
elif opt_lower == 'fusedmomentum':
opt_args.pop('eps', None)
optimizer = FusedSGD(parameters, momentum=args.momentum, nesterov=False, **opt_args)
elif opt_lower == 'fusedadam':
optimizer = FusedAdam(parameters, adam_w_mode=False, **opt_args)
elif opt_lower == 'fusedadamw':
optimizer = FusedAdam(parameters, adam_w_mode=True, **opt_args)
elif opt_lower == 'fusedlamb':
optimizer = FusedLAMB(parameters, **opt_args)
elif opt_lower == 'fusednovograd':
opt_args.setdefault('betas', (0.95, 0.98))
optimizer = FusedNovoGrad(parameters, **opt_args)
else:
assert False and "Invalid optimizer"
if len(opt_split) > 1:
if opt_split[0] == 'lookahead':
optimizer = Lookahead(optimizer)
return optimizer
这个程序文件是一个优化器工厂,用于创建不同类型的优化器。它导入了一些torch和timm库中的优化器类,并定义了一些辅助函数和类来帮助创建优化器。
主要函数和类包括:
get_num_layer_for_convnext_single
:根据变量名和深度列表计算卷积层的层级ID。get_num_layer_for_convnext
:将卷积层分成12个组,每个组包含三个连续的块,包括可能的相邻下采样层。LayerDecayValueAssigner
:根据给定的值、深度和层级衰减类型,为每个层级分配一个衰减值。get_parameter_groups
:根据模型、权重衰减、跳过列表和获取层级ID和衰减值的函数,将模型参数分组为不同的参数组。create_optimizer
:根据给定的参数和模型,使用不同的优化器类创建优化器。
这个程序文件还包含一些导入语句和一些全局变量的定义。
5.5 submitit_finetune.py
import argparse
import os
import uuid
from pathlib import Path
import main_finetune as trainer
import submitit
class Trainer(object):
def __init__(self, args):
self.args = args
def __call__(self):
import main_finetune as trainer
self._setup_gpu_args()
trainer.main(self.args)
def checkpoint(self):
import os
import submitit
self.args.dist_url = get_init_file().as_uri()
checkpoint_file = os.path.join(self.args.output_dir, "checkpoint.pth")
if os.path.exists(checkpoint_file):
self.args.resume = checkpoint_file
print("Requeuing ", self.args)
empty_trainer = type(self)(self.args)
return submitit.helpers.DelayedSubmission(empty_trainer)
def _setup_gpu_args(self):
import submitit
from pathlib import Path
job_env = submitit.JobEnvironment()
self.args.output_dir = Path(str(self.args.output_dir).replace("%j", str(job_env.job_id)))
self.args.log_dir = self.args.output_dir
self.args.gpu = job_env.local_rank
self.args.rank = job_env.global_rank
self.args.world_size = job_env.num_tasks
print(f"Process group: {job_env.num_tasks} tasks, rank: {job_env.global_rank}")
def main():
args = parse_args()
if args.job_dir == "":
args.job_dir = get_shared_folder() / "%j"
# Note that the folder will depend on the job_id, to easily track experiments
executor = submitit.AutoExecutor(folder=args.job_dir, slurm_max_num_timeout=30)
num_gpus_per_node = args.ngpus
nodes = args.nodes
timeout_min = args.timeout
partition = args.partition
kwargs = {}
if args.use_volta32:
kwargs['slurm_constraint'] = 'volta32gb'
if args.comment:
kwargs['slurm_comment'] = args.comment
executor.update_parameters(
mem_gb=40 * num_gpus_per_node,
gpus_per_node=num_gpus_per_node,
tasks_per_node=num_gpus_per_node, # one task per GPU
cpus_per_task=10,
nodes=nodes,
timeout_min=timeout_min, # max is 60 * 72
# Below are cluster dependent parameters
slurm_partition=partition,
slurm_signal_delay_s=120,
**kwargs
)
executor.update_parameters(name="finetune")
args.dist_url = get_init_file().as_uri()
args.output_dir = args.job_dir
trainer = Trainer(args)
job = executor.submit(trainer)
# print("Submitted job_id:", job.job_id)
print(job.job_id)
if __name__ == "__main__":
main()
这个程序文件是一个用于提交任务的脚本,文件名为submitit_finetune.py。它的主要功能是通过调用main_finetune.py中的训练函数来进行模型微调。以下是该程序文件的主要组成部分:
-
导入必要的库和模块:argparse、os、uuid、pathlib、main_finetune、submitit等。
-
定义了一个parse_args()函数,用于解析命令行参数。
-
定义了一个get_shared_folder()函数,用于获取共享文件夹的路径。
-
定义了一个get_init_file()函数,用于获取初始化文件的路径。
-
定义了一个Trainer类,用于执行训练任务。该类的__init__()方法接收参数args,并将其保存为实例变量。call()方法用于调用main_finetune.py中的训练函数。checkpoint()方法用于设置检查点,并返回一个DelayedSubmission对象。_setup_gpu_args()方法用于设置GPU相关的参数。
-
定义了一个main()函数,用于执行整个程序的逻辑。在该函数中,首先解析命令行参数,然后根据参数设置执行器的参数。接着创建一个Trainer对象,并使用执行器提交任务。
-
最后,通过判断__name__是否为"main"来执行main()函数。
总体来说,这个程序文件是一个用于提交模型微调任务的脚本,它通过调用main_finetune.py中的训练函数来执行任务,并使用submitit库来管理任务的提交和执行。
6.系统整体结构
整体功能和构架概述:
该程序是一个用于骨干网络ConvNeXtV2的改进YOLOv5的遥感图像目标检测系统。它包含了多个文件,每个文件都有不同的功能,用于实现整个系统的不同部分。
以下是每个文件的功能的整理:
文件名 | 功能 |
---|---|
datasets.py | 提供数据集的加载和预处理功能 |
engine_finetune.py | 定义了微调训练引擎的功能 |
engine_pretrain.py | 定义了预训练训练引擎的功能 |
main_finetune.py | 主函数,用于微调训练的入口 |
main_pretrain.py | 主函数,用于预训练训练的入口 |
model.py | 定义了模型的结构和参数计算功能 |
optim_factory.py | 定义了优化器的创建和参数分组功能 |
submitit_finetune.py | 提交微调训练任务的脚本 |
submitit_pretrain.py | 提交预训练训练任务的脚本 |
train.py | 定义了训练过程的函数 |
ui.py | 用户界面相关的功能 |
utils.py | 提供了一些通用的工具函数 |
yolo.py | 定义了YOLO模型的结构和相关函数 |
models/… | 包含了模型相关的文件 |
tools/… | 包含了一些工具函数和辅助脚本 |
utils/… | 包含了一些通用的工具函数和辅助脚本 |
以上是对每个文件功能的简要概括,具体的功能和实现细节可以参考每个文件的代码。
7.Yolov5简介
Yolo ( You Can Only Once)算法作为当下最流行的单阶段目标检测算法之一,它将检测问题转化为回归问题。通过一次检测,便可同时得出目标框和物体类别。与两阶段的算法不同,FasterRCNN 第一次通过 RPN 网络筛选出一些候选框,再从候选框里面进行检测。所以在时间上是,远远的超过单阶段算法。Yolo算法的思想是将一张图片分成dxd个网格,每个网格会有n个不同大小长宽比的先验框。当物体中心落到某个网格时,将会由该网格对这个物体进行检测,同时给出预测框的中心坐标位置、宽高、类别和置信度。
经过这些年的发展,Yolo已经发展到了第五个版本即 Yolov5。Yolov5共有Yolov5s、Yolov5m、Yolov5l和 Yolov5x 四个版本。Yolov5的四个版本的网络结构都是一样的,只是根据网络的深度和宽度不同来进行划分。其中 Yolov5s是四个模型里面最小的,同时检测速度也是最快的。我们选了检测速度最快的Yolov5s模型作为算法研究的基准。
Yolov5 网络结构如图1所示,主要分为输人端((Input ),骨干网络(Backbone),颈部网络( Neck)和检测层( Detect)四个部分。在输入端部分,对数据进行一些处理,包括 Mosaic数据增强,自适应锚框计算以及自适应图片缩放。Back-bone采用CSPDarknet53结构用来特征提取15l,Neck层主要使用特征融合部分,采用的是PAN结构,PAN在FPN的基础上进行修改,不仅有自下而上的特征融合,同时也有自上而下的特征融合,进一步提高模型的精度。最终的 Detect层,在大小不同的特征图上预测不同尺寸的目标。
8.改进YOLOv5的骨干网络
ConvNeXt V2骨干网络简介
在改进的架构和更好的表示学习О框架的推动下,视觉识别领域在21世纪20年代初实现了快速现代化和性能提升。例如,以
ConwNeXtf[2]为代表的现代ConvNets在各种场景中都表现出了强大的性能。虽然这些模型最初是为使用lmageNet标签的监督学习而设计的,但它们也可能受益于自监督学习技术,如蒙面自编码器(MAE)[3]。然而,我们发现,简单地结合这两种方法会导致性能不佳。在本文中,ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders
提出了一个全卷积掩码自编码器框架和一个新的全局响应归一化(GRN)层,可以添加到ConvNext架构中,以增强通道间的特征竞争。这种自我监督学习技术和架构改进的共同设计产生了一个名为ConvNext V2的新模型家族,它显著提高了纯ConvNets在各种识别基准上的性能,包括lmageNet分类、COCO检测和ADE20K分割。我们还提供各种大小的预训练ConvNext v2模型,从高效的3.7 m参数Atto模型,在lmageNet上具有76.7%的top-1精度,到仅使用公共训练数据实现最先进的88.9%精度的650M Huge模型。
Fully Convolutional Masked Autoencoder
我们的方法在概念上很简单,并以完全卷积的方式运行。学习信号是通过以高掩蔽率随机屏蔽原始输入视觉,并让模型在给定剩余上下文的情况下预测缺失部分来生成的。我们的框架如图2所示,现在我们将更详细地描述它的主要组件。
Masking.
我们使用掩码比为0.6的随机掩码策略。由于卷积模型具有分层设计,其中特征在不同阶段进行下采样,掩码在最后阶段生成,并递归上采样直至最佳分辨率。为了在实践中实现这一点,我们从原始输入图像中随机去除60%的32× 32块。我们使用最小的数据增强,只包括随机调整大小的裁剪。
Encoder design.
在我们的方法中我们使用ConvNeX[5]模型作为编码器。使蒙面图像建模有效的一个挑战是防止模型学习允许它从蒙面区域复制和粘贴信息的快捷方式。在基于变压器的模型中,这是相对容易防止的,它可以将可见的补丁作为编码器的唯一输入。然而,使用ConvNets实现这一点比较困难,因为必须保留2D图像结构。虽然朴素解决方案涉及在输入端引入可学习的掩码令牌[3.7],但这些方法降低了预训练的效率,并导致训练和测试时间不一致,因为在测试时没有掩码令牌。当掩蔽比很高时,这尤其成问题。
为了解决这个问题,我们的新见解是从“稀疏数据视角"来查看蒙面图像,这是受到了在3D任务中学习稀疏点云的启发[1,6]。我们的关键观察是,蒙面图像可以表示为一个二维稀疏像素阵列。基于这种见解,很自然地将稀疏卷积合并到我们的框架中,以促进掩码自动编码器的预训练。在实践中,在预训练期间,我们建议将编码器中的标准卷积层转换为子流形稀疏卷积,这使得模型只能在可见数据点上操作[1,7,8]。我们注意到,稀疏卷积层可以在微调阶段转换回标准卷积,而不需要额外的处理。作为一种替代方法,也可以在密集卷积运算前后应用二进制掩蔽运算。这种操作在数值上与稀疏卷积具有相同的效果,理论上计算量更大,但在TPU等AI加速器上更友好。
Decoder design.
我们使用一个轻量级的普通ConvNeXt块作为解码器。这在总体上形成了非对称编码器-解码器体系结构,因为编码器更重有等级制度。我们还考虑了更复杂的解码器,如分层解码器[(8,9]或变压器[1,3],但更简单的单一ConvNeXt块解码器在微调精度方面表现良好,并大大减少了预训练时间,如表1所示。我们将解码器的尺寸设置为512。
9.训练结果分析
硬件环境及数据处理
本文采用的实验环境如表1所示,数据集为西北工业大学遥感公开数据集 NWPU VHR-10 ,该数据是一个用于空间物体检测的10级地理遥感数据集,其拥有650张包含目标的图像和150张背景图像,共计800张,目标种类包括飞机、舰船、油罐、棒球场、网球场、篮球场、田径场、港口、桥梁和汽车共计10个类别。
评价指标
该results.csv文件包含 YOLOv5 模型训练过程中记录的详细指标。以下是 CSV 中提供的数据的细分:
epoch:训练纪元数。
train/box_loss:训练期间与边界框预测相关的损失。
train/obj_loss:训练期间的对象损失,衡量模型正确识别对象存在的能力。
train/cls_loss:训练期间的分类损失,指示模型将对象分类到正确类别的程度。
指标/精度:精度衡量阳性预测的准确性(真阳性数除以真阳性数和误报数)。
指标/召回率:召回率衡量模型在数据集中查找所有相关案例的能力(真阳性数除以真阳性数和假阴性数)。
metrics/mAP_0.5:IoU(并集交集)阈值为 0.5 时的平均精度。
metrics/mAP_0.5:0.95:在 0.5 到 0.95 的不同 IoU 阈值上平均的平均精度(这是 COCO 数据集评估中使用的标准指标)。
val/box_loss:边界框预测的验证损失。
val/obj_loss:对象性的验证损失。
val/cls_loss:分类的验证损失。
x/lr0、x/lr1、x/lr2:网络不同部分的学习率(可能是层组或不同的参数,如权重和偏差)。
分析内容
1.混乱矩阵分析-这将涉及查看模型在不同类别的预测表现,包括真积极、假积极、真阴性和假阴性。
2.性能曲线分析-我们将评估模型的F1分数、准确度(Precision)、召回率(Recall)和准确率-反应率(PR)曲线,以理解模型在不同阈值下的性能。
3.标签和相关性分析-通过查看标签的图片和相关性图,可以了解数据集中类别的分布和类别之间的可能关系。
4.实验结果的量化分析-查看实验结果的csv和png文件,了解模型的整体性能和其他相关的统计数据。
5.训练批次的样本分析-查看训练批次的样本图片,可以帮助我们了解模型过程中的数据样本。
import matplotlib.pyplot as plt
# Define a function to plot the training and validation losses
def plot_losses(df):
fig, ax1 = plt.subplots(figsize=(12, 7))
color = 'tab:red'
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Train Loss', color=color)
ax1.plot(df['epoch'], df['train/box_loss'], label='Train Box Loss', color='red', linestyle='--')
ax1.plot(df['epoch'], df['train/obj_loss'], label='Train Obj Loss', color='red', linestyle='-')
ax1.plot(df['epoch'], df['train/cls_loss'], label='Train Cls Loss', color='red', linestyle='-.')
ax1.tick_params(axis='y', labelcolor=color)
ax1.legend(loc='upper left')
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('Val Loss', color=color) # we already handled the x-label with ax1
ax2.plot(df['epoch'], df['val/box_loss'], label='Val Box Loss', color='blue', linestyle='--')
ax2.plot(df['epoch'], df['val/obj_loss'], label='Val Obj Loss', color='blue', linestyle='-')
ax2.plot(df['epoch'], df['val/cls_loss'], label='Val Cls Loss', color='blue', linestyle='-.')
ax2.tick_params(axis='y', labelcolor=color)
ax2.legend(loc='upper right')
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.title('Training and Validation Losses Over Epochs')
plt.show()
# Strip the whitespace from the column names
results_df.columns = results_df.columns.str.strip()
# Now let's try plotting the losses again
plot_losses(results_df)
现在我们已经成功绘制了训练和验证损失,我们可以观察到以下趋势:
训练损失:
训练的框损失(红色虚线)一开始很高,然后随着时间的推移逐渐减少,这表明模型在预测边界框的位置和大小方面变得越来越好。
训练的对象性损失(红色实线)也减少了,这表明模型检测边界框中对象的能力有所提高。
训练的分类损失(红色点划线)遵循类似的趋势,随着模型学会更准确地对对象进行分类,分类损失会减少。
验证损失:
验证的框损失(蓝色虚线)显示了模型在边界框预测方面对未见数据的泛化程度。它减少了,这是一个好兆头,但我们应该寻找任何与训练损失背离的迹象,这可能表明过度拟合。
验证的客观性损失(蓝色实线)和验证的分类损失(蓝色点划线)应该理想地反映训练损失。如果它们开始显着增加或偏离,而训练损失继续减少,则可能是过度拟合的迹象。
接下来,我们应该检查其他指标,例如精度、召回率和 mAP,以更好地了解模型的性能。这些指标将使我们深入了解误报、漏报以及不同类别的模型的整体准确性之间的平衡。
让我们继续绘制历元内的精度、召回率和平均精度 (mAP) 。
# Define a function to plot precision, recall, and mAP
def plot_performance_metrics(df):
epochs = df['epoch']
fig, ax1 = plt.subplots(figsize=(12, 7))
color = 'tab:green'
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Precision & Recall', color=color)
ax1.plot(epochs, df['metrics/precision'], label='Precision', color='green', linestyle='-')
ax1.plot(epochs, df['metrics/recall'], label='Recall', color='green', linestyle='--')
ax1.tick_params(axis='y', labelcolor=color)
ax1.legend(loc='upper left')
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:purple'
ax2.set_ylabel('mAP', color=color) # we already handled the x-label with ax1
ax2.plot(epochs, df['metrics/mAP_0.5'], label='mAP @0.5', color='purple', linestyle='-')
ax2.plot(epochs, df['metrics/mAP_0.5:0.95'], label='mAP @0.5:0.95', color='purple', linestyle='--')
ax2.tick_params(axis='y', labelcolor=color)
ax2.legend(loc='upper right')
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.title('Precision, Recall, and mAP Over Epochs')
plt.show()
# Plotting the precision, recall, and mAP
plot_performance_metrics(results_df)
历元内的精度、召回率和平均精度 (mAP) 的绘图提供了有关模型性能的重要见解:
Precision:它在初始时期显着增加,然后逐渐增加,表明模型正确识别正样本的能力不断提高。精度高意味着模型误报率低。
召回率:召回率也增加,表明模型在检测所有相关案例(真阳性)方面越来越好。召回率的增加意味着模型的假阴性率较低。
mAP @0.5:这是 IoU(交并集)阈值为 0.5 时的平均精度。它在开始时急剧上升,然后趋于平稳,表明该模型始终正确地检测对象,并与地面真实边界框有适当的重叠。
mAP @0.5:0.95:这是在 IoU 阈值 0.5 到 0.95 上平均的平均精度。该指标更为严格,因为它对一系列 IoU 阈值内的性能进行平均。该曲线遵循与 mAP @0.5 类似的模式,但绝对值较低,这是预期的,因为它是一个更严格的指标。
从这些观察中,我们可以推断该模型正在跨时期有效地学习。精确度和召回率之间的平衡对于理解模型的趋势至关重要:较高的精确度表明模型在预测正值方面比较保守,而较高的召回率表明模型倾向于捕获尽可能多的正值。
在理想的情况下,准确率和召回率都会很高,但通常,改进其中一个可能会以牺牲另一个为代价。这种权衡在遥感应用中尤其重要,因为误报和漏报都会产生严重后果。
为了完成分析,我们还应该查看混淆矩阵、精确召回曲线和相关图。这些将提供关于模型在每个单独类别中的表现如何以及这些类别如何影响彼此的表现的见解。
我们可以看到混合矩阵的图像,但为了进行详细分析,我们需要理解图像中的数据。混合矩阵显示了模型在各个类别上的预测结果与真实标签之间的关系。通常,矩阵的行代表真实类别,而列代表预测类别。
接下来,我们将查看其他的性能曲线图。
这是F1分数圆形,它结合了精确度(Precision)和召回率(Recall)的信息,是评估模型性能的重要指标。F1分数的范围是0到1,1表示完美的精确度和召回率。
对于遥感图像目标检测系统而言,一个高的F1分数意味着系统能够准确且全面地检测到图像中的目标。F1分数特别适合于数据集中正负图像样本分布不均的情况,这在遥感图像中是常见的,因为特定类型的对象可能比其他对象稀少。
我们现在将继续查看精确度曲线。
精确度曲线显示了模型在不同阈值下的精确度表现。精确度是指在所有被模型预测为正类的样本中,实际上为正类的比例。在检测目标任务中,一个高精确度意味着着模型产生的假积极(误报),换句话说,当模型声称找到某个目标时,它很可能是正确的。
对于遥感分析图像,准确度尤为重要,因为误报的目标可能会导致资源的浪费或者在某些应用(如救灾或安全监控)中的错误响应。
接下来,我们将查看收缩率曲线。
反应率曲线显示了模型在不同阈值下的反应率表现。反应率(也称为真实率)是指在所有实际正类样本中,被模型正确预测为正类的比例。
在目标检测系统中,认知率反映了模型捕获到的相关实例的能力。对于遥感图像,一个高认知率意味着系统能够检测到大多数的真实目标,这可以确保不遗漏任何重要信息(例如在搜救作业或土地覆盖分析中)是非常关键的。
现在,让我们看看准确度-反应率(PR)曲线,它结合了准确度和反应率的信息。
准确率-反应率(PR)曲线显示了在不同反应率水平下的准确率表现。PR曲线是一个很好的工具,用于评估在不同的阈值设置下,模型的性能如何随着反应率的变化增加而变化。理想情况下,我们希望曲线接近右上角,这意味着同时获得高准确率和高反应率。
对于遥感目标检测系统来说,由于目标的尺寸、形状和外观可能在不同的图像之间存在很大的变化,同时保持高准确率和高识别率是一个挑战。因此,图像PR曲线是评价模型多样性背景下如何平衡准确率和召回率的重要指标。
接下来,我们需要查看标签图片和标签相关性图,了解数据集的类别分布和类别之间的相关性。
这是数据集中的标签图像,它应该显示用于训练遥感图像目标检测系统的不同类别的标签。从这个图像中,我们可以看到目标类别的样本,这有助于理解需要识别的模型的目标类型。
为了深入了解,我们现在将查看标签相关性图,了解不同类别之间的关系。这有助于我们了解模型可能需要学习的类别间关系,某些类别经常是否一起出现,或者它们之间是否存在存在某种可预测的空间或上下文关系。
标签相关性图(或相关图)显示了数据集中不同类别标签之间的关系。在这种图表中,通常通过颜色深浅来显示变量间的相关性强度,颜色越深表示关系越强。在遥感中在图像目标检测的背景下,这种图表有助于识别哪些类别经常一起出现,这可能指示出现在相同的遥感图像场景中的物体或现象。
10.系统整合
参考博客《基于骨干网络ConvNeXtV2的改进YOLOv5的遥感图像目标检测系统》
11.参考文献
[1]廖育荣,王海宁,林存宝,等.基于深度学习的光学遥感图像目标检测研究进展[J].通信学报.2022,43(5).DOI:10.11959/j.issn.1000−436x.2022071 .
[2]王云艳,罗帅,王子健.基于改进MobileNetV3的遥感目标检测[J].陕西科技大学学报.2022,40(3).DOI:10.3969/j.issn.1000-5811.2022.03.024 .
[3]秦伟伟,宋泰年,刘洁瑜,等.基于轻量化YOLOv3的遥感军事目标检测算法[J].计算机工程与应用.2021,(21).DOI:10.3778/j.issn.1002-8331.2106-0026 .
[4]董丽君,曾志高,易胜秋,等.基于YOLOv5的遥感图像目标检测[J].湖南工业大学学报.2022,36(3).DOI:10.3969/j.issn.1673-9833.2022.03.007 .
[5]Ma, Lei,Liu, Yu,Zhang, Xueliang,等.Deep learning in remote sensing applications: A meta-analysis and review[J].ISPRS journal of photogrammetry and remote sensing.2019,152(Jun.).166-177.DOI:10.1016/j.isprsjprs.2019.04.015 .
[6]Young‐Jin Cha,Wooram Choi,Oral Büyüköztürk.Deep Learning‐Based Crack Damage Detection Using Convolutional Neural Networks[J].Computer-Aided Civil & Infrastructure Engineering.2017,32(5).361-378.DOI:10.1111/mice.12263 .
[7]Ren, Shaoqing,He, Kaiming,Girshick, Ross,等.Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks[J].IEEE Transactions on Pattern Analysis and Machine Intelligence.2017,39(6).1137-1149.DOI:10.1109/TPAMI.2016.2577031 .
[8]Cheng, Gong,Han, Junwei,Zhou, Peicheng,等.Multi-class geospatial object detection and geographic image classification based on collection of part detectors[J].ISPRS journal of photogrammetry and remote sensing.2014,98(Dec.).119-132.DOI:10.1016/j.isprsjprs.2014.10.002 .
[9]Girshick, R.,Donahue, J.,Darrell, T.,等.Rich Feature Hierarchies for Accurate Object Detection and Semantic Segmentation[C].