文章目录

三阶多项式拟合正弦函数(numpy, ndarray)

Numpy是科学计算的框架,不是专门用于计算图、深度学习或梯度的。但我们可以使用numpy实现网络的正向和反向传播。例如,用三阶多项式拟合正弦函数:

# -*- coding: utf-8 -*-
import numpy as np
import math
import time

time_start = time.time()
# 随机初始化输入和输出数据
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)

# 随机初始化权重
a = np.random.randn()
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()

learning_rate = 1e-6
for t in range(2000):
# 前向传播:计算预测值 y
# y = a + b x + c x^2 + d x^3
y_pred = a + b * x + c * x ** 2 + d * x ** 3

# 计算损失
loss = np.square(y_pred - y).sum()
if t % 100 == 99:
print(f'迭代次数:{t+1},损失值:{loss}')

# 反向传播:计算损失关于a, b, c, d 的梯度
grad_y_pred = 2.0 * (y_pred - y)
grad_a = grad_y_pred.sum()
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x ** 2).sum()
grad_d = (grad_y_pred * x ** 3).sum()

# 梯度下降更新权重
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
time_end = time.time()
print('消耗时间:', time_end - time_start, 's')
print(f'结果: y = {a} + {b} x + {c} x^2 + {d} x^3')

输出:

...
...
迭代次数:2000,损失值:9.68922650914574
消耗时间: 0.6189007759094238 s
结果: y = -0.03123761638932383 + 0.8577752449653959 x + 0.0053890086233387485 x^2 + -0.09347752370202965 x^3

既然numpy也可以实现网络的正向和反向传播,那为什么还要pytorch呢?

Numpy是一个很棒的框架,但它不能利用GPU来加速计算,这使得它不适用于大计算量的深度学习。而pytorch的一种数据结构——张量(tensor)则可以在GPU或其他硬件加速器上运行。另外,pytorch的自动求导机制,能自动的帮我们把反向传播全部计算好。

下面,我们将进入本文的主题:张量(tensor)。

张量

张量(tensor)是一种特殊的数据结构,与数组和矩阵非常相似。在pytorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码。张量与NumPy的数组很相似,只是它可以在GPU或其他硬件加速器上运行。

In[2]: import torch
In[3]: import numpy as np

直接由数据得到

使用​​torch.tensor()​

In[4]: data = [[1, 2],[3, 4]]
In[5]: x_data = torch.tensor(data)
In[6]: x_data
Out[6]:
tensor([[1, 2],
[3, 4]])

由NumPy array得到

使用​​torch.from_numpy()​

np_array = np.array(data)
x_np = torch.from_numpy(np_array)
x_np
Out[9]:
tensor([[1, 2],
[3, 4]], dtype=torch.int32)

由另一个张量得到

除非显式重写,否则新的张量将保留参数张量的属性(形状、数据类型)。

In[10]: x_ones = torch.ones_like(x_data)
In[11]: x_ones
Out[11]:
tensor([[1, 1],
[1, 1]])
In[12]: x_rand = torch.rand_like(x_data, dtype=torch.float) # 类型重写
In[13]: x_rand
Out[13]:
tensor([[0.2792, 0.9185],
[0.5906, 0.8662]])
In[14]: x_rand.dtype
Out[14]: torch.float32

初始化随机或常量值张量

​shape​​是表示张量维度的元组。在下面的函数中,它决定了输出张量的维数。

In[15]: shape = (2,3,)
...: rand_tensor = torch.rand(shape)
...: ones_tensor = torch.ones(shape)
...: zeros_tensor = torch.zeros(shape)
In[16]: rand_tensor
Out[16]:
tensor([[0.3380, 0.0584, 0.5423],
[0.6003, 0.6216, 0.9982]])
In[17]: ones_tensor
Out[17]:
tensor([[1., 1., 1.],
[1., 1., 1.]])
In[18]: zeros_tensor
Out[18]:
tensor([[0., 0., 0.],
[0., 0., 0.]])

