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

做视频上传可以赚钱的网站河北平台网站建设

做视频上传可以赚钱的网站,河北平台网站建设,广告设计招聘岗位要求,汽油最新价格1、引言 在本篇文章中#xff0c;我们将深入探讨并实现一些现代卷积神经网络#xff08;CNN#xff09;架构的变体。近年来#xff0c;学界提出了众多新颖的网络架构。其中一些最具影响力#xff0c;并且至今仍然具有重要地位的架构包括#xff1a;GoogleNet/Inception架…1、引言 在本篇文章中我们将深入探讨并实现一些现代卷积神经网络CNN架构的变体。近年来学界提出了众多新颖的网络架构。其中一些最具影响力并且至今仍然具有重要地位的架构包括GoogleNet/Inception架构2014年ILSVRC竞赛的冠军ResNet2015年ILSVRC竞赛的冠军以及DenseNet2017年CVPR会议的最佳论文奖得主。这些网络在刚提出时均代表了当时技术的最前沿它们的核心理念已成为当前大多数顶尖架构的基石。因此深入理解这些架构的细节并掌握其实现方法对于我们来说至关重要。 首先我们来导入一些常用的库。 ## 标准库 import os # 导入操作系统接口 import numpy as np # 导入科学计算库 import random # 导入随机数生成库 from PIL import Image # 从PIL库导入图像处理模块 from types import SimpleNamespace # 导入命名空间类型## 绘图相关导入 import matplotlib.pyplot as plt # 导入matplotlib的绘图模块 %matplotlib inline # 使matplotlib绘图在Jupyter Notebook中内联显示 from IPython.display import set_matplotlib_formats # 导入设置matplotlib格式的函数 set_matplotlib_formats(svg, pdf) # 设置matplotlib输出格式为SVG和PDF便于导出 import matplotlib # 导入matplotlib库 matplotlib.rcParams[lines.linewidth] 2.0 # 设置matplotlib线条宽度为2.0 import seaborn as sns # 导入seaborn库用于数据可视化 sns.reset_orig() # 重置seaborn的默认设置## PyTorch import torch # 导入PyTorch库 import torch.nn as nn # 导入PyTorch的神经网络模块 import torch.utils.data as data # 导入PyTorch的数据工具模块 import torch.optim as optim # 导入PyTorch的优化器模块 # Torchvision import torchvision # 导入Torchvision库用于处理图像和视频 from torchvision.datasets import CIFAR10 # 从Torchvision的datasets模块导入CIFAR-10数据集 from torchvision import transforms # 从Torchvision导入图像转换模块在本文中我们将沿用之前博文中使用的set_seed函数并利用DATASET_PATH和CHECKPOINT_PATH这两个路径变量。根据实际情况你可能需要对这些路径进行相应的调整。这样做可以确保我们的代码在不同环境中都能正确地找到数据集和模型检查点。 # 定义数据集存储路径例如CIFAR-10数据集的下载位置 DATASET_PATH ../data # 定义预训练模型的保存路径 CHECKPOINT_PATH ../saved_models/tutorial5# 设置随机种子的函数确保实验结果的可复现性 def set_seed(seed):random.seed(seed) # 设置Python内置随机数生成器的种子np.random.seed(seed) # 设置NumPy随机数生成器的种子torch.manual_seed(seed) # 设置PyTorch随机数生成器的种子if torch.cuda.is_available(): # 如果CUDA可用torch.cuda.manual_seed(seed) # 设置CUDA随机数生成器的种子torch.cuda.manual_seed_all(seed) # 设置CUDA所有设备的随机数生成器种子# 调用set_seed函数传入种子值42 set_seed(42)# 确保在GPU上的所有操作都是确定性的如果使用GPU以提高结果的可复现性 torch.backends.cudnn.deterministic True # 设置为True确保CuDNN的算法是确定性的 torch.backends.cudnn.benchmark False # 设置为False关闭CuDNN的自动调优功能# 根据CUDA是否可用选择使用GPU或CPU device torch.device(cuda:0) if torch.cuda.is_available() else torch.device(cpu)在本文中我们提供了预先训练好的模型以及TensorBoard日志文件稍后会对这些内容进行详细说明。你可以通过以下链接下载这些资源。这将帮助你更快速地开始实验同时确保你能够直观地查看训练过程中的各种指标变化。 import urllib.request # 导入urllib库的request模块用于处理网络请求 from urllib.error import HTTPError # 导入HTTPError用于捕获HTTP请求错误# 定义GitHub仓库中存储预训练模型的URL base_url https://raw.githubusercontent.com/phlippe/saved_models/main/tutorial5/ # 定义需要下载的文件列表 pretrained_files [GoogleNet.ckpt, ResNet.ckpt, ResNetPreAct.ckpt, DenseNet.ckpt,tensorboards/GoogleNet/events.out.tfevents.googlenet,tensorboards/ResNet/events.out.tfevents.resnet,tensorboards/ResNetPreAct/events.out.tfevents.resnetpreact,tensorboards/DenseNet/events.out.tfevents.densenet]# 如果检查点路径不存在则创建它 os.makedirs(CHECKPOINT_PATH, exist_okTrue)# 对于列表中的每个文件检查它是否已经存在。如果不存在尝试下载它。 for file_name in pretrained_files:file_path os.path.join(CHECKPOINT_PATH, file_name) # 构建文件的完整路径if / in file_name: # 如果文件名中包含/则需要创建相应的目录结构os.makedirs(file_path.rsplit(/, 1)[0], exist_okTrue) # 创建目录if not os.path.isfile(file_path): # 检查文件是否已经存在file_url base_url file_name # 构建文件的下载URLprint(f正在下载 {file_url}...)try:urllib.request.urlretrieve(file_url, file_path) # 尝试下载文件except HTTPError as e: # 捕获并处理HTTP请求错误print(下载过程中出现问题。请尝试从Google Drive文件夹下载文件或将完整的输出信息包括以下错误联系作者\n, e)在本文中我们将对CIFAR-10数据集进行模型的训练与评估。这样你便可以将本教程中得到的结果与你在首次作业中实现的模型结果进行对比。根据我们在上一个教程中学到的初始化知识数据经过预处理使其均值归零是至关重要的。因此我们首先需要计算CIFAR数据集的均值和标准差。 # 导入CIFAR-10数据集设置数据集的根目录为DATASET_PATH指定为训练集并自动下载 train_dataset CIFAR10(rootDATASET_PATH, trainTrue, downloadTrue)# 计算数据集的均值将数据归一化到[0,1]区间后沿所有图像通道计算平均值 DATA_MEANS (train_dataset.data / 255.0).mean(axis(0, 1, 2))# 计算数据集的标准差同样先归一化到[0,1]区间然后沿所有图像通道计算标准差 DATA_STD (train_dataset.data / 255.0).std(axis(0, 1, 2))# 打印数据集的均值和标准差 print(数据均值:, DATA_MEANS) print(数据标准差:, DATA_STD)在本文中我们将使用这些统计数据来构建一个transforms.Normalize模块以便对数据进行适当的标准化处理。此外为了提高模型的泛化能力并减少过拟合的风险我们在训练过程中引入了数据增强技术。具体来说我们会应用两种数据增强方法。 首先我们将对每张图像以50%的概率进行随机的水平翻转这是通过transforms.RandomHorizontalFlip实现的。一般来说图像的水平翻转不会影响其类别识别因为物体的类别与图像的水平方向无关。然而如果我们的目标是图像中的数字或字母识别那么方向性就变得重要了。 第二种数据增强方法是transforms.RandomResizedCrop它通过在一定范围内随机裁剪图像可能改变图像的纵横比然后再将其缩放回原始尺寸。这样尽管图像的像素值发生了变化但图像的内容和整体语义信息仍然保持一致。 接下来我们会将训练数据集随机划分为训练子集和验证子集。验证子集将用于评估模型在训练过程中的性能以便于我们实施早停策略early stopping。训练结束后我们将在CIFAR的测试集上对模型进行评估以测试其对未知数据的处理能力。 # 定义测试集的转换操作包括转换为张量以及标准化 test_transform transforms.Compose([transforms.ToTensor(), # 将PIL图像或Numpy数组转换为FloatTensor并将数值范围从[0, 255]缩放到[0.0, 1.0]transforms.Normalize(DATA_MEANS, DATA_STD) # 标准化使数据具有指定的均值和标准差 ])# 定义训练集的转换操作包括数据增强和标准化 train_transform transforms.Compose([transforms.RandomHorizontalFlip(), # 以50%的概率随机水平翻转图像transforms.RandomResizedCrop((32, 32), scale(0.8, 1.0), ratio(0.9, 1.1)), # 随机裁剪并缩放图像transforms.ToTensor(), # 转换为张量transforms.Normalize(DATA_MEANS, DATA_STD) # 标准化 ])# 加载训练数据集并将其划分为训练集和验证集注意验证集不使用数据增强 train_dataset CIFAR10(rootDATASET_PATH, trainTrue, transformtrain_transform, downloadTrue) val_dataset CIFAR10(rootDATASET_PATH, trainTrue, transformtest_transform, downloadTrue)# 设置随机种子以确保结果的可复现性 set_seed(42) # 将训练数据集随机划分为45000张图像用于训练5000张图像用于验证 train_set, _ torch.utils.data.random_split(train_dataset, [45000, 5000]) set_seed(42) # 将验证数据集随机划分这里使用相同的划分策略但实际上验证集不需要划分 _, val_set torch.utils.data.random_split(val_dataset, [45000, 5000])# 加载测试集 test_set CIFAR10(rootDATASET_PATH, trainFalse, transformtest_transform, downloadTrue)# 定义数据加载器用于后续的训练、验证和测试 train_loader data.DataLoader(train_set, batch_size128, shuffleTrue, drop_lastTrue, pin_memoryTrue, num_workers4) val_loader data.DataLoader(val_set, batch_size128, shuffleFalse, drop_lastFalse, num_workers4) test_loader data.DataLoader(test_set, batch_size128, shuffleFalse, drop_lastFalse, num_workers4)为了确保我们的标准化处理达到预期效果我们可以通过打印单个数据批次的均值和标准差来进行检验。理想情况下每个颜色通道的均值应该趋近于0标准差趋近于1。这是一种常用的做法用以确保数据经过标准化处理后分布更加接近标准正态分布从而有助于模型的训练和收敛。 # 获取训练数据加载器的第一个批次的数据 imgs, _ next(iter(train_loader))# 打印该批次数据的均值dim参数指定了要计算均值的维度 print(批次均值, imgs.mean(dim[0, 2, 3]))# 打印该批次数据的标准差dim参数指定了要计算标准差的维度 print(批次标准差, imgs.std(dim[0, 2, 3]))接下来我们将对训练集中的部分图像进行可视化展示并观察它们在应用了随机数据增强技术之后的效果。这有助于我们直观地理解数据增强对图像的影响以及它是如何帮助改善模型的泛化能力的。 # 定义要显示的图像数量 NUM_IMAGES 4# 从训练数据集中获取指定数量的图像 images [train_dataset[idx][0] for idx in range(NUM_IMAGES)] # 将原始图像数据转换为PIL图像格式 orig_images [Image.fromarray(np.transpose(train_dataset.data[idx], (1, 2, 0))) for idx in range(NUM_IMAGES)] # 应用测试转换操作到原始图像上 orig_images [test_transform(img) for img in orig_images]# 使用torchvision的make_grid函数创建图像网格将原始图像和增强后的图像堆叠起来 # nrow指定每行显示的图像数量normalize设置为True表示将像素值归一化到[0,1]pad_value设置为0.5表示填充值 img_grid torchvision.utils.make_grid(torch.stack(images orig_images, dim0), nrow4, normalizeTrue, pad_value0.5) # permute函数用于重新排列图像张量的维度以适应matplotlib的显示要求 img_grid img_grid.permute(1, 2, 0)# 使用matplotlib创建图像展示 plt.figure(figsize(8, 8)) # 设置图像展示窗口的大小 plt.title(CIFAR10数据增强示例) # 设置图像展示的标题 plt.imshow(img_grid) # 显示图像网格 plt.axis(off) # 不显示坐标轴 plt.show() # 展示图像 plt.close() # 展示完毕后关闭图像展示窗口2.使用PytTorch Linghtning 在本文和后续的文章中我们将利用一个名为PyTorch Lightning的库。PyTorch Lightning是一个框架它极大地简化了在PyTorch中编写训练、评估和测试模型的代码。它还负责将日志信息记录到TensorBoard——这是一个用于机器学习实验的可视化工具并且能够自动保存模型的检查点而我们几乎不需要编写额外的代码。这对于我们来说极为便利因为我们更愿意将精力集中在不同模型架构的实现上而不是花费大量时间处理其他代码问题。本文使用的是PyTorch Lightning1.8版本请读者自行到官网更新最新的版本。 现在我们将在PyTorch Lightning中迈出探索的第一步并在后续的文章中继续深入了解这个框架。首先我们需要导入这个库。 # 尝试导入PyTorch Lightning库 try:import pytorch_lightning as pl except ModuleNotFoundError: # 如果模块未找到异常例如Google Colab默认没有安装PyTorch Lightning# 使用pip命令静默安装PyTorch Lightning版本要求大于等于1.5!pip install --quiet pytorch-lightning1.5# 再次尝试导入PyTorch Lightning库import pytorch_lightning as plPyTorch Lightning框架内建了大量实用的功能包括一个用于设定随机种子的便捷方法 # 设置随机种子以确保实验的可重复性 pl.seed_everything(42)在未来的工作中我们将无需再自行定义设置随机种子的函数。 在PyTorch Lightning框架中我们通过pl.LightningModule继承自PyTorch的torch.nn.Module来构建我们的模型它将代码逻辑划分为五个核心部分 初始化 (__init__)在这里我们初始化所有必要的参数和模型结构。配置优化器 (configure_optimizers)在这部分我们定义模型的优化器、学习率调整策略等。训练步骤 (training_step)我们仅需指定单个数据批次的损失计算方法而优化器的梯度清零、损失反向传播和参数更新等步骤以及日志记录或保存操作都由框架在后台自动处理。验证步骤 (validation_step)与训练类似我们定义每个验证步骤需要进行的操作。测试步骤(test_step)这与验证步骤类似只不过是应用于测试数据集。 通过这种方式PyTorch Lightning并没有简化PyTorch的代码而是对其进行了有序组织并提供了一些常用的默认操作。如果你需要对训练、验证或测试流程进行特定的调整框架提供了多种可重写的方法来满足个性化需求具体细节请参考官方文档。 接下来我们可以观察一个使用PyTorch Lightning构建的用于训练卷积神经网络的模块示例。 class CIFARModule(pl.LightningModule):def __init__(self, model_name, model_hparams, optimizer_name, optimizer_hparams):构造函数初始化一个用于CIFAR数据集的模型模块。参数:model_name - 要运行的模型/CNN名称用于创建模型。model_hparams - 模型的超参数字典。optimizer_name - 使用的优化器名称目前支持Adam和SGD。optimizer_hparams - 优化器的超参数字典包括学习率、权重衰减等。super().__init__() # 调用父类构造函数self.save_hyperparameters() # 将超参数导出到YAML文件并创建self.hparams命名空间# 创建模型使用给定的模型名称和超参数self.model create_model(model_name, model_hparams)# 创建损失模块self.loss_module nn.CrossEntropyLoss()# 用于在Tensorboard中可视化图的示例输入self.example_input_array torch.zeros((1, 3, 32, 32), dtypetorch.float32)def forward(self, imgs):# 当可视化图时运行的前向函数return self.model(imgs)def configure_optimizers(self):# 根据选择的优化器名称创建优化器if self.hparams.optimizer_name Adam:# 使用AdamW即带有正确实现权重衰减的Adam详见提供的链接optimizer optim.AdamW(self.parameters(), **self.hparams.optimizer_hparams)elif self.hparams.optimizer_name SGD:optimizer optim.SGD(self.parameters(), **self.hparams.optimizer_hparams)else:# 如果提供了未知的优化器名称断言失败assert False, fUnknown optimizer: \{self.hparams.optimizer_name}\# 学习率调度器在第100和150个epoch后将学习率降低0.1scheduler optim.lr_scheduler.MultiStepLR(optimizer, milestones[100, 150], gamma0.1)return [optimizer], [scheduler] # 返回优化器和学习率调度器def training_step(self, batch, batch_idx):# 训练步骤对每个批次的数据执行imgs, labels batchpreds self.model(imgs) # 模型预测loss self.loss_module(preds, labels) # 计算损失acc (preds.argmax(dim-1) labels).float().mean() # 计算准确率# 在Tensorboard中记录每个epoch的准确率跨批次的加权平均值self.log(train_acc, acc, on_stepFalse, on_epochTrue)self.log(train_loss, loss)return loss # 返回损失张量以调用.backwarddef validation_step(self, batch, batch_idx):# 验证步骤对验证数据执行imgs, labels batchpreds self.model(imgs).argmax(dim-1)acc (labels preds).float().mean()# 默认每个epoch记录一次跨批次的加权平均值self.log(val_acc, acc)def test_step(self, batch, batch_idx):# 测试步骤对测试数据执行imgs, labels batchpreds self.model(imgs).argmax(dim-1)acc (labels preds).float().mean()# 默认每个epoch记录一次跨批次的加权平均值然后返回self.log(test_acc, acc)代码的组织结构清晰有序这有助于他人理解你的代码逻辑。 PyTorch Lightning框架中一个关键的概念是回调callbacks。回调函数是一些自包含的函数它们包含了Lightning Module中非核心的逻辑。这些回调函数通常在训练周期结束后被调用但它们也可能会影响到训练循环的其他环节。例如我们将采用以下两个预定义的回调LearningRateMonitor学习率监控器和ModelCheckpoint模型检查点。学习率监控器会将当前的学习率信息添加到TensorBoard中这有助于我们确认学习率调度器是否按预期工作。模型检查点回调则允许你定制检查点的保存策略比如保留多少个检查点、何时进行保存、根据哪个指标来决定保存等。下面是这些回调的导入代码 # 回调函数导入 from pytorch_lightning.callbacks import LearningRateMonitor, ModelCheckpoint为了能够使用同一个Lightning模块来运行多种不同的模型我们下面定义了一个函数它将模型名称映射到相应的模型类。目前model_dict 字典为空但我们将在接下来的笔记本使用过程中用新模型来填充它。 model_dict {}def create_model(model_name, model_hparams):if model_name in model_dict:return model_dict[model_name](**model_hparams)else:assert False, f未知的模型名称\{model_name}\。可用的模型有{str(model_dict.keys())} 类似地为了在我们的模型中将激活函数作为超参数使用我们下面定义了一个将名称映射到函数的字典 act_fn_by_name {tanh: nn.Tanh,relu: nn.ReLU,leakyrelu: nn.LeakyReLU,gelu: nn.GELU通过这种方式代码的复用性和灵活性得以增强同时也方便了不同模型和配置之间的切换保持了代码的整洁和易于维护。如果我们直接将类或对象作为参数传递给Lightning模块就无法享受PyTorch Lightning提供的自动保存和加载超参数的特性。 在PyTorch Lightning框架中除了Lightning模块外另一个核心组件是Trainer训练器。训练器的职责是执行Lightning模块中定义的训练步骤并确保整个训练流程的完整性。与Lightning模块一样你可以对任何你不想自动执行的关键部分进行覆盖但通常情况下默认设置就是最佳实践。有关全部功能的详细介绍请查看官方文档。我们下面使用到的最重要的几个函数包括 trainer.fit接收一个Lightning模块、一个训练数据集以及一个可选的验证数据集作为输入。此函数负责在训练数据集上训练指定的模块并且可以定期进行验证默认是每个epoch一次但这个频率可以调整。trainer.test接收一个模型和我们希望进行测试的数据集作为输入。它将返回该数据集上的测试指标。 在进行训练和测试时我们无需担心如将模型设置为评估模式model.eval()等细节因为这些操作都会自动完成。以下是我们如何定义模型的训练函数的示例 def train_model(model_name, save_nameNone, **kwargs):参数:model_name - 要运行的模型名称用于在model_dict中查找对应的类。save_name (可选) - 如果提供这个名称将被用于创建检查点和日志目录。if save_name是Nonesave_name model_name# 创建一个PyTorch Lightning训练器并配置生成回调trainer pl.Trainer(default_root_diros.path.join(CHECKPOINT_PATH, save_name), # 模型保存位置acceleratorgpu if str(device).startswith(cuda) else cpu, # 尽可能在GPU上运行devices1, # 使用的GPU/CPU数量笔记本示例中1足够了max_epochs180, # 训练的最大周期数若未设置早停则使用此值callbacks[ModelCheckpoint(save_weights_onlyTrue, modemax, monitorval_acc), # 基于最大val_acc记录保存最佳检查点仅保存权重而非优化器状态LearningRateMonitor(epoch)], # 每个epoch记录学习率enable_progress_barTrue) # 是否显示进度条trainer.logger._log_graph True # 是否在TensorBoard中绘制计算图trainer.logger._default_hp_metric None # 我们不需要的可选日志参数# 检查预训练模型是否存在如果存在则加载并跳过训练pretrained_filename os.path.join(CHECKPOINT_PATH, save_name .ckpt)if os.path.isfile(pretrained_filename):print(f在{pretrained_filename}找到预训练模型正在加载...)model CIFARModule.load_from_checkpoint(pretrained_filename) # 自动加载模型和保存的超参数else:pl.seed_everything(42) # 确保结果可复现model CIFARModule(model_namemodel_name, **kwargs)trainer.fit(model, train_loader, val_loader)# 训练完成后加载最佳检查点model CIFARModule.load_from_checkpoint(trainer.checkpoint_callback.best_model_path)# 在验证集和测试集上测试最佳模型的性能val_result trainer.test(model, val_loader, verboseFalse)test_result trainer.test(model, test_loader, verboseFalse)result {test: test_result[0][test_acc], val: val_result[0][test_acc]}return model, result最终我们可以专注于实现我们今天计划中的卷积神经网络GoogleNet、ResNet和DenseNet。 3、Inception(初始网络 2014年提出的GoogleNet因其创新的Inception模块设计而荣获ImageNet挑战赛的冠军。本文将重点介绍Inception的核心概念而非GoogleNet的具体细节。这是因为Inception理念催生了众多衍生模型如Inception-v2、Inception-v3、Inception-v4、Inception-ResNet等这些后续工作主要致力于提升网络效率并构建更深的Inception网络结构。然而对于基础理解而言研究最初的Inception模块已经足够。 Inception模块的特色在于它能够在同一特征图上并行地应用四种不同的卷积操作1x1、3x3和5x5的卷积操作以及一个最大池化操作。这样的设计使得网络能够以不同的感受野捕捉信息。虽然单纯学习5x5卷积在理论上可能更强大但这不仅计算量大、内存消耗高也更容易出现过拟合问题。下图展示了Inception模块的结构图源Szegedy等人的研究 1x1卷积操作在3x3和5x5卷积之前进行主要作用是降维。这一点至关重要因为所有分支的特征图最终会合并我们希望避免特征维度的爆炸式增长。由于5x5卷积的计算成本是1x1卷积的25倍因此在执行大尺寸卷积前进行降维可以大大节省计算资源和参数数量。 接下来我们可以尝试实现Inception模块 class InceptionBlock(nn.Module):def __init__(self, c_in, c_red: dict, c_out: dict, act_fn):参数:c_in - 前一层传递给模块的输入特征图数量c_red - 一个字典包含3x3和5x5两个键指明1x1卷积降维操作的输出维度c_out - 一个字典包含1x1, 3x3, 5x5, 和 max四个键act_fn - 激活函数的类构造器例如nn.ReLUsuper().__init__()# 1x1卷积分支self.conv_1x1 nn.Sequential(nn.Conv2d(c_in, c_out[1x1], kernel_size1),nn.BatchNorm2d(c_out[1x1]),act_fn())# 3x3卷积分支self.conv_3x3 nn.Sequential(nn.Conv2d(c_in, c_red[3x3], kernel_size1),nn.BatchNorm2d(c_red[3x3]),act_fn(),nn.Conv2d(c_red[3x3], c_out[3x3], kernel_size3, padding1),nn.BatchNorm2d(c_out[3x3]),act_fn())# 5x5卷积分支self.conv_5x5 nn.Sequential(nn.Conv2d(c_in, c_red[5x5], kernel_size1),nn.BatchNorm2d(c_red[5x5]),act_fn(),nn.Conv2d(c_red[5x5], c_out[5x5], kernel_size5, padding2),nn.BatchNorm2d(c_out[5x5]),act_fn())# 最大池化分支self.max_pool nn.Sequential(nn.MaxPool2d(kernel_size3, padding1, stride1),nn.Conv2d(c_in, c_out[max], kernel_size1),nn.BatchNorm2d(c_out[max]),act_fn())def forward(self, x):# 对输入x应用各个分支x_1x1 self.conv_1x1(x)x_3x3 self.conv_3x3(x)x_5x5 self.conv_5x5(x)x_max self.max_pool(x)# 沿着特征维度将各个分支的输出进行合并x_out torch.cat([x_1x1, x_3x3, x_5x5, x_max], dim1)return x_out上述代码定义了一个Inception模块的类InceptionBlock它接收输入特征图并分别通过不同尺寸的卷积和池化操作处理最终将结果合并为后续的网络层提供更丰富的特征表示。 GoogleNet架构通过堆叠多个Inception模块并在必要时使用最大池化操作来降低特征图的尺寸。最初的GoogleNet是针对ImageNet224x224像素设计的拥有近700万个参数。由于我们在CIFAR10数据集上进行训练图像尺寸为32x32像素因此不需要如此复杂的架构而是采用简化版。每个滤波器1x1、3x3、5x5和最大池化的降维和输出通道数需要手动设定如果需要这些参数也可以进行调整。通常的建议是为3x3卷积分配更多的滤波器因为它们能够在参数数量相对较少的情况下捕获足够的上下文信息。 class GoogleNet(nn.Module):def __init__(self, num_classes10, act_fn_namerelu, **kwargs):super().__init__()self.hparams SimpleNamespace(num_classesnum_classes,act_fn_nameact_fn_name,act_fnact_fn_by_name[act_fn_name])self._create_network()self._init_params()def _create_network(self):# 对原始图像进行首次卷积以扩展通道数self.input_net nn.Sequential(nn.Conv2d(3, 64, kernel_size3, padding1),nn.BatchNorm2d(64),self.hparams.act_fn())# 堆叠Inception模块self.inception_blocks nn.Sequential(# 此处应包含多个Inception模块的具体配置# InceptionBlock的具体参数应根据设计要求设定)# 特征映射到分类输出self.output_net nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(),nn.Linear(128, self.hparams.num_classes))def _init_params(self):# 根据我们在第4个教程中的讨论我们应该根据激活函数初始化卷积层的参数for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, nonlinearityself.hparams.act_fn_name)elif isinstance(m, nn.BatchNorm2d):nn.init.constant_(m.weight, 1)nn.init.constant_(m.bias, 0)def forward(self, x):x self.input_net(x)x self.inception_blocks(x)x self.output_net(x)return x现在我们可以将GoogleNet模型集成到之前定义的模型字典中 model_dict[GoogleNet] GoogleNet模型的训练由PyTorch Lightning框架负责我们只需定义启动命令即可。注意我们训练了近200个epoch在Snellius的默认GPUNVIDIA A100上不到一个小时即可完成。如果感兴趣我们建议直接使用保存的模型并根据需要训练自己的模型。 googlenet_model, googlenet_results train_model(model_nameGoogleNet,model_hparams{num_classes: 10,act_fn_name: relu},optimizer_nameAdam,optimizer_hparams{lr: 1e-3,weight_decay: 1e-4})# 根据实际环境和模型状态输出示例可能有所不同 print(GoogleNet Results, googlenet_results) # GoogleNet Results {test: 0.8970000147819519, val: 0.9039999842643738}我们将在后续的文章中比较结果但可以先在这里打印出来以供初步查看。 print(GoogleNet Results, googlenet_results)3.1.TensorBoard 日志记录 PyTorch Lightning 框架的一个亮点是其能够自动将训练过程中的数据记录到 TensorBoard。为了更直观地展示 TensorBoard 的功能我们可以参考 PyTorch Lightning 在训练 GoogleNet 模型时生成的日志。TensorBoard 为 Jupyter 笔记本提供了一个便捷的内联显示功能这里我们将展示如何使用它 1加载 TensorBoard 扩展 %load_ext tensorboard2启动 TensorBoard 调整日志目录路径以指向你的模型保存路径 %tensorboard --logdir ../saved_models/tutorial5/tensorboards/GoogleNet/c40d590b2e32488fb9b659e4ae7530f3TensorBoard 的界面被划分为多个标签页。其中最主要的是标量Scalar标签页这里可以记录单一数值的变化例如训练损失、准确率、学习率等。通过观察训练或验证的准确率我们可以看到引入学习率调度器的效果。降低学习率能够显著提升模型的训练表现。同样观察训练损失时我们会发现在某一点上损失值突然下降。然而训练集上的数值显著高于验证集这表明模型出现了过拟合现象对于这种大型网络来说过拟合是难以避免的。 另一个引人入胜的标签页是图Graph标签页。它展示了从输入到输出的网络架构按构建模块组织。基本上它展示了 CIFARModule 在前向传播过程中执行的操作。你可以通过双击某个模块来展开查看。不妨从不同角度探索模型的架构。图形可视化常常有助于验证模型是否按照预期进行操作确保计算图中没有遗漏任何层。 4.残差网络ResNet ResNet 论文堪称人工智能领域内被引用次数最多的经典之作它为构建超过千层的深度神经网络提供了理论基础。ResNet 的核心思想——残差连接虽然简单却极为高效它通过维持网络中的梯度稳定传播解决了深层网络训练中的难题。在传统的神经网络中我们通常将下一层的输出表达为 x l 1 F ( x l ) x_{l1}F(x_{l}) xl1​F(xl​)而在 ResNet 中我们采用的公式是 x l 1 x l F ( x l ) x_{l1}x_{l}F(x_{l}) xl1​xl​F(xl​)其中 F F F代表一系列非线性变换如卷积、激活函数和归一化操作。在残差连接的背景下反向传播的梯度表达式为 ∂ x l 1 ∂ x l I ∂ F ( x l ) ∂ x l \frac{\partial x_{l1}}{\partial x_{l}} \mathbf{I} \frac{\partial F(x_{l})}{\partial x_{l}} ∂xl​∂xl1​​I∂xl​∂F(xl​)​ 其中对单位矩阵的偏置确保了梯度传播的稳定性减少了网络深度对梯度衰减的影响。自 ResNet 问世以来研究者们提出了众多变体主要集中在 的功能实现或对求和操作的改进上。本文中将重点介绍两种变体传统的 ResNet 模块和预激活 ResNet 模块并在下方通过可视化对比这两种模块图示来源何等人 在传统的 ResNet 模块中跳跃连接之后通常会跟一个非线性激活函数如 ReLU。而预激活 ResNet 模块则将非线性激活提前至 的开始部分。这两种设计各有利弊。然而在构建极深的网络时预激活 ResNet 显示出更优的性能因为其梯度流始终保证了单位矩阵的存在不受任何非线性激活的影响。为了对比本教程实现了这两种浅层网络结构。 现在让我们深入探讨原始的 ResNet 模块。上述图示已经清晰展示了 中包含的层。特别地当我们需要在宽度和高度上减少图像的维度时需要特别注意。基本的 ResNet 模块要求 具有相同的形状。因此在将 加到 之前我们需要调整 的维度。最初的实现采用了步长为2的恒等映射并通过0填充来增加额外的特征维度。但更常见的做法是使用步长为2的 1x1 卷积这样既能在参数和计算成本上保持高效又能实现特征维度的调整。ResNet 模块的代码实现相对简洁具体如下所示 4.1.传统 ResNet 模块 import torch.nn as nnclass ResNetBlock(nn.Module):def __init__(self, c_in, act_fn, subsampleFalse, c_out-1):构造函数参数说明c_in - 输入特征的数量act_fn - 激活函数的类构造器例如nn.ReLUsubsample - 如果为 True则在模块内部应用步长并将输出形状在高度和宽度上减少2c_out - 输出特征的数量。注意这只在 subsample 为 True 时才相关否则 c_out 默认等于 c_insuper(ResNetBlock, self).__init__()self.subsample subsampleif self.subsample:c_out c_in if c_out 0 else c_out# 定义网络 Fself.net nn.Sequential(nn.Conv2d(c_in, c_out, kernel_size3, padding1, stride2 if subsample else 1, biasFalse), # 由于批量归一化处理不需要偏置nn.BatchNorm2d(c_out),act_fn(),nn.Conv2d(c_out, c_out, kernel_size3, padding1, biasFalse))# 1x1 卷积步长为 2用于维度缩减self.downsample nn.Conv2d(c_in, c_out, kernel_size1, stride2) if subsample else Nonedef forward(self, x):z self.net(x)if self.downsample is not None:x self.downsample(x)out z xout self.act_fn(out)return out4.2.预激活 ResNet 模块 class PreActResNetBlock(nn.Module):def __init__(self, c_in, act_fn, subsampleFalse, c_out-1):构造函数参数说明同上super(PreActResNetBlock, self).__init__()self.subsample subsampleif self.subsample:c_out c_in if c_out 0 else c_out# 定义带有预激活的网络 Fself.net nn.Sequential(nn.BatchNorm2d(c_in),act_fn(),nn.Conv2d(c_in, c_out, kernel_size3, padding1, stride2 if subsample else 1, biasFalse),nn.BatchNorm2d(c_out),act_fn(),nn.Conv2d(c_out, c_out, kernel_size3, padding1, biasFalse))# 1x1 卷积步长为 2同时应用预激活self.downsample nn.Sequential(nn.BatchNorm2d(c_in),act_fn(),nn.Conv2d(c_in, c_out, kernel_size1, stride2, biasFalse)) if subsample else Nonedef forward(self, x):z self.net(x)if self.downsample is not None:x self.downsample(x)out z xreturn out4.3.模块类映射字典 类似于模型选择我们定义一个字典来创建从字符串到模块类的映射。我们将使用字符串名称作为模型中的超参数值以便在 ResNet 模块之间进行选择。你也可以自由实现任何其他类型的 ResNet 模块并在这里添加。 resnet_blocks_by_name {ResNetBlock: ResNetBlock,PreActResNetBlock: PreActResNetBlock }4.4.残差模块堆叠 整体的ResNet架构由多个残差块堆叠而成其中一些块会对输入进行降采样。当我们讨论整个网络中的残差块时通常会根据它们的输出形状将它们分组。例如当我们说ResNet包含[3,3,3]个残差块时这意味着网络中有三组残差块每组包含三个残差块其中第四个和第七个块会进行子采样。在CIFAR10数据集上应用[3,3,3]块的ResNet结构如下所示。 这三组残差块分别处理不同分辨率的特征图。橙色的块表示进行了降采样的残差块。这种表示方法在许多其他实现中也很常见例如PyTorch的torchvision库。因此我们的代码实现如下 class ResNet(nn.Module):def __init__(self, num_classes10, num_blocks[3,3,3], c_hidden[16,32,64], act_fn_namerelu, block_nameResNetBlock, **kwargs):初始化参数num_classes - 分类任务的类别数对于CIFAR10是10num_blocks - 每组中ResNet块的数量列表。每组的第一个块除了第一组都会进行降采样c_hidden - 不同组中隐藏层的通道数。通常随着网络深度增加而翻倍act_fn_name - 要使用的激活函数名称在act_fn_by_name字典中查找block_name - 要使用的ResNet块的名称在resnet_blocks_by_name字典中查找super(ResNet, self).__init__()assert block_name in resnet_blocks_by_name # 确保指定的块名称在字典中self.hparams SimpleNamespace(num_classesnum_classes,c_hiddenc_hidden,num_blocksnum_blocks,act_fn_nameact_fn_name,act_fnact_fn_by_name[act_fn_name], # 获取激活函数block_classresnet_blocks_by_name[block_name] # 获取ResNet块的类)self._create_network() # 创建网络结构self._init_params() # 初始化参数def _create_network(self):c_hidden self.hparams.c_hidden# 对原始图像进行首次卷积增加通道数if self.hparams.block_class PreActResNetBlock: # 如果是预激活块不在输出上应用非线性激活self.input_net nn.Sequential(nn.Conv2d(3, c_hidden[0], kernel_size3, padding1, biasFalse))else:self.input_net nn.Sequential(nn.Conv2d(3, c_hidden[0], kernel_size3, padding1, biasFalse),nn.BatchNorm2d(c_hidden[0]),self.hparams.act_fn() # 应用激活函数)# 创建ResNet块blocks []for block_idx, block_count in enumerate(self.hparams.num_blocks):for bc in range(block_count):subsample (bc 0 and block_idx 0) # 除第一组外每组的第一个块进行子采样blocks.append(self.hparams.block_class(c_inc_hidden[block_idx if not subsample else (block_idx-1)],act_fnself.hparams.act_fn,subsamplesubsample,c_outc_hidden[block_idx]))self.blocks nn.Sequential(*blocks) # 将所有块串联起来# 映射到分类输出self.output_net nn.Sequential(nn.AdaptiveAvgPool2d((1,1)), # 适应性平均池化nn.Flatten(), # 展平特征nn.Linear(c_hidden[-1], self.hparams.num_classes) # 全连接层到分类输出)def _init_params(self):# 根据激活函数初始化卷积层的权重for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityself.hparams.act_fn_name)elif isinstance(m, nn.BatchNorm2d):nn.init.constant_(m.weight, 1)nn.init.constant_(m.bias, 0)def forward(self, x):x self.input_net(x) # 通过输入网络x self.blocks(x) # 通过残差块x self.output_net(x) # 通过输出网络return x我们还需要将新的ResNet类添加到我们的模型字典中 model_dict[ResNet] ResNet最终我们得以训练我们的 ResNet 模型。与训练 GoogleNet 相比一个显著的区别在于我们明确地选择了使用带有动量的 SGD随机梯度下降作为优化算法而不是 Adam。在普通的浅层 ResNet 上Adam 优化器往往只能达到略低的准确度。尽管为何 Adam 在这种情境下表现不佳尚无定论但可能的一个解释是与 ResNet 的损失面特性有关。研究表明ResNet 相较于没有跳跃连接的网络能够产生更平滑的损失面具体详情参见 Li 等人在 2018 年的研究。损失面有无跳跃连接的可视化可能如下图示来源 - Li 等人 在这个可视化中X 轴和 Y 轴表示参数空间的投影而 Z 轴表示由不同参数值计算出的损失值。在像右侧图示那样的平滑表面上我们可能并不需要 Adam 提供的自适应学习率。实际上Adam 可能会陷入局部最优解而 SGD 却能找到更广泛的最小值这些最小值通常能够带来更好的泛化性能。然而要详细解答这个问题我们需要一个额外的教程因为这不是三言两语能够解释清楚的。目前我们得出的结论是对于 ResNet 架构优化器的选择是一个重要的超参数建议尝试使用 Adam 和 SGD 进行训练。让我们使用 SGD 来训练下面的模型 # 训练模型函数调用具体参数根据需要配置 resnet_model, resnet_results train_model(model_nameResNet,model_hparams{num_classes: 10, # 分类任务的类别数c_hidden: [16,32,64], # 隐藏层的通道数num_blocks: [3,3,3], # 每组中 ResNet 块的数量act_fn_name: relu # 激活函数名称},optimizer_nameSGD, # 优化器名称optimizer_hparams{ # 优化器参数lr: 0.1, # 学习率momentum: 0.9, # 动量weight_decay: 1e-4 # 权重衰减} )接下来我们也将训练预激活 ResNet 作为对比 resnetpreact_model, resnetpreact_results train_model(model_nameResNet,model_hparams{num_classes: 10,c_hidden: [16,32,64],num_blocks: [3,3,3],act_fn_name: relu,block_name: PreActResNetBlock # 使用预激活 ResNet 块},optimizer_nameSGD,optimizer_hparams{lr: 0.1,momentum: 0.9,weight_decay: 1e-4},save_nameResNetPreAct # 保存的模型名称 )4.5.TensorBoard 日志 与我们的 GoogleNet 模型类似我们的 ResNet 模型也生成了 TensorBoard 日志。我们可以在下方打开它以进行更深入的分析。 # 在 Jupyter 笔记本中打开 TensorBoard根据需要调整日志目录路径 %tensorboard --logdir ../saved_models/tutorial5/tensorboards/ResNet/你可以自由探索 TensorBoard包括计算图。总体来看我们可以看到在训练初期使用 SGD 的 ResNet 相比于 GoogleNet 有更高的训练损失。然而在降低学习率之后ResNet 模型甚至能够达到更高的验证准确率。我们会在本文的末尾比较确切的分数。 5.DenseNet 架构 DenseNet 是一种创新的深度神经网络架构它通过一种新颖的方式利用残差连接。与常规的残差连接不同DenseNet 将这些连接视为在不同层之间重用特征的途径从而避免了学习重复特征图的需求。在网络的深层模型会学习到抽象的特征以识别复杂的模式。然而一些复杂的模式可能同时包含抽象特征如手、脸等和基础特征如边缘、基本颜色等。为了在深层网络中捕捉这些基础特征传统的卷积神经网络CNN不得不学习复制这些特征图这无疑增加了参数的复杂性。DenseNet 通过让每一层的卷积操作依赖于之前所有层的输入特征并且只增加少量的滤波器提供了一种高效的特征重用机制。以下是这种架构的直观展示图示来源Hu 等人 在网络的最后存在一种称为“过渡层”的结构它的任务是降低特征图在高度、宽度和通道数上的维度。虽然从理论上讲这些过渡层会中断恒等映射的反向传播但由于网络中这样的层并不多因此对梯度流动的影响有限。 5.1.DenseNet的实现 我们在实现 DenseNet 时将网络层的构建分为三个部分DenseLayer密集层、DenseBlock密集块和 TransitionLayer过渡层。DenseLayer 模块负责实现密集块内的单层操作。它首先通过 1x1 卷积进行维度缩减然后是 3x3 卷积。输出的通道会与原始输入的特征图进行拼接并返回。值得注意的是我们在每个块的第一层应用了批量归一化Batch Normalization这样做可以为不同层提供略微不同的激活以满足不同层的需求。以下是具体的实现方式 class DenseLayer(nn.Module):def __init__(self, c_in, bn_size, growth_rate, act_fn):初始化参数c_in - 输入通道数bn_size - 瓶颈大小增长率的倍数用于 1x1 卷积的输出growth_rate - 3x3 卷积的输出通道数act_fn - 激活函数的类构造器例如 nn.ReLUsuper(DenseLayer, self).__init__()Get an email address at self.net. Its ad-free, reliable email thats based on your own name | self.net nn.Sequential(nn.BatchNorm2d(c_in), # 批量归一化act_fn(), # 激活函数nn.Conv2d(c_in, bn_size * growth_rate, kernel_size1, biasFalse), # 1x1 卷积nn.BatchNorm2d(bn_size * growth_rate), # 批量归一化act_fn(), # 激活函数nn.Conv2d(bn_size * growth_rate, growth_rate, kernel_size3, padding1, biasFalse) # 3x3 卷积)def forward(self, x):out Get an email address at self.net. Its ad-free, reliable email thats based on your own name | self.net(x)out torch.cat([out, x], dim1) # 将新特征图与原始特征图在通道维度上进行连接return out DenseBlock 模块则将多个密集层按顺序应用。每个密集层的输入是原始输入与之前所有层的特征图的拼接 class DenseBlock(nn.Module):def __init__(self, c_in, num_layers, bn_size, growth_rate, act_fn):初始化参数c_in - 输入通道数num_layers - 块中应用的密集层的数量bn_size - 密集层中使用的瓶颈大小growth_rate - 密集层中使用的增长率act_fn - 密集层中使用的激活函数super(DenseBlock, self).__init__()layers []for layer_idx in range(num_layers): # 对于每个密集层layers.append(DenseLayer(c_inc_in layer_idx * growth_rate, # 输入通道是原始通道加上之前层的特征图bn_sizebn_size,growth_rategrowth_rate,act_fnact_fn))self.block nn.Sequential(*layers) # 将所有密集层串联起来def forward(self, x):out self.block(x)return out 5.2. DenseNet 添加至模型字典 首先我们将 DenseNet 模型加入到我们的模型字典中以便后续使用 model_dict[DenseNet] DenseNet 5.3.训练 DenseNet 网络 接下来我们开始训练 DenseNet 网络。与 ResNet 不同的是DenseNet 在使用 Adam 优化器时并没有出现任何问题因此我们选择使用 Adam 来训练它。我们选择的其他超参数旨在构建一个与 ResNet 和 GoogleNet 具有相似参数规模的网络。通常在设计非常深的网络时DenseNet 相较于 ResNet 在参数效率上更胜一筹同时能够实现相似甚至更优的性能。 densenet_model, densenet_results train_model(model_nameDenseNet, # 模型名称model_hparams{ # 模型超参数num_classes: 10, # 类别数num_layers: [6,6,6,6], # 每块中的层数bn_size: 2, # 批量归一化的瓶颈大小growth_rate: 16, # 增长率act_fn_name: relu # 激活函数名称},optimizer_nameAdam, # 优化器名称optimizer_hparams{ # 优化器超参数lr: 1e-3, # 学习率weight_decay: 1e-4 # 权重衰减} )5.4.TensorBoard 日志 最后我们还有另一个 TensorBoard 日志专门用于 DenseNet 的训练。我们可以在下方打开它以便更深入地了解训练过程 # 在 Jupyter 笔记本中打开 TensorBoard需要根据实际情况调整日志目录路径 %tensorboard --logdir ../saved_models/tutorial5/tensorboards/DenseNet/验证准确率的整体趋势和训练损失与 GoogleNet 的训练过程相似这也与使用 Adam 进行网络训练有关。你可以自由探索训练指标以获得更深入的理解。 6. 结论与比较 经过对每个模型的单独讨论和训练我们现在可以对它们进行综合比较。首先我们将所有模型的结果汇总到一个表格中以便于分析 %%html !-- 以下HTML代码用于增大表格中的字体大小 -- style th {font-size: 120%;} td {font-size: 120%;} /styleimport tabulate from IPython.display import display, HTML all_models [(GoogleNet, googlenet_results, googlenet_model),(ResNet, resnet_results, resnet_model),(ResNetPreAct, resnetpreact_results, resnetpreact_model),(DenseNet, densenet_results, densenet_model) ] table [[model_name,f{100.0*model_results[val]:4.2f}%,f{100.0*model_results[test]:4.2f}%,{:,}.format(sum([np.prod(p.shape) for p in model.parameters()]))]for model_name, model_results, model in all_models] display(HTML(tabulate.tabulate(table, tablefmthtml, headers[模型, 验证准确率, 测试准确率, 参数数量])))模型验证准确率测试准确率参数数量GoogleNet90.40%89.70%260,650ResNet91.84%91.06%272,378ResNetPreAct91.80%91.07%272,250DenseNet90.72%90.23%239,146 表格展示的结果显示了各模型在验证集和测试集上的准确率以及它们的参数数量。从数据可以看出所有模型都展现出了合理的性能。然而相较于我们实现的简单模型这些复杂模型不仅参数数量更多而且性能也更高这在一定程度上归功于它们的架构设计。 GoogleNet 在验证集和测试集上的表现略低于其他模型尽管与 DenseNet 的性能相差无几。如果对 GoogleNet 的通道尺寸进行彻底的超参数优化很可能能够提升其准确率至类似水平但这需要考虑到大量超参数因此成本较高。ResNet 在验证集上的性能优于 DenseNet 和 GoogleNet 超过 1%而原始 ResNet 与预激活 ResNet 之间的性能差异不大。我们可以得出结论对于浅层网络而言激活函数的位置似乎并不是决定性因素尽管有文献报道对于极深的网络情况可能正好相反。 总体而言ResNet 证明了自己是一种简单而强大的架构。如果我们将这些模型应用于更复杂的任务例如处理更大图像和需要网络内部更多层的任务我们可能会观察到 GoogleNet 与带有跳跃连接的架构如 ResNet 和 DenseNet 之间的性能差异更加明显。在 CIFAR10 数据集上与更深层模型的比较分析可以在这里找到。有趣的是DenseNet 在某些设置上优于原始的 ResNet但与预激活 ResNet 的性能相近。最佳模型双路径网络Chen 等人提出实际上是 ResNet 和 DenseNet 的结合体这表明两种架构各自都有其独特的优势。 6.1 模型选择建议 我们已经评估了四种不同的模型。那么面对新任务时我们应该选择哪一种模型呢通常鉴于 ResNet 在 CIFAR 数据集上的卓越性能和简单的实现方式从 ResNet 开始是一个不错的选择。此外考虑到我们选择的参数数量ResNet 在训练速度上也是最快的因为 DenseNet 和 GoogleNet 有更多的层需要顺序执行。然而如果你面对的是一项极具挑战性的任务比如在高清图像上进行语义分割那么更推荐使用更复杂的 ResNet 或 DenseNet 变体。
http://www.w-s-a.com/news/71140/

