栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

Python、Numpy、PyTorch中的深拷贝、浅拷贝、赋值

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Python、Numpy、PyTorch中的深拷贝、浅拷贝、赋值

1. Python中的赋值、浅拷贝、深拷贝 1.1 Python中的定义
  • 浅拷贝,指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。
  • 深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
  • 赋值:把原对象的引用赋值给新对象,二者指向的是内存中相同的地址
1.2 Python中的浅拷贝
  • 使用数据类型本身的构造器
  • 对于可变的序列,还可以通过切片操作符:来完成浅拷贝
  • Python 还提供了对应的函数 copy.copy() 函数,适用于任何数据类型
1.2.1 使用构造器工厂函数 —— list(), tuple(), set(), dict()
>>> list_1 = [1, 2, 3]
>>> list_2 = list(list_1)
>>> list_1 == list_2
True
>>> list_1 is list_2
False

>>> set_1 = {1, 2, 3}
>>> set_2 = set(set_1)
>>> set_1 == set_2
True
>>> set_1 is set_2
False

>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple(tuple_1)
>>> tuple_1 == tuple_2
True
>>> tuple_1 is tuple_2
True  # tuple与其他数据类型不同, 不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(等价于赋值)

>>> dict_1 = {1: "a", 2: "b", 3: "c"}
>>> dict_2 = dict(dict_1)
>>> dict_1 == dict_2
True
>>> dict_1 is dict_2
False

is与==的区别

  • ==是看前后两个变量的值是否相等(即二者是否指向同一个地址)
  • is是看二者的id是否相等(即二者是否是同一个变量)

因为浅拷贝会为新变量重新分配一块内存,所以这两个变量的id不同,但二者的数值是相同的,说明二者均指向同一个地址

1.2.2 [仅限list] 使用切片操作来完成浅拷贝
>>> list_1 = [1, 2, 3]
>>> list_2 = list_1[:]
>>> list_1 == list_2
True
>>> list_1 is list_2
False
1.2.3 [不限制数据类型] 使用copy.copy()函数完成浅拷贝
>>> import copy
>>> list_1 = [1, 2, 3]
>>> list_2 = copy.copy(list_1)
>>> list_1 == list_2
True
>>> list_1 is list_2
False

>>> set_1 = {1, 2, 3}
>>> set_2 = copy.copy(set_1)
>>> set_1 == set_2
True
>>> set_1 is set_2
False

>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = copy.copy(tuple_1)
>>> tuple_1 == tuple_2
True
>>> tuple_1 is tuple_2
True  # tuple与其他数据类型不同, 不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(等价于赋值)

>>> dict_1 = {1: "a", 2: "b", 3: "c"}
>>> dict_2 = copy.copy(dict_1)
>>> dict_1 == dict_2
True
>>> dict_1 is dict_2
False
1.2.4 tuple()没有浅拷贝

对于元组,不管是 tuple() 还是切片操作符:,亦或是copy.copy()均不会创建一份浅拷贝,相反它会返回一个指向相同元组的引用(即等价于赋值)

>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple(tuple_1)
>>> tuple_3 = tuple_1[:]
>>> tuple_4 = copy.copy(tuple_1)
>>> tuple_5 = tuple_1
>>> print(f"{tuple_1 == tuple_2}n{tuple_1 == tuple_3}n{tuple_1 == tuple_4}n{tuple_1 == tuple_5}")
True
True
True
True
>>> print(f"{tuple_1 is tuple_2}n{tuple_1 is tuple_3}n{tuple_1 is tuple_4}n{tuple_1 is tuple_5}")
True
True
True
True
1.2.5 字符串也没有浅拷贝

和tuple类似:

>>> str_1 = "PyTorch"
>>> str_2 = str(str_1)
>>> str_3 = str_1[:]
>>> str_4 = copy.copy(str_1)
>>> str_5 = str_1
>>> print(f"{str_1 == str_2}n{str_1 == str_3}n{str_1 == str_4}n{str_1 == str_5}")
True
True
True
True
>>> print(f"{str_1 is str_2}n{str_1 is str_3}n{str_1 is str_4}n{str_1 is str_5}")
True
True
True
True

