文章目录

  • 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是小批量梯度下降 (mini­batch gradient descent)

MSE=1N∑i=1N(yi−y^i)2MSE = \frac1N \sum^N_{i=1} (y_i - \widehat{y}_i)^2 MSE=N1​i=1∑N​(yi​−y​i​)2

MSE=1N∑i=1N(yi−a−bxi)2MSE = \frac1N \sum^N_{i=1} (y_i - a - bx_i)^2 MSE=N1​i=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∂MSE​b=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次参数更新。对于小批量梯度下降 (mini­batch 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个点 (mini­batch gradient descent) 来执行四个训练步骤 (即,每次训练为1个epoch,要把所有的训练数据过一遍 )。稍后会有一个mini­batch的示例。

# 随机初始化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维,一个矩阵matrix2维,一个张量tensor3维或更多。 但是,为了简单起见,我们通常也称向量和矩阵张量。即所有数据被分为两类:标量 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_trainnumpy.ndarrayx_train_tensortorch.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 arequires_grad设为False,计算图会发生什么变化?

不出意外,与参数a对应的蓝色框消失了!很简单的道理:no gradients, no graph


Optimizer

到目前为止,我们一直在根据计算出的梯度手动更新参数。两个参数可能还好,但是如果我们有很多参数呢?我们使用PyTorch的一个优化器,比如SGDAdam

优化器会包含我们要更新的参数、要使用的学习率 (可能还有其他超参数!) 并通过其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框架相关推荐

  1. 一个例子让你明白什么是CART回归树

    关于CART的原理我相信各位都有看过,是不是有些晕呢?没关系,这里我给大家讲个例子,你就理解原来CART回归树生成这么简单啊... 首先建立一个数据集,为了方便,就取少量数据,如下表,数据的创建仅作参 ...

  2. [paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng)

    [paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng) 本文基于吴恩达老师 (Andrew Ng) 在 Stanford Deep Learnin ...

  3. 纠结pytorch, tensorflow, keras 一个月之后,我自己创造了一个神经网络框架

    深度学习神经网络正步入成熟,而深度学习框架目前众多,大都可以在图像识别.手写识别.视频识别.语音识别.目标识别和自然语言处理等诸多领域大显身手. 深度学习框架平台占据人工智能产业生态的核心地位,具有统 ...

  4. python中self_一个例子带你入门Python装饰器

    ============ 欢迎关注我的公众号:早起python ============ 前言 在还未正式发布的python3.9中,有一个新功能值得关注,那就是任意表达式可以作为装饰器,如果你还不知 ...

  5. [机器学习]一个例子完美解释朴素贝叶斯分类器

    何为"朴素":属性条件独立性假设 如果已知条件不止一个属性,二是多个呢,这个时候贝叶斯公式可以写作 上述公式假设特征属性 a1,a2⋯ 相互独立,这也是"朴素" ...

  6. 一个例子带你搞懂python作用域中的global、nonlocal和local

    在编程中,只要接触过函数的,我相信都理解什么是全局变量和局部变量,概念比较简单,这里就不做解释了.在python中,用global语句就能将变量定义为全局变量,但是最近又发现有个nonlocal,一时 ...

  7. pyTorch自然语言处理简单例子

    正文共5225个字,预计阅读时间12分钟. 最近在学pyTorch的实际应用例子.这次说个简单的例子:给定一句话,判断是什么语言.这个例子是比如给定一句话: Give it to me 判断是  EN ...

  8. linux2.6.28内核对bio完成通知的改进--集中走向分离的另一个例子

    本文介绍一个例子,linux软中断是谁触发谁执行,这有点各司其职的意思,可是到了触发软中断的时候往往已经丢失触发这个"触发软中断"事件的源头,因此这种各司其职不是那么完善,于是2. ...

  9. 使用Zabbix的SNMP trap监控类型监控设备的一个例子

    转载来源 :使用Zabbix的SNMP trap监控类型监控设备的一个例子 :https://www.jianshu.com/p/aa795afdf655 介绍 本文以监控绿盟设备为例. 1.登录被监 ...

最新文章

  1. linux下载文件的常用命令wget
  2. js 截取 前后 空格 获取字符串长度
  3. c语文编程提取郑码的单字码表
  4. Linux的top命令
  5. centos7安装oracle12c 一
  6. ecshop category.php?id=4,categoryall.php
  7. JavaScript声明变量详解
  8. L2-028 秀恩爱分得快
  9. ubuntu 如何编译 java_在ubuntu中编译运行java程序
  10. html media设置自适应屏幕用法,css使用@media响应式适配各种屏幕的方法示例
  11. 神舟微型计算机登录密码忘记,win10开机密码忘记按f2(win10忘记密码强制重置)
  12. springboot版本导致Mabatis-Plus报错
  13. python语料库_NLPPython笔记——语料库
  14. Unity Mesh网格编程(三) Shader实现水面或旗帜飘扬效果
  15. 使用FTP进行主机与Linux的文件传输
  16. QT+OpenCv4编译过程,解决mingw32-make -j报错。
  17. C++——直角三角形面积
  18. 雷电网络RESTful API手册中文版
  19. Write Zeroes
  20. Qt 之文件选择对话框 QFileDialog

热门文章

  1. 媲美Siri语音 英朗自然语音识别系统体验
  2. 第T题 详解放苹果(递归) =========== 把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
  3. shiro 不过滤指定的带参数url_原创干货 | 过滤器设计缺陷导致权限绕过
  4. php 网关接口,[PHP] 通用网关接口CGI 的运行原理
  5. 【数据结构和算法笔记】图的相关概念(有向图,无向图......)
  6. oracle列转行wm_concat,Oracle列转行函数wm_concat版本不兼容解决方案
  7. html中加入数据库,HTML中如何连接数据库?
  8. 开发商微信选房后不退认筹金_新楼盘开盘的“认筹”和“认购”,劝您看懂后再去认!...
  9. linux系统编程shell,Linux系统中的 Shell 编程
  10. LC415字符串相加