YOLOv6 | 模型结构与训练策略详细解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: YOLOv6 | 模型结构与训练策略详细解析

1. 网络结构改进


1.1 EfficientRep Backbone

参考了重参数结构化的实现,对多卷积分支的拓扑结构进行融合。其实yolov5中也有使用到,yolov5中对3x3卷积和bn层进行了融合。

image.png


接下来就是看yolov6的网络结构部分,可以看见这里提出了3个模块:RepConv,RepBlock,SimSPPF,由于这里没有论文直接看是什么东西,所以这里分别来看源码。


SimSPPF

其实吧,通过源码分析,yolov6的代码很大部分是套用yolov5的代码结构的。所谓的SimSPPF和SimConv本质上只是激活函数使用了ReLU而不是SiLU


SimSPPF类

class SimSPPF(nn.Module):
    '''Simplified SPPF with ReLU activation'''
    def __init__(self, in_channels, out_channels, kernel_size=5):
        super().__init__()
        c_ = in_channels // 2  # hidden channels
        self.cv1 = SimConv(in_channels, c_, 1, 1)
        self.cv2 = SimConv(c_ * 4, out_channels, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=kernel_size, stride=1, padding=kernel_size // 2)
    def forward(self, x):
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')
            y1 = self.m(x)
            y2 = self.m(y1)
            return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1))


原yolov5的SPPF类

class SPPF(nn.Module):
    # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
    def __init__(self, c1, c2, k=5):  # equivalent to SPP(k=(5, 9, 13))
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)
    def forward(self, x):
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')  # suppress torch 1.9.0 max_pool2d() warning
            y1 = self.m(x)
            y2 = self.m(y1)
            return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))


RepConv

我最早接触参数重结构化这个词是看见了大佬丁霄汉发表的几篇论文:RepVGG,RepMLP,RepLKNet,这些构建新backbone的论文无一例外的全部使用了参数重结构化的思想。


