PaddleClas:自定义backbone

news2025/1/8 2:11:07

PaddleClas提供的都是现成的网络结构和权重,不一定适用,所以有必要掌握魔改的技能。

PaddleClas版本:2.5

1:新建 mynet.py

  • 在 ppcls/arch/backbone/model_zoo/ 文件夹下新建一个自己的模型结构文件 mynet.py,即你自己的 backbone;

2:模型搭建

  • 在mynet.py中搭建自己的网络,可以参考model_zoo或legendary_models中的众多模型文件;
#我以legendary_models/mobilenet_v3.py为参考,添加激活函数FReLU

# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# reference: https://arxiv.org/abs/1905.02244

from __future__ import absolute_import, division, print_function

import paddle
import paddle.nn as nn
from paddle import ParamAttr
from paddle.nn import AdaptiveAvgPool2D, BatchNorm, Conv2D, Dropout, Linear
from paddle.regularizer import L2Decay

from ..base.theseus_layer import TheseusLayer
from ....utils.save_load import load_dygraph_pretrain, load_dygraph_pretrain_from_url

MODEL_URLS = {
    "MobileNetV3_large_FRELU":""
}

MODEL_STAGES_PATTERN = {
    "MobileNetV3_large_FRELU":
    ["blocks[0]", "blocks[2]", "blocks[5]", "blocks[11]", "blocks[14]"]
}

__all__ = MODEL_URLS.keys()

# "large", "small" is just for MobinetV3_large, MobileNetV3_small respectively.
# The type of "large" or "small" config is a list. Each element(list) represents a depthwise block, which is composed of k, exp, se, act, s.
# k: kernel_size
# exp: middle channel number in depthwise block
# c: output channel number in depthwise block
# se: whether to use SE block
# act: which activation to use
# s: stride in depthwise block
NET_CONFIG = {
    "large": [
        # k, exp, c, se, act, s
        [3, 16, 16, False, "frelu", 1],#[3, 16, 16, False, "relu", 1],
        [3, 64, 24, False, "frelu", 2],
        [3, 72, 24, False, "frelu", 1],
        [5, 72, 40, True, "relu", 2],
        [5, 120, 40, True, "relu", 1],
        [5, 120, 40, True, "relu", 1],
        [3, 240, 80, False, "hardswish", 2],
        [3, 200, 80, False, "hardswish", 1],
        [3, 184, 80, False, "hardswish", 1],
        [3, 184, 80, False, "hardswish", 1],
        [3, 480, 112, True, "hardswish", 1],
        [3, 672, 112, True, "hardswish", 1],
        [5, 672, 160, True, "hardswish", 2],
        [5, 960, 160, True, "hardswish", 1],
        [5, 960, 160, True, "hardswish", 1],
    ]
}
# first conv output channel number in MobileNetV3
STEM_CONV_NUMBER = 16
# last second conv output channel for "large"
LAST_SECOND_CONV_LARGE = 960
# last conv output channel number for "large"
LAST_CONV = 1280


class FReLU(nn.Layer):
    def __init__(self, dim, init_weight=False):
        super().__init__()
        self.conv = nn.Conv2D(dim, dim, 3, 1, 1, groups=dim)
        self.bn = nn.BatchNorm2D(dim)
        if init_weight:
            self.apply(self._init_weight)

    def _init_weight(self, m):
        init = nn.initializer.Normal(mean=0, std=.02)
        zeros = nn.initializer.Constant(0.)
        ones = nn.initializer.Constant(1.)
        if isinstance(m, nn.Conv2D):
            init(m.weight)
            zeros(m.bias)
        if isinstance(m, nn.BatchNorm2D):
            ones(m.weight)
            zeros(m.bias)
            

    def forward(self, x):
        x1 = self.bn(self.conv(x))
        out = paddle.maximum(x, x1)
        return out


def _make_divisible(v, divisor=8, min_value=None):
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v


def _create_act(act, dim):
    if act == "hardswish":
        return nn.Hardswish()
    elif act == "relu":
        return nn.ReLU()
    elif act == "frelu":
        return FReLU(dim)
    elif act is None:
        return None
    else:
        raise RuntimeError(
            "The activation function is not supported: {}".format(act))