张量的属性

张量属性描述它们的形状(​​shape​​​)、数据类型(​​dtype​​​)和存储它们的设备(​​device​​)。

In[19]: tensor = torch.rand(3,4)
In[20]: tensor.shape
Out[20]: torch.Size([3, 4])
In[21]: tensor.dtype
Out[21]: torch.float32
In[22]: tensor.device
Out[22]: device(type='cpu')

张量运算

pytorch对张量有超过100种运算操作(​​传送门​​)。每个操作都可以在GPU上运算(速度通常高于CPU)。

默认情况下,创建张量都是在CPU上运算。我们需要使用​​.to​​显式地将张量移动到GPU中(在GPU可用的前提下)。但请记住,跨设备复制大量张量需要耗费许多时间和内存!

In[23]: tensor
Out[23]:
tensor([[0.0061, 0.1010, 0.5185, 0.8282],
[0.7172, 0.8436, 0.0652, 0.0033],
[0.2006, 0.7263, 0.8957, 0.8063]])
In[25]: # 如果GPU可用,将张量移动到GPU
...: if torch.cuda.is_available():
...: tensor = tensor.to('cuda')
In[26]: tensor
Out[26]:
tensor([[0.0061, 0.1010, 0.5185, 0.8282],
[0.7172, 0.8436, 0.0652, 0.0033],
[0.2006, 0.7263, 0.8957, 0.8063]], device='cuda:0')

下面列举一些简单的操作。如果你熟悉NumPy API,您会发现Tensor API很容易使用。

标准numpy式的索引和切片

In[33]: tensor = torch.rand(4, 4)
In[34]: tensor
Out[34]:
tensor([[0.1227, 0.2580, 0.4331, 0.9736],
[0.3351, 0.5426, 0.5793, 0.1816],
[0.7569, 0.6747, 0.0966, 0.9883],
[0.1359, 0.1780, 0.0796, 0.9142]])
In[35]: tensor[0] # 第一行
Out[35]: tensor([0.1227, 0.2580, 0.4331, 0.9736])
In[36]: tensor[:, 0] #第一列
Out[36]: tensor([0.1227, 0.3351, 0.7569, 0.1359])
In[37]: tensor[..., -1] # 最后一列
Out[37]: tensor([0.9736, 0.1816, 0.9883, 0.9142])

连接张量[2]

连接张量有两种方法:​​torch.cat()​​​或​​torch.stack()​

In[39]: tensor = torch.ones(2, 3)
In[40]: tensor
Out[40]:
tensor([[1., 1., 1.],
[1., 1., 1.]])

首先看一下​​torch.cat()​​:

​dim=1​​​时,形状变为​​[2, 6]​

In[41]: t1 = torch.cat([tensor, tensor, tensor], dim=1)
In[42]: t1
Out[42]:
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1., 1., 1., 1.]])

​dim=0​​​时,形状变为​​[6, 2]​

In[43]: t0 = torch.cat([tensor, tensor, tensor], dim=0)
In[44]: t0
Out[44]:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])

与​​torch.cat()​​​不同,​​torch.stack()​​是沿着一个新维度对输入张量序列进行连接。 序列中所有的张量都应该为相同形状。

浅显说法:就是把多个2维的张量凑成一个3维的张量;多个3维的凑成一个4维的张量……以此类推,也就是在增加新的维度进行堆叠。

例子:

准备2个​​[3,3]​​的张量数据。

In[45]: T1 = torch.tensor([[1, 2, 3],
...: [4, 5, 6],
...: [7, 8, 9]])
...: T2 = torch.tensor([[10, 20, 30],
...: [40, 50, 60],
...: [70, 80, 90]])

​dim=0​​​时,形状变为​​[2, 3, 3]​