RepVGG将3x3,1x1,identity分支的残差结果利用数学计算方法等价为一个3x3的卷积结构,实现训练与推断过程的解耦;RepMLP将局部的CNN先验信息加进了全连接层,使得其与MLP相结合等等。这里需要注意,重结构化层MLP结构也不是说变成Linear层,而是简化为1x1的卷积。(后续有机会把这几篇文章介绍一下,或者直接看大佬的知乎:https://www.zhihu.com/people/ding-xiao-yi-93/posts


这里源码中是改写了RepVGG的代码,原理如下图所示:

image.png


核心的思想是为了解耦训练过程与推理过程。训练过程中花的时间可以长点,缩短推理时间。在推理过程中,重结构化的网络拓扑结构会变得简单,加快推理速度。


相关的参数重结构化的技巧已经使用得比较的广泛,详细见参考资料3.


RepBlock

这个就没什么好说的了,就是多个RepConv的堆叠,不过其中 RepBlock 的第一个 RepConv 会做 channel 维度的变换和对齐。


RepBlock代码:

class RepBlock(nn.Module):
    '''
        RepBlock is a stage block with rep-style basic block
    '''
    def __init__(self, in_channels, out_channels, n=1, block=RepVGGBlock):
        super().__init__()
        self.conv1 = block(in_channels, out_channels)
        self.block = nn.Sequential(*(block(out_channels, out_channels) for _ in range(n - 1))) if n > 1 else None
    def forward(self, x):
        x = self.conv1(x)
        if self.block is not None:
            x = self.block(x)
        return x


1.2 Rep-PAN

结构示意图如下所示:

image.png

这里的1x1 Conv其实是1x1卷积的作为通道降维操作,Upsample是使用了反卷积nn.ConvTranspose2d(其实也是卷积的一种);3x3 Conv为了减半尺寸,RepBlock与backbone中的类似。


直接看源码就比较清晰了:


class RepPANNeck(nn.Module):
    """RepPANNeck Module
    EfficientRep is the default backbone of this model.
    RepPANNeck has the balance of feature fusion ability and hardware efficiency.
    """
    def __init__(
        self,
        channels_list=None,
        num_repeats=None,
        block=RepVGGBlock,
    ):
        super().__init__()
        assert channels_list is not None
        assert num_repeats is not None
        self.Rep_p4 = RepBlock(
            in_channels=channels_list[3] + channels_list[5],
            out_channels=channels_list[5],
            n=num_repeats[5],
            block=block
        )
        self.Rep_p3 = RepBlock(
            in_channels=channels_list[2] + channels_list[6],
            out_channels=channels_list[6],
            n=num_repeats[6],
            block=block
        )
        self.Rep_n3 = RepBlock(
            in_channels=channels_list[6] + channels_list[7],
            out_channels=channels_list[8],
            n=num_repeats[7],
            block=block
        )
        self.Rep_n4 = RepBlock(
            in_channels=channels_list[5] + channels_list[9],
            out_channels=channels_list[10],
            n=num_repeats[8],
            block=block
        )
        self.reduce_layer0 = SimConv(
            in_channels=channels_list[4],
            out_channels=channels_list[5],
            kernel_size=1,
            stride=1
        )
        self.upsample0 = Transpose(
            in_channels=channels_list[5],
            out_channels=channels_list[5],
        )
        self.reduce_layer1 = SimConv(
            in_channels=channels_list[5],
            out_channels=channels_list[6],
            kernel_size=1,
            stride=1
        )
        self.upsample1 = Transpose(
            in_channels=channels_list[6],
            out_channels=channels_list[6]
        )
        self.downsample2 = SimConv(
            in_channels=channels_list[6],
            out_channels=channels_list[7],
            kernel_size=3,
            stride=2
        )
        self.downsample1 = SimConv(
            in_channels=channels_list[8],
            out_channels=channels_list[9],
            kernel_size=3,
            stride=2
        )
    def forward(self, input):
        (x2, x1, x0) = input
        fpn_out0 = self.reduce_layer0(x0)
        upsample_feat0 = self.upsample0(fpn_out0)
        f_concat_layer0 = torch.cat([upsample_feat0, x1], 1)
        f_out0 = self.Rep_p4(f_concat_layer0)
        fpn_out1 = self.reduce_layer1(f_out0)
        upsample_feat1 = self.upsample1(fpn_out1)
        f_concat_layer1 = torch.cat([upsample_feat1, x2], 1)
        pan_out2 = self.Rep_p3(f_concat_layer1)
        down_feat1 = self.downsample2(pan_out2)
        p_concat_layer1 = torch.cat([down_feat1, fpn_out1], 1)
        pan_out1 = self.Rep_n3(p_concat_layer1)
        down_feat0 = self.downsample1(pan_out1)
        p_concat_layer2 = torch.cat([down_feat0, fpn_out0], 1)
        pan_out0 = self.Rep_n4(p_concat_layer2)
        outputs = [pan_out2, pan_out1, pan_out0]
        return


1.3 Decoupled Head

结构示意图:

image.png

咋一看可能也看不懂,其实其意思就是将解耦进行到底。为每一层的预测特征层都配置不一样的回归卷积,分类卷积,置信度卷积。按照一般的FPN结构是3层,那么现在就是每一层都会有其独立的卷积输出。整个detect head不共享任何的结构,参数完全是独立的。


源码如下:

class Detect(nn.Module):
    '''Efficient Decoupled Head
    With hardware-aware degisn, the decoupled head is optimized with
    hybridchannels methods.
    '''
    def __init__(self, num_classes=80, anchors=1, num_layers=3, inplace=True, head_layers=None):  # detection layer
        super().__init__()
        assert head_layers is not None
        self.nc = num_classes  # number of classes
        self.no = num_classes + 5  # number of outputs per anchor
        self.nl = num_layers  # number of detection layers
        if isinstance(anchors, (list, tuple)):
            self.na = len(anchors[0]) // 2
        else:
            self.na = anchors
        self.anchors = anchors
        self.grid = [torch.zeros(1)] * num_layers
        self.prior_prob = 1e-2
        self.inplace = inplace
        stride = [8, 16, 32]  # strides computed during build
        self.stride = torch.tensor(stride)
        # Init decouple head
        self.cls_convs = nn.ModuleList()
        self.reg_convs = nn.ModuleList()
        self.cls_preds = nn.ModuleList()
        self.reg_preds = nn.ModuleList()
        self.obj_preds = nn.ModuleList()
        self.stems = nn.ModuleList()
        # Efficient decoupled head layers
        for i in range(num_layers):
            idx = i*6
            self.stems.append(head_layers[idx])
            self.cls_convs.append(head_layers[idx+1])
            self.reg_convs.append(head_layers[idx+2])
            self.cls_preds.append(head_layers[idx+3])
            self.reg_preds.append(head_layers[idx+4])
            self.obj_preds.append(head_layers[idx+5])
    def initialize_biases(self):
        for conv in self.cls_preds:
            b = conv.bias.view(self.na, -1)
            b.data.fill_(-math.log((1 - self.prior_prob) / self.prior_prob))
            conv.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
        for conv in self.obj_preds:
            b = conv.bias.view(self.na, -1)
            b.data.fill_(-math.log((1 - self.prior_prob) / self.prior_prob))
            conv.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
    def forward(self, x):
        z = []
        for i in range(self.nl):
            x[i] = self.stems[i](x[i])
            cls_x = x[i]
            reg_x = x[i]
            cls_feat = self.cls_convs[i](cls_x)
            cls_output = self.cls_preds[i](cls_feat)
            reg_feat = self.reg_convs[i](reg_x)
            reg_output = self.reg_preds[i](reg_feat)
            obj_output = self.obj_preds[i](reg_feat)
            if self.training:
                x[i] = torch.cat([reg_output, obj_output, cls_output], 1)
                bs, _, ny, nx = x[i].shape
                x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            else:
                y = torch.cat([reg_output, obj_output.sigmoid(), cls_output.sigmoid()], 1)
                bs, _, ny, nx = y.shape
                y = y.view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
                if self.grid[i].shape[2:4] != y.shape[2:4]:
                    d = self.stride.device
                    yv, xv = torch.meshgrid([torch.arange(ny).to(d), torch.arange(nx).to(d)])
                    self.grid[i] = torch.stack((xv, yv), 2).view(1, self.na, ny, nx, 2).float()
                if self.inplace:
                    y[..., 0:2] = (y[..., 0:2] + self.grid[i]) * self.stride[i]  # xy
                    y[..., 2:4] = torch.exp(y[..., 2:4]) * self.stride[i] # wh
                else:
                    xy = (y[..., 0:2] + self.grid[i]) * self.stride[i]  # xy
                    wh = torch.exp(y[..., 2:4]) * self.stride[i]  # wh
                    y = torch.cat((xy, wh, y[..., 4:]), -1)
                z.append(y.view(bs, -1, self.no))
  # Train: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
        # Eval:  0: [1, 19200+4800+1200, 25]
        #        1: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
        return x if self.training else (torch.cat(z, 1), x)
def build_effidehead_layer(channels_list, num_anchors, num_classes):
    head_layers = nn.Sequential(
        # stem0
        Conv(
            in_channels=channels_list[6],
            out_channels=channels_list[6],
            kernel_size=1,
            stride=1
        ),
        # cls_conv0
        Conv(
            in_channels=channels_list[6],
            out_channels=channels_list[6],
            kernel_size=3,
            stride=1
        ),
        # reg_conv0
        Conv(
            in_channels=channels_list[6],
            out_channels=channels_list[6],
            kernel_size=3,
            stride=1
        ),
        # cls_pred0
        nn.Conv2d(
            in_channels=channels_list[6],
            out_channels=num_classes * num_anchors,
            kernel_size=1
        ),
        # reg_pred0
        nn.Conv2d(
            in_channels=channels_list[6],
            out_channels=4 * num_anchors,
            kernel_size=1
        ),
        # obj_pred0
        nn.Conv2d(
            in_channels=channels_list[6],
            out_channels=1 * num_anchors,
            kernel_size=1
        ),
        # stem1
        Conv(
            in_channels=channels_list[8],
            out_channels=channels_list[8],
            kernel_size=1,
            stride=1
        ),
        # cls_conv1
        Conv(
            in_channels=channels_list[8],
            out_channels=channels_list[8],
            kernel_size=3,
            stride=1
        ),
        # reg_conv1
        Conv(
            in_channels=channels_list[8],
            out_channels=channels_list[8],
            kernel_size=3,
            stride=1
        ),
        # cls_pred1
        nn.Conv2d(
            in_channels=channels_list[8],
            out_channels=num_classes * num_anchors,
            kernel_size=1
        ),
        # reg_pred1
        nn.Conv2d(
            in_channels=channels_list[8],
            out_channels=4 * num_anchors,
            kernel_size=1
        ),
        # obj_pred1
        nn.Conv2d(
            in_channels=channels_list[8],
            out_channels=1 * num_anchors,
            kernel_size=1
        ),
        # stem2
        Conv(
            in_channels=channels_list[10],
            out_channels=channels_list[10],
            kernel_size=1,
            stride=1
        ),
        # cls_conv2
        Conv(
            in_channels=channels_list[10],
            out_channels=channels_list[10],
            kernel_size=3,
            stride=1
        ),
        # reg_conv2
        Conv(
            in_channels=channels_list[10],
            out_channels=channels_list[10],
            kernel_size=3,
            stride=1
        ),
        # cls_pred2
        nn.Conv2d(
            in_channels=channels_list[10],
            out_channels=num_classes * num_anchors,
            kernel_size=1
        ),
        # reg_pred2
        nn.Conv2d(
            in_channels=channels_list[10],
            out_channels=4 * num_anchors,
            kernel_size=1
        ),
        # obj_pred2
        nn.Conv2d(
            in_channels=channels_list[10],
            out_channels=1 * num_anchors,
            kernel_size=1
        )
    )
    return head_layers


yolov5的detect head我之前是写过博客记录的,见:Yolov5-6.0系列 | yolov5的模型网络构建。


补充:注意美团这里yolov6的forward过程:


if self.grid[i].shape[2:4] != y.shape[2:4]:
    d = self.stride.device
    yv, xv = torch.meshgrid([torch.arange(ny).to(d), torch.arange(nx).to(d)])
    self.grid[i] = torch.stack((xv, yv), 2).view(1, self.na, ny, nx, 2).float()
if self.inplace:
    y[..., 0:2] = (y[..., 0:2] + self.grid[i]) * self.stride[i]  # xy
    y[..., 2:4] = torch.exp(y[..., 2:4]) * self.stride[i] # wh
else:
    xy = (y[..., 0:2] + self.grid[i]) * self.stride[i]  # xy
    wh = torch.exp(y[..., 2:4]) * self.stride[i]  # wh
    y = torch.cat((xy, wh, y[..., 4:]), -1)


其还是沿用了yolov3的偏移设置

image.png


并没有使用yolov5所提出的消除网格的方法,所以还会存在一些极端点网络无法处理。所以有理由详细使用yolov5的网格敏感度消除的方法会使得yolov6的性能进一步的提升。


if self.inplace:
      y[..., 0:2] = (y[..., 0:2] * 2 + self.grid[i]) * self.stride[i]  # xy
      y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh


在head这一部分,除了是完全参数独立之外,其实没有过太多的改进。训练和推理形式都是类似的,返回和推理的shape也是和yolov5类似的。


# Train: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
        # Eval:  0: [1, 19200+4800+1200, 25]
        #        1: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
        return x if self.training else (torch.cat(z, 1), x)


总结:


综上所述,将参数重结构化(3x3,1x1,残差分支合并为一个3x3卷积)方法使用在yolov5的框架中,改进了backbone与neck部分,方便部署。然后在head上使用了参数全独立的解耦。不过,缺点是甚至没有借鉴yolov5的网格敏感度消除,网络可能会出现极端点无法预测的情况。


2. 训练策略改进


2.1 Anchor-free 无锚范式

这个东西很早的时候就出现了,之前也写过一些列anchor-free算法的介绍:FCOS,CenterNet,ASFF,SAPD等等。具体参考专栏:目标检测论文专栏


由于 Anchor-based检测器需要在训练之前进行聚类分析以确定最佳 Anchor 集合,这会一定程度提高检测器的复杂度,而anchor-free就不需要任何anchor的手工或者是聚类的设置,直接是类似语义分割的想法在每个特征点上直接预测4个回归目标域k个分类目标。


不过不同算法的分配情况不同。


2.2 SimOTA 标签分配策略

YOLOv5 的标签分配策略是基于 Shape 匹配,并通过跨网格匹配策略增加正样本数量,从而使得网络快速收敛,但是该方法属于静态分配方法,并不会随着网络训练的过程而调整。


YOLOv6 引入了 SimOTA 算法动态分配正样本,进一步提高检测精度。在之前的文章已经对 SimOTA 标签分配策略进行了详细介绍,见:YoloX | SimOTA标签匹配策略


2.3 SIoU 边界框回归损失

近年来,常用的边界框回归损失包括IoU、GIoU、CIoU、DIoU loss等等,这些损失函数通过考虑预测框与目标框之前的重叠程度、中心点距离、纵横比等因素来衡量两者之间的差距,从而指导网络最小化损失以提升回归精度,但是这些方法都没有考虑到预测框与目标框之间方向的匹配性。


关于IoU、GIoU、CIoU、DIoU loss的具体介绍,之前的博客已有介绍,见:YOLOv4中的tricks概念总结——Bag of freebies


对于SIoU损失,其结果形式搞得相当的复杂,其具体包含了四个部分:角度损失(Angle cost)、距离损失(Distance cost)、形状损失(Shape cost)、IoU损失(IoU cost)


详细见:目标检测–边框回归损失函数SIoU原理详解及代码实现,这里作简要的摘抄。


角度损失

image.png

距离损失

image.png

其中:

image.png

形状损失

image.png

IoU损失

image.png

SIoU损失

image.png

3. 实验结果与总结


  • 消融实验

image.png


  • 效果对比

image.png

yolov6的实验对比更加偏向于轻量级模型的对比。


对yolov6的提出,我主要从两个方面去做总结,一个是模型上的改进,另外一个是训练策略上的改进。


但是可以看见,无论是模型上的改进还是训练策略上的改进并没有提出一些比较新颖的见解。对于网络结构上的改进,主要是利用了重结构化的思想改进了yolov5的框架,同时将head进行参数的完全独立不共享(这样做可能会有效,但是感觉增加了很大的参数量)。而参数重结构化可以使得推理过程的速度加快,改变网络的拓扑结构。


对于训练策略上,没有消除网格的敏感度,可能会导致特殊值无法处理的情况。使用了一个比较新颖的Iou损失函数。但是,无论是模型还是训练策略,更感觉是一个大杂烩。特别是完全的参数独立的解耦头,只提升了0.2%,对于我们个人的配置有点怀疑其有效性,根绝投入和收获不成比例。


不过,yolov6的成功,更说明了参数重结构化的可行性与有效性。也侧面反映出yolov5的成功,影响深远。


参考资料:


1. YOLOv6:又快又准的目标检测框架开源啦


2. YOLOv6 github地址


3. 深度学习中的重参数机制总结和实现


4. Yolov5-6.0系列 | yolov5的模型网络构建


5. YoloX | SimOTA标签匹配策略


6. YOLOv4中的tricks概念总结——Bag of freebies


7. 目标检测–边框回归损失函数SIoU原理详解及代码实现


目录
相关文章
|
10天前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
27 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
9天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
17天前
|
负载均衡 网络协议 定位技术
在数字化时代,利用DNS实现地理位置路由成为提升用户体验的有效策略
在数字化时代,利用DNS实现地理位置路由成为提升用户体验的有效策略。通过解析用户请求的来源IP地址,DNS服务器可判断其地理位置,并返回最近或最合适的服务器IP,从而优化网络路由,减少延迟,提高访问速度。示例代码展示了如何基于IP地址判断地理位置并分配相应服务器IP,实际应用中需结合专业地理数据库和动态调整机制,以应对复杂网络环境带来的挑战。
21 6
|
13天前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
26 2
|
17天前
|
机器学习/深度学习 存储 人工智能
AI助力电子邮件安全防护,CISO解析新策略
AI助力电子邮件安全防护,CISO解析新策略
|
25天前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
71 2
|
25天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
28天前
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
90 3
|
15天前
|
机器学习/深度学习 人工智能 自然语言处理
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
44 0
|
1月前
|
安全 测试技术 Go
Go语言中的并发编程模型解析####
在当今的软件开发领域,高效的并发处理能力是提升系统性能的关键。本文深入探讨了Go语言独特的并发编程模型——goroutines和channels,通过实例解析其工作原理、优势及最佳实践,旨在为开发者提供实用的Go语言并发编程指南。 ####

推荐镜像

更多