当前位置: 首页 > news >正文

【深入浅出PyTorch】--6.2.PyTorch进阶训练技巧2

目录

1.模型微调 - timm

1.1.timm的安装

1.2.查看模型

1.3.使用模型

1.4模型的保存

2.半精度训练

2.1.半精度训练的设置

3.数据增强-imgaug

3.1.imgaug简介和安装

3.2.imgaug的使用

3.3.单张图片处理

3.4.对批次图片进行处理

3.5.不同大小的图片处理

3.6.imgaug在PyTorch的应用

4.使用argparse进行调参

4.1.argparse的使用

4.2.argparse修改超参数


1.模型微调 - timm

除了使用torchvision.models进行预训练以外,还有一个常见的预训练模型库,叫做timm,这个库是由Ross Wightman创建的。里面提供了许多计算机视觉的SOTA模型,可以当作是torchvision的扩充版本,并且里面的模型在准确度上也较高。在本章内容中,我们主要是针对这个库的预训练模型的使用做叙述,其他部分内容(数据扩增,优化器等)如果大家感兴趣,可以参考以下两个链接。

  • Github链接:https://github.com/rwightman/pytorch-image-models

  • 官网链接:https://fastai.github.io/timmdocs/ https://rwightman.github.io/pytorch-image-models/

1.1.timm的安装

关于timm的安装,我们可以选择以下两种方式进行:

  • 通过pip安装

pip install timm
  • 通过源码编译安装

git clone https://github.com/rwightman/pytorch-image-models
cd pytorch-image-models && pip install -e .

1.2.查看模型

查看timm提供的预训练模型:截止到2025.10.10日为止,timm提供的预训练模型已经达到了1689个,我们可以通过timm.list_models()方法查看timm提供的预训练模型(注:本章测试代码均是在jupyter notebook上进行)

import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)

查看特定模型的所有种类:每一种系列可能对应着不同方案的模型,比如Resnet系列就包括了ResNet18,50,101等模型,我们可以在timm.list_models()传入想查询的模型名称(模糊查询),比如我们想查询densenet系列的所有模型。

all_densnet_models = timm.list_models("*densenet*")
all_densnet_models

我们发现以列表的形式返回了所有densenet系列的所有模型。

查看模型的具体参数 当我们想查看下模型的具体参数的时候,我们可以通过访问模型default_cfg属性来进行查看,具体操作如下

注意:这里要使用huggingface国内的镜像网站,

import os# ⚠️ 必须在 import timm 之前设置!
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'import timm
# 可选:提高超时时间
os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = '60'model = timm.create_model('resnet34', num_classes=10, pretrained=True)
print(model.default_cfg)

除此之外,我们可以通过访问这个链接 查看提供的预训练模型的准确度等信息。

1.3.使用模型

在得到我们想要使用的预训练模型后,我们可以通过timm.create_model()的方法来进行模型的创建,我们可以通过传入参数pretrained=True,来使用预训练模型。同样的,我们也可以使用跟torchvision里面的模型一样的方法查看模型的参数,类型/

import timm
import torchmodel = timm.create_model('resnet34',pretrained=True)
x = torch.randn(1,3,224,224)
output = model(x)
output.shape
torch.Size([1, 1000])
  • 查看某一层模型参数(以第一层卷积为例)

model = timm.create_model('resnet34',pretrained=True)
list(dict(model.named_children())['conv1'].parameters())
[Parameter containing:tensor([[[[-2.9398e-02, -3.6421e-02, -2.8832e-02,  ..., -1.8349e-02,-6.9210e-03,  1.2127e-02],[-3.6199e-02, -6.0810e-02, -5.3891e-02,  ..., -4.2744e-02,-7.3169e-03, -1.1834e-02],...[ 8.4563e-03, -1.7099e-02, -1.2176e-03,  ...,  7.0081e-02,2.9756e-02, -4.1400e-03]]]], requires_grad=True)]
  • 修改模型(将1000类改为10类输出)

model = timm.create_model('resnet34',num_classes=10,pretrained=True)
x = torch.randn(1,3,224,224)
output = model(x)
output.shape
torch.Size([1, 10])
  • 改变输入通道数(比如我们传入的图片是单通道的,但是模型需要的是三通道图片) 我们可以通过添加in_chans=1来改变

