PyTorch基础

什么是PyTorch

它是一个基于Python的科学计算包,本质是:

  • 类似Numpy,它可以使用GPU
  • 可以用它定义深度学习模型,可以灵活的进行深度学习模型的训练和使用
  • 深度学习的研究平台,提供了最大的灵活度以及速度
    PyTorch代码通俗易懂,是Torch再Python上的衍生,虽然Torch很好用,但它是基于Lua语言的,因为Lua语言不是特别流行,所以研究团队把Torch移植到了Python语言上。

PyTorch与TensorFlow的主要区别:

  • TensorFlow是基于静态计算图的,静态计算图是先定义再运行,一次定义多次运行(TensorFlow2.0开始也适用动态计算图)
  • PyTorch则是基于动态计算图,动态计算图是再运行过程中被定义的,在运行的时候构建,可以多次构建多次运行

张量(Tensor)

一、创建Tensor

Tensor类似于NumPy的ndarray,唯一区别是Tensor可以再GPU上加速运行

首先导入PyTorch:

1
import torch

构一个未初始化的5x3矩阵:

1
2
x = torch.empty(5, 3)
print(x)

构建一个随机初始化的矩阵:

1
2
x = torch.rand(5, 3)
print(x)

构建一个全部为0,类型为long的矩阵

1
2
3
4
5
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
print(x.dtype) #torch.int64
#另一种方法
x = torch.zeros(5, 3).long()

从数据直接构建tensor

1
2
x = torch.tensor([5.5, 3])
print(x) # tensor([5.5000, 3.0000])

也可以通过现有的tensor来创建,此方法会默认重用输入Tensor的一些属性,例如数据类型,除非自定义数据类型。

1
2
3
4
x = x.new_ones(5, 3, dtype=torch.double)#所得到的y会保持 x 原有的属性,比如dtype。
print(x)
y = torch.randn_like(x, dtype=torch.float)
print(y)

获取Tensor的形状:

1
2
print(x.size())
print(x.shape)

注意:返回的torch.Size其实就是一个tuple, 支持所有tuple的操作。

二、操作Tensor

1、算术操作

Tensor有很多种运算,我们先介绍加法运算。

1
2
3
4
5
6
7
8
9
10
11
12
方法一:
y = torch.rand(5, 3)
print(x + y)
方法二:
torch.add(x, y)
其中还可以把输出当作变量传进去:
result = torch.empty(5, 3)
torch.add(x, y, out=result)
#与上边这个方法有区别 result = x + y
print(result)
方法三(in-place加法):
y.add_(x)

注意1、方法二中add里边可以少copy一次,矩阵数据量大的时候,速度和内存占用有区别

注意2、方法三中会改变y的值,前边两中不会改变y的值

注意3、任何in-place的运算都会以结尾, 例如x.copy(y), x.t_()

2、索引

各种类似Numpy的indexing都可以在PyTorch tensor上面使用,要注意的是:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改。

1
2
3
4
y = x[0, :]
y += 1
print(y) #tensor([1.6035, 1.8110, 0.9549])
print(x[0, :]) #tensor([1.6035, 1.8110, 0.9549])

3、改变形状

如果你想改变tensor维数形状,可以用view()

1
2
3
y = x.view(15)
z = x.view(-1, 5) # -1所指的维度可以根据其他维度的值推出来,但后边那个参数不能是除不尽的否则会报错
print(x.size(), y.size(), z.size()) # torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])

注意 view()返回的新Tensor与引用Tensor虽然可能有不同的size,但是数据时共享的,改变一个另一个也会相应改变,view()只是改变了tensor的维数,内部数据没有改变

如果想得到一个不共享数据的tensor,可以用reshape()改变形状,但是此函数并不能保证返回的是其拷贝,所以不推荐使用。推荐先用clone创造一个副本然后再使用view。参考此处

1
2
3
4
x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)

使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor

4、转成python number

常用的函数item(),如果你又一个只有一个元素的tensor,它可以将一个标量Tensor里边的value转换成一个Python number

