【YOLO改进】换遍IoU损失函数之Innersiou Loss(基于MMYOLO)

Innersiou损失函数

设计原理

IoU(Intersection over Union)是用于评估物体检测算法性能的常用指标,它衡量了预测边界框和真实边界框之间的重叠程度。然而,IoU存在一些局限性,例如在处理高度重叠的物体时,IoU难以提供足够的区分度。为了解决这个问题,Inner SIoU(Inner Section over Union)被提出作为一种改进的损失函数,用于训练物体检测模型。

一、IoU的局限性

IoU是通过计算两个边界框的交集面积除以它们的并集面积来衡量它们的重叠程度。但是,当两个边界框高度重叠时,IoU可能无法提供足够的区分度,导致难以准确地评估物体检测算法的性能。

二、Innersiou的引入

Inner SIoU是一种改进的损失函数,它通过考虑边界框内部区域的重叠情况来提高重叠边界框的区分度。Inner SIoU的计算方式如下:

  1. 首先,计算两个边界框的交集面积。
  2. 然后,计算第一个边界框的面积。
  3. 最后,将交集面积除以第一个边界框的面积得到Inner SIoU。

计算步骤

假设有两个边界框A和B,它们的坐标分别为(x1, y1, x2, y2)和(x3, y3, x4, y4)。则Inner SIoU的计算步骤如下:

  1. 计算两个边界框的交集面积:

    • 交集区域的左上角坐标为(max(x1, x3), max(y1, y3))。
    • 交集区域的右下角坐标为(min(x2, x4), min(y2, y4))。
    • 交集区域的宽度为max(0, right - left),高度为max(0, bottom - top)。
    • 交集面积为交集区域的宽度乘以高度。
  2. 计算第一个边界框的面积:

    • 第一个边界框的宽度为x2 - x1,高度为y2 - y1。
    • 第一个边界框的面积为宽度乘以高度。
  3. 计算Inner SIoU:

    • Inner SIoU为交集面积除以第一个边界框的面积。

使用PyTorch实现Inner SIoU计算的源代码

import torch

def calculate_inner_siou(box1, box2):
    # box1: [x1, y1, x2, y2]
    # box2: [x3, y3, x4, y4]

    # 计算交集的左上角和右下角坐标
    inter_left_top = torch.stack((torch.maximum(box1[0], box2[0]), torch.maximum(box1[1], box2[1])), dim=0)
    inter_right_bottom = torch.stack((torch.minimum(box1[2], box2[2]), torch.minimum(box1[3], box2[3])), dim=0)

    # 计算交集的宽度和高度
    inter_width = torch.maximum(inter_right_bottom[0] - inter_left_top[0], torch.tensor(0.))
    inter_height = torch.maximum(inter_right_bottom[1] - inter_left_top[1], torch.tensor(0.))

    # 计算交集面积和第一个边界框的面积
    inter_area = inter_width * inter_height
    box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])

    # 计算Inner SIoU
    inner_siou = inter_area / box1_area

    return inner_siou

# 示例使用
box1 = torch.tensor([0., 0., 5., 5.])  # [x1, y1, x2, y2]
box2 = torch.tensor([2., 2., 7., 7.])  # [x3, y3, x4, y4]

inner_siou = calculate_inner_siou(box1, box2)
print(inner_siou.item())  # 打印Inner SIoU值

替换Inner SIoU损失函数(基于MMYOLO)

由于MMYOLO中没有实现Inner SIoU损失函数,所以需要在mmyolo/models/iou_loss.py中添加Inner SIoU的计算和对应的iou_mode,修改完以后在终端运行

python setup.py install

