Pytorch CUDA Reflect Padding 算子实现详解

CUDA 简介

  • CUDA(Compute Unified Device Architecture)是由NVIDIA开发的一种并行计算平台和应用编程接口(API),允许软件开发者和软件工程师使用NVIDIA的图形处理单元(GPU)进行通用计算。自2007年推出以来,CUDA已经使得利用GPU的强大计算能力进行高性能计算(HPC)和复杂图形渲染成为可能,广泛应用于科学计算、工程、机器学习和深度学习等领域。
  • CUDA 相关资料
    • 官方文档:https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
    • 入门样例:https://cuda-tutorial.readthedocs.io/en/latest/tutorials/tutorial01/

Reflect Padding 介绍

  • 反射填充是一种常见的图像边缘填充技术,用于卷积神经网络中,特别是在处理图像数据时。它通过镜像边缘像素来扩展图像的尺寸,从而使得边缘信息在卷积操作中得到更好的保留。reflect padding 样例如下图所示:
    reflect padding visualization
  • Q: 反射填充与零填充在实际应用中有何不同?
    • A: 反射填充通过复制边缘像素来扩展图像,保持了图像边缘的自然连续性,而零填充则在边缘添加零值,可能会在卷积后引入人为的边缘效应。

Pytorch Reflect Padding 实现

  • torch reflect padding 文档:https://pytorch.org/docs/stable/generated/torch.nn.ReflectionPad2d.html
>>> import torch.nn as nn
>>> import torch
>>> m = nn.ReflectionPad2d(2)
>>> input = torch.arange(9, dtype=torch.float).reshape(1, 1, 3, 3)
>>> input
tensor([[[[0., 1., 2.],[3., 4., 5.],[6., 7., 8.]]]])
>>> m(input)
tensor([[[[8., 7., 6., 7., 8., 7., 6.],[5., 4., 3., 4., 5., 4., 3.],[2., 1., 0., 1., 2., 1., 0.],[5., 4., 3., 4., 5., 4., 3.],[8., 7., 6., 7., 8., 7., 6.],[5., 4., 3., 4., 5., 4., 3.],[2., 1., 0., 1., 2., 1., 0.]]]])
>>> # using different paddings for different sides
>>> m = nn.ReflectionPad2d((1, 1, 2, 0))
>>> m(input)
tensor([[[[7., 6., 7., 8., 7.],[4., 3., 4., 5., 4.],[1., 0., 1., 2., 1.],[4., 3., 4., 5., 4.],[7., 6., 7., 8., 7.]]]])

CUDA Reflect Padding 代码实现理解

forward
  • reflection_pad2d_out_template 实现,用于执行二维反射填充。
