DDPM | 扩散模型代码详解【较为详细细致!!!】

news2024/11/15 8:29:19

文章目录

    • 1、UNet网络结构
      • 1.1 residual网络和attention网络的细节
      • 1.2 t 的作用
      • 1.3 DDPM 中的 Positional Embedding 的使用
      • 1.4 DDPM 中的 Positional Embedding 代码
      • 1.5 residual block
      • 1.6 attention block
      • 1.7 UNet结构
    • 2、命令行参数解析
    • 3、数据的获取与预处理
    • 4、模型的训练框架
    • 参考:

1、UNet网络结构

UNet网络的总体框架如下,右边是UNet网络的整体框架,左边是residual网络和attention网络,

在这里插入图片描述

下面是UNet网络的详解结构图,左边进行有规律地残差、下采样、attention,右边也是有规律地残差、上采样、attention,相关的代码在图中给出,

在这里插入图片描述

1.1 residual网络和attention网络的细节

熟悉CNN的同学应该能看懂下图中的大部分过程。其中的 t 是时间从0到1000的随机值,假如是888,经过Positional Embedding输出长度是128的向量,下面再经过全连接层和silu层等,下面会详细讲解Positional Embedding、residual网络和attention网络,

在这里插入图片描述

1.2 t 的作用

1、和原图像一起,计算出 t 时刻的图像 x t = 1 − α t ‾ ϵ + α t ‾ x 0 x_t=\sqrt{1-\overline{\alpha_t}}\epsilon+\sqrt{\overline{\alpha_t}}x_0 xt=1αt ϵ+αt x0
2、将 t 进行编码,编码后,加到模型中,使模型学习到当前在哪个时刻

在这里插入图片描述

1.3 DDPM 中的 Positional Embedding 的使用

左图是Transformer的Positional Embedding,行索引代表第几个单词,列索引代表每个单词的特征向量,右图是DDPM的Positional Embedding,DDPM的Positional Embedding和Transformer的Positional Embedding的区别是DDPM的Positional Embedding并不是给每个词位置编码的,只需要在1000行中随机取出一行就可以了;另一个区别是DDPM的Positional Embedding并没有按照奇数位和偶数位进行拼接,而是按照前后的sin和cos进行拼接的,虽然拼接方式不同,但是最终的效果是一样的。如下图所示,
位置编码只要能保证每一行的唯一性,以及每一行和其他行的关系性就可以了。

在这里插入图片描述

1.4 DDPM 中的 Positional Embedding 代码

代码:

class PositionalEmbedding(nn.Module):
    __doc__ = r"""..."""
    def init (self, dim, scale=1.0):
        super().__init__()
        assert dim % 2 == 0
        self.dim = dim  # 特征向量
        self.scale = scale  # 正弦函数和余弦函数的周期不做调整

    def forward(self, x):  # x:表示t,从0-1000中随机出来的一个数值,因为设置batch-size=2,所以假设x:tensor([645,958])
        device = x.device
        half_dim = self.dim // 2
        emb = math.log(10000) / half_dim
        emb = torch.exp(torch.arange(half_dim, device=device) * - emb)
        emb = torch.outer(x * self.scale, emb)
        emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
        return emb

代码解释:

下图中的 e m b 2 × 64 emb_{2\times64} emb2×64中的2表示batch-size等于2,

在这里插入图片描述

使用位置:

在这里插入图片描述

1.5 residual block

原代码:

