☰
深度学习入门:张量与随机张量创建完整指南
2026/10/13 14:48:32 网站建设 项目流程

开始接触深度学习时,我最常做的事情不是训练模型,而是在终端里反复创建张量。当时总觉得“创建张量”不过是一行API调用,直到后面做项目时踩了共享内存和随机种子的坑,才发现这个基础操作里藏着一堆值得掰扯清楚的细节。这篇文章就围绕张量和随机张量的创建,把从零开始正确创建一个张量的完整思考链路讲透,同时会带上一些我实际使用时的习惯和坑点,适合刚入门深度学习、以及被张量属性搞晕的读者。看完你会发现,很多“报错”其实在创建那一刻就已经埋下了伏笔。

在正式创建张量之前,我强烈建议先花两分钟梳理一下张量的三个底层属性:形状、数据类型、存储设备。这三个属性决定了你用哪一行创建代码最合适,也决定了后续操作是丝滑还是疯狂报错。我会把这三个属性串起来讲,然后进入具体的创建方法,最后用一个小项目演示随机张量在数据增强里的实际价值。

1. 张量创建前的准备工作与核心概念

1.1 张量到底是什么:一个普通数组的升级版

张量(Tensor)这个概念听起来高大上,但它本质上就是“多维数组”的学术版。一个一维张量就是一个向量,比如存储一组温度读数;一个二维张量就是一个矩阵,适合存放一张灰度图像的像素;三维张量可以表示多个图像堆叠,或者一个序列数据,比如一句话的词向量序列。

真正的关键在于张量除了数据本身,还捆绑了很多“元信息”,你可以把它理解成快递包裹:箱子里是数据(数值),箱子上贴着标签(形状、类型、设备)。只关注箱子里的东西而不看标签,后面的运算就很容易出问题。举个例子,同样是数值1,int64和float32在内存中占用的空间不同,参与GPU计算时的规则也不同。一个形状为(3, 4)的矩阵和一个形状为(4, 3)的矩阵,哪怕数值一模一样,矩阵乘法结果也完全不同。

所以创建张量这件事,第一课不是敲代码,而是建立“先想清楚形状、类型、设备”的思维习惯。后面我会反复用到这三个关键词。

1.2 创建前必须明确的三个属性:shape、dtype、device

在动手写torch.tensor(...)之前,我一般会先问自己三个问题。

第一个是shape。这个张量应该有几个维度?每个维度有多大?处理图像时通常希望形状是(C, H, W)或(B, C, H, W),处理文本序列时则可能是(batch, length, hidden)。如果一开始shape就设计错了,后面所有cat、stack、reshape都会跟着错,排查起来很痛苦。

第二个是dtype(数据类型)。PyTorch里常见的有torch.float32、torch.float64、torch.int64、torch.bool等。默认的torch.float32精度足够且运行速度最快,没有特殊理由不要用float64。做整数计数或索引时用int64,但你要知道,很多深度学习模型权重都是float32,如果你创建了一个float64的张量去喂给模型,某些情况下会遇到类型不匹配的报错。

第三个是device(设备)。默认创建的张量在CPU上。如果后续要在GPU上训练,你可以在创建时直接指定device='cuda',或者创建后调用.to('cuda')。这里有个常见的反直觉点:一个CPU张量和一个GPU张量哪怕形状、数值一样,也不能直接相互运算,因为它们在物理上位于不同的内存空间。这个坑我在第4节会详细说。

这三个属性搞定后,创建什么样的张量就有方向了。接下来我们分两类:一类是“从已知数据创建”,另一类是“按规则生成随机数据”。

2. 创建固定数据张量的几种常用姿势

2.1 从 Python 数据直接转换:torch.tensor 是首选

先把最容易踩坑的一组API说清楚:torch.tensor()和torch.Tensor()。这两个名字只差一个小写字母,但它们的行为完全不同,我见过太多新手在这里迷茫。

torch.tensor(data)是一个函数,它接收Python列表、NumPy数组、标量等,然后根据传入数据的类型自动推断出一个合适的dtype。例如torch.tensor([1, 2, 3])默认是torch.int64,而torch.tensor([1.0, 2.0, 3.0])默认是torch.float32。如果传入的是NumPy数组,它还会尽可能复用原数组的内存,这一点在性能敏感时很关键。