也就是说,对字符串和元组使用 copy()、[:]、本身的构造器(tuple(), str())完成的复制,都只是开辟了内存存储原对象的引用,而不是存储原对象的子对象的引用。

1.2.6 浅拷贝和赋值的本质区别,赋值只是把原对象的引用给到新对象
>>> list_1 = [1, 2, 3]
>>> list_2 = list_1
>>> list_3 = copy.copy(list_1)
>>> print(f"{list_1 == list_2}n{list_1 == list_3}")
True
True
>>> print(f"{list_1 is list_2}n{list_1 is list_3}")
True
False
>>> print(f"{id(list_1)}n{id(list_2)}n{id(list_3)}")
140164275963200
140164275963200
140164275885952

可以看到,赋值的话,list_1和list_2这两个变量的id都相同,但浅拷贝不是,浅拷贝两个变量的id不同,但都指的是相同的内容。

1.2.7 对数据采用浅拷贝的方式时,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会出现一些问题 1.2.7.1 赋值
>>> list_1 = [1, 2, 3]
>>> list_2 = list_1
>>> list_2.append("new value")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [1, 2, 3, 'new value']
list_2: [1, 2, 3, 'new value']

两个列表的值都发生变化了!这是因为list_1, list_2都指向的是同一块内存中的数据,不管修改list_1还是修改list_2,其实本质上修改的都是相同的东西。

但对于不可变对象来说,没有这种顾虑:

>>> tuple_1 = (1, 2, 3)
>>> tuple_2 = tuple_1
>>> tuple_2 += tuple([1, 2, 3])
>>> print(f"tuple_1: {tuple_1}ntuple_2: {tuple_2}")
tuple_1: (1, 2, 3)
tuple_2: (1, 2, 3, 1, 2, 3)
1.2.7.1 浅拷贝
>>> list_1 = [1, 2, 3]
>>> list_2 = copy.copy(list_1)
>>> list_2.append("new value")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [1, 2, 3]
list_2: [1, 2, 3, 'new value']

我们发现浅拷贝不会出现前面那种赋值的情况了!
是这样吗?

看下面的操作:

>>> list_1 = [[1, 2], [3, 4]]
>>> list_2 = copy.copy(list_1)
>>> list_2.append(100)  # 这个操作没问题
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2], [3, 4], 100]

>>> # 仔细看下面的操作
>>> list_2[0].append("元素中append")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2, '元素中append'], [3, 4]]
list_2: [[1, 2, '元素中append'], [3, 4], 100]

看到了吗?浅拷贝只能保证最外面的那层壳,不能保护里面的元素。

这也就是为什么copy.copy()被成为浅拷贝,因为它拷贝的东西只有一层,里面层的东西就不拷贝了,直接跟赋值一样用引用实现。

为了解决浅拷贝只管一层的弊端,我们可以使用深拷贝。

1.3 Python中的深拷贝

深拷贝不同于浅拷贝只负责一层,深拷贝负责每一层!

>>> list_1 = [[1, 2], [3, 4]]
>>> list_2 = copy.deepcopy(list_1)
>>> list_2.append(100)  # 这个操作深拷贝和浅拷贝是没有区别的,都会负责这一层
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2], [3, 4], 100]
>>> # 第二层
>>> list_2[0].append("元素中append")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[1, 2], [3, 4]]
list_2: [[1, 2, '元素中append'], [3, 4], 100]

我们多构建几层看看:

>>> list_1 = [[[[1,2],[3,4]], [5, 6]], [7, 8]]
>>> list_2 = copy.deepcopy(list_1)
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]
list_2: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]

