YOLOv10 超详细解析 | 网络结构、训练策略、论文解读

网络结构

1. Backbone

在这里插入图片描述

2. Head

在这里插入图片描述

3. 说明

  • 网络结构按 YOLOv10m 绘制,不同 scale 的模型在结构上略有不同,而不是像 YOLOv8 一样仅调整 depth 和 width。
  • Head 有部分后续计算与 YOLOv8 完全相同,上图省略,具体请看此文。
  • YOLOv10 整体的网络结构与 YOLOv8 相同,在一些细节模块上有所改进,查看具体的模块计算方式、优化策略跳转模型优化部分。

4. Predict Postprocess

  首先需要说明的是,网络实际包含两个 Head(结构相同、参数不同),分别对应了 One-to-many Head 和 One-to-one Head,在训练时两个 Head 同时参与,而推理预测时只需要 One-to-one Head。

class v10Detect(Detect):def __init__(self, nc=80, ch=()):super().__init__(nc, ch)c3 = max(ch[0], min(self.nc, 100))  # channelsself.cv3 = ...self.one2one_cv2 = copy.deepcopy(self.cv2)self.one2one_cv3 = copy.deepcopy(self.cv3)

后处理具体流程:

  1. one2one [1, 84, 6300] → \to boxes [1, 6300, 4] + scores [1, 6300, 80]
  2. 根据每个 Anchor 最高的分类得分 max_scores [1,6300] 排序,取前 max_det=300 个 Anchor
  3. 将这 300 个 Anchor 的 80 类分类得分再做一次排序,取前 max_det=300

  此过程可以看作直接将所有 scores 中得分较高的 300 个结果作为检测结果,代码中使用两次 topk 可能是效率更高。 由于 scores 计算使用的是 Sigmoid 而非 Softmax,这个后处理过程就可能会得到两个 Anchor 相同的框(即框的位置大小也都相同),但类别不同的结果。

  由于后处理的不同,作者建议在预测中可以自由调节置信度阈值,或者设较小的阈值来提升对小目标、远处目标的检测效果,并且可以看出调节阈值对于后处理速度并无影响,只是从这 300 个结果中用不同的阈值筛选出最终结果而已。

# step 1
preds = preds["one2one"][0]  	# [1,84,6300]
preds.transpose(-1, -2)  		# [1,6300,84]
boxes, scores = preds.split([4, nc], dim=-1)  # [1,6300,4], [1,6300,80]
# step 2
max_scores = scores.amax(dim=-1)  # [1,6300]
max_scores, index = torch.topk(max_scores, max_det, axis=-1)  # [1,300], [1,300]
index = index.unsqueeze(-1)
boxes = torch.gather(boxes, dim=1, index=index.repeat(1, 1, boxes.shape[-1]))  		# [1,300,4]
scores = torch.gather(scores, dim=1, index=index.repeat(1, 1, scores.shape[-1]))	# [1,300,80]
# step 3
scores, index = torch.topk(scores.flatten(1), max_det, axis=-1)
labels = index % nc
index = index // nc
boxes = boxes.gather(dim=1, index=index.unsqueeze(-1).repeat(1, 1, boxes.shape[-1]))
return boxes, scores, labels

Train

  YOLOv10 最大的特点就是在预测阶段不需要 NMS,而这个功能正是通过训练来实现的。
  YOLOv10 在训练中会对 One-to-many Head 和 One-to-one Head 两个头的输出都计算损失,而损失函数与 YOLOv8 相同。具体看下面代码,仅仅是 tal_topk 参数设置不同,也就是在参与损失计算的正样本的选择上有所不同。

class v10DetectLoss:def __init__(self, model):self.one2many = v8DetectionLoss(model, tal_topk=10)self.one2one = v8DetectionLoss(model, tal_topk=1)def __call__(self, preds, batch):one2many = preds["one2many"]loss_one2many = self.one2many(one2many, batch)one2one = preds["one2one"]loss_one2one = self.one2one(one2one, batch)return loss_one2many[0] + loss_one2one[0], torch.cat((loss_one2many[1], loss_one2one[1]))

  YOLOv8 的损失计算代码较多,关于代码逐句解析请看此文。本文将以尽可能简洁明了的方式说明损失的计算流程。

