PyTorch Tensor 创建方法

PyTorch Tensor 创建方法

PyTorch 把数据封装成张量(Tensor)来运算,它就是元素类型统一的多维数组。训练模型的第一步,永远是把原始数据喂成张量。可创建张量的接口有一长串,torch.tensor、torch.Tensor、torch.randn、torch.zeros,名字长得像,默认类型、是否初始化、数据从哪来却完全不同,刚上手经常踩坑,要么拿到一屏垃圾值,要么类型不对在 GPU 上报错。

这一节我们把最常用的创建方式分四组讲清楚:从已有数据创建、生成线性序列和随机数、造全零全一这类常量张量,以及创建之后怎么改元素类型。每组都给具体输入和输出对照,再配代码。读之前只要知道张量是多维数组,会 import torch 和 numpy 就行。

1. 从数据创建

最常见的场景是手里已经有一份数据,想把它包成张量。入口是 torch.tensor,它可以吃标量、Python 列表、numpy 数组。torch.tensor(10) 造出一个 0 维的标量张量,data = torch.tensor([[10.,20.,30.],[40.,50.,60.]]) 造出 2 乘 3 的张量。类型由输入自动推断,喂 Python 列表默认是 float32,喂 float64 的 numpy 数组就跟着用 float64。这里要记住一条:torch.tensor 会把数据拷一份进去,和原来的列表、数组不再共享内存。

容易混的是 torch.Tensor,注意这是大写。torch.Tensor(2, 3) 传的是形状,它造一个 2 行 3 列的未初始化张量,默认 float32,里面是什么值完全不可控,所以造完必须立刻填值再用。但如果传的是列表,比如 torch.Tensor([10]),它又会把列表当数据,造出含元素 10 的张量。同一个函数,参数是数字就按形状走,参数是列表就按数据走,这是新手最容易搞反的地方。

我们还可以直接用指定类型的构造器。torch.IntTensor(2, 3) 造 2 乘 3 的 int32 张量,同样未初始化。传列表时它会做类型转换,torch.IntTensor([2.5, 3.3]) 得到 [2, 3],注意这是截断不是四舍五入,小数部分直接扔掉。其他常用的对应关系我们列在下面:

构造器元素类型典型用法
torch.IntTensorint32torch.IntTensor(2, 3)
torch.ShortTensorint16torch.ShortTensor()
torch.LongTensorint64torch.LongTensor()
torch.FloatTensorfloat32torch.FloatTensor()
torch.DoubleTensorfloat64torch.DoubleTensor()

实际建议是优先用 torch.tensor 加显式 dtype,比如 torch.tensor(data, dtype=torch.float32),少用大写构造器,这样能避开未初始化和参数歧义两类坑。下面的代码把这几种创建方式都跑一遍:

import torch
import numpy as np
import random

# 1. 根据已有数据创建张量
def test01():

    # 1. 创建张量标量
    data = torch.tensor(10)
    print(data)

    # 2. numpy 数组, 由于 data 为 float64, 下面代码也使用该类型
    data = np.random.randn(2, 3)
    data = torch.tensor(data)
    print(data)

    # 3. 列表, 下面代码使用默认元素类型 float32
    data = [[10., 20., 30.], [40., 50., 60.]]
    data = torch.tensor(data)
    print(data)


# 2. 创建指定形状的张量
def test02():

    # 1. 创建2行3列的张量, 默认 dtype 为 float32
    data = torch.Tensor(2, 3)
    print(data)

    # 2. 注意: 如果传递列表, 则创建包含指定元素的张量
    data = torch.Tensor([10])
    print(data)

    data = torch.Tensor([10, 20])
    print(data)


# 3. 使用具体类型的张量
def test03():

    # 1. 创建2行3列, dtype 为 int32 的张量
    data = torch.IntTensor(2, 3)
    print(data)

    # 2. 注意: 如果传递的元素类型不正确, 则会进行类型转换
    data = torch.IntTensor([2.5, 3.3])
    print(data)

    # 3. 其他的类型
    data = torch.ShortTensor()  # int16
    data = torch.LongTensor()   # int64
    data = torch.FloatTensor()  # float32
    data = torch.DoubleTensor() # float64


if __name__ == '__main__':
    test02()

2. 线性与随机

有了从数据创建的方法,接下来就是按规则批量造数,最常用的是线性序列和随机数。torch.arange(start, end, step) 按步长取点,区间是左闭右开的,torch.arange(0, 10, 2) 得到 [0, 2, 4, 6, 8],10 本身不包含在内。torch.linspace(start, end, steps) 则按元素总数取点,两端都包含,torch.linspace(0, 11, 10) 的意思是在 0 到 11 之间均匀放 10 个点,步长是 11 除以 9 约等于 1.2222,依次是 0、1.2222、2.4444、3.6667、4.8889、6.1111、7.3333、8.5556、9.7778、11。一个按步长、一个按点数,这就是两者最直接的区别。

随机张量用 torch.randn(2, 3),它造一个 2 行 3 列、元素服从标准正态分布(均值 0、方差 1)的张量。每次运行结果都不一样,调试时这很烦人。解决办法是固定随机数种子,torch.random.manual_seed(100) 之后,同一个随机数函数在相同代码下就会产生同样的数,实验才能复现。torch.random.initial_seed() 可以查看当前生效的种子值。写训练代码时,正式实验前固定种子是基本习惯。

import torch