model = timm.create_model('resnet34',num_classes=10,pretrained=True,in_chans=1)
x = torch.randn(1,1,224,224)
output = model(x)

1.4模型的保存

timm库所创建的模型是torch.model的子类,我们可以直接使用torch库中内置的模型参数保存和加载的方法,具体操作如下方代码所示

torch.save(model.state_dict(),'./checkpoint/timm_model.pth')
model.load_state_dict(torch.load('./checkpoint/timm_model.pth'))

2.半精度训练

我们提到PyTorch时候,总会想到要用硬件设备GPU的支持。而GPU的性能主要分为两部分:算力和显存,前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。在可以使用的显存数量一定的情况下,每次训练能够加载的数据更多(也就是batch size更大),则也可以提高训练效率。另外,有时候数据本身也比较大(比如3D图像、视频等),显存较小的情况下可能甚至batch size为1的情况都无法实现。因此,合理使用显存也就显得十分重要。

我们观察PyTorch默认的浮点数存储方式用的是torch.float32,小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果,也就是使用torch.float16格式。由于数位减了一半,因此被称为“半精度”,具体如下图:

amp

显然半精度能够减少显存占用,使得显卡可以同时加载更多数据进行计算。本节会介绍如何在PyTorch中设置使用半精度计算。

经过本节的学习,你将收获:

  • 如何在PyTorch中设置半精度训练

  • 使用半精度训练的注意事项

2.1.半精度训练的设置

在PyTorch中使用autocast配置半精度训练,同时需要在下面三处加以设置:

  • import autocast

from torch.cuda.amp import autocast
  • 模型设置

在模型定义中,使用python的装饰器方法,用autocast装饰模型中的forward函数。关于装饰器的使用,可以参考这里:

@autocast()   
def forward(self, x):...return x
  • 训练过程

在训练过程中,只需在将数据输入模型及其之后的部分放入“with autocast():“即可:

 for x in train_loader:x = x.cuda()with autocast():output = model(x)...

注意:

半精度训练主要适用于数据本身的size比较大(比如说3D图像、视频等)。当数据本身的size并不大时(比如手写数字MNIST数据集的图片尺寸只有28*28),使用半精度训练则可能不会带来显著的提升。

3.数据增强-imgaug

在机器学习/深度学习中,我们经常会遇到模型过拟合的问题,为了解决过拟合问题,我们可以通过加入正则项或者减少模型学习参数来解决,但是最简单的避免过拟合的方法是增加数据,但是在许多场景我们无法获得大量数据,例如医学图像分析。数据增强技术的存在是为了解决这个问题,这是针对有限数据问题的解决方案。数据增强一套技术,可提高训练数据集的大小和质量,以便我们可以使用它们来构建更好的深度学习模型。 在计算视觉领域,生成增强图像相对容易。即使引入噪声或裁剪图像的一部分,模型仍可以对图像进行分类,数据增强有一系列简单有效的方法可供选择,有一些机器学习库来进行计算视觉领域的数据增强,比如:imgaug 官网它封装了很多数据增强算法,给开发者提供了方便。通过本章内容,您将学会以下内容:

  • imgaug的简介和安装

  • 使用imgaug对数据进行增强

3.1.imgaug简介和安装

imgaug是计算机视觉任务中常用的一个数据增强的包,相比于torchvision.transforms,它提供了更多的数据增强方法,因此在各种竞赛中,人们广泛使用imgaug来对数据进行增强操作。除此之外,imgaug官方还提供了许多例程让我们学习,本章内容仅是简介,希望起到抛砖引玉的功能。

  1. Github地址:imgaug

  2. Readthedocs:imgaug

  3. 官方提供notebook例程:notebook

#  install imgaug either via pypipip install imgaug#  install the latest version directly from githubpip install git+https://github.com/aleju/imgaug.git

3.2.imgaug的使用

imgaug仅仅提供了图像增强的一些方法,但是并未提供图像的IO操作,因此我们需要使用一些库来对图像进行导入,

  1. 建议使用imageio进行读入,
  2. 如果使用的是opencv进行文件读取的时候,需要进行手动改变通道,将读取的BGR图像转换为RGB图像。
  3. 除此以外,当我们用PIL.Image进行读取时,因为读取的图片没有shape的属性,所以我们需要将读取到的img转换为np.array()的形式再进行处理。