torch.Tensor(shape)则是类,它接收的不是数据本身,而是一个形状,然后返回一个未初始化的张量,内容完全随机且不可预测。比如torch.Tensor(3, 4)会得到一个3x4的张量,但里面的值是内存里残留的垃圾数据。这是非常危险的,因为后续如果你忘了给它赋值就直接参与运算,结果可能莫名其妙。所以我个人的习惯是:想从数据创建张量,只用小写字母的torch.tensor;想指定形状初始化,则用torch.zeros、torch.ones或专门的初始化方法。

import torch # 推荐:从列表创建 data = [[1, 2, 3], [4, 5, 6]] t = torch.tensor(data) print(t.shape) # torch.Size([2, 3]) print(t.dtype) # torch.int64 # 不推荐:小心未初始化的垃圾值 tmp = torch.Tensor(2, 2) print(tmp) # 内容可能是任何值,不可依赖

这里顺便提一点:如果你需要把Python的嵌套列表转成张量,列表的每一行长度必须一致,否则PyTorch会直接报错。例如[[1, 2], [3, 4, 5]]这种不规则列表是无法被直接转换的。遇到这种情况,你要么补齐列表,要么后续用其他方式处理。

2.2 构造全零与全一张量:zeros、ones 和 empty 的使用场景

创建张量最常见的需求之一是“初始化一个指定形状的容器”。torch.zeros(3, 4)会生成一个3行4列、所有元素都是0的浮点张量,默认dtype是torch.float32。当你需要掩码矩阵、或者要为某个参数赋初始偏置时,全零张量非常方便。

torch.ones(3, 4)则生成全1张量,常用来做矩阵乘法里的单位量测试,或者在代码里临时占位。这两个接口都支持额外的参数,比如dtype=torch.float64、device='cuda'。它们最实用的地方是接收一个形状元组:torch.zeros((2, 3, 4))。注意圆括号内是一个形状元组,等价于torch.zeros(2, 3, 4)。

真正让新手失手的是torch.empty(3, 4)。从名字看像是“创建空张量”,但这里的“空”指的是“只分配内存空间,不清理里面的数据”,所以内容完全依赖内存的当前状态,可能是任意垃圾值。我见过有人在empty后忘记赋值,直接把“随机垃圾值”当成了模型输入,结果训练曲线疯狂震荡。如果你非常清楚自己后续会逐元素填充,比如配合copy_()或normal_()方法,那empty性能会有优势;否则老老实实使用zeros,安全第一。

zeros = torch.zeros(2, 2, dtype=torch.float32) ones = torch.ones(3) empty = torch.empty(2, 2) # 不推荐依赖 empty 的当前内容,它是不确定的

2.3 序列张量:arange 与 linspace 的差异判断

如果我想生成一组连续的数字,比如0到9,或者从0到1均分20个点,我通常会选择torch.arange或torch.linspace。这两个接口虽然都用来生成序列,但设计思路完全不同。

torch.arange(start, end, step)类似于Python的range,它根据步长生成数字,且不包含end值。例如torch.arange(0, 10, 2)得到[0, 2, 4, 6, 8]。这里有一个我是后来才注意到的点:当step为浮点数时,由于浮点精度有限,生成的序列末尾可能比你预期多一个或少一个元素。比如torch.arange(0, 1, 0.1)在某些情况下最后会包含0.9999而不是1.0,导致元素个数不稳定。所以在需要“精确分成若干段”的场景,我更推荐用torch.linspace。

torch.linspace(start, end, steps)的参数不是步长,而是“总共生成多少个点”。它一定包含start和end两个端点,且点数精确为steps。例如torch.linspace(0, 1, 5)得到[0, 0.25, 0.5, 0.75, 1.0]。这种确定性让它在绘图、采样、归一化时非常可靠。简单总结我自己的选择标准:要按固定步长生成,用arange;要按固定数量均分区间,用linspace。

a = torch.arange(0, 10, 2) # tensor([0, 2, 4, 6, 8]) b = torch.linspace(0, 1, 5) # tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])

