一. 概念:张量、算子
在深度学习框架中,数据经常用张量(Tensor)的形式来存储。张量是矩阵的扩展与延伸,可以认为是高阶的矩阵。1阶张量为向量,2阶张量为矩阵。张量是类似于Numpy的多维数组(ndarray)的概念,可以具有任意多的维度。
个人理解:张量是一种有方向的量,如同点线面的升维,张量一阶为向量,二阶为矩阵,三阶个人感觉因该是类似于正方体的数阵。
算子:算子是一个函数空间到函数空间上的映射O:X→X。广义上的算子可以推广到任何空间,如内积空间等。
个人理解:感觉就是函数三大要素中的对应法则,是初中里的y,高中里的f(x)。
二. 使用pytorch实现张量运算
1.2 张量
1.2.1 创建张量
创建一个张量可以有多种方式,如:指定数据创建、指定形状创建、指定区间创建等。
1.2.1.1 指定数据创建张量
(1)通过指定的Python列表数据[2.0, 3.0, 4.0],创建一个一维张量。
import torch
ndim_1_Tensor = torch.tensor([2.0, 3.0, 4.0])
print(ndim_1_Tensor)
输出结果:
(2)通过指定的Python列表数据来创建类似矩阵(matrix)的二维张量。
import torch
ndim_2_Tensor =torch.tensor([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]])
print(ndim_2_Tensor)
输出结果:
(3)同样地,还可以创建维度为3、4...N等更复杂的多维张量。
import torch
ndim_3_Tensor =torch.tensor([[[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]],
[[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20]]])
print(ndim_3_Tensor)
输出结果:
注意:张量在任何一个维度上的元素数量必须相等,否则会抛出异常!
1.2.1.2 指定形状创建
如果要创建一个指定形状、元素数据相同的张量,可以使用torch.zeros
、torch.ones
、torch.full
等API。
import torch
m, n = 2, 3
# 使用torch.zeros创建数据全为0,形状为[m, n]的Tensor
zeros_Tensor = torch.zeros([m, n])
# 使用torch.ones创建数据全为1,形状为[m, n]的Tensor
ones_Tensor = torch.ones([m, n])
# 使用torch.full创建数据全为指定值,形状为[m, n]的Tensor,这里我们指定数据为10
full_Tensor = torch.full([m, n], 10)
print('zeros Tensor: ', zeros_Tensor)
print('ones Tensor: ', ones_Tensor)
print('full Tensor: ', full_Tensor)
输出结果:
1.2.1.3 指定区间创建
如果要在指定区间内创建张量,可以使用torch.arange
、torch.linspace
等AP
import torch
# 使用torch.arange创建以步长step均匀分隔数值区间[start, end)的一维Tensor
arange_Tensor = torch.arange(start=1, end=5, step=1)
# 使用torch.linspace创建以元素个数num均匀分隔数值区间[start, stop]的Tensor
linspace_Tensor = torch.linspace(start=1, end=5)
print('arange Tensor: ', arange_Tensor)
print('linspace Tensor: ', linspace_Tensor)
输出结果:
1.2.2 张量的属性
1.2.2.1 张量的形状
张量具有如下形状属性:
Tensor.ndim
:张量的维度,例如向量的维度为1,矩阵的维度为2。Tensor.shape
: 张量每个维度上元素的数量。Tensor.shape[n]
:张量第nn维的大小。第nn维也称为轴(axis)。Tensor.size
:张量中全部元素的个数。- 为了更好地理解ndim、shape、axis、size四种属性间的区别,创建一个如图1.6所示的四维张量。
-
ndim_4_Tensor = torch.ones([2, 3, 4, 5]) print('ndim_4_Tensor.ndim:', ndim_4_Tensor.ndim) print('ndim_4_Tensor.shape:', ndim_4_Tensor.shape) print('ndim_4_Tensor.shape[0]:', ndim_4_Tensor.shape[0]) print('ndim_4_Tensor.shape[-1]:', ndim_4_Tensor.shape[-1]) print('ndim_4_Tensor.size:', ndim_4_Tensor.size)
运行结果:
-
1.2.2.2 形状的改变
ndim_3_Tensor = torch.tensor([[[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]]])
print('the shape of nidm_3_Tensor:', ndim_3_Tensor.shape)
# torch.reshape 可以保持在输入数据不变的情况下,改变数据形状。这里我们设置reshape为[2,5,3]
reshape_Tensor = torch.reshape(ndim_3_Tensor, [2, 5, 3])
print('After reshape:', reshape_Tensor)
运行结果:
1.2.2.3 张量的数据类型
# 通过Tensor.dtype来查看张量的数据类型,通过Python元素创建的张量,可以通过dtype来指定数据类型,如果未指定:对于Python整型数据,则会创建int64型张量,对于Python浮点型数据,默认会创建float32型张量。
# 使用torch.tensor通过已知数据来创建一个Tensor
print('Tensor dtype from Python integers:', torch.tensor(1).dtype)
print('Tensor dtype from Python floating point:', torch.tensor(1.0).dtype)
运行结果:
# 定义dtype为float32的Tensor
float32_Tensor = torch.tensor(1.0)
# torch.cast可以将输入数据的数据类型转换为指定的dtype并输出。支持输出和输入数据类型相同。
int64_Tensor = float32_Tensor.to(torch.int64)
print('Tensor after cast to int64:', int64_Tensor.dtype)
运行结果:
1.2.2.4 张量的设备位置
初始化张量时可以通过place来指定其分配的设备位置,可支持的设备位置有三种:CPU、GPU和固定内存。
import torch
# 创建CPU上的Tensor
cpu_Tensor = torch.tensor(1, device=torch.device('cpu'))
print('cpu_Tensor:', cpu_Tensor.device)
运行结果:
1.2.3 张量与Numpy数组转换
# 张量与Numpy数组转换
ndim_1_Tensor = torch.tensor([1., 2.])
# 将当前 Tensor 转化为 numpy.ndarray
print('Tensor to convert: ', ndim_1_Tensor.numpy())
运行结果:
Tensor to convert: [1. 2.]
1.2.4 张量的访问
1.2.4.1 索引和切片
我们可以通过索引或切片方便地访问或修改张量。飞桨使用标准的Python索引规则与Numpy索引规则,具有以下特点:
1.基于0−n的下标进行索引,如果下标为负数,则从尾部开始计算。
2.通过冒号“:”分隔切片参数start:stop:step来进行切片操作,也就是访问start到stop范围内的部分元素并生成一个新的序列。其中start为切片的起始位置,stop为切片的截止位置,step是切片的步长,这三个参数均可缺省。
1.2.4.2 访问张量
# 定义1个一维Tensor
ndim_1_Tensor = torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])
print("Origin Tensor:", ndim_1_Tensor)
print("First element:", ndim_1_Tensor[0])
print("Last element:", ndim_1_Tensor[-1])
print("All element:", ndim_1_Tensor[:])
print("Before 3:", ndim_1_Tensor[:3])
print("Interval of 3:", ndim_1_Tensor[::3])
# pytorch中张量不支持step为小于0的数,这里使用flip来代替
print("Reverse:", ndim_1_Tensor.flip(-1))
运行结果:
1.2.4.3 修改张量
与访问张量类似,可以在单个或多个轴上通过索引或切片操作来修改张量。
ndim_2_Tensor = torch.ones([2, 3])
ndim_2_Tensor = ndim_2_Tensor.to(torch.float32)
print('Origin Tensor: ', ndim_2_Tensor)
# 修改第1维为0
ndim_2_Tensor[0] = 0
print('change Tensor: ', ndim_2_Tensor)
# 修改第1维为2.1
ndim_2_Tensor[0:1] = 2.1
print('change Tensor: ', ndim_2_Tensor)
# 修改全部Tensor
ndim_2_Tensor[...] = 3
print('change Tensor: ', ndim_2_Tensor)
运行结果:
1.2.5 张量的运算
张量支持包括基础数学运算、逻辑运算、矩阵运算等100余种运算操作,以加法为例,有如下两种实现方式:
# 定义两个Tensor
x = torch.tensor([[1.1, 2.2], [3.3, 4.4]])
x = x.to(torch.float64)
y = torch.tensor([[5.5, 6.6], [7.7, 8.8]])
y = y.to(torch.float64)
# 第一种调用方法,torch.add逐元素相加算子,并将各个位置的输出元素保存到返回结果中
print('Method 1: ', torch.add(x, y))
# 第二种调用方法
print('Method 2: ', x.add(y))
运行结果:
1.2.5.1 数学运算
张量类的基础数学函数如下:
x.abs() # 逐元素取绝对值
x.ceil() # 逐元素向上取整
x.floor() # 逐元素向下取整
x.round() # 逐元素四舍五入
x.exp() # 逐元素计算自然常数为底的指数
x.log() # 逐元素计算x的自然对数
x.reciprocal() # 逐元素求倒数
x.square() # 逐元素计算平方
x.sqrt() # 逐元素计算平方根
x.sin() # 逐元素计算正弦
x.cos() # 逐元素计算余弦
x.add(y) # 逐元素加
x.subtract(y) # 逐元素减
x.multiply(y) # 逐元素乘(积)
x.divide(y) # 逐元素除
x.mod(y) # 逐元素除并取余
x.pow(y) # 逐元素幂
x.max() # 指定维度上元素最大值,默认为全部维度
x.min() # 指定维度上元素最小值,默认为全部维度
x.prod() # 指定维度上元素累乘,默认为全部维度
x.sum() # 指定维度上元素的和,默认为全部维度
以下操作与上述结果相同:
x + y -> x.add(y) # 逐元素加
x - y -> x.subtract(y) # 逐元素减
x * y -> x.multiply(y) # 逐元素乘(积)
x / y -> x.divide(y) # 逐元素除
x % y -> x.mod(y) # 逐元素除并取余
x ** y -> x.pow(y) # 逐元素幂
1.2.5.2 逻辑运算
张量类的逻辑运算函数如下:
x.isfinite() # 判断Tensor中元素是否是有限的数字,即不包括inf与nan
x.equal_all(y) # 判断两个Tensor的全部元素是否相等,并返回形状为[1]的布尔类Tensor
x.equal(y) # 判断两个Tensor的每个元素是否相等,并返回形状相同的布尔类Tensor
x.not_equal(y) # 判断两个Tensor的每个元素是否不相等
x.less_than(y) # 判断Tensor x的元素是否小于Tensor y的对应元素
x.less_equal(y) # 判断Tensor x的元素是否小于或等于Tensor y的对应元素
x.greater_than(y) # 判断Tensor x的元素是否大于Tensor y的对应元素
x.greater_equal(y) # 判断Tensor x的元素是否大于或等于Tensor y的对应元素
x.allclose(y) # 判断两个Tensor的全部元素是否接近
矩阵运算
张量类还包含了矩阵运算相关的函数,如矩阵的转置、范数计算和乘法等。
x.t() # 矩阵转置
x.transpose([1, 0]) # 交换第 0 维与第 1 维的顺序
x.norm('fro') # 矩阵的弗罗贝尼乌斯范数
x.dist(y, p=2) # 矩阵(x-y)的2范数
x.matmul(y) # 矩阵乘法
有些矩阵运算中也支持大于两维的张量,比如matmul函数,对最后两个维度进行矩阵乘。比如x是形状为[j,k,n,m]的张量,另一个y是[j,k,m,p]的张量,则x.matmul(y)输出的张量形状为[j,k,n,p]。
1.2.5.4 广播机制
飞桨的广播机制主要遵循如下规则(参考Numpy广播机制):
1)每个张量至少为一维张量。
2)从后往前比较张量的形状,当前维度的大小要么相等,要么其中一个等于1,要么其中一个不存在。
# 当两个Tensor的形状一致时,可以广播
x = torch.ones((2, 3, 4))
y = torch.ones((2, 3, 4))
z = x + y
print('broadcasting with two same shape tensor: ', z.shape)
x = torch.ones((2, 3, 1, 5))
y = torch.ones((3, 4, 1))
# 从后往前依次比较:
# 第一次:y的维度大小是1
# 第二次:x的维度大小是1
# 第三次:x和y的维度大小相等,都为3
# 第四次:y的维度不存在
# 所以x和y是可以广播的
z = x + y
print('broadcasting with two different shape tensor:', z.shape)
运行结果:
从输出结果看,x与y在上述两种情况中均遵循广播规则,因此在张量相加时可以广播。我们再定义两个shape分别为[2, 3, 4]和[2, 3, 6]的张量,观察这两个张量是否能够通过广播操作相加。
x = torch.ones((2, 3, 4))
y = torch.ones((2, 3, 6))
z = x + y
运行结果:
ValueError: (InvalidArgument) Broadcast dimension mismatch.
1.3 算子
1.3.1 算子定义
算子是构建复杂机器学习模型的基础组件,包含一个函数f(x)的前向函数和反向函数。为了可以更便捷地进行算子组合,本书中定义算子Op}的接口如下:
class Op(object):
def __init__(self):
pass
def __call__(self, inputs):
return self.forward(inputs)
# 前向函数
# 输入:张量inputs
# 输出:张量outputs
def forward(self, inputs):
# return outputs
raise NotImplementedError
# 反向函数
# 输入:最终输出对outputs的梯度outputs_grads
# 输出:最终输出对inputs的梯度inputs_grads
def backward(self, outputs_grads):
# return inputs_grads
raise NotImplementedError
在上面的接口中,forward是自定义Op的前向函数,必须被子类重写,它的参数为输入对象,参数的类型和数量任意;backward是自定义Op的反向函数,必须被子类重写,它的参数为forward输出张量的梯度outputs_grads,它的输出为forward输入张量的梯度inputs_grads。
1.3.1.1 加法算子
class add(Op):
def __init__(self):
super(add, self).__init__()
def __call__(self, x, y):
return self.forward(x, y)
def forward(self, x, y):
self.x = x
self.y = y
outputs = x + y
return outputs
def backward(self, grads):
grads_x = grads * 1
grads_y = grads * 1
return grads_x, grads_y
定义x=1、y=4,根据反向计算,得到x、y的梯度
x = 1
y = 4
add_op = add()
z = add_op(x, y)
grads_x, grads_y = add_op.backward(grads=1)
print("x's grad is: ", grads_x)
print("y's grad is: ", grads_y)
运行结果:
1.3.1.2 乘法算子
class multiply(Op):
def __init__(self):
super(multiply, self).__init__()
def __call__(self, x, y):
return self.forward(x, y)
def forward(self, x, y):
self.x = x
self.y = y
outputs = x * y
return outputs
def backward(self, grads):
grads_x = grads * self.y
grads_y = grads * self.x
return grads_x, grads_y
1.3.1.3 指数算子
import math
class exponential(Op):
def __init__(self):
super(exponential, self).__init__()
def forward(self, x):
self.x = x
outputs = math.exp(x)
return outputs
def backward(self, grads):
grads = grads * math.exp(self.x)
return grads
分别指定a、b、c、d的值,通过实例化算子,调用加法、乘法和指数运算算子,计算得到y。
a, b, c, d = 2, 3, 2, 2
# 实例化算子
multiply_op = multiply()
add_op = add()
exp_op = exponential()
y = exp_op(add_op(multiply_op(a, b), multiply_op(c, d)))
print('y: ', y)
运行结果:
1.3.2 自动微分机制
目前大部分深度学习平台都支持自动微分(Automatic Differentiation),即根据forward()函数来自动构建backward()函数。
下面用一个比较简单的例子来了解整个过程。定义两个张量a和b,并用stop_gradient属性用来设置是否传递梯度。将a的stop_gradient属性设为False,会自动为a创建一个反向张量,将b的stop_gradient属性设为True,即不会为b创建反向张量。
# 定义张量a,stop_gradient=False代表进行梯度传导
a = torch.tensor(2.0)
a.requires_grad = False
# 定义张量b,stop_gradient=True代表不进行梯度传导
b = torch.tensor(5.0)
b.requires_grad = True
c = a * b
# 自动计算反向梯度
c.backward()
print("Tensor a's grad is: {}".format(a.grad))
print("Tensor b's grad is: {}".format(b.grad))
print("Tensor c's grad is: {}".format(c.grad))
运行结果:
三. 使用pytorch实现数据预处理
2.2.1. 读取数据集
举一个例子,我们首先创建一个人工数据集,并存储在CSV(逗号分隔值)文件 …/data/house_tiny.csv中。 以其他格式存储的数据也可以通过类似的方式进行处理。 下面我们将数据集按行写入CSV文件中。
import os
os.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
f.write('NumRooms,Alley,Price\n') # 列名
f.write('NA,Pave,127500\n') # 每行表示一个数据样本
f.write('2,NA,106000\n')
f.write('4,NA,178100\n')
f.write('NA,NA,140000\n')
要从创建的CSV文件中加载原始数据集,我们导入pandas包并调用read_csv函数。该数据集有四行三列。其中每行描述了房间数量(“NumRooms”)、巷子类型(“Alley”)和房屋价格(“Price”)。
# 如果没有安装pandas,只需取消对以下行的注释来安装pandas
# !pip install pandas
import pandas as pd
data = pd.read_csv(data_file)
print(data)
运行结果:
NumRooms Alley Price
0 NaN Pave 127500
1 2.0 NaN 106000
2 4.0 NaN 178100
3 NaN NaN 140000
2.2.2. 处理缺失值
注意,“NaN”项代表缺失值。 为了处理缺失的数据,典型的方法包括插值法和删除法, 其中插值法用一个替代值弥补缺失值,而删除法则直接忽略缺失值。 在这里,我们将考虑插值法。
通过位置索引iloc,我们将data分成inputs和outputs, 其中前者为data的前两列,而后者为data的最后一列。 对于inputs中缺少的数值,我们用同一列的均值替换“NaN”项。
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean())
print(inputs)
运行结果:
NumRooms Alley
0 3.0 Pave
1 2.0 NaN
2 4.0 NaN
3 3.0 NaN
对于inputs中的类别值或离散值,我们将“NaN”视为一个类别。 由于“巷子类型”(“Alley”)列只接受两种类型的类别值“Pave”和“NaN”, pandas可以自动将此列转换为两列“Alley_Pave”和“Alley_nan”。 巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。 缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。
inputs = pd.get_dummies(inputs, dummy_na=True)
print(inputs)
运行结果:
NumRooms Alley_Pave Alley_nan
0 3.0 1 0
1 2.0 0 1
2 4.0 0 1
3 3.0 0 1
2.2.3. 转换为张量格式
现在inputs和outputs中的所有条目都是数值类型,它们可以转换为张量格式。 当数据采用张量格式后,可以通过在上述实验中引入的那些张量函数来进一步操作。
from mxnet import np
X, y = np.array(inputs.values), np.array(outputs.values)
X, y
运行结果:
(array([[3., 1., 0.],
[2., 0., 1.],
[4., 0., 1.],
[3., 0., 1.]], dtype=float64),
array([127500, 106000, 178100, 140000], dtype=int64))
pandas软件包是Python中常用的数据分析工具中,pandas可以与张量兼容。
用pandas处理缺失的数据时,我们可根据情况选择用插值法和删除法。
实验心得:本次实验是关于pytorch的一些基础入门概念和算法代码等,首先是回顾张量和算子的概念,然后是联系了一些基础的tensor处理数据的函数(从paddle转化为torch在pycharm上运行出来),虽然只是一些比较基础的操作,但是对于我来说还是有很大的收获,学习是一个循序渐进的过程,所以要不放过任何一个可以进步的机会,继续努力!