머신러닝에서 Jacobian 이 필요한 이유

신경망 학습은 같은 질문을 계속 반복한다. “파라미터를 아주 조금 바꾸면 loss 가 얼마나 바뀌는가?” 이 답을 알아야 loss 가 줄어드는 방향으로 파라미터를 옮길 수 있다.

그런데 신경망은 벡터를 받아 벡터를 내놓는 층(layer)을 수십 개 쌓은 함수다. 그래서 위 질문은 층마다 쪼개진다. “이 층의 입력을 조금 바꾸면 이 층의 출력이 얼마나 바뀌는가?” 입력도 벡터, 출력도 벡터인 함수에 대해 이 질문의 답을 모아 놓은 것이 Jacobian matrix 다.

이 질문은 파라미터 학습 말고도 딥러닝의 여러 장면에서 다시 나온다.

장면Jacobian 이 답하는 질문다루는 절
역전파 (모든 학습)층마다 구한 Jacobian 을 어떤 순서로 곱해야 파라미터의 gradient 가 나오는가합성 함수의 Jacobian
깊은 모델의 학습 안정성Jacobian 을 수십 번 곱하면 gradient 가 사라지거나 폭발하는 이유, residual connection 이 이를 막는 이유합성 함수의 Jacobian
softmax 와 cross-entropy어휘 수의 제곱만큼 큰 softmax 의 Jacobian 이 왜 “예측 확률 − 정답” 이라는 짧은 gradient 로 줄어드는가Element-wise 연산의 Jacobian
입력 민감도 (attribution, robustness)입력 토큰이나 픽셀을 조금 바꾸면 출력이 얼마나 흔들리는가작은 예제로 계산하기
확률 밀도 변환 (normalizing flow, diffusion)변환이 공간을 얼마나 늘리거나 줄여서 확률 밀도가 묽어지거나 진해지는가Jacobian 행렬식

아래에서는 먼저 Jacobian 이 무엇이고 어떻게 계산하는지 정리한다. 위의 장면들은 각각 필요한 개념을 설명한 절의 끝에서 다룬다.

다변수 함수의 미분을 담는 행렬

변수가 하나인 함수 의 미분 는 숫자 하나다. 이 숫자는 “입력을 에서 아주 조금( 만큼) 움직이면 출력이 얼마나 움직이는가”를 알려준다.

그런데 입력이 벡터이고 출력도 벡터인 함수라면 사정이 달라진다. 예를 들어 입력이 2 개, 출력이 3 개라면, “입력 1번을 움직였을 때 출력 1번의 변화”, “입력 1번을 움직였을 때 출력 2번의 변화” 처럼 따져야 할 조합이 개 생긴다. 숫자 하나로는 이걸 다 담을 수 없다.

Jacobian matrix 는 이 조합들을 모두 모아 놓은 행렬이다. 그리고 위의 일변수 식에서 가 하던 역할을 그대로 이어받는다.

기호뜻
미분을 구하는 기준점 (입력 벡터)
기준점에서 입력을 움직인 작은 변화량 (벡터)
점 에서 계산한 의 Jacobian matrix
입력을 만큼 움직였을 때 출력이 움직이는 양의 어림값

즉, Jacobian 은 “입력 변화 벡터를 넣으면 출력 변화 벡터를 돌려주는 행렬”이다. 기준점 근처의 아주 좁은 범위에서는 복잡한 비선형 함수도 이 행렬 하나를 곱하는 일차식처럼 행동한다. 이렇게 함수를 기준점 근처에서 일차식으로 바꿔 어림하는 것을 선형 근사(linear approximation)라고 한다.

정의

원소 하나하나의 뜻

함수 이 있다고 하자. 이 함수는 차원 벡터 를 받아서 차원 벡터를 내놓는다. 출력의 각 성분을 따로 떼어 보면 개의 실숫값 함수가 나란히 있는 셈이다.

의 Jacobian matrix 는 행렬이고, 원소는 다음과 같다.

는 편미분이다. 다른 입력 변수는 모두 고정해 두고 하나만 조금 움직였을 때, 출력 성분 가 변하는 비율이다. Jacobian 은 이 편미분을 (출력 성분, 입력 성분) 의 모든 쌍에 대해 계산해서 표로 만든 것이다.

