Deep DiveBengio·Ducharme·Vincent·Jauvin JMLR 3 1137–1155 원문 PDF와 JMLR 서지 페이지 직접 확인·Vaswani et al. Transformer 원문과 Mikolov et al. word2vec 원문 직접 확인·Python 3.9.6 표준 라이브러리로 원 논문 식 (1)의 y=b+Wx+U tanh(d+Hx) 구현·C W H 세 parameter의 analytic gradient와 central finite difference 검산·한 gradient descent step의 NLL 감소 확인

단어를 ID가 아니라 좌표로 배우면 못 본 문장도 왜 확률을 가질까

Bengio·Ducharme·Vincent·Jauvin의 2003년 Neural Probabilistic Language Model 원 논문을 12세 독자 기준으로 해부한다. 희소한 n-gram 표의 한계에서 출발해 공유 embedding table C와 tanh hidden layer, direct path W, full softmax, log likelihood와 perplexity, gradient 흐름, Brown·AP News 결과와 병목, Python 표준 라이브러리 finite difference 직접 검산, Transformer와의 연결과 한계를 사실과 해석으로 분리한다.

3단계 두 번째 논문. 이 글은 Yoshua Bengio, Réjean Ducharme, Pascal Vincent, Christian Jauvin의 A Neural Probabilistic Language Model 원문 PDF를 직접 읽고 썼다. 논문은 2003년 Journal of Machine Learning Research 3권 1137–1155쪽에 실렸고, JMLR 서지 페이지도 같은 제목·저자·권호를 확인한다. **Neural Probabilistic Language Model, NNLM(신경 확률 언어 모델)**은 단어를 서로 완전히 무관한 번호로 두지 않고, 학습되는 실수 좌표인 embedding(임베딩)에 놓은 뒤 최근 몇 단어로 다음 단어의 확률 분포를 내는 모델이다. 이 글에서는 원 논문의 약칭이 따로 없으므로 NPLM이라고 적는다.

이 글에서 얻을 답과 범위

LLM을 먼저 보면 token embedding은 코드의 첫 layer이고 next-token prediction은 당연한 일처럼 보인다. 그러나 왜 단어마다 숫자 하나가 아니라 vector(벡터)가 필요한지, 왜 그 vector를 따로 학습하지 않고 언어 모델과 함께 고치는지 모르면 embedding은 그냥 큰 배열 이름이 된다.

이 글을 읽으면 다음 질문에 답할 수 있어야 한다.

  1. count n-gram(개수 기반 n-그램)은 왜 한 번도 못 본 문맥에 약하며 smoothing(스무딩)이 무엇을 해결하고 무엇을 못 하는가
  2. 원 논문의 C, H, W, U, b, d는 각각 무엇이며 어떤 shape(행렬 크기)를 가지는가
  3. context word의 embedding을 이어 붙인 x가 y=b+Wx+U tanh(d+Hx)를 거쳐 softmax 확률이 되는 순서는 무엇인가
  4. log likelihood(로그 우도)를 최대로 하는 것과 negative log likelihood, NLL(음의 로그 우도)를 최소로 하는 것이 왜 같은 학습인가
  5. output softmax가 왜 이 모델의 큰 병목이며 원 논문은 CPU cluster에서 무엇을 나누었는가
  6. embedding table의 한 원소가 실제로 gradient를 받는지 어떻게 finite difference(유한 차분)로 검산하는가
  7. 이 논문이 Transformer와 word2vec의 출발점 중 어디에 놓이고, RAG나 GraphRAG를 만들기 전에 왜 알아야 하는가

범위는 2003년 원 논문의 fixed-window neural language model(고정 창 신경 언어 모델), 병렬 학습, Brown corpus와 AP News 실험, 저자들이 적은 에너지 모델 확장까지다. subword tokenization(서브워드 토큰화), RNN, LSTM, attention, modern GPU kernel, Transformer pretraining 전체는 다루지 않는다. 다만 현재 시스템에서 무엇이 계승되고 무엇이 바뀌었는지는 이 논문을 과대평가하지 않도록 경계로 설명한다.

먼저 잡을 한 문장

이 논문은 단어를 독립된 이름표가 아니라 학습되는 좌표로 바꾸고, 비슷한 좌표의 문맥에는 비슷한 다음 단어 확률을 주도록 하여, 관측하지 못한 조합에도 확률을 나누어 주려 했다.

왜 필요한가와 해결하려는 문제

12살 비유로 보는 단어 조합 폭발

단어 사전에 100,000개 단어가 있다고 하자. 문장 10칸을 채우는 가능한 방법은 대략 100,000을 열 번 곱한 수다. 원 논문은 이 경우 자유롭게 정해야 할 확률 수가 100000^10 - 1 = 10^50 - 1개가 될 수 있다고 든다. 아무리 큰 책장을 가져와도 실제 학습 문장으로 이 표를 채울 수 없다.원 논문 1절

사전 크기 V = 100000
문장 길이 10

가능한 순서의 수 = V^10 = 10^50
훈련에서 본 문장 수 << 10^50
→ 한 번도 못 본 문장이 예외가 아니라 기본 상태다

문제의 이름은 curse of dimensionality(차원의 저주)다. 여기서 차원은 화면의 가로세로가 아니다. 앞 단어 자리에 들어갈 선택지가 하나 더 늘 때마다 가능한 문맥 조합이 곱셈으로 폭발한다는 뜻이다. 단어 ID만 비교하면 cat을 dog로 바꾼 문맥과 완전히 다른 문맥을 구별할 방법도 없다.