// 定义一个函数,用于对输入Tensor进行二维反射填充,并将结果输出到output Tensor。
void reflection_pad2d_out_template(Tensor &output, const Tensor &input_, IntArrayRef padding) {// 检查输入Tensor是否可以使用32位索引数学运算。TORCH_CHECK(canUse32BitIndexMath(input_),"input tensor must fit into 32-bit index math");// 初始化一些维度标识符和批次大小。int plane_dim = 0;int dim_h = 1;int dim_w = 2;int nbatch = 1;// 检查输入Tensor和padding参数是否合法。at::native::padding::check_valid_input<2>(input_, padding);// 如果输入Tensor是4维的,说明有批次维度,需要相应调整其他维度的索引,并更新批次大小。if (input_.ndimension() == 4) {nbatch = input_.size(0);plane_dim++;dim_h++;dim_w++;}// 从padding参数中提取左、右、上、下四个方向的填充大小。int64_t pad_l = padding[0];int64_t pad_r = padding[1];int64_t pad_t = padding[2];int64_t pad_b = padding[3];// 获取输入Tensor在不同维度上的大小。int nplane = input_.size(plane_dim);int input_h = input_.size(dim_h);int input_w = input_.size(dim_w);// 检查左右填充大小是否小于输入宽度,上下填充大小是否小于输入高度。TORCH_CHECK(pad_l < input_w && pad_r < input_w, ...);TORCH_CHECK(pad_t < input_h && pad_b < input_h, ...);// 计算输出Tensor的高度和宽度。int output_h = input_h + pad_t + pad_b;int output_w = input_w + pad_l + pad_r;// 确保计算出的输出Tensor尺寸是有效的。TORCH_CHECK(output_w >= 1 || output_h >= 1, ...);// 根据输入Tensor的维度,调整输出Tensor的尺寸。if (input_.ndimension() == 3) {output.resize_({nplane, output_h, output_w});} else {output.resize_({nbatch, nplane, output_h, output_w});}// 如果输出Tensor为空,则不执行后续操作。if (output.numel() == 0) {return;}// 确保输入Tensor是连续的,便于后续处理。Tensor input = input_.contiguous();// 计算输出平面的大小,用于配置CUDA核函数的参数。int64_t output_plane_size = output_h * output_w;dim3 block_size(output_plane_size > 256 ? 256 : output_plane_size);// 准备在CUDA核函数中使用的变量。int64_t size_y = nplane;int64_t size_z = nbatch;// 对所有数据类型执行反射填充操作AT_DISPATCH_ALL_TYPES_AND_COMPLEX_AND2(kHalf, kBFloat16,input.scalar_type(), "reflection_pad2d_out_template", [&] {// 遍历所有平面和批次进行填充for (int64_t block_y = 0; block_y < size_y; block_y += 65535) {int64_t block_y_size = std::min(size_y - block_y, static_cast<int64_t>(65535));for (int64_t block_z = 0; block_z < size_z; block_z += 65535) {int64_t block_z_size = std::min(size_z - block_z, static_cast<int64_t>(65535));// 计算网格大小并启动CUDA核心dim3 grid_size(at::ceil_div(output_plane_size, static_cast<int64_t>(256)), block_y_size, block_z_size);// 计算网格大小并启动CUDA核心// 这里使用了CUDA的核心启动语法,`<<<grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>`,// 其中grid_size和block_size是CUDA核心执行时网格和块的维度配置,这里的0表示使用默认的共享内存大小,// at::cuda::getCurrentCUDAStream()获取当前CUDA流,用于并行计算。reflection_pad2d_out_kernel<<<grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>(// 传递给核心函数的参数,包括输入和输出张量的数据指针,// 输入的宽度和高度,四个方向的填充大小,当前处理的平面和批次索引,以及平面的总数。input.const_data_ptr<scalar_t>(), output.mutable_data_ptr<scalar_t>(),input_w, input_h,pad_t, pad_b, pad_l, pad_r, block_y, block_z, nplane);// 检查CUDA核心启动后是否有错误发生C10_CUDA_KERNEL_LAUNCH_CHECK();}}});
}

代码的最后部分是关键的,它展示了如何调用CUDA核心函数(reflection_pad2d_out_kernel)来实际执行反射填充操作。这个核心函数利用 CUDA 的并行计算能力,对输入张量的每个元素进行填充处理,确保在 GPU 上高效地完成操作。C10_CUDA_KERNEL_LAUNCH_CHECK() 是用于检测核心启动后是否有任何错误发生。

  • reflection_pad2d_out_kernel 实现:CUDA reflect pad2d 核函数。它接收输入和输出张量的指针、输入尺寸、填充尺寸和平面偏移量,然后计算每个线程应处理的输出张量中的像素位置,并根据输入张量中相应位置的值来填充它。
template<typename scalar_t>
__global__ void reflection_pad2d_out_kernel(const scalar_t * input, scalar_t * output,int64_t input_dim_x, int64_t input_dim_y,int pad_t, int pad_b, int pad_l, int pad_r, int y_shift, int z_shift, int nplane) {// 计算当前线程负责的输出位置auto output_xy = threadIdx.x + blockIdx.x * blockDim.x;// 计算输出维度auto output_dim_x = input_dim_x + pad_l + pad_r;auto output_dim_y = input_dim_y + pad_t + pad_b;// 如果当前线程负责的位置在输出范围内if (output_xy < output_dim_x * output_dim_y) {// 获取输入和输出索引映射auto index_pair = get_index_mapping2d(input_dim_x, input_dim_y,output_dim_x, output_dim_y,pad_l, pad_t,output_xy, y_shift, z_shift, nplane);// 根据映射关系复制数据output[index_pair.second] = input[index_pair.first];}
}
  • get_index_mapping2d 函数实现:基于输出像素位置、填充参数和偏移量,计算出反射填充后的输入和输出索引。这个函数利用了 CUDA 的内置函数 abs 来处理反射逻辑,确保输出位置正确地映射到输入张量上
