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

长沙哪些公司做网站/seo俱乐部

长沙哪些公司做网站,seo俱乐部,低多边形生成网站,邳州网页定制TensorFlow模型导出到OpenCV调用引言1.模型训练(1)些许改进(2)整体训练模型的代码如下:(3)生成模型(4)控制台结果2.使用tensorboard查看模型架构,找出输入输出…

TensorFlow模型导出到OpenCV调用

  • 引言
  • 1.模型训练
    • (1)些许改进
    • (2)整体训练模型的代码如下:
    • (3)生成模型
    • (4)控制台结果
  • 2.使用tensorboard查看模型架构,找出输入输出(可跳过)
    • (1)生成事件文件
    • (2)在cmd中执行以下语句
    • (3)执行结果
    • (4)在浏览器中查看
  • 3.导出为pb文件
    • (1)注意事项
    • (2)代码如下:
    • (3)执行结果如下
  • 4.查看pb文件的节点名称(可跳过)
    • (1)代码如下:
    • (2)执行结果如下:
  • 5.使用OpenCV查看其网络信息(可跳过)
    • (1)代码如下:
    • (2)结果如下:
  • 6.使用OpenCV调用模型,输入图片测试分类结果
    • (1)代码如下:
    • (2)结果如下所示:
  • 结论

引言

本人因之前学习的c++,所以对c++的界面编程以及对OpenCV的使用较为熟悉,之前也学习过使用OpenCV调用face_detection基于tensorflow平台的模型文件(pb文件,pbtxt)文件,最近也在学习TensorFlow用来训练自己的分类模型,但对如何将已经训练好的模型进行导出,并应用于OpenCV这方面不是很了解,所以查阅了大量资料,总算能够将其实现。

1.模型训练

参考文章: 手把手教你训练分类自己的图片(车辆分类).

(1)些许改进

  • 该文章整体架构很清晰,因为要使用此训练的模型,故在其中加入了模型加载及保存功能,使其变为增量训练模型,这样可以进行多次训练,将上次训练的模型加载进来,在此基础上进行训练,可以保存训练结果。
  • 因为使用的训练集的分类只有7类,故将其最后一层全连接层的输出改为7,此处是多加了一层使其输出变为7。
  • 在此架构中加入了with tf.name_scope(‘name’): ,这是为了后面使用tensorboard查看整体架构时清晰一些。
  • 还在最后一层的位置加入了softmax层,这一层不与后面发生关系,因为后面已经有sparse_softmax_cross_entropy了,此softmax仅仅是为了后面用来导出pb文件来使用的,softmax的节点名称是output。
  • 此架构本身具有将读入图片变为特定大小的功能,将img = transform.resize(img, (w, h))前的注释去掉即可。
  • 当我们想要训练自己的模型时,直接在此架构的基础上将训练模型改成自己想要训练的模型即可。
  1. 模型训练所用到的数据
    直接使用上方参考文章中的数据即可(数据量并不大,可以自己在百度爬取一些图片加入)
    在这里插入图片描述
    其中black-car有123张,blue-truck有33张,green-bus有22张,red-bus有18张,red-car有114张,white-car有76张,white-truck有36张。总共422张图片。

(2)整体训练模型的代码如下:

1-car_cnn1.py