1. 前期准备

  在训练阶段使用的网络输出为 Head 的输出 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3,并将其分离为分类得分 pred_scores [b, a, 80]、原始检测框 pred_dist [b, a, 16] 以及解码后的检测框 pred_bboxes [b, a, 4]。对于标签也会做简单处理,得到分类标签 gt_labels [b, n, 1] 和目标框标签 gt_bboxes [b, n, 4]
b:BatchSize
a:Anchors
80:分类数量
4:xyxy
n:该 Batch 中的最大实例数,实例数不足的图像会填充0,方便后续矩阵运算

2. 正样本选择与指标计算

  选择 Anchor 坐标在 gt_bboxes 内部的输出来计算指标:

overlaps = CIoU(gt_boxes, pd_boxes)
align_metric = bbox_scores.pow(self.alpha) * overlaps.pow(self.beta)

此处 bbox_scores 仅保留 Anchor 所在 gt_bboxes 对应类别的得分,alpha=0.5, beta=6。对应 YOLOv10 论文中的公式( s s s 代表 Anchor 是否在实例内部):
m ( α , β ) = s ⋅ p α ⋅ I o U ( b ^ , b ) β m(\alpha,\beta) = s·p^{\alpha} · \mathrm{IoU}(\hat{b}, b)^\beta m(α,β)=spαIoU(b^,b)β

  选出每个 gt 目标中 align_metric 前 10 / 1 的输出,如果同一个 Anchor 的输出入选多个 gt 目标的 Top10 / 1,选择 overlaps 即 IoU 较高的。

  通俗来说对于每个实例,都会选择一些与其匹配得分较高的输出作为正样本,但对于一个 Anchor 的输出同时只能作为一个目标的正样本。而 One-to-many Head 和 One-to-one Head 的不同之处就在于一个会选择多个(Top 10)得分较高的作为正样本,一个只选择得分最高的作为正样本。

3. target scores

t j = u ∗ ⋅ m j m ∗ t_j = u^*·\frac{m_j}{m^*} tj=ummj
  其中 t j t_j tj 代表某个实例的 j j j 号正样本的 target_score u ∗ u^* u 代表所有正样本中 IoU 的最大值, m j m_j mj 代表该正样本的 align_metric m ∗ m^* m 同理为最大值。

4. 计算损失

4.1 Class

self.bce = nn.BCEWithLogitsLoss(reduction="none")
target_scores_sum = max(target_scores.sum(), 1)
loss[1] = self.bce(pred_scores, target_scores.to(dtype)).sum() / target_scores_sum

l n = − w n [ y n ⋅ log ⁡ σ ( x n ) + ( 1 − y n ) ⋅ log ⁡ ( 1 − σ ( x n ) ) ] l_{n}=-w_{n}\left[y_{n} \cdot \log \sigma\left(x_{n}\right)+\left(1-y_{n}\right) \cdot \log \left(1-\sigma\left(x_{n}\right)\right)\right] ln=wn[ynlogσ(xn)+(1yn)log(1σ(xn))]

l n l_n ln 表示某个 Anchor 在某个类别上的分类损失,一共有 b ∗ a ∗ 80 b*a*80 ba80 l l l
w n w_n wn 统一取 1;
y n y_n yn 在正样本处的值为对应的 target_score,其余负样本处的值为 0;

L c l s = ∑ l ∑ t L_{cls}=\frac{\sum{l}}{\sum{t}} Lcls=tl

4.2 Box

weight = target_scores.sum(-1)[fg_mask].unsqueeze(-1)
iou = bbox_iou(pred_bboxes[fg_mask], target_bboxes[fg_mask], xywh=False, CIoU=True)
loss_iou = ((1.0 - iou) * weight).sum() / target_scores_sum

L b o x = ∑ t j ( 1 − C I o U j ) ∑ t L_{box}=\frac{\sum{t_j(1 - \mathrm{CIoU}_j)}} {\sum{t}} Lbox=ttj(1CIoUj)

I o U \mathrm{IoU} IoU 仅计算正样本,并且会以 target_score 作为权重 weight
注:一个 Anchor 输出仅作为一个实例的正样本,仅在实例的类别上存在 target_score,可用以下代码作为验证

(target_scores.sum(-1) == target_scores.max(-1)[0]).all()
> True

4.3 DFL