>>> # 在深层append元素
>>> list_2[0][0][0].append("New Value!")
>>> print(f"list_1: {list_1}nlist_2: {list_2}")
list_1: [[[[1, 2], [3, 4]], [5, 6]], [7, 8]]
list_2: [[[[1, 2, 'New Value!'], [3, 4]], [5, 6]], [7, 8]]
1.4 Python中赋值、浅拷贝、深拷贝的总结
  • 赋值=:最不靠谱的操作,对于list来说,牵一发而动全身
  • 浅拷贝copy.copy():相比赋值操作,能拷贝一层的数据,但不拷贝第二层以后的数据
  • 深拷贝copy.deepcopy():对于list来说,能用这个就用这个
2. Numpy中的浅拷贝和深拷贝 2.1 引言

简单理解,numpy中没有浅拷贝,只有赋值和深拷贝

2.2 赋值
>>> a = np.array([1, 2, 3])
>>> b = a
>>> print(f"a: {a}nb: {b}")
a: [1 2 3]
b: [1 2 3]
>>> b[0] = 789
>>> print(f"a: {a}nb: {b}")
a: [789   2   3]
b: [789   2   3]

可以看到,和Python的赋值一样,对于list来说,一层都保护不了。

2.3 深拷贝

和Python的深拷贝一样,但是语法不同:

  • Python中的深拷贝:a = copy.deepcopy(数据)
  • Numpy中的深拷贝:a = 数据.copy()

看一下numpy中赋值=和深拷贝copy()的区别:

>>> import torch
>>> import numpy as np
>>> a = torch.zeros(1, 1, 1)
>>> a = a.numpy()
>>> a
array([[[0.]]], dtype=float32)
>>> b = a  # 赋值
>>> c = a.copy()  # 深拷贝
>>> b[0][0][0] = 1024
>>> c[0][0][0] = 2048
>>> print(f"a: {a}nb: {b}nc: {c}")
a: [[[1024.]]]
b: [[[1024.]]]
c: [[[2048.]]]

很明显,numpy中的深拷贝和Python中的深拷贝效果是一样的。

3. PyTorch中的浅拷贝、深拷贝以及其他注意事项 3.1 浅拷贝 —— torch.from_numpy()和torch.as_tensor()

torch.from_numpy和torch.as_tensor是浅拷贝,在内存中共享数据。

例子1:

>>> import torch
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a_1 = torch.from_numpy(a)  # 浅拷贝
>>> a_2 = torch.as_tensor(a)  # 浅拷贝
>>> a_1[0] = 1000
>>> a_2[1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [1000 2000    3]
 a_1: tensor([1000, 2000,    3])
 a_2: tensor([1000, 2000,    3])

例子2:

>>> import torch
>>> import numpy as np
>>> a = np.array([[1,2], [3,4]])
>>> a_1 = torch.from_numpy(a)
>>> a_2 = torch.as_tensor(a)
>>> a_1[0,0] = 1000
>>> a_2[0,1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [[1000 2000]
 [   3    4]]
a_1: tensor([[1000, 2000],
        [   3,    4]])
a_2: tensor([[1000, 2000],
        [   3,    4]])

我们可以看到,torch.from_numpy和torch.as_tensor的确是浅拷贝。

3.2 深拷贝 —— torch.Tensor()和torch.tensor()

torch.Tensor和torch.tensor是深拷贝,在内存中创建一个额外的数据副本,不共享内存,所以不受数组改变的影响。

例子1:

>>> import torch
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a_1 = torch.Tensor(a)  # 深拷贝
>>> a_2 = torch.tensor(a)  # 深拷贝
>>> a_1[0] = 1000
>>> a_2[1] = 2000
>>> print(f"a: {a}n a_1: {a_1}n a_2: {a_2}")
a: [1 2 3]
 a_1: tensor([1000.,    2.,    3.])
 a_2: tensor([   1, 2000,    3])

例子2:

>>> import torch
>>> import numpy as np
>>> a = np.array([[1,2], [3,4]])
>>> a_1 = torch.Tensor(a)  # 深拷贝
>>> a_2 = torch.tensor(a)  # 深拷贝
>>> a_1[0, 0] = 1000
>>> a_2[0, 1] = 2000
>>> print(f"a: {a}na_1: {a_1}na_2: {a_2}")
a: [[1 2]
 [3 4]]
a_1: tensor([[1000.,    2.],
        [   3.,    4.]])
a_2: tensor([[   1, 2000],
        [   3,    4]])

我们可以看到,torch.tensor和torch.Tensor的确是深拷贝。


接下来介绍一些其他PyTorch需要注意的事项。

3.3 inplace=True

inplace =True的意思是进行原地操作。举个例子:

x = x + 5  # 这是一个原地操作,可以节省内存

# ----------------------

y = x + 5
x = y  # 与x = x + 5虽然功能相同,但更加费内存

Note:在PyTorch中,你可以想 x = x + 5,但千万别写x += 5,这样很容易报错

3.4 tensor.detach()和tensor.clone()
  • tensor.clone()是深拷贝,开辟新的存储地址而不是引用来保存旧的tensor
    • tensor.clone()在梯度会传时充当中间变量,会将梯度传给源张量进行叠加,但是本身不保存其grad,值为None。
  • tensor.detach是浅拷贝,新的tensor会脱离计算图,不会牵扯梯度计算。

例子:

>>> import torch
>>> x = torch.tensor([2., 4.], requires_grad=True)
>>> clone_x = x.clone()  # 深拷贝
>>> detach_x = x.detach()  # 浅拷贝
>>> clone_detach_x = x.clone().detach()  # 先深拷贝再浅拷贝,最后为浅拷贝
>>> 
>>> y = 2 * x + 10
>>> y.backward(torch.FloatTensor([1, 1]))  # y进行反向传播,梯度值为[1,1]
>>>
>>> print(f"x.grad: {x.grad}nclone_x.requires_grad: {x.requires_grad}nclone_x.grad: {clone_x.grad}ndetach_x.requires_grad: {detach_x.requires_grad}nclone_detach_x.requires_grad: {clone_detach_x.requires_grad}")

x.grad: tensor([2., 2.])
clone_x.requires_grad: True  # 深拷贝,也会拷贝其梯度状态
clone_x.grad: None  # 但是不会存储梯度信息
detach_x.requires_grad: False  # 浅拷贝不会拷贝其梯度状态(自然也没有梯度信息了)
clone_detach_x.requires_grad: False  # 浅拷贝不会拷贝其梯度状态(自然也没有梯度信息了)
3.5 tensor.contiguous

在PyTorch中,很多操作都用到了浅拷贝的思路,只是重新定义下标与元素的对应关系。例如在使用transpose()进行转置操作时,PyTorch不会创建转置后的新的tensor,仅仅修改tensor中的一些属性(元数据)。转置和原数据的内存是共享的。

例子1:不使用tensor.contiguous()

>>> import torch
>>> x = torch.randn((3, 2))
>>> y = torch.transpose(x, 0, 1)  # tensor x 的第0维和第1维进行转置,结果给y -> 浅拷贝
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 0.4589,  1.3154],
        [-0.4431, -0.6626],
        [-0.7404,  1.9109]])
y: tensor([[ 0.4589, -0.4431, -0.7404],
        [ 1.3154, -0.6626,  1.9109]])
>>> print(f"x.shape: {x.shape}ny.shape: {y.shape}")
x.shape: torch.Size([3, 2])
y.shape: torch.Size([2, 3])


>>> y[0, 0] = 1024
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 1.0240e+03,  1.3154e+00],  # 变了
        [-4.4314e-01, -6.6265e-01],
        [-7.4039e-01,  1.9109e+00]])
y: tensor([[ 1.0240e+03, -4.4314e-01, -7.4039e-01],  # 变了
        [ 1.3154e+00, -6.6265e-01,  1.9109e+00]])
>>> print(f"x.shape: {x.shape}ny.shape: {y.shape}")
x.shape: torch.Size([3, 2])
y.shape: torch.Size([2, 3])

说明torch.transpose()操作是浅拷贝。

例子2:使用tensor.contiguous()

