开始接触深度学习时我最常做的事情不是训练模型而是在终端里反复创建张量。当时总觉得“创建张量”不过是一行API调用直到后面做项目时踩了共享内存和随机种子的坑才发现这个基础操作里藏着一堆值得掰扯清楚的细节。这篇文章就围绕张量和随机张量的创建把从零开始正确创建一个张量的完整思考链路讲透同时会带上一些我实际使用时的习惯和坑点适合刚入门深度学习、以及被张量属性搞晕的读者。看完你会发现很多“报错”其实在创建那一刻就已经埋下了伏笔。在正式创建张量之前我强烈建议先花两分钟梳理一下张量的三个底层属性形状、数据类型、存储设备。这三个属性决定了你用哪一行创建代码最合适也决定了后续操作是丝滑还是疯狂报错。我会把这三个属性串起来讲然后进入具体的创建方法最后用一个小项目演示随机张量在数据增强里的实际价值。1. 张量创建前的准备工作与核心概念1.1 张量到底是什么一个普通数组的升级版张量Tensor这个概念听起来高大上但它本质上就是“多维数组”的学术版。一个一维张量就是一个向量比如存储一组温度读数一个二维张量就是一个矩阵适合存放一张灰度图像的像素三维张量可以表示多个图像堆叠或者一个序列数据比如一句话的词向量序列。真正的关键在于张量除了数据本身还捆绑了很多“元信息”你可以把它理解成快递包裹箱子里是数据数值箱子上贴着标签形状、类型、设备。只关注箱子里的东西而不看标签后面的运算就很容易出问题。举个例子同样是数值1int64和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上训练你可以在创建时直接指定devicecuda或者创建后调用.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张量常用来做矩阵乘法里的单位量测试或者在代码里临时占位。这两个接口都支持额外的参数比如dtypetorch.float64、devicecuda。它们最实用的地方是接收一个形状元组torch.zeros((2, 3, 4))。注意圆括号内是一个形状元组等价于torch.zeros(2, 3, 4)。真正让新手失手的是torch.empty(3, 4)。从名字看像是“创建空张量”但这里的“空”指的是“只分配内存空间不清理里面的数据”所以内容完全依赖内存的当前状态可能是任意垃圾值。我见过有人在empty后忘记赋值直接把“随机垃圾值”当成了模型输入结果训练曲线疯狂震荡。如果你非常清楚自己后续会逐元素填充比如配合copy_()或normal_()方法那empty性能会有优势否则老老实实使用zeros安全第一。zeros torch.zeros(2, 2, dtypetorch.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附近标准差约13.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设为Truetorch.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 False4. 实操中常见问题与避坑指南4.1 随机种子没有生效的排查思路我经常收到类似“我明明写了torch.manual_seed为什么每次生成的随机张量还是不一样”的求助。这种问题99%出在下面几个环节。第一个环节是种子设置的位置不对。torch.manual_seed只影响设置之后创建的随机张量如果你在调用torch.randn之后才设置种子那当然不起作用。正确的做法是在所有随机操作之前设置种子。第二个环节是多个随机源混用。比如你用了NumPy生成随机索引又用PyTorch生成随机增强参数两边都依赖随机数但你只设置了torch.manual_seedNumPy那边没设每次运行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]) # 也变成了 100view只是改变了张量的“观察方式”它不复制数据。所以当你修改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)然后创建张量时显式传入devicedevice比如torch.randn(3, 3, devicedevice)。这样写虽然啰嗦但避免了后续到处调用.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_prob0.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:topcover_h, left:leftcover_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。这样就把单张增强变成了批量增强训练效率会高很多。我个人在实际使用中的体会是创建张量这件事看似基础但它决定了数据流的形状、精度和执行设备也决定了你的随机性从哪里来。每当你觉得“为什么我的结果不对”时先回头检查你创建张量的那一行代码往往就能发现问题。尤其是随机张量别看它只是一串数值背后的分布选择、种子设置和内存共享都是一本值得细读的账。希望这篇文章能帮你少踩几个坑也欢迎在实操中多试几次不同的初始化方式你会慢慢形成自己的使用手感。