In[48]: stack_0=torch.stack((T1,T2),dim=0)
In[49]: stack_0
Out[49]:
tensor([[[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9]],

[[10, 20, 30],
[40, 50, 60],
[70, 80, 90]]])
In[50]: stack_0.shape
Out[50]: torch.Size([2, 3, 3])

​dim=1​​​时,形状变为​​[3, 2, 3]​

In[51]: stack_1=torch.stack((T1,T2),dim=1)
In[52]: stack_1
Out[52]:
tensor([[[ 1, 2, 3],
[10, 20, 30]],

[[ 4, 5, 6],
[40, 50, 60]],

[[ 7, 8, 9],
[70, 80, 90]]])
In[53]: stack_1.shape
Out[53]: torch.Size([3, 2, 3])

​dim=2​​​时,形状变为​​[3, 3, 2]​

In[54]: stack_2=torch.stack((T1,T2),dim=2)
In[55]: stack_2
Out[55]:
tensor([[[ 1, 10],
[ 2, 20],
[ 3, 30]],

[[ 4, 40],
[ 5, 50],
[ 6, 60]],

[[ 7, 70],
[ 8, 80],
[ 9, 90]]])
In[56]: stack_2.shape
Out[56]: torch.Size([3, 3, 2])

算术运算

下面是计算两个张量之间的矩阵乘法的方法。​​y1、y2、y3​​将具有相同的值

In[62]: tensor=torch.ones(3,3)
In[63]: y1 = tensor @ tensor.t() #.t():矩阵转置
In[64]: y2 = tensor.matmul(tensor.t())
In[65]: y3 = torch.rand_like(tensor)
In[66]: torch.matmul(tensor, tensor.t(), out=y3)
Out[66]:
tensor([[3., 3., 3.],
[3., 3., 3.],
[3., 3., 3.]])
In[67]: y1
Out[67]:
tensor([[3., 3., 3.],
[3., 3., 3.],
[3., 3., 3.]])
In[68]: y2
Out[68]:
tensor([[3., 3., 3.],
[3., 3., 3.],
[3., 3., 3.]])
In[69]: y3
Out[69]:
tensor([[3., 3., 3.],
[3., 3., 3.],
[3., 3., 3.]])

下面将计算逐元素的乘积。​​z1、z2、z3​​将具有相同的值

In[70]: z1 = tensor * tensor
In[71]: z2 = tensor.mul(tensor)
In[72]: z3 = torch.rand_like(tensor)
In[73]: torch.mul(tensor, tensor, out=z3);
In[74]: z1
Out[74]:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
In[75]: z2
Out[75]:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
In[77]: z3
Out[77]:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])

单元素张量

如果有一个单元素张量,例如将张量的所有值加起来得到一个值,那么可以使用​​item()​​将其转换为Python数值:

In[82]: agg = tensor.sum()
In[83]: agg
Out[83]: tensor(9.)
In[84]: agg_item = agg.item()
In[85]: agg_item
Out[85]: 9.0
In[86]: type(agg_item)
Out[86]: float
In[87]: type(agg)
Out[87]: torch.Tensor

就地(In-place)操作[3]

就地操作是直接更改给定张量的内容而不会为变量分配新的内存进行复制。

将结果存储到操作数中的操作称为就地调用。它们由一个后缀表示。例如:​​x.copy_(y), x.t_()​​​。这种操作将更改​​x​​。

In[88]: tensor
Out[88]:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
In[89]: tensor.add_(5)
Out[89]:
tensor([[6., 6., 6.],
[6., 6., 6.],
[6., 6., 6.]])
In[90]: tensor
Out[90]:
tensor([[6., 6., 6.],
[6., 6., 6.],
[6., 6., 6.]])

注意:

就地操作可以节省一些内存,但在计算导数时可能会出现问题,因为会立即丢失历史记录。因此,不鼓励使用它们。

张量与Numpy 数组

在CPU的tensor和NumPy的array会共享其底层内存,即更改其中一个的同时另一个也会被更改。