크기를 외우는 방법은 간단하다. 행의 수는 출력 개수 , 열의 수는 입력 개수 이다. 이렇게 정해야 앞 절의 에서 차원 입력 변화 를 곱해 차원 출력 변화가 나온다.

행으로 읽기, 열로 읽기

전체 행렬을 펼치면 다음과 같다.

같은 행렬을 두 방향으로 읽을 수 있다.

  • 번째 행: 출력 성분 하나가 입력 각각에 얼마나 민감한지를 늘어놓은 것이다. 이것은 실숫값 함수 의 gradient 를 가로로 눕힌 것(transpose)과 같다.
  • 번째 열: 입력 성분 하나를 움직였을 때 출력 이 각각 얼마나 움직이는지를 세로로 늘어놓은 벡터 다.

여기서 는 열벡터로 쓰는 것이 관례라서, 행에 넣을 때 를 붙였다. gradient 와 Jacobian 의 관계는 Difference between gradient and Jacobian 에 정리되어 있다.

입력과 출력의 형태에 따른 모양

“행 = 출력 개수, 열 = 입력 개수” 규칙을 입력·출력이 스칼라인 경우까지 적용하면, 익숙한 미분들이 모두 Jacobian 의 특수한 경우라는 것이 보인다.

입력출력Jacobian 크기익숙한 이름
스칼라 스칼라 일변수 함수의 미분
벡터 스칼라 (행벡터)gradient 의 transpose
스칼라 벡터 (열벡터)성분별 미분을 세운 벡터 (곡선의 속도 벡터)
벡터 벡터 Jacobian matrix

딥러닝에서 loss 는 스칼라이므로, 파라미터에 대한 loss 의 미분은 두 번째 줄의 경우다. 반면 층 하나는 벡터를 받아 벡터를 내놓으므로 네 번째 줄의 경우다. 역전파는 이 두 종류를 이어 붙이는 계산이다.

실숫값 함수의 이차 미분을 모은 Hessian matrix 도 이 틀로 볼 수 있다. Hessian 은 gradient 를 하나의 벡터 함수로 보고 그 Jacobian 을 구한 행렬이다.

작은 예제로 계산하기

입력 2 개, 출력 2 개인 함수 를 보자.

Jacobian 의 네 칸을 하나씩 채운다. 로 편미분할 때는 를 상수로, 로 편미분할 때는 를 상수로 본다.

  1. : 에서 는 상수이므로
  2. : 에서 은 상수이므로
  3. : 에서 는 상수이므로
  4. : 에서 는 상수이므로

첫 행에는 의 편미분을, 둘째 행에는 의 편미분을 넣는다.

이 결과는 아직 에 따라 값이 바뀌는 행렬이다. 구체적인 점을 넣어야 숫자 행렬이 된다. 점 에서는 다음과 같다.

이 행렬이 정말 “출력 변화를 어림하는 행렬”인지 확인해 보자. 입력을 에서 로, 즉 만큼 움직인다.

  • Jacobian 으로 어림한 출력 변화:
  • 실제 출력 변화: 이고 이므로, 차이는 약

두 값이 소수 셋째 자리까지 맞는다. 가 작을수록 어림값과 실제 값은 더 가까워진다.

입력에 대한 Jacobian 은 민감도다

위 예제에서 출력 는 를 움직이자 나 움직였다. Jacobian 의 칸 값 가 그 배율이다. 이처럼 Jacobian 의 각 칸은 “어느 입력이 어느 출력을 얼마나 세게 흔드는가”를 나타낸다. 학습된 모델의 출력을 입력에 대해 미분하면 같은 정보를 얻는다.

  • 입력 attribution: 분류 모델이라면 정답 클래스의 점수, 언어 모델이라면 다음 토큰의 log-probability 처럼 스칼라 출력 하나를 고르고, 그것을 입력(픽셀, 토큰 embedding)으로 미분한다. 출력이 하나이므로 Jacobian 의 한 행, 즉 gradient 를 구하는 셈이다. 값이 큰 위치가 출력에 영향을 많이 준 입력이라고 보고, 이를 시각화한 것이 saliency map 같은 gradient 기반 설명 기법이다.
  • robustness: Jacobian 의 값이 크면 작은 입력 변화 에도 출력 변화 가 커진다. 사람 눈에는 안 보이는 작은 노이즈로 예측을 바꾸는 adversarial example 이 이 성질을 이용한다. 반대로 학습할 때 입력에 대한 Jacobian 의 모든 칸을 제곱해 더한 값(Frobenius norm 의 제곱)을 loss 에 더하면, 입력이 조금 흔들려도 출력이 덜 흔들리는 모델을 얻는다. Contractive Autoencoder(Rifai et al., 2011)와 Jacobian regularization(Hoffman et al., 2019)이 이 방식이다.