# 1. 创建线性空间的张量
def test01():

    # 1. 在指定区间按照步长生成元素 [start, end, step)
    data = torch.arange(0, 10, 2)
    print(data)

    # 2. 在指定区间按照元素个数生成
    data = torch.linspace(0, 11, 10)
    print(data)


# 2. 创建随机张量
def test02():

    # 1. 创建随机张量
    data = torch.randn(2, 3)  # 创建2行3列张量
    print(data)

    # 2. 随机数种子设置
    print('随机数种子:', torch.random.initial_seed())
    torch.random.manual_seed(100)
    print('随机数种子:', torch.random.initial_seed())


if __name__ == '__main__':
    test02()

3. 常量张量

初始化权重、造掩码、做累加器时,我们经常需要一张全是 0、全是 1,或者全是某个固定值的表。torch.zeros(2, 3) 造 2 乘 3 的全零张量,torch.ones(2, 3) 造全一,torch.full([2, 3], 10) 造 2 乘 3、每个元素都是 10 的张量。

实际代码里更常用的是带 _like 后缀的版本。手里已经有一个张量 data,想造一个和它形状一模一样的全零表,不用重新数形状,直接 torch.zeros_like(data) 就行,ones_like、full_like 同理。这种写法的好处是形状跟着原张量走,原张量形状变了,新张量自动跟着变,不会因为手改形状而漏掉某一处。

按形状创建按已有张量创建效果
torch.zeros(2, 3)torch.zeros_like(data)全零张量
torch.ones(2, 3)torch.ones_like(data)全一张量
torch.full([2, 3], 10)torch.full_like(data, 20)全部填充指定值
import torch


# 1. 创建全0张量
def test01():

    # 1. 创建指定形状全0张量
    data = torch.zeros(2, 3)
    print(data)

    # 2. 根据张量形状创建全0张量
    data = torch.zeros_like(data)
    print(data)


# 2. 创建全1张量
def test02():

    # 1. 创建指定形状全0张量
    data = torch.ones(2, 3)
    print(data)

    # 2. 根据张量形状创建全0张量
    data = torch.ones_like(data)
    print(data)


# 3. 创建全为指定值的张量
def test03():

    # 1. 创建指定形状指定值的张量
    data = torch.full([2, 3], 10)
    print(data)

    # 2. 根据张量形状创建指定值的张量
    data = torch.full_like(data, 20)
    print(data)


if __name__ == '__main__':
    test03()

4. 类型转换

张量造出来之后,元素类型不一定合心意。比如 torch.full([2,3], 10) 默认是 int64,可后面接的运算要求浮点型,这时候就需要转类型。转类型有两种写法,第一种是 data.type(torch.DoubleTensor),直接传目标类型的构造器类名,转完得到 float64。第二种是更短的快捷方法 data.double(),和上面那句等价。对应的其他类型也有快捷方法,short、int、long、float 一一对应。

type 写法快捷方法结果 dtype
data.type(torch.DoubleTensor)data.double()float64
data.type(torch.FloatTensor)data.float()float32
data.type(torch.IntTensor)data.int()int32
data.type(torch.LongTensor)data.long()int64
data.type(torch.ShortTensor)data.short()int16

要注意,类型转换返回的是新张量,原张量的 dtype 不会变,必须用返回值接住。GPU 上常见的报错就是整数张量直接进了需要浮点的运算,看到 expected scalar type Float but found Long 这种提示,基本就是这里忘了转。

import torch


def test():

    data = torch.full([2, 3], 10)
    print(data.dtype)

    # 将 data 元素类型转换为 float64 类型

    # 1. 第一种方法
    data = data.type(torch.DoubleTensor)
    print(data.dtype)

    # 转换为其他类型
    # data = data.type(torch.ShortTensor)
    # data = data.type(torch.IntTensor)
    # data = data.type(torch.LongTensor)
    # data = data.type(torch.FloatTensor)

    # 2. 第二种方法
    data = data.double()
    print(data.dtype)

    # 转换为其他类型
    # data = data.short()
    # data = data.int()
    # data = data.long()
    # data = data.float()


if __name__ == '__main__':
    test()

5. 常见误区

第一个误区,分不清 torch.tensor 和 torch.Tensor。小写 tensor 从已有数据创建并复制一份,大写 Tensor 传数字就按形状造未初始化张量,传列表又当数据处理。日常优先用小写 torch.tensor 加 dtype,能避开大半问题。

第二个误区,把未初始化张量直接拿来用。torch.Tensor(2, 3) 和 torch.IntTensor(2, 3) 造出来的值是内存里的残留,不是 0,造完必须先赋值,或者用 zeros、randn 这类带初始化的函数。

第三个误区,以为 IntTensor([2.5, 3.3]) 会四舍五入。它是向零截断,得到 [2, 3],想要四舍五入得自己先 round。

第四个误区,忘记类型转换要接返回值。data.double() 不改原对象,不赋值等于没转,GPU 上报 expected scalar type 多半是这个原因。

小结一下。创建张量记住四条主线。从已有数据走 torch.tensor,类型自动推断且复制数据,想省事用它。按规则批量造数,arange 按步长且右端开区间,linspace 按点数且两端闭合,randn 出标准正态随机数,配合 manual_seed 固定种子保证复现。造常量用 zeros、ones、full,配 _like 后缀跟着已有张量的形状走。最后类型不对,用 type 或 double 这类快捷方法转换,记得接返回值。把几段代码在本地跑一遍,对照着形状和 dtype 输出看,接口的区别就不再容易混了。