// 定义一个 mapping 函数,用于计算从输出位置到输入位置的索引映射。
__device__
inline thrust::pair<int64_t, int64_t>  get_index_mapping2d(int64_t input_dim_x, int64_t input_dim_y,int64_t output_dim_x, int64_t output_dim_y,int64_t pad_l, int64_t pad_t,int64_t output_xy, int y_shift, int z_shift, int nplane) {// 计算输入和输出的偏移量,考虑了批次和通道的变化。auto input_offset =((blockIdx.y + y_shift) + (blockIdx.z + z_shift) * nplane) * input_dim_x * input_dim_y;auto output_offset =((blockIdx.y + y_shift) + (blockIdx.z + z_shift) * nplane) * output_dim_x * output_dim_y;// 根据线性索引计算输出坐标。auto output_x = output_xy % output_dim_x;auto output_y = output_xy / output_dim_x;// 计算输入和输出坐标的起始点。auto i_start_x = ::max(int64_t(0), -pad_l);auto i_start_y = ::max(int64_t(0), -pad_t);auto o_start_x = ::max(int64_t(0), pad_l);auto o_start_y = ::max(int64_t(0), pad_t);// 根据反射逻辑计算输入坐标。auto input_x = ::abs(output_x - pad_l)- ::abs(output_x - (input_dim_x + pad_l - 1))- output_x+ 2 * pad_l + input_dim_x - 1- o_start_x + i_start_x;auto input_y = ::abs(output_y - pad_t)- ::abs(output_y - (input_dim_y + pad_t - 1))- output_y+ 2 * pad_t + input_dim_y - 1- o_start_y + i_start_y;// 返回输入和输出坐标的线性索引对。return thrust::make_pair<int64_t, int64_t>(input_offset + input_y * input_dim_x + input_x,output_offset + output_y * output_dim_x + output_x);
}
backward
  • backward 与 forward 整体实现思路接近,主要是梯度反传时逻辑与前传时需要反过来,代码实现思路基本和之前介绍的 forward 部分一致
  • backward 函数入口