Jacobian 행렬식: 넓이가 몇 배로 바뀌는가

입력과 출력의 차원이 같으면() Jacobian 은 정사각행렬이 되고, 행렬식(determinant)을 구할 수 있다. 이 값을 Jacobian determinant 라고 부르고, 흔히 줄여서 “Jacobian” 이라고도 부른다.

행렬식의 기하학적 뜻은 “이 행렬을 곱하면 넓이(3 차원 이상이면 부피)가 몇 배가 되는가” 다. Jacobian 은 기준점 근처에서 함수가 하는 일을 행렬 하나로 어림한 것이므로, Jacobian determinant 는 기준점 근처의 아주 작은 영역이 함수를 거치면서 넓이가 몇 배로 바뀌는지 를 알려준다.

앞 절의 예제로 계산하면 다음과 같다.

점 에서는 다. 이 값에서 두 가지를 읽는다.

  • 절댓값 : 근처의 작은 정사각형은 를 거친 뒤 넓이가 약 5 배인 평행사변형이 된다.
  • 부호가 음수: 도형이 거울에 비친 것처럼 뒤집힌다. 실제로 축 방향 단위 벡터 은 로, 축 방향 단위 벡터 은 로 옮겨진다. 원래는 첫 벡터에서 둘째 벡터로 반시계 방향으로 돌아가는데, 옮겨진 뒤에는 시계 방향이 된다.

이 성질이 가장 많이 쓰이는 곳은 적분의 변수 변환이다. 직교좌표 를 극좌표 로 바꾸는 함수 의 Jacobian 을 구하면 다음과 같다.

그래서 극좌표로 적분할 때 자리에 를 쓴다. 공간에서 가로 , 세로 인 작은 직사각형이 평면에서는 넓이 인 조각이 된다는 뜻이다. 원점에서 멀수록( 이 클수록) 같은 각도 가 더 긴 호를 쓸고 지나가므로 조각이 넓어진다.

확률 밀도를 변환할 때: normalizing flow

생성 모델 중에는 단순한 분포(예: 표준 정규분포)에서 뽑은 를 신경망 로 변환해 데이터 를 만드는 것들이 있다. 이때 “이 데이터 가 나올 확률 밀도는 얼마인가”를 계산하려면 Jacobian determinant 가 필요하다.

이유는 넓이와 밀도의 관계에 있다. 확률의 총량은 변환 전후에 같다. 그런데 가 어떤 작은 영역의 넓이를 5 배로 늘리면, 같은 양의 확률이 5 배 넓은 곳에 퍼지므로 밀도는 로 묽어진다. 이를 식으로 쓰면 다음과 같다.

기호뜻
변환 전 단순한 분포의 밀도
변환 후 데이터 분포의 밀도
변환 의 점 에서의 Jacobian

normalizing flow 는 이 식으로 log-likelihood 를 정확히 계산해서 학습한다. 문제는 비용이다. 행렬의 행렬식은 일반적으로 에 비례하는 계산이 필요하고, 이미지처럼 이 수만 이상이면 감당할 수 없다. 그래서 RealNVP, Glow 같은 flow 모델은 층의 Jacobian 이 삼각 행렬이 되도록 구조를 설계한다. 삼각 행렬의 행렬식은 대각 원소의 곱이므로 번의 곱셈으로 끝난다.

변환을 연속적인 시간 흐름으로 보는 continuous normalizing flow 나 diffusion 모델의 likelihood 계산에서는 행렬식 대신 Jacobian 의 trace(대각 원소의 합)가 나온다. 이 경우에도 Jacobian 전체를 만들지 않고 무작위 벡터와의 곱으로 trace 를 어림하는 Hutchinson 추정을 쓴다.