知道怎么创建确定数据张量后,随机张量就成了下一步的刚需。深度学习中几乎处处需要随机:初始参数需要随机,数据采样需要随机,Dropout需要随机,数据增强也需要随机。随机张量不像固定数据那样一眼看出结果,我们要理解它的生成规则和分布含义,否则很容易在训练时出奇怪的问题。

3. 随机张量的创建、分布与用法

3.1 均匀分布随机张量 torch.rand:底层逻辑与使用场景

torch.rand(*size)生成的张量里每个元素都来自$[0, 1)$区间上的均匀分布。这个“半开区间”意味着结果可以非常接近0,但永远不会等于1。PyTorch内部使用的伪随机数生成算法在默认设备上有很好的随机性和速度,所以你可以放心使用。

为什么均匀分布这么重要?一个很典型的场景是数据归一化:当你需要一份随机概率权重时,均匀分布是最公平的选择。还有一个场景是掩码:比如我要对一批样本随机遮挡一小部分区域,就可以通过torch.rand生成一个0到1之间的矩阵,再与预设阈值比较,把小于阈值的区域置0。这个操作天然就是均匀分布,因为所有位置被选中的概率相同。

我在实际项目里还用它做过一次“负样本随机采样”。给定一个全量索引张量,用torch.rand生成随机数,然后按比例挑选前百分之几作为负样本。这样做比手动写循环快了不止一个数量级,因为张量操作底层是向量化的。

random_uniform = torch.rand(3, 3) # 所有元素在 [0, 1) 内,接近均匀分布

3.2 标准正态分布 torch.randn:为什么更适合做参数初始化

如果说rand是均匀撒点,那torch.randn就是按“钟形曲线”撒点。它生成的每个元素来自标准正态分布,也就是均值为0、方差为1的高斯分布。整体看,生成结果大概率落在-3到3之间,但极端值偶尔也会出现。

这个分布特别适合深度学习参数的初始化。原因有两个维度。第一,从信号传播的角度看,正态分布的对称性让总和的均值天然为0,不容易让神经元输出偏向某个方向,有利于梯度的稳定传播。第二,从方差角度看,标准正态分布的方差为1,但实际初始化时通常还要乘一个小的缩放系数,比如Xavier初始化的实现,本质上就是让每层输入输出方差保持在一个合适范围内。直接全部初始化为0或常数会造成对称性问题,所有神经元学到同一样东西,这早已是被验证过的问题。

如果你需要非标准正态分布,比如均值0.5、方差2,你可以直接用变换公式x = torch.randn(size) * 2 + 0.5。背后的原理很简单:线性变换改变均值和方差,但保持分布的形状。这条公式在生成带噪声的特征时很好用。

random_normal = torch.randn(3, 3) # 结果集中在0附近,标准差约1

3.3 随机整数与随机排列:randint 和 randperm 的使用场景

有些场景需要整数随机数,比如随机抽取一些数据索引。torch.randint(low, high, size)会生成从low到high-1之间的均匀整数。注意同样不包含high。例如torch.randint(0, 10, (5,))得到5个0到9之间的整数。这个接口在实现“随机选择batch”时很方便,但如果你想要的是“不重复的随机采样”,那就不要用randint,因为它允许元素重复。

“不重复”的需求要用torch.randperm(n)。它会生成一个从0到n-1的随机排列,本质上是把整数顺序打乱。典型场景是数据集shuffle:在训练某个深度学习模型时,我通常先生成一个randperm(len(dataset)),然后用它去索引数据集样本,这样既保证了每个epoch样本不会顺序固定,又不会重复抽到同一个样本。相比逐次调用randint且手动去重,randperm优雅得多。

# 5个 [0, 10) 内的随机整数,可重复 ri = torch.randint(0, 10, (5,)) # 不重复的随机排列 [0, n-1] perm = torch.randperm(10)

顺带一提,如果你既要打乱数据,又要保持每个样本的标签对应关系,一个实用的做法是生成随机排列索引,然后用同一个索引数组去索引数据和标签,保证它们同步打乱。这也是randperm最常见的高级用法。

3.4 自定义分布与设置随机种子:训练可复现的关键

很多教程只会演示torch.rand和torch.randn,但实际项目中我们经常需要“给已有张量填充随机值”。PyTorch提供了原地填充方法,比如t.uniform_(a, b)和t.normal_(mean, std),注意方法名末尾的下划线表示这是一个原地操作,会直接修改张量本身。

