code: # -*- coding: utf-8 -*-
  """
  PyTorch: 새 autograd Function 정의하기
  ----------------------------------------
  
  :math:`y=\sin(x)` 을 예측할 수 있도록, :math:`-\pi` 부터 :math:`pi` 까지
  유클리드 거리(Euclidean distance)를 최소화하도록 3차 다항식을 학습합니다.
  다항식을 :math:`y=a+bx+cx^2+dx^3` 라고 쓰는 대신 :math:`y=a+b P_3(c+dx)` 로 다항식을 적겠습니다.
  여기서 :math:`P_3(x)=\frac{1}{2}\left(5x^3-3x\right)` 은 3차
  `르장드르 다항식(Legendre polynomial)`_ 입니다.
  
  .. _르장드르 다항식(Legendre polynomial):
      https://en.wikipedia.org/wiki/Legendre_polynomials
  
  이 구현은 PyTorch 텐서 연산을 사용하여 순전파 단계를 계산하고, PyTorch autograd를 사용하여
  변화도(gradient)를 계산합니다.
  
  아래 구현에서는 :math:`P_3'(x)` 을 수행하기 위해 사용자 정의 autograd Function를 구현합니다.
  수학적으로는 :math:`P_3'(x)=\frac{3}{2}\left(5x^2-1\right)` 입니다.
  """
  import torch
  import math
  
  
  class LegendrePolynomial3(torch.autograd.Function):
      """
      torch.autograd.Function을 상속받아 사용자 정의 autograd Function을 구현하고,
      텐서 연산을 하는 순전파 단계와 역전파 단계를 구현해보겠습니다.
      """
  
      @staticmethod
      def forward(ctx, input):
          """
          순전파 단계에서는 입력을 갖는 텐서를 받아 출력을 갖는 텐서를 반환합니다.
          ctx는 컨텍스트 객체(context object)로 역전파 연산을 위한 정보 저장에 사용합니다.
          ctx.save_for_backward 메소드를 사용하여 역전파 단계에서 사용할 어떤 객체도
          저장(cache)해 둘 수 있습니다.
          """
          ctx.save_for_backward(input)
          return 0.5 * (5 * input ** 3 - 3 * input)
  
      @staticmethod
      def backward(ctx, grad_output):
          """
          역전파 단계에서는 출력에 대한 손실(loss)의 변화도(gradient)를 갖는 텐서를 받고,
          입력에 대한 손실의 변화도를 계산해야 합니다.
          """
          input, = ctx.saved_tensors
          return grad_output * 1.5 * (5 * input ** 2 - 1)
  
  
  dtype = torch.float
  device = torch.device("cpu")
  # device = torch.device("cuda:0") # GPU에서 실행하려면 이 주석을 제거하세요
  
  # 입력값과 출력값을 갖는 텐서들을 생성합니다.
  # requires_grad=False가 기본값으로 설정되어 역전파 단계 중에 이 텐서들에 대한 변화도를 계산할
  # 필요가 없음을 나타냅니다.
  x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
  y = torch.sin(x)
  
  # 가중치를 갖는 임의의 텐서를 생성합니다. 3차 다항식이므로 4개의 가중치가 필요합니다:
  # y = a + b * P3(c + d * x) 
  # 이 가중치들이 수렴(convergence)하기 위해서는 정답으로부터 너무 멀리 떨어지지 않은 값으로
  # 초기화가 되어야 합니다. 
  # requires_grad=True로 설정하여 역전파 단계 중에 이 텐서들에 대한 변화도를 계산할 필요가
  # 있음을 나타냅니다. 
  a = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
  b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)
  c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
  d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)
  
  learning_rate = 5e-6
  for t in range(2000):
      # 사용자 정의 Function을 적용하기 위해 Function.apply 메소드를 사용합니다.
      # 여기에 'P3'라고 이름을 붙였습니다.
      P3 = LegendrePolynomial3.apply
  
      # 순전파 단계: 연산을 하여 예측값 y를 계산합니다; 
      # 사용자 정의 autograd 연산을 사용하여 P3를 계산합니다.
      y_pred = a + b * P3(c + d * x)
  
      # 손실을 계산하고 출력합니다.
      loss = (y_pred - y).pow(2).sum()
      if t % 100 == 99:
          print(t, loss.item())
  
      # autograd를 사용하여 역전파 단계를 계산합니다.
      loss.backward()
  
      # 경사하강법(gradient descent)을 사용하여 가중치를 갱신합니다.
      with torch.no_grad():
          a -= learning_rate * a.grad
          b -= learning_rate * b.grad
          c -= learning_rate * c.grad
          d -= learning_rate * d.grad
  
          # 가중치 갱신 후에는 변화도를 직접 0으로 만듭니다.
          a.grad = None
          b.grad = None
          c.grad = None
          d.grad = None
  
  print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
  
apis: torch.linspace torch.no_grad torch.full torch.sin torch.device