再在配置文件中进行修改即可。修改例子如下:

    elif iou_mode == "innersiou":
        ratio=1.0
        w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
        x1 = bbox1_x1 + w1_
        y1 = bbox1_y1 + h1_
        x2 = bbox2_x1 + w2_
        y2 = bbox2_y1 + h2_

        inner_b1_x1, inner_b1_x2, inner_b1_y1, inner_b1_y2 = x1 - w1_ * ratio, x1 + w1_ * ratio, \
                                                             y1 - h1_ * ratio, y1 + h1_ * ratio
        inner_b2_x1, inner_b2_x2, inner_b2_y1, inner_b2_y2 = x2 - w2_ * ratio, x2 + w2_ * ratio, \
                                                             y2 - h2_ * ratio, y2 + h2_ * ratio
        inner_inter = (torch.min(inner_b1_x2, inner_b2_x2) - torch.max(inner_b1_x1, inner_b2_x1)).clamp(0) * \
                      (torch.min(inner_b1_y2, inner_b2_y2) - torch.max(inner_b1_y1, inner_b2_y1)).clamp(0)
        inner_union = w1 * ratio * h1 * ratio + w2 * ratio * h2 * ratio - inner_inter + eps
        inner_iou = inner_inter / inner_union

        cw = torch.max(bbox1_x2, bbox2_x2) - torch.min(bbox1_x1, bbox2_x1)  # convex width
        ch = torch.max(bbox1_y2, bbox2_y2) - torch.min(bbox1_y1, bbox2_y1)  # convex height
        s_cw = (bbox2_x1 + bbox2_x2 - bbox1_x1 - bbox1_x2) * 0.5 + eps
        s_ch = (bbox2_y1 + bbox2_y2 - bbox1_y1 - bbox1_y2) * 0.5 + eps
        sigma = torch.pow(s_cw ** 2 + s_ch ** 2, 0.5)
        sin_alpha_1 = torch.abs(s_cw) / sigma
        sin_alpha_2 = torch.abs(s_ch) / sigma
        threshold = pow(2, 0.5) / 2
        sin_alpha = torch.where(sin_alpha_1 > threshold, sin_alpha_2, sin_alpha_1)
        angle_cost = torch.cos(torch.arcsin(sin_alpha) * 2 - math.pi / 2)
        rho_x = (s_cw / cw) ** 2
        rho_y = (s_ch / ch) ** 2
        gamma = angle_cost - 2
        distance_cost = 2 - torch.exp(gamma * rho_x) - torch.exp(gamma * rho_y)
        omiga_w = torch.abs(w1 - w2) / torch.max(w1, w2)
        omiga_h = torch.abs(h1 - h2) / torch.max(h1, h2)
        shape_cost = torch.pow(1 - torch.exp(-1 * omiga_w), 4) + torch.pow(1 - torch.exp(-1 * omiga_h), 4)
        ious = inner_iou - 0.5 * (distance_cost + shape_cost)
    

修改后的配置文件(以configs/yolov5/yolov5_s-v61_syncbn_8xb16-300e_coco.py为例)

_base_ = ['../_base_/default_runtime.py', '../_base_/det_p5_tta.py']

# ========================Frequently modified parameters======================
# -----data related-----
data_root = 'data/coco/'  # Root path of data
# Path of train annotation file
train_ann_file = 'annotations/instances_train2017.json'
train_data_prefix = 'train2017/'  # Prefix of train image path
# Path of val annotation file
val_ann_file = 'annotations/instances_val2017.json'
val_data_prefix = 'val2017/'  # Prefix of val image path

num_classes = 80  # Number of classes for classification
# Batch size of a single GPU during training
train_batch_size_per_gpu = 16
# Worker to pre-fetch data for each single GPU during training
train_num_workers = 8
# persistent_workers must be False if num_workers is 0
persistent_workers = True

# -----model related-----
# Basic size of multi-scale prior box
anchors = [
    [(10, 13), (16, 30), (33, 23)],  # P3/8
    [(30, 61), (62, 45), (59, 119)],  # P4/16
    [(116, 90), (156, 198), (373, 326)]  # P5/32
]

# -----train val related-----
# Base learning rate for optim_wrapper. Corresponding to 8xb16=128 bs
base_lr = 0.01
max_epochs = 300  # Maximum training epochs

model_test_cfg = dict(
    # The config of multi-label for multi-class prediction.
    multi_label=True,
    # The number of boxes before NMS
    nms_pre=30000,
    score_thr=0.001,  # Threshold to filter out boxes.
    nms=dict(type='nms', iou_threshold=0.65),  # NMS type and threshold
    max_per_img=300)  # Max number of detections of each image

