动手学深度学习笔记-tensor学习
torch.tensor在pytorch的底层实现中分成两部分, 分别是数据的存储区(Storage)和元数据(Metadata)两部分
在存储区中存储的是内存连续的数据
在元数据中存储的是如何读取数据
Tensor 对象(也就是我们平时操作的 x)其实只是一个壳(Wrapper),或者说是一个视图(View)。它里面没有存那 12 个数字,而是存了一堆元数据,用来告诉 CPU 如何从 Storage 里找数字。
Metadata 主要包含三个核心属性(并非真正可以修改的属性, 只是用于理解):
- Size (Shape): 张量的形状(比如
(2, 3))。 - Storage Offset: 数据从 Storage 的第几个位置开始读(切片时很有用)。
- Stride (步长): 这是最核心的魔法!
Stride 是一个元组,它告诉计算机:“在这个维度上,如果要移动到下一个元素,我在 Storage(那一维数组)里需要跳过几个坑?”
我们来看那个公式,假设我们要访问 x[i][j] (第 i 行,第 j 列):
内存地址=基地址+Offset+i×Stride[0]+j×Stride[1]\text{内存地址} = \text{基地址} + \text{Offset} + i \times \text{Stride}[0] + j \times \text{Stride}[1]内存地址=基地址+Offset+i×Stride[0]+j×Stride[1]
tensor基础属性总结
dtype张量数据类型shape张量维度
tensor常用方法总结
获取tensor的基础信息:
tensor_name.size()tensor_name.numel()
初始化tensor:
torch.arange(int)torch.tensor(list)torch.randn(list)torch.ones(list)torch.zeros(list)torch.ones_like(tensor)torch.rand_like(tensor)torch.tensor(data)torch.as_tensor(data)torch.from_numpy(data)
操作tensor:
tensor_name.view(int, ..., int)tensor_name.reshape(int, ..., int)tensor_name.contiguous()tensor_name.unsqueeze()tensor_name.squeeze(int)tensor_name.transpose(int, int)tensor_name.permute(int, ..., int)torch.where(条件tensor, 条件为true的tensor, 条件为false的tensor)tensor_name.masked_fill(mask tensor, int)tensor_name.masked_fill_(mask tensor, int)原地修改
tensor常见场景总结
2.1.1 创建tensor
加一个场景, 取出tensor里面的数据, 使用.item()方法
import torch
x = torch.arange(12).reshape(3, 4)
print(x[0, 0]) # 输出的是一个tensor tensor([0])
print(x[0, 0] == 0) # 输出的还是一个tensor tensor(True)
print(x[0, 0].item()) #输出的是一个int的0
print(x[0, 0].item() == 0) # 输出的是一个bool的true
print(x.item()) # 会报错 .item()方法只能对其中一个元素使用
创建tensor部分还有一个大坑, torch.arange()生成的tensor的数据类型是tensor.int64的, 而torch.ones和torch.zeros这样的生成的是torch.float32的数据类型, 不同的数据类型的张量进行计算的时候可能会出现问题(比如torch.dot(x, y)), 所以可以使用torch.arange(num, dtype=torch.float)来规避这个问题, 还有就是指定dtype=torch.float和dtype=torch.float32的效果是一样的
直接使用torch.tensor([[1, 2], [3, 4]]) 创建tensor的时候数据类型也是int64的
使用torch.ones_like(x)的结果的数据类型是跟随x的数据类型的
import torch
## 创建tensor
# 1. 从 Python 列表创建 (最直观)(注意会拷贝数据, 深拷贝)
t1 = torch.tensor([[1, 2], [3, 4]])
# 2. 创建全 0 或全 1 的 Tensor (用来占位)
t_zeros = torch.zeros(2, 3) # 2行3列的全0矩阵
t_ones = torch.ones(2, 3) # 2行3列的全1矩阵
# 3. 创建随机数 (初始化模型参数时常用)
t_rand = torch.randn(3, 4) # 正态分布(均值0,方差1)
# 4. 创建像别人一样的 Tensor (形状一样)
x = torch.zeros(2, 3)
y = torch.ones_like(x) # y 也是 2行3
y = torch.zeros_like(x)
y = torch.ones_like(x)
y = torch.rand_like(x)
y = torch.empty_like(x) # 直接征用一块内存, 速度最快, 不删除原始内存的信息
# 5. 创建等差数列的tensor
t_arange = torch.arange(12)
# 类似于之前python本身的range()函数, 但是python自带的range函数返回的是一个迭代器
# torch.arange()方法返回的是一个tensor对象, tensor对象为了加速操作是占据一个连续空间的内存块
2.1.2 形状操作
a 改变形状(reshape 和 view)
不管是reshape还是view必须保证操作前后的元素数量是一样的
view方法是必须保证当前的tensor的内存是连续的才能使用view, 如果把连续内存的矩阵进行转置, 或者从中间进行切片, 就会导致内存的不连续, 就无法使用view操作
reshape是更智能的修改, 如果当前tensor的内存是连续的就使用view, 否则会先整理内存再进行view
可以使用**-1**作为其中一个维度的占位符, 自动计算这个维度的大小
import torch
x = torch.arange(12) # 新建一个tensor
a = x.reshape(3, -1) # 变成 3行4列
print(a.shape)
a = x.view(4, -1) # 变成 4行3列
print(a.size())
a = a.transpose(1, 0) # 转置成(3, 4)
print(a.size())
a = a.contiguous().view(4, -1) # 必须使用.contiguous()
print(a.size())
b 增加/减少维度 (Squeeze/Unsqueeze) - 以后处理图像批次(Batch)时非常重要
import torch
a = torch.arange(24).view(1, 1, 2, 3, 4)
print(a.size()) # torch.Size([1, 1, 2, 3, 4])
print(a)
a = a.squeeze()
print(a.size()) # 去掉全部维度是1的维度 torch.Size([2, 3, 4])
print(a)
a = a.unsqueeze(0) # 在0维之前加入维度是1的一个维度
print(a.size()) # torch.Size([1, 2, 3, 4])
a = a.unsqueeze(2) # 在2维之前加入维度是1的一个维度
print(a.size()) #torch.Size([1, 2, 1, 3, 4])
c 转置(transpose)与维度重排(permute)
转置其实是直接交换shape和stride的位置
对于2d的tensor来说可以直接使用.t()方法或者.T属性来实现转置, 相当于在元数据中直接修改shape和stride
要注意: .t()方法和.T属性只适用于2d的tensor
或者使用.transpose(0, 1)交换两个维度, 或者使用.permute(1, 0)进行维度重排
对于下面的例子来说, 现在的shape = (3, 4), stride = (4, 1)转置之后, shape = (4, 3), stride = (1, 4), 以下三种方法都是实现了一样的效果
import torch
c = torch.arange(3*4).reshape(3, 4)
print(c)
# 以下的这些结果都是一样的
c_t = c.transpose(0, 1)
print(c_t)
c_t = c.T
print(c_t)
c_t = c.t()
print(c_t)
c_t = c.permute(1, 0)
print(c_t)
对于更高维来说, transpose实际上也是交换对应两个维度的shape和stride, premute是按照重排规则重排shape和stride
import torch
c = torch.arange(2*3*4).reshape(2, 3, 4)
print(c.stride(), c.shape) # (12, 4, 1) torch.Size([2, 3, 4])
c_t = c.transpose(0, 1)
print(c_t.stride(), c_t.shape) # (4, 12, 1) torch.Size([3, 2, 4])
c_t = c.transpose(1, 2)
print(c_t.stride(), c_t.shape) # (12, 1, 4) torch.Size([2, 4, 3])
c_t = c.permute(1, 0, 2)
print(c_t.stride(), c_t.shape) # (4, 12, 1) torch.Size([3, 2, 4])
c_t = c.permute(2, 1, 0)
print(c_t.stride(), c_t.shape) # (1, 4, 12) torch.Size([4, 3, 2])
d 切片
要注意切片之后的内存是否是连续的
import torch
d = torch.arange(12).view(3, 4)
d_cut = d[0:2, :] # 这样就是取前两行, 在内存中是相邻的可以直接.view(), 注意是左闭右开
print(d_cut)
d_cut = d[:, 0:2] # 这样子就是取前两列, 在内存中是不相邻的, 相当于每隔两个数据取了两个数据, 在view之前就需要.contiguous()
print(d_cut)
# 还有按照步长进行切片 是start:end:step
d_cut_step = d[:, ::2] # 这样就是隔一个点取一个数据
print(d_cut_step)
# 无敌的省略号, 对于一个很高维度的tensor, 只想对最后一个维度进行操作的时候就可以用省略号忽略前面的维度
d = torch.arange(2*3*4).view(2, 3, 4)
d_cut = d[..., ::2]
e 掩码切片
想要实现在一个tensor中选择大于某个阈值的数有下面这两种方法
a. 直接用元素级运算算出二值张量, 再使用元素级乘法进行掩码
b. 使用tensor.where(条件, 条件为true时的值, 条件为false时的值), 注意, 这里可以使用广播机制
c. 使用.masked_fill(条件, 条件为true的值) , 注意, 按照mask = d > 0.5这样的掩码要使用~mask取反才行
import torch
# 掩码切片
# 方法a
d = torch.randn(3, 4)
mask = d > 0.5 # 会直接生成一个同样大小的true和false的tensor
print(mask)
selected = d[mask] # 会按照mask的true和false选择该选择的tensor, 请注意, 生成的selected是一个1d的tensor, 把其中没有置零的全部排成一排
print(selected)
selected = d * mask # 这样子会按照原来的样子进行掩码
print(selected)
# 方法b 或者使用torch.where方法, torch.where(条件, 条件true选择的张量, 条件false选择的张量)
selected = torch.where(mask, d, torch.zeros_like(d))
print(selected)
# 甚至可以修改第三个参数把条件false的改成别的数值
selected = torch.where(mask, d, torch.tensor(-999)) # 注意这里也用了广播机制进行拓展
print(selected)
# 方法c
selected = d.masked_fill(~mask, 0)
print(selected)
# 或者如果你想原地修改 d (节省内存)
d.masked_fill_(~mask, 0)
2.1.3 数学计算
3.1元素级运算 (Element-wise) - 对应位置算对应位置
import torch
x = torch.tensor([1, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
print(x + y) # [3, 4, 6, 10]
print(x * y) # [2, 4, 8, 16]
print(x ** y) # 求幂
3.2广播机制
在元素级运算的时候会出现广播机制的运用, 在其中一个tensor a没有的维度(或者tensor a在这个维度上是1), 另一个tensor b有的话, 会复制tensor a到与tensor b对齐
广播机制遵循 “从右向左匹配” 规则。两个维度必须满足以下条件之一才能广播:
- 维度大小相等。
- 其中一个维度大小为 1。 这里
3==3(匹配),但2!=4且都不为 1,所以报错。如果 x 是(1, 3)或者是(3,),就可以广播。
import torch
x = torch.arange(3)
y = torch.arange(12).reshape(4, 3)
print(x + y) # 可以运行, 会把x复制成(4, 3)的形状
x = torch.arange(6).reshape(2, 3)
y = torch.arange(12).reshape(4, 3)
print(x + y) # 无法运行, 因为不知道如何复制x
3.3内存管理
在元素级运算的时候会出现创建新内存的情况
比如说y = x + y这样的操作, 就是按照tensor x y的大小, 重新创建一个内存空间, 将结果y链接到这个新的内存空间
尽量采用不修改内存地址的方法进行运算
import torch
x = torch.arange(3)
y = torch.arange(12).reshape(4, 3)
print(id(y)) # 139760592193792
y = x + y
print(id(y)) # 139760592195792 改变内存地址
y += y
print(id(y)) # 139760592195792 没变
y[:] = x + y
print(id(y)) # 139760592195792 没变
3.4矩阵乘法 (Matrix Multiplication) - 深度学习的核心!
import torch
X = torch.arange(12).reshape(3, 4)
Y = torch.arange(4).reshape(4, 1)
# 矩阵乘法用 @ 符号 或者 torch.matmul
result = X @ Y
# 矩阵乘法: @ 或 torch.matmul (支持自动广播,处理高维 Tensor 时很智能)。
# 严格的二维矩阵乘法: torch.mm (只允许 2D vs 2D,不允许广播,调试时用来确保维度严格正确很有用)。
3.5 tensor数据类型转换
tensor的数据类型有bool < int < float
两个tensor进行张量计算的时候, 会自动进行类型转换把dtype更小的转换为dtype更大的数据类型进行计算
import torch
x = torch.randn(12).view(3, 4)
print(x.dtype) # torch.float32
y = torch.arange(12).view(3, 4)
print(y.dtype) # torch.int64
z = x > 0.5
print(z.dtype) # torch.bool
xy = x + y
print(xy.dtype) # torch.float32
print(xy)
yz = y + z
print(yz.dtype) # torch.int64
print(yz)
也有如下两种方法进行类型转换
a. .to()方法
最推荐使用**.to()方法**强制进行类型转换, 最标准, 并且不仅可以转换数据类型还可以转换设备
import torch
x = torch.tensor([1, 2, 3]) # 默认 int64
print(x.dtype)
# 转成 float32
y = x.to(torch.float32)
print(y.dtype)
# 同时转类型和设备
z = x.to(dtype=torch.float32, device='cuda:0')
print(z.dtype)
b.使用快捷函数
pytorch可以直接使用tensor.数据类型()
.float() -> 转成 float32
.long() -> 转成 int64 (做索引时常用)
.int() -> 转成 int32
.double() -> 转成 float64
.bool() -> 转成 Bool (True/False)
import torch
x = torch.tensor([-1, 0, 1, 2, 3]) # 默认 int64
print(x.dtype)
y = x.float()
print(y.dtype)
y = x.int()
print(y.dtype)
y = x.bool()
print(y) # 只有0的地方是false tensor([ True, False, True, True, True])
print(y.dtype)
2.1.4 与numpy相互转换
- Numpy Array (ndarray) 和 PyTorch Tensor 在底层的设计逻辑几乎是一模一样的:
- 结构: 都是由 一块连续的内存(数据实体) + 头信息(元数据:形状、步长、数据类型) 组成的。
- 连续性:
- 初始创建时: 通常是连续的(Contiguous)。
- 变形后: 就像你在笔记里学到的,如果你对一个 NumPy 数组或 Tensor 进行了转置 (
transpose) 或跨步切片,它们并不会复制数据,而是修改步长(Stride)。此时,逻辑上它们变形了,但在物理内存中,数据依然是原来的样子。因此,它们变成了**非连续(Non-contiguous)**的。 - 后果: 某些特定的底层计算库(如 BLAS, cuDNN)要求内存必须连续才能全速运行。所以,有时需要调用
.contiguous()(PyTorch) 或np.ascontiguousarray()(NumPy) 来重新整理内存。
| 特性 | Numpy Array (numpy) | PyTorch Tensor (torch) |
|---|---|---|
| 主要用途 | 通用科学计算、数据分析、传统机器学习 (scikit-learn) | 深度学习、神经网络训练 |
| 硬件支持 | 仅 CPU | CPU + GPU (CUDA) + TPU |
| 自动求导 | 不支持 (只存数值) | 支持 (autograd),记录计算图,用于反向传播 |
| 内存共享 | 与 PyTorch CPU Tensor 互通 | 与 Numpy 互通 (零拷贝) |
| 生态系统 | 极其成熟,是 Python 数据科学的基石 | 深度学习领域的霸主 |
总结: Tensor 可以看作是 “支持 GPU 加速并带有自动求导功能的 Numpy 数组”。
代码实践
转换时最大的坑在于 GPU 和 梯度(Gradient)。
A. Numpy 转 Tensor
import torch
import numpy as np
np_data = np.array([[1, 2], [3, 4]])
# 方法 1: 共享内存 (推荐,速度快)
# 修改 tensor 会影响 numpy,反之亦然
tensor_share = torch.from_numpy(np_data)
# 方法 2: 深拷贝 (数据复制)
# 安全,互不影响,但多占一份内存
tensor_copy = torch.tensor(np_data)
print(tensor_share.type()) # 默认继承 numpy 的数据类型 (通常是 Double/Float64)
# 注意:神经网络通常用 Float32,所以进网络前常接 .float()
tensor_final = torch.from_numpy(np_data).float()
B. Tensor 转 Numpy
核心规则: Numpy 不支持 GPU,也不懂什么是梯度图。所以转换前必须 (1) 移到 CPU,(2) 剥离梯度。
import torch
# 假设这是一个在 GPU 上并且带有梯度的 Tensor (例如网络输出)
tensor_gpu = torch.randn(2, 2, device='cuda', requires_grad=True)
# 错误示范 (会报错)
# np_arr = tensor_gpu.numpy()
# 正确示范 (标准三步走)
# 1. .cpu() -> 把数据搬回内存
# 2. .detach() -> 切断反向传播的线(不再计算梯度)
# 3. .numpy() -> 转换为数组
np_arr = tensor_gpu.cpu().detach().numpy()
在实际的深度学习项目(如使用 PyTorch)中,两者有着明确的分工:
阶段 1:数据预处理 (Data Preprocessing) -> 主场:NumPy / Pandas
- 当你从硬盘读取图片 (OpenCV, PIL)、读取 CSV 表格 (Pandas) 或音频时,这些库通常返回 NumPy 数组。
- 做数据增强(旋转、裁剪)、归一化等操作时,NumPy 或基于 NumPy 的库(如
albumentations)非常方便。
阶段 2:数据加载 (Data Loading) -> 边界:Dataset & DataLoader
- 在 PyTorch 的
Dataset类中,__getitem__方法通常接收 NumPy 数据,并在返回时将其转换为 Tensor。 DataLoader会把这些 Tensor 批量打包(Batching)。
阶段 3:模型训练 (Model Training) -> 主场:Tensor
- 绝对统治区。 一旦数据进入神经网络(
model(input)),必须全是 Tensor。 - 因为只有 Tensor 能被送到显卡(
.to('cuda'))上进行矩阵加速,也只有 Tensor 能通过loss.backward()计算梯度来更新权重。
阶段 4:评估与指标计算 (Evaluation) -> 回归:NumPy
- 模型输出结果后,我们通常需要计算准确率、绘制混淆矩阵或画出 Loss 曲线。
- 由于
matplotlib(画图)和scikit-learn(算指标)只认 NumPy,我们需要把模型的预测结果(Tensor)用.cpu().detach().numpy()转回 NumPy 格式再处理。
更多推荐
所有评论(0)