pytorch

1.tensor

pytorch的tensor与numpy中的多维数组是类似的,不过,Tensor还提供了GPU计算和自动求梯度等更多功能。

创建tensor

(1)直接创建未初始化的tensor

x=torch.empty(5,3)

(2)随机创建,从[0,1)均匀分布中抽取随机数

x=torch.rand(5,3)

(3)创建全0

x=torch.zeros(5,3,dtype=long)

(4)根据list创建tensor

tensor = torch.as_tensor(my_list)
#不复制数据,共享list的内存

(5)numpy创建tensor

np_array = np.array(my_list)
tensor = torch.from_numpy(np_array)
#也是共享内存,即修改tensor也会修改np_array

(6)torch.tensor()

x = torch.tensor([5.5,3])
#创建一个新的tensor,将data复制过去
#参数既可以是list,也可以是array

tensor的操作

(1)加法

z=x+y
z=torch.add(x,y)
y.add_(x)

(2)乘法

遵循矩阵乘法

z=torch.matmul(x,y)
z=x@y
z=torch.dot(x,y) #仅使用与1维张量

逐元素乘法

z=x.mul(y)
z=x*y

(3)获取形状

a=tensor.shape
a=tensor.size()

(4)改变形状

tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
print("原始张量:\n", tensor)
reshaped = tensor.view(3, 2)  # 改变张量形状为 3x2
print("改变形状后的张量:\n", reshaped)
flattened = tensor.flatten()  # 将张量展平成一维
print("展平后的张量:\n", flattened)
#view也可以用来改变形状
b=a.view(2,3) #将a形状改变为2*3

(5)索引操作

PyTorch 的 tensor 索引规则 基本 和 NumPy 一样,大多数索引方式都可以直接通用

#切片
a[:2, :1, :, :] 
# 在第一个维度上取0和1,在第二个维度上取0,
# 等同于取第一、第二张图片中的第一个通道
t[0:2, 1:]
#t是2维张量,这里是取t的0,1行的第2列及以后的所有列
print(b[..., 1])
#...可以省略所有剩余的维度,等价于取张量最后一个维度

(6)其他操作

torch.sum(x) #求张量x各个元素之和
torch.mean(x) #求均值
torch.min(x) #求最小值
#这些都和numpy中数组的操作类似

storage()和stride()

pytorch中的一个tensor分为头信息区(Tensor)和存储(Storage)
信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息。
而真正的数据则保存成连续数组,存储在存储区。
不同tensor有着不同的Tensor信息区,但可以共享同一块存储区

stride

tensor实现从索引到内存位置的映射正是因为stride
stride表示在 tensor内存布局中,每个维度的索引变化时,对应元素在底层存储中的跳步(步长)
就类似于数组一样其实
举个例子:

t = torch.tensor([[1, 2, 3], [4, 5, 6]])
#t的stride应该是(3,1)

当t的第一个维度索引+1时,内存位置就会加+3
为了找出张量的任何元素所在的位置,将每个索引与该维度的相应步幅相乘,然后将它们相加。

2.tensor的自动微分

正是由于这一点,我们在用pytorch时可以很轻松地进行反向传播,没有这一点,pytorch的tensor和numpy差异其实就不大了

(1)每个torch.Tensor对象都有一个.requires_grad属性

默认是false,当设置为true时,PyTorch会自动追踪所有对该张量的操作,并构建计算图。

(2)自动微分原理基于计算图实现

代码实例:pytorch实现一个简单的梯度下降

import torch
# 创建一个需要梯度的张量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# 定义优化器(SGD)
optimizer = torch.optim.SGD([x], lr=0.1)
# 计算目标函数
y = x @ x  # y = x^T * x
# 反向传播
y.backward()
# 🚀 x.grad 现在有梯度
print(f"梯度 x.grad: {x.grad}")
# 执行优化步骤(基于 x.grad 更新 x)
optimizer.step()
# 打印更新后的 x
print(f"更新后的 x: {x.data}")

'''
这里只是梯度更新了一次,更新后的 x: tensor([0.8000, 1.6000, 2.4000])
y.backward()相当于在求整个forward过程的dy/dx
backward之后可以得到x.grad()即x的梯度
backward之前是得不到x的梯度的
利用优化器的step,相当于x' <- x-lr*x.grad进行梯度下降
'''

tensor与numpy的转化

i.numpy转化为tensor

这是很简单的,我们在上文中也提到了,array本身是不具有什么属性的,转化为tensor是一个可以附加属性的过程,我们只需要考虑是否需要.requires_grad设为True。另外注意的是共享内存还是不共享。

ii.tensor转化为numpy