// 定义一个函数,用于计算二维反射填充的梯度输出。
void reflection_pad2d_backward_out_template(Tensor &grad_input, const Tensor &grad_output_,const Tensor &input, IntArrayRef padding) {// 如果梯度输入的元素数为0,则不执行任何操作。if (grad_input.numel() == 0) {return;}// 检查输入张量和梯度输出张量是否可以使用32位索引进行数学运算,如果不可以则抛出错误。TORCH_CHECK(canUse32BitIndexMath(input),"input tensor must fit into 32-bit index math");TORCH_CHECK(canUse32BitIndexMath(grad_output_),"output gradient tensor must fit into 32-bit index math");// 初始化一些维度和批次的变量,用于后续的张量尺寸计算。int plane_dim = 0;int dim_h = 1;int dim_w = 2;int nbatch = 1;// 如果输入张量的维度是4,说明有一个批次维度,需要相应地调整其他维度的索引,并计算批次大小。if (input.ndimension() == 4) {nbatch = input.size(0);plane_dim++;dim_h++;dim_w++;}// 解析padding参数,得到左、右、上、下的填充尺寸。int64_t pad_l = padding[0];int64_t pad_r = padding[1];int64_t pad_t = padding[2];int64_t pad_b = padding[3];// 计算输入张量在特定维度上的尺寸。int nplane = input.size(plane_dim);int input_h = input.size(dim_h);int input_w = input.size(dim_w);// 根据输入尺寸和填充尺寸计算输出尺寸。int output_h = input_h + pad_t + pad_b;int output_w  = input_w + pad_l + pad_r;// 检查梯度输出张量的尺寸是否与预期一致,如果不一致则抛出错误。TORCH_CHECK(output_w == grad_output_.size(dim_w), "grad_output width unexpected. Expected: ", output_w, ", Got: ", grad_output_.size(dim_w));TORCH_CHECK(output_h == grad_output_.size(dim_h), "grad_output height unexpected. Expected: ", output_h, ", Got: ", grad_output_.size(dim_h));// 为了保证数据的连续性,将梯度输出张量转换为连续的。Tensor grad_output = grad_output_.contiguous();// 计算输出平面的大小,用于后续的CUDA核函数配置。int64_t output_plane_size = output_h * output_w;// 配置CUDA核函数的线程块大小,取256或输出平面大小的较小者。dim3 block_size(output_plane_size > 256 ? 256 : output_plane_size);// 准备循环遍历的尺寸变量。int64_t size_y = nplane;int64_t size_z = nbatch;// 对输入张量的数据类型进行分派,支持多种浮点和复数类型。AT_DISPATCH_FLOATING_AND_COMPLEX_TYPES_AND2(kHalf, kBFloat16,input.scalar_type(), "reflection_pad2d_backward_out_template", [&] {// 对每个平面(通道)和批次进行循环,处理大于65535的情况。for (int64_t block_y = 0; block_y < size_y; block_y += 65535) {int64_t block_y_size = std::min(size_y - block_y, static_cast<int64_t>(65535));for (int64_t block_z = 0; block_z < size_z; block_z += 65535) {int64_t block_z_size = std::min(size_z - block_z, static_cast<int64_t>(65535));// 计算网格大小,用于CUDA核函数的配置。dim3 grid_size(at::ceil_div(output_plane_size, static_cast<int64_t>(256)), block_y_size, block_z_size);// 调用CUDA核函数,计算梯度输入。reflection_pad2d_backward_out_kernel<<<grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>(grad_input.mutable_data_ptr<scalar_t>(), grad_output.const_data_ptr<scalar_t>(),input_w, input_h,pad_t, pad_b, pad_l, pad_r, block_y, block_z, nplane);// 检查CUDA核函数的启动是否有错误。C10_CUDA_KERNEL_LAUNCH_CHECK();}}});
}
  • reflection_pad2d_backward_out_kernel 实现:
// 定义模板函数,用于CUDA内核,处理反射填充的梯度反向传播。
template <typename scalar_t>
__global__ void reflection_pad2d_backward_out_kernel(scalar_t * grad_input, // 指向梯度输入的指针,即对应前向传播输入的梯度const scalar_t * grad_output, // 指向梯度输出的指针,即损失函数对输出的偏导int64_t input_dim_x, // 输入的宽度int64_t input_dim_y, // 输入的高度int pad_t, // 顶部填充的大小int pad_b, // 底部填充的大小int pad_l, // 左侧填充的大小int pad_r, // 右侧填充的大小int y_shift, // 平面(plane)的偏移量,用于多通道数据处理int z_shift, // 批量的偏移量,用于批处理int nplane) { // 通道数或平面数auto output_xy = threadIdx.x + blockIdx.x * blockDim.x; // 计算当前线程处理的输出位置索引auto output_dim_x = input_dim_x + pad_l + pad_r; // 计算经过填充后的输出宽度auto output_dim_y = input_dim_y + pad_t + pad_b; // 计算经过填充后的输出高度// 判断当前线程负责的输出位置是否在有效范围内if (output_xy < output_dim_x * output_dim_y) {// 计算输出位置对应的输入位置索引auto index_pair = get_index_mapping2d(input_dim_x, input_dim_y,output_dim_x, output_dim_y,pad_l, pad_t,output_xy, y_shift, z_shift, nplane);// 使用原子操作累加计算梯度输入。这里的原子操作确保了多个线程更新同一位置时的正确性。gpuAtomicAddNoReturn(&grad_input[index_pair.first], grad_output[index_pair.second]);}
}

