0. 요약
import numpy as np
a = np.array([1, 2])
print(id(a))
a += 1
print(id(a))
b = np.array([1, 2])
print(id(b))
b = b + 1
print(id(b))
1906123730816
1906123730816
1906122615264
1906162448144
Process finished with exit code 0
위 코드를 실행 시 알 수 있는 것처럼, numpy 배열 연산 시
a += 1
a = a + 1
은 다르게 동작한다.
위의 코드(제자리 연산)는 변수 a의 주소를 바꾸지 않고, 아래의 코드는 a의 주소를 바꾼다.
이를 알지 못하면 함수에 매개변수로 전달 시 예상치 못한 결과를 가져올 수 있다.
1. 문제 인식
def gradient_descent(f, init_x, lr=0.01, step_num=5):
for i in range(step_num):
init_x -= gradient(f, init_x) * lr
print(id(init_x))
return init_x
init_array = np.array([-3.0, 4.0])
print(id(init_array))
print(gradient_descent(f1, init_array, lr=0.1))
print(init_array)
실행 시,
1971976333872
1971976333872
1971976333872
1971976333872
1971976333872
1971976333872
[-0.98304 1.31072]
[-0.98304 1.31072]
Process finished with exit code 0
가 출력된다.
init_x 변수가 원래 [-3.0, 4.0] 이었고 값이 바뀌지 않을 거라 생각했는데, 기대와는 달리 값이 바뀌었다.
그래서 다음과 같이 #1 에서 #2로 코드를 바꾸었다.
#1
init_x -= gradient(f, init_x) * lr # 1
#2
init_x = init_x - gradient(f, init_x) * lr # 2
처음엔 결과가 같으리라고 예상했지만 다른 출력 결과를 보였다.
1887893252656
1887894083184
1887894443232
1887894083184
1887894443232
1887894083184
[-0.98304 1.31072]
[-3. 4.]
Process finished with exit code 0
#1번 코드는 init_x의 id값을 바꾸지 않지만 #2번 코드는 id값을 바꾼다. 따라서 함수 안에서 실행 시 마치 call by value인 것처럼 매개변수의 원래 값에 영향을 주지 않았다. 왜 그럴까?
2. int형 값들끼리는 문제 없는데...
a = 5
print(id(a))
a += 1
print(id(a))
b = 4
print(id(b))
b = b + 1
print(id(b))
140718013822880
140718013822912
140718013822848
140718013822880
Process finished with exit code 0
+= 연산을 하든 + 연산을 하든 변수의 id값이 바뀌는 걸 알 수 있다.
그 와중에 같은 5를 저장하는 변수는 같은 id를 가지는 걸 알 수 있다. 메모리를 절약하기 위해 최적화된 결과다.
찾아보니까 +는 __add__를, +=는 __iadd__를 호출하는데 int형 변수에 dir로 메소드를 확인해봤을 때 __iadd__가 정의되어 있지 않음을 확인했다. __iadd__가 정의되어 있지 않으면 __add__를 호출한다고 한다.
그러므로 numpy 배열 연산시 -=와 -가 다른 이유는 __iadd__가 오버로딩되어 있기 때문일 것이라고 생각하고 확인해보았다.
역시 __add__와 __iadd__가 따로 정의되어 있었다.
3. 제자리 연산과 그렇지 않은 연산의 속도 차이
고성능 파이썬 2판(미샤 고렐릭, 이안 오스발트) 책에 따르면,
제자리 연산(+=)시 추가로 메모리를 할당하지 않기 때문에(id값 바뀌지 않음) 속도가 빠른 반면 그렇지 않은 연산(+)은 운영체제에 메모리를 할당하는 과정 거치므로 다른 프로세스, 즉 커널과 통신해야 하고 이는 속도를 매우 느려지게 한다.
하지만 원래 변수의 값을 보존해야 할 때는 제자리 연산이 아닌 연산을 사용해야 할 수 있다.
4. 돌아보며
파이썬의 Call by assignment에 대해 잘 알면 매우 당연한 결과인 것을 알 수 있다.
위의 코드에서 += 연산은 in-place 연산인 반면 + 연산은 새로운 메모리 주소를 할당하기 때문이다.
'NLP lab > 파이썬' 카테고리의 다른 글
[pytorch] Batch 연산하면 값이 달라지는 이유 (0) | 2023.04.23 |
---|---|
[Solved] device-side assert triggered, Assertion `t >= 0 && t < n_classes` failed. (0) | 2022.08.05 |