[PyTorch] 译+注:一个例子,让你明白PyTorch框架
文章目录
- Introduction
- Motivation
- Table of Contents
- A Simple Regression Problem (一个简单的线性回归)
- Data Generation (生成数据)
- Gradient Descent
- Step 1: Compute the Loss
- Step 2: Compute the Gradients
- Step 3: Update the Parameters
- Step 4: Rinse and Repeat!
- Linear Regression in Numpy
- PyTorch
- Tensor
- Loading Data, Devices and CUDA
- Creating Parameters
- Autograd
- Dynamic Computation Graph
- Optimizer
- Loss
- Model
- Nested Models
- Sequential Models
- Training Step
- Dataset
- DataLoader
- Random Split
- Evaluation
- 使用PyTorch搭建深度学习框架的baseline
- 写在最后的话
写在前面的话:
虽然一直在使用PyTorch框架,但是对于PyTorch框架的思路还是有点不太清楚。所以就一直在等待一个契机,能好好梳理一下PyTorch框架的思路。本文便是出于这样的目的。
本文是在《Understanding PyTorch with an example: a step-by-step tutorial》的基础上进行的。在对其进行翻译的同时,删去了一些无关紧要的内容以使得内容更加连续,同时在 (·) 中加入了一些自己的见解,并以斜体 表示。
整体来说,本文确实是step-by-step,它从我们常用的numpy和简单的线性回归讲起,先讲解numpy实现,然后转向tensor实现,并逐步使用PyTorch框架替代自己写的代码,把PyTorch的核心模块讲的很明白。
在最后,本文给出使用PyTorch搭建深度学习平台的基本代码baseline。
文章有两张图来自瑾er的一篇文章:一文理解PyTorch:附代码实例。这篇文章其实也是对《Understanding PyTorch with an example: a step-by-step tutorial》的翻译,但是一些关键的地方并没有解释清楚,给我一种机翻的感觉,这也能算是我写这篇文章的另一个motivation吧。
最后,这篇文章的原文写的真的好,英文的原版放到最后了。
Introduction
PyTorch是增长最快的深度学习框架 (较早的论文中使用的深度学习框架是TensotFlow,近些年的论文中PyTorch用的越来越多) ,而且它对Python匹配得很好。
Motivation
既然已经有了很多PyTorch教程,其文档也非常完整和广泛 (查阅 PyTorch 相关资料的时候,还是推荐官网 ) , 那你为什么要继续阅读这个step-by-step的教程吗?
在本文中,将展示PyTorch如何使Python能够更轻松,更直观地构建深度学习模型——autograd,dynamic computation graph,model classes等,以及如何避免一些常见的陷阱和错误。
Table of Contents
- A Simple Regression Problem
- Gradient Descent
- Linear Regression in Numpy
- PyTorch
- Autograd
- Dynamic Computation Graph
- Optimizer
- Loss
- Model
- Dataset
- DataLoader
- Evaluation
A Simple Regression Problem (一个简单的线性回归)
许多教程用图像分类问题来展示如何使用PyTorch,这确实挺酷的,但我认为这使得你错过了主要目标:PyTorch是如何工作的?
因此,本教程通篇使用一个简单而熟悉的例子:单特征x的线性回归 (即一元一次多项式) 。
y=a+bx+ϵy = a + b x + \epsilon y=a+bx+ϵ
( ϵ\epsilonϵ 为高斯噪声 )
Data Generation (生成数据)
生成一些数据:我们生成一个100点的向量,作为特征 xxx,设定上式中的 a=1a=1a=1 , b=2b=2b=2 来生成标签,并加入高斯噪声。
之后,将生成的数据分割为训练集和验证集,打乱数组的索引 (即,将训练集和验证集随机排序 ) ,并使用前80个点作为训练集。
# Data Generation
np.random.seed(42)
x = np.random.rand(100, 1)
y = 1 + 2 * x + .1 * np.random.randn(100, 1)# Shuffles the indices
idx = np.arange(100)
np.random.shuffle(idx)# Uses first 80 random indices for train
train_idx = idx[:80]
# Uses the remaining indices for validation
Gradient Descent
关于梯度下降算法,本文限于篇幅无法完全介绍清楚梯度下降如何工作。
这里只简单介绍梯度下降的四个基本步骤。
Step 1: Compute the Loss
对于回归问题,损失由均方误差(MSE)给出,即所有标签 (y) 和预测 (a + bx) 的误差的平方的均值。
值得一提的是,如果我们使用训练集(N)中的所有点来计算损失,我们是执行批量梯度下降 (batch gradient descent)。如果我们每次都用一个点,就是随机梯度下降 (stochastic gradient descent)。在1和N之间的n是小批量梯度下降 (minibatch gradient descent)。
MSE=1N∑i=1N(yi−y^i)2MSE = \frac1N \sum^N_{i=1} (y_i - \widehat{y}_i)^2 MSE=N1i=1∑N(yi−yi)2
MSE=1N∑i=1N(yi−a−bxi)2MSE = \frac1N \sum^N_{i=1} (y_i - a - bx_i)^2 MSE=N1i=1∑N(yi−a−bxi)2
(上面两个式子中的 MSEMSEMSE 其实是损失 losslossloss )
Step 2: Compute the Gradients
梯度是偏导数,为什么偏导数呢?因为它对于 (with respect to, 论文中简写为w.r.t) 单一参数来计算的。我们有两个参数,a和b,所以我们必须计算两个偏导。
当稍微改变一些其他值时,导数会反映给定值有多少变化 (即,自变量变化时,因变量变化的程度 ) 。在我们的例子中,偏导数反映了当我们改变a, b两个参数中的每一个时,均方误差会有多大的变化。
下面等式的最右边,是在简单线性回归下实现的梯度下降。中间部分通过链式法则 (即,求导的链式法则 ) 显示了有关的所有元素,以展示最终表达式是如何得到的。
Step 3: Update the Parameters
在最后一步,我们使用梯度来更新参数。因为我们试图最小化损失,所以参数将向负梯度的方向更新。
我们还需要考虑另一个参数:学习率lr,用 η\etaη 表示,它是在使用梯度来更新参数时的乘子。
a=a−η∂MSE∂ab=b−η∂MSE∂ba = a - \eta \frac{\partial{MSE}} {\partial a} \\ b = b - \eta \frac{\partial{MSE}} {\partial b} a=a−η∂a∂MSEb=b−η∂b∂MSE
即使用计算得到的梯度和学习率来更新系数a和b。
(学习率其实是一个认为设定的超参数,一般来说不应该太大,也不应该太小。而且在实际训练中,先大后小是比较好的 ) 。关于如何选择学习率,由于篇幅限制,本文不再赘述。
借用瑾er的一张图片来可视化通过梯度来优化参数的过程。
Step 4: Rinse and Repeat!
现在,我们使用更新的参数重新回到步骤1并重新启动流程。
当训练集中所有的点都用来计算损失后,一个epoch完成 (即,所有的训练数据用于优化模型参数,称为1个epoch) 。
对于批量梯度下降 ( batch gradient descent) ,这个概念是多余的,因为它每次都对所有的训练样本计算损失,即1个epoch就等价于一次参数更新。对于随机梯度下降 (stochastic gradient descent) ,一个epoch等价于N次参数更新。对于小批量梯度下降 (minibatch gradient descent) ,一个epoch等价于N/n次参数更新。
重复多个epoch,即训练一个模型。
Linear Regression in Numpy
接下来在Numpy上通过梯度下降来实现线性回归模型。
之所以先使用Numpy而不是PyTorch,有两个目的:
- 展示整个流程的结构,以便于理解
- 展示主要的痛点,以充分理解使用PyTorch的方便之处
要训练一个模型,有两个初始化步骤:
- 参数/权重的随机初始化 (这里的a和b)
- 超参数的初始化 (这里的学习率和epoch的数量)
确保始终初始化随机数种子,以确保结果的可复现。一般来说,随机的种子是42,是所有随机种子中最不随机的:-)。
对于每个epoch,有4个训练步骤:
- 计算模型的预测,即正向传递 (forward pass)
- 计算损失。使用预测和标签,以及当前任务下合适的损失函数
- 计算每个参数的梯度
- 更新参数
注意,如果不使用 batch gradient descent (示例中为 batch gradient descent ),则必须有一个内部循环来为每个点 (stochastic gradient descent) 或n个点 (minibatch gradient descent) 来执行四个训练步骤 (即,每次训练为1个epoch,要把所有的训练数据过一遍 )。稍后会有一个minibatch的示例。
# 随机初始化a和b
np.random.seed(42)
a = np.random.randn(1)
b = np.random.randn(1)print(a, b)# 设定学习率
lr = 1e-1
# 设定epochs
n_epochs = 1000for epoch in range(n_epochs):# 计算模型输出yhat = a + b * x_train# 计算errorerror = (y_train - yhat)# 计算均方损失 MSEloss = (error ** 2).mean()# 对a,b分别计算梯度a_grad = -2 * error.mean()b_grad = -2 * (x_train * error).mean()# 根据学习率和梯度更新参数a,ba = a - lr * a_gradb = b - lr * b_gradprint(a, b)# 安全测试: 得到的梯度下降结果是否正确?
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print(linr.intercept_, linr.coef_[0])
结果为:
# a and b after initialization
[0.49671415] [-0.1382643]
# a and b after our gradient descent
[1.02354094] [1.96896411]
# intercept and coef from Scikit-Learn
[1.02354075] [1.96896447]
Numpy的做法如上,下面就是PyTorch的做法。
PyTorch
首先要介绍一些基本概念,否则下面会看不懂。
在深度学习中,用到的都是张量。包括谷歌的框架也被称为TensorFlow。所以,什么是张量tensor?
Tensor
在Numpy中,3维的数组array,严格来讲,就已经是tensor了。
一个标量scalar (单个数字) 有0维,一个向量vector 有1维,一个矩阵matrix有2维,一个张量tensor有3维或更多。 但是,为了简单起见,我们通常也称向量和矩阵为张量。即所有数据被分为两类:标量 or 张量。
以及瑾er的一张图:
Loading Data, Devices and CUDA
如何将Numpy的array转换为PyTorch的tensor?使用PyTorch中的from_numpy()
的函数即可。但要注意,该函数返回值为CPU张量。to()
函数可以将一个CPU张量放到GPU上,实际上它把数据放到你指定的设备device上,包括你的GPU (referred to as cuda or cuda:0)。
“如果如果没有可用的GPU,我想让我的代码回退到CPU?” cuda.is_available()
可以检测是否有GPU可用,以增加程序的鲁棒性。
还可以使用float()
轻松地将其转换为较低精度(32位浮点数)。
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dotdevice = 'cuda' if torch.cuda.is_available() else 'cpu'# numpy's array ==> pytorch's tensor,并将其放到相应device
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)# 通过type()查看区别
# since it also tells us WHERE the tensor is (device)
print(type(x_train), type(x_train_tensor), x_train_tensor.type())
比较两个变量的类型,x_train
是numpy.ndarray
,x_train_tensor
是torch.Tensor
。
使用PyTorch的type()
,会显示变量的位置 (即,CPU or CUDA)。
反过来,使用numpy()
将tensor转换回Numpy的array。但是注意,只能将CPU上的tensor转换为array,而GPU上的不行,要先使用cpu()
将tensor放到CPU。具体的报错信息如下:
TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
(而实际中,将用于计算的tensor转换为numpy的array用的是:tensor.cpu().detach().numpy()
,因为tensor一般也在计算图中 )。
Creating Parameters
如何区分用作data的tensor、用作可训练的parameter/weight的tensor?
后一个张量需要计算它的梯度,以更新它们的值 (即,parameters’ values)。这就是requires_grad=True
参数的作用。它告诉PyTorch我们想让它为我们计算梯度。
# FIRST
# 随机初始化a和b,由于想要计算关于两个参数的梯度, 我们需要 REQUIRES_GRAD = TRUE
a = torch.randn(1, requires_grad=True, dtype=torch.float)
b = torch.randn(1, requires_grad=True, dtype=torch.float)
print(a, b)# SECOND
# 如果想要在GPU上运行程序,我们需要将其发送到对应device, right?
a = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
b = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
print(a, b)
# Sorry, but NO! The to(device) "shadows" the gradient...# THIRD
# 我们可以创建常规张量,并将其发送到对应device (as we did with our data)
a = torch.randn(1, dtype=torch.float).to(device)
b = torch.randn(1, dtype=torch.float).to(device)
# 然后对其设置 requiring gradients...
a.requires_grad_()
b.requires_grad_()
print(a, b)
第一段代码块为我们的parameters、gradients and all 创建了很好的张量。但它们是CPU张量。
# FIRST
tensor([-0.5531], requires_grad=True)
tensor([-0.7314], requires_grad=True)
第二段代码中,尝试将它们发送到GPU的简单方法。我们成功地将它们发送到对应设备上,但是不知怎么地“丢失”了梯度……
(注意,这里是对parameter来说的。用于计算的tensor (即data) 不用设定requires_grad=True,因为它们不在计算图中,对于这种data,直接to(device)
就行了 )
# SECOND
tensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)
第三段代码中,我们先将张量发送到设备,然后使用requires_grad_()
方法将其requires_grad
设置为True
。
# THIRD
tensor([-0.8915], device='cuda:0', requires_grad=True)
tensor([0.3616], device='cuda:0', requires_grad=True)
在PyTorch中,每个以下划线_结尾的方法都是 in-place 的改变,这意味着它们将修改底层变量。
尽管最后一种方法也能用,但最好在张量创建时将device分配给它们。
(即,提前计算出device,然后创建parameter时候就直接to(device)
)
# 推荐!!!在创建parameter的时候就将其放到相应的设备上
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
# tensor([0.6226], device='cuda:0', requires_grad=True)
# tensor([1.4505], device='cuda:0', requires_grad=True)
知道了如何创建需要梯度的张量 (即parameter) ,下面就是PyTorch如何处理它们。
Autograd
Autograd是PyTorch的自动微分包。多亏了它,我们不需要担心偏导,链条规则之类的东西,直接使用backward()
即可计算梯度 (即,反向传播过程 )。
一开始我们计算梯度的原因,是因为要计算损失对于parameters的偏导。因此,我们对相应的Python变量调用backward()
方法,比如loss.backward()
。
那么梯度的实际值呢?我们可以通过观察张量的grad
属性来查看。
如果查看该方法的文档,它清楚地提到梯度是累积的。因此,每次我们使用梯度来更新参数时,我们都需要在之后将梯度归零。这就是zero_()
的用处。
因此,让我们抛弃手工计算梯度的方法,使用backward()
和zero_()
方法。
就这些吗? 嗯,差不多…但是,总是有一个陷阱,这一次它与参数的更新有关…
lr = 1e-1
n_epochs = 1000torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)for epoch in range(n_epochs):yhat = a + b * x_train_tensorerror = y_train_tensor - yhatloss = (error ** 2).mean()# 不再手工计算梯度# a_grad = -2 * error.mean()# b_grad = -2 * (x_tensor * error).mean()# 只需要告诉PyTorch对特定的loss进行backwardloss.backward()# 来检验一下梯度...print(a.grad)print(b.grad)# 怎么更新参数呢? 还没那么快...# 第一次尝试# AttributeError: 'NoneType' object has no attribute 'zero_'# a = a - lr * a.grad# b = b - lr * b.grad# print(a)# 第二次尝试# RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.# a -= lr * a.grad# b -= lr * b.grad # 第三次尝试# 我们需要使用 NO_GRAD 来保持更新操作免于计算梯度# 为什么? 这归结为PyTorch使用的动态图...with torch.no_grad():a -= lr * a.gradb -= lr * b.grad# PyTorch is "clingy" to its computed gradients, we need to tell it to let it go...a.grad.zero_()b.grad.zero_()print(a, b)
在第一次尝试中,如果我们使用和Numpy代码相同的更新结构,我们会得到下面的奇怪的报错…但是我们可以通过查看tensor本身来了解发生了什么——在将更新结果重新分配给我们的参数的同时,我们再次“失去”了梯度。因此,grad属性为None,它会引发错误…
# FIRST ATTEMPT
tensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)
AttributeError: 'NoneType' object has no attribute 'zero_'
然后,我们稍微更改一下,在第二次尝试中使用熟悉的 in-place Python赋值。而且,PyTorch再一次报错。
# SECOND ATTEMPT
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
为什么?!这是一个“ too much of a good thing ”的例子。罪魁祸首是PyTorch的能力,它能够从每一个涉及任何要计算的梯度张量或其依赖项的Python操作中构建一个动态计算图。
在下一节中,我们将深入讨论动态计算图的内部工作方式。
那么,我们如何告诉PyTorch “ back off ” 并让我们更新参数,而不影响它的动态计算图呢?这就是torch.no_grad()
的用处。no_grad()
的好处,它允许我们对tensor执行常规的Python操作,而不影响PyTorch的计算图。
最后,我们成功地运行了模型并获得了结果参数。当然,它们与我们在纯numpy实现中得到的那些差不多。
# THIRD ATTEMPT
tensor([1.0235], device='cuda:0', requires_grad=True)
tensor([1.9690], device='cuda:0', requires_grad=True)
Dynamic Computation Graph
“Unfortunately, no one can be told what the dynamic computation graph is. You have to see it for yourself.” ——Morpheus
PyTorchViz软件包及其make_dot(variable)
方法允许我们轻松地可视化与给定的Python变量相关的图形。
(详见另一篇博客:[Python] 绘制Python代码的函数调用关系:graphviz+pycallgraph)
关于静态图和动态图,引用瑾er的一段话:
目前神经网络框架分为静态图框架和动态图框架,PyTorch 和 TensorFlow、Caffe 等框架最大的区别就是他们拥有不同的计算图表现形式。 TensorFlow 使用静态图,这意味着我们先定义计算图,然后不断使用它,而在 PyTorch 中,每次都会重新构建一个新的计算图。
对于使用者来说,两种形式的计算图有着非常大的区别,同时静态图和动态图都有他们各自的优点,比如动态图比较方便debug,使用者能够用任何他们喜欢的方式进行debug,同时非常直观,而静态图是通过先定义后运行的方式,之后再次运行的时候就不再需要重新构建计算图,所以速度会比动态图更快。
从最简单的开始:两个要对parameters、predictions、errors和loss计算梯度的tensor。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
调用make_dot(yhat)
,我们将得到下图中最左边的图形:
让我们仔细看看其组成部分:
- 蓝方框:这些对应于我们用作parameters的张量,即要求PyTorch计算梯度的张量;
- 灰箱:包含要计算梯度的张量或其依赖量的Python操作;
- 绿色方框:与灰色方框相同,只是它是计算梯度的起点——它们是从图形中的自底向上计算的。
如果我们为error (中间图) 和loss (右边图) 绘制图形,那么它们与第一个变量之间的惟一区别就是中间步骤的数量 (灰色框)。
为什么我们没有 data x 的方框呢?答案是:我们不对它计算梯度!因此,即使计算图的操作涉及更多的张量,也只显示了要计算梯度的张量及其依赖量。
如果我们将 parameters a 的requires_grad
设为False
,计算图会发生什么变化?
不出意外,与参数a对应的蓝色框消失了!很简单的道理:no gradients, no graph。
Optimizer
到目前为止,我们一直在根据计算出的梯度手动更新参数。两个参数可能还好,但是如果我们有很多参数呢?我们使用PyTorch的一个优化器,比如SGD或Adam。
优化器会包含我们要更新的参数、要使用的学习率 (可能还有其他超参数!) 并通过其step()
方法执行参数的更新。
此外,我们也不需要一个一个地将梯度归零了。我们只需调用优化器的zero_grad()
方法就可以!
在下面的代码中,我们使用随机梯度下降 (SGD) 优化器来更新参数a和b。
不要被优化器的名字所欺骗:如果我们一次使用所有的训练数据进行更新——就像我们在代码中实际做的那样——优化器执行的是批量梯度下降,而不是它的名字。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)lr = 1e-1
n_epochs = 1000# 定义SGD优化器,以更新参数
optimizer = optim.SGD([a, b], lr=lr)for epoch in range(n_epochs):yhat = a + b * x_train_tensorerror = y_train_tensor - yhatloss = (error ** 2).mean()loss.backward() # 不再手动更新# with torch.no_grad():# a -= lr * a.grad# b -= lr * b.gradoptimizer.step()# 不再对每个参数梯度置零# a.grad.zero_()# b.grad.zero_()optimizer.zero_grad()print(a, b)
前后对比一下两个参数,以确保一切正常:
# BEFORE: a, b
tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
# AFTER: a, b
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)
We’ve optimized the optimization process
[PyTorch] 译+注:一个例子,让你明白PyTorch框架相关推荐
- 一个例子让你明白什么是CART回归树
关于CART的原理我相信各位都有看过,是不是有些晕呢?没关系,这里我给大家讲个例子,你就理解原来CART回归树生成这么简单啊... 首先建立一个数据集,为了方便,就取少量数据,如下表,数据的创建仅作参 ...
- [paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng)
[paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng) 本文基于吴恩达老师 (Andrew Ng) 在 Stanford Deep Learnin ...
- 纠结pytorch, tensorflow, keras 一个月之后,我自己创造了一个神经网络框架
深度学习神经网络正步入成熟,而深度学习框架目前众多,大都可以在图像识别.手写识别.视频识别.语音识别.目标识别和自然语言处理等诸多领域大显身手. 深度学习框架平台占据人工智能产业生态的核心地位,具有统 ...
- python中self_一个例子带你入门Python装饰器
============ 欢迎关注我的公众号:早起python ============ 前言 在还未正式发布的python3.9中,有一个新功能值得关注,那就是任意表达式可以作为装饰器,如果你还不知 ...
- [机器学习]一个例子完美解释朴素贝叶斯分类器
何为"朴素":属性条件独立性假设 如果已知条件不止一个属性,二是多个呢,这个时候贝叶斯公式可以写作 上述公式假设特征属性 a1,a2⋯ 相互独立,这也是"朴素" ...
- 一个例子带你搞懂python作用域中的global、nonlocal和local
在编程中,只要接触过函数的,我相信都理解什么是全局变量和局部变量,概念比较简单,这里就不做解释了.在python中,用global语句就能将变量定义为全局变量,但是最近又发现有个nonlocal,一时 ...
- pyTorch自然语言处理简单例子
正文共5225个字,预计阅读时间12分钟. 最近在学pyTorch的实际应用例子.这次说个简单的例子:给定一句话,判断是什么语言.这个例子是比如给定一句话: Give it to me 判断是 EN ...
- linux2.6.28内核对bio完成通知的改进--集中走向分离的另一个例子
本文介绍一个例子,linux软中断是谁触发谁执行,这有点各司其职的意思,可是到了触发软中断的时候往往已经丢失触发这个"触发软中断"事件的源头,因此这种各司其职不是那么完善,于是2. ...
- 使用Zabbix的SNMP trap监控类型监控设备的一个例子
转载来源 :使用Zabbix的SNMP trap监控类型监控设备的一个例子 :https://www.jianshu.com/p/aa795afdf655 介绍 本文以监控绿盟设备为例. 1.登录被监 ...
最新文章
- linux下载文件的常用命令wget
- js 截取 前后 空格 获取字符串长度
- c语文编程提取郑码的单字码表
- Linux的top命令
- centos7安装oracle12c 一
- ecshop category.php?id=4,categoryall.php
- JavaScript声明变量详解
- L2-028 秀恩爱分得快
- ubuntu 如何编译 java_在ubuntu中编译运行java程序
- html media设置自适应屏幕用法,css使用@media响应式适配各种屏幕的方法示例
- 神舟微型计算机登录密码忘记,win10开机密码忘记按f2(win10忘记密码强制重置)
- springboot版本导致Mabatis-Plus报错
- python语料库_NLPPython笔记——语料库
- Unity Mesh网格编程(三) Shader实现水面或旗帜飘扬效果
- 使用FTP进行主机与Linux的文件传输
- QT+OpenCv4编译过程,解决mingw32-make -j报错。
- C++——直角三角形面积
- 雷电网络RESTful API手册中文版
- Write Zeroes
- Qt 之文件选择对话框 QFileDialog
热门文章
- 媲美Siri语音 英朗自然语音识别系统体验
- 第T题 详解放苹果(递归) =========== 把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
- shiro 不过滤指定的带参数url_原创干货 | 过滤器设计缺陷导致权限绕过
- php 网关接口,[PHP] 通用网关接口CGI 的运行原理
- 【数据结构和算法笔记】图的相关概念(有向图,无向图......)
- oracle列转行wm_concat,Oracle列转行函数wm_concat版本不兼容解决方案
- html中加入数据库,HTML中如何连接数据库?
- 开发商微信选房后不退认筹金_新楼盘开盘的“认筹”和“认购”,劝您看懂后再去认!...
- linux系统编程shell,Linux系统中的 Shell 编程
- LC415字符串相加