yolo.py文件解读

部署运行你感兴趣的模型镜像

一、前言

文件位置:./models/yolo.py
本周任务:将yolov5s网络模型中的C3模块按照下图方式修改形成C2模块,并将C2模块插入第2层与第3层之间,且跑通yolov5。
💫任务提示:

    提示1:需要修改./models/common.py、./models/yolo.py、./models/yolov5s.yaml文件
    提示2:C2模块与C3模块是非常相似的两个模块,我们要插入C2到模型当中,只需要找到哪里有C3模块,然后在其附近加上C2即可(插入到Backbone中)。

 

二、导入需要的包

import argparse     # 解析命令行参数模块
import contextlib
import os
import platform
import sys          # sys系统模块,包含了与Python解释器和它的环境有关的函数
from copy import deepcopy  # 数据拷贝模块,深拷贝
from pathlib import Path   # Path将str转换为Path对象,使字符串路径易于操作

FILE = Path(__file__).resolve()
ROOT = FILE.parents[1]  # YOLOv5 root directory
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))  # add ROOT to PATH
if platform.system() != 'Windows':
    ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative

from models.common import *
from models.experimental import *
from utils.autoanchor import check_anchor_order
from utils.general import LOGGER, check_version, check_yaml, make_divisible, print_args
from utils.plots import feature_visualization
from utils.torch_utils import (fuse_conv_and_bn, initialize_weights, model_info, profile, scale_img, select_device,
                               time_sync)

# 导入thop包,用于计算FLOPs
try:
    import thop  # for FLOPs computation
except ImportError:
    thop = None

三、改动parse_model函数

