线性回归的从零实现

https://zh-v2.d2l.ai/chapter_linear-networks/linear-regression-scratch.html

sgd函数没有返回值,里面的变量都是临时变量

param -= lr * param.grad / batch_size

上面这行代码只是在函数里做更新,更新的也只是临时变量,而且sgd函数也没有返回值,是怎么更新到w和b的呢?

你可以认为这边的 param 是对 [w, b] 的引用,更新 param 就改变了 wb。参见 Why are variables in Python different from other programming languages’?

4 Likes

python函数传递的是一个引用,你可以当作C里的指针,再看看python的基础语法吧 :smiley:

1 Like

我的观点是这样的:
不应该用传递的参数是不是引用来解释,其实这关系到变量的作用域(scope)问题,即函数内的变量是全局的(global)还是局部的(local)。

当某一变量var在函数外面已经声明时 (如var=v0),函数内部默认var为全局变量且可以访问该变量,除非在函数内部有修改变量var的行为(如重新赋值 var=v1 或者代数运算 var=var+v1 等)。在这种修改变量的情况下,变量var会被定义为局部变量并被重新分配内存,它在函数内部的变化不会影响到外部的全局变量var的值(即var=v0保持不变)。

以上规则无论变量有没有被传入函数都适用,即 f(var…) 或者 f(…)。

特殊之处在于本节sgd中使用的运算符(-=)会执行原地操作(in-place operation),也就是运算结果会赋给同一块内存。由于params本身就是全局变量,修改后的结果仍然赋给它的内存,所以变化的也就是全局变量了。如修改为 param = param - … 结果就不对了,大家可以试一试。

类似的运算符还有 +=, *=, /= … 关于 in-place operation的讲解可以参考之前的 2.1.5节 :“节省内存”。

1 Like

造成你困惑的最主要原因的核心是“可变对象与不可变对象”。
对于函数中的for循环,param得到的是列表中元素的引用,这没有问题。
但是呢,会不会就地改变(直接作用到变量)这得看具体的实现。
“-=”操作符会调用__isub__函数,而"-"操作符会调用__sub__函数,一般对于可变对象来说“-=”操作符会直接改变self自身。对于pytorch来说,应该会调用sub_函数(找了一下源码,翻到C源码,没找到具体代码在哪)。
举个例子

import torch

x1 = 1
x2 = 2
params = [x1, x2]
for p in params:
    print(id(p), id(x1), id(x2))
    p -= 4
    print(id(p), id(x1), id(x2))
print(params)

x1 = torch.Tensor([1])
x2 = torch.Tensor([2])
params = [x1, x2]
for p in params:
    print(id(p), id(x1), id(x2))
    p -= 4
    print(id(p), id(x1), id(x2))
print(params)

你会得到(你自己运行的话,id得到的地址会不一样)

9784896 9784896 9784928
9784768 9784896 9784928
9784928 9784896 9784928
9784800 9784896 9784928
[1, 2]
139752445458112 139752445458112 139752445458176
139752445458112 139752445458112 139752445458176
139752445458176 139752445458112 139752445458176
139752445458176 139752445458112 139752445458176
[tensor([-3.]), tensor([-2.])]

可以看到对于int类型,地址变换了,而torch类型,地址却没有变化。
p -= 4等价于p.sub_(4)。这个可变对象改变了自身。而若如vin100提到的写成p = p - 4则会调用构造函数,并返回一个新的变量,也就不可能作用到原先的“可变对象”。
int类没有发生就地变化是因为它是一个不可变对象。

2 Likes