합성 함수의 Jacobian 은 Jacobian 들의 곱이다

여러 함수를 차례로 적용한 합성 함수의 Jacobian 은, 각 함수의 Jacobian 을 곱해서 구한다. 일변수 미분의 chain rule 에서 곱셈이 행렬 곱셈으로 바뀐 것이다.

입력 에서 출력 을 만드는 함수 가 네 함수의 합성이라고 하자.

계산은 오른쪽 함수부터 순서대로 일어나고, 중간 결과에 이름을 붙이면 다음과 같다.

단계계산함수의 입력 → 출력 차원그 함수의 Jacobian 크기
1
2
3
4

전체 Jacobian 은 각 단계의 Jacobian 을 마지막 단계부터 왼쪽에 놓고 곱한다.

이 식에서 두 가지를 확인할 수 있다.

  • 크기가 맞물린다. 을 곱하면 이 되어, 의 Jacobian 크기와 일치한다. 순서를 바꾸면 행렬 곱셈 자체가 정의되지 않는다.
  • 각 Jacobian 은 그 함수가 실제로 받은 입력에서 계산한다. 은 원래 입력 가 아니라 에 들어간 중간 결과 에서 구한다. 그래서 먼저 입력부터 순서대로 계산하면서 중간 결과를 저장해 둬야 한다.

역전파는 Jacobian 을 출력 쪽부터 곱한다

신경망이 바로 이런 합성 함수다. 층 하나하나가 이고, 마지막 함수는 스칼라 loss 를 내놓는다(). 그러면 맨 왼쪽 Jacobian 은 행벡터가 된다.

행렬 곱셈은 어느 쪽부터 묶어도 결과가 같지만 비용은 크게 다르다.

  • 왼쪽(출력 쪽)부터 곱하면 매 단계가 “행벡터 × 행렬 = 행벡터” 다. 중간 결과가 늘 벡터 하나라서 저장과 계산이 가볍다.
  • 오른쪽(입력 쪽)부터 곱하면 처럼 행렬끼리 곱해야 하고, 중간 결과의 열 수가 입력 차원 이다. 입력이 수십억 개 파라미터라면 이 행렬을 들고 다닐 수 없다.

backpropagation 이 출력에서 입력 쪽으로 거꾸로 진행하는 이유가 이것이다. 이렇게 행벡터에 Jacobian 을 곱하는 연산을 vector-Jacobian product(VJP)라고 부른다. 역전파 노트의 오차 전달 식 도 행벡터로 바꿔 쓰면 VJP 두 번이다. 여기서 은 번째 층의 activation 이전 값 에 대한 loss 의 미분이다.

은 선형 층 의 Jacobian 이고, 은 activation 의 Jacobian 이다. 뒤쪽 대각 행렬이 나오는 이유는 Element-wise 연산 절에서 다룬다.

실제 프레임워크는 Jacobian 행렬을 만들지 않는다. 크기를 따져보면 만들 수가 없다. hidden 차원이 4096 이고 sequence 길이가 2048 인 Transformer 층은 입력과 출력이 각각 만 개 숫자다. 이 층의 Jacobian 은 칸이고, fp32 로 약 280 TB 다. 그래서 PyTorch 의 각 연산은 Jacobian 대신 “출력 쪽 gradient 벡터를 받아 입력 쪽 gradient 벡터를 돌려주는” VJP 함수만 구현한다. 이것이 각 연산의 backward 다.

Jacobian 이 직접 필요할 때는 torch.func.jacrev(VJP 를 출력 개수만큼 반복), torch.func.jacfwd(입력 쪽부터 곱하는 JVP 를 입력 개수만큼 반복)로 구할 수 있다. 출력이 입력보다 적으면 jacrev, 많으면 jacfwd 가 빠르다.

깊은 모델에서 Jacobian 곱이 커지거나 작아지는 문제

역전파에서 입력 가까운 층에 도착하는 gradient 는 그 위의 모든 층의 Jacobian 을 곱한 결과다. 층이 많으면 같은 효과가 수십 번 반복된다.