总结

  • PyTorch 中的 CUDA 反射填充通过两个 CUDA 核函数实现:reflection_pad2d_out_kernelreflection_pad2d_backward_out_kernel
    这两个核函数利用了 CUDA 的并行计算能力,可以高效地执行反射填充操作。
    • 其中 reflection_pad2d_out_kernel 理解了之后 reflection_pad2d_backward_out_kernel 理解起来就比较容易了
    • 代码的核心逻辑主要是在 padding 时输入输出之间的映射关系实现部分,也即 get_index_mapping2d 函数实现需要关注下具体实现细节
  • 通过这篇博客,我们简单介绍了 CUDA 和反射填充的概念和应用,提供了实际的代码实现理解和对应资源的链接,希望能帮助读者更深入地理解并利用这些技术。

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

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

相关文章

NodeJS 集群模块: 为App创建集群实例

NodeJS 集群模块: 为App创建集群实例 目录 NodeJS 集群模块: 为App创建集群实例Node.js 集群介绍:终极扩展策略使用集群模块开始扩展 Node.js扩展 Node.js 的两种集群策略使用集群受益的 Node.js 应用程序示例没有集群的 Node.js:不可伸缩的原则集群操作:一个扩展的 Node.js…

Redis 配置与优化

目录 一、Redis 介绍 1.1、关系数据库与非关系型数据库 1&#xff09;关系型数据库 2&#xff09;非关系型数据库 3&#xff09;非关系型数据库产生背景 1.2、Redis 基础 1&#xff09;Redis 简介 2&#xff09;Redis 安装部署 3&#xff09;配置参数 1.3、Redi…

【聊一聊】三种工厂模式的创建

三种工厂模式的创建 今天终于星期五了,最近由于碰上一个需求,中间涉及Oracle改国产数据库的改造,好家伙,差点没把我忙坏了 不过今天终于有空啦!~哈哈哈 这篇本应该是上周就结束的,但是拖到今天,我们就今天进行结束 (还有一件快乐的事情,就是我遇见自己喜欢的人啦!嘻嘻) 好啦!~话…

【AI】发现一款运行成本较低的SelfHosting语言模型

【背景】 作为一个想构建局域网AI服务的屌丝,一直苦恼的自然是有限的资源下有没有对Spec要求低一点的SelfHosting的AI服务框架了。今天给大家介绍这款听起来有点希望,但是我也还没试验过,感兴趣的可以去尝试看看。 【介绍】 大模型生成式AI与别的技术不同,由于资源要求高…

Linux第83步_采用“Linux内核定时器”点灯以及相关API函数

“Linux内核定时器”是采用“系统时钟”来实现的。它不是周期性运行的&#xff0c;一旦发生超时就会自动关闭。如果想要实现周期性定时&#xff0c;那么就需要在定时处理函数中重新开启定时器。 Limux内核使用全局变量jiffies来记录“系统从启动以来的系统节拍数”&#xff0c…

路由器的端口映射能实现什么?

路由器的端口映射是一项重要的网络配置功能&#xff0c;它可以帮助实现局域网内外的设备之间的通信。通过端口映射&#xff0c;我们可以在公网上访问局域网内的设备&#xff0c;方便的进行远程访问、共享文件和资源等操作。 什么是端口映射&#xff1f; 在介绍端口映射之前&am…

【springcloud开发教程】spring cloud config——分布式配置

什么是SpringCloud config分布式配置中心&#xff1f; spring cloud config 为微服务架构中的微服务提供集中化的外部支持&#xff0c;配置服务器为各个不同微服务应用的所有环节提供了一个中心化的外部配置。 spring cloud config 分为服务端和客户端两部分。 服务端也称为 …

MySQL 查询性能优化

优质博文&#xff1a;IT-BLOG-CN​ 如果把查询看作是一个任务&#xff0c;那么它由一些列子任务组成&#xff0c;每个子任务都会消耗一定的时间。如果要优化查询&#xff0c;实际上要优化其子任务&#xff0c;要么消除其中一些子任务&#xff0c;要么减少子任务的执行次数。通常…

【prometheus-operator】k8s监控redis

1、准备exporter https://github.com/oliver006/redis_exporter oliver006-redis_exporter-amd64.tar # 安装镜像 docker load -i oliver006-redis_exporter-amd64.tar # 上传镜像 docker tag oliver006/redis_exporter ip/monitor/redis_exporter:latest docker push ip/mo…

DevEco Profiler性能调优工具简介