import os
import glob
import time
import numpy as np
import tensorflow as tf
from skimage import io, transform# os.environ["TF_CPP_MIN_LOG_LEVEL"] = '1'
# 这是默认的显示等级,显示所有信息
# os.environ["TF_CPP_MIN_LOG_LEVEL"] = '2'
# 只显示 warning 和 Error
os.environ["TF_CPP_MIN_LOG_LEVEL"] = '3'
# 只显示 Error# 读取图片
def read_img(path, w, h):cate = [path + x for x in os.listdir(path) if os.path.isdir(path + x)]# print(cate)imgs = []labels = []print('Start read the image ...')for index, folder in enumerate(cate):# print(index, folder)for im in glob.glob(folder + '/*.jpg'):# print('Reading The Image: %s' % im)img = io.imread(im)img = transform.resize(img, (w, h))imgs.append(img)# print(index)labels.append(index)                    # 每个子文件夹是一个label# print(len(labels))print('Finished ...')print(len(imgs))print(len(labels))return np.asarray(imgs, np.float32), np.asarray(labels, np.float32)# 打乱顺序 为了均匀取数据
def messUpOrder(data, label):num_example = data.shape[0]arr = np.arange(num_example)np.random.shuffle(arr)data = data[arr]label = label[arr]return data, label# 将所有数据分为训练集和验证集
def segmentation(data, label, ratio=0.8):num_example = data.shape[0]s = np.int(num_example * ratio)x_train = data[:s]y_train = label[:s]x_val = data[s:]y_val = label[s:]return x_train, y_train, x_val, y_val# 构建网络
def buildCNN(w, h, c):# 占位符with tf.name_scope('input'):x = tf.placeholder(tf.float32, shape=[None, w, h, c], name='x-input')y_ = tf.placeholder(tf.int32, shape=[None, ], name='y_')# 第一个卷积层 + 池化层(100——>50)with tf.name_scope('conv2d-pool-1'):conv1 = tf.layers.conv2d(inputs=x,filters=32,kernel_size=[5, 5],padding="same",activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01))pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)# 第二个卷积层 + 池化层 (50->25)with tf.name_scope('conv2d-pool-2'):conv2 = tf.layers.conv2d(inputs=pool1,filters=64,kernel_size=[5, 5],padding="same",activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01))pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)# 第三个卷积层 + 池化层 (25->12)with tf.name_scope('conv2d-pool-3'):conv3 = tf.layers.conv2d(inputs=pool2,filters=128,kernel_size=[3, 3],padding="same",activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01))pool3 = tf.layers.max_pooling2d(inputs=conv3, pool_size=[2, 2], strides=2)# 第四个卷积层 + 池化层 (12->6)with tf.name_scope('conv2d-pool-4'):conv4 = tf.layers.conv2d(inputs=pool3,filters=128,kernel_size=[3, 3],padding="same",activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01))pool4 = tf.layers.max_pooling2d(inputs=conv4, pool_size=[2, 2], strides=2)re1 = tf.reshape(pool4, [-1, 6 * 6 * 128])# 全连接层with tf.name_scope('layer-1'):dense1 = tf.layers.dense(inputs=re1,units=1024,activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01),kernel_regularizer=tf.contrib.layers.l2_regularizer(0.003))with tf.name_scope('layer-2'):dense2 = tf.layers.dense(inputs=dense1,units=512,activation=tf.nn.relu,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01),kernel_regularizer=tf.contrib.layers.l2_regularizer(0.003))with tf.name_scope('layer-3'):dense3 = tf.layers.dense(inputs=dense2,units=32,activation=None,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01),kernel_regularizer=tf.contrib.layers.l2_regularizer(0.003))with tf.name_scope('layer-4'):logits = tf.layers.dense(inputs=dense3,units=7,activation=None,kernel_initializer=tf.truncated_normal_initializer(stddev=0.01),kernel_regularizer=tf.contrib.layers.l2_regularizer(0.003))# 定义最后一层以此(此层只是用来挤压最后一个全连接层,得到概率,这个层次并未在后面的优化器中使用,只是为了)#为了得到pb文件,或者在tensorboard可视化,以及用于导入OpenCV中使用output = tf.nn.softmax(logits, name='output')return logits, x, y_# 返回损失函数的值,准确值等参数
def accCNN(logits, y_):with tf.name_scope('cross_entropy'):loss = tf.losses.sparse_softmax_cross_entropy(labels=y_, logits=logits)with tf.name_scope('AdamOptimizer'):train_op = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)   #优化器with tf.name_scope('acc'):correct_prediction = tf.equal(tf.cast(tf.argmax(logits, 1), tf.int32), y_)acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))return loss, train_op, correct_prediction, acc# 定义一个函数,按批次取数据
def minibatches(inputs=None, targets=None, batch_size=None, shuffle=False):assert len(inputs) == len(targets), 'len(inputs) != len(targets)'if shuffle:indices = np.arange(len(inputs))   # [0, 1, 2, ..., 422]np.random.shuffle(indices)     # 打乱下标顺序#从0开始,到结束,步长为batch_sizefor start_idx in range(0, len(inputs) - batch_size + 1, batch_size):if shuffle:excerpt = indices[start_idx:start_idx + batch_size]else:excerpt = slice(start_idx, start_idx + batch_size)yield inputs[excerpt], targets[excerpt]# 训练和测试(加入模型加载及保存功能,使其为增量训练)
def runable(x_train, y_train, train_op, loss, acc, x, y_, x_val, y_val):# 训练和测试数据,可将n_epoch设置更大一些n_epoch = 10              #之前是100轮,有点多,改成增量训练模型后就可以多轮次增加训练了batch_size = 30             #20是每批读取数据的数量###创建saver对象用于模型保存和加载saver = tf.train.Saver()sess = tf.InteractiveSession()                          #手动启动session对话,还需关闭(也可使用with)sess.run(tf.global_variables_initializer())             #对全局变量初始化先###训练之前,检查是否存在已经训练的模型(要在初始化后进行判断)# 如果存在则加载进来,进行增量训练if os.path.exists("./model/checkpoint"):  # 判断此文件是否存在saver.restore(sess, "./model/")  # 将此模型加载到sess中,sess就有了经过训练的参数了,如权重和偏执,经过梯度下降训练了一段时间的print("模型加载成功")for epoch in range(n_epoch):print('epoch: ', epoch)             #打印轮次# trainingtrain_loss, train_acc, n_batch = 0, 0, 0for x_train_a, y_train_a in minibatches(x_train, y_train, batch_size, shuffle=True):        #定义每轮的训练批次# print('x_val_a: ', x_val_a.shape())# print('y_val_a: ', y_val_a.shape())_, err, ac = sess.run([train_op, loss, acc], feed_dict={x: x_train_a, y_: y_train_a}) #执行操作并返回参数train_loss += errtrain_acc += acn_batch += 1print("train loss: %f" % (train_loss / n_batch))print("train acc: %f" % (train_acc / n_batch))# validationval_loss, val_acc, n_batch = 0, 0, 0for x_val_a, y_val_a in minibatches(x_val, y_val, batch_size, shuffle=False):# print('x_val_a: ', x_val_a)# print('y_val_a: ', y_val_a)err, ac = sess.run([loss, acc], feed_dict={x: x_val_a, y_: y_val_a})val_loss += errval_acc += acn_batch += 1print("validation loss: %f" % (val_loss / n_batch))print("validation acc: %f" % (val_acc / n_batch))print('*' * 50)###训练完成后保存模型saver.save(sess, "./model/")sess.close()            #关闭会话if __name__ == '__main__':imgpath = 'dataset/'w = 100h = 100c = 3ratio = 0.8  # 选取训练集的比例data, label = read_img(path=imgpath, w=w, h=h)#读取图片print(0)data, label = messUpOrder(data=data, label=label)#打乱顺序print(1)x_train, y_train, x_val, y_val = segmentation(data=data, label=label, ratio=ratio)  #分为训练集和验证集print('x_train: ', len(x_train))print('y_train: ', len(y_train))print(2)logits, x, y_ = buildCNN(w=w, h=h, c=c)     #构建网络返回最后一层结果,此时还不执行,只是操作print(3)print(logits)loss, train_op, correct_prediction, acc = accCNN(logits=logits, y_=y_)      #评估网络,返回损失函数,训练,预测值,准确率#返回损失函数,优化器,预测值,准确率等操作,需在后面的训练函数中执行操作print(4)runable(x_train=x_train, y_train=y_train, train_op=train_op, loss=loss, acc=acc, x=x, y_=y_, x_val=x_val, y_val=y_val)