1
2
3
x = torch.randn(1)
print(x) # tensor([2.3466])
print(x.item()) # 2.3466382026672363

线性代数

PyTorch还支持一些线性函数,具体用法参考官方文档。如下表所示:

三、Tensor和NumPy相互转换

用numpy()和from_numpy()将Tensor和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变.

还有一个常用的将NumPy中的array转换成Tensor的方法就是torch.tensor(), 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的Tensor和原来的数据不再共享内存。

1、Tensor转NumPy

使用numpy()将Tensor转换成NumPy数组:

1
2
3
4
5
6
7
8
a = torch.ones(3)
b = a.numpy()
print(a, b) # tensor([1., 1., 1.]) [1. 1. 1.]

a += 1
print(a, b) # tensor([2., 2., 2.]) [2. 2. 2.]
b += 1
print(a, b) # tensor([3., 3., 3.]) [3. 3. 3.]

2、NumPy数组转Tensor

使用numpy()将Tensor转换成NumPy数组:

1
2
3
4
5
6
7
8
9
import numpy as np
a = np.ones(3)
b = torch.from_numpy(a)
print(a, b) # [1. 1. 1.] tensor([1., 1., 1.], dtype=torch.float64)

a += 1
print(a, b) # [2. 2. 2.] tensor([2., 2., 2.], dtype=torch.float64)
b += 1
print(a, b) # [3. 3. 3.] tensor([3., 3., 3.], dtype=torch.float64)

所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。

此外上面提到还有一个常用的方法就是直接用torch.tensor()将NumPy数组转换成Tensor,需要注意的是该方法总是会进行数据拷贝,返回的Tensor和原来的数据不再共享内存。

1
2
3
c = torch.tensor(a)
a += 1
print(a, c) # [4. 4. 4.] tensor([3., 3., 3.], dtype=torch.float64)

四、Tensor在GPU

用方法to()可以将Tensor在CPU和GPU(需要硬件支持)之间相互移动。

1
2
3
4
5
6
7
8
# 以下代码只有在PyTorch GPU版本上才会执行
if torch.cuda.is_available():
device = torch.device("cuda") # GPU
y = torch.ones_like(x, device=device) # 直接创建一个在GPU上的Tensor
x = x.to(device) # 等价于 .to("cuda")
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()还可以同时更改数据类型

如果y是GPUTensor可以拿到y.data但不能直接变成numpy这样y.data.numpy()必须先变成cpu,因为numpy()的操作都是在CPU上操作的,GPU上不能,因为两者效率差距非常大

1
2
3
# y.data.numpuy()不能
y..to("cpu").data.numpy()
y.cpu().data.numpy()

变量(Variable)和自动求梯度

Variable和Tensor的区别是,Variable可以自动求导,它具有三个重要属性:data,grad,grad_fn。将一个Tensor a转化成Variable只需要Variable(a)就可以了。

一、概念

Tensor是这个包的核心类,如果将其属性.requires_grad设置为True,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。

注意在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor。

1
2
3
4
a = torch.randn(3, requires_grad=True)
y = 2 * a + 3
y.backward(torch.Tensor([1.0, 0.1, 0.01]))
print(a.grad)

如果不想要被继续追踪,可以调用.detach()将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。此外,还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True)的梯度。

Function是另外一个很重要的类。Tensor和Function互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor都有一个.grad_fn属性,该属性即创建该Tensor的Function, 就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。

二、Tensor

创建一个Tensor并设置requires_grad=True

1
2
3
x = torch.randn(3, requires_grad=True)
print(x)
print(x.grad_fn) # None

做一下运算操作

1
2
3
y = x + 1
print(y)
print(y.grad_fn) # <AddBackward0 object at 0x11c8f3128>

注意 x是用户直接创建的,所以没有grad_fn,而y是通过一个加法操作创建的,所以他有的grad_fn。

像x这种直接创建的称为叶子结点,叶子结点对应的grad_fn是None。

1
print(x.is_leaf, y.is_leaf) # True False