class ResidualBlock(nn.Module):
    __doc__ = r"""Applies two conv blocks with resudual connection. Adds time and class conditioning by adding bias after first convolution.

    Input:
        x: tensor of shape (N, in_channels, H, W)
        time_emb: time embedding tensor of shape (N, time_emb_dim) or None if the block doesn't use time conditioning
        y: classes tensor of shape (N) or None if the block doesn't use class conditioning
    Output:
        tensor of shape (N, out_channels, H, W)
    Args:
        in_channels (int): number of input channels
        out_channels (int): number of output channels
        time_emb_dim (int or None): time embedding dimension or None if the block doesn't use time conditioning. Default: None
        num_classes (int or None): number of classes or None if the block doesn't use class conditioning. Default: None
        activation (function): activation function. Default: torch.nn.functional.relu
        norm (string or None): which normalization to use (instance, group, batch, or none). Default: "gn"
        num_groups (int): number of groups used in group normalization. Default: 32
        use_attention (bool): if True applies AttentionBlock to the output. Default: False
    """

    def __init__(
        self,
        in_channels,
        out_channels,
        dropout,
        time_emb_dim=None,
        num_classes=None,
        activation=F.relu,
        norm="gn",
        num_groups=32,
        use_attention=False,
    ):
        super().__init__()

        self.activation = activation

        self.norm_1 = get_norm(norm, in_channels, num_groups)
        self.conv_1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)

        self.norm_2 = get_norm(norm, out_channels, num_groups)
        self.conv_2 = nn.Sequential(
            nn.Dropout(p=dropout),
            nn.Conv2d(out_channels, out_channels, 3, padding=1),
        )

        self.time_bias = nn.Linear(time_emb_dim, out_channels) if time_emb_dim is not None else None
        self.class_bias = nn.Embedding(num_classes, out_channels) if num_classes is not None else None

        self.residual_connection = nn.Conv2d(in_channels, out_channels, 1) if in_channels != out_channels else nn.Identity()
        self.attention = nn.Identity() if not use_attention else AttentionBlock(out_channels, norm, num_groups)
    
    def forward(self, x, time_emb=None, y=None):
        out = self.activation(self.norm_1(x))
        out = self.conv_1(out)

        if self.time_bias is not None:
            if time_emb is None:
                raise ValueError("time conditioning was specified but time_emb is not passed")
            out += self.time_bias(self.activation(time_emb))[:, :, None, None]

        if self.class_bias is not None:
            if y is None:
                raise ValueError("class conditioning was specified but y is not passed")

            out += self.class_bias(y)[:, :, None, None]

        out = self.activation(self.norm_2(out))
        out = self.conv_2(out) + self.residual_connection(x)
        out = self.attention(out)

        return out

代码解释:

在这里插入图片描述
在这里插入图片描述

1.6 attention block

UNet网络中一共有5个attention block,每个attention block的输入尺寸都是256x16x16,输入尺寸和输出尺寸相同,

原代码:

class AttentionBlock(nn.Module):
    __doc__ = r"""Applies QKV self-attention with a residual connection.
    
    Input:
        x: tensor of shape (N, in_channels, H, W)
        norm (string or None): which normalization to use (instance, group, batch, or none). Default: "gn"
        num_groups (int): number of groups used in group normalization. Default: 32
    Output:
        tensor of shape (N, in_channels, H, W)
    Args:
        in_channels (int): number of input channels
    """
    def __init__(self, in_channels, norm="gn", num_groups=32):
        super().__init__()
        
        self.in_channels = in_channels
        self.norm = get_norm(norm, in_channels, num_groups)
        self.to_qkv = nn.Conv2d(in_channels, in_channels * 3, 1)
        self.to_out = nn.Conv2d(in_channels, in_channels, 1)

    def forward(self, x):
        b, c, h, w = x.shape
        q, k, v = torch.split(self.to_qkv(self.norm(x)), self.in_channels, dim=1)

        q = q.permute(0, 2, 3, 1).view(b, h * w, c)
        k = k.view(b, c, h * w)
        v = v.permute(0, 2, 3, 1).view(b, h * w, c)

        dot_products = torch.bmm(q, k) * (c ** (-0.5))
        assert dot_products.shape == (b, h * w, h * w)

        attention = torch.softmax(dot_products, dim=-1)
        out = torch.bmm(attention, v)
        assert out.shape == (b, h * w, c)
        out = out.view(b, h, w, c).permute(0, 3, 1, 2)

        return self.to_out(out) + x

代码解释:

在这里插入图片描述

1.7 UNet结构

UNet的输入有2个部分,一个输入是之前介绍的time embedding,它是需要在每个 residual block 添加进来,另外一个输入是加噪后的数据 x t x_t xt,加噪后尺寸不变,

UNet原代码:

class UNet(nn.Module):
    __doc__ = """UNet model used to estimate noise.

    Input:
        x: tensor of shape (N, in_channels, H, W)
        time_emb: time embedding tensor of shape (N, time_emb_dim) or None if the block doesn't use time conditioning
        y: classes tensor of shape (N) or None if the block doesn't use class conditioning
    Output:
        tensor of shape (N, out_channels, H, W)
    Args:
        img_channels (int): number of image channels
        base_channels (int): number of base channels (after first convolution)
        channel_mults (tuple): tuple of channel multiplers. Default: (1, 2, 4, 8)
        time_emb_dim (int or None): time embedding dimension or None if the block doesn't use time conditioning. Default: None
        time_emb_scale (float): linear scale to be applied to timesteps. Default: 1.0
        num_classes (int or None): number of classes or None if the block doesn't use class conditioning. Default: None
        activation (function): activation function. Default: torch.nn.functional.relu
        dropout (float): dropout rate at the end of each residual block
        attention_resolutions (tuple): list of relative resolutions at which to apply attention. Default: ()
        norm (string or None): which normalization to use (instance, group, batch, or none). Default: "gn"
        num_groups (int): number of groups used in group normalization. Default: 32
        initial_pad (int): initial padding applied to image. Should be used if height or width is not a power of 2. Default: 0
    """

    def __init__(
        self,
        img_channels,
        base_channels,
        channel_mults=(1, 2, 4, 8),
        num_res_blocks=2,
        time_emb_dim=None,
        time_emb_scale=1.0,
        num_classes=None,
        activation=F.relu,
        dropout=0.1,
        attention_resolutions=(),
        norm="gn",
        num_groups=32,
        initial_pad=0,
    ):
        super().__init__()

        self.activation = activation
        self.initial_pad = initial_pad

        self.num_classes = num_classes
        self.time_mlp = nn.Sequential(
            PositionalEmbedding(base_channels, time_emb_scale),
            nn.Linear(base_channels, time_emb_dim),
            nn.SiLU(),
            nn.Linear(time_emb_dim, time_emb_dim),
        ) if time_emb_dim is not None else None
    
        self.init_conv = nn.Conv2d(img_channels, base_channels, 3, padding=1)

        self.downs = nn.ModuleList()
        self.ups = nn.ModuleList()

        channels = [base_channels]
        now_channels = base_channels

        for i, mult in enumerate(channel_mults):
            out_channels = base_channels * mult

            for _ in range(num_res_blocks):
                self.downs.append(ResidualBlock(
                    now_channels,
                    out_channels,
                    dropout,
                    time_emb_dim=time_emb_dim,
                    num_classes=num_classes,
                    activation=activation,
                    norm=norm,
                    num_groups=num_groups,
                    use_attention=i in attention_resolutions,
                ))
                now_channels = out_channels
                channels.append(now_channels)
            
            if i != len(channel_mults) - 1:
                self.downs.append(Downsample(now_channels))
                channels.append(now_channels)

        self.mid = nn.ModuleList([
            ResidualBlock(
                now_channels,
                now_channels,
                dropout,
                time_emb_dim=time_emb_dim,
                num_classes=num_classes,
                activation=activation,
                norm=norm,
                num_groups=num_groups,
                use_attention=True,
            ),
            ResidualBlock(
                now_channels,
                now_channels,
                dropout,
                time_emb_dim=time_emb_dim,
                num_classes=num_classes,
                activation=activation,
                norm=norm,
                num_groups=num_groups,
                use_attention=False,
            ),
        ])

        for i, mult in reversed(list(enumerate(channel_mults))):
            out_channels = base_channels * mult

            for _ in range(num_res_blocks + 1):
                self.ups.append(ResidualBlock(
                    channels.pop() + now_channels,
                    out_channels,
                    dropout,
                    time_emb_dim=time_emb_dim,
                    num_classes=num_classes,
                    activation=activation,
                    norm=norm,
                    num_groups=num_groups,
                    use_attention=i in attention_resolutions,
                ))
                now_channels = out_channels
            
            if i != 0:
                self.ups.append(Upsample(now_channels))
        
        assert len(channels) == 0
        
        self.out_norm = get_norm(norm, base_channels, num_groups)
        self.out_conv = nn.Conv2d(base_channels, img_channels, 3, padding=1)
    
    def forward(self, x, time=None, y=None):
        ip = self.initial_pad
        if ip != 0:
            x = F.pad(x, (ip,) * 4)

        if self.time_mlp is not None:
            if time is None:
                raise ValueError("time conditioning was specified but tim is not passed")
            
            time_emb = self.time_mlp(time)
        else:
            time_emb = None
        
        if self.num_classes is not None and y is None:
            raise ValueError("class conditioning was specified but y is not passed")
        
        x = self.init_conv(x)

        skips = [x]

        for layer in self.downs:
            x = layer(x, time_emb, y)
            skips.append(x)
        
        for layer in self.mid:
            x = layer(x, time_emb, y)
        
        for layer in self.ups:
            if isinstance(layer, ResidualBlock):
                x = torch.cat([x, skips.pop()], dim=1)
            x = layer(x, time_emb, y)

        x = self.activation(self.out_norm(x))
        x = self.out_conv(x)
        
        if self.initial_pad != 0:
            return x[:, :, ip:-ip, ip:-ip]
        else:
            return x