target_ltrb = bbox2dist(anchor_points, target_bboxes, self.reg_max)
loss_dfl = self._df_loss(pred_dist[fg_mask].view(-1, self.reg_max + 1), target_ltrb[fg_mask]) * weight
loss_dfl = loss_dfl.sum() / target_scores_sumdef _df_loss(pred_dist, target):tl = target.long()  # target lefttr = tl + 1  # target rightwl = tr - target  # weight leftwr = 1 - wl  # weight rightreturn (F.cross_entropy(pred_dist, tl.view(-1), reduction="none").view(tl.shape) * wl+ F.cross_entropy(pred_dist, tr.view(-1), reduction="none").view(tl.shape) * wr).mean(-1, keepdim=True)

注:这里计算损失时使用的 pred_dist 是网络对于 Box 的原始输出,未经过 DFL 解码成常规的 Box,而标签则是将常规的 target_bboxes 转化成了距离对应 Anchor 坐标的距离,与 pred_dist 的形式相匹配。

l n = − w y n log ⁡ exp ⁡ ( x n , y n ) ∑ c = 1 C exp ⁡ ( x n , c ) ⋅ 1 { y n ≠ i g n o r e _ i n d e x } l_{n}=-w_{y_{n}} \log \frac{\exp \left(x_{n, y_{n}}\right)}{\sum_{c=1}^{C} \exp \left(x_{n, c}\right)} \cdot 1\left\{y_{n} \neq \mathrm { ignore\_index }\right\} ln=wynlogc=1Cexp(xn,c)exp(xn,yn)1{yn=ignore_index}

上式为 F.cross_entropy 计算公式,等价于用 Softmax 计算分类概率 p p p l n = − log ⁡ p n l_n = -\log p_n ln=logpn 作为损失,其中 p n p_n pn 为标签类别对应的概率。
l d f l = ( y i + 1 − y ) l i + ( y − y i ) l i + 1 l_{dfl} = (y_{i+1} - y)l_i + (y-y_i)l_{i+1} ldfl=(yi+1y)li+(yyi)li+1
其中 y y y 为真实的标签,即某个坐标到 Anchor 坐标的距离, y i + 1 y_{i+1} yi+1 代表对 y y y 向上取整, y i y_i yi 代表对 y y y 向下取整。

  举例进行说明,假设 y = 3.7 y = 3.7 y=3.7 l d f l = ( 4 − 3.7 ) l 3 + ( 3.7 − 3 ) l 4 l_{dfl} = (4-3.7)l_3 + (3.7-3)l_4 ldfl=(43.7)l3+(3.73)l4,由于实际的 y y y 通常不会是个整数,单纯的将输出往单一类别收敛是不行的,理想情况就是让 3 和 4 两个类别的概率都比较高,那么通过 DFL 层的计算坐标就会落在 3~4 之间,同时以 y y y 到两边的距离作为权重来平衡两个类别的概率,往理想的 0.3 × 3 + 0.7 × 4 = 3.7 0.3 \times 3 + 0.7 \times 4=3.7 0.3×3+0.7×4=3.7 收敛。

注:每个正样本的损失为 4 个坐标的 DFL 损失的均值,最终当前 Batch 的 DFL 损失为所有正样本损失的和除以 target_scores_sum

模型优化

1. Lightweight Classification Head

  在 YOLOv8 的检测头中,分类头比回归头的参数量和计算量都要大。论文中表示在分析了分类误差和回归误差的影响后,发现回归头意义更大,因此减少分类头的开销不会对性能有较大损害。
  论文的分析实验为分别用真实的回归值和分类值代替输出,使回归或分类的损失置零,最终在验证集上的精度无回归的要比无分类的高很多。个人理解就是类似单独训练两个头,已知检测框位置训练分类,和已知分类训练检测框位置,得出的结论就是针对检测框的回归任务更难,分类任务简单很多,那么分类头就可以简化一些,至少参数量没必要比回归头还多。

  分类头从两个 3 × 3 3\times 3 3×3 卷积轻量化为两个的深度可分离卷积,深度可分离卷积由一个 3 × 3 3\times 3 3×3 的深度卷积和一个 1 × 1 1\times 1 1×1 的逐点卷积构成。

  在结构图中,卷积上方参数对应 [size, stride, padding, groups]。下面结合代码,感受 groups 对于卷积计算的影响(在深度可分离卷积中通常输入通道数和输出通道数相同,为了更直观,示例中使用不同的通道数)