먼저 있던 답인 n-gram과 그 경계

언어 모델은 문장 전체 확률을 다음 단어 conditional probability(조건부 확률)의 곱으로 쓸 수 있다.

[ \hat P(w_1^T)=\prod_{t=1}^{T}\hat P(w_t\mid w_1^{t-1}) ]

기호
(w_t)t번째 단어 또는 token
(w_i^j)i번째부터 j번째까지의 연속 구간
(\hat P)데이터로 배운 확률 추정값
(T)문장 또는 corpus의 길이

하지만 과거 전체를 조건으로 한 표는 너무 크다. 그래서 n-gram은 최근 n-1개 단어만 남긴다.

[ \hat P(w_t\mid w_1^{t-1}) \approx \hat P(w_t\mid w_{t-n+1}^{t-1}) ]

예를 들어 trigram(트라이그램)은 앞 두 단어만 보고 다음 단어를 세는 방식이다. 본 적 있는 i like cats의 횟수는 바로 쓸 수 있다. 하지만 i adore cats를 못 봤다면 그 정확한 key는 없다. smoothing은 이때 확률을 0으로 만들지 않기 위해 bigram, unigram, 전체 빈도 쪽으로 물러나는 방법이다. 원 논문이 비교한 interpolated trigram도 문맥 빈도에 따라 unigram·bigram·trigram 확률을 섞는다.

count table의 질문
  i adore라는 정확한 두 단어를 본 적 있는가

NPLM의 질문
  adore의 좌표가 like의 좌표 근처인가
  그리고 그 작은 좌표 변화 뒤에도 다음 단어 점수가 자연스럽게 바뀌는가

둘은 경쟁만 하는 관계가 아니다. 원 논문은 neural model과 interpolated trigram의 출력 확률을 반반 섞었을 때도 실험에서 perplexity가 더 낮아졌다고 보고했다. 이는 두 모델이 서로 다른 종류의 실수를 했다는 관찰이지, 단어 의미를 사람이 이해했다는 증거는 아니다.원 논문 2절과 4.2절

이 논문이 제안한 탈출구는 표 전체가 아니라 공유 좌표다

원 논문의 직관은 다음과 같다. The cat is walking in the bedroom을 봤다면 A dog was running in a room도 완전히 처음부터 배울 필요가 없다. cat과 dog, bedroom과 room이 문법적·의미적 역할이 비슷한 좌표에 놓이고, 확률 함수가 그 좌표에서 부드럽게 변한다면 한 문장이 여러 이웃 조합에 정보를 준다.

여기서 부드럽다는 사람 감정의 표현이 아니다. 입력 vector가 조금 바뀔 때 neural network의 score와 softmax 확률도 보통 조금씩 바뀌도록 parameter를 학습한다는 모델 가정이다. 그 가정이 data에 맞는지는 validation perplexity와 실제 downstream task로 별도 검증해야 한다.

무엇인가와 하지 않는 일

NPLM은 finite vocabulary(유한 어휘집) 안의 각 word ID에 m개 실수 feature를 붙인다. 원 논문은 이 table을 C라고 쓴다. 이어서 최근 n-1개 단어의 feature를 이어 붙여 feed-forward neural network(순방향 신경망)에 넣고, vocabulary 전체 다음 단어의 확률 분포를 낸다.

word ID
  → C에서 행을 찾는다
  → 최근 n-1개 행을 순서대로 이어 x를 만든다
  → hidden layer와 output layer가 각 후보 단어의 점수 y를 만든다
  → softmax가 y 전체를 합 1인 확률 p로 바꾼다
  → 실제 다음 단어의 log p를 크게 만든다

NPLM이 하는 일

입력 문맥이 i like일 때 다음 단어가 cats, dogs, 마침표 같은 후보일 확률을 모두 낸다. 중요한 것은 C(i)가 단어 i의 설명문이나 정답 label이 아니라 학습 중에 함께 움직이는 parameter 행이라는 점이다. like가 앞에서 나올 때마다 같은 C(like) 행을 재사용하므로, 한 위치에서 배운 변화가 다른 문맥 위치에서도 공유된다.

NPLM이 하지 않는 일