代码解释:

整体解释结构如下:

在这里插入图片描述

—分割线—

这是时间编码的解释,self.time_mlp的输入是 t ,是0-1000中的随机数值,

在这里插入图片描述

—分割线—

这是下采样模块的解释,

在这里插入图片描述

—分割线—

这是middle部分的解释:

在这里插入图片描述

—分割线—

这是up部分的解释:

在这里插入图片描述

2、命令行参数解析

在这里插入图片描述


在这里插入图片描述

原代码:

'''
code from https://github.com/abarankab/DDPM/tree/main
'''


import argparse
import datetime
import torch
import wandb

from torch.utils.data import DataLoader
from torchvision import datasets
from ddpm import script_utils


def main():
    args = create_argparser().parse_args()
    device = args.device

    try:
        diffusion = script_utils.get_diffusion_from_args(args).to(device)
        optimizer = torch.optim.Adam(diffusion.parameters(), lr=args.learning_rate)

        if args.model_checkpoint is not None:
            diffusion.load_state_dict(torch.load(args.model_checkpoint))
        if args.optim_checkpoint is not None:
            optimizer.load_state_dict(torch.load(args.optim_checkpoint))

        if args.log_to_wandb:
            if args.project_name is None:
                raise ValueError("args.log_to_wandb set to True but args.project_name is None")

            run = wandb.init(
                project=args.project_name,
                # entity='treaptofun',  # 用于指定实验所属的团队或组织
                config=vars(args),
                name=args.run_name,
            )
            wandb.watch(diffusion)

        batch_size = args.batch_size

        train_dataset = datasets.CIFAR10(
            root='./cifar_train',
            train=True,
            download=True,
            transform=script_utils.get_transform(),
        )

        test_dataset = datasets.CIFAR10(
            root='./cifar_test',
            train=False,
            download=True,
            transform=script_utils.get_transform(),
        )

        train_loader = script_utils.cycle(DataLoader(
            train_dataset,
            batch_size=batch_size,
            shuffle=True,
            drop_last=True,
            num_workers=0,
        ))
        test_loader = DataLoader(test_dataset, batch_size=batch_size, drop_last=True, num_workers=0)
        
        acc_train_loss = 0

        for iteration in range(1, args.iterations + 1):
            diffusion.train()

            x, y = next(train_loader)
            x = x.to(device)
            y = y.to(device)

            if args.use_labels:
                loss = diffusion(x, y)
            else:
                loss = diffusion(x)

            acc_train_loss += loss.item()

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            diffusion.update_ema()
            
            if iteration % args.log_rate == 0:
                test_loss = 0
                with torch.no_grad():
                    diffusion.eval()
                    for x, y in test_loader:
                        x = x.to(device)
                        y = y.to(device)

                        if args.use_labels:
                            loss = diffusion(x, y)
                        else:
                            loss = diffusion(x)

                        test_loss += loss.item()
                
                if args.use_labels:
                    samples = diffusion.sample(10, device, y=torch.arange(10, device=device))
                else:
                    samples = diffusion.sample(10, device)
                
                samples = ((samples + 1) / 2).clip(0, 1).permute(0, 2, 3, 1).numpy()

                test_loss /= len(test_loader)
                acc_train_loss /= args.log_rate

                wandb.log({
                    "test_loss": test_loss,
                    "train_loss": acc_train_loss,
                    "samples": [wandb.Image(sample) for sample in samples],
                })

                acc_train_loss = 0
            
            if iteration % args.checkpoint_rate == 0:
                model_filename = f"{args.log_dir}/{args.project_name}-{args.run_name}-iteration-{iteration}-model.pth"
                optim_filename = f"{args.log_dir}/{args.project_name}-{args.run_name}-iteration-{iteration}-optim.pth"

                torch.save(diffusion.state_dict(), model_filename)
                torch.save(optimizer.state_dict(), optim_filename)
        
        if args.log_to_wandb:
            run.finish()
    except KeyboardInterrupt:
        if args.log_to_wandb:
            run.finish()
        print("Keyboard interrupt, run finished early")


