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

在灵璧怎样做网站b2b电子商务网

在灵璧怎样做网站,b2b电子商务网,人民日报客户端app,青岛三吉互联网站建设公司目录 写在开头 一、CNN的原理 1. 概述 2. 卷积层 内参数(卷积核本身) 外参数(填充和步幅) 输入与输出的尺寸关系 3. 多通道问题 多通道输入 多通道输出 4. 池化层 平均汇聚 最大值汇聚 二、手写数字识别 1. 任务…

目录

写在开头

一、CNN的原理

1. 概述

2. 卷积层

内参数(卷积核本身)

外参数(填充和步幅)

输入与输出的尺寸关系 

3. 多通道问题 

多通道输入

多通道输出

4. 池化层

平均汇聚

最大值汇聚

二、手写数字识别

1. 任务描述和数据集加载

2. 网络结构(LeNet-5)

3. 模型训练

4. 模型测试

5. 直观显示预测结果

写在最后

写在开头

    本文将将介绍如何使用PyTorch框架搭建卷积神经网络(CNN)模型。简易介绍卷积神经网络的原理,并实现模型的搭建、数据集加载、模型训练、测试、网络的保存等。实现机器学习领域的Hello world——手写数字识别。本文的讲解参考了B站两位up主:爆肝杰哥、炮哥带你学。有关Pytorch环境配置和CNN具体原理大家可以自行查阅资料,本文多数图片也来自于爆肝杰哥的讲解。这里也放上两位up主的视频链接:

从0开始撸代码--手把手教你搭建LeNet-5网络模型_哔哩哔哩_bilibili

 Python深度学习:安装Anaconda、PyTorch(GPU版)库与PyCharm_哔哩哔哩_bilibili

    本文使用的PyTorch为1.12.0版本,Numpy为1.21版本,相近的版本语法差异很小。有关数组的数据结构教程、神经网络的基本原理(前向传播/反向传播)、神经网络作为“函数模拟器”直观感受、深度神经网络的实现DNN详见本专栏的前三篇文章,链接如下: 

【深度学习基础】NumPy数组库的使用-CSDN博客

【深度学习基础】用PyTorch从零开始搭建DNN深度神经网络_如何搭建一个深度学习神经网络dnn pytorch-CSDN博客

【深度学习基础】使用Pytorch搭建DNN深度神经网络与手写数字识别_dnn网络模型 代码-CSDN博客

    基于深度神经网络DNN实现的手写数字识别,将灰度图像转换后的二维数组展平到一维,将一维的784个特征作为模型输入。在“展平”的过程中必然会失去一些图像的形状结构特征,因此基于DNN的实现方式并不能很好的利用图像的二维结构特征,而卷积神经网络CNN对于处理图像的位置信息具有一定的优势。因此卷积神经网络经常被用于图像识别/处理领域。下面我们将对CNN进行具体介绍。

一、CNN的原理

1. 概述

    在上一篇博客介绍的深度神经网络DNN中,网络的每一层神经元相互直接都有链接,每一层都是全连接层,我们的目标就是训练这个全连接层的权重w和偏执b,最终得到预测效果良好的网络结构。

    DNN的全连接层对应于CNN中的卷积层,而池化层(汇聚)其实与激活函数的作用类似。CNN中完整的卷积层的结构是:卷积-激活函数-池化(汇聚),其中池化层也有时可以省略。一个卷积神经网络的结构如下:

    如上图所示,CNN的优势在于可以处理多为输入数据,并同样以多维数据的形式输出至下一层,保留了更多的空间信息特征。而DNN却只能将多维数据展平成一维数据,必然会损失一些空间特征。

2. 卷积层

内参数(卷积核本身)

    CNN中的卷积层和DNN中的全连接层是平级关系,在DNN中,我们训练的内参数是全连接层的权重w和偏置b,CNN也类似,CNN训练的是卷积核,也就相当于包含了权重和偏置两个内部参数。下面我们首先描述什么是卷积运算。当输入数据进入卷积层后,输入数据会与卷积核进行卷积运算,运算方法如下图所示:

     输入一个多维数据(上图为二维),与卷积核进行运算,即输入中与卷积核形状相同的部分,分别与卷积核进行逐个元素相乘再相加。例如计算结果中坐上角的15是根据如下过程计算得到的:

逐个元素相乘再相加,即:

1 * 2 + 2 * 0 + 3* 1 + 0 * 0 + 1 * 1 + 2 * 2 + 3 * 1 + 0 * 0 + 1 * 2 = 15 

     卷积核本身相当于权重,再卷积运算的过程中也可以存在偏置,如下:

    卷积核(即CNN的权重和偏置)本身为内参数,(具体里面的数字)是我们通过训练得出的,我们写代码的时候只要关注一些外部设定的参数即可。下面我们将介绍一些外参数。

外参数(填充和步幅)

   填充(padding)

   显然,只要卷积核的大小>1*1,必然会导致图像越卷越小,为了防止输入经过多个卷积层后变得过小,可以在发生卷积层之前,先向输入图像的外围填入固定的数据(比如0),这个步骤称之为填充,如下图:

     在我们使用Pytorch搭建卷积层的时候,需要在对应的接口中添加这个padding参数,向上图中这种情况,相当于在3*3的卷积核外围添加了“一圈”,则padding = 1,卷积层的接口中就要这样写:

nn.Conv2d(in_channels=1, out_channels=6, kernel_size=3, paddding=1)

      参数in_channels和out_channels是对应于这个卷积层输入和输出的通道数参数,这里我们先放一放。

   步幅(stride)

   步幅指的是使用卷积核的位置间隔,即输入中参与运算的那个范围每次移动的距离。前面几个示意图中的步幅均为1,即每次移动一格,如果设置stride=2,kernel_size=2,则效果如下:

     此时需要在卷积层接口中添加参数stride=2。

输入与输出的尺寸关系 

   综上所述,结合外参数(步幅、填充)和内参数(卷积核),可以看出如下规律:

卷积核越大,输出越小。

步幅越大,输出越小。

填充越大,输出越大。

     用公式表示定量关系:

      如果输入和卷积核均为方阵,设输入尺寸为W*W,输出尺寸为N*N,卷积核尺寸为F*F,填充的圈数为P,步幅为S,则有关系:

N = \frac{W+2P-F}{S} + 1

    这个关系大家要重点掌握,也可以自己推导一下,并不复杂。如果输入和卷积核不为方阵,设输入尺寸是H*W,输出尺寸是OH*OW,卷积核尺寸为FH*FW,填充为P,步幅为S,则输出尺寸OH*OW的计算公式是:

OH = \frac{H+2P-FH}{S} + 1

OW = \frac{W+2P-FW}{S} + 1 

3. 多通道问题 

多通道输入

    对于手写数字识别这种灰度图像,可以视为仅有(高*长)二维的输入。然而,对于彩色图像,每一个像素点都相当于是RGB的三个值的组合,因此对于彩色的图像输入,除了高*长两个维度外,还有第三个维度——通道,即红、绿、蓝三个通道,也可以视为3个单通道的二维图像的混合叠加。

当输入数据仅为二维时,卷积层的权重往往被称作卷积核(Kernel);

当输入数据为三维或更高时,卷积层的权重往往被称作滤波器(Filter)

     对于多通道输入,输入数据和滤波器的通道数必须保持一致。这样会导致输出结果降维成二维,如下图:

     对形状进行一下抽象,则输入数据C*H*W和滤波器C*FH*FW都是长方体,结果是一个长方形1*OH*OW,注意C,H,W是固定的顺序,通道数要写在最前。

多通道输出

    如果要实现多通道输出,那么就需要多个滤波器,让三维输入与多个滤波器进行卷积,就可以实现多通道输出,输出的通道数FN就是滤波器的个数FN,如下图:

    和单通道一样,卷积运算后也有偏置,如果进一步追加偏置,则结果如下:每个通道都有一个单独的偏置。 

4. 池化层

   池化,也叫汇聚(Pooling)。池化层通常位于卷积层之后(有时也可以不设置池化层),其作用仅仅是在一定范围内提取特征值,所以并不存在要学习的内部参数。池化仅仅对图像的高H和宽W进行特征提取,并不改变通道数C