class MobileNetV3_FReLU(TheseusLayer):
    """
    MobileNetV3_FReLU
    Args:
        config: list. MobileNetV3 depthwise blocks config.
        scale: float=1.0. The coefficient that controls the size of network parameters. 
        class_num: int=1000. The number of classes.
        inplanes: int=16. The output channel number of first convolution layer.
        class_squeeze: int=960. The output channel number of penultimate convolution layer. 
        class_expand: int=1280. The output channel number of last convolution layer. 
        dropout_prob: float=0.2.  Probability of setting units to zero.
    Returns:
        model: nn.Layer. Specific MobileNetV3 model depends on args.
    """

    def __init__(self,
                 config,
                 stages_pattern,
                 scale=1.0,
                 class_num=1000,
                 inplanes=STEM_CONV_NUMBER,
                 class_squeeze=LAST_SECOND_CONV_LARGE,
                 class_expand=LAST_CONV,
                 dropout_prob=0.2,
                 return_patterns=None,
                 return_stages=None,
                 **kwargs):
        super().__init__()

        self.cfg = config
        self.scale = scale
        self.inplanes = inplanes
        self.class_squeeze = class_squeeze
        self.class_expand = class_expand
        self.class_num = class_num

        self.conv = ConvBNLayer(
            in_c=3,
            out_c=_make_divisible(self.inplanes * self.scale),
            filter_size=3,
            stride=2,
            padding=1,
            num_groups=1,
            if_act=True,
            act="frelu")

        self.blocks = nn.Sequential(* [
            ResidualUnit(
                in_c=_make_divisible(self.inplanes * self.scale if i == 0 else
                                     self.cfg[i - 1][2] * self.scale),
                mid_c=_make_divisible(self.scale * exp),
                out_c=_make_divisible(self.scale * c),
                filter_size=k,
                stride=s,
                use_se=se,
                act=act) for i, (k, exp, c, se, act, s) in enumerate(self.cfg)
        ])

        self.last_second_conv = ConvBNLayer(
            in_c=_make_divisible(self.cfg[-1][2] * self.scale),
            out_c=_make_divisible(self.scale * self.class_squeeze),
            filter_size=1,
            stride=1,
            padding=0,
            num_groups=1,
            if_act=True,
            act="hardswish")

        self.avg_pool = AdaptiveAvgPool2D(1)

        self.last_conv = Conv2D(
            in_channels=_make_divisible(self.scale * self.class_squeeze),
            out_channels=self.class_expand,
            kernel_size=1,
            stride=1,
            padding=0,
            bias_attr=False)

        self.hardswish = nn.Hardswish()
        if dropout_prob is not None:
            self.dropout = Dropout(p=dropout_prob, mode="downscale_in_infer")
        else:
            self.dropout = None
        self.flatten = nn.Flatten(start_axis=1, stop_axis=-1)

        self.fc = Linear(self.class_expand, class_num)

        super().init_res(
            stages_pattern,
            return_patterns=return_patterns,
            return_stages=return_stages)

    def forward(self, x):
        x = self.conv(x)
        x = self.blocks(x)
        x = self.last_second_conv(x)
        x = self.avg_pool(x)
        x = self.last_conv(x)
        x = self.hardswish(x)
        if self.dropout is not None:
            x = self.dropout(x)
        x = self.flatten(x)
        x = self.fc(x)

        return x


class ConvBNLayer(TheseusLayer):
    def __init__(self,
                 in_c,
                 out_c,
                 filter_size,
                 stride,
                 padding,
                 num_groups=1,
                 if_act=True,
                 act=None):
        super().__init__()

        self.conv = Conv2D(
            in_channels=in_c,
            out_channels=out_c,
            kernel_size=filter_size,
            stride=stride,
            padding=padding,
            groups=num_groups,
            bias_attr=False)
        self.bn = BatchNorm(
            num_channels=out_c,
            act=None,
            param_attr=ParamAttr(regularizer=L2Decay(0.0)),
            bias_attr=ParamAttr(regularizer=L2Decay(0.0)))
        self.if_act = if_act
        self.act = _create_act(act,out_c)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        if self.if_act:
            x = self.act(x)
        return x