我们可以通过.requires_grad_()来用in-place的方式改变requires_grad属性:

1
2
3
4
5
6
7
a = torch.randn(2, 2) # 缺失情况下默认 equires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn) # <SumBackward0 object at 0x118f50cc0>

三、梯度

如果out是一个标量时,调用backward()时不需要指定求导变量:

1
a.backward() # out.backward(torch.tensor(1.))

注意 grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。通过.grad.data.zero_()

1
2
3
4
5
6
7
8
9
# 再来反向传播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

y.backward(),如果y是标量,则不需要为bcakward()传入任何参数;否则,需要穿入一个与y同形的Tensor.

1
2
3
4
5
x = torch.randn(3, requires_grad=True)
w = torch.Tensor([1., 0.1, 0.01])
y = 2 * x + 1
y.backward(w)
print(x.grad) # tensor([2.0000, 0.2000, 0.0200])

注意 x.grad是和x同形的张量。

中断梯度追踪的例子:

1
2
3
4
5
6
7
8
9
10
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2

print(x.requires_grad)
print(y1, y1.requires_grad) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True

可以看到,上面y2没有grad_fn,而y2.requires_grad=False的,而y3是有grad_fn的。如果我们将y3对x求梯度的话会是tensor(2.),因为y2的定义被torch.no_grad():包裹的,所以与y2有关的梯度是不会回传的。

1
2
y3.backward()
print(x.grad)# tensor(2.)

如果我们想要修改tensor数值,但又不希望被autograd记录(不会影响反向传播),我们可以对tensor.data进行操作。

1
2
3
4
5
6
7
8
9
10
11
x = torch.ones(1,requires_grad=True)

print(x.data) # 还是一个tensor tensor([1.])
print(x.data.requires_grad) # 但是已经是独立于计算图之外 False

y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

y.backward()
print(x) # 更改data的值也会影响tensor的值 tensor([100.], requires_grad=True)
print(x.grad) # tensor([2.])

nn.Module(模组)

在PyTorch里面编写神经网络,所有的曾结构和损失函数都来自于torch.nn,所有的模型构建都是从这个基类nn.Module继承的,一般继承光重写下面两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H, bias=False)
self.linear2 = torch.nn.Linear(H, D_out, bias=False)

def forward(self, x):
y_prev = self.linear2(self.linear1(x).clamp(min=0))
return y_prev

#另一个方法
net2 = torch.nn.Sequential(
torch.nn.Linear(D_in, H)
torch.nn.ReLU()
torch.nn.Linear(H, D_out)
)
# 创建

model = netWork(other_param)

torch.optim(优化)

在机器学习或者深度学习中,我们需要通过修改参数使损失函数最小化(活最大化),优化算法就是一种调整模型参更新的策略。

1
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

这样我们就将模型的参数作为需要更新的参数传入优化器,设定学习速率是0.01,动量是0.9的随机梯度下洋。在优化之前我们需要先将梯度归零,optimizer.zeros(),然后通过loss.backward()反向出ABB哦,自动求导得到每个参数的梯度,最后只需要optimizer.step()就可以通过梯度做一步参数更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#简单的两层神经网络
import torch

N, D_in, H, D_out = 64, 1000, 100, 10

# 随机创建一些训练数据
x = torch.randn(N, D_in, requires_grad=True)
y = torch.randn(N, D_out, requires_grad=True)


class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H, bias=False)
self.linear2 = torch.nn.Linear(H, D_out, bias=False)

def forward(self, x):
y_prev = self.linear2(self.linear1(x).clamp(min=0))
return y_prev


model = TwoLayerNet(D_in, H, D_out)

loss_fn = torch.nn.MSELoss(reduction='sum')

learning_rate = 1e-4
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

for it in range(500):
# Forward pass
y_pred = model(x) # model.forward()

# compute loss
loss = loss_fn(y_pred, y)
print(it, loss.item())

optimizer.zero_grad()
# Backward pass
loss.backward()

# update model parameters
optimizer.step()