(3)生成模型

会生成四个文件,在后面导出成pb文件时会用到
在这里插入图片描述

(4)控制台结果

运行了10多次,每次差不多10轮(epoch),总共100轮,最终训练结果的准确率达到100%。在其本身的训练集下,当然其训练集本身图片数量并不多,所得的模型在此训练集表现良好,可以在其数据集下再增加一些图片,这样训练得到的模型会更好一些。
在这里插入图片描述

2.使用tensorboard查看模型架构,找出输入输出(可跳过)

实际中此使用tensorboard步骤可以忽略,因为在之前的代码中已经将节点的输出名称命名好了,直接在后面的导出成pb文件的代码中就可以使用了。

(1)生成事件文件

先执行以下代码,生成事件文件,此事件文件会导入到tensorboard,可以在浏览器中查看
2-check_meta_tensor.py

import tensorflow as tfsess = tf.Session()
tf.train.import_meta_graph("./model/.meta")
tf.summary.FileWriter("./summary", sess.graph)

生成的事件文件如下
在这里插入图片描述

(2)在cmd中执行以下语句

  • 进入到python创建的项目的scripts文件下
    C:\Users\zhaocai>G:
    G:>cd G:\AILearning\AID2002\venv\Scripts
  • 执行以下操作
    G:\AILearning\AID2002\venv\Scripts>tensorboard --logdir=“G:\AILearning\AID2002\Neural_network-master\summary”