class ResidualUnit(TheseusLayer):
    def __init__(self,
                 in_c,
                 mid_c,
                 out_c,
                 filter_size,
                 stride,
                 use_se,
                 act=None):
        super().__init__()
        self.if_shortcut = stride == 1 and in_c == out_c
        self.if_se = use_se

        self.expand_conv = ConvBNLayer(
            in_c=in_c,
            out_c=mid_c,
            filter_size=1,
            stride=1,
            padding=0,
            if_act=True,
            act=act)
        self.bottleneck_conv = ConvBNLayer(
            in_c=mid_c,
            out_c=mid_c,
            filter_size=filter_size,
            stride=stride,
            padding=int((filter_size - 1) // 2),
            num_groups=mid_c,
            if_act=True,
            act=act)
        if self.if_se:
            self.mid_se = SEModule(mid_c)
        self.linear_conv = ConvBNLayer(
            in_c=mid_c,
            out_c=out_c,
            filter_size=1,
            stride=1,
            padding=0,
            if_act=False,
            act=None)

    def forward(self, x):
        identity = x
        x = self.expand_conv(x)
        x = self.bottleneck_conv(x)
        if self.if_se:
            x = self.mid_se(x)
        x = self.linear_conv(x)
        if self.if_shortcut:
            x = paddle.add(identity, x)
        return x


# nn.Hardsigmoid can't transfer "slope" and "offset" in nn.functional.hardsigmoid
class Hardsigmoid(TheseusLayer):
    def __init__(self, slope=0.2, offset=0.5):
        super().__init__()
        self.slope = slope
        self.offset = offset

    def forward(self, x):
        return nn.functional.hardsigmoid(
            x, slope=self.slope, offset=self.offset)


class SEModule(TheseusLayer):
    def __init__(self, channel, reduction=4):
        super().__init__()
        self.avg_pool = AdaptiveAvgPool2D(1)
        self.conv1 = Conv2D(
            in_channels=channel,
            out_channels=channel // reduction,
            kernel_size=1,
            stride=1,
            padding=0)
        self.relu = nn.ReLU()
        self.conv2 = Conv2D(
            in_channels=channel // reduction,
            out_channels=channel,
            kernel_size=1,
            stride=1,
            padding=0)
        self.hardsigmoid = Hardsigmoid(slope=0.2, offset=0.5)

    def forward(self, x):
        identity = x
        x = self.avg_pool(x)
        x = self.conv1(x)
        x = self.relu(x)
        x = self.conv2(x)
        x = self.hardsigmoid(x)
        return paddle.multiply(x=identity, y=x)


def _load_pretrained(pretrained, model, model_url, use_ssld):
    if pretrained is False:
        pass
    else:
        raise RuntimeError(
            "pretrained type is not available. Please use `string` or `boolean` type."
        )


def MobileNetV3_large_FRelu(pretrained=False, use_ssld=False, **kwargs):
    """
    MobileNetV3_large_x1_0
    Args:
        pretrained: bool=False or str. If `True` load pretrained parameters, `False` otherwise.
                    If str, means the path of the pretrained model.
        use_ssld: bool=False. Whether using distillation pretrained model when pretrained=True.
    Returns:
        model: nn.Layer. Specific `MobileNetV3_large_x1_0` model depends on args.
    """
    model = MobileNetV3_FReLU(
        config=NET_CONFIG["large"],
        scale=1.0,
        stages_pattern=MODEL_STAGES_PATTERN["MobileNetV3_large_FRELU"],
        class_squeeze=LAST_SECOND_CONV_LARGE,
        **kwargs)
    _load_pretrained(pretrained, model, MODEL_URLS["MobileNetV3_large_FRELU"],
                     use_ssld)
    return model

3:声明自己的模型

  • 在 ppcls/arch/backbone/__init__.py 中添加自己设计的 backbone 的类;

 4配置训练yaml文件

  • 参考 ppcls/configs/ImageNet/MobileNetV3/MobileNetV3_small_x1_0.yaml;

具体如下:根据自己的数据进行类别数目、数据位置的调整 

# global configs
Global:
  checkpoints: null
  pretrained_model: null
  output_dir: ./output/
  device: gpu
  save_interval: 1
  eval_during_train: True
  eval_interval: 1
  epochs: 100
  print_batch_step: 10
  use_visualdl: False
  # used for static mode and model export
  image_shape: [3, 224, 224]
  save_inference_dir: ./inference

# model architecture
Arch:
  name: MobileNetV3_large_FRELU
  class_num: 100
 
# loss function config for traing/eval process
Loss:
  Train:
    - CELoss:
        weight: 1.0
  Eval:
    - CELoss:
        weight: 1.0


Optimizer:
  name: Momentum
  momentum: 0.9
  lr:
    name: Cosine
    learning_rate: 0.04
  regularizer:
    name: 'L2'
    coeff: 0.0001


# data loader for train and eval
DataLoader:
  Train:
    dataset:
      name: ImageNetDataset
      image_root: ./dataset/CIFAR100/
      cls_label_path: ./dataset/CIFAR100/train_list.txt
      transform_ops:
        - DecodeImage:
            to_rgb: True
            channel_first: False
        - RandCropImage:
            size: 32
        - RandFlipImage:
            flip_code: 1
        - AutoAugment:
        - NormalizeImage:
            scale: 1.0/255.0
            mean: [0.485, 0.456, 0.406]
            std: [0.229, 0.224, 0.225]
            order: ''

    sampler:
      name: DistributedBatchSampler
      batch_size: 64
      drop_last: False
      shuffle: True
    loader:
      num_workers: 4
      use_shared_memory: True

  Eval:
    dataset: 
      name: ImageNetDataset
      image_root: ./dataset/CIFAR100/
      cls_label_path: ./dataset/CIFAR100/test_list.txt
      transform_ops:
        - DecodeImage:
            to_rgb: True
            channel_first: False
        - ResizeImage:
            resize_short: 36
        - CropImage:
            size: 32
        - NormalizeImage:
            scale: 1.0/255.0
            mean: [0.485, 0.456, 0.406]
            std: [0.229, 0.224, 0.225]
            order: ''
    sampler:
      name: DistributedBatchSampler
      batch_size: 64
      drop_last: False
      shuffle: False
    loader:
      num_workers: 4
      use_shared_memory: True

Infer:
  infer_imgs: docs/images/inference_deployment/whl_demo.jpg
  batch_size: 10
  transforms:
    - DecodeImage:
        to_rgb: True
        channel_first: False
    - ResizeImage:
        resize_short: 36
    - CropImage:
        size: 32
    - NormalizeImage:
        scale: 1.0/255.0
        mean: [0.485, 0.456, 0.406]
        std: [0.229, 0.224, 0.225]
        order: ''
    - ToCHWImage:
  PostProcess:
    name: Topk
    topk: 5
    class_id_map_file: ppcls/utils/imagenet1k_label_list.txt

Metric:
  Train:
    - TopkAcc:
        topk: [1, 5]
  Eval:
    - TopkAcc:
        topk: [1, 5]

 5:启动训练

python3 -m paddle.distributed.launch \
    --gpus="0" \
    tools/train.py \
        -c ./ppcls/configs/ImageNet/MobileNetV3/MobileNetV3_large_FRELU.yaml  \
        -o Global.output_dir="./output/output_CIFAR_mbv3_FRELU" \
        -o Optimizer.lr.learning_rate=0.01
         

顺利跑完,不过很尴尬的是:自己改动的FRELU版本比未原版精度低了好几个点。。。也许FRELU需要调参数吧。

 

参考:

https://github.com/PaddlePaddle/PaddleClas/blob/release/2.5/docs/zh_CN/FAQ/faq_2020_s1.md#1

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

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

相关文章

抖音seo矩阵系统源码开发部署-开源分享(二)

目录 市场背景分析 一、 抖音seo矩阵系统开发部署流程 二、 源码开发功能构思 三、 抖音seo源码开发部署注意事项 四、 部分开发代码展示 市场背景分析 账号矩阵是通过不同平台不同账号之间建立联系&#xff0c;通过将同一品牌下不同平台不同账号的粉丝流量进行账号互通&a…

【MySQL】不允许你不了解联结表

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集&#xff01; &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指…

测试人如何高效地设计自动化测试框架?

目录 前言&#xff1a; 什么是自动化测试框架&#xff1f; 好框架的定义 设计框架的思路 自动化测试实施思路&#xff1a; 自动化开展建议&#xff1a; 总结&#xff1a; 前言&#xff1a; 关于测试框架的好处&#xff0c;比如快速回归提高测试效率&#xff0c;提高测试…

IBM服务器RAID5磁盘阵列出现故障的数据恢复案例

服务器数据恢复环境&#xff1a; IBM某型号服务器&#xff0c;服务器中5块SAS磁盘组建了一组RAID5磁盘阵列。划分了一个LUN以及3个分区&#xff1a;第一个分区存放windows server系统&#xff0c;第二个分区存放SQL Server数据库&#xff0c;第三个分区存放备份文件。 服务器故…

第一章:L2JMobius学习 - 安装mariadb10数据库

L2JMobius是一套开源的 LineageII 的服务器端代码&#xff0c;使用Java语言编写。想要运行L2JMobius源程序的话&#xff0c;首先要搭建环境&#xff0c;需要安装mariadb数据库和jdk。本章节&#xff0c;我们介绍如何安装mariadb10 数据库。下载地址为&#xff08;阿里云镜像&am…

分布式运用——监控平台 Zabbix

分布式运用——监控平台 Zabbix 一、监控平台种类二、我们今天介绍Linux操作系统的传统监控平台——zabbix 6.0版本1.zabbix 是什么&#xff1f;2.**zabbix 监控原理&#xff1a;**3.Zabbix 6.0 新特性&#xff1a;4. Zabbix 6.0 功能组件&#xff1a;5.数据库6.Web 界面7.Zabb…

Xcode通过Add package自动集成第三方SDK问题汇总

问题1&#xff1a; 解决方法&#xff1a;这个问题可能是因为 Adjust 或者 Facebook 的库当中依赖的某些类库的仓库地址是 git:// 协议&#xff0c;通过这种协议与 GitHub 通讯时会使用到你的 SSH 配置&#xff0c;你电脑上相关的 ssh key 使用了 GitHub 不再支持的格式&#xf…

Java集合框架进阶学习(ArrayList源码分析、HashMap实现原理)

文章目录 1、算法复杂度1.1、时间复杂度分析1.2、空间复杂度小总结 2、List2.1、数组小总结 2.2、ArrayList源码分析2.3、单向链表2.4、双向链表小总结 3、HashMap3.1、二叉树小总结 3.2、散列表小总结 3.3、HashMap的实现原理3.4、HashMap的put方法的具体流程3.5、HashMap的扩…

简历石沉大海!这份新鲜出炉的测试用人需求分析报告揭示了原因

最近有朋友吐槽简历投递后石沉大海&#xff0c;而主动打电话面试的除了外包还是外包。软件测试就业形势真的这么糟糕了&#xff1f; 小酋决定用数据揭开真相。因此小酋选取“软件测试”、“自动化测试”、“测试开发”作为搜索关键词&#xff0c;统计了 无忧网 近一个月用人市…

目标跟踪基础:两张图片相似度算法

本文来自公众号“AI大道理” —————— 目标跟踪就是在时序帧中搜索目标的过程&#xff0c;本质上就是检索。 不管是传统的目标跟踪中的生成模型和判别模型&#xff0c;还是用深度学习来做目标跟踪&#xff0c;本质上都是来求取目标区域与搜索区域的相似度&#xff0c;这就…

C# 依赖倒置原则(DIP)

目录 一&#xff0c;引子 1.1 传统的程序架构 1.2 依赖倒置 1.3 依赖倒置的作用 二&#xff0c;依赖注入 一&#xff0c;引子 1.1 传统的程序架构 在程序执行过程中&#xff0c;传统的程序架构如图&#xff1a; 可以看到&#xff0c;在传统的三层架构中&#xff0c;层与…

CSS的学习4(盒子模型及浮动)

CSS的学习3&#xff1a;http://t.csdn.cn/xDxIJ 盒子模型 网页布局过程 先准备好相关的网页元素&#xff0c;网页元素基本都是盒子Box利用CSS设置好盒子样式&#xff0c;然后摆放到相应位置往盒子里面装内容 封装周围的HTML元素&#xff1a; 边框&#xff08;border&#…

[网鼎杯 2020 青龙组]AreUSerialz1

代码审计 得到一大串源码&#xff0c;但是不要慌&#xff0c;虽然源码很多&#xff0c;其实题目并不难 这段代码是一个简单的文件读写类 FileHandler&#xff0c;以及一个反序列化函数 unserialize() 的使用。 <?phpinclude("flag.php");highlight_file(__FILE__…

Stable Diffusion 中英文对照中文tag补全

Stable Diffusion是老外做的&#xff0c;因此全部界面都是英文的&#xff0c;因此会对国内很多英语不好的小伙伴来说是一个灾难&#xff0c;不过这里介绍大家一个自定义翻译插件的方法如下图。 还有我们在输入关键词的时候&#xff0c;由于英语水平有限对我们造成阻碍&#xf…

QTTCP客户端服务端通信

目录 网络模块介绍 TCP介绍 TCP 服务端应用实例 TCP 客户端应用实例 运行结果&#xff1a; 网络模块介绍 Qt 网络模块为我们提供了编写 TCP / IP 客户端和服务器的类。它提供了较低级别的类&#xff0c;例 如代表低级网络概念的 QTcpSocket &#xff0c; QTcpServer 和 …

智慧旅游卡APP小程序开发方案

旅游业的蓬勃发展&#xff0c;旅游卡作为一种便捷的旅游支付方式越来越受到人们的喜爱。智慧旅游卡APP小程序开发方案是一种利用微信小程序实现的在线购买旅游卡、查询旅游信息、预约旅游服务等功能的旅游卡APP。下面将详细介绍智慧旅游卡APP小程序的开发方案。 一、智慧旅…

【CDC 2023 Cooperative Aerial Robots Inspection Challenge】

CDC 2023 Cooperative Aerial Robots Inspection Challenge 合作空中机器人检查挑战赛 Install the CARIC packagesRun the flight testThe benchmark designThe UAV fleet CDC 2023 Cooperative Aerial Robots Inspection Challenge 网址 Install the CARIC packages Once t…

旅游卡APP开发解决方案

旅游业的不断发展&#xff0c;旅游卡成为了人们出行时必不可少的一项工具。旅游卡APP开发解决方案旨在为用户提供更加便捷、高效的旅游卡购买和使用体验。下面将详细介绍旅游卡APP开发解决方案的几个方面。 一、旅游卡APP开发解决方案的技术方面 旅游卡APP开发解决方案…

Python pyecharts实时画图自定义可视化经纬度热力图

目录 背景基于pyecharts内置经纬度的热力图基于自定义经纬度的热力图pyecharts库缺点不同地图坐标系区别 WGS-84 - 世界大地测量系统GCJ-02 - 国测局坐标BD-09 - 百度坐标系 背景 在业务数据统计分析中基本都会涉及到各省区的分析&#xff0c;数据可视化是数据分析的一把利器…

事件机制(事件流、事件委托、事件类型)

HTML DOM 允许 JavaScript 对 HTML 事件作出反应。JavaScript 能够在事件发生时执行&#xff0c;比如当用户点击某个 HTML 元素时。 JavaScript与HTML之间的交互是通过事件实现的。事件就是文档或浏览器窗口中发生的一些特定的交互瞬间。 目录 事件是由三部分组成 执行事件的步…