张量 到 NumPy 数组

In[91]: t = torch.ones(5)
In[92]: n = t.numpy()
In[93]: t
Out[93]: tensor([1., 1., 1., 1., 1.])
In[94]: n
Out[94]: array([1., 1., 1., 1., 1.], dtype=float32)

张量的改变将导致对应NumPy数组的改变。

In[95]: t.add_(1)
Out[95]: tensor([2., 2., 2., 2., 2.])
In[96]: t
Out[96]: tensor([2., 2., 2., 2., 2.])
In[97]: n
Out[97]: array([2., 2., 2., 2., 2.], dtype=float32)

NumPy 数组 到 张量

In[99]: n = np.ones(5)
In[99]: t = torch.from_numpy(n)
In[100]: np.add(n, 1, out=n)
Out[100]: array([2., 2., 2., 2., 2.])
In[101]: t
Out[101]: tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
In[102]: n
Out[102]: array([2., 2., 2., 2., 2.])

三阶多项式拟合正弦函数(pytorch,tensor)

下面我们基于PyTorch的张量,将三阶多项式拟合正弦函数。

# -*- coding: utf-8 -*-
import torch
import math
import time

time_start = time.time()
dtype = torch.float
device = torch.device("cpu")
#device = torch.device("cuda:0") # 取消这一行注释以在GPU上运行

# 随机初始化输入和输出数据
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 随机初始化权重
a = torch.randn((), device=device, dtype=dtype)
b = torch.randn((), device=device, dtype=dtype)
c = torch.randn((), device=device, dtype=dtype)
d = torch.randn((), device=device, dtype=dtype)

learning_rate = 1e-6

for t in range(2000):
# 前向传播:计算预测值 y
y_pred = a + b * x + c * x ** 2 + d * x ** 3

# 计算损失
loss = (y_pred - y).pow(2).sum().item()
if t % 100 == 99:
print(f'迭代次数:{t + 1},损失值:{loss}')

# 反向传播:计算损失关于a, b, c, d 的梯度
grad_y_pred = 2.0 * (y_pred - y)
grad_a = grad_y_pred.sum()
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x ** 2).sum()
grad_d = (grad_y_pred * x ** 3).sum()

# 梯度下降更新权重
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
time_end = time.time()
print('消耗时间:', time_end - time_start, 's')
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')

使用CPU,输出:

...
...
迭代次数:2000,损失值:14.256892204284668
消耗时间: 0.8661777973175049 s
Result: y = 0.05625741183757782 + 0.8070318698883057 x + -0.00970533862709999 x^2 + -0.08625971525907516 x^3

使用GPU,输出:

...
...
迭代次数:2000,损失值:9.254937171936035
消耗时间: 8.750219345092773 s
Result: y = -0.000127201754366979 + 0.8364022374153137 x + 2.19428966374835e-05 x^2 + -0.0904374048113823 x^3

???说好的GPU加速呢?

用电锯切菜能快么,电锯是用来砍树的。同样道理,GPU在大规模网络才有明显的加速。

GPU比CPU慢的原因大致为[4]:

数据传输会有很大的开销,而GPU处理数据传输要比CPU慢,而GPU的专长矩阵计算在小规模神经网络中无法明显体现出来。

下面以单纯的矩阵乘法做对比:

import torch
import time

a = torch.ones(10000, 1000)
b = torch.ones(1000, 10000)

t0 = time.time()
c = torch.matmul(a, b)
t1 = time.time()
print('CPU:', t1 - t0, 's')

device = torch.device('cuda')

a = a.to(device)
b = b.to(device)

t0 = time.time()
c = torch.matmul(a, b)
t1 = time.time()
print('GPU:', t1 - t0, 's')

输出:

CPU: 2.434323310852051 s
GPU: 0.5386180877685547 s

这不,快了。

参考:

[1] https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html

[3]https://zhuanlan.zhihu.com/p/344455805