def create_argparser():
    device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
    run_name = datetime.datetime.now().strftime("ddpm-%Y-%m-%d-%H-%M")
    defaults = dict(
        learning_rate=2e-4,
        batch_size=2,
        iterations=800000,

        log_to_wandb=True,
        log_rate=1000,
        checkpoint_rate=1000,
        log_dir="~/ddpm_logs",
        project_name="Enzo_ddpm",
        run_name=run_name,

        model_checkpoint=None,
        optim_checkpoint=None,

        schedule_low=1e-4,
        schedule_high=0.02,

        device=device,
    )
    defaults.update(script_utils.diffusion_defaults())

    parser = argparse.ArgumentParser()
    script_utils.add_dict_to_argparser(parser, defaults)
    return parser


if __name__ == "__main__":
    main()

3、数据的获取与预处理

为什么要将图像 x 0 x_0 x0 像素值映射到 [-1,1]之间?

  • 因为图像后面加入的噪声是服从均值为0,方差为1的分布,原图像的像素值要和噪音的值做一个加权和(也就是加噪过程),所以也需要把原图像处理为均值为0的分布,

在这里插入图片描述

4、模型的训练框架

下图是betas的生成代码,以及代码的整体框架,

在这里插入图片描述

参考:

1、哔哩哔哩视频
2、https://github.com/Enzo-MiMan/cv_related_collections/tree/main/diffusion

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

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

相关文章

stm32的UART重定向printf()

1配置好uart 2打开usart.c文件 3在此文件前面添加头文件 4在末尾添加重定向代码 添加的代码 /* USER CODE BEGIN 1 *///加入以下代码,支持printf函数,而不需要选择use MicroLIB //#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #if 1 //#pragma import(__use_n…

microsoft edge怎么关闭安全搜索

microsoft edge浏览器为用户提供了安全搜索功能,旨在帮助用户过滤掉搜索结果中出现的不当信息。然而,有些用户可能觉得安全搜索功能限制了他们的浏览体验或工作需求。下面就给大家带来关闭microsoft edge安全搜索的相关内容,一起来看看吧。&a…

java 函数接口Consumer简介与示例【函数式编程】【Stream】

Java 8 中的 消费者接口Consumer 是一个函数接口,它可以接受一个泛型 类型参数,它属于java.util.function包。 accept(T) 方法:是 Consumer 函数式接口的方法,传入单个输入参数,无返回值,可以用于 Lambda 表…

日本央行还会加息?机构与市场唱反调!

最近,关于日本央行是否会继续加息的话题引发了市场热议。一边是市场对加息预期大幅下降,另一边却有像先锋、M&G这样的国际知名资产管理公司坚定地看好日本央行的进一步紧缩。 这究竟是怎么回事呢? 市场与机构的观点分歧 市场看跌加息&am…

如何通过社交媒体有效促进口碑营销?

在一个广告无处不在的时代,大多数品牌不能再盲目的选择传统的广告轰炸,而应依靠口碑营销,通过消费者的自发传播实现了品牌的快速崛起。 口碑营销的几个关键的传播要素: 真实性:在广告反感的时代,消费者更倾…

Java常用API第二篇