>>> import torch
>>> x = torch.randn((3, 2))
>>> y = torch.transpose(x, 0, 1).contiguous()  # 加了tensor.contiguous(),从浅拷贝变为深拷贝
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.5056,  0.4376],
        [ 0.0163,  0.5593],
        [-1.1808,  0.7779]])
y: tensor([[-0.5056,  0.0163, -1.1808],
        [ 0.4376,  0.5593,  0.7779]])
>>> 
>>> y[0, 0] = 11
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.5056,  0.4376],  # 没变
        [ 0.0163,  0.5593],
        [-1.1808,  0.7779]])
y: tensor([[11.0000,  0.0163, -1.1808],  # 变了
        [ 0.4376,  0.5593,  0.7779]])

加了tensor.contiguous(),torch.transpose()从浅拷贝变为深拷贝。

3.6 tensor.view()

我们看一下tensor.view()是浅拷贝还是深拷贝。

例子1: 不加tensor.contiguous()

>>> x = torch.randn(3, 2)
>>> y = x.view(2, 3)
>>> print(f"x: {x}ny: {y}")
x: tensor([[-1.6152,  2.3585],
        [ 0.4665,  1.6596],
        [ 1.1893,  0.3895]])
y: tensor([[-1.6152,  2.3585,  0.4665],
        [ 1.6596,  1.1893,  0.3895]])
>>> y[0, 0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[9.0000, 2.3585],
        [0.4665, 1.6596],
        [1.1893, 0.3895]])
y: tensor([[9.0000, 2.3585, 0.4665],
        [1.6596, 1.1893, 0.3895]])

说明tensor.view()也是浅拷贝。

例子2:加了tensor.contiguous()

>>> import torch
>>> x = torch.randn(3, 2)
>>> y = x.view(2, 3).contiguous()
>>> y[0, 0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.3736],  # 变了
        [ 1.9279, -0.1798],
        [-0.1475, -1.0268]])
y: tensor([[ 9.0000, -0.3736,  1.9279],  # 变了
        [-0.1798, -0.1475, -1.0268]])

说明tensor.view不管加不加contiguous都是浅拷贝。

3.7 tensor.reshape

我们看一下tensor.reshape()是浅拷贝还是深拷贝。

例子1:不加contiguous

>>> x = torch.randn(2, 3)
>>> y = x.reshape(3, 2)
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.2499,  1.4942],
        [-0.4103, -2.7727,  0.4829]])
y: tensor([[ 9.0000, -0.2499],
        [ 1.4942, -0.4103],
        [-2.7727,  0.4829]])

tensor.reshape本身是一个浅拷贝。

例子2:加了tensor.contiguous:

>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.reshape(3, 2).contiguous()
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -0.7157,  0.0864],
        [-1.3975,  0.8973, -0.5598]])
y: tensor([[ 9.0000, -0.7157],
        [ 0.0864, -1.3975],
        [ 0.8973, -0.5598]])

仍然是一个浅拷贝。

3.8 tensor.permute()

例子1:不加contiguous

>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.permute(1, 0)
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[ 9.0000, -1.2463, -1.3497],
        [-0.2729, -0.6057, -1.0876]])
y: tensor([[ 9.0000, -0.2729],
        [-1.2463, -0.6057],
        [-1.3497, -1.0876]])

tensor.permute本身是一个浅拷贝。

例子2:加了tensor.contiguous:

>>> import torch
>>> x = torch.randn(2, 3)
>>> y = x.permute(1, 0).contiguous()
>>> y[0,0] = 9
>>> print(f"x: {x}ny: {y}")
x: tensor([[-0.6026, -0.7970,  1.0808],
        [-1.4109, -1.2785,  1.5553]])
y: tensor([[ 9.0000, -1.4109],
        [-0.7970, -1.2785],
        [ 1.0808,  1.5553]])

加了contiguous之后,permute变为深拷贝。

3.9 总结

PyTorch很多操作都是浅拷贝,加contiguous有的可以将其变为深拷贝,有的不行,慢慢摸索吧

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/861463.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号