# ========================Possible modified parameters========================
# -----data related-----
img_scale = (640, 640)  # width, height
# Dataset type, this will be used to define the dataset
dataset_type = 'YOLOv5CocoDataset'
# Batch size of a single GPU during validation
val_batch_size_per_gpu = 1
# Worker to pre-fetch data for each single GPU during validation
val_num_workers = 2

# Config of batch shapes. Only on val.
# It means not used if batch_shapes_cfg is None.
batch_shapes_cfg = dict(
    type='BatchShapePolicy',
    batch_size=val_batch_size_per_gpu,
    img_size=img_scale[0],
    # The image scale of padding should be divided by pad_size_divisor
    size_divisor=32,
    # Additional paddings for pixel scale
    extra_pad_ratio=0.5)

# -----model related-----
# The scaling factor that controls the depth of the network structure
deepen_factor = 0.33
# The scaling factor that controls the width of the network structure
widen_factor = 0.5
# Strides of multi-scale prior box
strides = [8, 16, 32]
num_det_layers = 3  # The number of model output scales
norm_cfg = dict(type='BN', momentum=0.03, eps=0.001)  # Normalization config

# -----train val related-----
affine_scale = 0.5  # YOLOv5RandomAffine scaling ratio
loss_cls_weight = 0.5
loss_bbox_weight = 0.05
loss_obj_weight = 1.0
prior_match_thr = 4.  # Priori box matching threshold
# The obj loss weights of the three output layers
obj_level_weights = [4., 1., 0.4]
lr_factor = 0.01  # Learning rate scaling factor
weight_decay = 0.0005
# Save model checkpoint and validation intervals
save_checkpoint_intervals = 10
# The maximum checkpoints to keep.
max_keep_ckpts = 3
# Single-scale training is recommended to
# be turned on, which can speed up training.
env_cfg = dict(cudnn_benchmark=True)

# ===============================Unmodified in most cases====================
model = dict(
    type='YOLODetector',
    data_preprocessor=dict(
        type='mmdet.DetDataPreprocessor',
        mean=[0., 0., 0.],
        std=[255., 255., 255.],
        bgr_to_rgb=True),
    backbone=dict(
        ##使用YOLOv8的主干网络

        type='YOLOv8CSPDarknet',
        deepen_factor=deepen_factor,
        widen_factor=widen_factor,
        norm_cfg=norm_cfg,
        act_cfg=dict(type='SiLU', inplace=True)

    ),
    neck=dict(
        type='YOLOv5PAFPN',
        deepen_factor=deepen_factor,
        widen_factor=widen_factor,
        in_channels=[256, 512, 1024],
        out_channels=[256, 512, 1024],
        num_csp_blocks=3,
        norm_cfg=norm_cfg,
        act_cfg=dict(type='SiLU', inplace=True)),
    bbox_head=dict(
        type='YOLOv5Head',
        head_module=dict(
            type='YOLOv5HeadModule',
            num_classes=num_classes,
            in_channels=[256, 512, 1024],
            widen_factor=widen_factor,
            featmap_strides=strides,
            num_base_priors=3),
        prior_generator=dict(
            type='mmdet.YOLOAnchorGenerator',
            base_sizes=anchors,
            strides=strides),
        # scaled based on number of detection layers
        loss_cls=dict(
            type='mmdet.CrossEntropyLoss',
            use_sigmoid=True,
            reduction='mean',
            loss_weight=loss_cls_weight *
            (num_classes / 80 * 3 / num_det_layers)),
        # 修改此处实现IoU损失函数的替换
        loss_bbox=dict(
            type='IoULoss',
            iou_mode='innersiou',
            bbox_format='xywh',
            eps=1e-7,
            reduction='mean',
            loss_weight=loss_bbox_weight * (3 / num_det_layers),
            return_iou=True),
        loss_obj=dict(
            type='mmdet.CrossEntropyLoss',
            use_sigmoid=True,
            reduction='mean',
            loss_weight=loss_obj_weight *
            ((img_scale[0] / 640)**2 * 3 / num_det_layers)),
        prior_match_thr=prior_match_thr,
        obj_level_weights=obj_level_weights),
    test_cfg=model_test_cfg)