x = torch.empty(4, 4) x.uniform_(0, 1) # 原地填充均匀分布 y = torch.empty(3) y.normal_(0, 2) # 原地填充均值为0、方差为2的正态分布

然后是随机种子,这是我想特别强调的部分。为了让实验可以复现,我们会在代码开头设置torch.manual_seed(42)。一旦设置了全局种子,PyTorch生成的随机序列在相同软硬件环境下就是确定且可复现的。但很多人发现“明明设了种子,第二次跑同一段代码结果还是不一样”,这里的关键在于:如果代码里使用了GPU,由于GPU上并行运算和异步执行,随机序列对线程调度敏感,单靠torch.manual_seed未必能完全保证复现。更稳妥的做法是同时设置torch.cuda.manual_seed_all(42),并且把torch.backends.cudnn.deterministic设为True,torch.backends.cudnn.benchmark设为False。当然,完全确定性会牺牲一些性能,所以一般只在调试阶段这样做。

下面是一个我常用的“全局随机种子”设置函数,它会把Python内置的随机、NumPy随机、PyTorch随机三者的种子全部统一设置,避免不同库之间彼此独立导致复现困难:

import random import numpy as np import torch def set_seed(seed: int) -> None: """统一设置所有随机源种子,便于复现实验""" random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

4. 实操中常见问题与避坑指南

4.1 随机种子没有生效的排查思路

我经常收到类似“我明明写了torch.manual_seed,为什么每次生成的随机张量还是不一样”的求助。这种问题99%出在下面几个环节。

第一个环节是种子设置的位置不对。torch.manual_seed只影响设置之后创建的随机张量,如果你在调用torch.randn之后才设置种子,那当然不起作用。正确的做法是在所有随机操作之前设置种子。

第二个环节是多个随机源混用。比如你用了NumPy生成随机索引,又用PyTorch生成随机增强参数,两边都依赖随机数,但你只设置了torch.manual_seed,NumPy那边没设,每次运行NumPy生成的序列就会变化,最终结果自然不复现。解决方案就是我上面写的统一set_seed函数。

第三个环节是CUDA并发。GPU上的随机操作是异步并行的,即便设置了种子,每次调用时的线程调度差异也会导致结果稍有不同。如果要严格复现,除了设置cudnn.deterministic,还需要把CPU上的固定随机源也处理好,并且避免在验证阶段使用任何非确定性算法。

4.2 张量共享内存与数据拷贝:修改一个全变了

新手最容易忽略的张量“隐藏特性”是内存共享。两个看起来独立的张量,底层可能指向同一块物理内存。最常见的来源是view()操作。我举个例子:

a = torch.arange(1, 7).view(2, 3) b = a.view(3, 2) # b 与 a 共享同一份数据 b[0, 0] = 100 print(a[0, 0]) # 也变成了 100

view只是改变了张量的“观察方式”,它不复制数据。所以当你修改b中的元素时,a的内容也跟着变。这通常在反向传播和参数更新时是期望行为,但在数据预处理时却很容易引发诡异问题。如果你想要一个独立的副本,应该使用a.clone(),克隆后的张量拥有自己的内存空间,修改互不影响。

另一个容易混淆的是torch.tensor对NumPy数组的处理。当传入的NumPy数组是连续内存且对齐条件满足时,torch.tensor(arr)可能与原数组共享内存。如果你之后原地修改了NumPy数组,张量也会改变,反之亦然。为了避免这种粘连,显式调用torch.tensor(arr).clone()是简单粗暴的隔离方案。

4.3 设备不一致导致的神秘错误

深度学习开发中最常见的报错之一是“Expected all tensors to be on the same device”,这种问题追根溯源往往就是创建张量时没有统一设备。你在CPU上创建了个torch.zeros(3),然后想直接与模型在GPU上的权重相乘,框架就会直接罢工。

我习惯的做法是定义一个统一的计算设备变量,比如:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

然后创建张量时显式传入device=device,比如torch.randn(3, 3, device=device)。这样写虽然啰嗦,但避免了后续到处调用.to(device)。特别是在使用DataLoader时,默认返回的批次张量在CPU上,如果模型在GPU上,必须在训练循环里显式调用batch = batch.to(device),否则一定会遇到设备不匹配的报错。