def parse_model(d, ch):  # model_dict, input_channels(3)
    # Parse a YOLOv5 model.yaml dictionary
    ''' 用在上面DetectionModel模块中
    解析模型文件(字典形式),并搭建网络结构
    这个函数其实主要做的就是:
        更新当前层的args(参数),计算c2(当前层的输出channel)
        ->使用当前层的参数搭建当前层
        ->生成 layers + save
    :params d: model_dict模型文件,字典形式{dice: 7}(yolov5s.yaml中的6个元素 + ch)
    :params ch: 记录模型每一层的输出channel,初始ch=[3],后面会删除
    :return nn.Sequential(*layers): 网络的每一层的层结构
    :return sorted(save): 把所有层结构中的from不是-1的值记下,并排序[4,6,10,14,17,20,23]
    '''
    LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10}  {'module':<40}{'arguments':<30}")
    # 读取字典d中的anchors和parameters(nc,depth_multiple,width_multiple)
    anchors, nc, gd, gw, act = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'], d.get('activation')
    if act:
        Conv.default_act = eval(act)  # redefine default activation, i.e. Conv.default_act = nn.SiLU()
        LOGGER.info(f"{colorstr('activation:')} {act}")  # print
    # na: number of anchors 每一个predict head上的anchor数=3
    na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors  # number of anchors
    # no: number of outputs 每一个predict head层的输出channel=anchors*(classes+5)=75(VOC)
    no = na * (nc + 5)  # number of outputs = anchors * (classes + 5)

    ''' 开始搭建网络
    layers: 保存每一层的层结构
    save: 记录下所有层结构中from不是-1的层结构序号
    c2: 保存当前层的输出channel
    '''
    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
    # from: 当前层输入来自哪些层
    # number: 当前层数,初定
    # module: 当前层类别
    # args: 当前层类参数,初定
    # 遍历backbone和head的每一层
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, args
        # 得到当前层的真实类名,例如:m = Focus -> <class 'models.common.Focus'>
        m = eval(m) if isinstance(m, str) else m  # eval strings
        # 没什么用
        for j, a in enumerate(args):
            with contextlib.suppress(NameError):
                args[j] = eval(a) if isinstance(a, str) else a  # eval strings

        # --------------------更新当前层的args(参数),计算c2(当前层的输出channel)--------------------
        # depth gain 控制深度,如yolov5s: n*0.33,n: 当前模块的次数(间接控制深度)
        n = n_ = max(round(n * gd), 1) if n > 1 else n  # depth gain
        if m in {
                Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
                BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x}:
            # c1: 当前层的输入channel数; c2: 当前层的输出channel数(初定); ch: 记录着所有层的输出channel数
            c1, c2 = ch[f], args[0]
            # no=75,只有最后一层c2=no,最后一层不用控制宽度,输出channel必须是no
            if c2 != no:  # if not output
                # width gain 控制宽度,如yolov5s: c2*0.5; c2: 当前层的最终输出channel数(间接控制宽度)
                c2 = make_divisible(c2 * gw, 8)

            # 在初始args的基础上更新,加入当前层的输入channel并更新当前层
            # [in_channels, out_channels, *args[1:]]
            args = [c1, c2, *args[1:]]
            # 如果当前层是BottleneckCSP/C3/C3TR/C3Ghost/C3x,则需要在args中加入Bottleneck的个数
            # [in_channels, out_channels, Bottleneck个数, Bool(shortcut有无标记)]
            if m in {BottleneckCSP, C3, C3TR, C3Ghost, C3x}:
                args.insert(2, n)  # number of repeats 在第二个位置插入Bottleneck的个数n
                n = 1 # 恢复默认值1
        elif m is nn.BatchNorm2d:
            # BN层只需要返回上一层的输出channel
            args = [ch[f]]
        elif m is Concat:
            # Concat层则将f中所有的输出累加得到这层的输出channel
            c2 = sum(ch[x] for x in f)
        # TODO: channel, gw, gd
        elif m in {Detect, Segment}:  # Detect/Segment(YOLO Layer)层
            # 在args中加入三个Detect层的输出channel
            args.append([ch[x] for x in f])
            if isinstance(args[1], int):  # number of anchors 几乎不执行
                args[1] = [list(range(args[1] * 2))] * len(f)
            if m is Segment:
                args[3] = make_divisible(args[3] * gw, 8)
        elif m is Contract:  # 不怎么用
            c2 = ch[f] * args[0] ** 2
        elif m is Expand:  # 不怎么用
            c2 = ch[f] // args[0] ** 2
        else:  # Upsample
            c2 = ch[f]  # args不变
        # -------------------------------------------------------------------------------------------

        # m_: 得到当前层的module,如果n>1就创建多个m(当前层结构),如果n=1就创建一个m
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        # 打印当前层结构的一些基本信息
        t = str(m)[8:-2].replace('__main__.', '')  # module type  <'modules.common.Focus'>
        np = sum(x.numel() for x in m_.parameters())  # number params 计算这一层的参数量
        m_.i, m_.f, m_.type, m_.np = i, f, t, np  # attach index, 'from' index, type, number params
        LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f}  {t:<40}{str(args):<30}')  # print
        # 把所有层结构中的from不是-1的值记下 [6,4,14,10,17,20,23]
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        # 将当前层结构module加入layers中
        layers.append(m_)
        if i == 0:
            ch = []  # 去除输入channel[3]
        # 把当前层的输出channel数加入ch
        ch.append(c2)
    return nn.Sequential(*layers), sorted(save)

四、调整模型

C2模块

class C2(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super().__init__()
        c_ = int(c2 * 0.5)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))

    def forward(self, x):
        # 移除cv3卷积层后,若要保持最终输出的channel仍为c2,则中间层的channel需为c2/2
        # 设置e=0.5即可,取默认值不变
        return torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1)


class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        ''' 在C3RT模块和yolo.py的parse_model函数中被调用
        :params c1: 整个C3的输入channel
        :params c2: 整个C3的输出channel
        :params n: 有n个子模块[Bottleneck/CrossConv]
        :params shortcut: bool值,子模块[Bottlenec/CrossConv]中是否有shortcut,默认True
        :params g: 子模块[Bottlenec/CrossConv]中的3x3卷积类型,=1普通卷积,>1深度可分离卷积
        :params e: expansion ratio,e*c2=中间其它所有层的卷积核个数=中间所有层的的输入输出channel
        '''
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
        # 实验性 CrossConv
        #self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)])

    def forward(self, x):
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

./models/yolo.py 在parse_model中增加对C2的解析