albu_train_transforms = [
    dict(type='Blur', p=0.01),
    dict(type='MedianBlur', p=0.01),
    dict(type='ToGray', p=0.01),
    dict(type='CLAHE', p=0.01)
]

pre_transform = [
    dict(type='LoadImageFromFile', file_client_args=_base_.file_client_args),
    dict(type='LoadAnnotations', with_bbox=True)
]

train_pipeline = [
    *pre_transform,
    dict(
        type='Mosaic',
        img_scale=img_scale,
        pad_val=114.0,
        pre_transform=pre_transform),
    dict(
        type='YOLOv5RandomAffine',
        max_rotate_degree=0.0,
        max_shear_degree=0.0,
        scaling_ratio_range=(1 - affine_scale, 1 + affine_scale),
        # img_scale is (width, height)
        border=(-img_scale[0] // 2, -img_scale[1] // 2),
        border_val=(114, 114, 114)),
    dict(
        type='mmdet.Albu',
        transforms=albu_train_transforms,
        bbox_params=dict(
            type='BboxParams',
            format='pascal_voc',
            label_fields=['gt_bboxes_labels', 'gt_ignore_flags']),
        keymap={
            'img': 'image',
            'gt_bboxes': 'bboxes'
        }),
    dict(type='YOLOv5HSVRandomAug'),
    dict(type='mmdet.RandomFlip', prob=0.5),
    dict(
        type='mmdet.PackDetInputs',
        meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape', 'flip',
                   'flip_direction'))
]

train_dataloader = dict(
    batch_size=train_batch_size_per_gpu,
    num_workers=train_num_workers,
    persistent_workers=persistent_workers,
    pin_memory=True,
    sampler=dict(type='DefaultSampler', shuffle=True),
    dataset=dict(
        type=dataset_type,
        data_root=data_root,
        ann_file=train_ann_file,
        data_prefix=dict(img=train_data_prefix),
        filter_cfg=dict(filter_empty_gt=False, min_size=32),
        pipeline=train_pipeline))

test_pipeline = [
    dict(type='LoadImageFromFile', file_client_args=_base_.file_client_args),
    dict(type='YOLOv5KeepRatioResize', scale=img_scale),
    dict(
        type='LetterResize',
        scale=img_scale,
        allow_scale_up=False,
        pad_val=dict(img=114)),
    dict(type='LoadAnnotations', with_bbox=True, _scope_='mmdet'),
    dict(
        type='mmdet.PackDetInputs',
        meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape',
                   'scale_factor', 'pad_param'))
]

val_dataloader = dict(
    batch_size=val_batch_size_per_gpu,
    num_workers=val_num_workers,
    persistent_workers=persistent_workers,
    pin_memory=True,
    drop_last=False,
    sampler=dict(type='DefaultSampler', shuffle=False),
    dataset=dict(
        type=dataset_type,
        data_root=data_root,
        test_mode=True,
        data_prefix=dict(img=val_data_prefix),
        ann_file=val_ann_file,
        pipeline=test_pipeline,
        batch_shapes_cfg=batch_shapes_cfg))

test_dataloader = val_dataloader

param_scheduler = None
optim_wrapper = dict(
    type='OptimWrapper',
    optimizer=dict(
        type='SGD',
        lr=base_lr,
        momentum=0.937,
        weight_decay=weight_decay,
        nesterov=True,
        batch_size_per_gpu=train_batch_size_per_gpu),
    constructor='YOLOv5OptimizerConstructor')

default_hooks = dict(
    param_scheduler=dict(
        type='YOLOv5ParamSchedulerHook',
        scheduler_type='linear',
        lr_factor=lr_factor,
        max_epochs=max_epochs),
    checkpoint=dict(
        type='CheckpointHook',
        interval=save_checkpoint_intervals,
        save_best='auto',
        max_keep_ckpts=max_keep_ckpts))

custom_hooks = [
    dict(
        type='EMAHook',
        ema_type='ExpMomentumEMA',
        momentum=0.0001,
        update_buffers=True,
        strict_load=False,
        priority=49)
]

val_evaluator = dict(
    type='mmdet.CocoMetric',
    proposal_nums=(100, 1, 10),
    ann_file=data_root + val_ann_file,
    metric='bbox')
