PyTorch 模型训练教程(一)-数据
第一章 数 据
1.1 Cifar10 转 png
下载 cifar-10-python.tar.gz
下载方式:
官网:http://www.cs.toronto.edu/~kriz/cifar.html
linux命令:
cd Data
wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
下载 cifar-10-python.tar.gz,存放到 /Data 文件夹下,并且解压,获得文件夹/Data/cifar-10-batches-py/
运行代码:
# coding:utf-8
"""将cifar10的data_batch_12345 转换成 png格式的图片每个类别单独存放在一个文件夹,文件夹名称为0-9
"""
from imageio import imwrite
import numpy as np
import os
import pickledata_dir = os.path.join("..", "..", "Data", "cifar-10-batches-py")
train_o_dir = os.path.join("..", "..", "Data", "cifar-10-png", "raw_train")
test_o_dir = os.path.join("..", "..", "Data", "cifar-10-png", "raw_test")Train = False # 不解压训练集,仅解压测试集# 解压缩,返回解压后的字典
def unpickle(file):with open(file, 'rb') as fo:dict_ = pickle.load(fo, encoding='bytes')return dict_def my_mkdir(my_dir):if not os.path.isdir(my_dir):os.makedirs(my_dir)# 生成训练集图片,
if __name__ == '__main__':if Train:for j in range(1, 6):data_path = os.path.join(data_dir, "data_batch_" + str(j)) # data_batch_12345train_data = unpickle(data_path)print(data_path + " is loading...")for i in range(0, 10000):img = np.reshape(train_data[b'data'][i], (3, 32, 32))img = img.transpose(1, 2, 0)label_num = str(train_data[b'labels'][i])o_dir = os.path.join(train_o_dir, label_num)my_mkdir(o_dir)img_name = label_num + '_' + str(i + (j - 1)*10000) + '.png'img_path = os.path.join(o_dir, img_name)imwrite(img_path, img)print(data_path + " loaded.")print("test_batch is loading...")# 生成测试集图片test_data_path = os.path.join(data_dir, "test_batch")test_data = unpickle(test_data_path)for i in range(0, 10000):img = np.reshape(test_data[b'data'][i], (3, 32, 32))img = img.transpose(1, 2, 0)label_num = str(test_data[b'labels'][i])o_dir = os.path.join(test_o_dir, label_num)my_mkdir(o_dir)img_name = label_num + '_' + str(i) + '.png'img_path = os.path.join(o_dir, img_name)imwrite(img_path, img)print("test_batch loaded.")
可在文件夹 Data/cifar-10-png/raw_test/下看到 0-9 个文件夹,对应10 个类别。
脚本中未将训练集解压出来,这里只是为了实验,因此未使用过多的数据。这里仅将测试集中的 10000 张图片解压出来,作为原始图片,将从这 10000 张图片中划分出训练集(train),验证集(valid),测试集(test)。
运行完成,在 Data/cifar-10-png/raw_test 下将有 10 个文件夹,对应 10 个类别,接着进入下一步:划分训练集、验证集和测试集。
1.2 训练集、验证集和测试集的划分
1.1把 cifar-10 的测试集转换成了 png 图片,充当实验的原始数据。1.2将把原始数据按 8:1:1 的比例划分为训练集(train set)、验证集(valid/dev set)和测试集(test set)。
运行 1_2_split_dataset.py,将会获得以下三个文件夹
/Data/train/
/Data/valid/
/Data/test/
# coding: utf-8
"""将原始数据集进行划分成训练集、验证集和测试集
"""import os
import glob
import random
import shutildataset_dir = os.path.join("..", "..", "Data", "cifar-10-png", "raw_test")
train_dir = os.path.join("..", "..", "Data", "train")
valid_dir = os.path.join("..", "..", "Data", "valid")
test_dir = os.path.join("..", "..", "Data", "test")train_per = 0.8
valid_per = 0.1
test_per = 0.1def makedir(new_dir):if not os.path.exists(new_dir):os.makedirs(new_dir)if __name__ == '__main__':for root, dirs, files in os.walk(dataset_dir):for sDir in dirs:imgs_list = glob.glob(os.path.join(root, sDir, '*.png'))random.seed(666)random.shuffle(imgs_list)imgs_num = len(imgs_list)train_point = int(imgs_num * train_per)valid_point = int(imgs_num * (train_per + valid_per))for i in range(imgs_num):if i < train_point:out_dir = os.path.join(train_dir, sDir)elif i < valid_point:out_dir = os.path.join(valid_dir, sDir)else:out_dir = os.path.join(test_dir, sDir)makedir(out_dir)out_path = os.path.join(out_dir, os.path.split(imgs_list[i])[-1])shutil.copy(imgs_list[i], out_path)print('Class:{}, train:{}, valid:{}, test:{}'.format(sDir, train_point, valid_point-train_point, imgs_num-valid_point))
数据划分完毕,下一步是制作存放有图片路径及其标签的 txt,PyTorch 依据该 txt 上的信息进行寻找图片,并读取图片数据和标签数据。
1.3 让 PyTorch 能读你的数据集
1.2中,将源数据(10000 张图片)划分为训练集、验证集和测试集,接下来就要让PyTorch 能读取这批数据。想让 PyTorch 能读取我们自己的数据,首先要了解 pytroch 读取图片的机制和流程,然后按流程编写代码。
Dataset 类
PyTorch 读取图片,主要是通过 Dataset 类,所以先简单了解一下 Dataset 类。Dataset类作为所有的 datasets 的基类存在,所有的 datasets 都需要继承它,类似于 C++中的虚基类。
class Dataset(object):
"""
表示数据集的抽象类.
所有其他数据集应该子类化它。所有的子类都应该重写'__len__', 它提供了数据集的大小, '__getitem__',
提供从0到len(self)范围内的整数索引排除了数据集的大小.
"""def __getitem__(self, index):raise NotImplementedErrordef __len__(self):raise NotImplementedErrordef __add__(self, other):return ConcatDataset([self, other])
这里重点看 getitem 函数,getitem 接收一个 index,然后返回图片数据和标签,这个index 通常指的是一个 list 的 index,这个 list 的每个元素就包含了图片数据的路径和标签信息。然而,如何制作这个 list 呢,通常的方法是将图片的路径和标签信息存储在一个 txt中,然后从该 txt 中读取。
那么读取自己数据的基本流程就是:
- 制作存储了图片的路径和标签信息的 txt
- 将这些信息转化为 list,该 list 每一个元素对应一个样本
- 通过 getitem 函数,读取数据和标签,并返回数据和标签
在训练代码里是感觉不到这些操作的,只会看到通过 DataLoader 就可以获取一个batch 的数据,其实触发去读取图片这些操作的是 DataLoader 里的__iter__(self),后面会详细讲解读取过程。1.3,主要讲 Dataset 子类。
因此,要让 PyTorch 能读取自己的数据集,只需要两步:
- 制作图片数据的索引
- 构建 Dataset 子类
制作图片数据的索引
这个比较简单,就是读取图片路径,标签,保存到 txt 文件中,这里注意格式就好,特别注意的是,txt 中的路径,是以训练时的那个 py 文件所在的目录为工作目录,所以这里需要提前算好相对路径!
# coding:utf-8
import os
'''为数据集生成对应的txt文件
'''train_txt_path = os.path.join("..", "..", "Data", "train.txt")
train_dir = os.path.join("..", "..", "Data", "train")valid_txt_path = os.path.join("..", "..", "Data", "valid.txt")
valid_dir = os.path.join("..", "..", "Data", "valid")def gen_txt(txt_path, img_dir):f = open(txt_path, 'w')for root, s_dirs, _ in os.walk(img_dir, topdown=True): # 获取 train文件下各文件夹名称for sub_dir in s_dirs:i_dir = os.path.join(root, sub_dir) # 获取各类的文件夹 绝对路径img_list = os.listdir(i_dir) # 获取类别文件夹下所有png图片的路径for i in range(len(img_list)):if not img_list[i].endswith('png'): # 若不是png文件,跳过continuelabel = img_list[i].split('_')[0]img_path = os.path.join(i_dir, img_list[i])line = img_path + ' ' + label + '\n'f.write(line)f.close()if __name__ == '__main__':gen_txt(train_txt_path, train_dir)gen_txt(valid_txt_path, valid_dir)
运行代码 1_3_generate_txt.py,即会在/Data/文件夹下面看到train.txt valid.txt
txt 中是这样的:
构建 Dataset 子类
下面是本实验构建的 Dataset 子类——MyDataset 类:
# coding: utf-8
from PIL import Image
from torch.utils.data import Dataset# Dataset类作为所有的 datasets 的基类存在,所有的 datasets 都需要继承它
class MyDataset(Dataset):def __init__(self, txt_path, transform=None, target_transform=None):fh = open(txt_path, 'r')imgs = []for line in fh:line = line.rstrip()words = line.split()imgs.append((words[0], int(words[1])))self.imgs = imgs # 最主要就是要生成这个list, 然后DataLoader中给index,通过getitem读取图片数据self.transform = transformself.target_transform = target_transformdef __getitem__(self, index):fn, label = self.imgs[index]img = Image.open(fn).convert('RGB') # 像素值 0~255,在transfrom.totensor会除以255,使像素值变成 0~1if self.transform is not None:img = self.transform(img) # 在这里做transform,转为tensor等等return img, labeldef __len__(self):return len(self.imgs)
首先看看初始化,初始化中从我们准备好的 txt 里获取图片的路径和标签,并且存储在 self.imgs,self.imgs 就是上面提到的 list,其一个元素对应一个样本的路径和标签,其实就是 txt 中的一行。
初始化中还会初始化 transform,transform 是一个 Compose 类型,里边有一个 list,list中就会定义了各种对图像进行处理的操作,可以设置减均值,除标准差,随机裁剪,旋转,翻转,仿射变换等操作。在这里我们可以知道,一张图片读取进来之后,会经过数据处理(数据增强),最终变成输入模型的数据。这里就有一点需要注意,PyTorch 的数据增强是将原始图片进行了处理,并不会生成新的一份图片,而是“覆盖”原图,当采用 randomcrop 之类的随机操作时,每个 epoch 输入进来的图片几乎不会是一模一样的,这达到了样本多样性的功能。
然后看看核心的 getitem 函数:
第一行:self.imgs 是一个 list,也就是一开始ᨀ到的 list,self.imgs 的一个元素是一个 str,包含图片路径,图片标签,这些信息是从 txt 文件中读取
第二行:利用 Image.open 对图片进行读取,img 类型为 Image ,mode=‘RGB’
第三行与第四行: 对图片进行处理,这个 transform 里边可以实现 减均值,除标准差,随机裁剪,旋转,翻转,放射变换,等等操作,这个放在后面会详细讲解。
当 Mydataset 构建好,剩下的操作就交给 DataLoder,在 DataLoder 中,会触发Mydataset 中的 getiterm 函数读取一张图片的数据和标签,并拼接成一个 batch 返回,作为模型真正的输入。1.4将会通过一个小例子,介绍 DataLoder 是如何获取一个 batch,以及一张图片是如何被 PyTorch 读取,最终变为模型的输入的。
1.4 图⽚从硬盘到模型
1.3中介绍了如何构建自己的 Dataset 子类——MyDataset,在 MyDataset 中,主要获取图片的索引以及定义如何通过索引读取图片及其标签。但是要触发 MyDataset 去读取图片及其标签却是在数据加载器 DataLoder 中。本小节,将进行单步调试,学习图片是如何从硬盘上流到模型的输入口的,并观察图片经历了哪些处理。
对应代码:
# coding: utf-8import torch
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import numpy as np
import os
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import sys
sys.path.append("..")
from utils.utils import MyDataset, validate, show_confMat
from tensorboardX import SummaryWriter
from datetime import datetimetrain_txt_path = os.path.join("..", "..", "Data", "train.txt")
valid_txt_path = os.path.join("..", "..", "Data", "valid.txt")classes_name = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']train_bs = 16
valid_bs = 16
lr_init = 0.001
max_epoch = 1# log
result_dir = os.path.join("..", "..", "Result")now_time = datetime.now()
time_str = datetime.strftime(now_time, '%m-%d_%H-%M-%S')log_dir = os.path.join(result_dir, time_str)
if not os.path.exists(log_dir):os.makedirs(log_dir)writer = SummaryWriter(log_dir=log_dir)# ------------------------------------ step 1/5 : 加载数据------------------------------------# 数据预处理设置
normMean = [0.4948052, 0.48568845, 0.44682974]
normStd = [0.24580306, 0.24236229, 0.2603115]
normTransform = transforms.Normalize(normMean, normStd)
trainTransform = transforms.Compose([transforms.Resize(32),transforms.RandomCrop(32, padding=4),transforms.ToTensor(),normTransform
])validTransform = transforms.Compose([transforms.ToTensor(),normTransform
])# 构建MyDataset实例
train_data = MyDataset(txt_path=train_txt_path, transform=trainTransform)
valid_data = MyDataset(txt_path=valid_txt_path, transform=validTransform)# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=train_bs, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=valid_bs)# ------------------------------------ step 2/5 : 定义网络------------------------------------class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(3, 6, 5)self.pool1 = nn.MaxPool2d(2, 2)self.conv2 = nn.Conv2d(6, 16, 5)self.pool2 = nn.MaxPool2d(2, 2)self.fc1 = nn.Linear(16 * 5 * 5, 120)self.fc2 = nn.Linear(120, 84)self.fc3 = nn.Linear(84, 10)def forward(self, x):x = self.pool1(F.relu(self.conv1(x)))x = self.pool2(F.relu(self.conv2(x)))x = x.view(-1, 16 * 5 * 5)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return x# 定义权值初始化def initialize_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d):torch.nn.init.xavier_normal_(m.weight.data)if m.bias is not None:m.bias.data.zero_()elif isinstance(m, nn.BatchNorm2d):m.weight.data.fill_(1)m.bias.data.zero_()elif isinstance(m, nn.Linear):torch.nn.init.normal_(m.weight.data, 0, 0.01)m.bias.data.zero_()net = Net() # 创建一个网络
net.initialize_weights() # 初始化权值# ------------------------------------ step 3/5 : 定义损失函数和优化器 ------------------------------------criterion = nn.CrossEntropyLoss() # 选择损失函数
optimizer = optim.SGD(net.parameters(), lr=lr_init, momentum=0.9, dampening=0.1) # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1) # 设置学习率下降策略# ------------------------------------ step 4/5 : 训练 --------------------------------------------------for epoch in range(max_epoch):loss_sigma = 0.0 # 记录一个epoch的loss之和correct = 0.0total = 0.0scheduler.step() # 更新学习率for i, data in enumerate(train_loader):# if i == 30 : break# 获取图片和标签inputs, labels = datainputs, labels = Variable(inputs), Variable(labels)# forward, backward, update weightsoptimizer.zero_grad()outputs = net(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()# 统计预测信息_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).squeeze().sum().numpy()loss_sigma += loss.item()# 每10个iteration 打印一次训练信息,loss为10个iteration的平均if i % 10 == 9:loss_avg = loss_sigma / 10loss_sigma = 0.0print("Training: Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(epoch + 1, max_epoch, i + 1, len(train_loader), loss_avg, correct / total))# 记录训练losswriter.add_scalars('Loss_group', {'train_loss': loss_avg}, epoch)# 记录learning ratewriter.add_scalar('learning rate', scheduler.get_lr()[0], epoch)# 记录Accuracywriter.add_scalars('Accuracy_group', {'train_acc': correct / total}, epoch)# 每个epoch,记录梯度,权值for name, layer in net.named_parameters():writer.add_histogram(name + '_grad', layer.grad.cpu().data.numpy(), epoch)writer.add_histogram(name + '_data', layer.cpu().data.numpy(), epoch)# ------------------------------------ 观察模型在验证集上的表现 ------------------------------------if epoch % 2 == 0:loss_sigma = 0.0cls_num = len(classes_name)conf_mat = np.zeros([cls_num, cls_num]) # 混淆矩阵net.eval()for i, data in enumerate(valid_loader):# 获取图片和标签images, labels = dataimages, labels = Variable(images), Variable(labels)# forwardoutputs = net(images)outputs.detach_()# 计算lossloss = criterion(outputs, labels)loss_sigma += loss.item()# 统计_, predicted = torch.max(outputs.data, 1)# labels = labels.data # Variable --> tensor# 统计混淆矩阵for j in range(len(labels)):cate_i = labels[j].numpy()pre_i = predicted[j].numpy()conf_mat[cate_i, pre_i] += 1.0print('{} set Accuracy:{:.2%}'.format('Valid', conf_mat.trace() / conf_mat.sum()))# 记录Loss, accuracywriter.add_scalars('Loss_group', {'valid_loss': loss_sigma / len(valid_loader)}, epoch)writer.add_scalars('Accuracy_group', {'valid_acc': conf_mat.trace() / conf_mat.sum()}, epoch)
print('Finished Training')# ------------------------------------ step5: 保存模型 并且绘制混淆矩阵图 ------------------------------------
net_save_path = os.path.join(log_dir, 'net_params.pkl')
torch.save(net.state_dict(), net_save_path)conf_mat_train, train_acc = validate(net, train_loader, 'train', classes_name)
conf_mat_valid, valid_acc = validate(net, valid_loader, 'valid', classes_name)show_confMat(conf_mat_train, classes_name, 'train', log_dir)
show_confMat(conf_mat_valid, classes_name, 'valid', log_dir)
大体流程:
- main.py: train_data = MyDataset(txt_path=train_txt_path, …) —>
- main.py: train_loader = DataLoader(dataset=train_data, …) —>
- main.py: for i, data in enumerate(train_loader, 0) —>
- dataloder.py: class DataLoader(): def iter(self): return _DataLoaderIter(self) —>
- dataloder.py: class _DataLoderIter(): def next(self): batch = self.collate_fn([self.dataset[i]
for i in indices]) —> - tool.py: class MyDataset(): def getitem(): img = Image.open(fn).convert(‘RGB’) —>
- tool.py: class MyDataset(): img = self.transform(img) —>
- main.py: inputs, labels = data inputs, labels = Variable(inputs), Variable(labels) outputs =
net(inputs)
一句话概括就是,从 MyDataset 来,到 MyDataset 去。
一开始通过 MyDataset 创建一个实例,在该实例中有路径,有读取图片的方法(函 数)。然后需要 pytroch 的一系列规范化流程,在第 6 步中,才会调用 MyDataset 中的__getitem__()函数,最终通过 Image.open()读取图片数据。然后对原始图片数据进行一系列预处理(transform 中设置),最后回到 main.py,对数据进行转换成 Variable 类型,最终成为模型的输入。流程详细描述:
从 MyDataset 类中初始化 txt,txt 中有图片路径和标签
初始化 DataLoder 时,将 train_data 传入,从而使 DataLoder 拥有图片的路径
在一个 iteration 进行时,才读取一个 batch 的图片数据 enumerate()函数会返回可迭代数
据的一个“元素”,在这里 data 是一个 batch 的图片数据和标签,data 是一个 listclass DataLoader()
中再调用class _DataLoderIter()
在 _DataLoderiter()类中会跳到
__next__(self)
函数,在该函数中会通过indices = next(self.sample_iter)
获取一个 batch 的 indices再通过batch = self.collate_fn([self.dataset[i] for i in indices])
获取一个 batch 的数据
在batch = self.collate_fn([self.dataset[i] for i in indices])
中会调用self.collate_fn
函数self.collate_fn
中会调用 MyDataset 类中的__getitem__()
函数,在__getitem__()
中通过Image.open(fn).convert('RGB')
读取图片通过 Image.open(fn).convert(‘RGB’)读取图片之后,会对图片进行预处理,例如减均值,除以标准差,随机裁剪等等一系列ᨀ前设置好的操作。具体 transform 的用法将用单独一小节介绍,最后返回 img,label,再通过 self.collate_fn 来拼接成一个 batch。一个 batch 是一个 list,有两个元素,第一个元素是图片数据,是一个4D 的 Tensor,shape 为(64,3,32,32),第二个元素是标签 shape 为(64)。
将图片数据转换成 Variable 类型,然后称为模型真正的输入
inputs, labels = Variable(inputs), Variable(labels)
outputs = net(inputs)
通过了解图片从硬盘到模型的过程,我们可以更好的对数据做处理(减均值,除以标准差,裁剪,翻转,放射变换等等),也可以灵活的为模型准备数据,最后总结两个需要注意的地方。图片是通过 Image.open()函数读取进来的,当涉及如下问题:
图片的通道顺序(RGB or BGR ?)
图片是 whc or cwh ?
像素值范围[0-1] or [0-255] ?
就要查看 MyDataset()类中__getitem__()
下读取图片用的是什么方法从 MyDataset()类中
__getitem__()
函数中发现,PyTorch 做数据增强的方法是在原
始图片上进行的,并覆盖原始图片,这一点需要注意。
PyTorch 模型训练教程(一)-数据相关推荐
- PyTorch模型的保存加载以及数据的可视化
文章目录 PyTorch模型的保存和加载 模块和张量的序列化和反序列化 模块状态字典的保存和载入 PyTorch数据的可视化 TensorBoard的使用 总结 PyTorch模型的保存和加载 在深度 ...
- pyTorch 图像分类模型训练教程
pyTorch 图像识别教程 代码: https://github.com/dwSun/classification-tutorial.git 这里以 TinyMind <汉字书法识别>比 ...
- PyTorch主要组成模块 | 数据读入 | 数据预处理 | 模型构建 | 模型初始化 | 损失函数 | 优化器 | 训练与评估
文章目录 一.深度学习任务框架 二.数据读入 三.数据预处理模块-transforms 1.数据预处理transforms模块机制 2.二十二种transforms数据预处理方法 1.裁剪 2. 翻转 ...
- yoloV5模型训练教程并进行量化
yoloV5模型训练教程 数据标注 数据标注我们要用labelimg pip install labelimg 百度爬虫爬取图像 import os import re import sys impo ...
- TensorFlow与PyTorch模型部署性能比较
TensorFlow与PyTorch模型部署性能比较 前言 2022了,选 PyTorch 还是 TensorFlow?之前有一种说法:TensorFlow 适合业界,PyTorch 适合学界.这种说 ...
- PyTorch 深度剖析:如何保存和加载PyTorch模型?
点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 作者丨科技猛兽 编辑丨极市平台 导读 本文详解了PyTorch 模型 ...
- 手把手教你洞悉 PyTorch 模型训练过程,彻底掌握 PyTorch 项目实战!(文末重金招聘导师)...
(文末重金招募导师) 在CVPR 2020会议接收中,PyTorch 使用了405次,TensorFlow 使用了102次,PyTorch使用数是TensorFlow的近4倍. 自2019年开始,越来 ...
- pytorch模型转onnx-量化rknn(bisenet)
1.pytorch模型转化onnx 先把pytorch的.pth模型转成onnx,例如我这个是用Bisenet转的,执行export_onnx.py import argparse import os ...
- pytorch模型加载测试_使用Pytorch实现物体检测(Faster R-CNN)
在本示例中,介绍一种two-stage算法(Faster R-CNN),将目标区域检测和类别识别分为两个任务进行物体检测.本示例采用PyTorch引擎进行模型构建. 如果您已熟练使用Notebook和 ...
- Intel发布神经网络压缩库Distiller:快速利用前沿算法压缩PyTorch模型
Intel发布神经网络压缩库Distiller:快速利用前沿算法压缩PyTorch模型 原文:https://blog.csdn.net/u011808673/article/details/8079 ...
最新文章
- Google VC投资SDN初创公司Plexxi
- 设置Eclipse智能提示
- 前端学习(2457):文章发布
- cif t t操作流程图_T+操作手册
- testing framework
- 中国水平电泳系统市场趋势报告、技术动态创新及市场预测
- 苹果Mac临时文件存储助手工具:Yoink
- 用js控制网页播放器
- 3d打印机c语言程序下载,C语言下载
- modbus rtu与计算机通讯,关于modbus rtu一个主站与多个从站通信的一点总结
- ECSHOP模板修改
- 企业网站怎么制作?企业网站制作,只需记住这8个步骤
- 【问题记录】Parallels Desktop黑屏无法进入Windows系统
- JS中如何获取JSON子项的个数或叫length
- USB 对拷线材 YYDS
- 力扣刷题篇——双指针
- 全新UI简洁H5商城网站源码/带易支付接口
- Windows系统安装Mentor的Xpedition Enterprise VX.2.11工具
- 安装MySQL遇到的问题
- JavaScript入门,js基础教学
热门文章
- 高仿爱鲜蜂购物应用源码
- centos下nginx bind() to 0.0.0.0:8090 failed
- 微软Hyper-V虚拟化技术全面体验
- 在 java 中_关于final 关键字,在Java中,关于final关键字的说法正确的是()
- 【Android自定义控件】圆圈交替,仿progress效果
- [2018.03.13 T3]联盟(alliances)
- webpack5学习与实战-(十)-source_map
- background-size失效
- android plaid,Plaid 开源库学习
- 动态代理和静态代理的区别_动态代理与静态代理