什么情况下需要这样做?比如说,我现在要把这个张量作为一个函数的参数参与计算,可惜的是这个函数的参数要求是一个array,而pytorch中又没有可以实现这一功能的函数,那就需要转化了。
然而,必须要考虑的是,tensor转化为numpy,如果不加处理可能会破坏梯度,假如原先tensor的requires_grad属性为false,那么基本没影响。但如果是true,结合上文我们知道,pytorch的自动微分原理通过计算图实现,假如直接转换,对这个tensor的梯度追踪就会被中断了。

解决办法:
1.尽可能使用pytorch中的函数
2.可以用torch中的函数自己实现对应功能的函数
3.使用detach()
但是用detach必须要清楚是否合适
detach() 方法用于返回一个新的 Tensor,这个Tensor和原来的Tensor共享相同的内存空间,但是不会被计算图所追踪(新的tensor的requires_grad为false,而原张量还是true),也就是说它不会参与反向传播,不会影响到原有的计算图
即使之后重新将它的requires_grad置为true,它也不会具有梯度grad
写个代码试一试~:

# 创建一个需要梯度的张量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# 定义优化器(SGD)
optimizer = torch.optim.SGD([x], lr=0.1)
# 计算目标函数
x_=x.detach()
y = x_ @ x_  # y = x^T * x
# 反向传播
y.backward()
# 🚀 x.grad 现在有梯度
print(f"梯度 x.grad: {x.grad}")
# 执行优化步骤(基于 x.grad 更新 x)
optimizer.step()
# 打印更新后的 x
print(f"更新后的 x: {x.data}")

这样会报错,仔细一看,我们使用了detach切断了计算图,x_的梯度是不被追踪的,我们用x_来得到y,使用backward来求dy/dx自然是不行的

# 创建一个需要梯度的张量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# 定义优化器(SGD)
optimizer = torch.optim.SGD([x], lr=0.1)
# 计算目标函数
x_=x.detach()
y = x @ x # y = x^T * x
# 反向传播
y.backward()
# 🚀 x.grad 现在有梯度
print(f"梯度 x.grad: {x.grad}")
# 执行优化步骤(基于 x.grad 更新 x)
optimizer.step()
# 打印更新后的 x
print(f"更新后的 x: {x.data}")
'''
这样就不会报错,detach并不会影响对原先张量的梯度追踪!!!
'''

那么应该如何使用?

a.假如我们要进行的这一步计算并不在梯度追踪之内

使用detach().numpy()切断计算图转化为numpy的array

x_=x.detach().numpy()
y=func(x_)
#我们不需要追踪func这一过程中的计算的梯度,因而可以这样

#对上面的例子稍加修改
# 创建一个需要梯度的张量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# 定义优化器(SGD)
optimizer = torch.optim.SGD([x], lr=0.1)
# 计算目标函数
x_=x.detach()
y_ = x_ @ x_ # y = x^T * x
# 反向传播
y=torch.tensor(y_,requires_grad=True)
y.backward()
# 打印的的结果是None
print(f"梯度 x.grad: {x.grad}")
# 执行优化步骤(基于 x.grad 更新 x)
optimizer.step()
# 打印更新后的 x
#x没有被更新
print(f"更新后的 x: {x.data}")

'''
为何x没有被更新呢?这是尽管张量y是由y_转化而来的,而x并没有直接参与对y的计算
'''

b.假如我们要进行的这一步也应该被追踪梯度

对于这种情况,我们不能使用detach()来切断计算图进而转化为numpy(),因而在这种情况下,解决方案就只有手动更新梯度或者自己重新我们要用到的函数,使之参数可以是张量。

#手动更新梯度
# 1. 创建 tensor
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# 2. 转换为 numpy 进行计算
x_np = x.detach().numpy()  # ❌ 这个步骤会丢失梯度信息
y_np = np.sin(x_np)  # NumPy 计算
y = torch.tensor(y_np, dtype=torch.float32, requires_grad=True)  # 转回 tensor
#y.retain_grad()  # 保留 y 的梯度
#这一步是因为非叶子张量(这里的叶子张量是x)默认不保存梯度,所以需要这一步来保存y的梯度

# 3. 计算梯度
y.sum().backward()  # 反向传播

# 4. 手动赋值回 x.grad(因为 x 被 detach() 了)
x.grad = y.grad * torch.cos(x)  # 因为 y = sin(x),所以 dy/dx = cos(x)

print(x.grad)  # ✅ 仍然正确计算出梯度

可见要么写一个张量可以用的forward函数,要么要自己手动计算forward过程中的梯度

3.代码示例

以我在角色动画这门课上的lab1为例,lab1的part2是需要实现一个IK算法,由于IK本身也是一种优化问题,可以基于梯度下降来实现,这里我们不妨用ptorch框架来实现


pytorch
https://fightforql.github.io/2025/03/29/pytorch/
Author
lsy
Posted on
March 29, 2025
Licensed under