test_evaluator = val_evaluator

train_cfg = dict(
    type='EpochBasedTrainLoop',
    max_epochs=max_epochs,
    val_interval=save_checkpoint_intervals)
val_cfg = dict(type='ValLoop')
test_cfg = dict(type='TestLoop')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/588268.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Vue Router和element-ui的LayOut

一、展示 二、代码 app.vue <template><div id"app"><el-container style"border: 1px solid #eee; height: 100vh"><el-aside v-bind:width"asideWidth" style"background-color: rgb(48, 65, 86);"><…

基于ROS从零开始构建自主移动机器人:仿真和硬件

书籍&#xff1a;Build Autonomous Mobile Robot from Scratch using ROS&#xff1a;Simulation and Hardware 作者&#xff1a;Rajesh Subramanian 出版&#xff1a;Apress 书籍下载-《基于ROS从零开始构建自主移动机器人&#xff1a;仿真和硬件》您将开始理解自主机器人发…

ip地址与硬件地址的区别是什么

在数字世界的浩瀚海洋中&#xff0c;每一台联网的设备都需要一个独特的标识来确保信息的准确传输。这些标识&#xff0c;我们通常称之为IP地址和硬件地址。虽然它们都是用来识别网络设备的&#xff0c;但各自扮演的角色和所处的层次却大相径庭。虎观代理小二将带您深入了解IP地…

karpathy make more -- 4

1 Introduction 这个部分要完成一个网络的模块化&#xff0c;然后实现一个新的网络结构。 2 使用torch的模块化功能 2.1 模块化 将输入的字符长度变成8&#xff0c;并将之前的代码模块化 # Near copy paste of the layers we have developed in Part 3# -----------------…

爬虫学习:基本网络请求库的使用

目录 一、urllib网络库 1.urlopen()方法 2.request方法 二、requests网络请求库 1.主要方法 2.requests.get()和requests.post() 一、urllib网络库 1.urlopen()方法 语法格式&#xff1a; urlopen(url,data,timeout,cafile,capath,context) # url:地址 # data:要提交的数据…

[华为OD]C卷 机场航班调度 ,XX市机场停放了多架飞机,每架飞机都有自己的航班号100

题目&#xff1a; XX市机场停放了多架飞机&#xff0c;每架飞机都有自己的航班号CA3385, CZ6678, SC6508 等&#xff0c;航班号的前2个大写字母&#xff08;或数字&#xff09;代表航空公司的缩写&#xff0c;后面4个数字代表航班信息。 但是XX市机场只有一条起飞用跑道&am…

uniapp源码+计划任务 台股平台源码 新股申购 分类后台控制

台股平台源码集成了新股申购与折扣申购功能&#xff0c;结合了计划任务和UniApp源码&#xff0c;为用户提供了一个全面的股票交易解决方案。 经过初步测试&#xff0c;系统可正常运行。测试时没有配置计划任务和WebSocket 。有兴趣的自行研究。 本系统基于PHP 7.3版本开发&am…

【记录】Springboot项目集成docker实现一键部署

公司管理平台完成后&#xff0c;为了方便其他不懂开发的同事部署和测试&#xff0c;集成docker进行一键部署&#xff0c;也为后面自动化部署做准备。本文做个简单记录。 1、安装docker yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/cont…

maven插件:dockerfile-maven-plugin和docker-maven-plugin

Maven插件dockerfile-maven-plugin和docker-maven-plugin都是为Java开发人员提供了一种便捷的方式&#xff0c;通过Maven构建流程来自动化创建、管理和推送Docker镜像。虽然它们有着相似的目标&#xff0c;即集成Docker与Maven项目&#xff0c;但这两个插件在实现细节、功能侧重…

嵌入式全栈开发学习笔记---C语言笔试复习大全3

目录 笔试题3 笔试题4 笔试题5 上一篇介绍了数据类型的长度和数据范围&#xff0c;并且分别讲解了两个经典的笔试题&#xff0c;这一篇我们再来看三道非常经典的考数据类型长度、数据范围和数据类型转换的笔试题。 说明&#xff1a;我们学过单片机的一般都是有C语言基础的了…

Flask路由的使用