正则表达式: 正则表达式(简称 regex)是用来描述字符串模式的工具,常用于字符串的查找、匹配、替换等操作。它在文本处理、数据验证、以及编程中非常常见。以下是正则表达式的基本知识点: 1. 正则表达式的基础符号 . (…

【Datawhale AI夏令营第四期】 魔搭-大模型应用开发方向笔记 Task04 RAG模型 人话八股文Bakwaan_Buddy项目创空间部署

【Datawhale AI夏令营第四期】 魔搭-大模型应用开发方向笔记 Task04 RAG模型 人话八股文Bakwaan_Buddy项目创空间部署 什么是RAG: 我能把这个过程理解为Kimi.ai每次都能列出的一大堆网页参考资料吗?Kimi学了这些资料以后,根据这里面的信息综…

期权有指定的交易场所吗?

在当前的金融市场环境下,设立专门的股票期权交易所,比如上交所,深交所和中金所,都是国内出门的交易场所,不过期权交易都是在券商和期货开通账户买卖,但这并不妨碍投资者通过其他途径参与期权投资&#xff0…

DHCP原理及实验

目录 1.基础知识 (1)基本概念 (2)DHCP优点 2.工作原理 3.私接路由器解决方法 4.实验搭建 (1)实验top (2)实验概述 5.配置命令 (1)基础配置 &#…

音境剧场:开启声学体验的全新纪元—轻空间

在现代建筑和空间设计中,声学环境越来越受到重视。一个好的声学空间,不仅能提升使用者的感受,还能为各种活动提供最优质的音效体验。作为创新声学空间的代表作,“音境剧场”应运而生,它不仅是一个多功能场馆&#xff0…

FunHPC算力平台评测

作为内测老用户,已经用DeepLn平台(现改名为FunHPC平台)好久了,一路见证了平台从最初100多人的小群到现在满群的状态,FunHpc平台确实在一步步的走向成熟,一步步的变大。趁着现在活动的时间,发篇文…

ant design pro 中用户的表单如何控制多个角色

ant design pro 如何去保存颜色ant design pro v6 如何做好角色管理ant design 的 tree 如何作为角色中的权限选择之一ant design 的 tree 如何作为角色中的权限选择之二ant design pro access.ts 是如何控制多角色的权限的 看上面的图片 当创建或编辑一个用户时,…

vue3里面的组件实例类型(包括原生的html标签类型)

在 通过 ref(null)获取组件的时候,我们想要为 组件标注组件类型,可以通过 any 类型来进行标注,但是很明显,这些的代码很不优雅,所以我们可以利用 vue3 里面的 InstanceType 来进行类型标注 这是…

基于STM32开发的智能窗帘控制系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化光照检测与窗帘控制手动控制与状态指示Wi-Fi通信与远程控制应用场景 家庭智能窗帘办公室环境控制常见问题及解决方案 常见问题解决方案结论 1. 引言 智能窗帘控制系统通过集成光照…

去中心的投票平台

项目介绍: 前端: vue3vant4 工具: vscode和vant在线平台:Vant - Mobile UI Components 交互: Web3.js库 工具:小狐狸钱包(MetaMask) 后端: solisity 工具&#xff1…

Java并发必杀技!线程池让你的程序速度飙升不止一点点!

文章目录 1 线程池的工作机制是什么?2 线程池的任务出现异常该怎么解决?3 线程池的内存泄露该如何解决? 近期迷上了举例子来结合知识点学习,尽量减少枯燥,如有错见谅哈~ 1 线程池的工作机制是什么? 线程池…

登录方式(c语言)

1.//描述 //有个软件系统登录的用户名和密码为(用户名:admin,密码:admin),用户输入用户名和密码,判断是否登录成功。 //输入描述: //多组测试数据,每行有两个用空格分开的…

Java语言程序设计——篇十五(1)

🌿🌿🌿跟随博主脚步,从这里开始→博主主页🌿🌿🌿 欢迎大家:这里是我的学习笔记、总结知识的地方,喜欢的话请三连,有问题可以私信🌳🌳&…

K8s节点状态 NotReady排查

k8s节点由 Ready变成 NotReady izbp12ghzy6koox6fqt0suz NotReady slave 97d v1.23.3 izbp12ghzy6koox6fqt0svz Ready control-plane,master 98d v1.23.3节点进入 NotReady 状态可能是由于多种原因引起的,尤其是在资源过量分配&am…

CSS+JS实现一个鼠标移动的高亮边框效果

一、过程分析 先上效果: 在Windows系统里有一个很棒的细节效果,元素的渐变高亮边框是可以感知鼠标的,边框的高亮部分会跟随鼠标的移动而移动。 这种效果也是比较常见的,但是实现起来还是需要一点时间和思路的。 首先&#xff0…