밑바닥부터 시작하는 딥러닝3 - STEP 17

2021. 3. 15. 18:08개인 공부 공간/딥러닝

메모리 관리

파이썬은 필요 없어진 객체를 메모리에서 자동으로 삭제해주기 때문에 메로리 관리를 의식할 일이 별로 없습니다. 하지만 신경망과 같이 큰 데이터를 다루는 경우 메모리 관리를 잘 하지 않으면 실행 시간이 자주 걸리는 일이 발생합니다. 기본적으로 파이썬이 메모리를 관리하는 두 방법은 참조 수를 세는 방식(참조 카운트)과 세대를 기준으로 쓸모 없어진 객체를 회수하는 방식(GC)입니다.

참조 카운트 방식의 메모리 관리

참조 카운트는 구조가 간단하고 속도도 빠르다는 장점이 있습니다. 모든 객체는 참조 카운트가 0인 상태로 생성되고, 다른 객체가 참조할 때마다 1씩 증가합니다. 반대로 객체에 대한 참조가 끊킬 때마다 1만큼 감소하다가 0이 되면 파이썬 인터프리터가 회수해갑니다.

다음은 예시 코드입니다.

 

class obj:
  pass

def f(x):
  print(x)

a = obj() # 함수에 대입: 참조 카운트 1
f(a) # 함수에 전달: 함수 안에서는 참조 카운트 2
# 함수 완료: 빠져나오면 참조 카운트 1
a = None # 대입 해제: 참조 카운트 0

 

위의 코드를 통해 참조 카운트가 증가하였다가 최종적으로 None 을 이용해 참조를 끊어 참조 카운트를 0으로 만들었습니다. 다음 코드는 이를 이용해 메모리 문제를 해결하는 과정입니다.

 

a = obj()
b = obj()
c = obj()

a.b = b
b.c = c

a = b = c = None

 

None 을 지정하기 전의 a, b, c 의 참조 카운트는 각각 1, 2, 2 이였다가 a의 참조 카운트가 0이 되면서 도미노처럼 b, c의 참조 카운트도 0이 되었습니다. 하지만 순환 참조의 경우 이 방법이 통하지 않습니다.

순환 참조

a = obj()
b = obj()
c = obj()

a.b = b
b.c = c
c.a = a

a = b = c = None

 

위의 코드와 다르게 c에서 a로의 참조를 추가하면서 순환 참조가 되었습니다. 이 경우 None 을 지정하여도 참조 카운트가 0이 되지 않습니다. 이 경우 참조 카운트 방법이 아닌 GC(Garbage Collection)을 이용해 불필요한 객체를 찾아내서 메모리를 관리해야 합니다. 기존의 DeZero 코드에는 순환 참조가 존재하기 때문에 파이썬 표준 모듈인 weakref 를 이용해서 이를 해결해야 합니다.

weakref 모듈

weakref 를 이용하면 약한 참조를 만들 수 있고, 이를 통해 다른 객체를 참조하되 참조 카운트는 증가시키지 않을 수 있습니다.

 

import weakref
import numpy as np

a = np.array([1,2,3])
b = weakref.ref(a)

print(b)
print(b())

 

실행결과

<weakref at 0x7ff85b11e958; to 'numpy.ndarray' at 0x7ff85b141b70>
[1 2 3]

 

b는 약한참조(weakref)임을 확인했습니다.

 

a = None
b

 

실행결과

<weakref at 0x7ff85b11e958; dead>

 

b도 참조를 갖고 있지만 약한 참조이기 때문에 참조 카운트에 영향을 주지 못하고 a가 None 으로 지정되자 b도 같이 삭제된 것을 볼 수 있습니다. weakref 를 기존 DeZero 코드에 도입하면 다음과 같습니다.

 

import weakref

class Function:
  def __call__(self, *inputs):
    xs = [x.data for x in inputs]
    ys = self.forward(*xs)
    if not isinstance(ys, tuple):
      ys = (ys,)    
    outputs = [Variable(as_array(y)) for y in ys]

    self.generation = max([x.generation for x in inputs])
    for output in outputs:
      output.set_creator(self)
    self.inputs = inputs
    self.outputs = [weakref.ref(output) for output in outputs]

    return outputs if len(outputs) > 1 else outputs[0]

 

이제 인스턴스 변수 self.outputs 가 대상을 약한 참조로 가리키게 변경되었습니다. 이로 인해 함수는 출력 변수를 약하게 참조합니다. 이를 Variable 클래스에서도 고려하기 위해 다음과 같은 수정이 필요합니다.

 

class Variable:
  ....

  def backward(self):
    ....

    while funcs:
      f = funcs.pop()
      # 수정 전: gys = [output.grad for output in f.outputs]
      gys = [output().grad for output in f.outputs] 

출처:- 사이토 고키, 『밑바닥부터 시작하는 딥러닝3』, 개앞맵시, 한빛미디어(2020)

출처: https://privatedevelopnote.tistory.com/81 [개인노트]