还有一次我踩过一个更隐蔽的坑:模型参数在GPU上,但我创建了一个目标掩码时忘记指定设备,导致它默默留在CPU,然后整个损失计算就报错。从那以后,凡是要参与运算的张量,我都会在创建那一行把device写好,绝不事后补救。

4.4 NumPy随机库与PyTorch随机库的联动细节

很多项目会同时使用NumPy和PyTorch,比如先用NumPy做数据载入和简单变换,再用PyTorch做模型计算。这时候随机性管理就要格外小心。np.random.seed(0)只影响NumPy的全局随机状态,torch.manual_seed(0)只影响PyTorch的全局随机状态,两者互不相干。如果你在同一个脚本里混用,必须在设置种子时把两个库都覆盖到。

从实际经验看,建议直接在项目入口处调用我上面写的set_seed函数,并且把它放在所有导入和初始化之后、任何随机操作之前。有些库比如某些数据增强库也有自己的随机状态,这类复杂情况我会在加载库之后立即设置种子,防止库内部初始化时消耗掉一部分随机序列。

5. 一个小项目:用随机张量做数据增强模拟

5.1 模拟需求与核心思路

前面讲了很多“创建”层面的知识,它们组合起来能做什么呢?我来分享一个我用随机张量做的数据增强模拟。假设我们在做一个图像分类任务,但训练样本不够多,常见的做法是对图片做随机水平翻转、随机裁剪和随机遮挡。这里我不依赖现成的数据增强库,直接用最基础的张量操作来演示随机性的威力。

核心思路是:先把一张图片抽象成一个三维张量(C, H, W),然后生成若干个随机数,分别决定是否翻转、裁剪位置以及遮挡区域的位置。这些随机数全部来自torch.rand系列接口,最后组合成新的样本。

为了代码可复现,我在开头设置了种子。实际项目中,训练时往往要关闭固定种子以保证样本多样性,但这里是为了演示原理,所以要打开。

5.2 代码实现与关键解释

下面我写一个简化版的模拟程序,只演示核心思想,不引入真实图片文件。

import torch from torchvision.transforms.functional import hflip def random_transform_sample(image_tensor, flip_prob=0.5): """ 对单张图像张量做简单的随机增强。 image_tensor: shape (C, H, W),取值范围假设在 [0, 1] """ # step1: 随机翻转 if torch.rand(1).item() < flip_prob: image_tensor = hflip(image_tensor) # step2: 随机遮挡 C, H, W = image_tensor.shape mask = torch.ones_like(image_tensor) cover_h = int(H * 0.2) cover_w = int(W * 0.2) top = torch.randint(0, H - cover_h, (1,)).item() left = torch.randint(0, W - cover_w, (1,)).item() mask[:, top:top+cover_h, left:left+cover_w] = 0 image_tensor = image_tensor * mask return image_tensor # 创建模拟图像:3通道,64x64,像素值来自均匀分布随机张量 fake_image = torch.rand(3, 64, 64) # 执行一次随机增强 augmented = random_transform_sample(fake_image) print(augmented.shape) # torch.Size([3, 64, 64])

这个简例里用到了本节讲的很多知识:torch.rand生成了图像的模拟像素;torch.rand(1)生成了翻转判断;torch.randint生成了遮挡区域的位置。整个过程没有使用任何复杂结构,但随机张量的价值体现得淋漓尽致。

如果你要扩展成批量操作,可以用torch.stack把多个增强后的张量堆叠成一个batch,或者把torch.rand(batch, C, H, W)直接应用于所有样本,利用广播机制一次性生成不同样本的mask。这样就把单张增强变成了批量增强,训练效率会高很多。

我个人在实际使用中的体会是:创建张量这件事看似基础,但它决定了数据流的形状、精度和执行设备,也决定了你的随机性从哪里来。每当你觉得“为什么我的结果不对”时,先回头检查你创建张量的那一行代码,往往就能发现问题。尤其是随机张量,别看它只是一串数值,背后的分布选择、种子设置和内存共享,都是一本值得细读的账。希望这篇文章能帮你少踩几个坑,也欢迎在实操中多试几次不同的初始化方式,你会慢慢形成自己的使用手感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询