因此官方的例程中也是使用imageio进行图片读取。

3.3.单张图片处理

在该单元,我们仅以几种数据增强操作为例,主要目的是教会大家如何使用imgaug来对数据进行增强操作。

import imageio.v2 as imageio
import matplotlib.pyplot as plt# 图片的读取
img = imageio.imread("./test.png")# 使用matplotlib进行可视化
plt.imshow(img)
plt.axis('off')  # 关闭坐标轴
plt.show()

现在我们已经得到了需要处理的图片,imgaug包含了许多从Augmenter继承的数据增强的操作。在这里我们以Affine为例子。

from imgaug import augmenters as iaa
# 设置随机数种子
ia.seed(4)
# 实例化方法
rotate = iaa.Affine(rotate=(-4,45))#旋转度数:-4到45度
img_aug = rotate(image=img)plt.imshow(img_aug)
plt.axis('on')
plt.show()

这是对一张图片进行一种操作方式,但实际情况下,我们可能对一张图片做多种数据增强处理。这种情况下,我们就需要利用imgaug.augmenters.Sequential()来构造我们数据增强的pipline,该方法与torchvison.transforms.Compose()相类似。

iaa.Sequential(children=None, # Augmenter集合random_order=False, # 是否对每个batch使用不同顺序的Augmenter listname=None,deterministic=False,random_state=None)
# 构建处理序列
aug_seq = iaa.Sequential([iaa.Affine(rotate=(-25,25)),#旋转度数:-25到25度iaa.AdditiveGaussianNoise(scale=(10,60)),#高斯噪声:噪声大小在10到60之间iaa.Crop(percent=(0,0.2))#裁剪:裁剪比例在0到20%之间
])
# 对图片进行处理,image不可以省略,也不能写成images
image_aug = aug_seq(image=img)plt.imshow(image_aug)
plt.axis('on')  
plt.show()

总的来说,对单张图片处理的方式基本相同,我们可以根据实际需求,选择合适的数据增强方法来对数据进行处理。

3.4.对批次图片进行处理

在实际使用中,我们通常需要处理更多份的图像数据。此时,可以将图形数据按照NHWC的形式或者由列表组成的HWC的形式对批量的图像进行处理。主要分为以下两部分,对批次的图片以同一种方式处理和对批次的图片进行分部分处理。

1.对批次的图片以同一种方式处理

对一批次的图片进行处理时,我们只需要将待处理的图片放在一个list中,并将函数的image改为images即可进行数据增强操作,具体实际操作如下:

import numpy as npimages = [img,img,img,img,]
images_aug = rotate(images=images)
# ia.imshow()plt.imshow(np.hstack(images_aug))
plt.axis('on')
plt.show()

我们就可以得到如下的展示效果:

 在上述的例子中,我们仅仅对图片进行了仿射变换,同样的,我们也可以对批次的图片使用多种增强方法,与单张图片的方法类似,我们同样需要借助Sequential来构造数据增强的pipline。

对批次的图片分部分处理

imgaug相较于其他的数据增强的库,有一个很有意思的特性,即就是我们可以通过imgaug.augmenters.Sometimes()对batch中的一部分图片应用一部分Augmenters,剩下的图片应用另外的Augmenters。

iaa.Sometimes(p=0.5,  # 代表划分比例then_list=None,  # Augmenter集合。p概率的图片进行变换的Augmenters。else_list=None,  #1-p概率的图片会被进行变换的Augmenters。注意变换的图片应用的Augmenter只能是then_list或者else_list中的一个。name=None,deterministic=False,random_state=None)

3.5.不同大小的图片处理

上面提到的图片都是基于相同的图像。以下的示例具有不同图像大小的情况,我们从维基百科加载三张图片,将它们作为一个批次进行扩充,然后一张一张地显示每张图片。具体的操作跟单张的图片都是十分相似,因此不做过多赘述。

import os
import requests
from io import BytesIO
import imageio.v2 as imageio  # 导入imageio库用于读取图片
import matplotlib.pyplot as plt  # 导入matplotlib.pyplot用于显示图片
from imgaug import augmenters as iaa  # 导入imgaug的增强器模块
import numpy as np  # 导入numpy用于处理数组plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体作为默认字体
plt.rcParams['axes.unicode_minus'] = False    # 正常显示负号
# 下载图片函数
def download_image(url, save_path):if not os.path.exists(save_path):response = requests.get(url)response.raise_for_status()  # 检查请求是否成功with open(save_path, 'wb') as f:f.write(response.content)# 确保目录存在
images_dir = "images"
os.makedirs(images_dir, exist_ok=True)# 图片URL列表
image_urls = ["https://images.pexels.com/photos/9430775/pexels-photo-9430775.jpeg","https://images.pexels.com/photos/34235838/pexels-photo-34235838.jpeg","https://images.pexels.com/photos/34125813/pexels-photo-34125813.jpeg"
]# 下载图片并保存
image_paths = []
for idx, url in enumerate(image_urls):filename = os.path.join(images_dir, f'image{idx+1}.jpg')download_image(url, filename)image_paths.append(filename)# 构建图像处理流程
seq = iaa.Sequential([iaa.CropAndPad(percent=(-0.2, 0.2), pad_mode="edge"),  # 裁剪和填充图像iaa.AddToHueAndSaturation((-60, 60)),  # 改变图像的颜色iaa.ElasticTransformation(alpha=90, sigma=9),  # 添加类似水波纹的效果iaa.Cutout()  # 在图像中替换一个正方形区域为常量强度值
], random_order=True)  # 随机顺序应用上述变换# 加载本地图片
images_different_sizes = [imageio.imread(path) for path in image_paths]# 对图片进行增强处理
images_aug = seq(images=images_different_sizes)# 使用matplotlib可视化结果
for idx, (original, augmented) in enumerate(zip(images_different_sizes, images_aug)):print("图片 %d (原始尺寸: %s, 增强后尺寸: %s)" % (idx, original.shape, augmented.shape))# 将两张图横向拼接在一起以便对比comparison = np.hstack([original, augmented])# 使用matplotlib显示图像plt.figure(figsize=(15, 10))  # 设置显示窗口大小plt.title(f"图片 {idx} 对比 - 原始 vs 增强")  # 设置图像标题plt.imshow(comparison)  # 显示图像plt.axis('off')  # 关闭坐标轴plt.show()  # 展示图像

3.6.imgaug在PyTorch的应用

关于PyTorch中如何使用imgaug每一个人的模板是不一样的,我在这里也仅仅给出imgaug的issue里面提出的一种解决方案,大家可以根据自己的实际需求进行改变。 具体链接:how to use imgaug with pytorch

import numpy as np
from imgaug import augmenters as iaa          # 导入imgaug图像增强库
from torch.utils.data import DataLoader, Dataset  # 用于构建数据加载器
from torchvision import transforms               # 提供常见的图像转换操作(如ToTensor)# ==================== 构建图像增强和转换的pipline ====================
# 使用transforms.Compose将多个操作组合成一个处理流程
tfs = transforms.Compose([# 使用imgaug的Sequential定义一组图像增强操作iaa.Sequential([iaa.Fliplr(p=0.5),           # 以50%的概率进行水平翻转(左右翻转)iaa.Flipud(p=0.5),           # 以50%的概率进行垂直翻转(上下翻转)iaa.GaussianBlur(sigma=(0.0, 0.1)),  # 添加轻微的高斯模糊,sigma控制模糊程度iaa.MultiplyBrightness(mul=(0.65, 1.35)),  # 调整亮度,亮度乘以0.65~1.35之间的随机值]).augment_image,  # 将imgaug的增强器应用到单张图像上(注意:这里是对单张图像调用augment_image)# 注意:imgaug处理的是numpy数组,而ToTensor需要将图像转为PyTorch的Tensortransforms.ToTensor()  # 将PIL或numpy图像转换为归一化到[0,1]的Tensor (C, H, W)
])# ==================== 自定义数据集类 ====================
class CustomDataset(Dataset):"""自定义数据集类,用于模拟图像和标签数据"""def __init__(self, n_images, n_classes, transform=None):"""初始化数据集:param n_images: 生成的图像数量:param n_classes: 分类任务的类别数(此处为回归目标,模拟10维输出):param transform: 可选的图像变换操作(如增强)"""# 模拟生成图像数据:随机生成50张 224x224x3 的uint8图像(像素值0-255)self.images = np.random.randint(0, 255, (n_images, 224, 224, 3), dtype=np.uint8)# 模拟生成标签数据:随机生成50个10维的浮点数标签(可用于回归任务)self.targets = np.random.randn(n_images, n_classes)# 保存变换操作(增强+ToTensor)self.transform = transformdef __getitem__(self, item):"""获取索引为item的数据样本:param item: 索引:return: 增强后的图像tensor和对应标签"""image = self.images[item]      # 获取第item张图像target = self.targets[item]    # 获取第item个标签# 如果定义了变换(如增强),则对图像进行处理if self.transform:image = self.transform(image)  # 应用增强和ToTensorreturn image, target  # 返回处理后的图像和标签def __len__(self):"""返回数据集的总样本数"""return len(self.images)# ==================== 多进程数据加载的worker初始化函数 ====================
def worker_init_fn(worker_id):"""每个数据加载worker的随机种子初始化函数避免多个worker生成相同的随机增强结果:param worker_id: worker的ID"""# 获取主进程的numpy随机状态,并为每个worker设置不同的种子np.random.seed(np.random.get_state()[1][0] + worker_id)# ==================== 创建数据集和数据加载器 ====================
# 实例化自定义数据集:50张图像,10类标签,使用定义的变换
custom_ds = CustomDataset(n_images=50, n_classes=10, transform=tfs)# 创建DataLoader:
# - batch_size=64: 每个batch包含64个样本(但总共只有50个,所以最后一个batch会少)
# - num_workers=4: 使用4个子进程并行加载数据
# - pin_memory=True: 锁页内存,加快GPU传输速度(如果使用GPU)
# - worker_init_fn: 为每个worker设置不同的随机种子,确保增强多样性
custom_dl = DataLoader(custom_ds, batch_size=64,num_workers=4, pin_memory=True,worker_init_fn=worker_init_fn)# ==================== 输出信息 ====================
print("数据集样本总数:", len(custom_ds))     # 应输出50
print("DataLoader的batch数量:", len(custom_dl))  # 应输出1(因为50 < 64,所以只有1个batch)

4.使用argparse进行调参

https://blog.csdn.net/qq_58602552/article/details/149043428

在深度学习中时,超参数的修改和保存是非常重要的一步,尤其是当我们在服务器上跑我们的模型时,如何更方便的修改超参数是我们需要考虑的一个问题。这时候,要是有一个库或者函数可以解析我们输入的命令行参数再传入模型的超参数中该多好。到底有没有这样的一种方法呢?答案是肯定的,这个就是 Python 标准库的一部分:Argparse。那么下面让我们看看他是多么方便。通过本节课,您将会收获以下内容

  • argparse的简介

  • argparse的使用

  • 如何使用argparse修改超参数

4.1.argparse的使用

总的来说,我们可以将argparse的使用归纳为以下三个步骤。

  • 创建ArgumentParser()对象

  • 调用add_argument()方法添加参数

  • 使用parse_args()解析参数 在接下来的内容中,我们将以实际操作来学习argparse的使用方法。

# demo.py
import argparse# 创建ArgumentParser()对象
parser = argparse.ArgumentParser()# 添加参数
parser.add_argument('-o', '--output', action='store_true', help="shows output")
# action = `store_true` 会将output参数记录为True
# type 规定了参数的格式
# default 规定了默认值
parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3') parser.add_argument('--batch_size', type=int, required=True, help='input batch size')  
# 使用parse_args()解析函数
args = parser.parse_args()if args.output:print("This is some output")print(f"learning rate:{args.lr} ")

我们在命令行使用,就可以看到以下的输出

python demo.py --lr 3e-4 --batch_size 32

argparse的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr参数相类似,未输入的情况下会设置为默认值。必选参数就跟我们的batch_size参数相类似,当我们给参数设置required =True后,我们就必须传入该参数,否则就会报错。看到我们的输入格式后,我们可能会有这样一个疑问,我输入参数的时候不使用--可以吗?答案是肯定的,不过我们需要在设置上做出一些改变。

# positional.py
import argparse# 位置参数
parser = argparse.ArgumentParser()parser.add_argument('name')
parser.add_argument('age')args = parser.parse_args()print(f'{args.name} is {args.age} years old')

当我们不使用--后,将会严格按照参数位置进行解析。

4.2.argparse修改超参数

每个人都有着不同的超参数管理方式,在这里我将分享我使用argparse管理超参数的方式,希望可以对大家有一些借鉴意义。通常情况下,为了使代码更加简洁和模块化,我一般会将有关超参数的操作写在config.py,然后在train.py或者其他文件导入就可以。具体的config.py可以参考如下内容。

import argparse  def get_options(parser=argparse.ArgumentParser()):  parser.add_argument('--workers', type=int, default=0,  help='number of data loading workers, you had better put it '  '4 times of your gpu')  parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')  parser.add_argument('--niter', type=int, default=10, help='number of epochs to train for, default=10')  parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')  parser.add_argument('--seed', type=int, default=118, help="random seed")  parser.add_argument('--cuda', action='store_true', default=True, help='enables cuda')  parser.add_argument('--checkpoint_path',type=str,default='',  help='Path to load a previous trained model if not empty (default empty)')  parser.add_argument('--output',action='store_true',default=True,help="shows output")  opt = parser.parse_args()  if opt.output:  print(f'num_workers: {opt.workers}')  print(f'batch_size: {opt.batch_size}')  print(f'epochs (niters) : {opt.niter}')  print(f'learning rate : {opt.lr}')  print(f'manual_seed: {opt.seed}')  print(f'cuda enable: {opt.cuda}')  print(f'checkpoint_path: {opt.checkpoint_path}')  return opt  if __name__ == '__main__':  opt = get_options()

随后在train.py等其他文件,我们就可以使用下面的这样的结构来调用参数。

# 导入必要库
...
import configopt = config.get_options()manual_seed = opt.seed
num_workers = opt.workers
batch_size = opt.batch_size
lr = opt.lr
niters = opt.niters
checkpoint_path = opt.checkpoint_path# 随机数的设置,保证复现结果
def set_seed(seed):torch.manual_seed(seed)torch.cuda.manual_seed_all(seed)random.seed(seed)np.random.seed(seed)torch.backends.cudnn.benchmark = Falsetorch.backends.cudnn.deterministic = True...if __name__ == '__main__':set_seed(manual_seed)for epoch in range(niters):train(model,lr,batch_size,num_workers,checkpoint_path)val(model,lr,batch_size,num_workers,checkpoint_path)

http://www.dtcms.com/a/478368.html

相关文章:

  • JS - 运算符
  • Ethical use of recommender systems|推荐系统的道德使用
  • 网站建设 全网推广建网站的域名
  • MySQL——表操作
  • 基于MATLAB的海图快速临近值插值地图构建方法
  • 深度解析“rgss102e.dll丢失”问题:原因、影响与完整解决指南
  • 正规的媒体发稿网有哪些
  • 知名的汽车媒体发稿有哪些
  • 乐昌市建设网站网站开发就业薪酬
  • 【前缀和】| LeetCode 1314题解 矩阵区域和
  • 深圳网站建设服务便宜wordpress 获取导航
  • UniApp 实现双语功能
  • 保定哪做网站好云南云桥建设股份有限公司官方网站
  • xss-labs靶场安装+通关(1)
  • 3002. 移除后集合的最多元素数
  • 深圳的网站建设公司的外文名是南阳专业做网站公司哪家好
  • 电脑卡顿?快速解决CPU占用率过高问题
  • 免费制作网站net域名儿童网站开发方面外文文献
  • 自定义网络协议与序列化/反序列化
  • 如何给网站做第三方流量监测海珠高端网站建设
  • 守好电网的“最后一公里”:配电台区综合在线监控系统
  • 从零部署 Astro 静态网站到云服务器(含 HTTPS 一键配置)
  • 重生之我在大学自学鸿蒙开发第二天-《MVVM模式》
  • Sequence Encoder-based Spatio temporal Knowledge Graph Completion
  • 学习笔记:Vue Router 中的链接匹配机制与样式控制
  • 做彩票网站电话多少钱网站在线建设
  • 网站建设入什么费用站规划在网站建设中的作用
  • c语言-流程控制语句
  • for和while循环,continue和break的用法
  • Redis-持久化之RDB