하지 않는 일이유
과거 전체를 기억입력이 마지막 n-1개로 고정된다
문장 의미를 논리적으로 증명확률이 높다는 것은 학습 분포에서 그 다음 단어가 그럴듯하다는 뜻뿐이다
vocabulary 밖 단어를 기본 구조에서 정상 예측기본 C와 output은 고정된 (
embedding만 따로 제공C는 next-word 확률 objective와 함께 배운다
RAG처럼 외부 문서를 검색retrieval component가 전혀 없다
Transformer처럼 모든 이전 위치를 attention으로 선택각 예제에서 보는 창 길이가 고정이다

원 논문 5.1절은 기본 architecture에서 output word 자체의 feature vector를 쓰지 않고, 거대한 output layer가 output word 사이의 유사성을 활용하지 못한다고 스스로 지적한다. 이후 제안한 energy model은 target word도 C에 넣지만, 이 글의 중심인 식 (1)의 기본 모델과 같은 구현이 아니다.

선행 개념을 재귀적으로 닫기

vocabulary와 word ID는 뜻이 아니라 주소다

vocabulary는 model이 아는 token 목록이다. C의 17번째 행을 찾으려면 cat → 17 같은 mapping이 필요하다. 17이라는 숫자 자체에 고양이 의미는 없다. training, validation, inference가 완전히 같은 vocabulary 순서와 same unknown-word rule을 써야 한다. 행 순서가 달라지면 C[17]의 좌표가 다른 단어에 붙어 모델은 조용히 망가진다.

원 논문 Brown 실험은 출현 빈도가 3 이하인 rare word를 하나의 symbol로 합쳐 vocabulary를 47,578개에서 16,383개로 줄였다. AP News는 대소문자와 숫자 형식을 매핑하고 rare word와 proper noun에도 special symbol을 사용해 17,964개를 썼다. 이 전처리는 architecture 장식이 아니라 output 크기·OOV·perplexity 비교를 바꾸는 모델 일부다.원 논문 4절

embedding은 압축된 뜻 사전이 아니라 학습되는 좌표 행이다

[ C\in\mathbb R^{|V|\times m} \qquad C(i)\in\mathbb R^m ]

항목shape역할
(C)(V
(C(i))(m)i번 단어를 lookup한 한 행
(V)
(m)스칼라단어 하나에 부여한 feature 수

C의 각 축에 사람이 이름을 붙일 필요는 없다. 0.72가 반드시 동물성, -0.18이 반드시 복수성을 뜻하지 않는다. 여러 축이 함께 다음 단어 확률을 낮추는 방향으로만 학습된다. 그래서 가까운 vector가 사람 기준의 동의어라는 보장은 없고, task에 유용한 문맥 역할이 비슷하다는 것이 더 정확한 설명이다.

score와 softmax와 확률은 같은 숫자가 아니다

network는 먼저 각 후보 단어의 unnormalized log-probability(정규화 전 로그 점수) y_i를 만든다. y_i가 크면 i번 단어를 더 선호하지만, 아직 확률은 아니다. softmax는 모든 후보를 양수이고 합이 1인 확률로 바꾼다.

[ p_i=\hat P(w_t=i\mid\text{context}) =\frac{e^{y_i}}{\sum_{j=1}^{|V|}e^{y_j}} ]

해석가능한 범위
(y_i)i번 단어의 비교용 score모든 실수
(e^{y_i})양수 weight0보다 큼
(p_i)i번 단어의 normalized probability0과 1 사이
(\sum_i p_i)후보 전체 확률정확히 1

softmax의 분모가 모든 vocabulary 단어를 훑어야 한다는 점이 뒤에서 볼 병목이다. 또한 큰 y에 바로 exp를 하면 overflow(표현 범위 초과)가 날 수 있다. 원 논문 3.2절은 모든 logit에서 최대값 Q=max(y)를 빼 exp(y_i-Q)를 계산하라고 적는다. 분자와 분모에 같은 양수를 곱하거나 나눈 것은 비율을 바꾸지 않으므로 확률은 같다.

log likelihood와 perplexity는 무엇을 벌주는가

정답 next word가 cats이고 model이 그 확률을 0.8로 줬다면 좋은 예측이다. 0.001을 줬다면 심하게 나쁘다. log를 쓰면 문장 확률의 곱이 합으로 바뀐다.

[ \log\hat P(w_1^T)=\sum_{t=1}^{T}\log \hat P(w_t\mid\text{context}_t) ]

학습에서는 이 값을 최대화한다. 현대 코드에서는 보통 부호를 뒤집은 NLL을 최소화한다.

[ \operatorname{NLL} =-\frac{1}{N}\sum_{t=1}^{N}\log p_{t,\text{target}} \qquad \operatorname{PPL}=\exp(\operatorname{NLL}) ]

perplexity, PPL(혼란도)은 평균적으로 model이 몇 개 후보 중 하나를 고르는 것처럼 불확실한지를 나타내는 변환값이다. 낮을수록 같은 tokenization과 같은 evaluation set에서 더 좋다. 서로 다른 vocabulary, unknown token 처리, 문장 끝 처리, tokenization으로 낸 PPL을 나란히 비교하면 숫자가 공정하지 않을 수 있다.

밑바닥 원리와 알고리즘을 식으로 끝까지 따라가기

입력 창 하나를 실제 shape로 만든다

model order를 n=3, feature 수를 m=2라고 하자. 최근 두 단어를 본다는 뜻이다.

context       [i, like]
C(i)          [ 0.20,  0.10]
C(like)       [-0.10,  0.30]

x             [ 0.20,  0.10, -0.10,  0.30]
shape         (n-1)m = 4

원 논문은 시간상 가장 가까운 단어부터 x=(C(w_{t-1}), C(w_{t-2}), …, C(w_{t-n+1}))로 쓴다. 위 예시는 읽기 쉬워 앞에서 먼 쪽부터 적었을 뿐이다. 순서를 임의로 뒤집어 바꾸면 안 된다. H와 W의 column은 특정 위치의 의미를 학습하므로 training과 serving에서 같은 convention을 유지해야 한다.

원 논문 식 (1)은 두 길을 합친다

hidden unit 수를 h라 하면 원 논문의 network는 다음과 같다.

[ \begin{aligned} o &= d+Hx \ a &= \tanh(o) \ y &= b+Wx+Ua \ p &= \operatorname{softmax}(y) \end{aligned} ]

parametershape한 줄 역할
(C)(V
(H)(h\times(n-1)m)feature 창을 hidden pre-activation으로 보낸다
(d)(h)hidden bias
(U)(V
(W)(V
(b)(V
(y,p)(V

U tanh(d+Hx)는 비선형 hidden path다. Wx는 word feature에서 output으로 가는 direct connection(직결 경로)다. 원 논문은 direct connection을 원하면 쓰고, 원하지 않으면 W=0으로 둘 수 있다고 명시한다. 이 path를 빼고 단순 MLP 그림만 그리면 원 논문의 식 (1)을 빠뜨린 설명이 된다.

왜 두 길인가. 직결 경로는 input feature에서 logit으로 가는 비교적 선형적인 관계를 빨리 담을 capacity를 주고, hidden path는 더 복잡한 조합을 담는다. 원 논문의 Brown 결과 해석은 direct connection이 더 빠른 수렴에 기여할 수 있지만, 없는 경우 hidden bottleneck이 generalization에 도움을 줬을 가능성도 있다고 적는다. 인과가 확정된 결론은 아니다.원 논문 4.2절

학습 목적과 gradient가 흘러가는 길

논문의 regularized objective는 corpus에서 다음을 크게 만든다.

[ L(\theta)=\frac{1}{T}\sum_t \log f(w_t,w_{t-1},\ldots,w_{t-n+1};\theta)+R(\theta) ]

R(θ)는 overfitting을 줄이려는 regularization term이다. 원 논문 실험에서는 neural network weight와 C에는 weight decay를 적용하고 bias에는 적용하지 않았다. 한 example의 target을 r이라고 하고 NLL을 미분하면 softmax output 쪽 출발점은 다음처럼 간결하다.

[ \frac{\partial\operatorname{NLL}}{\partial y_i} =p_i-\mathbb 1[i=r] ]

𝟙[i=r]은 i가 정답이면 1, 아니면 0이다. 직관은 간단하다. 정답 단어에는 p_r-1이라는 음수 gradient가 생겨 gradient descent 후 그 score가 올라가고, 다른 단어에는 양수 gradient가 생겨 score가 내려간다.

target r
  ← output logit y 전체
  ← U와 W 그리고 hidden activation a
  ← tanh 전 값 o와 H
  ← 이어 붙인 x의 각 구간
  ← context에 실제로 등장한 C의 행

마지막 줄이 embedding 학습의 핵심이다. C(like)는 별도 사전에서 고정한 값이 아니라, like가 input window에 들어온 example의 NLL을 줄이는 gradient를 받는다. 같은 word가 window 두 위치에 반복되면 두 위치에서 온 gradient를 더해야 한다. 원 논문 3.2절의 parallel algorithm도 C(w_{t-k})를 ∂L/∂x의 해당 block으로 갱신한다고 적는다.

논문 표기와 현대 코드 표기가 부호만 다른 이유

원 논문은 log P를 키우므로 stochastic gradient ascent를 쓴다.

[ \theta\leftarrow\theta+\epsilon \frac{\partial\log \hat P(w_t\mid\text{context})}{\partial\theta} ]

이 글의 재현 코드는 -log P를 줄이므로 gradient descent를 쓴다.

[ \theta\leftarrow\theta-\epsilon \frac{\partial\operatorname{NLL}}{\partial\theta} ]

두 식은 NLL=-log P라서 같은 방향이다. 더하기와 빼기만 보고 구현을 섞으면 target 확률이 줄어드는 반대 학습을 하게 된다.

내부 구조와 실제 실행 흐름

한 training example에서 메모리와 계산이 움직이는 순서

다음은 n=5이면 앞 네 token으로 다섯 번째 token을 맞히는 한 example의 실제 흐름이다.

corpus token IDs
  → 최근 4개 ID로 C의 4개 행을 gather
  → concatenate하여 x 길이 4m 생성
  → o=d+Hx
  → a=tanh(o)
  → y=b+Wx+Ua
  → y의 최대값을 뺀 full softmax
  → target token의 -log p 계산
  → output에서 C의 선택된 행까지 backward
  → parameter update

C lookup은 한 행 전체를 읽는 table access다. C 전체를 매 example마다 고치지 않는다. 현재 context에 등장한 행만 gradient를 받는다. 반대로 full softmax를 정확히 계산하면 b, U, W의 모든 output word 행은 p_i-1[i=r] 때문에 gradient를 받는다. vocabulary가 커질 때 output 쪽이 비싸지는 이유다.

원 논문은 |V|=17,964, h=60, n=6, m=100인 AP News architecture에서 output unit weighted sum이 한 example 연산의 약 99.7%라고 계산했다. 이를 줄이기 위해 CPU마다 vocabulary output block을 맡기고, local exponential sum을 합쳐 softmax 분모를 만들며, hidden·input 쪽 gradient를 다시 합쳤다. 논문이 사용한 환경은 1.2 GHz Athlon 32×2 CPU와 Myrinet network, MPI였다. 이는 2003년 해당 구현의 관찰이지 2026년 GPU throughput benchmark가 아니다.원 논문 3절

parameter 수를 직접 세면 왜 output이 지배적인지 보인다

원 논문이 direct connection을 포함해 제시한 free parameter 수는 다음이다.

[ |V|(1+nm+h)+h(1+(n-1)m) ]

앞의 |V|nm은 C와 W를 합쳐 나온 항이고 |V|h는 U다. 예를 들어 |V|=100,000, n=5, m=256, h=1024를 넣으면 231,549,600개 parameter다. float32 weight만 둬도 약 883.3 MiB다. 이 수치는 원 논문 benchmark가 아니라 위 식에 내가 대입해 계산한 값이며, optimizer state·activation·gradient memory는 포함하지 않는다.

비용 항목대략적 시간 또는 저장 비용커지는 원인
embedding lookup(O((n-1)m)) 읽기context 길이와 feature 수
hidden 계산(O(h(n-1)m))hidden 수와 창 길이
full output score와 softmax(O(V
C 저장(O(V
W와 U 저장(O(V

Big-O만으로는 W를 빼거나 h를 줄였을 때의 실제 shape가 보이지 않는다. output 후보 수가 커질수록 한 문맥의 모든 후보를 다시 점수화하는 dense softmax가 지배한다. 원 논문 5.2절의 class tree와 output subset 제안, 이후 hierarchical softmax·sampled objective 계열은 이 벽에서 나온다.

원 논문 실험을 숫자와 경계까지 읽기

원 논문은 Brown corpus 1,181,041 words를 앞 800,000 training, 다음 200,000 validation, 나머지 181,041 test로 나눴다. AP News는 training 약 13,994,528 words, validation 963,138, test 963,071 words다. 이 수치와 preprocessing은 원 논문 4절의 값이다.

corpus비교에서 낮은 test PPL비교한 상대해석 경계
BrownMLP10 mixture 252class-based back-off 31224퍼센트 차이는 neural output과 trigram을 0.5로 섞은 MLP10을 포함한다
AP NewsMLP10 mixture 1095-gram Kneser-Ney back-off 1178퍼센트 차이 역시 mixture 결과다

이 표가 말하는 확인된 사실은 당시 두 corpus와 그 split·전처리·model selection에서 PPL이 낮았다는 것이다. 이것만으로 오늘의 tokenizer, long context, web-scale data, GPU 학습에서도 같은 비율로 우월하다고 결론내릴 수 없다. AP News 실험은 40 CPU로 약 3주 동안 5 epoch를 수행했다고 보고한다. 성능 숫자를 읽을 때 dataset, vocabulary, mixture 여부, hardware를 떼어내면 논문의 주장을 바꾸게 된다.원 논문 표 1·표 2와 4.2절

직접 검증으로 C와 softmax gradient를 확인하기

검증 범위와 환경

직접 실행한 사실이다. macOS에서 Python 3.9.6 표준 라이브러리만 썼다. vocabulary 6개, feature 2개, hidden unit 3개인 고정 parameter의 i like → cats 한 example을 만들었다. 이 구현은 원 논문 식 (1)의 두 output path Wx와 U tanh(d+Hx)를 모두 포함한다. corpus 재현이나 논문 PPL 재현은 아니다.

다음 파일을 저장해 실행하면 된다. central_difference는 관심 parameter를 +ε, -ε로 조금씩 움직여 실제 NLL 차이를 나눈 값이다. backpropagation으로 얻은 analytic gradient와 거의 같아야 한다.

#!/usr/bin/env python3
import copy
import math

tokens = ["<s>", "i", "like", "cats", "dogs", "."]
idx = {token: i for i, token in enumerate(tokens)}
context = [idx["i"], idx["like"]]
target = idx["cats"]
V, m, h = len(tokens), 2, 3

def zeros(rows, cols):
    return [[0.0] * cols for _ in range(rows)]

def softmax(y):
    q = max(y)
    e = [math.exp(value - q) for value in y]
    return [value / sum(e) for value in e]

def init():
    return {
        "C": [[.10,-.20],[.20,.10],[-.10,.30],[.40,-.10],[-.30,.20],[.00,.10]],
        "H": [[.20,-.10,.10,.30],[-.20,.20,.10,-.10],[.10,.20,-.30,.10]],
        "W": [[.10,-.20,.00,.10],[.00,.10,-.10,.20],[.20,-.10,.10,.00],[-.10,.20,.30,-.20],[.10,.00,-.20,.10],[.00,-.10,.20,.10]],
        "U": [[.10,.00,-.10],[-.10,.20,.10],[.00,.10,.20],[.20,-.10,.10],[-.20,.10,.00],[.10,.20,-.20]],
        "b": [.00,.10,-.10,.20,-.20,.00],
        "d": [.00,.10,-.10],
    }

def forward(p):
    x = [value for word in context for value in p["C"][word]]
    o = [p["d"][r] + sum(p["H"][r][c] * x[c] for c in range(4)) for r in range(h)]
    a = [math.tanh(value) for value in o]
    y = [p["b"][word] + sum(p["W"][word][c] * x[c] for c in range(4)) + sum(p["U"][word][unit] * a[unit] for unit in range(h)) for word in range(V)]
    return x, a, softmax(y)

def loss_and_grad(p):
    x, a, prob = forward(p)
    loss = -math.log(prob[target])
    dy = prob[:]
    dy[target] -= 1.0
    g = {"C": zeros(V,m), "H": zeros(h,4), "W": zeros(V,4), "U": zeros(V,h), "b": dy[:], "d": [0.0]*h}
    for word in range(V):
        for col in range(4):
            g["W"][word][col] = dy[word] * x[col]
        for unit in range(h):
            g["U"][word][unit] = dy[word] * a[unit]
    da = [sum(p["U"][word][unit] * dy[word] for word in range(V)) for unit in range(h)]
    do = [da[unit] * (1.0-a[unit]*a[unit]) for unit in range(h)]
    for unit in range(h):
        g["d"][unit] = do[unit]
        for col in range(4):
            g["H"][unit][col] = do[unit] * x[col]
    dx = [sum(p["W"][word][col]*dy[word] for word in range(V)) + sum(p["H"][unit][col]*do[unit] for unit in range(h)) for col in range(4)]
    for pos, word in enumerate(context):
        for feature in range(m):
            g["C"][word][feature] += dx[pos*m+feature]
    return loss, g, (x, a, prob)

def loss(p):
    return -math.log(forward(p)[2][target])

def numeric(p, name, row, col, eps=1e-5):
    plus, minus = copy.deepcopy(p), copy.deepcopy(p)
    plus[name][row][col] += eps
    minus[name][row][col] -= eps
    return (loss(plus)-loss(minus))/(2*eps)

def step(p, g, lr=.20):
    for name in ("C","H","W","U"):
        for r in range(len(p[name])):
            for c in range(len(p[name][r])):
                p[name][r][c] -= lr*g[name][r][c]
    for name in ("b","d"):
        for i in range(len(p[name])):
            p[name][i] -= lr*g[name][i]

p = init()
before, g, (x, a, prob) = loss_and_grad(p)
print("x", [round(v,6) for v in x])
print("hidden", [round(v,6) for v in a])
print("p(cats) before", f"{prob[target]:.9f}")
for label, name, row, col, analytic in [
    ("C[like][0]", "C", idx["like"], 0, g["C"][idx["like"]][0]),
    ("W[cats][1]", "W", idx["cats"], 1, g["W"][idx["cats"]][1]),
    ("H[0][2]", "H", 0, 2, g["H"][0][2]),
]:
    estimated = numeric(p, name, row, col)
    print(label, f"analytic={analytic:.9f}", f"numeric={estimated:.9f}", f"diff={abs(analytic-estimated):.3e}")
step(p, g)
print("NLL before", f"{before:.9f}")
print("NLL after", f"{loss(p):.9f}")

내 실행 결과는 다음과 같다.

Python 3.9.6
x [0.2, 0.1, -0.1, 0.3]
hidden [0.109558, 0.039979, 0.0]
p(cats) before 0.183880746
C[like][0] analytic=-0.218752263 numeric=-0.218752263 diff=1.284e-12
W[cats][1] analytic=-0.081611925 numeric=-0.081611925 diff=5.882e-13
H[0][2] analytic=0.017497667 numeric=0.017497667 diff=7.913e-12
NLL before 1.693467850
NLL after 1.476821074

세 결과의 차이가 10^-12에서 10^-11 수준인 것은 analytic backprop gradient가 이 입력에서 실제 loss 곡선의 local slope와 맞았다는 확인이다. 한 step 뒤 NLL이 1.693에서 1.477로 줄고 cats 확률은 0.183880746에서 0.228362483으로 올랐다. 이 결과는 이 고정 toy parameter와 한 example의 정상 경로만 보인다. generalization, 장기 training 안정성, 원 논문 실험 결과를 증명하지 않는다.

값을 하나 바꾸고 실패를 읽는 방법

step의 learning rate를 크게 바꾸면 한 step의 NLL이 항상 줄지 않을 수 있다. gradient는 현재 점의 가장 가파른 1차 근사 방향이지, 모든 크기의 이동에 대한 보증이 아니다. softmax에서 q=max(y)를 지우고 큰 score를 주면 math.exp가 overflow할 수 있다. 논문이 2003년에 이미 최대 logit을 빼는 안정화 단계를 parallel algorithm에 넣은 이유다.

검증할 때는 아래 순서가 빠르다.

  1. sum(prob)가 부동소수점 오차 범위에서 1인지 확인한다.
  2. target probability가 0보다 큰지 확인한다.
  3. 한 parameter의 analytic gradient와 central difference를 비교한다.
  4. 아주 작은 learning rate 한 step 뒤 NLL이 줄어드는지 본다.
  5. context 순서, vocabulary ID 순서, target index가 training과 evaluation에서 같은지 확인한다.

gradient check가 틀리면 먼저 dy[target] -= 1, tanh 미분 1-a², direct path W에서 dx로 들어가는 gradient, repeated context word의 +=를 점검한다. 이 네 곳은 식을 외우고 구현을 생략할 때 자주 빠진다.

성능과 트레이드오프

얻는 일반화와 치르는 계산 비용

선택얻는 것잃는 것
learned C비슷한 input word를 서로 다른 문맥에 재사용vector 거리가 사람이 기대한 의미 유사도라는 보장 없음
fixed window를 2에서 4 단어로 확대원 논문 Brown에서 neural model은 더 긴 문맥을 활용한 결과를 보고창보다 먼 의존성은 원천적으로 못 봄
hidden path비선형 문맥 조합matrix multiply와 parameter 증가
direct W path더 큰 용량과 빠른 수렴 가능성거대한 output 관련 parameter 증가와 일반화 tradeoff
full softmax정확히 normalize된 다음 단어 확률매 example마다 모든 vocabulary output 계산
trigram mixture서로 다른 오류를 보완할 가능성두 model 저장·학습·tuning이 필요하고 원인 해석이 더 어려움

원 논문의 핵심 일반화는 input context 쪽에서 일어난다. C(cat)과 C(dog)가 가까워지면 input으로 cat을 본 경우의 학습이 dog 문맥에도 영향을 줄 수 있다. 하지만 기본 output layer의 cats와 dogs는 각자 U·W·b 행을 가지므로 output 쪽의 parameter 공유는 약하다. 논문 5.1절의 self-critique를 함께 읽어야 embedding이라는 단어 하나로 모델 능력을 과장하지 않게 된다.

training과 serving에서 따로 보는 병목

training은 target을 알고 있으므로 NLL와 gradient를 만든다. full softmax에서는 정답 하나만 알아도 분모의 모든 후보 score가 필요하다. serving은 target 없이 argmax나 sampling을 위해 같은 분포를 계산한다. 둘 다 output dimension이 병목이지만 training은 backward와 optimizer state까지 더 든다.

원 논문은 output unit block을 processor에 나눴다. 오늘의 system에서는 matrix multiplication kernel, data parallelism, tensor parallelism, vocabulary parallelism 같은 다른 구현을 쓸 수 있다. 그러나 정확한 full vocabulary normalization이 비싸다는 계산 모양 자체는 이 작은 모델에서도 이미 보인다. hardware 이름을 바꾼다고 vocabulary output의 크기가 사라지는 것은 아니다.

실패와 운영에서 지켜야 할 기준

모델이 조용히 틀리는 다섯 경우

실패겉으로 보이는 현상먼저 확인할 것
vocabulary mapping 불일치그럴듯해 보이지만 엉뚱한 다음 단어model artifact와 serving dictionary의 hash와 row order
context 순서 반전loss가 잘 안 내려가거나 성능 급락x의 block이 w_{t-1}부터인지 반대인지
unstable softmaxinf, NaN, 0 확률max logit subtraction과 dtype 범위
OOV 증가unknown token 예측이 과도하게 많음rare word rule과 domain drift
창 밖 의존성멀리 나온 주어·부정·인용을 놓침n-1이라는 hard limit와 long-context evaluation

vocabulary와 preprocessing은 versioned model artifact로 묶어 배포해야 한다. tokenizer나 unknown token rule을 바꾸면 같은 C checkpoint를 그대로 쓰지 않는다. PPL dashboard도 corpus release, split, tokenization, vocabulary, sentence-boundary rule을 함께 기록해야 회귀를 원인별로 비교할 수 있다.

병렬 업데이트는 속도와 재현성을 동시에 보지 않는다

원 논문 3.1절은 shared-memory data parallel 구현에서 lock 때문에 느려진 뒤 asynchronous update를 사용했다고 쓴다. 동시에 쓴 update 일부가 덮여 noise가 생길 수 있지만 그 실험에서는 training이 명백히 느려지지 않았다고 관찰했다. 이것은 당시 설정의 경험적 관찰이다. 현재 production training에서 그 문장을 읽고 loss된 update를 무조건 허용하면 안 된다.

재현 가능성이 필요하면 seed, data ordering, learning-rate schedule, vocabulary artifact, weight decay 범위, hardware와 distributed update semantics를 남긴다. 속도를 택해 stale gradient나 non-deterministic reduction을 허용했다면, validation PPL의 분산과 재시작 가능성까지 함께 확인한다.

품질 지표의 경계도 운영한다

PPL이 낮아도 factuality(사실 충실성), safety(안전성), retrieval quality(검색 품질), user task success(사용자 과업 성공)를 보장하지 않는다. NPLM은 외부 knowledge base를 보지 않고, 문장 후보의 사실 여부를 검사하지 않으며, 길이가 긴 reasoning을 유지하지도 않는다. RAG나 GraphRAG service를 설계할 때 NPLM의 교훈은 답을 생성하는 model 자체보다도 ID를 shared continuous representation으로 바꾸면 관측 희소성을 어떻게 완화하는가에 있다.

대안과 선택 기준

같은 문제를 푸는 층위가 다르다

방법문맥 처리output 비용지금 선택할 때
smoothed n-gram최근 정확한 word 조합lookup 중심작은 도메인에서 해석 가능한 local frequency baseline이 필요할 때
NPLM최근 n-1개 embedding의 nonlinear 함수full softmax가 큼distributed representation과 neural LM의 출발점을 손으로 이해할 때
class 또는 hierarchical language model문맥 model은 설계에 따라 다름tree나 class로 output 감소 가능vocabulary output 비용이 먼저 막힐 때
RNN 또는 LSTMrecurrent state로 더 긴 과거 요약output softmax는 여전히 필요fixed window가 명백히 짧고 순차 계산을 감수할 때
Transformer decoderattention으로 context 위치를 선택output head와 softmax는 남음현재 대규모 next-token model을 구축하거나 활용할 때
word2vec CBOW 또는 skip-gram주변 단어로 representation 학습목적과 근사 방식이 다름빠른 word vector 자체가 목표일 때

word2vec를 NPLM의 축소판이라고 부르면 정확하지 않다. Mikolov 등은 CBOW와 skip-gram을 큰 data에서 continuous word representation을 빠르게 구하는 architecture로 제안했고, hierarchical softmax로 output 계산을 줄이는 방법도 설명한다.Efficient Estimation of Word Representations in Vector Space 원문 NPLM은 normalized next-word distribution과 corpus likelihood가 목표이고, word2vec 논문은 representation 품질과 효율적인 학습이 중심이다. 둘 다 vector를 쓰지만 objective와 평가가 다르다.

Transformer는 NPLM의 모든 것을 버린 것이 아니다. input token을 learned embedding으로 바꾸고 decoder output에서 softmax로 다음 token 확률을 만드는 큰 틀은 남는다. 달라진 결정적 부분은 fixed concatenation 대신 self-attention이 context 위치 사이의 의존성을 계산한다는 점이다. Vaswani 등은 Transformer가 recurrence와 convolution 없이 attention mechanism에 기반한다고 설명하고, 3.4절에서 input embedding과 decoder output의 next-token softmax를 명시한다.Attention Is All You Need 원문

학습 순서에서 이 논문을 어디에 둘까

이 글을 읽고 바로 NPLM을 production model로 만들 필요는 없다. top-down으로 Llama, RAG, agent, data pipeline을 먼저 보면서 아래 질문에 막힐 때 이 논문으로 내려오면 효율적이다.

왜 token embedding table이 필요한가
왜 language model은 다음 token 확률의 곱인가
왜 loss가 -log p인가
왜 vocabulary가 커지면 output head가 비싸지는가
왜 C의 특정 행만 sparse하게 update되는가

그 질문의 답을 이 글에서 직접 계산으로 잡은 뒤 RNN·attention·subword tokenization·LayerNorm·AdamW로 가면 된다. 반대로 LLM application을 만들고 싶다는 목적에 이 논문만 여러 주 정독하는 것은 비효율적이다. NPLM은 current production architecture 선택지가 아니라, 현재 LLM의 embedding·softmax·NLL·output bottleneck을 한 화면에 작게 펼친 최소 지도다.

흔한 오해와 최초 질문의 답

오해 하나 embedding을 쓰면 못 본 문장을 이해한다

아니다. 원 논문의 주장도 semantic neighbor로 일반화할 수 있다는 확률 모델의 가정과 당시 corpus PPL 결과다. C가 하나의 좌표만 주므로 polysemy(다의성)가 약하며, 논문 5.2절도 한 단어에 여러 의미가 있을 때 multiple point를 둘 수 있는 확장을 미래 과제로 적는다. 가까운 vector는 task·data·objective가 만든 관계이지 인간의 완전한 의미론이 아니다.

오해 둘 NPLM은 word2vec과 같은 모델이다

아니다. 둘은 distributed representation을 쓰지만, NPLM은 full next-word probability를 normalize하고 sequence likelihood를 학습한다. word2vec은 더 효율적인 representation learning 목적과 다른 context prediction setup을 가진다. vector file만 저장해 두면 NPLM의 H, W, U, b, vocabulary mapping, output probability가 자동으로 생기지 않는다.

오해 셋 n-gram보다 PPL이 낮았으니 neural model만 쓰면 된다

아니다. 원 논문의 강한 Brown와 AP News 비교 숫자에는 trigram mixture가 들어간 MLP10이 포함된다. PPL 비교에는 dataset split, rare-word rule, model size, training duration이 붙는다. 실제 서비스에서는 simple baseline이 data issue와 tokenization bug를 더 빨리 드러내기도 한다.

오해 넷 softmax는 마지막에 붙는 작은 함수다

아니다. 확률의 합을 1로 만드는 마지막 단계이면서 vocabulary 전체 score를 요구하는 큰 계산이다. 원 논문 AP example에서 output unit 계산이 약 99.7퍼센트라는 것은 이 작은 feed-forward LM도 output layer 설계와 hardware 분할을 피할 수 없었다는 직접 근거다.

처음 질문에 대한 답

LLM을 top-down으로 공부하면서 bottom-up을 놓치지 않으려면, 이 논문은 Transformer 전에 모든 딥러닝 역사를 외우기 위한 과목이 아니다. RAG·GraphRAG·agent를 만들다가 embedding, next-token probability, NLL, softmax, vocabulary output cost에 막혔을 때 한 번 내려와 직접 손으로 확인하는 최소 바닥이다. 다음 논문을 읽을 때 C의 lookup, x의 context, p-y gradient, full output bottleneck을 떠올릴 수 있으면 이 글의 목표는 달성한 것이다.

출처

1차 자료

  1. Yoshua Bengio, Réjean Ducharme, Pascal Vincent, Christian Jauvin. A Neural Probabilistic Language Model PDF. Journal of Machine Learning Research 3, 1137–1155, 2003. 이 글의 architecture, 식 (1), parameter count, parallel algorithm, corpus split, 표 1·2, extension의 1차 근거다.
  2. JMLR article record. 제목, 저자, 2003년 권호와 abstract 검증 경로다.
  3. Tomas Mikolov, Kai Chen, Greg Corrado, Jeffrey Dean. Efficient Estimation of Word Representations in Vector Space, arXiv 1301.3781v3, 2013. word2vec의 CBOW·skip-gram 목적과 hierarchical softmax 비교 경계에 썼다.
  4. Ashish Vaswani 외. Attention Is All You Need, NeurIPS 2017. Transformer가 attention에 기반하고 learned embedding과 decoder output softmax를 쓰는 연결 경계에 썼다.

직접 실행한 검증

  • 2026-07-24 macOS에서 Python 3.9.6과 표준 라이브러리만 사용했다.
  • vocabulary 6개, m=2, h=3, context i like, target cats로 원 논문 식 y=b+Wx+U tanh(d+Hx)를 실행했다.
  • C[like][0], W[cats][1], H[0][2]의 analytic gradient가 central finite difference와 각각 1.284e-12, 5.882e-13, 7.913e-12 차이로 일치했다.
  • learning rate 0.20의 한 gradient descent step에서 NLL이 1.693467850에서 1.476821074로 감소했다. 이 toy 검증은 논문 corpus benchmark 재현이 아니다.
대화

댓글

0
댓글을 불러오는 중입니다.