이상한 pytorch4: 숨겨진 optimizer

ddang ddang ball·2022년 9월 30일
0

파이토치

목록 보기
4/6

딥러닝에서 optimizer은 무엇을 사용할까?
???: 히히 optimizer은 Adam만 쓰면 되는거 아닌가요?

흐음...틀린 소리는 아닌데...아닐때도 가아끔? 있더라
오늘은 그 '가아끔'을 쓰려고 한다.

우리는 무엇으로 파라미터를 업데이트를 하는가?

파라미터의 변화량에 따른 목적함수에 나오는 오차의 변화량을 미분해서 파라미터를 업데이트하는 것은 알것이다.
이렇게 한번만 나온 행렬을 '야코비안 행렬'이라고 한다.(그냥 미분해서 파라미터를 업데이트한다는 것만 알아도 큰 손해는 없을 것이다.)

근데 이걸 2번 미분한 다면?

그걸 우린 '헤시안 행렬'이라고 한다.

  • 헤시안 행렬을 사용하면 오차함수의 '전체적인 흐름'을 알 수 있다.
  • 지금 우리가 사용하고 있는 gradient descent는 가중치의 변화량에 따른 오차함수의 '순간적'인 변화량만 알기에 이게 global minimum을 가는지 아니면 단순 local minimum를 가는지 제대로 알 길이 없다!

Q) 근데 아직도 왜 Adam이 짱이네요?

헤시안 행렬은 현재적인 기술로는 활용할 수 없기 때문이다 ㅜㅜ
헤시안 행렬을 만들기에는 컴퓨팅 파워가 부족해서 불가능하다. 그래서 우리는 '부분적으로' 2차 미분을 하려고 한다.

그게 LBFGS다! (LBFGS알고리즘은 추가로 조사해보겠다.)

LBFGS

LBFGS란 준-뉴턴 방식 (quasi-Newton methods)의 최적화 알고리즘이다. 제한된 컴퓨터 메모리를 이용하여 기존 BFGS (Broyden–Fletcher–Goldfarb–Shanno algorithm) 알고리즘을 속도면에서 개선한 알고리즘이다. (결과는 근사값).
본 알고리즘은 Adam과 함께 머신 러닝에 있어서 널리 사용되는 파라메터 추정 알고리즘이다.

원래의 BFGS와 같이, L-BFGS 알고리즘은 variable space의 검색을 조정하는 추정된 inverse Hessian matrix를 이용한다. 하지만 BFGS는 inverse Hessian에서 추정된 전체 n×nn\times{}n 을 이용하고, L-BFGS는 단지 몇개의 벡터만을 저장한다. 비록 일부의 벡터이지만 이런 일부의 벡터 역시 암시적으로 대표성을 지니고 있다.
(출처: 위키백과 https://ko.wikipedia.org/wiki/L-BFGS)

이 친구는 신기한 특징이 하나있다.
배치 사이즈 따윈 없다!
그냥 전체 데이터를 보고 한 번에 gradient descent를 수행한다.
이 친구는 거의 안쓰긴 하지만 신기해서 가져왔다.

코드로 사용 방법

pytorch로 사용하려고 하니 고생좀 했다. 그래서 올리니 한번 살펴보자.

optimizer = torch.optim.LBFGS(
            model.parameters(), 
            lr=1, 
            max_iter=50000, 
            max_eval=50000, 
            history_size=50,
        )

optimizer을 정의할 때부터 예사롭지 않다.
이 하이퍼 파라미터들이 뭘 의미하는 지는 모르겠다.(공부해라 주인장...)
그리고 이 친구를 쓰는 데 중요한 특징이 하나 더 있다!

모델이 학습하기 위한 함수를 따로 정의하고 그 안에다가 closure()이란 함수를 적용해야 이 알고리즘이 돌아간다!!

def train_model(epoch, model, cost_function, optimizer, train_data, train_label):
  loss_graph = []
  net = net.to(device)
  
  for i in range(epoch): #훈련 횟수 설정
  
  	#학습 데이터와 라벨을 gpu에 보내는 코드
  	train_data = train_data.to(device)
    train_label = train_label.to(device)
    net.train()
    
      #LBGFS가 돌아가기 위한 closure
      def closure():
     
        optimizer.zero_grad()

        y_pred = model(train_data)
        loss = cost_function(y_pred, train_label)
        loss.backward()
        return loss

    optimizer.step(closure)
    loss = closure()
    loss_graph.append(loss.item())
  return loss_graph

이런식으로 구현이 된다.

def closure():
     
        optimizer.zero_grad()

        y_pred = model(train_data)
        loss = cost_function(y_pred, train_label)
        loss.backward()
        return loss

사실 자세히 살펴보자!
1. optimizer 그래프 초기화
2. 데이터를 모델에 넣고 출력
3. 정답과 비교하여 오차 생성
4. 이로인해 생기는 오차 역전파를 보낸뒤 오차 반환
5.optimizer.step(closure)
loss = closure()

사실 pytorch에서 주로 사용하는 학습과정을 closure()이라고 따로 정의한 다음
optimizer.step(closure)로 함수를 불렀다고 생각하면 좋을 거 같다! loss graph를 그리고 싶으면 loss = closure()로 부른뒤 리스트에 잘 담으면 되니 너무 겁을 먹을 필요는 없는 것 같다.

이 친구를 어디서 배웠냐면 Pinn 논문을 실험하다가 알게 되었다. 딥러닝으로 편미분 방정식을 푸는 것이 Pinn의 핵심 개념인데 LBFGS를 optimizer로 활용하였다.

한번 돌려보니 Adam으로 40000번 학습 시킨 오차 값보다 LBFGS를 1번 학습시킨 뒤 나온 오차 값이 훨씬 작았다!
결론) 아주 가아끔은 LBFGS를 사용하는 것도 좋을 거 같다!

profile
배우고 싶은것은 많으나 용두사미인 사람.

0개의 댓글