[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)
contents
- 前馈神经网络(part 2)
- 写在开头
- 自动梯度计算
- torch中自动梯度的封装
- 简介
- 过程内容
- 对比
- 模型简化
- 直接创建
- 利用预定义算子重新实现前馈神经网络
- 使用pytorch预定义算子重新实现二分类
- 增加一个3个神经元的隐藏层,再次实现二分类,进行对比
- 完善Runner类
- 模型训练、性能评价
- 思考
- 优化问题
- 参数初始化
- 梯度消失问题
- 死亡ReLU问题
- 额外科普:早停
- 附:Git使用(以GitHub为例)
- Git安装
- 配置个人信息
- 创建版本库
- 文件操作
- 查看提交历史
- 版本回退
- 连接Github
- 生成ssh key
- 添加远程仓库及操作
- 指令太多啦
- 写在最后
前馈神经网络(part 2)
写在开头
上一部分我们已经了解了神经元和一些基础的元素,以及如何使用他们来进行简单的二分类任务。本章我们将进行自动梯度计算以及优化问题的研究学习。
自动梯度计算
torch中自动梯度的封装
简介
在hw3中,我们通过自己造轮子实现了这一部分的基础结构,其实事实远不止我们造轮子的那般简单。自动梯度计算在torch中拥有更好的封装格式和自动梯度结构:自动微分引擎 torch.autograd。
过程内容
该核心功能主要在Tensor类中进行构建。当requires_grad
被设置为True时,将会对当前的Tensor开始追踪梯度。该过程也是基于计算图的。当在进行前向传播时,torch将会对其构建计算图,计算图记录下所有的tensor数据、执行过的算子等(这些能够自动微分的都基于torch.autograd.Function
,前向和反向的计算核心都在这个函数中,相当于将hw3中抽象的类再进一步抽象出通用的前向和反向部分。官方传送门)
要实现自动梯度计算,离不开这个类。下面给出官方的使用样例:
# 这边随便定义一个继承自Function的类,以线性为例,本部分仅包含前向和反向的计算函数
class SomeFunc(Function):
@staticmethods@staticmethoddef forward(ctx, input, weight, bias=None):ctx.save_for_backward(input, weight, bias)output = input.mm(weight.t())if bias is not None:output += bias.unsqueeze(0).expand_as(output)return output@staticmethoddef backward(ctx, grad_output):input, weight, bias = ctx.saved_tensorsgrad_input = grad_weight = grad_bias = Noneif ctx.needs_input_grad[0]:grad_input = grad_output.mm(weight)if ctx.needs_input_grad[1]:grad_weight = grad_output.t().mm(input)if bias is not None and ctx.needs_input_grad[2]:grad_bias = grad_output.sum(0)return grad_input, grad_weight, grad_bias
由此我们能将一些复杂且量大的计算封装,也能自定义各种算子和层,以及一些torch不可导的操作。同时,由于一些计算方法需要在前向传播时就保存一些数据(如Linear需要保存输入来进行梯度计算),torch还提供张量钩子,用以保存数据。
由Function类开始,能够进行具体的层和算子的构建。对此torch又构建了一个Module类(但是很奇怪,Module不继承Function类,个人理解是有些不需要单独构建前向或反向,全部依赖内部使用的层和算子的梯度进行链式求导即可,比如构建一个模型,这样泛用性更好,要用时利用tensor_hook处理)。可以如此理解:Module是由一系列Function组成,Function和Variable组成计算图,因此在反向时回调Function的backward,不需要自己定义backward。同时,Module还有很多对应参数和其他函数及变量。
总体的自动微分还是基于计算图的,具体过程通过官网可知:
这边的计算图是动态的,每次backward操作都会重新操作一次计算图的创建。这就是为什么动态图比较慢。如果一些常量不需要创建计算图,只需要将requires_grad
设置成False即可。
对比
paddlepaddle中有层的概念,torch中通过查看代码显然可以发现:不论是Linear,Conv还是Sigmoid等算子,都继承了nn.Module,我们所构建的模型也是基于nn.Module,因此torch中相应的都为nn.Module,偶尔使用如上文所说的Function也需要通过构建Module并使用apply函数来进行对接使用。
反向传播通过调用backward函数来进行,torch自动计算梯度并在优化时更新参数。一般的模型结构和优化过程代码如下:
class Model(nn.Module):def __init__(self, *args, **kwargs):super(Model, self).__init__()self.layer1 = ...self.layer2 = ......def parameters(self): # 一般不需要定义,不进行显式计算时自动有parameters...def forward(self, x):...
...
...
# train
for i in range(epochs):y_pred = model(x)loss = criterion(y, y_pred)optimizer.zero_grad() #清零梯度loss.backward() # 梯度反向传播optimizer.step() # 更新参数... # 一些输出和测试
...
...
模型简化
在前面hw3的pytorch代码进行模型构建的时候,我们使用了torch.nn.Sequential进行模型的简化创建,nn.Sequential将其中的各个层(基于nn.Module)进行级联并得到最终模型。官方文档中有个有趣的小东西:torch中同时还存在着一个ModuleList,但是它只是存下来了各个层,并没有连接操作,因此它只是用来缓存一些结构,方便批量创建和遍历等使用的。
Sequential有多种创建方式,这边只介绍常用的两种:
直接创建
直接创建如hw3中,直接在类初始化中填入各个层即可模型将会按照顺序进行模型构建(问我为啥按顺序?SEQUENTIAL就是答案):
model = Sequential(foo,bar,...)
这里给个小科普:foo,bar在英语中就像中文的张三李四,甲乙丙丁,小A小B一样,没有含义,文中放在这边仅仅代表可以是任意层的实例化类XP
我们也可以通过add_module一层一层地添加:
model = Sequential()
model.add_module('foo', foo)
model.add_module('bar', bar)
...
...
利用预定义算子重新实现前馈神经网络
使用pytorch预定义算子重新实现二分类
有了前面的基础,这边我们直接给出各部分代码和结果:
###数据集构建
dataset = DatasetGenerator()
# 定义两类数据生成函数
def func_claz1(x):ox = torch.cos(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))oy = torch.sin(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))return torch.hstack([ox,oy])
def func_claz2(x):ox = 1-torch.cos(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))oy = 0.5- torch.sin(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))return torch.hstack([ox,oy])
#生成两类数据
dataset.generate(torch.linspace(0,torch.pi,1000),func_claz1,0)
dataset.generate(torch.linspace(0,torch.pi,1000),func_claz2,1)
dataset.shuffle()
#生成训练、评估、测试集
dataset_train,dataset_test = dataset.train_test_split(False,split_size=(1600,400))
dataset_eval,dataset_test = dataset_test[:200],dataset_test[200:]
# 可视化
plt.rcParams['font.family'] = 'Microsoft YaHei'
plt.scatter(dataset_train[:,0],dataset_train[:,1],label='train',c='r',alpha=0.5)
plt.scatter(dataset_eval[:,0],dataset_eval[:,1],label='eval',c='g',alpha=0.5)
plt.scatter(dataset_test[:,0],dataset_test[:,1],label='test',c='b',alpha=0.5)
plt.title('依然是弯月数据集')
plt.legend()### 模型和损失函数、优化器、Runner类构建
model = torch.nn.Sequential(torch.nn.Linear(2,5),torch.nn.Sigmoid(),torch.nn.Linear(5,1),torch.nn.Sigmoid()
)
criterion = torch.nn.MSELoss
optimizer = torch.optim.SGD
runner = Runner(model, criterion, optimizer, {'lr':1})### 训练2000代,以最后一位为标签值,每隔200代输出测试结果
runner.train(dataset_train,-1,2000,dataset_eval,200)### 模型测试
runner.test(dataset_test, -1)
输出结果如下:
增加一个3个神经元的隐藏层,再次实现二分类,进行对比
这边只需要对模型进行简单修改即可:
model = torch.nn.Sequential(torch.nn.Linear(2,5),torch.nn.Sigmoid(),torch.nn.Linear(5,3),torch.nn.Sigmoid(),torch.nn.Linear(3,1),torch.nn.Sigmoid()
)
输出结果如下:
和前面少一个3神经元隐藏层的模型相比,保持训练代数不变且学习率不变,我们发现模型收敛速度反而慢了,准确率也并没有显著提升。对此分析,可能是因为网络参数变多,导致的收敛变慢和欠拟合,也可能是学习率过低导致模型训练2000代后依然效果不佳。当尝试提高学习率到8后,得到在验证集上的准确率为1:
另外,由于是随机权重初始化,因此每次运行的结果也会有所变化。
完善Runner类
本次的Runner类有较大改动,具体为:
- 在类中实例化损失函数和优化器
- 增加重置模型函数
- 修改训练、测试、评估部分的代码
代码如下:
class Runner(object):def __init__(self, model, criterion, optimizer, **kwargs):self.model = modelself.criterion = criterionself.optimizer = optimizerself.kwargs = kwargsdef train(self, dataset_train, split_x_y_pos, epochs, dataset_eval, epochs_display):if self.imodel is None:self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))icriterion = self.criterion()ioptimizer = self.optimizer(self.imodel.parameters(),self.kwargs.get('lr',0.01),self.kwargs.get('momentum',0))accum_loss = []train_x, train_y = dataset_train[:,:split_x_y_pos],dataset_train[:,split_x_y_pos:]for i in range(1, epochs + 1):out = self.imodel(train_x)loss = icriterion(out, train_y)accum_loss.append(loss.data.item())ioptimizer.zero_grad()loss.backward()ioptimizer.step()if i % epochs_display == 0:print('[{} / {}] loss = {}, acc = {}'.format(i,epochs,loss))self.eval(dataset_eval,-1)return accum_lossdef eval(self,model,dataset_eval,split_x_y_pos):if self.imodel is None:self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))eval_x, eval_y = dataset_eval[:,:split_x_y_pos],dataset_eval[:,split_x_y_pos:]icriterion = self.criterion()loss = icriterion(model(eval_x),eval_y)print('eval loss = {}'.format(loss.data.item()))return lossdef test(self, dataset_test, split_x_y_pos, additional_func = None, description = ''):if self.imodel is None:self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))test_x, test_y = dataset_test[:,:split_x_y_pos],dataset_test[:,split_x_y_pos:]icriterion = self.criterion()y_pred = self.imodel(test_x)loss = icriterion(y_pred, test_y)print('test loss = {}, '.format(loss.data.item()), end='')if additional_func is not None:print('{} = {}'.format(description, additional_func(test_x, test_y, y_pred, loss)))else:print('')def save_model(self, path):if self.imodel is None:self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))torch.save(self.imodel, path)def load_model(self, path):self.imodel = torch.load(path)def reset_model(self):self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))
模型训练、性能评价
该部分在前面的小题中就已经完成了,故此不再赘述。
思考
自定义梯度计算和自动梯度计算:
从计算性能、计算结果等多方面比较,谈谈自己的看法。
答:计算性能:由于自动梯度计算在torch中是由C++进行底层实现的,而自定义梯度计算由自己定义并使用python进行创建,因此自动梯度计算更加快。个人认为,如果全都使用python实现,自定义梯度已经将梯度过程写好,而自动梯度则还需要创建计算图,则自定义速度更快。
计算结果:如果自定义的梯度计算写对了,按理说是与自动梯度完全相同的,在hw3中自造轮子的反向传播,已经检验发现与torch自动梯度得到的值完全相同,不会存在计算结果的差异。
优化问题
参数初始化
实现一个神经网络前,需要先初始化模型参数。
如果对每一层的权重和偏置都用0初始化,那么通过第一遍前向计算,所有隐藏层神经元的激活值都相同;在反向传播时,所有权重的更新也都相同,这样会导致隐藏层神经元没有差异性,出现对称权重现象。
测试的代码我们使用hw3中对应的pytorch代码:
import torch
x = torch.tensor([0.5,0.3])
weights = torch.tensor([0.2, -0.4, 0.5, 0.6, 0.1, -0.5, -0.3, 0.8])
y = torch.tensor([0.23, -0.07])
print('inputs={}'.format(x))
print('real outputs={}'.format(y))
model = torch.nn.Sequential(torch.nn.Linear(2,2,False),torch.nn.Sigmoid(),torch.nn.Linear(2,2,False),torch.nn.Sigmoid()
)
model[0].weight.data = torch.zeros(4).reshape(2,2)
model[2].weight.data = torch.zeros(4).reshape(2,2)
optimizer = torch.optim.SGD(model.parameters(),1,momentum=0)
loss_fn = torch.nn.MSELoss()
for i in range(1):y_pred = model(x)loss = loss_fn(y,y_pred)optimizer.zero_grad()loss.backward()optimizer.step()
print('w1,w3,w2,w4:{}'.format(model[0].weight.data.detach().reshape(1,-1).squeeze()))
print('w5,w7,w6,w8:{}'.format(model[2].weight.data.detach().reshape(1,-1).squeeze()))
y_pred = model(x)
print('pred={}'.format(y_pred))
loss = loss_fn(y,y_pred)
print('MSE loss={}'.format(loss))
在进行一轮反向传播后,得到:
经过训练我们发现准确率≈50%,即模型废掉了,所以我们要使用随机权重的初始化。
梯度消失问题
在神经网络的构建过程中,随着网络层数的增加,理论上网络的拟合能力也应该是越来越好的。但是随着网络变深,参数学习更加困难,容易出现梯度消失问题。
由于Sigmoid型函数的饱和性,饱和区的导数更接近于0,误差经过每一层传递都会不断衰减。当网络层数很深时,梯度就会不停衰减,甚至消失,使得整个网络很难训练,这就是所谓的梯度消失问题。
在深度神经网络中,减轻梯度消失问题的方法有很多种,一种简单有效的方式就是使用导数比较大的激活函数,如:ReLU。
我们在原来的网络上增加两层,训练一代并观察其中权重:
import torch
x = torch.tensor([2.,3.])
y = torch.tensor([1.23, 1.07])
print('inputs={}'.format(x))
print('real outputs={}'.format(y))
model = torch.nn.Sequential(torch.nn.Linear(2,1,False),torch.nn.Sigmoid(),torch.nn.Linear(1,1,False),torch.nn.Sigmoid(),torch.nn.Linear(1,2,False),torch.nn.Sigmoid()
)
optimizer = torch.optim.SGD(model.parameters(),1,momentum=0)
loss_fn = torch.nn.MSELoss()
for i in range(1):y_pred = model(x)loss = loss_fn(y,y_pred)optimizer.zero_grad()loss.backward()optimizer.step()for i in range(0,len(model),2):print(model[i].weight.grad)
训练一代,我们发现梯度在反向传播中越来越小:
我们将sigmoid替换成relu再次实验,可见:
使用relu确实能够实现对梯度消失的抑制。
死亡ReLU问题
ReLU激活函数可以一定程度上改善梯度消失问题,但是在某些情况下容易出现死亡ReLU问题,使得网络难以训练。
这是由于当x<0x<0时,ReLU函数的输出恒为0。在训练过程中,如果参数在一次不恰当的更新后,某个ReLU神经元在所有训练数据上都不能被激活(即输出为0),那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远都不能被激活。
一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU的变种。
在前面的代码基础上训练两代,即可发现所有权重因为ReLU的存在全部变成了0。我们尝试LeakyReLU来补偿这个梯度:
相比普通的ReLU在第二代全部清零的效果好太多了。
额外科普:早停
模型训练代数越多,其拟合效果越好,但是会存在过于好的情况,即过拟合问题,过拟合后模型泛化能力显著下降。
对此,我们设计早停的方法:当在模型的验证集上权重更新低于某个值或错误率低于某个值或到了迭代次数就停止训练。
我们定义早停类用于进行带早停的训练,这部分也可以整合进Runner类。
class EarlyStopping():def __init__(self,patience=7,verbose=False,delta=0):self.patience = patienceself.verbose = verboseself.counter = 0self.best_score = Noneself.early_stop = Falseself.val_loss_min = np.Infself.delta = deltadef __call__(self,val_loss,model,path):print("val_loss={}".format(val_loss))score = -val_lossif self.best_score is None:self.best_score = scoreself.save_checkpoint(val_loss,model,path)elif score < self.best_score+self.delta:self.counter+=1print(f'EarlyStopping counter: {self.counter} out of {self.patience}')if self.counter>=self.patience:self.early_stop = Trueelse:self.best_score = scoreself.save_checkpoint(val_loss,model,path)self.counter = 0def save_checkpoint(self,val_loss,model,path):if self.verbose:print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model ...')torch.save(model.state_dict(), path+'/'+'model_checkpoint.pth')self.val_loss_min = val_loss
附:Git使用(以GitHub为例)
Git 是一个免费和开源的分布式版本控制系统,旨在以速度和效率处理从小型到大型项目的所有内容。
Git 易于学习,占用空间小,性能快如闪电。 它优于 SCM 工具,如 Subversion、CVS、Perforce 和 ClearCase,具有廉价的本地分支、方便的暂存区域和多个工作流等功能。
通过学习Git的使用,我们能够更好地进行版本控制和项目协作。
Git安装
Git可以通过访问Git官网的下载页面,根据操作系统进行安装。在安装完成后能够打开终端使用git:
配置个人信息
第一次使用时,我们需要配置好个人信息,具体指令如下:
git config --global user.name "name"
git config --global user.email "email@xxx.com"
创建版本库
配置完成后,我们进入一个文件夹,这里以test_git文件夹为例:
输入
git init
此时可见如下初始化内容:
文件操作
此时对该文件目录进行的所有编辑操作,都将会被记录下来,以创建readme.md为例,首先创建文件,然后添加到仓库,最后提交到仓库。删除、编辑等操作是相同的。
echo # hello, git > readme.md
git add -A
git commit -m "initial commit"
结果如下:
查看提交历史
通过如下指令
git log
能够查看到所有的编辑内容:
版本回退
当修改错了的时候,我们需要进行版本回退。当前版本为,HEAD
,在前面追加^
(经过验证这个符号在新版本已经不支持了)或是使用HEAD~xxx
来进行回退量的控制:
git reset --hard HEAD^
因此我们使用
git reset --hard HEAD~1
可见版本回退成功,内容也变成了上一个提交后的内容。
我们还能够使用版本的commit id来进行回退,只需要记住前面的几位就行:
git reset --hard a85d
连接Github
Github作为全球最大的开源仓库,里面有非常多的优质开源代码,我们也同样可以通过Git来提交自己的开源代码。
生成ssh key
自己的仓库当然只能自己进行编辑,因此需要有个令牌告诉这是你自己在进行操作。Github通过ssh key来进行访问的控制。
首先我们需要生成ssh key:
ssh-keygen -t rsa -C "email@xxx.com"
中途全部回车即可,得到如下结果即为成功
然后根据此部分:
从目录下即可看到ssh key的公钥和私钥文件。私钥需要自己保管好,提交至GitHub的是公钥:打开settings>SSH and GPG keys
点击添加并粘贴刚刚公钥(在id_rsa.pub文件中)并确定
点击“Add SSH key”后即可在列表中看见:
添加远程仓库及操作
当在GitHub上创建一个新仓库后,即有远程仓库的添加教程。相对在本地编辑,远程多了push用于将本地修改最终传送至远程仓库、pull和fetch用于从远程仓库下载内容(pull获取最新版本且与本地仓库合并)。
指令太多啦
git非常好用,但是有些懒人(比如我)想要更为傻瓜式地进行仓库的管理维护。由此我再次案例地表最强编辑器VSCode(不许有人不知道VSCode!XD)。在远程或本地仓库初始化完毕后,用VSCode打开这个仓库所在文件夹,就能享受一键上床下载!具体过程就算不教都能够明白,直接放之前数模时候的一个仓库:
超级简单!修改完点击提交并填写commit内容即可享受一条龙服务~其中的菜单还支持几乎所有git功能:
写在最后
通过之前的实验基础以及对原理的认识,本次实验难度并不是很大,但是本次让我们更加直观地见识了深度学习中一些巧妙结构如计算图、自动微分等大杀器,也通过阅读文献和技术文档对于我们将长时间使用下去的框架有了一个更深的认识。同时,我们也了解了不同算子在不同数据分布上的性能表现,了解了解决如梯度消失、死亡ReLU等问题的可行手段。另外,通过了解git的使用,我们能够更好地在团队协作的项目中保持高效。
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)相关推荐
- [2022-10-13]神经网络与深度学习第3章-前馈神经网络(part3)
contents 前馈神经网络(part 3) 写在开头 鸢尾花数据集介绍 Iris数据集背景和内容 Iris数据集数据 Iris数据集使用 PCA降维呈现数据 选取前两个特征绘制 实践:基于前馈神经 ...
- 神经网络与深度学习(五)前馈神经网络(2)自动梯度计算和优化问题
注:本次使用的数据集依旧是前两章的Moon1000数据集 from nndl.dataset import make_moons [详细代码见 神经网络与深度学习(五)前馈神经网络(1)--二分类任 ...
- 神经网络与深度学习(五)前馈神经网络(3)鸢尾花分类
目录 4.5实践:基于前馈神经网络完成鸢尾花分类 深入研究鸢尾花数据集 4.5.1 小批量梯度下降法 4.5.1.1 数据分组 4.5.2 数据处理 4.5.2.2 用DataLoader进行封装 4 ...
- [翻译] 神经网络与深度学习 第三章 提升神经网络学习的效果 - Chapter 3 Improving the way neural networks learn
目录: 首页 译序 关于本书 关于习题和难题 第一章 利用神经网络识别手写数字 第二章 反向传播算法是如何工作的 > 第三章 提升神经网络学习的效果 第四章 可视化地证明神经网络可以计算任何函数 ...
- 【神经网络与深度学习】第一章 使用神经网络来识别手写数字
人类的视觉系统,是大自然的奇迹之一. 来看看下面一串手写的数字: 大多数人可以毫不费力地认出这些数字是504192.这种轻松是欺骗性的,我们觉得很轻松的一瞬,其实背后过程非常复杂. 在我们大脑的每个半 ...
- 花书+吴恩达深度学习(一)前馈神经网络(多层感知机 MLP)
目录 0. 前言 1. 每一个神经元的组成 2. 梯度下降改善线性参数 3. 非线性激活函数 4. 输出单元 4.1. 线性单元 4.2. sigmoid 单元 4.3. softmax 单元 5. ...
- [翻译] 神经网络与深度学习 第六章 深度学习 - Chapter 6 Deep learning
目录: 首页 译序 关于本书 关于习题和难题 第一章 利用神经网络识别手写数字 第二章 反向传播算法是如何工作的 第三章 提升神经网络学习的效果 第四章 可视化地证明神经网络可以计算任何函数 第五章 ...
- [2022-09-20]神经网络与深度学习第2章-simple classification
contents classification 写在开头 Logistic 回归 数据集构建 DatasetGenerator类 数据生成和可视化 模型构建 损失函数 模型优化 评价指标 完善Runn ...
- 图像处理神经网络python_深度学习使用Python进行卷积神经网络的图像分类教程
深度学习使用Python进行卷积神经网络的图像分类教程 好的,这次我将使用python编写如何使用卷积神经网络(CNN)进行图像分类.我希望你事先已经阅读并理解了卷积神经网络(CNN)的基本概念,这里 ...
最新文章
- 武汉理工大学计算机复试笔试重要吗,2017武汉理工计算机复试
- 解决在Yii2中使用PHPExcel出现Class ‘app\controllers\PHPExcel‘ not found的问题
- jpa执行mysql存储过程_基于Spring Boot,使用JPA调用Sql Server数据库的存储过程并返回记录集合...
- adb命令怎么打开_用python撸支付宝体验金,才是程序员正确的打开方式!
- jmeter JDBC 连接数据库
- python 判断是否连接wifi_python操作 linux连接wifi,查看wifi连接状态方法
- 交换机集群管理(锐捷)
- 多线程NSInvocationOperation(NSOperationQueue)的基本用法
- adb shell 命令详解
- 免费「模拟面试」福利反馈连载(20180128期)
- 从微软Lync看企业办公通讯平台的演变
- PHP 14:类的实例
- JS 打印 data数据_用D3.js 十分钟实现字符跳动效果
- win10照片查看器_win10最好的看图软件?win10照片查看软件推荐
- 计算机图片组合快捷键,Windows电脑常用的10个Win组合快捷键功能,看看你都知道吗?...
- 学习OpenCV 4(一)
- 让iPhone不能自动下载系统更新的一个办法
- 手机如何把PDF文件压缩的小一点?教你手机压缩文件方法
- 室内定位中非视距的识别和抑制算法研究综述(部分)
- 一元线性模型用R语言进行拟合