각 층의 Jacobian 이 벡터의 길이를 0.9 배로 줄인다고 해보자. 100 층을 지나면 배가 되어 gradient 가 사실상 사라진다. 반대로 1.1 배로 늘린다면 배로 폭발한다. 이것이 vanishing / exploding gradients 다.

residual connection 은 이 문제를 Jacobian 수준에서 해결한다. 층의 출력을 로 만들면 그 Jacobian 은 다음과 같다.

는 대각선이 모두 1 인 단위 행렬(identity matrix)로, 벡터를 그대로 통과시킨다. 의 Jacobian 이 0 에 가까워져도 항은 남으므로, 이 층을 지나는 gradient 가 0 으로 무너지지 않는다. 또 가 작은 동안에는 각 층의 Jacobian 이 단위 행렬에 가까워서, 수십 개를 곱해도 크기가 크게 변하지 않는다. 대부분의 LLM 이 쓰는 Pre-LN Transformer 는 이 더하기 경로에 정규화를 끼우지 않아 항이 그대로 남는다.

Element-wise 연산의 Jacobian 은 대각 행렬이다

신경망에는 벡터의 같은 위치끼리만 계산하는 연산이 많다. 두 벡터의 덧셈 , 같은 위치끼리의 곱 , 벡터의 각 원소에 따로 적용하는 activation 같은 것들이다. 이런 연산을 element-wise 연산이라고 한다.

element-wise 연산에서는 출력의 번째 성분 가 입력의 번째 성분(, )에만 의존한다. 따라서 이면 를 아무리 움직여도 는 변하지 않는다.

Jacobian 에서 인 칸은 대각선 바깥 칸이므로, 대각선만 남은 대각 행렬이 된다.

이고 , 즉 인 경우를 계산해 보자. 이고 나머지 칸은 0 이다.

같은 방법으로 자주 쓰는 연산의 Jacobian 을 구하면 다음과 같다.

연산Jacobian이유
(항등 함수)
(원소별 activation) 는 에만 의존

대각 행렬은 대각선의 개 값만 알면 되므로, 실제 구현에서는 행렬을 만들지 않는다. 대각선 값을 벡터로 들고 있다가 같은 위치끼리 곱하면 행렬 곱셈과 같은 결과가 나온다. 역전파에서 activation 을 지날 때 gradient 에 를 원소별로 곱하는() 이유가 이것이다.

softmax 는 대각 행렬이 아니다: cross-entropy 와 함께 쓰는 이유

출력 성분 하나가 여러 입력 성분에 의존하면 대각선 바깥 칸이 0 이 아니다. softmax 가 대표적이다. 원소마다 를 계산하니 element-wise 처럼 보이지만, 정규화하는 분모 에 모든 입력이 들어간다.

여기서 는 이면 1, 아니면 0 인 기호다. 모든 칸이 채워진 행렬이다. 언어 모델이라면 이 어휘 크기라서, 어휘가 약 13 만 개면 토큰 하나마다 약 170 억 칸짜리 Jacobian 이 생긴다.

그런데 softmax 뒤에 cross-entropy loss 를 붙이면 이 큰 행렬이 사라진다. 정답 분포를 (정답 토큰 위치만 1 인 one-hot 벡터)라 하고, loss 를 로 두자. chain rule 로 loss 를 softmax 입력 로 미분한다.

  1. loss 를 softmax 출력으로 미분하면 다. 이것이 행벡터다.
  2. 이 행벡터에 softmax 의 Jacobian 을 곱한다(VJP).

마지막 단계에서는 이고, 정답 분포의 합 이라는 것을 썼다. 결과는 다음과 같다.

“예측 확률에서 정답을 뺀 것”이라는 짧은 벡터다. 170 억 칸 행렬을 거치지 않고 어휘 크기만큼의 뺄셈으로 끝난다. PyTorch 의 F.cross_entropy 가 확률이 아니라 softmax 이전의 logit 을 입력으로 받는 이유도 여기에 있다. 두 연산을 한 번에 처리하면 이 단순한 gradient 를 바로 쓸 수 있고, 확률을 따로 계산했다가 log 를 취할 때 생기는 수치 오차도 피할 수 있다.

References