相关文章:

  • 建设银行官网首页网站购纪念币接做网站需要问什么条件
  • 网站的ftp地址是什么江苏做网站
  • 宁波网站建设制作公司哪家好潍坊建公司网站
  • 云端网站建设php7 wordpress速度
  • 建站的公司中小企业网站建设报告
  • 上海高档网站建设网站设计入门
  • 德尔普网站建设做网站线
  • 宁波网站搭建定制非模板网站建设电子商务公司名称大全简单大气
  • 巴中哪里做网站推销网站的方法
  • wordpress建站动画网站宣传的手段有哪些?(写出五种以上)
  • 做么网站有黄医疗机构网站备案
  • 企业年金是1比3还是1比4北京厦门网站优化
  • 政务信息网站建设工作方案云南建设工程质量监督网站
  • 如何做一份企业网站免费的短视频素材库
  • 云脑网络科技网站建设咸阳软件开发
  • seo对网站优化网站更换程序
  • 网站建设放什么科目中小学生在线做试卷的网站6
  • 网站建设推广公司排名绥化建设局网站
  • 凡科做的网站为什么打不开苏州行业网站建设
  • 南昌定制网站开发费用微信小商店官网入口
  • 深圳网站建设费用找人做的网站怎么看ftp
  • 做网站cookie传值dedecms网站后台
  • 温州网站推广网站建设要学会什么
  • c 网站开发框架品牌策划方案范文
  • 儿童摄影作品网站多元网络兰州网站建设
  • 电脑上不了建设厅网站常德网站建设费用
  • 做单页免费模板网站最新办公室装修风格效果图
  • 中国铁路建设投资公司网站熊学军想开网站建设公司
  • 优化一个网站多少钱网站开发北京
  • html教学关键词优化价格