Flask 是一个轻量级的 Python Web 框架&#xff0c;其简洁的设计使得构建 Web 应用变得轻而易举。其中&#xff0c;路由是 Flask 中至关重要的一部分&#xff0c;它定义了 URL 与视图函数之间的映射关系&#xff0c;决定了用户请求的处理方式。在本文中&#xff0c;我们将深入探…

vue3项目引入VueQuill富文本编辑器(成功)及 quill-image-uploader 图像模块(未成功)

tip&#xff1a;重点解释都写在代码注释里了&#xff0c;方便理解&#xff0c;所以看起来比较密集 富文本基本使用 项目文件夹路径安装依赖 npm install vueup/vue-quilllatest --save 全局注册&#xff1a;main.js // main.js// 自己项目的一些配置&#xff08;只放了主要…

【C语言】文件操作(万字解读超详细解析)

最好的时光&#xff0c;在路上;最好的生活&#xff0c;在别处。独自上路去看看这个世界&#xff0c;你终将与最好的自己相遇。&#x1f493;&#x1f493;&#x1f493; 目录 • ✨说在前面 &#x1f34b;知识点一&#xff1a;什么是文件&#xff1f; • &#x1f330;1.程序…

【项目学习01_2024.05.01_Day03】

学习笔记 3.6 开发业务层3.6.1 创建数据字典表3.6.2 编写Service3.6.3 测试Service 3.7 接口测试3.7.1 接口完善3.7.2 Httpclient测试 3.8 前后端联调3.8.1 准备环境3.8.2 安装系统管理服务3.8.3 解决跨域问题解决跨域的方法&#xff1a;我们准备使用方案2解决跨域问题。在内容…

模方试用版水面修整,调整水岸线功能进程缓慢该怎么解决?

答&#xff1a;水面修整&#xff0c;第一个点选取准确的高程位置和水边&#xff0c;其他点就可以包含整个水面范围就行&#xff0c;可以绘制大一些。上图绘制区域没有包含到所有的水面&#xff0c;可以尝试下图的红线绘制区域。 模方是一款针对实景三维模型的冗余碎片、水面残缺…

使用Neo4j和Langchain创建知识图谱

使用Neo4j和Langchain创建知识图谱 知识图谱是组织和整合信息的强大工具。通过使用实体作为节点和关系作为边缘&#xff0c;它们提供了一种系统的知识表示方法。这种有条理的表示有利于简化查询、分析和推理&#xff0c;使知识图在搜索引擎、推荐系统、自然语言处理和人工智能…

Docker:centos7安装docker

官网&#xff1a;https://www.docker.com/官网 文档地址 - 确认centos7及其以上的版本 查看当前系统版本 cat /etc/redhat-release- 卸载旧版本 依照官网执行 - yum安装gcc相关 yum -y install gccyum -y install gcc-c- 安装需要的软件包 yum install -y yum-utils- 设置s…

Java 基础重点知识-(泛型、反射、注解、IO)

文章目录 什么是泛型? 泛型有什么用?泛型原理是什么? Java 反射什么是反射? 反射作用是什么?动态代理有几种实现方式? 有什么特点? Java 注解什么是注解, 作用是什么? Java I/O什么是序列化?Java 是怎么实现系列化的?常见的序列化协议有哪些?BIO/NIO/AIO 有什么区别…

可靠的Mac照片恢复解决方案

当您在搜索引擎搜索中输入“Mac照片恢复”时&#xff0c;您将获得数以万计的结果。有很多Mac照片恢复解决方案声称他们可以在Mac OS下恢复丢失的照片。但是&#xff0c;并非互联网上的所有Mac照片恢复解决方案都可以解决您的照片丢失问题。而且您不应该花太多时间寻找可靠的Mac…

数据库(MySQL)—— DQL语句(聚合,分组,排序,分页)

数据库&#xff08;MySQL&#xff09;—— DQL语句&#xff08;聚合&#xff0c;分组&#xff0c;排序&#xff0c;分页&#xff09; 聚合函数常见的聚合函数语法 分组查询语法 排序查询语法 分页查询语法 DQL的执行顺序 我们今天来继续学习MySQL的DQL语句的聚合和分组查询&…
最新文章