import torch.nn as nn
from ptflops import get_model_complexity_infoconv1 = nn.Conv2d(64, 128, 3, 1, 1, bias=False)
conv2 = nn.Conv2d(64, 128, 3, 1, 1, groups=64, bias=False)
conv3 = nn.Conv2d(64, 128, 3, 1, 1, groups=2, bias=False)flops, params = get_model_complexity_info(conv1, (64, 320, 320), as_strings=False, print_per_layer_stat=True)
print(f"FLOPs: {flops}, {64*3*3*128*320*320}")
print(f"Parameters: {params}, {128*64*3*3}")flops, params = get_model_complexity_info(conv2, (64, 320, 320), as_strings=False, print_per_layer_stat=True)
print(f"FLOPs: {flops}, {1*3*3*128*320*320}")
print(f"Parameters: {params}, {128*1*3*3}")flops, params = get_model_complexity_info(conv3, (64, 320, 320), as_strings=False, print_per_layer_stat=True)
print(f"FLOPs: {flops}, {32*3*3*128*320*320}")
print(f"Parameters: {params}, {128*32*3*3}")

  in_channelsout_channels 都需要可被 groups 整除,标准卷积 groups=1。可以看作把输入按通道维度划分为 groups 组,并将卷积核按输出通道即卷积核个数也划分为 groups 组,然后进行 groups 个并行的标准卷积。

2. SCDown

在这里插入图片描述
  YOLOv8 使用一个标准卷积同时实现空间下采样 h , w → h / 2 , w / 2 h,w\to h/2,w/2 h,wh/2,w/2 和通道变化 c → 2 c c \to 2c c2c,计算成本高。
  SCDown(Spatial-channel decoupled downsampling)将上面两个操作——空间和通道解耦。先通过 1 × 1 1\times 1 1×1 的逐点卷积调节通道数,再通过 3 × 3 3\times 3 3×3 的深度卷积做空间下采样,在降低计算成本的同时最大限度保留信息。

3. CIB

在这里插入图片描述
  在结构上 C2fCIB 就是用 CIB (compact inverted block)替换了原本 C2f 中的 BottleneckCIB 则是将 Bottleneck 中的标准卷积用深度卷积加逐点卷积进行替换。

  论文中,计算每个 stage 最后一个 basic block 中最后一个卷积的数值秩(numerical rank),数值秩越低表示冗余越多,即可进行简化。数值秩是先计算矩阵的奇异值,奇异值大于阈值的数量为数值秩,奇异值大代表重要的信息,奇异值小代表冗余或噪声。
  最终得到的结论如下图,stage 越大,模型越大,冗余越多。这里的 stage 看起来是对应了网络中的 8 个 C2f
在这里插入图片描述
  在模块替换时,大致方法为对数值秩进行排序,逐步替换,直到出现性能下降。因此不同 scale 的模型在模块选择上也有所不同。(具体原理和细节暂不深究)

4. Large-kernel Conv

  用 size 较大的深度卷积可以扩大感受野,增强模型能力,但简单使用会影响针对小目标的浅层特征,也会在高分辨率阶段引入 I/O 开销和延迟。建议在小模型的深层阶段(例如 YOLOv10n 的 22 层),将 CIB 中的第二个深层卷积替换为 7 × 7 7\times 7 7×7 3 × 3 3\times3 3×3 的深度卷积,具体还是看代码比较直观。

class CIB(nn.Module):def __init__(self, c1, c2, shortcut=True, e=0.5, lk=False):super().__init__()c_ = int(c2 * e)  # hidden channelsself.cv1 = nn.Sequential(Conv(c1, c1, 3, g=c1),Conv(c1, 2 * c_, 1),Conv(2 * c_, 2 * c_, 3, g=2 * c_) if not lk else RepVGGDW(2 * c_),Conv(2 * c_, c2, 1),Conv(c2, c2, 3, g=c2),)class RepVGGDW(torch.nn.Module):def __init__(self, ed) -> None:super().__init__()self.conv = Conv(ed, ed, 7, 1, 3, g=ed, act=False)self.conv1 = Conv(ed, ed, 3, 1, 1, g=ed, act=False)self.dim = edself.act = nn.SiLU()def forward(self, x):return self.act(self.conv(x) + self.conv1(x))