一、概述 应用或服务运行期间可能出现响应速度慢、动画播放不流畅、列表拖动卡顿、应用崩溃或耗电量过高、发烫、交互延迟等现象,这些现象表明应用或服务可能存在性能问题。造成性能问题的原因可能是业务逻辑、应用代码对系统API的误用、对ArkTS对象的不合理持有导致内存泄露…

记录开发STM32遇到的卡死问题-串口

背景&#xff1a;以STM32作为主控&#xff0c;广州大彩显示屏显示&#xff0c;主控实时采集数据&#xff0c;串口波特率115200.设置收发频率为50Hz&#xff0c;即单片机每秒发送50帧数据&#xff0c;每秒接收50帧数据&#xff0c;每帧数据大概14字节。 问题&#xff1a;系统长…

部署prometheus 监控k8s集群

目录 1、主机清单 2、拉取镜像 3、服务安装 4、安装prometheus-operator 5、查看custom metrics api 6、获取prometheus端口 7、将 alertmanager-main 、grafana、prometheus-k8s的端口暴露出来 8、再次查看prometheus端口 9、浏览器访问IP&#xff1a;31940 部署k8集群…

隐私计算实训营学习三:隐私计算框架的架构和技术要点

文章目录 一、隐语架构二、产品层三、算法层3.1 PSI与PIR3.2 Data Analysis-SCQL3.3 Federated Learning 四、计算层4.1 混合调度编译-RayFed4.2 密态引擎4.3 密码原语YACL 五、资源管理层六、互联互通七、跨域管控 一、隐语架构 1、完备性&#xff1a;支持多种技术&#xff0…

基于Springboot的牙科就诊管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的牙科就诊管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍: 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c…

RocketMq 顺序消费、分区消息、延迟发送消息、Topic、tag分类 实战 (消费者) (三)

消费端配置 如下所示&#xff1a;是消费者的配置类&#xff0c;有以下几点需要注意的地方 1、是TargetMessageListener这个监听类&#xff08;下文会把这个监听类的具体代码贴出来&#xff09;&#xff0c;需要把这个监听类订阅。 2、rocketMqDcProperties.getTargetProperties…

【机器学习】k近邻(k-nearest neighbor )算法

文章目录 0. 前言1. 算法原理1.1 距离度量1.2 参数k的选择 2. 优缺点及适用场景3. 改进和扩展4. 案例5. 总结 0. 前言 k近邻&#xff08;k-nearest neighbors&#xff0c;KNN&#xff09;算法是一种基本的监督学习算法&#xff0c;用于分类和回归问题。k值的选择、距离度量及分…

Linux中Oracle数据库启动顺序

首先使用oracle用户登录Linux&#xff0c;用lsnrctl status查看监听状态 1、&#xff1a;进入sqlplus $ sqlplus /nolog SQL> 2&#xff1a;使用sysdab角色登录sqlplus SQL> conn /as sysdba 3&#xff1a;启动数据库 SQL> startup 4&#xff1a;打开Oracle监听 …

微信小程序 - picker-viewer实现省市选择器

简介 本文会基于微信小程序picker viewer组件实现省市选择器的功能。 实现效果 实现代码 布局 <picker-view value"{{value}}" bindchange"bindChange" indicator-style"height: 50px;" style"width: 100%; height: 300px;" &…

OCR研究背景及相关论文分享

光学字符识别&#xff08;Optical Character Recognition&#xff0c;OCR&#xff09;是指使用光学方法将图像中的文字转换为机器可编辑的文本的技术。OCR技术的研究和应用已有数十年的历史&#xff0c;其背景和发展受到多方面因素的影响。 技术需求背景 1.自动化文档处理&am…

【数据分享】2012-2023年全球范围逐年NPP/VIIRS夜间灯光数据

夜间灯光数据是我们在各项研究中经常使用的数据&#xff01;本次我们给大家分享的是2012-2023年全球范围的逐年的NPP/VIIRS夜间灯光数据&#xff0c;数据格式为栅格格式(.tif)。该数据来自于NCEI国家环境信息中心&#xff0c;近期该网站更新了2023年的夜间灯光数据&#xff0c;…