平均汇聚

一般有平均汇聚和最大值汇聚两种。平均汇聚如下:

    如上图,池化的窗口大小为2*2,对应的步幅为2,因此对于上图这种情况,对应的Pytorch接口如下:

nn.AvgPool2d(kernel_size=2, stride=2) 

最大值汇聚

   同理,如果使用最大值汇聚,如下图所示:

    此处Pytorch函数就这么写:

nn.MaxPool2d(kernel_size=2, stride=2) 

二、手写数字识别

1. 任务描述和数据集加载

    此处和上一篇博客类似,详情见:

【深度学习基础】使用Pytorch搭建DNN深度神经网络与手写数字识别_dnn网络模型 代码-CSDN博客

     接下来我们实现机器学习领域的Hello World——手写数字识别。使用的数据集MNIST是机器学习领域的标准数据集,其中的每一个样本都是一副二维的灰度图像,尺寸为28*28:

   输入就相当于一个单通道的图像,是二维的。我们在实现的时候,要将每个样本图像转换为28*28的张量,作为输入,此处和上一篇DNN都一致。数据集则通过包torchvision中的datasets库进行下载。这里我快速给一段代码好了,详情可见上一篇博客。

import torch
from torchvision import datasets, transforms# 设定下载参数 (数据集转换参数),将图像转换为张量
data_transform = transforms.Compose([transforms.ToTensor()
])# 加载训练数据集
train_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径,读者请自行设置train=True,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)# 批次加载器,在接下来的训练中进行小批次(16批次)的载入数据,有助于提高准确度,对训练集的样本进行打乱,
train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=True)# 加载测试数据集
test_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径train=False,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=True)

2. 网络结构(LeNet-5)

   本文搭建的LeNet-5起源于1998年,在手写数字识别上非常成功。其结构如下:

再列一个表格,具体结构如下:

     注:输出层的激活函数目前已经被Softmax取代。

     至于这些尺寸关系,我举个两例子吧:

   以第一层C1的输入和输出为例。输入尺寸W是28*28,卷积核F尺寸为5*5,步幅S为1,填充P为2,那么输出N的28*28怎么来的呢?按照公式如下:

N = \frac{W + 2P - F}{S} + 1= \frac{28 + 2*2 - 5}{1} + 1 = 28

   我们也可以观察到第一层的卷积核个数为6,则输出的通道数也为6。

  再看一下第一个池化层S2,输入尺寸是28*28,卷积核F大小为2*2(此处的“卷积核”实际上指的是采样范围),步幅S=2,填充P为0,则输出的14*14是这么算出来的:

N = \frac{W + 2P - F}{S} + 1= \frac{28 + 2*0 - 2}{2} + 1 = 14

  其他的没啥好说的,读者们可以自行计算这个尺寸关系。接下来我们给出完整的CNN网络代码,net.py如下:

import torch
from torch import nn# 定义网络模型
class MyLeNet5(nn.Module):# 初始化网络def __init__(self):super(MyLeNet5, self).__init__()self.net = nn.Sequential(nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), nn.Tanh(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), nn.Tanh(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5),  nn.Tanh(),nn.Flatten(),nn.Linear(120, 84),  nn.Tanh(),nn.Linear(84, 10))# 前向传播def forward(self, x):y = self.net(x)return y# 以下为测试代码,也可不添加
if __name__ == '__main__': x1 = torch.rand([1, 1, 28, 28])model = MyLeNet5()y1 = model(x1)print(x1)print(y1)

     这里我还在'__main__'添加了一些测试代码,正如表格中所示,假设我们向网络中输入一个1*1*28*28的向量,模拟批次大小为1,一个单通道28*28的灰度图输入。最终y应该是由10个数字组成的张量,结果如下:

     从输出我们可以直观的看到输入经过神经网络前向传播后的结果。另外特别注意这个网络的结构中的参数,其中卷积层的搭建API有5个外参数:

in_channels:输入通道数

out_channels:输出通道数

kernel_size: 卷积核尺寸

padding: 填充,不写则默认0

