我对以下代码段中的方法view()
感到困惑。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2,2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16*5*5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
我的困惑在于以下几行。
x = x.view(-1, 16*5*5)
tensor.view()
函数有什么作用?我已经在许多地方看到了它的用法,但我无法理解它如何解释它的参数。
如果我将负值作为参数提供给view()
函数,会发生什么?例如,如果我打电话tensor_variable.view(1, 1, -1)
会怎么样?
有人可以用一些例子解释view()
函数的主要原理吗?
答案 0 :(得分:164)
视图功能旨在重塑张量。
说你有一个张量
import torch
a = torch.range(1, 16)
a
是一个张量,有16个元素,从1到16(包括在内)。如果你想重塑这个张量使其成为4 x 4
张量,那么你可以使用
a = a.view(4, 4)
现在a
将是4 x 4
张量。 请注意,重新整形后,元素总数需要保持不变。将张量a
重新定义为3 x 5
张量是不合适的。
如果有任何情况你不知道你想要多少行,但确定列数,那么你可以用-1指定它。 (请注意,您可以将其扩展到具有更多尺寸的张量。只有一个轴值可以是-1 )。这是告诉库的一种方式:“给我一个具有这么多列的张量,并计算实现这一点所需的适当行数”。
这可以在您上面给出的神经网络代码中看到。在前向功能中的行x = self.pool(F.relu(self.conv2(x)))
之后,您将拥有一个16深度的要素图。您必须将其展平以将其提供给完全连接的图层。因此,您告诉pytorch重新构造您获得的具有特定列数的张量,并告诉它自己决定行数。
在numpy和pytorch之间绘制相似性,view
类似于numpy的reshape函数。
答案 1 :(得分:20)
让我们举一些例子,从简单到困难。
view
方法返回张量与self
张量相同的数据(这意味着返回的张量具有相同数量的元素),但形状不同。例如:
a = torch.arange(1, 17) # a's shape is (16,)
a.view(4, 4) # output below
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
[torch.FloatTensor of size 4x4]
a.view(2, 2, 4) # output below
(0 ,.,.) =
1 2 3 4
5 6 7 8
(1 ,.,.) =
9 10 11 12
13 14 15 16
[torch.FloatTensor of size 2x2x4]
假设-1
不是其中一个参数,当您将它们相乘时,结果必须等于张量中的元素数。如果您执行:a.view(3, 3)
,则会引发RuntimeError
,因为对于具有16个元素的输入,形状(3 x 3)无效。换句话说:3 x 3不等于16但是9。
您可以使用-1
作为传递给函数的参数之一,但只能使用一次。所有发生的事情是该方法将为您填写关于如何填充该维度的数学。例如,a.view(2, -1, 4)
相当于a.view(2, 2, 4)
。 [16 /(2 x 4)= 2]
请注意,返回的张量共享相同的数据。如果您在"视图"中进行了更改你正在改变原始张量的数据:
b = a.view(4, 4)
b[0, 2] = 2
a[2] == 3.0
False
现在,对于更复杂的用例。文档说每个新的视图维度必须是原始维度的子空间,或者只是满足 d,d + 1,...,d + k 的范围以下连续性条件,对于所有 i = 0,...,k - 1,stride [i] = stride [i + 1] x size [i + 1] < / strong>即可。否则,在查看张量之前需要调用contiguous()
。例如:
a = torch.rand(5, 4, 3, 2) # size (5, 4, 3, 2)
a_t = a.permute(0, 2, 3, 1) # size (5, 3, 2, 4)
# The commented line below will raise a RuntimeError, because one dimension
# spans across two contiguous subspaces
# a_t.view(-1, 4)
# instead do:
a_t.contiguous().view(-1, 4)
# To see why the first one does not work and the second does,
# compare a.stride() and a_t.stride()
a.stride() # (24, 6, 2, 1)
a_t.stride() # (24, 2, 1, 6)
请注意,对于a_t
, stride [0]!= stride [1] x size [1] ,因为 24! = 2 x 3
答案 2 :(得分:13)
view()
通过将张量的元素“拉伸”或“挤压”到您指定的形状来重塑它:
view()
是如何工作的?首先让我们看看引擎盖下的张量是什么:
在这里您看到 PyTorch 通过添加 shape
和 stride
属性将底层连续内存块转换为类似矩阵的对象来生成张量:
shape
表示每个维度的长度stride
表示在到达每个维度中的下一个元素之前需要在内存中执行多少步骤view(dim1,dim2,...)
返回具有相同基础信息的 view,但重新整形为形状为 dim1 x dim2 x ...
的张量(通过修改 shape
和 {{1} } 属性).
请注意,这隐含地假设新维度大小的乘积等于原始维度大小的乘积(即旧张量和新张量具有相同数量的值)。
stride
是 PyTorch 的别名,用于“在其他维度都已指定的情况下推断此维度”(即原始产品与新产品的商)。这是取自 numpy.reshape()
的约定。
因此,我们示例中的 -1
相当于 t1.view(3,2)
或 t1.view(3,-1)
。
答案 3 :(得分:2)
让我们尝试通过以下示例了解视图:
a=torch.range(1,16)
print(a)
tensor([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14.,
15., 16.])
print(a.view(-1,2))
tensor([[ 1., 2.],
[ 3., 4.],
[ 5., 6.],
[ 7., 8.],
[ 9., 10.],
[11., 12.],
[13., 14.],
[15., 16.]])
print(a.view(2,-1,4)) #3d tensor
tensor([[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.]],
[[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]])
print(a.view(2,-1,2))
tensor([[[ 1., 2.],
[ 3., 4.],
[ 5., 6.],
[ 7., 8.]],
[[ 9., 10.],
[11., 12.],
[13., 14.],
[15., 16.]]])
print(a.view(4,-1,2))
tensor([[[ 1., 2.],
[ 3., 4.]],
[[ 5., 6.],
[ 7., 8.]],
[[ 9., 10.],
[11., 12.]],
[[13., 14.],
[15., 16.]]])
-1作为自变量值是一种计算say x的值的简便方法,只要我们知道y,z的值,或者在3d情况下反之亦然,对于2d来说,这又是一种计算say值的简便方法x,前提是我们知道y的值,反之亦然。
答案 4 :(得分:0)
我发现x.view(-1, 16 * 5 * 5)
等效于x.flatten(1)
,其中参数1表示扁平化过程从第1维开始(而不是“样本”维)
如您所见,后一种用法在语义上更加清晰并且易于使用,因此我更喜欢flatten()
。
答案 5 :(得分:0)
weights.reshape(a, b)
将返回一个新的张量,该张量的数据与权重为(a,b)的权重相同,因为它会将数据复制到内存的另一部分。
weights.resize_(a, b)
返回具有不同形状的相同张量。但是,如果新形状导致的元素数量少于原始张量,则某些元素将从张量中删除(但不会从内存中删除)。如果新形状导致的元素数量多于原始张量,则新元素将在内存中未初始化。
weights.view(a, b)
将返回一个新的张量,该张量具有与大小(a,b)的权重相同的数据
答案 6 :(得分:0)
参数-1是什么意思?
您可以将-1
读为动态参数或“任何内容”。因此,-1
中只能有一个参数view()
。
如果您询问x.view(-1,1)
,则将根据[anything, 1]
中元素的数量输出张量形状x
。例如:
import torch
x = torch.tensor([1, 2, 3, 4])
print(x,x.shape)
print("...")
print(x.view(-1,1), x.view(-1,1).shape)
print(x.view(1,-1), x.view(1,-1).shape)
将输出:
tensor([1, 2, 3, 4]) torch.Size([4])
...
tensor([[1],
[2],
[3],
[4]]) torch.Size([4, 1])
tensor([[1, 2, 3, 4]]) torch.Size([1, 4])
答案 7 :(得分:0)
torch.Tensor.view()
简单地说,受torch.Tensor.view()
或numpy.ndarray.reshape()
启发的numpy.reshape()
会创建张量的新视图,只要新形状兼容具有原始张量的形状。
让我们通过一个具体的例子来详细了解这一点。
In [43]: t = torch.arange(18)
In [44]: t
Out[44]:
tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
使用形状为t
的张量(18,)
,可以仅为以下形状创建新的视图:
(1, 18)
或等效的 (1, -1)
或 (-1, 18)
(2, 9)
或等效的 (2, -1)
或 (-1, 9)
(3, 6)
或等效的 (3, -1)
或 (-1, 6)
(6, 3)
或等效的 (6, -1)
或 (-1, 3)
(9, 2)
或等效的 (9, -1)
或 (-1, 2)
(18, 1)
或等效的 (18, -1)
或 (-1, 1)
从上面的形状元组中我们已经可以观察到,形状元组的元素的乘法(例如2*9
,3*6
等)必须总是相等到原始张量中的元素总数(在我们的示例中为18
)。
要观察的另一件事是,我们在每个形状元组的一个位置使用了-1
。通过使用-1
,我们很懒惰地自己进行计算,而是将任务委托给PyTorch在创建新的 view 时为形状进行该值的计算。需要注意的一件事是,我们只能在形状元组中仅使用单个-1
。其余值应由我们明确提供。其他PyTorch会抛出RuntimeError
来投诉:
RuntimeError:只能推断一个维度
因此,对于上述所有形状,PyTorch将始终返回原始张量t
的新视图。这基本上意味着,它只是更改了所请求的每个新视图的张量的步幅信息。
下面是一些示例,说明每个新的 view 中张量的步幅如何变化。
# stride of our original tensor `t`
In [53]: t.stride()
Out[53]: (1,)
现在,我们将看到新的 views 的大步前进:
# shape (1, 18)
In [54]: t1 = t.view(1, -1)
# stride tensor `t1` with shape (1, 18)
In [55]: t1.stride()
Out[55]: (18, 1)
# shape (2, 9)
In [56]: t2 = t.view(2, -1)
# stride of tensor `t2` with shape (2, 9)
In [57]: t2.stride()
Out[57]: (9, 1)
# shape (3, 6)
In [59]: t3 = t.view(3, -1)
# stride of tensor `t3` with shape (3, 6)
In [60]: t3.stride()
Out[60]: (6, 1)
# shape (6, 3)
In [62]: t4 = t.view(6,-1)
# stride of tensor `t4` with shape (6, 3)
In [63]: t4.stride()
Out[63]: (3, 1)
# shape (9, 2)
In [65]: t5 = t.view(9, -1)
# stride of tensor `t5` with shape (9, 2)
In [66]: t5.stride()
Out[66]: (2, 1)
# shape (18, 1)
In [68]: t6 = t.view(18, -1)
# stride of tensor `t6` with shape (18, 1)
In [69]: t6.stride()
Out[69]: (1, 1)
这就是view()
函数的魔力。只要新的 view 的形状与原始形状兼容,它只会更改每个新的 views 的(原始)张量的步幅。
从跨步元组可能会观察到的另一件有趣的事情是,第0 位置的元素值等于第1 st 形状元组的位置。
In [74]: t3.shape
Out[74]: torch.Size([3, 6])
|
In [75]: t3.stride() |
Out[75]: (6, 1) |
|_____________|
这是因为:
In [76]: t3
Out[76]:
tensor([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17]])
步幅(6, 1)
表示,要沿第0 维从一个元素跳到下一元素,我们必须 jump 或采取6个步骤。 (即,从0
到6
,必须执行6步。)但是,要从1 st 维度中的一个元素转到下一个元素,我们只是只需一步(例如,从2
到3
)。
因此,步幅信息是如何从内存访问元素以执行计算的核心。
此函数将返回一个 view ,并且与新torch.Tensor.view()
相同,只要新形状与原始张量的形状兼容即可。否则,它将返回一个副本。
但是,torch.reshape()
的音符警告:
连续的输入和具有兼容步幅的输入可以在不复制的情况下进行重塑,但其中一个不应依赖于复制与查看行为。
答案 8 :(得分:0)
我真的很喜欢@Jadiel de Armas的例子。
我想对.view(...)的元素排序方式有一个小见解