首先就是最基础的部分导入必要的包
import torch
import numpy as np
from einops import rearrange
einops简介:
einops库网址:einops库
einops库的指导文档的网址:einops库指导文档
einops是一个用于张量操作的库,可以让开发者方便的对张量进行操作,现在已经支持包括但不限于numpy,tensorflow,pytorch等多种框架
einops库的优点:
1:在操作张量时使用的是具有语义性的信息,方便我们编写可靠易读的代码
假设我们现在有一个形状为(32,3,32,32)的张量x,表示这一个张量的批量为32,通道数为3,空间维为32*32,如果我们要对其将后面三个维度合成一个维度,在pytorch中我们会这样写
# 创建一个形状为(32,3, 32, 32)的张量
x = torch.randn([32, 3, 32, 32])
y1 = x.view(x.shape[0], -1)
print(y1.shape)
torch.Size([32, 3072])
上面的代码在语义方面是不清晰的,因为我们在对张量的维度进行操作时并没有写出一些关于每一个维度的语义信息,而如果采用einops中的写法则可以写作下面的样子
y2 = rearrange(x, 'b c h w -> b (c h w)')
print(y2.shape)
torch.Size([32, 3072])
在einops中的写法,我们可以清晰的看出我们对张量的每一个维度做了一个什么样的操作"b c h w -> b (c h w)", 将后三个维度合成一个维度,将语义方面的信息也写出回来了,这就方便了别人阅读我们的代码,也方便我们对自己所写代码进行检查,可能有人质疑,另外我也好奇,到这里我们只说明了使用rearrange这种einops库中的操作,队长两进行操作更加方便,但是操作之后的信息与pytroch中进行操作的信息相同吗?接下来我们就看一下这两种操作产生的数据相同吗?
验证思路:
如果使用pytorch与einops中产生的数据一样的话,那么y1与y2就相同,如果y1与y2相同,那么y1中的每一个元素都与y2中的一个对应的元素相同,有没有将y1与y2按元素进行比较的方法呢?有的==,这一个符号就可以实现两个张量之间的按元素进行比较,如果y1与y2完全一样,那么True的个数就应该等于y1中元素的个数,如何统计y1==y2中True的个数呢?.sum()方法可以轻松的统计,然后我们将数值取出与y1中总的元素个数进行比较如果输出为True则证明两种方式完全相同
print((y1==y2).sum().item()==32*3072)
True
可以看出einops中对张量进行操作与在pytroch中进行操作是一样的,确实按照我们指定的方式发生了改变。
2:方便我们进行检查,可以让我们不用写注释,注释并不会揭示出错误,在einops中可以让我们以另一种方式替代注释并防止错误的发生
在einops的官方指导文档中,是不建议我们在我们的代码中写一些关于张量形状的注释的,因为注释并不能阻止错误的发生,而是鼓励我们将在语义性的代码后面加上张量的每一个形状的大小就像下面这样
y = x.view(x.shape[0], -1) # x: (batch, 3, 32, 32)
y = rearrange(x, 'b c h w -> b (c h w)', c=3, h=32, w=32)
在上面的例子中rearrange(x, ‘b c h w -> b (c h w)’, c=3, h=32, w=32),我们写出了c,h,w的大小,这帮助我们了解张量的形状,后面的c=3, h=32, w=32起到了我们以前注释的作用,但是又不完全相同,这是因为代码中会将(32, 3, 32, 32)中的c与我们前面的3进行比较,如果不一样,说明你这里记录的张量大小在c这一个维度并不是3,你记错了,这时就会出错,程序终止,例子如下:
y = rearrange(x, 'b c h w -> b (c h w)', c=2, h=32, w=32)
---------------------------------------------------------------------------
EinopsError Traceback (most recent call last)
File D:\software\development\envs\jupyter\Lib\site-packages\einops\einops.py:412, in reduce(tensor, pattern, reduction, **axes_lengths)
411 recipe = _prepare_transformation_recipe(pattern, reduction, axes_lengths=hashable_axes_lengths)
--> 412 return _apply_recipe(recipe, tensor, reduction_type=reduction)
413 except EinopsError as e:
File D:\software\development\envs\jupyter\Lib\site-packages\einops\einops.py:235, in _apply_recipe(recipe, tensor, reduction_type)
233 backend = get_backend(tensor)
234 init_shapes, reduced_axes, axes_reordering, added_axes, final_shapes = \
--> 235 _reconstruct_from_shape(recipe, backend.shape(tensor))
236 tensor = backend.reshape(tensor, init_shapes)
File D:\software\development\envs\jupyter\Lib\site-packages\einops\einops.py:191, in _reconstruct_from_shape_uncached(self, shape)
190 if isinstance(length, int) and isinstance(known_product, int) and length != known_product:
--> 191 raise EinopsError('Shape mismatch, {} != {}'.format(length, known_product))
192 # this is enforced when recipe is created
193 # elif len(unknown_axes) > 1:
194 # raise EinopsError(
(...)
197 # )
198 else:
EinopsError: Shape mismatch, 3 != 2
During handling of the above exception, another exception occurred:
EinopsError Traceback (most recent call last)
Cell In[7], line 1
----> 1 y = rearrange(x, 'b c h w -> b (c h w)', c=2, h=32, w=32)
File D:\software\development\envs\jupyter\Lib\site-packages\einops\einops.py:483, in rearrange(tensor, pattern, **axes_lengths)
481 raise TypeError("Rearrange can't be applied to an empty list")
482 tensor = get_backend(tensor[0]).stack_on_zeroth_dimension(tensor)
--> 483 return reduce(cast(Tensor, tensor), pattern, reduction='rearrange', **axes_lengths)
File D:\software\development\envs\jupyter\Lib\site-packages\einops\einops.py:420, in reduce(tensor, pattern, reduction, **axes_lengths)
418 message += '\n Input is list. '
419 message += 'Additional info: {}.'.format(axes_lengths)
--> 420 raise EinopsError(message + '\n {}'.format(e))
EinopsError: Error while processing rearrange-reduction pattern "b c h w -> b (c h w)".
Input tensor shape: torch.Size([32, 3, 32, 32]). Additional info: {'c': 2, 'h': 32, 'w': 32}.
Shape mismatch, 3 != 2
可以看出他说形状不匹配3!=2,这有助于我们记录的张量的形状是真实的张量形状,而如果采用注释的形式如下面:
y = x.view(x.shape[0], -1) # x: (batch, 2, 32, 32)
我们可以看出这里运行是不会报错的,即使我们对于x张量的形状标记错了将实际的3错误的标记成了2,但是程序正常进行了下去,这不利于我们查找错误
3:结果确定
我们首先来了解一下什么是depth-to-space操作,depth to space操作是将一部分深度上的数据划分到空间维度,这样使得数据在深度维度上的数据量减少,使得在空间维度上的数据量增加,
这一位大佬使用浅显易懂的语言将depth-to-space操作讲解了出来:图解depth-to-space
在进行depth-to-space的操作时,我们有多种方法可以实现,举一个例子:
y1 = rearrange(x, 'b c (h h2) (w w2) -> b (c h2 w2) h w', h2=2, w2=2)
y2 = rearrange(x, 'b c (h h2) (w w2) -> b (h2 w2 c) h w', h2=2, w2=2)
y3 = rearrange(x, 'b c (h h2) (w w2) -> b (h2 c w2) h w', h2=2, w2=2)
y4 = rearrange(x, 'b c (h h2) (w w2) -> b (w2 c h2) h w', h2=2, w2=2)
print(y1.shape)
print(y2.shape)
print(y3.shape)
print(y4.shape)
torch.Size([32, 12, 16, 16])
torch.Size([32, 12, 16, 16])
torch.Size([32, 12, 16, 16])
torch.Size([32, 12, 16, 16])
上面这4种都实现了depth-to-space的转换,但是这四种方式产生的结果是不一样的,这说明使用einops对一种操作如果你写的细节不同是可以处理出不同的结果的,einops是可以按照你写的结果的细节进行处理并产生确定的结果,即einops既照顾到了细节又省去了复杂的代码
print((y1==y2).sum().item()==32*12*16*16)
print((y1==y3).sum().item()==32*12*16*16)
print((y1==y4).sum().item()==32*12*16*16)
print((y2==y3).sum().item()==32*12*16*16)
print((y2==y4).sum().item()==32*12*16*16)
print((y3==y4).sum().item()==32*12*16*16)
False
False
False
False
False
False
4:统一性,操作是在统一的模式下进行的
像下面给出的1d,2d,3d池化,在einops中的实现,与在pytorch中的实现
# 1d池化
x = torch.randn([1, 1, 10])
y = reduce(x, 'b c (x dx) -> b c x', 'max', dx=2)
print("原始数据:", x)
print("einops中的1d池化:", y)
print("pytorch中的1d最大池化:", torch.nn.MaxPool1d(kernel_size=2, stride=2)(x))
if ((y == torch.nn.MaxPool1d(kernel_size=2, stride=2)(x)).sum().item() == torch.prod(torch.tensor(y.shape)).item()):
print("两种写法产生了一样的结果")
原始数据: tensor([[[ 1.1664, 1.3495, -1.2656, 1.9663, -0.3509, -0.1467, 0.6881,
-0.6829, -2.3062, 1.0979]]])
einops中的1d池化: tensor([[[ 1.3495, 1.9663, -0.1467, 0.6881, 1.0979]]])
pytorch中的1d最大池化: tensor([[[ 1.3495, 1.9663, -0.1467, 0.6881, 1.0979]]])
两种写法产生了一样的结果
# 2d池化
x = torch.randn([1, 1, 10, 15])
y = reduce(x, 'b c (x dx) (y dy) -> b c x y', 'max', dx=2, dy=3)
print("原始数据:", x)
print("einops中的1d池化:", y)
print("pytorch中的1d最大池化:", torch.nn.MaxPool2d(kernel_size=[2, 3], stride=[2, 3])(x))
if ((y == torch.nn.MaxPool2d(kernel_size=[2, 3], stride=[2, 3])(x)).sum().item() == torch.prod(torch.tensor(y.shape)).item()):
print("两种写法产生了一样的结果")
原始数据: tensor([[[[-2.3920, -0.6831, -0.2869, -0.7498, 1.4964, -1.2216, -0.8163,
0.1859, -1.1654, -0.7483, 0.8499, 0.1841, -1.5465, -1.6600,
0.2797],
[-1.0140, 0.3107, 3.0464, 1.2541, -1.1369, 0.7394, -0.2719,
1.5636, 0.2397, 0.0394, -0.5654, -0.0345, 0.1755, 0.6835,
0.0799],
[ 1.3788, -0.8967, 1.7335, -1.5464, 0.7990, -0.3748, -0.3624,
2.2351, -0.4514, -0.6497, 1.2265, 0.4608, -0.3435, 0.1344,
-0.8932],
[ 0.0761, 0.3828, 0.7802, 0.9375, -1.9827, 1.6247, -0.9220,
-1.4104, 0.3827, 0.3351, -1.1957, -2.2622, 2.1508, -1.4895,
1.7536],
[ 0.1275, -0.6619, 2.4027, -0.5519, 0.1976, 1.1205, 0.7631,
0.6916, 0.1367, 0.6092, -0.7835, -0.4501, 0.1889, -0.0070,
0.7118],
[-0.2566, 1.1016, -0.8230, 0.6844, 0.1027, 0.5869, -0.9229,
0.3682, 0.2249, 0.8168, 0.0114, 0.9890, -1.7599, -0.3061,
-0.4512],
[ 0.2446, -0.0505, -0.3988, -0.7851, -2.0113, -0.7517, -0.0230,
-1.0032, -0.5034, 1.3380, -0.1391, -1.9457, 1.9752, 0.7266,
-0.1901],
[-1.1142, -0.8428, -0.8824, -0.4545, 1.3667, 0.3917, -0.7109,
-0.7559, 0.0117, 1.0479, -1.6300, -0.0613, -2.3666, -1.1826,
-0.9516],
[-0.8399, 1.0281, 0.4352, -0.2322, 0.2929, 1.8062, 0.1615,
0.3860, 1.6725, 1.4579, 0.2524, 0.3141, 0.1710, -0.0693,
1.1127],
[ 1.3811, 0.8230, -0.3885, -1.2562, 0.0288, -1.1839, -0.5073,
-0.4757, 1.3023, -0.9393, -0.1366, 1.1150, 0.3797, 0.6036,
-0.1412]]]])
einops中的1d池化: tensor([[[[3.0464, 1.4964, 1.5636, 0.8499, 0.6835],
[1.7335, 1.6247, 2.2351, 1.2265, 2.1508],
[2.4027, 1.1205, 0.7631, 0.9890, 0.7118],
[0.2446, 1.3667, 0.0117, 1.3380, 1.9752],
[1.3811, 1.8062, 1.6725, 1.4579, 1.1127]]]])
pytorch中的1d最大池化: tensor([[[[3.0464, 1.4964, 1.5636, 0.8499, 0.6835],
[1.7335, 1.6247, 2.2351, 1.2265, 2.1508],
[2.4027, 1.1205, 0.7631, 0.9890, 0.7118],
[0.2446, 1.3667, 0.0117, 1.3380, 1.9752],
[1.3811, 1.8062, 1.6725, 1.4579, 1.1127]]]])
两种写法产生了一样的结果
# 3d池化
x = torch.randn([1, 1, 10, 15, 20])
y = reduce(x, 'b c (x dx) (y dy) (z dz) -> b c x y z', 'max', dx=2, dy=3, dz=4)
print("原始数据:", x)
print("einops中的1d池化:", y)
print("pytorch中的1d最大池化:", torch.nn.MaxPool3d(kernel_size=[2, 3, 4], stride=[2, 3, 4])(x))
if ((y == torch.nn.MaxPool3d(kernel_size=[2, 3, 4], stride=[2, 3, 4])(x)).sum().item() == torch.prod(torch.tensor(y.shape)).item()):
print("两种写法产生了一样的结果")
原始数据: tensor([[[[[-1.4280, 0.8243, 0.1118, ..., -0.5074, 0.5869, -0.5303],
[ 0.8834, 0.2430, -0.1527, ..., -0.6195, 1.1189, 1.3253],
[ 1.2062, -2.2362, -1.0694, ..., 0.2259, -1.5342, 1.6444],
...,
[-0.8071, 0.9804, 1.6733, ..., 0.6847, 0.3967, 0.3554],
[-0.0124, -1.0315, 0.4742, ..., 1.0781, -0.5394, -0.2534],
[ 0.5107, 0.5698, 1.6554, ..., 0.2555, 0.7612, 0.1606]],
[[-0.1458, -0.8875, 0.1263, ..., 0.8562, -1.0398, -0.2637],
[-0.1130, 0.2352, -0.4515, ..., 0.7552, -1.4515, -1.9401],
[ 1.1593, -0.7695, -0.0036, ..., -1.5395, 0.4994, -0.0818],
...,
[-0.1205, 0.2880, 2.4444, ..., 0.9304, 1.6296, 1.2618],
[-0.8266, 0.4902, 0.1088, ..., -0.2036, 1.3552, -0.0187],
[ 0.4231, 0.2750, -2.4921, ..., -1.0541, -0.5339, -1.8072]],
[[-1.0995, 1.5515, 1.5648, ..., -0.6347, -0.0575, -1.6871],
[ 0.3554, -0.8294, -0.1540, ..., 1.4537, 0.7001, 0.1883],
[-0.4243, 1.2251, -0.2586, ..., 0.3360, 0.4393, 0.5688],
...,
[ 0.0562, 0.0538, -1.9919, ..., 1.1827, 1.3924, 0.1228],
[-1.5382, -1.0588, -2.4814, ..., 0.2646, 0.3562, -0.2394],
[ 0.5340, -1.4001, 0.7956, ..., 0.6212, 0.9341, 0.4784]],
...,
[[-1.6704, 1.4961, 0.0255, ..., -0.1300, 0.5181, -1.1021],
[ 1.3669, 2.0935, -0.1906, ..., 0.1808, 1.3379, 0.5810],
[ 0.2012, 1.5269, -0.4198, ..., -0.6139, -0.9073, -1.0657],
...,
[ 1.3680, -0.4728, 0.4887, ..., -0.9484, 1.2381, -1.5673],
[ 2.0022, -0.0255, 1.4614, ..., 0.2082, 2.1880, -0.5121],
[ 0.8368, -1.3986, 0.1638, ..., -0.2055, 1.9883, -1.0029]],
[[ 0.5656, -1.7609, -0.3142, ..., -0.4367, 0.8827, -0.0408],
[-2.0038, -2.2506, 1.2556, ..., 0.0055, -0.4263, 1.4571],
[ 0.7883, 0.9635, -0.1777, ..., -0.7183, -0.6570, 0.2688],
...,
[ 1.0658, -0.3033, 0.1150, ..., -0.1490, 2.2674, 0.4355],
[ 2.3222, 0.7259, -0.9285, ..., 2.4385, 1.8574, -1.4496],
[-0.6662, 0.1878, 1.2530, ..., -0.1872, 1.1079, -0.2728]],
[[-0.4730, -0.6685, -1.2885, ..., 0.7592, 0.3943, 0.8723],
[-0.5585, -0.8962, 1.1734, ..., -0.7074, -2.5574, 0.7183],
[ 0.2731, 0.0772, 0.0846, ..., -0.5878, -1.0141, -0.8540],
...,
[-1.5890, 0.8647, -1.5512, ..., 1.6127, -0.8931, -0.6005],
[-0.6089, 0.2880, -1.4373, ..., -0.6372, 0.3776, 1.6481],
[-0.1791, -0.7838, -0.7646, ..., -0.8543, -2.2068, -1.0930]]]]])
einops中的1d池化: tensor([[[[[1.2062, 1.1514, 0.9088, 2.0657, 1.6444],
[2.4614, 2.0076, 2.6845, 1.2944, 1.9335],
[3.5129, 2.8723, 2.0677, 1.7359, 1.2360],
[1.5521, 1.7481, 3.0657, 2.0558, 1.1136],
[2.6293, 1.0543, 2.1528, 2.0432, 1.6296]],
[[1.9354, 1.6923, 1.6766, 1.1279, 1.4537],
[1.4777, 2.5995, 1.5160, 1.4766, 1.1773],
[1.1075, 2.1137, 1.8980, 2.6785, 1.3117],
[1.8381, 1.8093, 1.7341, 2.0017, 2.5028],
[1.5179, 1.7725, 1.8776, 1.7243, 2.9747]],
[[1.6808, 1.7929, 2.5858, 2.0532, 2.2684],
[2.3366, 2.2301, 1.8893, 1.9472, 1.9442],
[1.6411, 1.1062, 1.4044, 1.7801, 2.2027],
[2.6804, 1.5717, 2.3056, 1.6408, 1.9045],
[1.5550, 1.4955, 3.2904, 1.4233, 1.4877]],
[[2.0935, 1.9230, 1.4156, 1.5069, 1.3379],
[2.0103, 2.5312, 2.1124, 1.8951, 2.0860],
[2.4890, 1.6606, 1.9536, 1.4004, 1.3886],
[1.6776, 2.5188, 1.3871, 1.6539, 2.0570],
[3.0720, 1.7830, 1.4761, 1.7118, 2.1880]],
[[1.9803, 2.5748, 1.3203, 1.9004, 1.4571],
[1.6769, 1.4259, 1.5382, 1.0996, 1.9665],
[3.0497, 2.4174, 1.7597, 1.7959, 1.4537],
[2.5080, 2.4812, 1.3417, 2.0717, 1.5571],
[2.3222, 2.2023, 2.9739, 2.7714, 2.4385]]]]])
pytorch中的1d最大池化: tensor([[[[[1.2062, 1.1514, 0.9088, 2.0657, 1.6444],
[2.4614, 2.0076, 2.6845, 1.2944, 1.9335],
[3.5129, 2.8723, 2.0677, 1.7359, 1.2360],
[1.5521, 1.7481, 3.0657, 2.0558, 1.1136],
[2.6293, 1.0543, 2.1528, 2.0432, 1.6296]],
[[1.9354, 1.6923, 1.6766, 1.1279, 1.4537],
[1.4777, 2.5995, 1.5160, 1.4766, 1.1773],
[1.1075, 2.1137, 1.8980, 2.6785, 1.3117],
[1.8381, 1.8093, 1.7341, 2.0017, 2.5028],
[1.5179, 1.7725, 1.8776, 1.7243, 2.9747]],
[[1.6808, 1.7929, 2.5858, 2.0532, 2.2684],
[2.3366, 2.2301, 1.8893, 1.9472, 1.9442],
[1.6411, 1.1062, 1.4044, 1.7801, 2.2027],
[2.6804, 1.5717, 2.3056, 1.6408, 1.9045],
[1.5550, 1.4955, 3.2904, 1.4233, 1.4877]],
[[2.0935, 1.9230, 1.4156, 1.5069, 1.3379],
[2.0103, 2.5312, 2.1124, 1.8951, 2.0860],
[2.4890, 1.6606, 1.9536, 1.4004, 1.3886],
[1.6776, 2.5188, 1.3871, 1.6539, 2.0570],
[3.0720, 1.7830, 1.4761, 1.7118, 2.1880]],
[[1.9803, 2.5748, 1.3203, 1.9004, 1.4571],
[1.6769, 1.4259, 1.5382, 1.0996, 1.9665],
[3.0497, 2.4174, 1.7597, 1.7959, 1.4537],
[2.5080, 2.4812, 1.3417, 2.0717, 1.5571],
[2.3222, 2.2023, 2.9739, 2.7714, 2.4385]]]]])
两种写法产生了一样的结果
可以看出无论是pytorch的1d,2d与3d池化都与einops中对应的操作产生了相同的结果,而且可以看出einops中的1d,2d,3d池化是在统一的模式下实现的,在写具体的程序中也便于我们实现。
在官方文档中提到了depth-to-space与space-to-depth这些操作在许多框架中都有实现,但是高度到宽度呢?在einops中就可以很方便的实现这种操作,因为他们都是在统一的模式下进行的,如下
x = torch.randn([32, 3, 32, 32])
x = rearrange(x, 'b c h (w w2) -> b c (h w2) w', w2=2)
5:独立性
在不同的框架中,同样的功能可能由不同的函数来进行实现,这就导致了我们在面对这些不同的函数是可能会造成困惑,但是由于einops支持多种深度学习框架,我们就可以统一的使用einop
s中的操作来实现,例如官方所举例子:
image = torch.randn((1, 2))
y1 = torch.tensor(np.tile(image, (1, 2))) # in numpy
y2 = image.repeat(1, 2) # pytorch's repeat ~ numpy's tile
print(image)
print(y1)
print(y2)
print(((y1==y2).sum().item()==torch.prod(torch.tensor(y1.shape))))
tensor([[1.4851, 1.8031]])
tensor([[1.4851, 1.8031, 1.4851, 1.8031]])
tensor([[1.4851, 1.8031, 1.4851, 1.8031]])
tensor(True)
上述代码起到了可控广播机制的作用,通过复制自身,来扩充自己元素的数量,形成一个更大的张量,在numpy与pytorch都有具体的实现方法,但是这两种方法名称不同,容易造成困惑,但是如果使用einops就不会产生这种困惑,因为在所有不同einops支持的框架下,都可以通过einops中的方式来进行实现,这样就减少了人们的疑惑,例子如下:
repeat(image, 'h w -> h (tile w)', tile=2) # in numpy
repeat(image, 'h w -> h (tile w)', tile=2) # in pytorch
repeat(image, 'h w -> h (tile w)', tile=2) # in tf
repeat(image, 'h w -> h (tile w)', tile=2) # in jax
repeat(image, 'h w -> h (tile w)', tile=2) # in cupy
... (etc.)
Cell In[19], line 6
... (etc.)
^
SyntaxError: invalid syntax
可以看出在einops支持的这些框架下,我们都可以采用einops中的方式实现相同的功能,这就是einops的方便之处。
6:einops中产生的结果独立于深度学习的框架
einops中产生的结果独立于深度学习的框架也就是说eniops在不同的深度学习框架中的工作方式是一样的,也就是在不同的深度学习框架中只要使用了einops来操作张量,那么就会得到相同的结果,而在不同的学习框架中使用某些功能时可能会产生不相同的结果,使用einops中的操作就可以避免这种不一致性的产生,例子如下:
y = x.flatten() # or flatten(x)
这一个例子时官方文档的例子,产生的结果在不同的框架下不一定相同,如:
假设x的形状是(3, 4, 5), 那么y的形状 …
numpy, pytorch, cupy, chainer: (60,)
keras, tensorflow.layers, gluon: (3, 20)
这表明在不同的深度学习框架下有可能会产生不同的结果,但是使用einops就不会产生这些差别,因为einops的行为独立于深度学习的框架,即einops中的方法产生的结果是独立与深度学习框架的,不因框架的不同而产生不同的结果。