5. PSA

在这里插入图片描述
  论文中对这个模块并没有原理上的说明,主要是自注意力的开销比较大,所以设计了 PSA(Partial self-attention)对分辨率最低的特征的一半进行计算,将对于全局的学习能力以较小的计算成本融入 YOLO 中。

  Attention 部分的具体计算方式结合下图和代码查看:
在这里插入图片描述

class Attention(nn.Module):def __init__(self, dim, num_heads=8, attn_ratio=0.5):super().__init__()self.num_heads = num_headsself.head_dim = dim // num_headsself.key_dim = int(self.head_dim * attn_ratio)self.scale = self.key_dim ** -0.5nh_kd = self.key_dim * num_headsh = dim + nh_kd * 2self.qkv = Conv(dim, h, 1, act=False)self.proj = Conv(dim, dim, 1, act=False)self.pe = Conv(dim, dim, 3, 1, g=dim, act=False)def forward(self, x):B, C, H, W = x.shapeN = H * Wqkv = self.qkv(x)q, k, v = qkv.view(B, self.num_heads, self.key_dim*2 + self.head_dim, N).split([self.key_dim, self.key_dim, self.head_dim], dim=2)attn = ((q.transpose(-2, -1) @ k) * self.scale)attn = attn.softmax(dim=-1)x = (v @ attn.transpose(-2, -1)).view(B, C, H, W) + self.pe(v.reshape(B, C, H, W))x = self.proj(x)return x

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue3+Vite报错:vite忽略.vue扩展名 Failed to resolve import ..... Does the file exist?

Vue3Vite报错:vite忽略.vue扩展名 Failed to resolve import … Does the file exist? 先看报错: 分析原因 原因是我们没有写后缀名 建议你在你的vite.config.js中加上如下配置 import { defineConfig } from "vite"; import vue from &qu…

人工智能程序员应该有什么职业素养?

人工智能程序员应该有什么职业素养? 面向企业需求去学习AI必备技能实战能力实战能力提升策略 面向企业需求去学习 如果想要应聘AI相关的岗位,就需要知道HR和管理层在招聘时需要考察些什么,面向招聘的需求去学习就能具备AI程序员该有的职业素…

RJ45 PCB布线

RJ45底盘接地和数字地通过一个1M欧姆的电阻和一个0.1uF的去耦电容隔离。其底盘接地和数字地的间距,必须比60mil宽。如图11及图12所示。 图11 典型变压器集成单RJ45的机箱/数字地平面 图12 典型RJ45和变压器分开的机箱/数字地平面https://www.bilibili.com/read/…

Java——ArrayList与顺序表

一、线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列... 线性表在逻辑上是线性结构,也就是连续的一条直…

Qt无边框

最简单的可拖动对话框(大小不可改变) #ifndef DIALOG_H #define DIALOG_H/*** file dialog.h* author lpl* brief 无边框dialog类* date 2024/06/05*/ #include <QDialog> #include <QMouseEvent> namespace Ui { class Dialog; } /*** brief The Dialog class* 无…

java版知识付费saas租户平台:剖析现代知识付费平台的功能架构与运营逻辑

在数字化学习的时代背景下&#xff0c;知识付费平台已经成为教育行业的一颗璀璨明星&#xff0c;以其用户需求为中心&#xff0c;提供便捷高效的学习途径。这些平台汇聚了众多专业知识&#xff0c;覆盖职业技能、生活兴趣和人文社科等多个领域&#xff0c;满足不同用户的学习需…

基于Python的AI动物识别技术研究

基于Python的AI动物识别技术研究 开发语言:Python 数据库&#xff1a;MySQL所用到的知识&#xff1a;Django框架工具&#xff1a;pycharm、Navicat、Maven 系统功能实现 系统的登录模块设计 本次设计的AI动物识别系统为了保证用户的数据安全&#xff0c;设计了登录的模块&…

网络基础-IP协议

文章目录 前言一、IP报文二、IP报文分片重组IP分片IP分片示例MTUping 命令可以验证MTU大小Windows系统&#xff1a;Linux系统: 前言 基础不牢&#xff0c;地动山摇&#xff0c;本节我们详细介绍IP协议的内容。 一、IP报文 第一行&#xff1a; 4位版本号指定IP协议的版本&#…