def parse_model(d, ch):  # model_dict, input_channels(3)
    # Parse a YOLOv5 model.yaml dictionary
    ''' 用在上面DetectionModel模块中
    解析模型文件(字典形式),并搭建网络结构
    这个函数其实主要做的就是:
        更新当前层的args(参数),计算c2(当前层的输出channel)
        ->使用当前层的参数搭建当前层
        ->生成 layers + save
    :params d: model_dict模型文件,字典形式{dice: 7}(yolov5s.yaml中的6个元素 + ch)
    :params ch: 记录模型每一层的输出channel,初始ch=[3],后面会删除
    :return nn.Sequential(*layers): 网络的每一层的层结构
    :return sorted(save): 把所有层结构中的from不是-1的值记下,并排序[4,6,10,14,17,20,23]
    '''
    LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10}  {'module':<40}{'arguments':<30}")
    # 读取字典d中的anchors和parameters(nc,depth_multiple,width_multiple)
    anchors, nc, gd, gw, act = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'], d.get('activation')
    if act:
        Conv.default_act = eval(act)  # redefine default activation, i.e. Conv.default_act = nn.SiLU()
        LOGGER.info(f"{colorstr('activation:')} {act}")  # print
    # na: number of anchors 每一个predict head上的anchor数=3
    na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors  # number of anchors
    # no: number of outputs 每一个predict head层的输出channel=anchors*(classes+5)=75(VOC)
    no = na * (nc + 5)  # number of outputs = anchors * (classes + 5)

    ''' 开始搭建网络
    layers: 保存每一层的层结构
    save: 记录下所有层结构中from不是-1的层结构序号
    c2: 保存当前层的输出channel
    '''
    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
    # from: 当前层输入来自哪些层
    # number: 当前层数,初定
    # module: 当前层类别
    # args: 当前层类参数,初定
    # 遍历backbone和head的每一层
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, args
        # 得到当前层的真实类名,例如:m = Focus -> <class 'models.common.Focus'>
        m = eval(m) if isinstance(m, str) else m  # eval strings
        # 没什么用
        for j, a in enumerate(args):
            with contextlib.suppress(NameError):
                args[j] = eval(a) if isinstance(a, str) else a  # eval strings

        # --------------------更新当前层的args(参数),计算c2(当前层的输出channel)--------------------
        # depth gain 控制深度,如yolov5s: n*0.33,n: 当前模块的次数(间接控制深度)
        n = n_ = max(round(n * gd), 1) if n > 1 else n  # depth gain
        if m in {
                Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
                BottleneckCSP, C2, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x}:
            # c1: 当前层的输入channel数; c2: 当前层的输出channel数(初定); ch: 记录着所有层的输出channel数
            c1, c2 = ch[f], args[0]
            # no=75,只有最后一层c2=no,最后一层不用控制宽度,输出channel必须是no
            if c2 != no:  # if not output
                # width gain 控制宽度,如yolov5s: c2*0.5; c2: 当前层的最终输出channel数(间接控制宽度)
                c2 = make_divisible(c2 * gw, 8)

            # 在初始args的基础上更新,加入当前层的输入channel并更新当前层
            # [in_channels, out_channels, *args[1:]]
            args = [c1, c2, *args[1:]]
            # 如果当前层是BottleneckCSP/C2/C3/C3TR/C3Ghost/C3x,则需要在args中加入Bottleneck的个数
            # [in_channels, out_channels, Bottleneck个数, Bool(shortcut有无标记)]
            if m in {BottleneckCSP, C2, C3, C3TR, C3Ghost, C3x}:
                args.insert(2, n)  # number of repeats 在第二个位置插入Bottleneck的个数n
                n = 1 # 恢复默认值1
        elif m is nn.BatchNorm2d:
            # BN层只需要返回上一层的输出channel
            args = [ch[f]]
        elif m is Concat:
            # Concat层则将f中所有的输出累加得到这层的输出channel
            c2 = sum(ch[x] for x in f)
        # TODO: channel, gw, gd
        elif m in {Detect, Segment}:  # Detect/Segment(YOLO Layer)层
            # 在args中加入三个Detect层的输出channel
            args.append([ch[x] for x in f])
            if isinstance(args[1], int):  # number of anchors 几乎不执行
                args[1] = [list(range(args[1] * 2))] * len(f)
            if m is Segment:
                args[3] = make_divisible(args[3] * gw, 8)
        elif m is Contract:  # 不怎么用
            c2 = ch[f] * args[0] ** 2
        elif m is Expand:  # 不怎么用
            c2 = ch[f] // args[0] ** 2
        else:  # Upsample
            c2 = ch[f]  # args不变
        # -------------------------------------------------------------------------------------------

        # m_: 得到当前层的module,如果n>1就创建多个m(当前层结构),如果n=1就创建一个m
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        # 打印当前层结构的一些基本信息
        t = str(m)[8:-2].replace('__main__.', '')  # module type  <'modules.common.Focus'>
        np = sum(x.numel() for x in m_.parameters())  # number params 计算这一层的参数量
        m_.i, m_.f, m_.type, m_.np = i, f, t, np  # attach index, 'from' index, type, number params
        LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f}  {t:<40}{str(args):<30}')  # print
        # 把所有层结构中的from不是-1的值记下 [6,4,14,10,17,20,23]
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        # 将当前层结构module加入layers中
        layers.append(m_)
        if i == 0:
            ch = []  # 去除输入channel[3]
        # 把当前层的输出channel数加入ch
        ch.append(c2)
    return nn.Sequential(*layers), sorted(save)

