有关张量(Tensor)运算的练习
使用 Tensor
PyTorch 是一个开源的深度学习框架,由 Facebook 支持开发。它的前身为 Torch,但因为 Torch 使用的编程语言是 Lua,在国内流行度很小。Facebook 为了迎合数量更多的 Python 用户的需求,推出了 PyTorch。PyTorch 完全开源意味着你可以轻易获取它的代码,并按照自己的需求对它进行修改。比如让 PyTorch 支持复数运算等等。PyTorch 还有另外一个非常出众的特点是,使用 PyTorch 框架编写出的神经网络模型的代码非常简洁。实现同样的功能,使用 PyTorch 框架编写的代码往往更清晰明了,这点我们可以从下图中略见一斑:
PyTorch 的基本数据单元是张量(Tensor),它实际上是一种 N 维数组。
下面我们列举了三种张量,可以看到它们的维度阶数是不同的。
1 阶的张量可以看做是一个向量,通过索引可以取到一个“值”。
2 阶张量可以看做为一个矩阵,通过索引可以取到一个个的向量。
3 阶张量有点抽象,不过我们可以从图中看出,
3 阶张量其实就是在 2 阶张量的矩阵中增加了一个深度。
也就是说在 3 阶张量中我们可以通过索引取到一个个的矩阵。我们不难想象,4 阶张量也就是在 3 阶张量上增加了另外一个轴……我们可以使用 Tensor.size() 方法获得一个张量的“尺寸”。
在这里注意“尺寸”和维度是两个概念。
就比如对于上图中的 1 阶张量,它的维度为 1,尺寸为 8;
对于上图中的 2 阶张量,它的维度为 2,尺寸为(8,6)。
要使用 PyTorch,首先需要在 Python 中引入 PyTorch 的包。
import torch #导入torch包
可以通过以下代码查看当前系统中 PyTorch 的版本:
print(torch.__version__)
可以生成随机数张量:
y = torch.ones(5, 3) #产生一个5*3的Tensor,元素都是1
y
基本 Tensor 运算
两个 2 阶张量相加的方法实际上就是矩阵加法。
注意,要使两个张量相加,必须保证两个张量的尺寸是一致的。
z = x + y #两个tensor可以直接相加
z
下面的语句展示了两个 tensor 按照矩阵的方式相乘,注意 x 的尺寸是 53,y 的尺寸也是 53 无法进行矩阵乘法,所以先将 y 进行转置。转置操作可以用 .t() 来完成,也可以用 transpose(0, 1) 来完成。
q = x.mm(y.t()) #x乘以y的转置
q
所有的Tensor的使用方法请见参考链接中的“Tensor支持的所有操作”。
Tensor 与 numpy.ndarray 之间的转换
PyTorch 的 Tensor 可以与 Python 的常用数据处理包 Numpy 中的多维数组进行转换。
import numpy as np #导入numpy包
a = np.ones([5, 3]) #建立一个5*3全是1的二维数组(矩阵)
b = torch.from_numpy(a) #利用from_numpy将其转换为tensor
b
下面是另外一种转换 Tensor 的方法,类型为 FloatTensor。
# 还可以是LongTensor,整型数据类型
c = torch.FloatTensor(a)
c
还可以从一个 tensor 转化为 numpy 的多维数组
b.numpy()
Tensor 和 Numpy 的最大区别在于 Tensor 可以在 GPU 上进行运算。默认情况下,Tensor 是在 CPU 上进行运算的,如果我们需要一个 Tensor 在 GPU 上的实例,需要运行这个 Tensor 的 .cuda() 方法。在下面的代码中,首先判断在本机上是否有 GPU 环境可用(有 NVIDIA的 GPU,并安装了驱动)。如果有 GPU 环境可用,那么再去获得张量 x,y 的 GPU 实例。
注意在最后打印 x 和 y 这两个 GPU 张量的和的时候,我们调用了 .cpu() 方法,意思是将 GPU 张量转化为 CPU 张量,否则系统会报错。
if torch.cuda.is_available(): #检测本机器上有无GPU可用
x = x.cuda() #返回x的GPU上运算的版本
y = y.cuda()
z = x + y
print(z.cpu()) # 打印时注意要把GPU变量转化为CPU变量。
有关自动微分(Autograd)变量的练习
动态运算图(Dynamic Computation Graph)是 PyTorch 的最主要特性,它可以让我们的计算模型更灵活、复杂,并可以让反向传播算法随时进行。而反向传播算法就是深度神经网络的核心。下面是一个计算图的结构以及与它对应的 PyTorch 代码:
用来构建计算图的数据叫做自动微分变量(Variable),它与 Tensor 不同。每个 Variable 包含三个属性,分别对应着数据(data),父节点(creator),以及梯度(grad)。其中“梯度”就是反向传播算法所要传播的信息。而父节点用于将每个节点连接起来构建计算图(如上图所示)。
下面我们编写代码实际使用自动微分变量。
#导入自动梯度的运算包,主要用Variable这个类
from torch.autograd import Variable
#创建一个Variable,包裹了一个2*2张量,将需要计算梯度属性置为True
x = Variable(torch.ones(2, 2), requires_grad=True)
x
可以按照 Tensor 的方式进行计算
y = x + 2 #可以按照Tensor的方式进行计算
y.grad_fn #每个Variable都有一个creator(创造者节点)
经过上面变量 x 和 y 的运算,我们就有了一个简单的计算图,它是下面这个样子的:
下面我们让计算图再复杂一点,我们再加入变量 z:
注意,.data 可以返回一个 Variable 所包裹的 Tensor
z = torch.mean(y * y) #也可以进行复合运算,比如求均值mean
z.data #.data属性可以返回z所包裹的tensor
现在我们的计算图是这个样子的:
backward 可以实施反向传播算法,并计算所有计算图上叶子节点(没有子节点)的导数(梯度)信息。
注意,由于 z 和 y 都不是叶子节点,所以都没有梯度信息
z.backward() #梯度反向传播
print(z.grad) # 无梯度信息
print(y.grad) # 无梯度信息
print(x.grad)
在下面的例子中,会让矩阵 x 反复作用在向量 s 上,系统会自动记录中间的依赖关系和长路径。
s = Variable(torch.FloatTensor([[0.01, 0.02]]), requires_grad = True) #创建一个1*2的Variable(1维向量)
x = Variable(torch.ones(2, 2), requires_grad = True) #创建一个2*2的矩阵型Variable
for i in range(10):
s = s.mm(x) #反复用s乘以x(矩阵乘法),注意s始终是1*2的Variable
z = torch.mean(s) #对s中的各个元素求均值,得到一个1*1的scalar(标量,即1*1张量)
然后我们得到了一个复杂的“深度”计算图:
z.backward() #在具有很长的依赖路径的计算图上用反向传播算法计算叶节点的梯度
print(x.grad) #x作为叶节点可以获得这部分梯度信息
print(s.grad) #s不是叶节点,没有梯度信息
如果大家觉得去理解这个计算图的叶子结点很困难,这也没有关系。
因为我们研究的主题是深度学习,PyTorch 框架会自动搭建好计算图的。
利用PyTorch实现简单的线性回归算法
准备数据
下面使用 PyTorch 实现一个简单的线性回归算法。线性回归是机器学习中最基础和简单的算法,你可以将它视为深度学习界的 Hello World。如果不了解线性回归,你可以简单的理解为:训练一条直线,让这条直线拟合一些数据点的趋势。大部分的同学在高中或者大学肯定接触过线性回归,所以我在这里也不做过多的介绍了。首先生成一些样本点作为原始数据。这些原始“数据点”就是直线需要拟合的对象。
# linspace可以生成0-100之间的均匀的100个数字
x = Variable(torch.linspace(0, 100).type(torch.FloatTensor))
# 随机生成100个满足标准正态分布的随机数,均值为0,方差为1.
# 将这个数字乘以10,标准方差变为10
rand = Variable(torch.randn(100)) * 10
# 将x和rand相加,得到伪造的标签数据y。
# 所以(x,y)应能近似地落在y=x这条直线上
y = x + rand
将生成的原始数据点画在图上,用视觉观察下数据点的“趋势”。
import matplotlib.pyplot as plt #导入画图的程序包
%matplotlib inline
plt.figure(figsize=(10,8)) #设定绘制窗口大小为10*8 inch
# 绘制数据,考虑到x和y都是Variable,
# 需要用data获取它们包裹的Tensor,并专成numpy
plt.plot(x.data.numpy(), y.data.numpy(), 'o')
plt.xlabel('X') #添加X轴的标注
plt.ylabel('Y') #添加Y轴的标注
plt.show() #将图形画在下面
所有的数据点以视觉可见的增势增长。下面我们构造模型,来拟合这些数据点。
构造模型 计算损失函数
我们要使用一条直线去拟合若干个点的走势,那在数学上怎么表示这条直线哪?这个大家在高中应该都学过,可以用 ax+b 来表示一条直线。因为这条直线是由参数 a 和 b 控制的,所以模型就是要“学习”出这两个参数。那么下面首先建立变量,随机初始化用于线性拟合的参数 a 和 b。
#创建a变量,并随机赋值初始化
a = Variable(torch.rand(1), requires_grad = True)
#创建b变量,并随机赋值初始化
b = Variable(torch.rand(1), requires_grad = True)
print('Initial parameters:', [a, b])
在当前的模型中,这两个参数的初始值无关紧要,因为下面会通过 1000 次的训练,来反复修正这两个参数。在下面的代码中,需要注意 expand_as 和 mul 的使用。首先,a 的维度为 1,x 是维度为 1001 的 Tensor,这两者不能直接相乘,因为维度不同。所以,先要将 a 升维成 11 的 Tensor。这就好比将原本在直线上的点被升维到了二维平面上,同时直线仍然在二维平面中。
learning_rate = 0.0001 #设置学习率
for i in range(1000):
### 下面这三行代码非常重要,这部分代码,清空存储在变量a,b中的梯度信息,
### 以免在backward的过程中会反复不停地累加
#如果a和b的梯度都不是空
if (a.grad is not None) and (b.grad is not None):
a.grad.data.zero_() #清空a的数值
b.grad.data.zero_() #清空b的数值
#计算在当前a、b条件下的模型预测数值
predictions = a.expand_as(x) * x + b.expand_as(x)
#通过与标签数据y比较,计算误差
loss = torch.mean((predictions - y) ** 2)
print('loss:', loss.data.numpy())
loss.backward() #对损失函数进行梯度反传
#利用上一步计算中得到的a的梯度信息更新a中的data数值
a.data.add_(- learning_rate * a.grad.data)
#利用上一步计算中得到的b的梯度信息更新b中的data数值
b.data.add_(- learning_rate * b.grad.data)
从打印出的损失中我们可以观察到损失一直在下降。我们现在可以把直线绘制出来,看看这条直线是什么样子。
x_data = x.data.numpy() # 获得x包裹的数据
plt.figure(figsize = (10, 7)) #设定绘图窗口大小
xplot, = plt.plot(x_data, y.data.numpy(), 'o') # 绘制原始数据
yplot, = plt.plot(x_data, a.data.numpy() * x_data + b.data.numpy()) #绘制拟合数据
plt.xlabel('X') #更改坐标轴标注
plt.ylabel('Y') #更改坐标轴标注
str1 = str(a.data.numpy()[0]) + 'x +' + str(b.data.numpy()[0]) #图例信息
plt.legend([xplot, yplot],['Data', str1]) #绘制图例
plt.show()
可以观察到直线已经拟合了点的走势。你可以修改训练次数,把训练次数改的很小,或者增加 10 倍,看看线性拟合会有什么变化。
测试模型
虽然模型很简单,但我们仍要进行测试的流程。因为“准备数据”、“模型设计”、“训练”、“测试”是完成深度学习任务的基本套路。
x_test = Variable(torch.FloatTensor([1, 2, 10, 100, 1000])) #随便选择一些点1,2,……,1000
predictions = a.expand_as(x_test) * x_test + b.expand_as(x_test) #计算模型的预测结果
predictions #输出