C++使用thread_local实现每个线程下的单例

对于一个类&#xff0c;想要在每个线程种有且只有一个实例对象&#xff0c;且线程之间不共享该实例&#xff0c;可以按照单例模式的写法&#xff0c;同时使用C11提供的thread_local关键字实现。 在单例模式的基础上&#xff0c;使用thread_local关键字修饰单例的instance&…

NineData云原生智能数据管理平台新功能发布|2024年5月版

重点发布​ 数据库 DevOps - 表分组查询​ 在企业用户规模达到一定程度后&#xff0c;分库分表成为一种常见的数据库架构选择。在这种情况下&#xff0c;查询和维护数据需要高效的解决方案&#xff0c;以避免手动逐一查询、变更和汇总多个分库和分表的繁琐操作。 库分组变更…

LLM中完全消除矩阵乘法,效果惊人!10亿参数在FPGA上运行功耗接近大脑!!

一直以来&#xff0c;矩阵乘法&#xff08;MatMul&#xff09;在神经网络操作中占据主导地位&#xff0c;主要因为GPU针对MatMul进行了优化。 老黄一举揭秘三代GPU&#xff01;打破摩尔定律&#xff0c;打造AI帝国&#xff0c;量产Blackwell解决ChatGPT全球耗电难题 这种优化使…

【适配鸿蒙next】Flutter 新一代混合栈管理框架

前言 据最新消息显示&#xff0c;华为今年下半年将全面转向其自主平台HarmonyOS&#xff0c;放弃Android系统。 报道中提到&#xff0c;下一版HarmonyOS预计将随华为即将推出的Mate 70旗舰系列一起发布。 据悉&#xff0c;HarmonyOS Next 已经扩展到4000个应用程序&#xff0c;…

C++【STL】改造红黑树简单模拟实现set map(带你了解set map的底层实现结构)

目录 一、学前铺垫&#xff08;泛型编程&#xff09; 二、改造红黑树 1.红黑树节点的改造 2.insert的改造 3.迭代器的实现 4.完整改造代码 三、set的模拟实现封装 四、map的模拟实现封装 五、完结撒❀ 前言&#xff1a; 下面为了简单模拟实现set map所出现的代码是以…

Tensorflow入门实战 P03-天气识别

目录 1、完整代码 2、运行结果 2.1 查看20张图片 2.2 程序运行 2.3 运行结果 3、小结 ① 代码运行过程中有报错&#xff1a; ② 修改代码如下&#xff1a; ③ 分析原因&#xff1a; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&…

NDIS Filter开发-PNP响应和安装

NDIS filter驱动可能是最容易生成的驱动之一&#xff0c;如果你安装了VS 2015 WDK之后&#xff0c;你可以直接生成一个能运行的Filter驱动&#xff0c;它一般是ndislwf。 和大部分硬件不同&#xff0c;NDIS Filter驱动介于软件和硬件抽象层之上&#xff0c;它和硬件相关&…

SpringCloud Gateway中Route Predicate Factories详细说明

官网地址&#xff1a;https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。 Spring Cloud Gateway …

使用 GPT-4 创作高考作文 2024年

使用 GPT-4 创作高考作文 2024年 使用 GPT-4 创作高考作文&#xff1a;技术博客指南 &#x1f914;✨摘要引言正文内容&#xff08;详细介绍&#xff09; &#x1f4da;&#x1f4a1;什么是 GPT-4&#xff1f;高考作文题目分析 ✍️&#x1f9d0;新课标I卷 人类智慧的进步&…

【C51】C51单片机实现的 抽奖机 设计与编程指南

文章目录 前言&#xff1a;1. 实现效果2. 准备工作3. 编写代码总结&#xff1a; 前言&#xff1a; 在本文中&#xff0c;我们将介绍如何使用C51单片机来实现一个简单的抽奖机。这个项目不仅能够展示C51单片机的基本应用&#xff0c;还能让我们了解如何通过编程来控制硬件&…

9.3 Go 接口的多态性

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Django 视图类

在Django框架中&#xff0c;视图类&#xff08;Class-based views&#xff0c;简称CBVs&#xff09;提供了一个面向对象的方式来定义视图。这种方式可以让你通过创建类来组织视图逻辑&#xff0c;而不是使用基于函数的视图&#xff08;Function-based views&#xff0c;简称FBV…