(3)执行结果

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

(4)在浏览器中查看

  • 在浏览器输入http://localhost:6006/
    下方就是该模型的架构,上面的节点名称有的是通过with tf.name_scope(‘name’)命名的,而有的是直接在操作中命名的,如softmax中的output名称。可以在tensorboard中清晰的看到模型的结构。
    在这里插入图片描述

3.导出为pb文件

参考链接:从TensorFlow模型导出到OpenCV部署详解.

(1)注意事项

  • 此处要注意的是输出层的名称,在之前训练模型的代码中加入了softmax层,并为其命名为‘output’,就是为了在此导出pb文件而使用的。
  • 此处为了适应本地文件,在此代码中做了一些调整

(2)代码如下:

3-out_pb.py

#转换成pb文件
import tensorflow as tf
import os.path
import argparse
from tensorflow.python.framework import graph_utildef freeze_graph(output_node_names):#checkpoint = tf.train.get_checkpoint_state(model_folder)#input_checkpoint = checkpoint.model_checkpoint_pathsaver = tf.train.import_meta_graph('./model/.meta', clear_devices=True)graph = tf.get_default_graph()input_graph_def = graph.as_graph_def()with tf.Session() as sess:saver.restore(sess, './model/')for node in input_graph_def.node:if node.op == 'RefSwitch':node.op = 'Switch'for index in range(len(node.input)):if 'moving_' in node.input[index]:node.input[index] = node.input[index] + '/read'elif node.op == 'AssignSub':node.op = 'Sub'if 'use_locking' in node.attr: del node.attr['use_locking']output_graph_def = graph_util.convert_variables_to_constants(sess,input_graph_def,output_node_names)with tf.gfile.GFile('classify-car.pb', "wb") as f:f.write(output_graph_def.SerializeToString())if __name__ == '__main__':freeze_graph(['output'])

(3)执行结果如下

在这里插入图片描述

4.查看pb文件的节点名称(可跳过)

参考链接: TensorFlow查看输入节点和输出节点名称.

(1)代码如下:

4-checkPB.py

import tensorflow as tf
import os# model_dir = './'
out_path = './'
model_name = 'classify-car.pb'def create_graph():with tf.gfile.FastGFile(os.path.join(out_path + model_name), 'rb') as f:graph_def = tf.GraphDef()graph_def.ParseFromString(f.read())tf.import_graph_def(graph_def, name='')create_graph()
tensor_name_list = [tensor.name for tensor in tf.get_default_graph().as_graph_def().node]
for tensor_name in tensor_name_list:print(tensor_name)

(2)执行结果如下:

input/x-input
conv2d/kernel
conv2d/kernel/read
conv2d/bias
conv2d/bias/read
conv2d-pool-1/conv2d/Conv2D
conv2d-pool-1/conv2d/BiasAdd
conv2d-pool-1/conv2d/Relu
conv2d-pool-1/max_pooling2d/MaxPool
conv2d_1/kernel
conv2d_1/kernel/read
conv2d_1/bias
conv2d_1/bias/read
conv2d-pool-2/conv2d/Conv2D
conv2d-pool-2/conv2d/BiasAdd
conv2d-pool-2/conv2d/Relu
conv2d-pool-2/max_pooling2d/MaxPool
conv2d_2/kernel
conv2d_2/kernel/read
conv2d_2/bias
conv2d_2/bias/read
conv2d-pool-3/conv2d/Conv2D
conv2d-pool-3/conv2d/BiasAdd
conv2d-pool-3/conv2d/Relu
conv2d-pool-3/max_pooling2d/MaxPool
conv2d_3/kernel
conv2d_3/kernel/read
conv2d_3/bias
conv2d_3/bias/read
conv2d-pool-4/conv2d/Conv2D
conv2d-pool-4/conv2d/BiasAdd
conv2d-pool-4/conv2d/Relu
conv2d-pool-4/max_pooling2d/MaxPool
Reshape/shape
Reshape
dense/kernel
dense/kernel/read
dense/bias
dense/bias/read
layer-1/dense/MatMul
layer-1/dense/BiasAdd
layer-1/dense/Relu
dense_1/kernel
dense_1/kernel/read
dense_1/bias
dense_1/bias/read
layer-2/dense/MatMul
layer-2/dense/BiasAdd
layer-2/dense/Relu
dense_2/kernel
dense_2/kernel/read
dense_2/bias
dense_2/bias/read
layer-3/dense/MatMul
layer-3/dense/BiasAdd
dense_3/kernel
dense_3/kernel/read
dense_3/bias
dense_3/bias/read
layer-4/dense/MatMul
layer-4/dense/BiasAdd
output

