영넌 개발로그

[밑시딥3] 자연스러운 코드로 3 - 메모리 관리, 절약 모드 본문

코딩/ML , Deep

[밑시딥3] 자연스러운 코드로 3 - 메모리 관리, 절약 모드

영넌 2023. 9. 6. 21:48

파이썬은 필요 없어진 객체를 메모리에서 자동으로 삭제함 -> 메모리 관리 의식x

 

파이썬이 메모리 관리 하는 방법

1. 참조 카운트

모든 객체는 참조 카운트가 0인 상태로 생성됨

다른 객체가 참조할 때마다 1씩 증가

  • 대입 연산자를 사용할 때
  • 함수에 인수로 전달할 때
  • 컨테이너 타입 객체(리스트, 튜플, 클래스)에 추가할 때

객체에 대한 참조가 끊길 때마다 1만큼 감소

카운트가 0이 되면 파이썬 인터프리터가 회수

class obj:
    pass

def f(x):
    print(x)

a = obj()   #변수에 대입: 카운트 1
f(a)        #함수에 전달: 함수 안에서 카운트 2
            #함수 완료: 카운트 1
a= None     #대입 해제: 카운트 0
a = obj()
b = obj()
c = obj()

a.b = b    #a가 b를 참조
b.c = c    #b가 c를 참조
a = b = c = None

오른쪽 그림은 a=b=c=None 했을 경우

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

a = b = c = None

순환 참조

사용자는 세 객체 중 어느 것에도 접근할 수 없음 (불필요한 객체)

a = b = c = None으로 카운트가 0이 되지 않아 삭제가 되지 않음

그래서 GC 사용

 

2. 세대를 기준으로 쓸모없어진 객체를 회수 (generation garbage collection, GC)

메모리가 부족해지는 시점에 파이썬 인터프리터에 의해 자동 호출

명시적 호출 가능 (import gc / gc.collect() 실행)

GC가 순환 참조를 처리하므로 프로그래밍에서 의식할 필요는 없지만 해제를 미루다보면 프로그램의 전체 메모리 사용량이 커지는 원인이 된다.

 

 

 

구현 코드에서 순환참조 해결하기

구현한 코드에서 Variable과 Function 사이의 순환 참조가 일어나고 있음

표준 파이썬 모듈 weakref
weakref.ref 함수를 사용하여 약한 참조를 만들 수 있음
약한 참조는 다른 객체를 참조하되 참조 카운트는 증가시키지 않는 기능
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 = outputs
        self.outputs = [weakref.ref(output) for output in outputs]

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


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]
            gxs = f.backward(*gys)
            if not isinstace(gxs, tuple):
                gxs = (gxs, )

		...

for문이 두 번 째 반복될때 x와 y가 덮어지므로 사용자는 이전의 계산 그래프를 더이상 참조하지 않게 된다.

 


메모리 사용 개선할 수 있는 구조 도입

1. 역전파 시 사용하는 메모리 줄이는 방법 (불필요한 미분 결과 즉시 삭제)

class Variable:
    def backward(self, retain_grad=False)
	...
            
        while funcs:
            f = funcs.pop()
          
            gys = [output().grad for output in f.outputs]
            gxs = f.backward(*gys)
            if not isinstace(gxs, tuple):
                gxs = (gxs, )

            for x, gx in zip(f.inputs, gxs):
                
                if x.grad is None:
                    x.grad = gx
                else:
                    x.grad = x.grad + gx
                    
                if x.creator is not None:
                    add_func(x.creator) #funcs.append(x.creator)
                    
            ##코드 추가##
            if not retain_grad:
                for y in f.outputs:
                    y().grad = None # y는 약한 참조

retain_grad가 True면 모든 변수가 미분 결과 유지

False(기본값)이면 중간 변수의 미분값을 모두 None으로 재설정

 

y가 약한 참조이기에 y()

y().grad = None이 실행되면 참조 카운트가 0이 되어 미분값 데이터가 메모리에서 삭제됨

 

역전파 시에는 순전파의 계산 결과가 필요함

Function class에 self.inputs = inputs 부분. 참조 카운트를 1만큼 증가시켜 __call__ 메서드에서 벗어난 뒤에도 생존

 

 

신경망에는 학습(training)과 추론(inference) 두 단계가 있음

추론 시에는 단순히 순전파만 하기 때문에 중간 계산 결과를 곧바로 버리면 메모리 사용량을 크게 줄일 수 있음

 

2. 역전파가 필요 없는 경우용 모드 제공

class Config:
    #역전파가 가능한지 여부
    #True : 역전파 활성 모드
    enable_backprop = True
    
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]

        #참조하게 하여 모드를 전환할 수 있게 만듬
        if Config.enable_backprop:
            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]
#모드 전환 실행

#중간 계산 결과를 저장되어 메모리 차지
Config.enable_backprop = True
x = Variable(np.ones((100,100,100)))
y = square(square(square(x)))
y.backward()

#중간 계산 결과를 사용 후 곧바로 삭제
Config.enable_backprop = False
x = Variable(np.ones((100,100,100)))
y = square(square(square(x)))

 

 with 구문

후처리를 자동으로 수행하고자 할 때 사용하는 구문

with 블록에 들어갈 때의 처리(전처리) 와 with 블록을 빠져나올 때의 처리(후처리)를 자동으로 할 수 있음

ex ) 파일 open, close

      with 블록에 들어갈 때 파일이 열리고 블록에서 빠져나올 때 자동으로 닫힘

with open('sample.txt','w') as f:
    f.write('hello world!')

 

역전파 비활성 모드에 적용

with문 안에서만 역전파 비활성 모드가 됨

with using_config('enable_backprop', False):
    x = Variable(np.array(2.0))
    y = square(x)

* 신경망 학습에서 모델 평가를 하기 위해 학습 도중 기울기가 필요 없는 모드를 사용하기 위함

 

contextlib 모듈 사용

import contextlib

@contextlib.contextmanager
def config_test():
    print('start') #전처리
    try:
        yield
    finally:
        print('done') #후처리

with config_test():
    print('process...')

>> start

      process....

      done

@contextlib.contextmanager 데코레이터
문맥(context)을 판단하는 함수

with 블록 안에서 예외가 발생할 수 있고, 발생한 예외는 yield를 실행하는 코드로도 전달되기 때문에
try/finally로 감싸야 함
import contextlib

@contextlib.contextmanager
def using_config(name, value):
    old_value = getattr(Config, name)  #매개변수 중 name은 타입이 str이며, 사용할 Config 클래스 속성의 이름을 가리킴 
    setattr(Config, name, value)       #getattr 함수에 name을 넘겨 Config 클래스에서 꺼내옴
    try:
        yield
    finally:
        setattr(Config, name, old_value)  #setattr 함수를 사용하여 새로운 값 설정
        
#실행 코드
with using_config('enable_backprro', False): #with 블록 진입시 name으로 지정한 Config 클래스 속성이 value로 설정됨
    x = Variable(np.array(2.0))
    y = square(x)
#with문을 매번 쓰기 귀찮으니 함수로 만들자
def no_grad():
    return using_config('enable_backprop', False)

with no_grad():
    x = Variable(np.array(2.0))
    y = square(x)
Comments