stride: 步幅,不写则默认1

     这个LeNet-5网络结构就长这样,我们一定要严格遵守,否则有可能出现无论怎么训练,都始终欠拟合的情况。我就曾经试过更改/添加不同的激活函数,结果无论是训练还是测试,准确率都在10%徘徊,相当于随机瞎猜的效果,因此大家一定要严格遵循这个网络结构。 

3. 模型训练

    网络搭建好之后,所有的内参数(即卷积核)都是随机的,下面我们要通过训练尽可能提高网络的预测能力。在训练前,我们首先要选择损失函数(这里使用交叉熵损失函数),定义优化器、进行学习率调整等,代码如下:

import torch
from torch import nn
from net import MyLeNet5
from torch.optim import lr_scheduler# 判断是否有gpu
device = "cuda" if torch.cuda.is_available() else "cpu"# 调用net,将模型数据转移到gpu
model = MyLeNet5().to(device)# 选择损失函数
loss_fn = nn.CrossEntropyLoss()    # 交叉熵损失函数,自带Softmax激活函数# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9)# 学习率每隔10轮次, 变为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

    然后我们可以写一个用于训练网络的函数,四个参数分别是批次加载器、模型、损失函数、优化器,代码如下:

# 定义模型训练的函数
def train(dataloader, model, loss_fn, optimizer):loss, current, n = 0.0, 0.0, 0for batch, (X, y) in enumerate(dataloader):# 前向传播X, y = X.to(device), y.to(device)output = model(X)cur_loss = loss_fn(output, y)# 用_和pred分别接收输出10个元素中的最大值和对应下标位置_, pred = torch.max(output, dim=1)# 计算当前轮次时,训练集的精确度,将所有标签值与预测值(即下标位置)cur_acc = torch.sum(y == pred)/output.shape[0]# 反向传播,对内部参数(卷积核)进行优化optimizer.zero_grad()cur_loss.backward()optimizer.step()# 计算准确率和损失,这里只是为了实时显示训练集的拟合情况。也可以不写loss += cur_loss.item()current += cur_acc.item()n = n + 1print("train_loss: ", str(loss/n))print("train_acc: ", str(current/n))

只要调用这个函数,即可实现模型训练:

train(train_dataloader, model, loss_fn, optimizer)

当然,我们最好是设定一个轮次epoch,我们后续会写这样一个循环,每训练一个epoch,就进行一次测试,实时显示一定轮次后训练集和测试集的拟合情况。

4. 模型测试

   这里和模型训练类似,只不过我们要观察训练好的模型,在测试集的预测效果。与训练的代码相似,只是没有了反向传播优化参数的过程。用于测试的函数代码如下:

def test(dataloader, model, loss_fn):model.eval()loss, current, n = 0.0, 0.0, 0# 该局部关闭梯度计算功能,提高运算效率with torch.no_grad():  for batch, (X, y) in enumerate(dataloader):# 前向传播X, y = X.to(device), y.to(device)output = model(X)cur_loss = loss_fn(output, y)_, pred = torch.max(output, dim=1)# 计算当前轮次时,训练集的精确度cur_acc = torch.sum(y == pred) / output.shape[0]loss += cur_loss.item()current += cur_acc.item()n = n + 1print("test_loss: ", str(loss / n))print("test_acc: ", str(current / n))return current/n    # 返回精确度

    如上代码,将测试集的精确度作为返回值,我们在外围调用这个函数时,可以通过循环找到测试集最大的精确度。

    最终我们设定一个训练轮次epochs,此处epochs=50,每经过一个epoch的训练,就进行测试,实时打印观察训练集和测试集的拟合情况。当测试集的精确度是当前的最大值时,我们就保存这个模型的参数到save_model/best_model.pth,代码如下:

import os# 开始训练
epoch = 50
max_acc = 0
for t in range(epoch):print(f"epoch{t+1}\n---------------")# 训练模型train(train_dataloader, model, loss_fn, optimizer)# 测试模型a = test(test_dataloader, model, loss_fn)# 保存最好的模型参数if a > max_acc:folder = 'save_model'if not os.path.exists(folder):os.mkdir(folder)max_acc = aprint("current best model acc = ", a)torch.save(model.state_dict(), 'save_model/best_model.pth')
print("Done!")

    运行之后可以发现,测试集的精度经过1个epochs就达到了90%以上,最终经过50轮次的训练,测试集精度达到了99%左右:

   模型参数也得以保存:

    最后给出用于训练和测试的完整代码train.py,如下所示:

import torch
from torch import nn
from net import MyLeNet5
from torch.optim import lr_scheduler
from torchvision import datasets, transforms
import os# 将图像转换为张量形式
data_transform = transforms.Compose([transforms.ToTensor()
])# 加载训练数据集
train_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径train=True,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)# 批次加载器
train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=True)# 加载测试数据集
test_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径train=False,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)
test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=True)# 判断是否有gpu
device = "cuda" if torch.cuda.is_available() else "cpu"# 调用net,将模型数据转移到gpu
model = MyLeNet5().to(device)# 选择损失函数
loss_fn = nn.CrossEntropyLoss()    # 交叉熵损失函数,自带Softmax激活函数# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9)# 学习率每隔10轮次, 变为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)# 定于训练函数
def train(dataloader, model, loss_fn, optimizer):loss, current, n = 0.0, 0.0, 0for batch, (X, y) in enumerate(dataloader):# 前向传播X, y = X.to(device), y.to(device)output = model(X)cur_loss = loss_fn(output, y)_, pred = torch.max(output, dim=1)# 计算当前轮次时,训练集的精确度cur_acc = torch.sum(y == pred)/output.shape[0]# 反向传播optimizer.zero_grad()cur_loss.backward()optimizer.step()loss += cur_loss.item()current += cur_acc.item()n = n + 1print("train_loss: ", str(loss/n))print("train_acc: ", str(current/n))def test(dataloader, model, loss_fn):model.eval()loss, current, n = 0.0, 0.0, 0# 该局部关闭梯度计算功能,提高运算效率with torch.no_grad():for batch, (X, y) in enumerate(dataloader):# 前向传播X, y = X.to(device), y.to(device)output = model(X)cur_loss = loss_fn(output, y)_, pred = torch.max(output, dim=1)# 计算当前轮次时,训练集的精确度cur_acc = torch.sum(y == pred) / output.shape[0]loss += cur_loss.item()current += cur_acc.item()n = n + 1print("test_loss: ", str(loss / n))print("test_acc: ", str(current / n))return current/n    # 返回精确度# 开始训练
epoch = 50
max_acc = 0
for t in range(epoch):print(f"epoch{t+1}\n---------------")train(train_dataloader, model, loss_fn, optimizer)a = test(test_dataloader, model, loss_fn)# 保存最好的模型参数if a > max_acc:folder = 'save_model'if not os.path.exists(folder):os.mkdir(folder)max_acc = aprint("current best model acc = ", a)torch.save(model.state_dict(), 'save_model/best_model.pth')
print("Done!")

5. 直观显示预测结果

     截至目前,我们已经完成了手写数字识别这个任务,但是我们好像对于数据集长什么样并不是很了解,似乎仅仅是用torchvision中的datasets库下载了一下。因此本小节,我们的目标是从数据集取出几个特定的手写数字图片,并查看我们模型对其的预测效果。

   首先我们还是加载数据集,和之前的代码一样,这里省略。然后我们加载模型:

from net import  MyLeNet5# 调用net,将模型数据转移到gpu
model = MyLeNet5().to(device)
model.load_state_dict(torch.load('./save_model/best_model.pth'))

     我们取出测试集中的前5长图片做一个展示即可,完整代码show.py如下:

import torch
from net import MyLeNet5
from torchvision import datasets, transforms
from torchvision.transforms import ToPILImagedata_transform = transforms.Compose([transforms.ToTensor()
])# 加载训练数据集
train_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径train=True,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)# 加载测试数据集
test_dataset = datasets.MNIST(root='D:\\Jupyter\\dataset\\minst',  # 下载路径train=False,   # 是训练集download=True,   # 如果该路径没有该数据集,则进行下载transform=data_transform   # 数据集转换参数
)# 判断是否有gpu
device = "cuda" if torch.cuda.is_available() else "cpu"# 调用net,将模型数据转移到gpu
model = MyLeNet5().to(device)
model.load_state_dict(torch.load('./save_model/best_model.pth'))# 获取结果
classes = ["0","1","2","3","4","5","6","7","8","9"
]# 把tensor转化为图片,方便可视化
image = ToPILImage()# 进入验证
for i in range(5):X, y = test_dataset[i][0], test_dataset[i][1]      # X,y对应第i张图片和标签# image是ToPILImage的实例,将Pytorch张量转换为PIL图像,.show()方法会打开图像查看器并显示图像image(X).show()'''unsqueeze 方法在指定的 dim 维度上扩展张量的维度。这里 dim=0,所以它会在第0维添加一个维度.例如,原来的 X 形状是 (1, 28, 28),经过 unsqueeze 处理后,形状变为 (1, 1, 28, 28)。这样做的目的是将单张图像扩展成批次大小为1的形式,这样模型可以接收单张图像作为输入。'''X = torch.unsqueeze(X, dim=0).float().to(device)with torch.no_grad():# 前向传播获得预测结果pred(由10个元素组成的张量)pred = model(X)print(pred)# 将预测值和标签转化为对应的数字分类结果,pred中的最大值视为预测分类predicted, actual = classes[torch.argmax(pred[0])], classes[y]print(f"predicted: {predicted}, actual: {actual}")

   运行结果如下,我们可以看到测试集中的前五张图片分别是7,2,1,0,4,且我们的模型都能对其进行成功预测分类。

   预测结果均正确,如下:

写在最后

        本文介绍了如何使用PyTorch框架搭建卷积神经网络模型CNN。将CNN与DNN进行了类比。CNN中的卷积层与DNN的全连接层是平级关系。我们实现了LeNet-5的模型的搭建、模型训练、测试、网络的复用、直观查看数据集的图片预测结果等,实现了机器学习领域的Hello world——手写数字识别。在CNN原理中,读者应当重点关注输入输出的尺寸关系,并可以对照LeNet-5结构示意图写出对应Pytorch代码。至于模型训练和测试基本都是固定的代码形式。

      这篇文章到这里就结束了,后续我还会继续更新深度学习的相关知识,另外近期我个人的研究方向涉及到图神经网络,回头也会更新一些相关博客。如果读者有相关建议或疑问也欢迎评论区与我共同探讨,我一定知无不言。总结不易,还请读者多多点赞关注支持!


 

http://www.hengruixuexiao.com/news/48748.html

相关文章:

  • 深圳沙井做网站公司百度手机助手安卓版
  • 上海高端网站建设公公司seo
  • 做网站多少钱角西宁君博特惠江小白网络营销案例
  • 电商网站设计规范浙江seo关键词
  • 成人高考准考证打印网站品牌营销推广代运营
  • 广州一建筑外墙脚手架坍塌天津百度网站快速优化
  • 1920网页设计尺寸规范商丘seo外包
  • 网站网络推广站优云seo优化
  • 域名交易网站代做seo排名
  • 政府网站建设先进推荐材料黄页88网
  • 怎么用linux做网站电商运营培训课程有哪些
  • mx wordpressseo推广优化的方法
  • 好的学习网站打广告青岛seo服务公司
  • 如何进行网站维护如何开展网络营销
  • 上传商品的网站长沙seo公司排名
  • 做标书有哪些好网站seo系统培训班
  • 丰城住房和城乡建设部网站友情链接推广平台
  • 郑州电力高等专科学校录取分数线seo关键词的选择步骤
  • 网站设计结果德阳网站seo
  • 石家庄住房和城乡建设部网站百度seo培训
  • html5 微网站布局企业网站是什么
  • 网站变灰微信小程序开发流程
  • 网络推广怎么能做好不错宁波seo公司
  • 网站建设面谈销售话术网站接广告
  • 东莞做网站优化天助网络广告联盟下载app
  • 东莞建设网站企业沟通平台查询网站注册信息
  • 烟台网站优化公司知乎seo
  • 男女做那个真实的视频网站太原网站优化公司
  • 网站域名费会计分录怎么做成品视频直播软件推荐哪个好用
  • 做网站属于什么技术资源网