很明显,从输入到输出的节点都已经包括在其内了

5.使用OpenCV查看其网络信息(可跳过)

此处加载网络并未用到pbtxt文件,网上也有将pb文件导出为pbtxt文件的代码,但当将pb导出为pbtxt时,无法也将pbtxt加载到网络中,具体原因暂不明确。
不过此处只加载pb文件就能够使用了

(1)代码如下:

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>			//包含dnn模块的头文件
#include <iostream>
#include <fstream>				//文件流进行txt文件读取using namespace cv;
using namespace cv::dnn;			//包含dnn的命名空间
using namespace std;int main() {//车辆分类,输入模型的地址string bin_model = "G:/AILearning/models/classify-car/classify-car.pb";//加载模型Net net = readNetFromTensorflow(bin_model);//获取各层信息vector<string> layer_names = net.getLayerNames();		//此时我们就可以获取所有层的名称了,有了这些可以将其ID取出for (int i = 0; i < layer_names.size(); i++) {int id = net.getLayerId(layer_names[i]);			//通过name获取其idauto layer = net.getLayer(id);						//通过id获取layerprintf("layer id:%d,type:%s,name:%s\n", id, layer->type.c_str(), layer->name.c_str());	//将每一层的id,类型,姓名打印出来(可以明白此网络有哪些结构信息了)}waitKey(0);return 0;
}

(2)结果如下:

在这里插入图片描述

6.使用OpenCV调用模型,输入图片测试分类结果

(1)代码如下:

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>			//包含dnn模块的头文件
#include <iostream>
#include <fstream>				//文件流进行txt文件读取using namespace cv;
using namespace cv::dnn;			//包含dnn的命名空间
using namespace std;//定义名称,用于后面的显示操作
String objNames[] = { "black-car","blue-truck","green-bus","red-bus","red-car","white-car","white-truck" };int main() {//模型的pb文件string bin_model = "G:/AILearning/models/classify-car/classify-car3.pb";load DNN modelNet net = readNetFromTensorflow(bin_model);//获取各层信息//vector<string> layer_names = net.getLayerNames();		//此时我们就可以获取所有层的名称了,有了这些可以将其ID取出//for (int i = 0; i < layer_names.size(); i++) {//	int id = net.getLayerId(layer_names[i]);			//通过name获取其id//	auto layer = net.getLayer(id);						//通过id获取layer//	printf("layer id:%d,type:%s,name:%s\n", id, layer->type.c_str(), layer->name.c_str());	//将每一层的id,类型,姓名打印出来(可以明白此网络有哪些结构信息了)//}Mat src = imread("G:/AILearning/testImage/2-21.jpg");	//G:\AILearning\testImage//2-20,2-21,2-11,2-1if (src.empty()) {cout << "could not load image.." << endl;getchar();return -1;}imshow("src", src);//构建输入(根据建立的网络模型时的输入)Mat inputBlob = blobFromImage(src, 1.0, Size(100, 100), Scalar(), true, false);	//我们要将图像resize成100*100的才是我们神经网络可以接受的宽高//参数1:输入图像,参数2:默认1.0表示0-255范围的,参数3:设置输出的大小,参数4:均值对所有数据中心化预处理,参数5:是否进行通道转换(需要),参数6:,参数7:默认深度为浮点型//上方得到的inputBlob是4维的(在变量窗口看dim),所以在imagewatch中无法查看//设置输入//现在要将其输入到创建的网络中net.setInput(inputBlob);//进行推断得到输出//让网络执行得到output,调用forward可以得到一个结果//此处不给参数,得到的是最后一层的结果,也可以输入层数得到任何一层的输出结果Mat probMat = net.forward();	//通过前面的输出层看最后一层,可以知道输出7个分类对数据进行序列化(变成1行n列的,可以在后面进行方便的知道是哪个index了)Mat prob = probMat.reshape(1, 1);		//reshape函数可以进行序列化,(输出为1通道1行的数据,参数1:1个通道,参数2:1行)将输出结果变成1行n列的,但前面probMat本身就是7*1*1//										//实际结果probMat和prob相同//										//当其他网络probMat需要序列化的时候,reshape就可以了此时找到最大的那个Point classNum;double classProb;minMaxLoc(prob, NULL, &classProb, NULL, &classNum);//此时只获取最大值及最大值位置,最小值不管他int index = classNum.x;		//此时得到的是最大值的列坐标。就是其类的索引值,就可以知道其类名了printf("\n current index=%d,possible:%2f,name=%s\n", index, classProb, objNames[index].c_str());	此时可以将名称打印到图片上去putText(src, objNames[index].c_str(), Point(50, 50), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0, 0, 255), 2, 8);imshow("result", src);waitKey(0);return 0;
}