./models/yolov5s.yaml 插入C2模块

# YOLOv5 v6.0 backbone
backbone:
  # [from, number, module, args]
  [[-1, 1, Conv, [64, 6, 2, 2]],  # 0-P1/2
   [-1, 1, Conv, [128, 3, 2]],  # 1-P2/4
   [-1, 3, C3, [128]],
   [-1, 3, C2, [128]],  # 在原第2层和原第3层之间插入C2模块
   [-1, 1, Conv, [256, 3, 2]],  # 3-P3/8
   [-1, 6, C3, [256]],
   [-1, 1, Conv, [512, 3, 2]],  # 5-P4/16
   [-1, 9, C3, [512]],
   [-1, 1, Conv, [1024, 3, 2]],  # 7-P5/32
   [-1, 3, C3, [1024]],
   [-1, 1, SPPF, [1024, 5]],  # 9
  ]

您可能感兴趣的与本文相关的镜像

Yolo-v5

Yolo-v5

Yolo

YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型,由华盛顿大学的Joseph Redmon 和Ali Farhadi 开发。 YOLO 于2015 年推出,因其高速和高精度而广受欢迎

<think>我们正在讨论YOLOv8项目中的yolo.py文件。根据用户提供的引用,用户似乎对YOLOv5中的yolo.py文件有了解,现在想知道YOLOv8中对应的文件来源或作用。 注意:用户提供的引用都是关于YOLOv5的,而用户现在询问的是YOLOv8。 在YOLOv8项目中,文件结构已经发生了变化。YOLOv8由Ultralytics公司开发,其代码库与YOLOv5有相似之处但也有所不同。 在YOLOv8中,模型定义主要位于`ultralytics/nn`目录下,而训练、验证、预测等任务的管理则通过`ultralytics/models/yolo`目录下的文件进行。 具体到yolo.py文件: 1.YOLOv8的GitHub仓库中,存在一个`ultralytics/models/yolo/model.py`文件,它定义了YOLO模型类(如YOLO, DetectionModel, SegmentationModel等)。 2. 另外,在`ultralytics/nn/tasks.py`中定义了BaseModel,它是所有模型的基类,而YOLOv8的特定模型(如DetectionModel)则是在`ultralytics/nn/tasks.py`中通过构建函数创建的。 因此,YOLOv8中并没有一个与YOLOv5中完全对应的`yolo.py`文件,而是将功能分散到了多个文件中。 总结: - YOLOv8的模型构建核心文件是`ultralytics/nn/tasks.py`,其中定义了如何根据配置文件构建模型。 - 而`ultralytics/models/yolo/model.py`则定义了YOLO类,它封装了模型训练、验证、预测和导出的方法。 所以,用户询问的yolo.py文件YOLOv8中可能指的是`ultralytics/models/yolo/model.py`,或者更核心的模型构建文件`ultralytics/nn/tasks.py`。 下面,我将详细说明这两个文件的作用: 1. **`ultralytics/nn/tasks.py`**: - 这个文件定义了`BaseModel`类,所有模型都继承自这个类。 - 它包含了一个关键函数`attempt_load_weights`,用于加载预训练权重。 - 还包含了一个`DetectionModel`类,专门用于目标检测任务,这个类负责构建YOLOv8的检测网络。 - 在构建模型时,会解析模型的配置文件(yaml文件),并根据配置文件中的结构逐层构建网络。 2. **`ultralytics/models/yolo/model.py`**: - 这个文件定义了`YOLO`类,它是用户直接使用的模型类。 - 用户通过`YOLO('yolov8n.yaml')`或`YOLO('yolov8n.pt')`来创建模型时,调用的就是这个类。 - 这个类集成了训练、验证、预测和导出模型的功能,并调用`tasks.py`中的模型构建功能。 因此,如果用户想要修改模型结构,通常需要查看`ultralytics/nn/tasks.py`;如果用户想要修改训练流程等,则需要查看`ultralytics/models/yolo/model.py`。 注意:在YOLOv8中,模型配置是通过yaml文件定义的,例如`ultralytics/cfg/models/v8/yolov8n.yaml`。 为了帮助用户理解,我们可以类比YOLOv5: - YOLOv5中的`models/yolo.py`文件(如引用[1]所述)在YOLOv8中对应的核心模型构建部分被放在了`tasks.py`中。 所以,我们可以这样回答用户: 在YOLOv8中,与YOLOv5的`yolo.py`文件功能类似的核心文件是`ultralytics/nn/tasks.py`(负责模型构建)和`ultralytics/models/yolo/model.py`(负责模型任务管理)。其中,模型构建的核心逻辑在`tasks.py`中,包括解析模型配置、构建网络层等。而`model.py`则提供了高级API,用于加载、训练、验证和导出模型。 如果用户需要自定义模型结构,应该主要修改`tasks.py`中的相关类(如`DetectionModel`)或者创建新的模型类。 最后,生成相关问题。</think>在YOLOv8项目中,`yolo.py`文件是模型定义和任务执行的核心脚本,其来源和功能如下: ### 1. **文件来源** YOLOv8的`yolo.py`文件位于官方仓库的`ultralytics/models/yolo/`目录下: - **GitHub路径**:[ultralytics/ultralytics/models/yolo/model.py](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/models/yolo/model.py) - **主要功能**: - 实现`YOLO`类,提供统一的模型接口 - 集成训练、验证、预测和导出功能 - 管理模型生命周期(加载配置/权重/执行任务) ### 2. **核心功能解析** #### (1) **模型构建** 继承自`DetectionModel`类(定义在`ultralytics/nn/tasks.py`),通过解析YAML配置构建网络: ```python class YOLO(DetectionModel): def __init__(self, model='yolov8n.yaml', **kwargs): super().__init__(model=model, **kwargs) # 调用父类解析模型配置 ``` #### (2) **多任务接口** 提供统一的API支持检测/分割/姿态估计等任务: ```python # 示例:目标检测任务 model = YOLO('yolov8n.pt') # 加载预训练权重 results = model.predict('image.jpg') # 执行推理 ``` #### (3) **ONNX导出** 通过`export()`方法实现模型转换,支持多输出层: ```python model.export(format='onnx', dynamic=True, # 启用动态维度 simplify=True, # 优化模型结构 opset=13) # 指定ONNX算子集版本 ``` ### 3. **与YOLOv5的差异** | 特性 | YOLOv5 (引用[1][2]) | YOLOv8 | |--------------|-----------------------------------|---------------------------------| | **模型定义** | `models/yolo.py`中的`Detect`类 | `nn/tasks.py`中的`DetectionModel` | | **配置解析** | `parsemodel()`函数解析字典 | 直接加载YAML配置 | | **任务管理** | 分散在多个脚本 | 统一通过`YOLO`类集成 | | **损失函数** | 在`loss.py`修改(引用[4]) | 在`nn/modules/loss.py`实现 | ### 4. **关键修改点** 若要实现多输出层ONNX导出,需修改两处: 1. **`tasks.py`中的前向传播** 取消后处理,直接返回特征图: ```python class DetectionModel(BaseModel): def forward(self, x): return self._predict_once(x) # 返回原始输出层 ``` 2. **`exporter.py`中的导出逻辑** 添加动态输出命名: ```python output_names = [f"output{i}" for i in range(num_layers)] # 自动生成输出层名 ``` > 提示:Ultralytics官方推荐通过`model.export()`接口导出模型,再根据部署需求调整输出层[^1][^2]。 --- ### 相关问题 1. YOLOv8中如何修改`DetectionModel`类以实现自定义输出层? 2. YOLOv8的ONNX导出与TensorRT部署有哪些最佳实践? 3. YOLOv8的损失函数修改位置与YOLOv5有何不同? 4. 如何验证YOLOv8导出的多输出层ONNX模型的精度? [^1]: YOLOv5模型构建解析:YOLO.py文件深度解读 [^2]: YOLOv8官方文档:模型导出与部署指南
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值