(2)结果如下所示:

在这里插入图片描述

current index=1,possible:1.000000,name=blue-truck

结论

  • 从数据集中随机挑选了10张图片,全部分类正确。但从网上再次下载新的图片对其分类时,有一部分分类出现了错误,这说明我们的训练的模型还不够完善,尽管在422张的数据集上达到了100%的准确率,但这些数据还不够,需要下载更多的图片对其进行训练。
  • 可以使用爬虫工具批量爬取图片,使得数据集每个类下的数据达到200张以上即可,当然越多越好,但数量越多,所需的计算时间也会越多,根据具体情况,自己自行确定。
  • 本文主要提供了一种从TensorFlow模型训练到使用OpenCV调用的一种方式,这样大家可以将自己训练好的模型能够结合OpenCV真实应用出来,可以训练好模型部署到实际项目中。
  • 因为本文章参考了大量网上的公开的文章,故也将此文章公开,希望能对大家有所帮助。
http://www.jmfq.cn/news/4965247.html

相关文章:

  • 英文网站建设网站/金昌网站seo
  • dw创建网站导航栏菜单怎么做/百度快照优化排名
  • 唐山企业建网站/怎么拿到百度推广的代理
  • dede做英文网站优化/今日军事新闻最新消息中国
  • 百度为何不收录你的网站产品页/外贸网站优化
  • 网站三站合一/网站注册流程和费用
  • 县网站建设/写软文能赚钱吗
  • 做网站的策划方案/如何做好网上销售
  • 网站关于我们怎么做/百度联盟怎么加入
  • 对网站的赏析/爱站网挖掘词
  • 国内外画画做的好网站/电商培训有用吗
  • 销售产品做单页还是网站/网站seo策划方案案例分析
  • 西安seo网站关键词/裂变营销
  • 中国做木线条的网站/seo北京公司
  • app 网站开发团队人员配置/网络营销策略都有哪些
  • 宁波做网站设计/女教师遭网课入侵直播
  • 推广是不是骗局/seo优化推广技巧
  • 济南自适应网站建设/个人模板建站
  • 网站下载织梦模板/免费关键词搜索工具
  • 什么网站做蜘蛛池/免费seo工具大全
  • 在线免费logo设计生成器/seo网络优化日常工作内容
  • 2015做微网站多少钱/全网整合营销推广方案
  • com网站建设中/线上推广平台哪些好
  • 康定网站建设/网站建设推广
  • 自己做网站分销/近期国际新闻热点大事件
  • 使用wampserver做响应式网站/代理公司注册
  • 什么网站可以做数据调查/百度公司高管排名
  • 长春住房和城乡建设部官方网站/如何制作微信小程序
  • 花店网站建设构思/搭建网站教程
  • 佛山做pc端网站/营销型网站定制