<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://yuseokkang.com/feed.xml" rel="self" type="application/atom+xml"/><link href="https://yuseokkang.com/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-09-11T01:01:06+00:00</updated><id>https://yuseokkang.com/feed.xml</id><title type="html">blank</title><subtitle>I play piano, percussion, and computers. One has keys, one you hit — somehow the third needs both. </subtitle><entry><title type="html">왜 하필 Cross Entropy인가: BCE에서 KL Divergence까지</title><link href="https://yuseokkang.com/blog/2026/cross-entropy-to-kl-divergence/" rel="alternate" type="text/html" title="왜 하필 Cross Entropy인가: BCE에서 KL Divergence까지"/><published>2026-08-30T00:00:00+00:00</published><updated>2026-08-30T00:00:00+00:00</updated><id>https://yuseokkang.com/blog/2026/cross-entropy-to-kl-divergence</id><content type="html" xml:base="https://yuseokkang.com/blog/2026/cross-entropy-to-kl-divergence/"><![CDATA[<p>분류 모델을 짤 때 손실 함수 자리에 <code class="language-plaintext highlighter-rouge">CrossEntropyLoss</code>를 넣는 건 거의 반사적이다. 그런데 왜 하필 그건지, 왜 회귀에서 잘 쓰던 MSE(Mean Squared Error)는 안 되는지, <code class="language-plaintext highlighter-rouge">BCELoss</code>와는 뭐가 다른지, 그리고 논문마다 나오는 KL Divergence와는 무슨 관계인지를 이어서 설명하려면 생각보다 막힌다.</p> <p>이 글은 그 사슬을 처음부터 끝까지 한 번에 잇는 것을 목표로 한다. 정보량에서 시작해서 엔트로피, Cross Entropy, BCE, 최대우도추정, KL Divergence 순서로 간다.</p> <h2 id="시작-질문-mse는-왜-안-되나">시작 질문: MSE는 왜 안 되나</h2> <p>이진 분류에서 모델이 $z$라는 logit을 뱉고 sigmoid를 통과시켜 $\hat y = \sigma(z)$를 만든다고 하자. 정답은 $y \in {0, 1}$이다. MSE를 쓰면 손실은 이렇다.</p> \[L_{\text{MSE}} = \frac{1}{2}(\hat y - y)^2\] <p>$z$에 대한 기울기를 구해보면 문제가 드러난다.</p> \[\frac{\partial L_{\text{MSE}}}{\partial z} = (\hat y - y)\,\sigma'(z) = (\hat y - y)\,\hat y (1 - \hat y)\] <p>$\sigma’(z) = \hat y(1-\hat y)$라는 항이 붙어 있다. 이 값은 $\hat y$가 0이나 1에 가까울 때 0으로 죽는다. 그런데 $\hat y$가 0에 가까운데 정답이 1인 상황은 <strong>모델이 가장 크게 틀린 상황</strong>이다. 즉 MSE + sigmoid 조합은 가장 크게 틀렸을 때 기울기가 가장 작아진다. 학습이 필요한 지점에서 학습이 멈춘다.</p> <p>숫자로 보면 심각성이 분명해진다. $y=1$인데 모델이 $z=-8$을 뱉은, 완전히 틀린 경우다.</p> <table> <thead> <tr> <th> </th> <th>$\hat y$</th> <th>$\partial L / \partial z$</th> </tr> </thead> <tbody> <tr> <td>MSE + sigmoid</td> <td>0.000335</td> <td>$-0.000335$</td> </tr> <tr> <td>BCE + sigmoid</td> <td>0.000335</td> <td>$-0.999665$</td> </tr> </tbody> </table> <p>같은 상황인데 기울기 크기가 약 3000배 차이난다. MSE는 사실상 아무것도 배우지 못한다.</p> <p>기울기 문제를 걷어내더라도 더 근본적인 이유가 남는다. 분류 모델의 출력은 숫자가 아니라 <strong>확률분포</strong>다. 두 확률분포가 얼마나 다른지를 재는 데 유클리드 거리를 쓸 이유가 없다. 확률분포에는 확률분포의 척도가 있고, 그 척도는 정보 이론에서 나온다.</p> <h2 id="정보량-놀람의-크기">정보량: 놀람의 크기</h2> <p>“내일 해가 뜬다”는 정보량이 거의 0이다. “내일 눈이 온다”는 8월이라면 정보량이 크다. 드문 사건일수록 정보량이 크다는 직관을 수식으로 옮기려면 어떤 함수여야 할까. 세 가지 조건을 걸어보자.</p> <ol> <li>확률이 낮을수록 정보량이 크다 (단조 감소)</li> <li>확률이 1인 사건의 정보량은 0이다</li> <li>독립인 두 사건을 함께 관측한 정보량은 각각의 합이다: $I(pq) = I(p) + I(q)$</li> </ol> <p>3번이 결정적이다. 곱을 합으로 바꾸는 연속함수는 로그뿐이다. 1번과 2번까지 맞추려면 부호를 뒤집어야 한다.</p> \[I(x) = -\log p(x)\] <p>로그의 밑이 2면 단위는 <strong>bit</strong>, 자연로그면 <strong>nat</strong>이다. 딥러닝 구현은 거의 항상 자연로그를 쓴다.</p> <h2 id="엔트로피-평균-놀람">엔트로피: 평균 놀람</h2> <p>정보량은 사건 하나에 대한 값이다. 분포 전체에 대해 평균을 내면 엔트로피다.</p> \[H(P) = \mathbb{E}_{x \sim P}\left[-\log P(x)\right] = -\sum_x P(x) \log P(x)\] <p>Shannon의 source coding theorem에 따르면 이 값은 <strong>$P$를 따르는 데이터를 인코딩하는 데 필요한 최소 평균 비트 수</strong>다. 공정한 동전은 $H = 1$ bit, 항상 앞면만 나오는 동전은 $H = 0$ bit다. 후자는 결과를 전송할 필요조차 없으니 당연하다.</p> <p>엔트로피는 분포 하나의 고유한 성질이다. 여기까지는 모델이 등장하지 않는다.</p> <h2 id="cross-entropy-틀린-코드북을-쓸-때의-비용">Cross Entropy: 틀린 코드북을 쓸 때의 비용</h2> <p>이제 두 분포를 등장시킨다. 실제 데이터는 $P$를 따르는데, 우리는 그걸 모르고 $Q$가 맞다고 믿고 코드북을 만들었다. 이때 실제로 드는 평균 비트 수가 Cross Entropy다.</p> \[H(P, Q) = \mathbb{E}_{x \sim P}\left[-\log Q(x)\right] = -\sum_x P(x) \log Q(x)\] <p>기댓값은 <strong>실제 분포 $P$</strong>로 잡고, 로그 안에는 <strong>내 예측 $Q$</strong>가 들어간다. 이 비대칭이 핵심이다.</p> <p>$Q = P$일 때 이 값은 최소가 되고, 그 최솟값이 정확히 $H(P)$다. 따라서 언제나</p> \[H(P, Q) \ge H(P)\] <p>내 믿음이 실제와 어긋난 만큼 추가 비용을 지불한다. 그 초과분이 뒤에 나올 KL Divergence다.</p> <h2 id="딥러닝에서의-cross-entropy">딥러닝에서의 Cross Entropy</h2> <p>$P$를 정답 라벨 분포, $Q$를 모델 예측 분포로 두면 손실 함수가 된다. 라벨이 one-hot이면 $P$는 정답 클래스 $c$에서만 1이고 나머지는 0이므로 합이 한 항만 남는다.</p> \[L = -\sum_k P(k) \log Q(k) = -\log Q(c)\] <p><strong>정답 클래스에 부여한 확률의 음의 로그.</strong> 이게 실무에서 마주치는 Cross Entropy의 전부다. 정답에 1을 주면 손실 0, 정답 확률이 0으로 가면 손실이 무한대로 발산한다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>

<span class="n">logits</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([[</span><span class="mf">2.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">]])</span>
<span class="n">target</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([</span><span class="mi">0</span><span class="p">])</span>

<span class="c1"># softmax([2.0, 1.0, 0.1]) = [0.6590, 0.2424, 0.0986]
# CE = -log(0.6590) = 0.4170
</span><span class="n">F</span><span class="p">.</span><span class="nf">cross_entropy</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>
</code></pre></div></div> <h2 id="bce-클래스가-둘일-때">BCE: 클래스가 둘일 때</h2> <p>Binary Cross Entropy는 별개의 개념이 아니라 클래스가 2개인 특수 케이스다. $P = (y,\ 1-y)$, $Q = (\hat y,\ 1-\hat y)$를 위 정의에 그대로 대입하면 나온다.</p> \[L_{\text{BCE}} = -\left[\,y \log \hat y + (1-y)\log(1-\hat y)\,\right]\] <p>그럼 왜 이름을 따로 붙였을까. 두 가지 이유가 있다.</p> <p>첫째는 표현의 경제성이다. 클래스가 2개면 $\hat y$ 하나만 알면 나머지는 $1 - \hat y$로 결정된다. 출력 노드 2개 + softmax는 파라미터가 중복된다. 노드 1개 + sigmoid로 충분하다.</p> <p>둘째가 실질적으로 더 중요하다. <strong>multi-label 문제를 다룰 수 있다.</strong> softmax는 출력의 합을 1로 강제하므로 클래스들이 상호배타적이라고 가정한다. 반면 sigmoid + BCE는 각 클래스를 독립된 이진 문제로 취급해서 합에 제약이 없다. 사진 한 장에 “고양이”와 “실내”가 동시에 참일 수 있는 문제라면 BCE를 써야 한다.</p> <table> <thead> <tr> <th> </th> <th>CE + softmax</th> <th>BCE + sigmoid</th> </tr> </thead> <tbody> <tr> <td>출력 노드</td> <td>클래스 수 $K$개</td> <td>클래스당 1개</td> </tr> <tr> <td>출력 합</td> <td>항상 1</td> <td>제약 없음</td> </tr> <tr> <td>클래스 가정</td> <td>상호배타 (multi-class)</td> <td>독립 (multi-label)</td> </tr> <tr> <td>대표 문제</td> <td>“이 사진은 개인가 고양이인가”</td> <td>“이 사진에 뭐뭐가 있는가”</td> </tr> </tbody> </table> <h2 id="왜-이게-옳은-손실인가-최대우도추정">왜 이게 “옳은” 손실인가 (최대우도추정)</h2> <p>여기까지는 정보 이론 쪽 해석이었다. 통계 쪽에서 접근하면 같은 식이 전혀 다른 경로로 도출된다.</p> <p>파라미터 $\theta$인 모델이 데이터 ${(x_i, y_i)}$를 만들어냈을 가능성(우도)을 최대화하고 싶다. 샘플이 독립이라면 우도는 곱이고, 로그를 씌우면 합이 된다.</p> \[\hat\theta = \arg\max_\theta \sum_i \log Q_\theta(y_i \mid x_i) = \arg\min_\theta \sum_i -\log Q_\theta(y_i \mid x_i)\] <p>우변이 정확히 Cross Entropy의 합이다.</p> <blockquote> <p><strong>Cross Entropy 최소화 = 최대우도추정</strong></p> </blockquote> <p>여러 후보 중 그럴듯해 보여서 고른 손실이 아니다. “데이터를 가장 잘 설명하는 파라미터를 찾는다”는 통계학의 표준 원리를 분류 문제에 적용하면 필연적으로 나오는 식이다.</p> <h2 id="kl-divergence-그래서-실제로-줄이고-있는-것">KL Divergence: 그래서 실제로 줄이고 있는 것</h2> <p>두 분포의 차이 자체를 재고 싶다면 Cross Entropy는 조금 불편하다. $Q = P$인 완벽한 경우에도 값이 0이 아니라 $H(P)$이기 때문이다. 그 바닥값을 빼주자.</p> \[D_{KL}(P \parallel Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}\] <p>로그 안의 분수를 쪼개면 정체가 드러난다.</p> \[D_{KL}(P \parallel Q) = \sum_x P(x)\log P(x) - \sum_x P(x) \log Q(x) = -H(P) + H(P, Q)\] <p>정리하면 이 글 전체를 관통하는 한 줄이 나온다.</p> \[\boxed{\,H(P, Q) = H(P) + D_{KL}(P \parallel Q)\,}\] <p><strong>Cross Entropy = 데이터 자체의 엔트로피 + 내 예측이 틀린 만큼의 초과 비용.</strong></p> <p>그리고 여기서 실무적으로 중요한 결론이 따라 나온다. 학습 중에 정답 분포 $P$는 고정되어 있으므로 $H(P)$는 $\theta$에 대한 <strong>상수</strong>다. 상수를 빼도 argmin은 변하지 않는다.</p> \[\arg\min_\theta H(P, Q_\theta) = \arg\min_\theta D_{KL}(P \parallel Q_\theta)\] <p>Cross Entropy를 최소화하는 것과 KL Divergence를 최소화하는 것은 <strong>완전히 동일한 최적화 문제</strong>다. 그래서 굳이 $H(P)$를 계산하지 않고 더 싼 Cross Entropy를 쓴다.</p> <p>라벨이 one-hot이면 한 걸음 더 나간다. 확률 1인 사건 하나뿐이라 $H(P) = 0$이고, 따라서</p> \[H(P, Q) = D_{KL}(P \parallel Q)\] <p>두 값이 같아진다. 앞의 예제에서 CE가 0.4170이었는데, $D_{KL}$을 직접 계산해도 $1 \cdot \log(1/0.6590) = 0.4170$으로 같은 값이다.</p> <h3 id="kl의-성질-두-가지">KL의 성질 두 가지</h3> <p><strong>항상 0 이상이다.</strong> $D_{KL}(P\parallel Q) \ge 0$이고, 등호는 $P = Q$일 때만 성립한다 (Gibbs’ inequality). 앞서 $H(P,Q) \ge H(P)$라고 했던 것의 다른 표현이다.</p> <p><strong>대칭이 아니다.</strong> $D_{KL}(P \parallel Q) \ne D_{KL}(Q \parallel P)$이고 삼각부등식도 성립하지 않는다. 그래서 “KL 거리”라는 말은 엄밀히는 틀렸다. divergence라고 부르는 이유다.</p> <p>이 비대칭은 성가신 결점이 아니라 실제로 쓰이는 성질이다.</p> <ul> <li><strong>Forward KL</strong> $D_{KL}(P \parallel Q)$: $P$가 큰 곳에서 $Q$가 작으면 $\log(P/Q)$가 폭발한다. $Q$는 $P$가 존재하는 영역을 전부 덮으려 한다 (mass-covering). 지도학습이 여기 해당한다.</li> <li><strong>Reverse KL</strong> $D_{KL}(Q \parallel P)$: 기댓값을 $Q$로 잡으므로 $Q$가 0인 영역은 아예 벌점을 받지 않는다. $Q$는 $P$의 봉우리 하나에 안전하게 몰린다 (mode-seeking). 변분추론이나 RLHF의 KL penalty가 여기 해당한다.</li> </ul> <h3 id="kl을-직접-쓰는-경우">KL을 직접 쓰는 경우</h3> <p>Cross Entropy로 대체 가능한데도 KL을 명시적으로 계산하는 상황이 있다. $P$가 one-hot이 아닐 때다.</p> <ul> <li><strong>Knowledge distillation</strong>: teacher의 softmax 분포를 $P$로 쓴다. $H(P) \ne 0$이지만 student 파라미터에 대해서는 여전히 상수라, 사실 CE를 써도 최적해는 같다. 그래도 “두 분포를 맞춘다”는 의도를 드러내려고 관례적으로 KL을 쓴다.</li> <li><strong>VAE</strong>: latent 분포를 prior에 붙들어두는 정규화 항이 KL이다. 이건 손실의 일부라 상수항이 아니고, 실제로 값 자체가 필요하다.</li> <li><strong>PPO 등 RL</strong>: 새 정책이 기존 정책에서 너무 멀어지지 않게 KL로 제약한다.</li> </ul> <h2 id="다시-기울기-cross-entropy가-학습을-살리는-이유">다시 기울기: Cross Entropy가 학습을 살리는 이유</h2> <p>첫 절의 문제로 돌아가자. BCE의 $z$에 대한 기울기를 구해보면 이렇게 된다.</p> \[\frac{\partial L}{\partial \hat y} = \frac{\hat y - y}{\hat y(1-\hat y)}, \qquad \frac{\partial \hat y}{\partial z} = \hat y(1 - \hat y)\] \[\frac{\partial L}{\partial z} = \hat y - y\] <p>분모의 $\hat y(1-\hat y)$가 sigmoid의 미분과 정확히 상쇄된다. MSE를 죽이던 포화 항이 사라지고, 기울기가 <strong>오차 그 자체</strong>가 된다. 크게 틀릴수록 크게 움직인다.</p> <p>softmax + CE에서도 똑같은 일이 벌어진다.</p> \[\frac{\partial L}{\partial z_i} = \hat y_i - y_i\] <p>이건 우연이 아니다. sigmoid와 softmax는 exp로 만들어진 함수이고, Cross Entropy는 log를 취한다. log와 exp가 서로를 지운다. 정보 이론에서 유도된 손실과 확률로 정규화하는 활성 함수가 짝이 맞도록 설계되어 있는 것이다.</p> <h2 id="구현할-때-자주-밟는-지뢰">구현할 때 자주 밟는 지뢰</h2> <p><strong>logit을 넘겨라, 확률을 넘기지 말고.</strong> <code class="language-plaintext highlighter-rouge">F.cross_entropy</code>와 <code class="language-plaintext highlighter-rouge">nn.BCEWithLogitsLoss</code>는 내부에서 log-sum-exp trick으로 수치 안정성을 확보한다. 직접 softmax를 거친 뒤 log를 취하면 확률이 0으로 언더플로했을 때 <code class="language-plaintext highlighter-rouge">-inf</code>가 나오고 그대로 NaN이 된다.</p> <p><strong><code class="language-plaintext highlighter-rouge">nn.CrossEntropyLoss</code>는 softmax를 이미 포함한다.</strong> 모델 마지막에 softmax를 넣고 이 손실을 쓰면 softmax가 두 번 걸린다. 에러 없이 조용히 학습만 안 되는, 가장 찾기 짜증나는 종류의 버그다. <code class="language-plaintext highlighter-rouge">nn.BCEWithLogitsLoss</code>와 <code class="language-plaintext highlighter-rouge">nn.BCELoss</code>의 관계도 동일하다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 나쁨: softmax 두 번
</span><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">CrossEntropyLoss</span><span class="p">()(</span><span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="n">target</span><span class="p">)</span>

<span class="c1"># 좋음: raw logit 그대로
</span><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">CrossEntropyLoss</span><span class="p">()(</span><span class="n">logits</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>
</code></pre></div></div> <p><strong><code class="language-plaintext highlighter-rouge">F.kl_div</code>의 인자 규약은 직관과 다르다.</strong> 첫 인자는 확률이 아니라 <strong>log 확률</strong>이고, 두 번째 인자가 확률이다 (<code class="language-plaintext highlighter-rouge">log_target=False</code> 기준). 게다가 기본 <code class="language-plaintext highlighter-rouge">reduction='mean'</code>은 원소 단위 평균이라 수식의 정의와 맞지 않는다. 분포 단위로 맞추려면 <code class="language-plaintext highlighter-rouge">reduction='batchmean'</code>을 써야 한다.</p> <p><strong>Label smoothing은 $P$를 건드리는 기법이다.</strong> one-hot을 살짝 뭉개서 $P = (1-\epsilon, \epsilon/(K-1), \ldots)$로 만든다. 이러면 $H(P) \ne 0$이 되어 CE와 KL의 값이 더 이상 같지 않다. 다만 여전히 상수 차이라 최적화 대상으로서는 동일하다. 손실 값의 절대 크기를 다른 실험과 비교할 때만 주의하면 된다.</p> <h2 id="정리">정리</h2> <ul> <li>정보량 $-\log p$는 “곱을 합으로” 조건에서 필연적으로 나오는 형태다.</li> <li>엔트로피 $H(P)$는 분포 하나의 고유한 최소 인코딩 비용이다.</li> <li>Cross Entropy $H(P,Q)$는 실제 $P$인데 $Q$를 믿고 인코딩할 때의 비용이고, 항상 $H(P)$ 이상이다.</li> <li>BCE는 클래스가 2개인 CE다. 다만 sigmoid와 짝지어 multi-label을 다룰 수 있다는 실질적 차이가 있다.</li> <li>CE 최소화는 최대우도추정과 같다. 임의로 고른 손실이 아니다.</li> <li>$H(P,Q) = H(P) + D_{KL}(P\parallel Q)$이고, 학습 중 $H(P)$는 상수다. 따라서 CE 최소화와 KL 최소화는 같은 문제이며, one-hot 라벨에서는 값까지 같다.</li> <li>KL은 비대칭이라 거리가 아니다. 그 비대칭이 mass-covering과 mode-seeking을 가른다.</li> <li>log와 exp가 상쇄되어 기울기가 $\hat y - y$로 떨어지는 것이, MSE 대신 CE를 쓰는 실전적 이유다.</li> </ul>]]></content><author><name></name></author><category term="deep-learning"/><category term="cross-entropy"/><category term="kl-divergence"/><category term="information-theory"/><category term="loss-function"/><category term="deep-learning"/><summary type="html"><![CDATA[분류 문제에 MSE 대신 Cross Entropy를 쓰는 이유를 정보량과 엔트로피에서 출발해 BCE, MLE, KL Divergence까지 하나의 흐름으로 정리.]]></summary></entry><entry><title type="html">Why cross entropy, of all things: from BCE to KL divergence</title><link href="https://yuseokkang.com/en/blog/2026/cross-entropy-to-kl-divergence/" rel="alternate" type="text/html" title="Why cross entropy, of all things: from BCE to KL divergence"/><published>2026-08-30T00:00:00+00:00</published><updated>2026-08-30T00:00:00+00:00</updated><id>https://yuseokkang.com/en/blog/2026/cross-entropy-to-kl-divergence</id><content type="html" xml:base="https://yuseokkang.com/en/blog/2026/cross-entropy-to-kl-divergence/"><![CDATA[<p>Reaching for <code class="language-plaintext highlighter-rouge">CrossEntropyLoss</code> when you write a classifier is close to a reflex. But explaining why it is that particular loss, why MSE (mean squared error) — which served us well in regression — will not do, what makes it different from <code class="language-plaintext highlighter-rouge">BCELoss</code>, and how it relates to the KL divergence that shows up in every paper turns out to be surprisingly hard to do in one breath.</p> <p>The goal of this post is to connect that chain from end to end, in one pass. Starting from information content, then entropy, cross entropy, BCE, maximum likelihood estimation, and KL divergence.</p> <h2 id="the-opening-question-why-not-mse">The opening question: why not MSE?</h2> <p>Suppose in binary classification the model emits a logit $z$, passes it through a sigmoid to produce $\hat y = \sigma(z)$, and the label is $y \in {0, 1}$. With MSE the loss is</p> \[L_{\text{MSE}} = \frac{1}{2}(\hat y - y)^2\] <p>Taking the gradient with respect to $z$ exposes the problem.</p> \[\frac{\partial L_{\text{MSE}}}{\partial z} = (\hat y - y)\,\sigma'(z) = (\hat y - y)\,\hat y (1 - \hat y)\] <p>There is a factor $\sigma’(z) = \hat y(1-\hat y)$ attached. It dies to zero when $\hat y$ approaches 0 or 1. But “$\hat y$ near 0 while the label is 1” is <strong>exactly the situation where the model is most badly wrong</strong>. So the MSE + sigmoid combination has its smallest gradient precisely when it is most wrong. Learning stops at the very point where learning is needed.</p> <p>The numbers make the severity obvious. Take $y=1$ while the model emits $z=-8$ — completely wrong.</p> <table> <thead> <tr> <th> </th> <th>$\hat y$</th> <th>$\partial L / \partial z$</th> </tr> </thead> <tbody> <tr> <td>MSE + sigmoid</td> <td>0.000335</td> <td>$-0.000335$</td> </tr> <tr> <td>BCE + sigmoid</td> <td>0.000335</td> <td>$-0.999665$</td> </tr> </tbody> </table> <p>Same situation, gradient magnitudes about 3000x apart. MSE learns essentially nothing.</p> <p>Even setting the gradient issue aside, a deeper reason remains. A classifier’s output is not a number but a <strong>probability distribution</strong>. There is no reason to measure how different two probability distributions are with Euclidean distance. Probability distributions have their own yardstick, and that yardstick comes from information theory.</p> <h2 id="information-content-the-size-of-the-surprise">Information content: the size of the surprise</h2> <p>“The sun rises tomorrow” carries almost zero information. “It snows tomorrow” carries a lot, if it is August. To turn the intuition that rarer events carry more information into a formula, what shape must the function have? Impose three conditions.</p> <ol> <li>Lower probability means more information (monotonically decreasing)</li> <li>An event with probability 1 carries zero information</li> <li>Observing two independent events together carries the sum of their information: $I(pq) = I(p) + I(q)$</li> </ol> <p>Condition 3 is the decisive one. The only continuous function that turns products into sums is the logarithm. Satisfying 1 and 2 as well requires flipping the sign.</p> \[I(x) = -\log p(x)\] <p>With base 2 the unit is a <strong>bit</strong>; with the natural logarithm it is a <strong>nat</strong>. Deep learning implementations almost always use the natural logarithm.</p> <h2 id="entropy-average-surprise">Entropy: average surprise</h2> <p>Information content is a value for a single event. Averaging it over the whole distribution gives entropy.</p> \[H(P) = \mathbb{E}_{x \sim P}\left[-\log P(x)\right] = -\sum_x P(x) \log P(x)\] <p>By Shannon’s source coding theorem, this value is the <strong>minimum average number of bits needed to encode data drawn from $P$</strong>. A fair coin has $H = 1$ bit; a coin that always lands heads has $H = 0$ bits. The latter is obvious — there is no need to transmit the outcome at all.</p> <p>Entropy is an intrinsic property of a single distribution. No model has entered the picture yet.</p> <h2 id="cross-entropy-the-cost-of-using-the-wrong-codebook">Cross entropy: the cost of using the wrong codebook</h2> <p>Now bring in two distributions. The real data follows $P$, but not knowing that, we believed $Q$ was correct and built our codebook from it. The average number of bits this actually costs is the cross entropy.</p> \[H(P, Q) = \mathbb{E}_{x \sim P}\left[-\log Q(x)\right] = -\sum_x P(x) \log Q(x)\] <p>The expectation is taken over the <strong>true distribution $P$</strong>, while <strong>our prediction $Q$</strong> sits inside the logarithm. That asymmetry is the whole point.</p> <p>The value is minimised when $Q = P$, and that minimum is exactly $H(P)$. So always</p> \[H(P, Q) \ge H(P)\] <p>We pay an extra cost in proportion to how far our belief diverges from reality. That excess is the KL divergence, coming up later.</p> <h2 id="cross-entropy-in-deep-learning">Cross entropy in deep learning</h2> <p>Let $P$ be the label distribution and $Q$ the model’s predicted distribution, and it becomes a loss function. With a one-hot label, $P$ is 1 only at the true class $c$ and 0 elsewhere, so only one term of the sum survives.</p> \[L = -\sum_k P(k) \log Q(k) = -\log Q(c)\] <p><strong>The negative log of the probability assigned to the correct class.</strong> That is all of the cross entropy you meet in practice. Assign 1 to the correct class and the loss is 0; drive the correct-class probability toward 0 and the loss diverges to infinity.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>

<span class="n">logits</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([[</span><span class="mf">2.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">]])</span>
<span class="n">target</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([</span><span class="mi">0</span><span class="p">])</span>

<span class="c1"># softmax([2.0, 1.0, 0.1]) = [0.6590, 0.2424, 0.0986]
# CE = -log(0.6590) = 0.4170
</span><span class="n">F</span><span class="p">.</span><span class="nf">cross_entropy</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>
</code></pre></div></div> <h2 id="bce-when-there-are-two-classes">BCE: when there are two classes</h2> <p>Binary cross entropy is not a separate concept but the special case of two classes. Substituting $P = (y,\ 1-y)$ and $Q = (\hat y,\ 1-\hat y)$ straight into the definition above gives it.</p> \[L_{\text{BCE}} = -\left[\,y \log \hat y + (1-y)\log(1-\hat y)\,\right]\] <p>So why give it its own name? Two reasons.</p> <p>The first is economy of representation. With two classes, knowing $\hat y$ alone determines the rest as $1 - \hat y$. Two output nodes plus softmax means duplicated parameters. One node plus a sigmoid is enough.</p> <p>The second matters more in practice: <strong>it can handle multi-label problems.</strong> Softmax forces the outputs to sum to 1, which assumes the classes are mutually exclusive. Sigmoid + BCE, by contrast, treats each class as an independent binary problem with no constraint on the sum. If “cat” and “indoors” can both be true of one photo, you need BCE.</p> <table> <thead> <tr> <th> </th> <th>CE + softmax</th> <th>BCE + sigmoid</th> </tr> </thead> <tbody> <tr> <td>Output nodes</td> <td>$K$, one per class</td> <td>one per class</td> </tr> <tr> <td>Sum of outputs</td> <td>always 1</td> <td>unconstrained</td> </tr> <tr> <td>Class assumption</td> <td>mutually exclusive (multi-class)</td> <td>independent (multi-label)</td> </tr> <tr> <td>Typical problem</td> <td>“is this photo a dog or a cat”</td> <td>“what things are in this photo”</td> </tr> </tbody> </table> <h2 id="why-this-is-the-right-loss-maximum-likelihood-estimation">Why this is the “right” loss (maximum likelihood estimation)</h2> <p>Everything so far was the information-theoretic reading. Approach it from statistics and the same formula falls out along an entirely different route.</p> <p>We want to maximise the likelihood that a model with parameters $\theta$ produced the data ${(x_i, y_i)}$. If the samples are independent the likelihood is a product, and taking the logarithm turns it into a sum.</p> \[\hat\theta = \arg\max_\theta \sum_i \log Q_\theta(y_i \mid x_i) = \arg\min_\theta \sum_i -\log Q_\theta(y_i \mid x_i)\] <p>The right-hand side is exactly a sum of cross entropies.</p> <blockquote> <p><strong>Minimising cross entropy = maximum likelihood estimation</strong></p> </blockquote> <p>This is not a loss picked because it looked plausible among candidates. It is the formula that necessarily appears when you apply the standard statistical principle of “find the parameters that best explain the data” to classification.</p> <h2 id="kl-divergence-what-we-are-actually-reducing">KL divergence: what we are actually reducing</h2> <p>If you want to measure the difference between two distributions in its own right, cross entropy is slightly awkward — even in the perfect case $Q = P$ its value is not 0 but $H(P)$. Let us subtract that floor.</p> \[D_{KL}(P \parallel Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}\] <p>Splitting the fraction inside the logarithm reveals what it is.</p> \[D_{KL}(P \parallel Q) = \sum_x P(x)\log P(x) - \sum_x P(x) \log Q(x) = -H(P) + H(P, Q)\] <p>Rearranged, we get the one line that runs through this whole post.</p> \[\boxed{\,H(P, Q) = H(P) + D_{KL}(P \parallel Q)\,}\] <p><strong>Cross entropy = the entropy inherent in the data + the excess cost of my prediction being wrong.</strong></p> <p>And a practically important conclusion follows. During training the label distribution $P$ is fixed, so $H(P)$ is a <strong>constant</strong> with respect to $\theta$. Subtracting a constant does not change the argmin.</p> \[\arg\min_\theta H(P, Q_\theta) = \arg\min_\theta D_{KL}(P \parallel Q_\theta)\] <p>Minimising cross entropy and minimising KL divergence are <strong>exactly the same optimisation problem</strong>. That is why we use the cheaper cross entropy rather than bothering to compute $H(P)$.</p> <p>With one-hot labels we can go one step further. There is only one event with probability 1, so $H(P) = 0$, and therefore</p> \[H(P, Q) = D_{KL}(P \parallel Q)\] <p>the two values coincide. In the earlier example CE was 0.4170, and computing $D_{KL}$ directly gives $1 \cdot \log(1/0.6590) = 0.4170$ — the same value.</p> <h3 id="two-properties-of-kl">Two properties of KL</h3> <p><strong>It is always non-negative.</strong> $D_{KL}(P\parallel Q) \ge 0$, with equality only when $P = Q$ (Gibbs’ inequality). This is another way of stating what we said earlier, $H(P,Q) \ge H(P)$.</p> <p><strong>It is not symmetric.</strong> $D_{KL}(P \parallel Q) \ne D_{KL}(Q \parallel P)$, and the triangle inequality does not hold either. Strictly speaking, then, “KL distance” is wrong. Hence divergence.</p> <p>That asymmetry is not an annoying flaw but a property that is actually used.</p> <ul> <li><strong>Forward KL</strong> $D_{KL}(P \parallel Q)$: where $P$ is large and $Q$ is small, $\log(P/Q)$ explodes. $Q$ tries to cover the whole region where $P$ has mass (mass-covering). Supervised learning falls here.</li> <li><strong>Reverse KL</strong> $D_{KL}(Q \parallel P)$: the expectation is taken over $Q$, so regions where $Q$ is 0 are not penalised at all. $Q$ safely collapses onto one mode of $P$ (mode-seeking). Variational inference and the KL penalty in RLHF fall here.</li> </ul> <h3 id="when-kl-is-used-directly">When KL is used directly</h3> <p>There are situations where KL is computed explicitly even though cross entropy would do — namely when $P$ is not one-hot.</p> <ul> <li><strong>Knowledge distillation</strong>: the teacher’s softmax distribution is used as $P$. $H(P) \ne 0$, but it is still constant with respect to the student’s parameters, so CE would in fact give the same optimum. KL is used by convention to make the intent — “match the two distributions” — explicit.</li> <li><strong>VAE</strong>: the regularisation term that pins the latent distribution to the prior is a KL. Here it is part of the loss rather than a constant, and the value itself is genuinely needed.</li> <li><strong>PPO and other RL</strong>: KL constrains a new policy from drifting too far from the old one.</li> </ul> <h2 id="back-to-gradients-why-cross-entropy-keeps-learning-alive">Back to gradients: why cross entropy keeps learning alive</h2> <p>Return to the problem from the first section. Taking BCE’s gradient with respect to $z$:</p> \[\frac{\partial L}{\partial \hat y} = \frac{\hat y - y}{\hat y(1-\hat y)}, \qquad \frac{\partial \hat y}{\partial z} = \hat y(1 - \hat y)\] \[\frac{\partial L}{\partial z} = \hat y - y\] <p>The $\hat y(1-\hat y)$ in the denominator cancels the sigmoid’s derivative exactly. The saturating factor that killed MSE disappears, and the gradient becomes <strong>the error itself</strong>. The more wrong you are, the more you move.</p> <p>The same thing happens with softmax + CE.</p> \[\frac{\partial L}{\partial z_i} = \hat y_i - y_i\] <p>This is no accident. Sigmoid and softmax are built from exp, and cross entropy takes a log. The log and the exp erase each other. A loss derived from information theory and an activation that normalises into probabilities were designed to be a matched pair.</p> <h2 id="landmines-people-step-on-when-implementing-this">Landmines people step on when implementing this</h2> <p><strong>Pass logits, not probabilities.</strong> <code class="language-plaintext highlighter-rouge">F.cross_entropy</code> and <code class="language-plaintext highlighter-rouge">nn.BCEWithLogitsLoss</code> secure numerical stability internally with the log-sum-exp trick. Applying softmax yourself and then taking a log gives <code class="language-plaintext highlighter-rouge">-inf</code> the moment a probability underflows to 0, and NaN follows.</p> <p><strong><code class="language-plaintext highlighter-rouge">nn.CrossEntropyLoss</code> already includes the softmax.</strong> Putting a softmax at the end of the model and then using this loss applies softmax twice. No error, it just quietly fails to learn — the most irritating kind of bug to find. The relationship between <code class="language-plaintext highlighter-rouge">nn.BCEWithLogitsLoss</code> and <code class="language-plaintext highlighter-rouge">nn.BCELoss</code> is the same.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bad: softmax twice
</span><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">CrossEntropyLoss</span><span class="p">()(</span><span class="n">F</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="n">target</span><span class="p">)</span>

<span class="c1"># good: raw logits
</span><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">CrossEntropyLoss</span><span class="p">()(</span><span class="n">logits</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>
</code></pre></div></div> <p><strong><code class="language-plaintext highlighter-rouge">F.kl_div</code>’s argument convention is not what you would guess.</strong> The first argument is not probabilities but <strong>log probabilities</strong>, and the second is probabilities (with <code class="language-plaintext highlighter-rouge">log_target=False</code>). Worse, the default <code class="language-plaintext highlighter-rouge">reduction='mean'</code> averages element-wise, which does not match the definition in the formula. To match it at the distribution level you need <code class="language-plaintext highlighter-rouge">reduction='batchmean'</code>.</p> <p><strong>Label smoothing is a technique that modifies $P$.</strong> It slightly blurs the one-hot into $P = (1-\epsilon, \epsilon/(K-1), \ldots)$. Now $H(P) \ne 0$, so CE and KL no longer have the same value. They still differ by a constant, though, so as optimisation targets they are identical. You only need to be careful when comparing absolute loss values against other experiments.</p> <h2 id="summary">Summary</h2> <ul> <li>Information content $-\log p$ is the form that necessarily emerges from the “products into sums” condition.</li> <li>Entropy $H(P)$ is the intrinsic minimum encoding cost of a single distribution.</li> <li>Cross entropy $H(P,Q)$ is the cost of encoding with $Q$ when reality is $P$, and it is always at least $H(P)$.</li> <li>BCE is CE with two classes. The substantive difference is that, paired with a sigmoid, it can handle multi-label problems.</li> <li>Minimising CE is maximum likelihood estimation. It is not an arbitrarily chosen loss.</li> <li>$H(P,Q) = H(P) + D_{KL}(P\parallel Q)$, and during training $H(P)$ is constant. So minimising CE and minimising KL are the same problem, and with one-hot labels even the values coincide.</li> <li>KL is asymmetric and therefore not a distance. That asymmetry is what separates mass-covering from mode-seeking.</li> <li>The log and exp cancelling so the gradient collapses to $\hat y - y$ is the practical reason to use CE instead of MSE.</li> </ul>]]></content><author><name></name></author><category term="deep-learning"/><category term="cross-entropy"/><category term="kl-divergence"/><category term="information-theory"/><category term="loss-function"/><category term="deep-learning"/><summary type="html"><![CDATA[Why classification uses cross entropy instead of MSE, traced as a single line from information content and entropy through BCE and MLE to KL divergence.]]></summary></entry><entry><title type="html">ROUGE</title><link href="https://yuseokkang.com/blog/2026/rogue/" rel="alternate" type="text/html" title="ROUGE"/><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://yuseokkang.com/blog/2026/rogue</id><content type="html" xml:base="https://yuseokkang.com/blog/2026/rogue/"><![CDATA[<p>ROUGE는 <strong>R</strong>ecall-<strong>O</strong>riented <strong>U</strong>nderstudy for <strong>G</strong>isting <strong>E</strong>valuation 의 약자로, 모델이 생성한 텍스트가 정답과 얼마나 겹치는지 측정하는 지표다. 원래 요약(summarization) 평가용으로 만들어졌지만, QA나 번역 등 텍스트 생성 전반에서 쓰인다.</p> <p>핵심 아이디어는 단순하다: <strong>정답에 있는 단어가 예측에 얼마나 등장하는가?</strong></p> <h2 id="공통-수식-precision-recall-f1">공통 수식: Precision, Recall, F1</h2> <p>모든 ROUGE 변형은 동일한 프레임워크를 따른다. 겹침(overlap)을 세는 단위만 달라질 뿐이다.</p> \[\text{Recall} = \frac{|\text{overlap}|}{|\text{Reference}|}\] \[\text{Precision} = \frac{|\text{overlap}|}{|\text{Prediction}|}\] \[F_1 = \frac{2 \times P \times R}{P + R}\] <ul> <li><strong>Recall</strong>: 정답 중 얼마나 잡아냈는가 (빠뜨린 게 없나)</li> <li><strong>Precision</strong>: 예측 중 정답에 해당하는 비율 (헛소리가 없나)</li> <li><strong>F1</strong>: 둘의 조화 평균. HuggingFace <code class="language-plaintext highlighter-rouge">evaluate</code>가 기본 리포트하는 값이 이것이다.</li> </ul> <h2 id="예시-설정">예시 설정</h2> <p>아래 예시로 모든 ROUGE를 계산해본다.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답(Reference):    "서울의 인구는 약 천만 명이고 대한민국의 수도이다"
예측(Prediction):   "서울은 대한민국의 수도이며 인구는 약 구백만 명이다"
</code></pre></div></div> <p>토큰 분리 (공백 기준):</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [서울의, 인구는, 약, 천만, 명이고, 대한민국의, 수도이다]     → m = 7
P = [서울은, 대한민국의, 수도이며, 인구는, 약, 구백만, 명이다]   → n = 7
</code></pre></div></div> <h2 id="rouge-1-unigram">ROUGE-1 (Unigram)</h2> <h3 id="수식">수식</h3> \[\text{ROUGE-1}_{recall} = \frac{\sum_{w \in R} \min(\text{count}_R(w),\ \text{count}_P(w))}{\sum_{w \in R} \text{count}_R(w)}\] <p>각 unigram $w$에 대해, 정답에서의 출현 횟수와 예측에서의 출현 횟수 중 작은 값을 취해 합산한다.</p> <h3 id="카운팅">카운팅</h3> <table> <thead> <tr> <th>정답 토큰</th> <th>예측에 있나?</th> </tr> </thead> <tbody> <tr> <td>서울의</td> <td>❌ (“서울은”과 다른 토큰)</td> </tr> <tr> <td>인구는</td> <td>✅</td> </tr> <tr> <td>약</td> <td>✅</td> </tr> <tr> <td>천만</td> <td>❌ (“구백만”과 다른 토큰)</td> </tr> <tr> <td>명이고</td> <td>❌ (“명이다”와 다른 토큰)</td> </tr> <tr> <td>대한민국의</td> <td>✅</td> </tr> <tr> <td>수도이다</td> <td>❌ (“수도이며”와 다른 토큰)</td> </tr> </tbody> </table> <p>겹치는 토큰: {인구는, 약, 대한민국의} → $|\text{overlap}| = 3$</p> <h3 id="계산">계산</h3> \[R = \frac{3}{7} \approx 0.4286\] \[P = \frac{3}{7} \approx 0.4286\] \[F_1 = \frac{2 \times 0.4286 \times 0.4286}{0.4286 + 0.4286} \approx 0.4286\] <h2 id="rouge-2-bigram">ROUGE-2 (Bigram)</h2> <h3 id="수식-1">수식</h3> \[\text{ROUGE-2}_{recall} = \frac{\sum_{b \in R_{bigram}} \min(\text{count}_R(b),\ \text{count}_P(b))}{\sum_{b \in R_{bigram}} \text{count}_R(b)}\] <p>unigram 대신 연속 2개 단어 쌍(bigram)으로 겹침을 센다.</p> <h3 id="카운팅-1">카운팅</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답 bigrams (6개):
  (서울의, 인구는)       (인구는, 약)         (약, 천만)
  (천만, 명이고)         (명이고, 대한민국의)   (대한민국의, 수도이다)

예측 bigrams (6개):
  (서울은, 대한민국의)   (대한민국의, 수도이며) (수도이며, 인구는)
  (인구는, 약)           (약, 구백만)          (구백만, 명이다)

겹치는 bigram:
  (인구는, 약) ✅
  나머지 전부 ❌
</code></pre></div></div> <p>$|\text{overlap}| = 1$</p> <h3 id="계산-1">계산</h3> \[R = \frac{1}{6} \approx 0.1667\] \[P = \frac{1}{6} \approx 0.1667\] \[F_1 \approx 0.1667\] <p>ROUGE-2가 ROUGE-1보다 낮은 이유: “인구는”과 “약”이 각각 겹쳐도, 이 둘이 <strong>연속으로</strong> 나타나야만 bigram으로 잡힌다. “대한민국의”가 겹치지만 앞뒤 토큰이 달라서 어떤 bigram에도 기여하지 못한다.</p> <h2 id="rouge-l-longest-common-subsequence">ROUGE-L (Longest Common Subsequence)</h2> <h3 id="수식-2">수식</h3> <p>LCS(Longest Common Subsequence)의 길이를 이용한다.</p> \[R_{lcs} = \frac{\text{LCS}(R, P)}{m}\] \[P_{lcs} = \frac{\text{LCS}(R, P)}{n}\] \[F_{lcs} = \frac{(1 + \beta^2) \times R_{lcs} \times P_{lcs}}{R_{lcs} + \beta^2 \times P_{lcs}}\] <p>$m$ = 정답 길이, $n$ = 예측 길이, $\beta = P_{lcs} / R_{lcs}$ (이렇게 설정하면 $F_{lcs} = F_1$과 동일해진다)</p> <p><strong>핵심</strong>: subsequence는 연속일 필요 없다. 원래 순서만 유지하면 건너뛸 수 있다.</p> <h3 id="lcs-구하기-dp-테이블">LCS 구하기 (DP 테이블)</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [서울의, 인구는, 약, 천만, 명이고, 대한민국의, 수도이다]
P = [서울은, 대한민국의, 수도이며, 인구는, 약, 구백만, 명이다]
</code></pre></div></div> <p>DP 테이블 $L[i][j]$ = $R[0..i]$와 $P[0..j]$의 LCS 길이:</p> <table> <thead> <tr> <th> </th> <th>서울은</th> <th>대한민국의</th> <th>수도이며</th> <th>인구는</th> <th>약</th> <th>구백만</th> <th>명이다</th> </tr> </thead> <tbody> <tr> <td>서울의</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td> </td> <td> </td> </tr> <tr> <td>인구는</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>1</td> <td> </td> <td> </td> </tr> <tr> <td>약</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>2</td> <td> </td> <td> </td> </tr> <tr> <td>천만</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>2</td> <td> </td> <td> </td> </tr> <tr> <td>명이고</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>2</td> <td> </td> <td> </td> </tr> <tr> <td>대한민국의</td> <td>0</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td> </td> <td> </td> </tr> <tr> <td>수도이다</td> <td>0</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td> </td> <td> </td> </tr> </tbody> </table> <p>$\text{LCS}(R, P) = 2$ → [인구는, 약]</p> <p>“대한민국의”가 양쪽에 존재하지만, 정답에서는 6번째(인구는, 약 뒤), 예측에서는 2번째(인구는, 약 앞)이므로 [인구는, 약]과 동시에 LCS에 포함될 수 없다.</p> <h3 id="계산-2">계산</h3> \[R_{lcs} = \frac{2}{7} \approx 0.2857\] \[P_{lcs} = \frac{2}{7} \approx 0.2857\] \[F_1 \approx 0.2857\] <p>이 예시에서 ROUGE-L &lt; ROUGE-1이다. ROUGE-1은 순서를 무시하고 {인구는, 약, 대한민국의} 3개를 세지만, ROUGE-L은 순서 제약 때문에 2개만 센다.</p> <h3 id="rouge-1--10인데-rouge-l이-낮은-극단-예시">ROUGE-1 = 1.0인데 ROUGE-L이 낮은 극단 예시</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [A, B, C]
P = [C, B, A]

ROUGE-1: overlap = {A, B, C} → 3/3 = 1.0
ROUGE-L: LCS = [A] 또는 [B] 또는 [C] → 1/3 = 0.33
</code></pre></div></div> <p>모든 단어가 있지만 순서가 완전히 뒤집혀서 ROUGE-L은 크게 떨어진다. 이것이 ROUGE-L의 존재 이유다.</p> <h2 id="rouge-lsum">ROUGE-Lsum</h2> <h3 id="수식-3">수식</h3> <p>다중 문장에서 문장별로 LCS를 따로 구한 뒤 합산한다.</p> <p>정답이 문장 $r_1, r_2, \ldots, r_k$로 구성될 때:</p> \[R_{lsum} = \frac{\sum_{i=1}^{k} \text{LCS}(r_i, P)}{m}\] <h3 id="예시">예시</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답:
  r1: "서울의 인구는 약 천만 명이다"           (5토큰)
  r2: "서울은 대한민국의 수도이다"              (3토큰)

예측:
  "서울은 대한민국의 수도이며 인구는 약 구백만 명이다"
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LCS(r1, P) = [인구는, 약] → 2
LCS(r2, P) = [서울은, 대한민국의] → 2

Recall = (2 + 2) / (5 + 3) = 4/8 = 0.5
</code></pre></div></div> <p>ROUGE-L을 전체에 한 번 적용하면 문장 경계를 무시해서 한 문장의 매칭이 다른 문장의 점수를 방해할 수 있다. ROUGE-Lsum은 문장별로 독립 계산하므로 요약처럼 여러 문장이 있는 태스크에서 더 공정하다.</p> <p>단일 문장이면 ROUGE-L = ROUGE-Lsum.</p> <h2 id="전체-비교-정리">전체 비교 정리</h2> <table> <thead> <tr> <th>지표</th> <th>수식의 overlap 단위</th> <th>예시 F1</th> <th>특징</th> </tr> </thead> <tbody> <tr> <td>ROUGE-1</td> <td>unigram</td> <td>0.4286</td> <td>가장 관대. 순서 무시</td> </tr> <tr> <td>ROUGE-2</td> <td>bigram</td> <td>0.1667</td> <td>구문 유사도. 연속 매칭 필요</td> </tr> <tr> <td>ROUGE-L</td> <td>LCS</td> <td>0.2857</td> <td>순서 고려. 비연속 허용</td> </tr> <tr> <td>ROUGE-Lsum</td> <td>문장별 LCS 합</td> <td>-</td> <td>다중 문장에서 ROUGE-L 개선</td> </tr> </tbody> </table> <p>일반적으로: <strong>ROUGE-1 &gt;= ROUGE-L &gt;= ROUGE-2</strong></p> <h2 id="rouge의-한계">ROUGE의 한계</h2> <h3 id="동의어를-못-잡는다">동의어를 못 잡는다</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답: "기쁘다"
예측: "행복하다"
→ ROUGE = 0 (의미는 같지만 토큰이 다름)
</code></pre></div></div> <h3 id="길게-쓰면-precision이-희석된다">길게 쓰면 Precision이 희석된다</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답: "교향곡"               → m = 1
예측: "바그너는 교향곡을 쓰려 했다"  → n = 4

Recall = 1/1 = 1.0  (정답 토큰을 다 잡음)
Precision = 1/4 = 0.25  (예측의 75%가 불필요)
F1 = 2 × (1.0 × 0.25) / (1.0 + 0.25) = 0.4
</code></pre></div></div> <h3 id="의미적-정확도를-못-본다">의미적 정확도를 못 본다</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정답: "2023년"
예측: "2024년"
→ ROUGE-1 = 0이지만, 사실상 거의 맞춘 것
</code></pre></div></div> <p>이런 한계 때문에 실무에서는 ROUGE 단독이 아니라 BERTScore(임베딩 유사도), Exact Match, Human Evaluation 등을 함께 쓴다.</p> <h2 id="파이썬으로-직접-계산">파이썬으로 직접 계산</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">evaluate</span>

<span class="n">rouge</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="sh">"</span><span class="s">rouge</span><span class="sh">"</span><span class="p">)</span>

<span class="n">predictions</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">서울은 대한민국의 수도이며 인구는 약 구백만 명이다</span><span class="sh">"</span><span class="p">]</span>
<span class="n">references</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">서울의 인구는 약 천만 명이고 대한민국의 수도이다</span><span class="sh">"</span><span class="p">]</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">rouge</span><span class="p">.</span><span class="nf">compute</span><span class="p">(</span><span class="n">predictions</span><span class="o">=</span><span class="n">predictions</span><span class="p">,</span> <span class="n">references</span><span class="o">=</span><span class="n">references</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
<span class="c1"># {'rouge1': 0.4286, 'rouge2': 0.1667, 'rougeL': 0.2857, 'rougeLsum': 0.2857}
</span></code></pre></div></div>]]></content><author><name></name></author><category term="deep-learning"/><category term="rouge"/><category term="nlp"/><category term="evaluation"/><summary type="html"><![CDATA[Recall-Oriented Understudy for Gisting Evaluation]]></summary></entry><entry><title type="html">ROUGE</title><link href="https://yuseokkang.com/en/blog/2026/rogue/" rel="alternate" type="text/html" title="ROUGE"/><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://yuseokkang.com/en/blog/2026/rogue</id><content type="html" xml:base="https://yuseokkang.com/en/blog/2026/rogue/"><![CDATA[<p>ROUGE stands for <strong>R</strong>ecall-<strong>O</strong>riented <strong>U</strong>nderstudy for <strong>G</strong>isting <strong>E</strong>valuation, a metric that measures how much a model’s generated text overlaps with the reference. It was originally built for evaluating summarisation, but it is used across text generation generally — QA, translation, and so on.</p> <p>The core idea is simple: <strong>how many of the words in the reference show up in the prediction?</strong></p> <h2 id="the-shared-formulas-precision-recall-f1">The shared formulas: precision, recall, F1</h2> <p>Every ROUGE variant follows the same framework. Only the unit in which overlap is counted changes.</p> \[\text{Recall} = \frac{|\text{overlap}|}{|\text{Reference}|}\] \[\text{Precision} = \frac{|\text{overlap}|}{|\text{Prediction}|}\] \[F_1 = \frac{2 \times P \times R}{P + R}\] <ul> <li><strong>Recall</strong>: how much of the reference did we capture (did we miss anything)</li> <li><strong>Precision</strong>: what fraction of the prediction was actually in the reference (is there any nonsense)</li> <li><strong>F1</strong>: the harmonic mean of the two. This is what HuggingFace <code class="language-plaintext highlighter-rouge">evaluate</code> reports by default.</li> </ul> <h2 id="the-running-example">The running example</h2> <p>We will compute every ROUGE variant on the example below.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference:   "Seoul has about ten million residents and is the capital"
Prediction:  "Seoul is the capital and has about nine million residents"
</code></pre></div></div> <p>Tokenised on whitespace:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [Seoul, has, about, ten, million, residents, and, is, the, capital]   -&gt; m = 10
P = [Seoul, is, the, capital, and, has, about, nine, million, residents]  -&gt; n = 10
</code></pre></div></div> <h2 id="rouge-1-unigram">ROUGE-1 (unigram)</h2> <h3 id="formula">Formula</h3> \[\text{ROUGE-1}_{recall} = \frac{\sum_{w \in R} \min(\text{count}_R(w),\ \text{count}_P(w))}{\sum_{w \in R} \text{count}_R(w)}\] <p>For each unigram $w$, take the smaller of its count in the reference and its count in the prediction, and sum.</p> <h3 id="counting">Counting</h3> <table> <thead> <tr> <th>Reference token</th> <th>In the prediction?</th> </tr> </thead> <tbody> <tr> <td>Seoul</td> <td>✅</td> </tr> <tr> <td>has</td> <td>✅</td> </tr> <tr> <td>about</td> <td>✅</td> </tr> <tr> <td>ten</td> <td>❌ (the prediction says “nine”)</td> </tr> <tr> <td>million</td> <td>✅</td> </tr> <tr> <td>residents</td> <td>✅</td> </tr> <tr> <td>and</td> <td>✅</td> </tr> <tr> <td>is</td> <td>✅</td> </tr> <tr> <td>the</td> <td>✅</td> </tr> <tr> <td>capital</td> <td>✅</td> </tr> </tbody> </table> <p>Overlapping tokens: everything but “ten” → $|\text{overlap}| = 9$</p> <h3 id="computation">Computation</h3> \[R = \frac{9}{10} = 0.9\] \[P = \frac{9}{10} = 0.9\] \[F_1 = \frac{2 \times 0.9 \times 0.9}{0.9 + 0.9} = 0.9\] <h2 id="rouge-2-bigram">ROUGE-2 (bigram)</h2> <h3 id="formula-1">Formula</h3> \[\text{ROUGE-2}_{recall} = \frac{\sum_{b \in R_{bigram}} \min(\text{count}_R(b),\ \text{count}_P(b))}{\sum_{b \in R_{bigram}} \text{count}_R(b)}\] <p>Overlap is counted over pairs of adjacent words (bigrams) instead of unigrams.</p> <h3 id="counting-1">Counting</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference bigrams (9):
  (Seoul, has)         (has, about)        (about, ten)
  (ten, million)       (million, residents) (residents, and)
  (and, is)            (is, the)           (the, capital)

Prediction bigrams (9):
  (Seoul, is)          (is, the)           (the, capital)
  (capital, and)       (and, has)          (has, about)
  (about, nine)        (nine, million)     (million, residents)

Overlapping bigrams:
  (has, about) ✅   (million, residents) ✅   (is, the) ✅   (the, capital) ✅
  everything else ❌
</code></pre></div></div> <p>$|\text{overlap}| = 4$</p> <h3 id="computation-1">Computation</h3> \[R = \frac{4}{9} \approx 0.4444\] \[P = \frac{4}{9} \approx 0.4444\] \[F_1 \approx 0.4444\] <p>ROUGE-2 comes out lower than ROUGE-1 because a word only counts once it appears <strong>adjacent</strong> to the right neighbour. “Seoul” and “and” overlap as unigrams, but their surrounding tokens differ, so they contribute to no bigram at all.</p> <h2 id="rouge-l-longest-common-subsequence">ROUGE-L (longest common subsequence)</h2> <h3 id="formula-2">Formula</h3> <p>This one uses the length of the LCS (longest common subsequence).</p> \[R_{lcs} = \frac{\text{LCS}(R, P)}{m}\] \[P_{lcs} = \frac{\text{LCS}(R, P)}{n}\] \[F_{lcs} = \frac{(1 + \beta^2) \times R_{lcs} \times P_{lcs}}{R_{lcs} + \beta^2 \times P_{lcs}}\] <p>$m$ = reference length, $n$ = prediction length, $\beta = P_{lcs} / R_{lcs}$ (setting it this way makes $F_{lcs}$ identical to $F_1$).</p> <p><strong>The key point</strong>: a subsequence does not have to be contiguous. It can skip, as long as the original order is preserved.</p> <h3 id="finding-the-lcs-the-dp-table">Finding the LCS (the DP table)</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [Seoul, has, about, ten, million, residents, and, is, the, capital]
P = [Seoul, is, the, capital, and, has, about, nine, million, residents]
</code></pre></div></div> <p>DP table $L[i][j]$ = the LCS length of $R[0..i]$ and $P[0..j]$:</p> <table> <thead> <tr> <th> </th> <th>Seoul</th> <th>is</th> <th>the</th> <th>capital</th> <th>and</th> <th>has</th> <th>about</th> <th>nine</th> <th>million</th> <th>residents</th> </tr> </thead> <tbody> <tr> <td>Seoul</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> </tr> <tr> <td>has</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> </tr> <tr> <td>about</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>3</td> <td>3</td> <td>3</td> <td>3</td> </tr> <tr> <td>ten</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>3</td> <td>3</td> <td>3</td> <td>3</td> </tr> <tr> <td>million</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>3</td> <td>3</td> <td>4</td> <td>4</td> </tr> <tr> <td>residents</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>3</td> <td>3</td> <td>4</td> <td>5</td> </tr> <tr> <td>and</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>2</td> <td>2</td> <td>3</td> <td>3</td> <td>4</td> <td>5</td> </tr> <tr> <td>is</td> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> <td>3</td> <td>3</td> <td>4</td> <td>5</td> </tr> <tr> <td>the</td> <td>1</td> <td>2</td> <td>3</td> <td>3</td> <td>3</td> <td>3</td> <td>3</td> <td>3</td> <td>4</td> <td>5</td> </tr> <tr> <td>capital</td> <td>1</td> <td>2</td> <td>3</td> <td>4</td> <td>4</td> <td>4</td> <td>4</td> <td>4</td> <td>4</td> <td>5</td> </tr> </tbody> </table> <p>$\text{LCS}(R, P) = 5$ → [Seoul, has, about, million, residents]</p> <p>“and”, “is”, “the” and “capital” all exist on both sides, but in the reference they come <em>after</em> “residents” while in the prediction they come <em>before</em> “has”, so they cannot be part of the LCS at the same time as [has, about, million, residents].</p> <h3 id="computation-2">Computation</h3> \[R_{lcs} = \frac{5}{10} = 0.5\] \[P_{lcs} = \frac{5}{10} = 0.5\] \[F_1 = 0.5\] <p>Here ROUGE-L &lt; ROUGE-1. ROUGE-1 ignores order and counts 9 tokens; ROUGE-L counts only 5 because of the ordering constraint.</p> <h3 id="an-extreme-case-where-rouge-1--10-but-rouge-l-is-low">An extreme case where ROUGE-1 = 1.0 but ROUGE-L is low</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R = [A, B, C]
P = [C, B, A]

ROUGE-1: overlap = {A, B, C} -&gt; 3/3 = 1.0
ROUGE-L: LCS = [A] or [B] or [C] -&gt; 1/3 = 0.33
</code></pre></div></div> <p>Every word is present, but the order is completely reversed, so ROUGE-L falls sharply. This is exactly why ROUGE-L exists.</p> <h2 id="rouge-lsum">ROUGE-Lsum</h2> <h3 id="formula-3">Formula</h3> <p>For multi-sentence text, compute the LCS per reference sentence and sum.</p> <p>When the reference consists of sentences $r_1, r_2, \ldots, r_k$:</p> \[R_{lsum} = \frac{\sum_{i=1}^{k} \text{LCS}(r_i, P)}{m}\] <h3 id="example">Example</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference:
  r1: "Seoul has about ten million residents"    (6 tokens)
  r2: "Seoul is the capital of South Korea"      (7 tokens)

Prediction:
  "Seoul is the capital and has about nine million residents"
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LCS(r1, P) = [Seoul, has, about, million, residents] -&gt; 5
LCS(r2, P) = [Seoul, is, the, capital] -&gt; 4

Recall = (5 + 4) / (6 + 7) = 9/13 ~ 0.6923
</code></pre></div></div> <p>Applying ROUGE-L once over the whole text ignores sentence boundaries, so a match in one sentence can get in the way of another sentence’s score. ROUGE-Lsum computes each sentence independently, which is fairer for tasks like summarisation where there are several sentences.</p> <p>For a single sentence, ROUGE-L = ROUGE-Lsum.</p> <h2 id="overall-comparison">Overall comparison</h2> <table> <thead> <tr> <th>Metric</th> <th>Overlap unit in the formula</th> <th>F1 on the example</th> <th>Character</th> </tr> </thead> <tbody> <tr> <td>ROUGE-1</td> <td>unigram</td> <td>0.9</td> <td>most lenient; ignores order</td> </tr> <tr> <td>ROUGE-2</td> <td>bigram</td> <td>0.4444</td> <td>phrase similarity; requires adjacency</td> </tr> <tr> <td>ROUGE-L</td> <td>LCS</td> <td>0.5</td> <td>order-aware; allows gaps</td> </tr> <tr> <td>ROUGE-Lsum</td> <td>sum of per-sentence LCS</td> <td>-</td> <td>improves ROUGE-L for multi-sentence text</td> </tr> </tbody> </table> <p>Generally: <strong>ROUGE-1 &gt;= ROUGE-L &gt;= ROUGE-2</strong></p> <h2 id="the-limits-of-rouge">The limits of ROUGE</h2> <h3 id="it-cannot-catch-synonyms">It cannot catch synonyms</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference:  "glad"
Prediction: "happy"
-&gt; ROUGE = 0 (same meaning, different token)
</code></pre></div></div> <h3 id="writing-at-length-dilutes-precision">Writing at length dilutes precision</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference:  "symphony"                 -&gt; m = 1
Prediction: "Wagner wrote a symphony"  -&gt; n = 4

Recall = 1/1 = 1.0  (caught every reference token)
Precision = 1/4 = 0.25  (75% of the prediction is unnecessary)
F1 = 2 x (1.0 x 0.25) / (1.0 + 0.25) = 0.4
</code></pre></div></div> <h3 id="it-does-not-see-semantic-accuracy">It does not see semantic accuracy</h3> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reference:  "2023"
Prediction: "2024"
-&gt; ROUGE-1 = 0, even though it is essentially almost right
</code></pre></div></div> <p>Because of limits like these, practitioners use ROUGE alongside BERTScore (embedding similarity), exact match, and human evaluation rather than on its own.</p> <h2 id="computing-it-in-python">Computing it in Python</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">evaluate</span>

<span class="n">rouge</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="sh">"</span><span class="s">rouge</span><span class="sh">"</span><span class="p">)</span>

<span class="n">predictions</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Seoul is the capital and has about nine million residents</span><span class="sh">"</span><span class="p">]</span>
<span class="n">references</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Seoul has about ten million residents and is the capital</span><span class="sh">"</span><span class="p">]</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">rouge</span><span class="p">.</span><span class="nf">compute</span><span class="p">(</span><span class="n">predictions</span><span class="o">=</span><span class="n">predictions</span><span class="p">,</span> <span class="n">references</span><span class="o">=</span><span class="n">references</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
<span class="c1"># {'rouge1': 0.9, 'rouge2': 0.4444, 'rougeL': 0.5, 'rougeLsum': 0.5}
</span></code></pre></div></div>]]></content><author><name></name></author><category term="deep-learning"/><category term="rouge"/><category term="nlp"/><category term="evaluation"/><summary type="html"><![CDATA[Recall-Oriented Understudy for Gisting Evaluation]]></summary></entry><entry><title type="html">덕 타이핑, 그리고 파이썬이 타입을 다루는 방식</title><link href="https://yuseokkang.com/blog/2026/duck-typing/" rel="alternate" type="text/html" title="덕 타이핑, 그리고 파이썬이 타입을 다루는 방식"/><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://yuseokkang.com/blog/2026/duck-typing</id><content type="html" xml:base="https://yuseokkang.com/blog/2026/duck-typing/"><![CDATA[<blockquote> <p>“오리처럼 걷고, 오리처럼 꽥꽥거리면 그것은 오리다.”</p> </blockquote> <h2 id="덕-타이핑이란">덕 타이핑이란?</h2> <p>덕 타이핑은 <strong>객체의 타입을 클래스 선언이 아니라, 그 객체가 가진 메서드와 속성으로 판단</strong>하는 프로그래밍 철학이다. Python, JavaScript, Ruby 같은 동적 타입 언어에서 자주 볼 수 있는 접근 방식이다.</p> <p>타입을 명시적으로 확인하지 않고, 필요한 동작을 그냥 호출한다. 해당 메서드나 속성이 존재하면 성공, 없으면 런타임 에러가 난다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Duck</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">꽥꽥!</span><span class="sh">"</span>

<span class="k">class</span> <span class="nc">Dog</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">멍멍!</span><span class="sh">"</span>

<span class="k">class</span> <span class="nc">Robot</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">삐빅!</span><span class="sh">"</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">):</span>
    <span class="c1"># 타입 확인 없이 그냥 호출
</span>    <span class="nf">print</span><span class="p">(</span><span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">())</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Duck</span><span class="p">())</span>   <span class="c1"># 꽥꽥!
</span><span class="nf">make_sound</span><span class="p">(</span><span class="nc">Dog</span><span class="p">())</span>    <span class="c1"># 멍멍!
</span><span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># 삐빅!
</span></code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Duck</code>이든 <code class="language-plaintext highlighter-rouge">Dog</code>든 <code class="language-plaintext highlighter-rouge">Robot</code>이든, <code class="language-plaintext highlighter-rouge">sound()</code> 메서드만 있으면 동일하게 취급된다. <strong>“이 객체가 무엇인가”보다 “이 객체가 무엇을 할 수 있는가”</strong> 에 집중하는 셈이다.</p> <h2 id="abc는-덕-타이핑을-깨뜨리나">ABC는 덕 타이핑을 깨뜨리나?</h2> <p>결론부터 말하면 <strong>꼭 그렇지는 않다.</strong> ABC를 어떻게 사용하느냐에 따라 달라진다.</p> <h3 id="isinstance-강제-체크--덕-타이핑-깨짐">isinstance() 강제 체크 → 덕 타이핑 깨짐</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">abc</span> <span class="kn">import</span> <span class="n">ABC</span><span class="p">,</span> <span class="n">abstractmethod</span>

<span class="k">class</span> <span class="nc">Animal</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
    <span class="nd">@abstractmethod</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span> <span class="k">pass</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">animal</span><span class="p">,</span> <span class="n">Animal</span><span class="p">):</span>  <span class="c1"># 타입 직접 확인!
</span>        <span class="k">raise</span> <span class="nc">TypeError</span><span class="p">(</span><span class="sh">"</span><span class="s">Animal이 아님</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">())</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ❌ sound()가 있어도 거부됨
</span></code></pre></div></div> <h3 id="abc는-명세-역할만-체크-없음--덕-타이핑-유지">ABC는 명세 역할만, 체크 없음 → 덕 타이핑 유지</h3> <p>함수 안에서 타입을 체크하지 않으면 ABC를 써도 덕 타이핑은 그대로 동작한다. ABC가 하는 일은 “이 메서드는 꼭 구현해야 해”라는 개발자 간의 약속일 뿐이다.</p> <h3 id="register로-절충">register()로 절충</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Animal</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="n">Robot</span><span class="p">)</span>  <span class="c1"># Robot을 Animal로 등록
</span>
<span class="nf">print</span><span class="p">(</span><span class="nf">isinstance</span><span class="p">(</span><span class="nc">Robot</span><span class="p">(),</span> <span class="n">Animal</span><span class="p">))</span>  <span class="c1"># ✅ True (상속 없이도 통과)
</span></code></pre></div></div> <p>덕 타이핑의 유연함을 유지하면서 타입 체계도 일부 활용하는 절충안이다. 단, 개발자가 수동으로 등록해야 한다는 번거로움이 있다.</p> <table> <thead> <tr> <th>상황</th> <th>덕 타이핑</th> </tr> </thead> <tbody> <tr> <td>ABC + <code class="language-plaintext highlighter-rouge">isinstance()</code> 강제 체크</td> <td>❌ 깨짐</td> </tr> <tr> <td>ABC만 정의, 체크 없이 호출</td> <td>✅ 유지</td> </tr> <tr> <td>ABC + <code class="language-plaintext highlighter-rouge">register()</code> 활용</td> <td>〰 절충</td> </tr> </tbody> </table> <h2 id="타입-힌트는-덕-타이핑과-무관하다">타입 힌트는 덕 타이핑과 무관하다</h2> <p>타입 힌트는 런타임에 <strong>완전히 무시</strong>된다. <code class="language-plaintext highlighter-rouge">animal: Duck</code>이라고 써도 파이썬 인터프리터는 실행 중에 이를 전혀 검사하지 않는다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">:</span> <span class="n">Duck</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">()</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ✅ 아무 문제 없이 실행됨
</span></code></pre></div></div> <p>타입 힌트가 실제로 하는 일은 다음과 같다.</p> <ul> <li><strong>정적 분석</strong>: mypy, pyright 같은 타입 체커가 코드 실행 전에 경고를 띄워줌</li> <li><strong>IDE 지원</strong>: 자동완성과 경고 표시에 활용됨</li> <li><strong>문서화</strong>: 코드를 읽는 사람에게 의도를 전달하는 역할</li> </ul> <p>mypy를 CI에 붙여 강제하면 실질적으로 덕 타이핑을 제한하는 효과가 생기지만, 이는 어디까지나 개발 단계의 검사다. 런타임 동작은 여전히 덕 타이핑이다.</p> <h2 id="protocol-타입-힌트를-위한-덕-타이핑">Protocol: 타입 힌트를 위한 덕 타이핑</h2> <p>mypy를 사용하면서도 덕 타이핑의 철학을 유지하고 싶을 때 <strong>Protocol</strong>을 쓴다. Protocol의 핵심 존재 이유는 바로 <strong>타입 힌트와 정적 분석</strong>이다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Protocol</span>

<span class="k">class</span> <span class="nc">Soundable</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span> <span class="bp">...</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">:</span> <span class="n">Soundable</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="c1"># "sound() 메서드만 있으면 OK"
</span>    <span class="k">return</span> <span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">Robot</span><span class="p">:</span>  <span class="c1"># Soundable을 상속하지 않아도
</span>    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">삐빅!</span><span class="sh">"</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ✅ mypy 통과, 런타임도 통과
</span></code></pre></div></div> <p>이를 <strong>구조적 서브타이핑(Structural Subtyping)</strong> 이라고 부른다. 상속 관계 없이 “이 메서드를 가지고 있으면 이 타입으로 간주”하는 방식으로, 타입 힌트 세계에서 덕 타이핑의 정신을 그대로 구현한다.</p> <h3 id="runtime_checkable-런타임에서도-사용하기">@runtime_checkable: 런타임에서도 사용하기</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Protocol</span><span class="p">,</span> <span class="n">runtime_checkable</span>

<span class="nd">@runtime_checkable</span>
<span class="k">class</span> <span class="nc">Soundable</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span> <span class="bp">...</span>

<span class="nf">print</span><span class="p">(</span><span class="nf">isinstance</span><span class="p">(</span><span class="nc">Robot</span><span class="p">(),</span> <span class="n">Soundable</span><span class="p">))</span>  <span class="c1"># ✅ True
# 메서드 존재 여부를 자동으로 확인
</span></code></pre></div></div> <p>ABC의 <code class="language-plaintext highlighter-rouge">register()</code>와 비슷하지만, 개발자가 수동으로 등록할 필요 없이 메서드의 존재만으로 자동으로 판단한다.</p> <table> <thead> <tr> <th>방식</th> <th>덕 타이핑</th> <th>정적 분석</th> <th>런타임 isinstance</th> </tr> </thead> <tbody> <tr> <td>순수 덕 타이핑</td> <td>✅</td> <td>❌</td> <td>❌</td> </tr> <tr> <td>ABC + <code class="language-plaintext highlighter-rouge">register()</code></td> <td>〰</td> <td>❌</td> <td>✅ (수동)</td> </tr> <tr> <td>Protocol</td> <td>✅</td> <td>✅</td> <td>❌</td> </tr> <tr> <td>Protocol + <code class="language-plaintext highlighter-rouge">runtime_checkable</code></td> <td>✅</td> <td>✅</td> <td>✅ (자동)</td> </tr> </tbody> </table> <h2 id="정리">정리</h2> <ul> <li>덕 타이핑은 “무엇인가”보다 “무엇을 할 수 있는가”로 타입을 판단한다.</li> <li>ABC는 사용 방식에 따라 덕 타이핑을 깨뜨릴 수도, 유지할 수도 있다.</li> <li>타입 힌트는 런타임에 무시되므로 덕 타이핑에 직접 영향을 주지 않는다.</li> <li>Protocol은 타입 힌트 + 정적 분석 환경에서 덕 타이핑 철학을 유지하는 수단이다.</li> </ul>]]></content><author><name></name></author><category term="python"/><category term="python"/><category term="type-system"/><category term="oop"/><summary type="html"><![CDATA[덕 타이핑이란 무엇인지, ABC와 Protocol이 덕 타이핑에 어떤 영향을 미치는지, 그리고 타입 힌트가 덕 타이핑과 어떻게 공존하는지에 대한 설명.]]></summary></entry><entry><title type="html">Duck typing, and how Python handles types</title><link href="https://yuseokkang.com/en/blog/2026/duck-typing/" rel="alternate" type="text/html" title="Duck typing, and how Python handles types"/><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://yuseokkang.com/en/blog/2026/duck-typing</id><content type="html" xml:base="https://yuseokkang.com/en/blog/2026/duck-typing/"><![CDATA[<blockquote> <p>“If it walks like a duck and quacks like a duck, it’s a duck.”</p> </blockquote> <h2 id="what-is-duck-typing">What is duck typing?</h2> <p>Duck typing is the programming philosophy of <strong>judging an object’s type by the methods and attributes it has, not by its class declaration</strong>. It is a common approach in dynamically typed languages such as Python, JavaScript, and Ruby.</p> <p>Rather than checking the type explicitly, you simply call the behaviour you need. If the method or attribute exists, it works; if not, you get a runtime error.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Duck</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">Quack!</span><span class="sh">"</span>

<span class="k">class</span> <span class="nc">Dog</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">Woof!</span><span class="sh">"</span>

<span class="k">class</span> <span class="nc">Robot</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">Beep!</span><span class="sh">"</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">):</span>
    <span class="c1"># just call it, no type check
</span>    <span class="nf">print</span><span class="p">(</span><span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">())</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Duck</span><span class="p">())</span>   <span class="c1"># Quack!
</span><span class="nf">make_sound</span><span class="p">(</span><span class="nc">Dog</span><span class="p">())</span>    <span class="c1"># Woof!
</span><span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># Beep!
</span></code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Duck</code>, <code class="language-plaintext highlighter-rouge">Dog</code>, <code class="language-plaintext highlighter-rouge">Robot</code> — all are treated identically as long as they have a <code class="language-plaintext highlighter-rouge">sound()</code> method. The focus is on <strong>“what can this object do?” rather than “what is this object?”</strong></p> <h2 id="do-abcs-break-duck-typing">Do ABCs break duck typing?</h2> <p>The short answer is <strong>not necessarily.</strong> It depends on how you use them.</p> <h3 id="forcing-an-isinstance-check--duck-typing-broken">Forcing an isinstance() check → duck typing broken</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">abc</span> <span class="kn">import</span> <span class="n">ABC</span><span class="p">,</span> <span class="n">abstractmethod</span>

<span class="k">class</span> <span class="nc">Animal</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
    <span class="nd">@abstractmethod</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span> <span class="k">pass</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">animal</span><span class="p">,</span> <span class="n">Animal</span><span class="p">):</span>  <span class="c1"># checking the type directly!
</span>        <span class="k">raise</span> <span class="nc">TypeError</span><span class="p">(</span><span class="sh">"</span><span class="s">not an Animal</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">())</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ❌ rejected even though it has sound()
</span></code></pre></div></div> <h3 id="abc-as-specification-only-no-check--duck-typing-preserved">ABC as specification only, no check → duck typing preserved</h3> <p>If you do not check the type inside the function, duck typing keeps working even with an ABC in play. All the ABC does then is act as an agreement among developers: “this method must be implemented.”</p> <h3 id="a-compromise-with-register">A compromise with register()</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Animal</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="n">Robot</span><span class="p">)</span>  <span class="c1"># register Robot as an Animal
</span>
<span class="nf">print</span><span class="p">(</span><span class="nf">isinstance</span><span class="p">(</span><span class="nc">Robot</span><span class="p">(),</span> <span class="n">Animal</span><span class="p">))</span>  <span class="c1"># ✅ True (passes without inheritance)
</span></code></pre></div></div> <p>This is a middle ground that keeps duck typing’s flexibility while making partial use of the type system. The drawback is that a developer has to register things manually.</p> <table> <thead> <tr> <th>Situation</th> <th>Duck typing</th> </tr> </thead> <tbody> <tr> <td>ABC + forced <code class="language-plaintext highlighter-rouge">isinstance()</code> check</td> <td>❌ broken</td> </tr> <tr> <td>ABC defined only, called without a check</td> <td>✅ preserved</td> </tr> <tr> <td>ABC + <code class="language-plaintext highlighter-rouge">register()</code></td> <td>〰 compromise</td> </tr> </tbody> </table> <h2 id="type-hints-have-nothing-to-do-with-duck-typing">Type hints have nothing to do with duck typing</h2> <p>Type hints are <strong>completely ignored</strong> at runtime. Writing <code class="language-plaintext highlighter-rouge">animal: Duck</code> does not make the Python interpreter check anything during execution.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">:</span> <span class="n">Duck</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">()</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ✅ runs without any problem
</span></code></pre></div></div> <p>What type hints actually do is this:</p> <ul> <li><strong>Static analysis</strong>: type checkers like mypy and pyright warn you before the code runs</li> <li><strong>IDE support</strong>: used for autocompletion and warnings</li> <li><strong>Documentation</strong>: conveying intent to whoever reads the code</li> </ul> <p>Wiring mypy into CI effectively constrains duck typing, but that is a check at development time. The runtime behaviour is still duck typing.</p> <h2 id="protocol-duck-typing-for-type-hints">Protocol: duck typing for type hints</h2> <p>When you want to use mypy and still keep the spirit of duck typing, you use <strong>Protocol</strong>. The whole reason Protocol exists is <strong>type hints and static analysis</strong>.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Protocol</span>

<span class="k">class</span> <span class="nc">Soundable</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span> <span class="bp">...</span>

<span class="k">def</span> <span class="nf">make_sound</span><span class="p">(</span><span class="n">animal</span><span class="p">:</span> <span class="n">Soundable</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="c1"># "having a sound() method is enough"
</span>    <span class="k">return</span> <span class="n">animal</span><span class="p">.</span><span class="nf">sound</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">Robot</span><span class="p">:</span>  <span class="c1"># without inheriting from Soundable
</span>    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">Beep!</span><span class="sh">"</span>

<span class="nf">make_sound</span><span class="p">(</span><span class="nc">Robot</span><span class="p">())</span>  <span class="c1"># ✅ passes mypy, passes at runtime
</span></code></pre></div></div> <p>This is called <strong>structural subtyping</strong>. It treats an object as belonging to a type if it has the required methods, with no inheritance relationship — the spirit of duck typing, implemented inside the world of type hints.</p> <h3 id="runtime_checkable-using-it-at-runtime-too">@runtime_checkable: using it at runtime too</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Protocol</span><span class="p">,</span> <span class="n">runtime_checkable</span>

<span class="nd">@runtime_checkable</span>
<span class="k">class</span> <span class="nc">Soundable</span><span class="p">(</span><span class="n">Protocol</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">sound</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span> <span class="bp">...</span>

<span class="nf">print</span><span class="p">(</span><span class="nf">isinstance</span><span class="p">(</span><span class="nc">Robot</span><span class="p">(),</span> <span class="n">Soundable</span><span class="p">))</span>  <span class="c1"># ✅ True
# checks for the presence of the methods automatically
</span></code></pre></div></div> <p>This resembles an ABC’s <code class="language-plaintext highlighter-rouge">register()</code>, but there is no manual registration: the decision is made automatically from the presence of the methods.</p> <table> <thead> <tr> <th>Approach</th> <th>Duck typing</th> <th>Static analysis</th> <th>Runtime isinstance</th> </tr> </thead> <tbody> <tr> <td>Pure duck typing</td> <td>✅</td> <td>❌</td> <td>❌</td> </tr> <tr> <td>ABC + <code class="language-plaintext highlighter-rouge">register()</code></td> <td>〰</td> <td>❌</td> <td>✅ (manual)</td> </tr> <tr> <td>Protocol</td> <td>✅</td> <td>✅</td> <td>❌</td> </tr> <tr> <td>Protocol + <code class="language-plaintext highlighter-rouge">runtime_checkable</code></td> <td>✅</td> <td>✅</td> <td>✅ (automatic)</td> </tr> </tbody> </table> <h2 id="summary">Summary</h2> <ul> <li>Duck typing judges a type by “what it can do”, not “what it is”.</li> <li>Depending on how you use them, ABCs can either break duck typing or preserve it.</li> <li>Type hints are ignored at runtime, so they do not directly affect duck typing.</li> <li>Protocol is the way to keep the duck typing philosophy in a type-hinted, statically analysed codebase.</li> </ul>]]></content><author><name></name></author><category term="python"/><category term="python"/><category term="type-system"/><category term="oop"/><summary type="html"><![CDATA[What duck typing is, how ABCs and Protocol affect it, and how type hints coexist with duck typing.]]></summary></entry><entry><title type="html">행렬 곱의 네 가지 관점</title><link href="https://yuseokkang.com/blog/2025/four-different-perspectives-of-matrix-multiplication/" rel="alternate" type="text/html" title="행렬 곱의 네 가지 관점"/><published>2025-12-25T00:00:00+00:00</published><updated>2025-12-25T00:00:00+00:00</updated><id>https://yuseokkang.com/blog/2025/four-different-perspectives-of-matrix-multiplication</id><content type="html" xml:base="https://yuseokkang.com/blog/2025/four-different-perspectives-of-matrix-multiplication/"><![CDATA[<p>유튜브 혁펜하임 영상 중 행렬 곱셈에 대한 네 가지 관점을 소개하는 부분이 인상적이었다. 이 네 가지 관점을 머릿속에서 자유자재로 오갈 수 있으면 선형대수학에 대한 이해가 훨씬 빠를 것으로 기대한다.</p> <p>$A\in\mathbb{R}^{m\times n},\ B\in\mathbb{R}^{n\times p}$</p> <h2 id="내적">내적</h2> <p>$A$의 $i$번째 <strong>행</strong>을 $a_i^T$, $B$의 $j$번째 <strong>열</strong>을 $b_j$라 두면</p> \[A= \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix}, \qquad B=[b_1\ b_2\ \cdots\ b_p]\] <p>그때 $C=AB$의 원소는</p> \[c_{ij}=a_i^T b_j\] <p>즉, “$C$의 $(i,j)$”는 <strong>$A$의 $i$번째 행</strong>과 <strong>$B$의 $j$번째 열</strong>의 내적이다.</p> <h2 id="rank-1-matrix들의-합">rank-1 matrix들의 합</h2> <p>이번엔 $A$를 <strong>열벡터</strong>로, $B$를 <strong>행벡터</strong>로 풀어 쓰면,</p> \[A=[a_1\ a_2\ \cdots\ a_n], \qquad B= \begin{bmatrix} b_1^T\\ b_2^T\\ \vdots\\ b_n^T \end{bmatrix}\] <ul> <li>$a_k\in\mathbb{R}^{m}$: $A$의 $k$번째 열</li> <li>$b_k^T\in\mathbb{R}^{1\times p}$: $B$의 $k$번째 행</li> </ul> <p>그럼 행렬곱은</p> \[AB= a_1 b_1^T + a_2 b_2^T + \cdots + a_n b_n^T= \sum_{k=1}^{n} a_k b_k^T\] <p>즉 $AB$는 이런 <strong>rank-1 행렬들을 $k=1\sim n$까지 더한 것</strong>이다.</p> <h2 id="column-space-관점--ax">Column space 관점 = $Ax$</h2> <p>$A$를 열벡터로 나타내면,</p> \[A = [a_1\ a_2\ \cdots\ a_n] \quad (a_k \in \mathbb{R}^m)\] <p>그리고 $x\in\mathbb{R}^n$을</p> \[x=\begin{bmatrix}x_1\\ x_2\\ \vdots\\ x_n \end{bmatrix}\] <p>라고 하면,</p> \[Ax = [a_1\ a_2\ \cdots\ a_n] \begin{bmatrix}x_1\\ x_2\\ \vdots \\x_n \end{bmatrix} = x_1 a_1 + x_2 a_2 + \cdots + x_n a_n\] <p><strong>$Ax$는 $A$의 열벡터 $a_k$들의 선형결합</strong>이라서 항상 $\mathrm{Col}(A)$에 있다.</p> <h2 id="row-space-관점--xta">Row space 관점 = $x^TA$</h2> <p>이번엔 $A$를 행벡터로 나타내면,</p> \[A = \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix} \quad (a_i^T \in \mathbb{R}^{1\times n})\] <p>그리고 $x\in\mathbb{R}^m$을</p> \[x^T=[x_1\ x_2\ \cdots\ x_m]\] <p>라고 하면,</p> \[x^T A= [x_1\ x_2\ \cdots\ x_m] \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix} = x_1 a_1^T + x_2 a_2^T + \cdots + x_m a_m^T\] <p><strong>$x^T A$는 $A$의 행벡터 $a_i^T$들의 선형결합</strong>이라서 항상 $\mathrm{Row}(A)$에 있다.</p>]]></content><author><name></name></author><category term="mathematics"/><category term="linear-algebra"/><summary type="html"><![CDATA[The four different perspectives of matrix multiplication.]]></summary></entry><entry><title type="html">Four perspectives on matrix multiplication</title><link href="https://yuseokkang.com/en/blog/2025/four-different-perspectives-of-matrix-multiplication/" rel="alternate" type="text/html" title="Four perspectives on matrix multiplication"/><published>2025-12-25T00:00:00+00:00</published><updated>2025-12-25T00:00:00+00:00</updated><id>https://yuseokkang.com/en/blog/2025/four-different-perspectives-of-matrix-multiplication</id><content type="html" xml:base="https://yuseokkang.com/en/blog/2025/four-different-perspectives-of-matrix-multiplication/"><![CDATA[<p>In one of Hyukppen Heim’s YouTube videos, the section introducing four different perspectives on matrix multiplication stuck with me. Being able to move freely between these four views should make linear algebra click much faster.</p> <p>$A\in\mathbb{R}^{m\times n},\ B\in\mathbb{R}^{n\times p}$</p> <h2 id="inner-products">Inner products</h2> <p>Write the $i$-th <strong>row</strong> of $A$ as $a_i^T$ and the $j$-th <strong>column</strong> of $B$ as $b_j$:</p> \[A= \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix}, \qquad B=[b_1\ b_2\ \cdots\ b_p]\] <p>Then the entries of $C=AB$ are</p> \[c_{ij}=a_i^T b_j\] <p>That is, “$(i,j)$ of $C$” is the inner product of <strong>the $i$-th row of $A$</strong> and <strong>the $j$-th column of $B$</strong>.</p> <h2 id="a-sum-of-rank-1-matrices">A sum of rank-1 matrices</h2> <p>This time expand $A$ into <strong>column vectors</strong> and $B$ into <strong>row vectors</strong>:</p> \[A=[a_1\ a_2\ \cdots\ a_n], \qquad B= \begin{bmatrix} b_1^T\\ b_2^T\\ \vdots\\ b_n^T \end{bmatrix}\] <ul> <li>$a_k\in\mathbb{R}^{m}$: the $k$-th column of $A$</li> <li>$b_k^T\in\mathbb{R}^{1\times p}$: the $k$-th row of $B$</li> </ul> <p>The product then becomes</p> \[AB= a_1 b_1^T + a_2 b_2^T + \cdots + a_n b_n^T= \sum_{k=1}^{n} a_k b_k^T\] <p>So $AB$ is <strong>the sum of these rank-1 matrices over $k=1$ through $n$</strong>.</p> <h2 id="the-column-space-view--ax">The column space view = $Ax$</h2> <p>Writing $A$ as column vectors,</p> \[A = [a_1\ a_2\ \cdots\ a_n] \quad (a_k \in \mathbb{R}^m)\] <p>and taking $x\in\mathbb{R}^n$ to be</p> \[x=\begin{bmatrix}x_1\\ x_2\\ \vdots\\ x_n \end{bmatrix}\] <p>we get</p> \[Ax = [a_1\ a_2\ \cdots\ a_n] \begin{bmatrix}x_1\\ x_2\\ \vdots \\x_n \end{bmatrix} = x_1 a_1 + x_2 a_2 + \cdots + x_n a_n\] <p><strong>$Ax$ is a linear combination of the column vectors $a_k$ of $A$</strong>, so it always lies in $\mathrm{Col}(A)$.</p> <h2 id="the-row-space-view--xta">The row space view = $x^TA$</h2> <p>Now write $A$ as row vectors,</p> \[A = \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix} \quad (a_i^T \in \mathbb{R}^{1\times n})\] <p>and take $x\in\mathbb{R}^m$ to be</p> \[x^T=[x_1\ x_2\ \cdots\ x_m]\] <p>which gives</p> \[x^T A= [x_1\ x_2\ \cdots\ x_m] \begin{bmatrix} a_1^T\\ a_2^T\\ \vdots\\ a_m^T \end{bmatrix} = x_1 a_1^T + x_2 a_2^T + \cdots + x_m a_m^T\] <p><strong>$x^T A$ is a linear combination of the row vectors $a_i^T$ of $A$</strong>, so it always lies in $\mathrm{Row}(A)$.</p>]]></content><author><name></name></author><category term="mathematics"/><category term="linear-algebra"/><summary type="html"><![CDATA[The four different perspectives of matrix multiplication.]]></summary></entry><entry><title type="html">임베딩 모델에 새로운 단어 가르치기</title><link href="https://yuseokkang.com/blog/2025/fine-tune-embedding/" rel="alternate" type="text/html" title="임베딩 모델에 새로운 단어 가르치기"/><published>2025-10-25T00:00:00+00:00</published><updated>2025-10-25T00:00:00+00:00</updated><id>https://yuseokkang.com/blog/2025/fine-tune-embedding</id><content type="html" xml:base="https://yuseokkang.com/blog/2025/fine-tune-embedding/"><![CDATA[<p>Hugging Face에 있는 사전학습 임베딩 모델들은 대단히 강력하다. 방대한 어휘의 의미와 맥락을 이해한다. 그런데 모델이 한 번도 본 적 없는 단어가 들어간 텍스트를 다뤄야 한다면 어떻게 될까?</p> <p>이런 것들을 떠올려 보자:</p> <ul> <li><strong>기술 전문 용어:</strong> “Gecko-Embeddings”</li> <li><strong>틈새 용어:</strong> “DeepLearningGenius”</li> </ul> <p>모델이 이 단어들을 본 적이 없다면, 의미 없는 subword로 쪼갤 가능성이 크다 (예: “XIGN” + “CODE” 또는 “xig” + “##nco” + “##de”). 이러면 용어의 고유한 의미가 <em>사라진다</em>.</p> <p>해법은 <strong>새 단어를 모델의 어휘에 추가하고, 그 의미를 학습하도록 fine-tuning하는 것</strong>이다. 이 글에서는 <code class="language-plaintext highlighter-rouge">sentence-transformers</code> 라이브러리를 예로 그 방법을 보인다.</p> <h2 id="새-단어를-추가하는-두-단계">새 단어를 추가하는 두 단계</h2> <p>새 단어를 추가하는 건 한 단계가 아니다. 모델의 “사전”(tokenizer)과 “뇌”(모델 가중치)를 둘 다 갱신해야 한다.</p> <ol> <li><strong>tokenizer 갱신:</strong> 먼저 새 단어가 하나의 완결된 단위로 존재한다고 tokenizer에게 알려준다. 이러면 subword로 쪼개지지 않는다.</li> <li><strong>모델 갱신 및 fine-tuning:</strong> 단어를 추가하면 모델이 임베딩 행렬에 “자리”를 만들지만, 이 새 임베딩 벡터는 그저 랜덤 노이즈다. 아무 의미가 없다. 그 단어가 들어간 새 문장들로 모델을 <strong>반드시</strong> fine-tuning해서, 맥락으로부터 단어의 의미를 배우게 해야 한다.</li> </ol> <h2 id="새-토큰-추가하기-코드">새 토큰 추가하기 (코드)</h2> <p><code class="language-plaintext highlighter-rouge">sentence-transformers</code>로 모델을 불러오고 내부 tokenizer와 임베딩 행렬을 수정해 보자.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">import</span> <span class="n">torch</span>

<span class="c1"># 베이스 모델 로드
</span><span class="n">model_name</span> <span class="o">=</span> <span class="sh">'</span><span class="s">sentence-transformers/all-MiniLM-L6-v2</span><span class="sh">'</span>
<span class="n">model</span> <span class="o">=</span> <span class="nc">SentenceTransformer</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>

<span class="c1"># 1. 내부 tokenizer와 모델 얻기
# 수정하려면 하위의 'transformers' 모델에 접근해야 한다
</span><span class="n">tokenizer</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">tokenizer</span>
<span class="n">transformer_model</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">auto_model</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Original vocabulary size: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># 2. 새 단어 정의
# 중요: 모델이 'cased'인지 'uncased'인지 먼저 확인할 것!
# 이 모델('all-MiniLM-L6-v2')은 uncased라서 전부 소문자로 바꾼다.
# 소문자 버전만 추가하면 된다.
# 'cased' 모델을 쓴다면 이렇게 추가할 수도 있다: ['XIGNCODE', 'xigncode', 'Xigncode']
</span><span class="n">new_words</span> <span class="o">=</span> <span class="p">[</span><span class="sh">'</span><span class="s">xigncode</span><span class="sh">'</span><span class="p">]</span>

<span class="c1"># 3. tokenizer에 새 토큰 추가
</span><span class="n">num_added_toks</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">add_tokens</span><span class="p">(</span><span class="n">new_words</span><span class="p">)</span>

<span class="k">if</span> <span class="n">num_added_toks</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Added </span><span class="si">{</span><span class="n">num_added_toks</span><span class="si">}</span><span class="s"> new tokens.</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 4. 모델의 토큰 임베딩 크기 조정
</span>    <span class="c1"># 새 토큰(들)을 위해 무작위로 초기화된 벡터가 추가된다
</span>    <span class="n">transformer_model</span><span class="p">.</span><span class="nf">resize_token_embeddings</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">))</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">New vocabulary size: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Tokens already exist in the vocabulary.</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># tokenizer 테스트
</span><span class="n">test_sentence</span> <span class="o">=</span> <span class="sh">"</span><span class="s">This is a test of xigncode and XIGNCODE3.</span><span class="sh">"</span>
<span class="n">tokenized</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">tokenize</span><span class="p">(</span><span class="n">test_sentence</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Tokenization test: </span><span class="si">{</span><span class="n">tokenized</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="c1"># 기대 출력: ['this', 'is', 'a', 'test', 'of', 'xigncode', 'and', 'xigncode', '3']
</span></code></pre></div></div> <p>위 출력에서 두 가지를 눈여겨보자:</p> <ol> <li><code class="language-plaintext highlighter-rouge">xigncode</code>가 단일 토큰으로 인식됐다.</li> <li><code class="language-plaintext highlighter-rouge">XIGNCODE3</code>는 <code class="language-plaintext highlighter-rouge">['xigncode', '3']</code>으로 토큰화됐다. “최장 일치” 규칙이 작동한 것이고, 우리가 원하던 결과다!</li> </ol> <h2 id="fine-tuning-의미-가르치기">fine-tuning (의미 가르치기)</h2> <p>이제 모델이 “xigncode”를 <em>인식</em> 하니, 그것이 무엇을 <em>의미하는지</em> 가르쳐야 한다. 그 단어가 맥락 속에서 쓰인 문장들로 fine-tuning하면 된다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">torch.utils.data</span> <span class="kn">import</span> <span class="n">DataLoader</span>
<span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">InputExample</span><span class="p">,</span> <span class="n">losses</span><span class="p">,</span> <span class="n">models</span>

<span class="c1"># 1. 학습 예제 만들기
# 새 단어에 맥락을 제공하는 문장이 필요하다.
</span><span class="n">train_examples</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1"># 맥락 부여: "xigncode"는 "anti-cheat"과 관련이 있다
</span>    <span class="nc">InputExample</span><span class="p">(</span><span class="n">texts</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">xigncode is an anti-cheat solution.</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">This software prevents cheating in online games.</span><span class="sh">'</span><span class="p">]),</span>

    <span class="c1"># 또 다른 맥락 부여:
</span>    <span class="nc">InputExample</span><span class="p">(</span><span class="n">texts</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">Many gamers are familiar with xigncode.</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">It is a well-known security program.</span><span class="sh">'</span><span class="p">])</span>

    <span class="c1"># 더 잘 배우려면 예제를 훨씬 많이 추가할 것...
</span><span class="p">]</span>

<span class="c1"># 2. 데이터로더와 손실 함수 설정
</span><span class="n">train_dataloader</span> <span class="o">=</span> <span class="nc">DataLoader</span><span class="p">(</span><span class="n">train_examples</span><span class="p">,</span> <span class="n">shuffle</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">batch_size</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
<span class="n">train_loss</span> <span class="o">=</span> <span class="n">losses</span><span class="p">.</span><span class="nc">MultipleNegativesRankingLoss</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">)</span>

<span class="c1"># 3. 모델 fine-tuning
</span><span class="n">num_epochs</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">warmup_steps</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">train_dataloader</span><span class="p">)</span> <span class="o">*</span> <span class="n">num_epochs</span> <span class="o">*</span> <span class="mf">0.1</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Starting fine-tuning...</span><span class="sh">"</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span>
    <span class="n">train_objectives</span><span class="o">=</span><span class="p">[(</span><span class="n">train_dataloader</span><span class="p">,</span> <span class="n">train_loss</span><span class="p">)],</span>
    <span class="n">epochs</span><span class="o">=</span><span class="n">num_epochs</span><span class="p">,</span>
    <span class="n">warmup_steps</span><span class="o">=</span><span class="n">warmup_steps</span><span class="p">,</span>
    <span class="n">output_path</span><span class="o">=</span><span class="sh">'</span><span class="s">./my-finetuned-model</span><span class="sh">'</span><span class="p">,</span>
    <span class="n">show_progress_bar</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Fine-tuning complete. Model saved to </span><span class="sh">'</span><span class="s">./my-finetuned-model</span><span class="sh">'</span><span class="s">.</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <h2 id="새-모델-사용하기-langchain과-함께">새 모델 사용하기 (LangChain과 함께)</h2> <p>더 똑똑해진 새 모델이 <code class="language-plaintext highlighter-rouge">./my-finetuned-model</code> 디렉토리에 저장됐다. <code class="language-plaintext highlighter-rouge">sentence-transformers</code>에서 직접 불러올 수도 있고, 저장 경로만 가리키면 <code class="language-plaintext highlighter-rouge">LangChain</code>에서도 쓸 수 있다.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">langchain_huggingface</span> <span class="kn">import</span> <span class="n">HuggingFaceEmbeddings</span>

<span class="c1"># 로컬에 fine-tuning된 모델 디렉토리를 가리킨다
</span><span class="n">model_path</span> <span class="o">=</span> <span class="sh">"</span><span class="s">./my-finetuned-model</span><span class="sh">"</span>

<span class="c1"># LangChain이 모델과 새 tokenizer를 함께 로드한다
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="nc">HuggingFaceEmbeddings</span><span class="p">(</span>
    <span class="n">model_name</span><span class="o">=</span><span class="n">model_path</span><span class="p">,</span>
    <span class="n">model_kwargs</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">device</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">cpu</span><span class="sh">'</span><span class="p">},</span> <span class="c1"># 가능하면 'cuda'
</span>    <span class="n">encode_kwargs</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">normalize_embeddings</span><span class="sh">'</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>
<span class="p">)</span>

<span class="c1"># 이제 이 텍스트가 더 정확한 임베딩을 만든다
</span><span class="n">text</span> <span class="o">=</span> <span class="sh">"</span><span class="s">Tell me about the xigncode anti-cheat.</span><span class="sh">"</span>
<span class="n">query_embedding</span> <span class="o">=</span> <span class="n">embeddings</span><span class="p">.</span><span class="nf">embed_query</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Successfully created embedding with the new word!</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Vector dimension: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <h2 id="놓치기-쉬운-지점-대소문자와-subword">놓치기 쉬운 지점: 대소문자와 subword</h2> <p>기억해둘 만한 중요한 지점 몇 가지:</p> <h3 id="xigncode-vs-xigncode-대소문자-구분"><code class="language-plaintext highlighter-rouge">XIGNCODE</code> vs. <code class="language-plaintext highlighter-rouge">xigncode</code> (대소문자 구분)</h3> <ul> <li><strong>Uncased 모델</strong> (<code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code> 같은): 전부 자동으로 소문자화한다. 소문자 <code class="language-plaintext highlighter-rouge">xigncode</code>만 추가하면 된다. <code class="language-plaintext highlighter-rouge">XIGNCODE</code>, <code class="language-plaintext highlighter-rouge">xigncode</code>, <code class="language-plaintext highlighter-rouge">XignCode</code> 모두 매칭된다.</li> <li><strong>Cased 모델</strong> (<code class="language-plaintext highlighter-rouge">bert-base-cased</code> 같은): <strong><code class="language-plaintext highlighter-rouge">XIGNCODE</code>와 <code class="language-plaintext highlighter-rouge">xigncode</code>를 서로 다른 단어로 취급한다.</strong> <code class="language-plaintext highlighter-rouge">XIGNCODE</code>를 추가하면 <em>정확히</em> 그 대소문자 형태만 매칭된다. cased 모델에서는 등장할 만한 변형들을 모두 추가해두는 편이 안전하다.</li> </ul> <h3 id="unk-vs-subword"><code class="language-plaintext highlighter-rouge">[UNK]</code> vs. subword</h3> <ul> <li><strong>애초에 왜 이걸 하나?</strong> 새 단어(예: <code class="language-plaintext highlighter-rouge">xigncode</code>)가 <code class="language-plaintext highlighter-rouge">['xig', '##nco', '##de']</code>로 쪼개지는 걸 막기 위해서다. 이렇게 쪼개지면 용어의 고유한 의미가 사라진다.</li> <li><strong><code class="language-plaintext highlighter-rouge">[UNK]</code>가 되지는 않나?</strong> 현대 모델(BERT, RoBERTa 등)에서는 <strong>거의 절대</strong> 그렇지 않다. 이 모델들은 subword 기반이고, <em>어떤</em> 미지의 단어든 구성 조각으로 쪼개도록 설계됐다. <code class="language-plaintext highlighter-rouge">[UNK]</code>(Unknown) 토큰은 대체로 단어 기반의 옛 모델 (Word2Vec 같은)의 유물이거나, 입력에 tokenizer의 문자 집합을 <em>완전히</em> 벗어난 문자가 들어 있을 때만 쓰인다.</li> </ul> <p>토큰을 추가하고 fine-tuning하는 것은 모델에게 이렇게 말하는 것이다. “이 단어를 그만 쪼개라! 하나로 취급하고, <em>이것</em> 이 그 의미다.”</p>]]></content><author><name></name></author><category term="deep-learning"/><category term="embeddings"/><category term="fine-tuning"/><category term="nlp"/><summary type="html"><![CDATA[Hugging Face sentence-transformer 모델에 새 어휘를 추가하고 fine-tuning한 뒤 LangChain에서 쓰는 단계별 가이드.]]></summary></entry><entry><title type="html">How to teach your embedding model new words</title><link href="https://yuseokkang.com/en/blog/2025/fine-tune-embedding/" rel="alternate" type="text/html" title="How to teach your embedding model new words"/><published>2025-10-25T00:00:00+00:00</published><updated>2025-10-25T00:00:00+00:00</updated><id>https://yuseokkang.com/en/blog/2025/fine-tune-embedding</id><content type="html" xml:base="https://yuseokkang.com/en/blog/2025/fine-tune-embedding/"><![CDATA[<p>Pre-trained embedding models like those on Hugging Face are incredibly powerful. They understand the meaning and context of a vast vocabulary. But what happens when you need to work with text that includes words they’ve never seen?</p> <p>Think of:</p> <ul> <li><strong>Technical Jargon:</strong> “Gecko-Embeddings”</li> <li><strong>Niche Terms:</strong> “DeepLearningGenius”</li> </ul> <p>If the model has never seen these words, it will likely tokenize them into meaningless subwords (e.g., “XIGN” + “CODE” or “xig” + “##nco” + “##de”). This <em>loses</em> the specific meaning of your term.</p> <p>The solution is to <strong>add the new words to the model’s vocabulary and fine-tune it</strong> to learn their meaning. This post will show you how, using the <code class="language-plaintext highlighter-rouge">sentence-transformers</code> library as an example.</p> <h2 id="the-two-step-process-to-add-new-words">The Two-Step Process to Add New Words</h2> <p>Adding a new word isn’t just one step. You have to update both the model’s “dictionary” (the tokenizer) and its “brain” (the model weights).</p> <ol> <li><strong>Update the Tokenizer:</strong> You first tell the tokenizer that your new word exists as a single, complete unit. This prevents it from being broken into subwords.</li> <li><strong>Update the Model &amp; Fine-Tune:</strong> After you add the word, the model creates a “slot” for it in its embedding matrix, but this new embedding vector is just random noise. It has no meaning. You <strong>must</strong> fine-tune the model on new sentences containing your word so it can learn what that word means from its context.</li> </ol> <h2 id="adding-a-new-token-the-code">Adding a New Token (The Code)</h2> <p>Let’s use <code class="language-plaintext highlighter-rouge">sentence-transformers</code> to load a model and modify its internal tokenizer and embedding matrix.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">import</span> <span class="n">torch</span>

<span class="c1"># Load your base model
</span><span class="n">model_name</span> <span class="o">=</span> <span class="sh">'</span><span class="s">sentence-transformers/all-MiniLM-L6-v2</span><span class="sh">'</span>
<span class="n">model</span> <span class="o">=</span> <span class="nc">SentenceTransformer</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>

<span class="c1"># 1. Get the internal tokenizer and model
# We need to access the underlying 'transformers' model to modify it
</span><span class="n">tokenizer</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">tokenizer</span>
<span class="n">transformer_model</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">auto_model</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Original vocabulary size: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># 2. Define your new words
# IMPORTANT: Check if your model is 'cased' or 'uncased' first!
# This model ('all-MiniLM-L6-v2') is uncased, so it lowercases everything.
# We only need to add the lowercase version.
# If using a 'cased' model, you might add: ['XIGNCODE', 'xigncode', 'Xigncode']
</span><span class="n">new_words</span> <span class="o">=</span> <span class="p">[</span><span class="sh">'</span><span class="s">xigncode</span><span class="sh">'</span><span class="p">]</span>

<span class="c1"># 3. Add new tokens to the tokenizer
</span><span class="n">num_added_toks</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">add_tokens</span><span class="p">(</span><span class="n">new_words</span><span class="p">)</span>

<span class="k">if</span> <span class="n">num_added_toks</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Added </span><span class="si">{</span><span class="n">num_added_toks</span><span class="si">}</span><span class="s"> new tokens.</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 4. Resize the model's token embeddings
</span>    <span class="c1"># This adds a new, randomly initialized vector for our new token(s)
</span>    <span class="n">transformer_model</span><span class="p">.</span><span class="nf">resize_token_embeddings</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">))</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">New vocabulary size: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Tokens already exist in the vocabulary.</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># Test the tokenizer
</span><span class="n">test_sentence</span> <span class="o">=</span> <span class="sh">"</span><span class="s">This is a test of xigncode and XIGNCODE3.</span><span class="sh">"</span>
<span class="n">tokenized</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">tokenize</span><span class="p">(</span><span class="n">test_sentence</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Tokenization test: </span><span class="si">{</span><span class="n">tokenized</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="c1"># Expected output: ['this', 'is', 'a', 'test', 'of', 'xigncode', 'and', 'xigncode', '3']
</span></code></pre></div></div> <p>In the output above, notice two things:</p> <ol> <li><code class="language-plaintext highlighter-rouge">xigncode</code> was recognized as a single token.</li> <li><code class="language-plaintext highlighter-rouge">XIGNCODE3</code> was tokenized as <code class="language-plaintext highlighter-rouge">['xigncode', '3']</code>. This is the “longest-match” rule in action and is exactly what we want!</li> </ol> <h2 id="fine-tuning-teaching-the-meaning">Fine-Tuning (Teaching the Meaning)</h2> <p>Now that our model <em>recognizes</em> “xigncode”, we need to teach it what it <em>means</em>. We do this by fine-tuning it on sentences where the word is used in context.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">torch.utils.data</span> <span class="kn">import</span> <span class="n">DataLoader</span>
<span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">InputExample</span><span class="p">,</span> <span class="n">losses</span><span class="p">,</span> <span class="n">models</span>

<span class="c1"># 1. Create training examples
# We need sentences that provide context for our new word.
</span><span class="n">train_examples</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1"># Give it context: "xigncode" is related to "anti-cheat"
</span>    <span class="nc">InputExample</span><span class="p">(</span><span class="n">texts</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">xigncode is an anti-cheat solution.</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">This software prevents cheating in online games.</span><span class="sh">'</span><span class="p">]),</span>

    <span class="c1"># Give it another context:
</span>    <span class="nc">InputExample</span><span class="p">(</span><span class="n">texts</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">Many gamers are familiar with xigncode.</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">It is a well-known security program.</span><span class="sh">'</span><span class="p">])</span>

    <span class="c1"># Add many more examples for better learning...
</span><span class="p">]</span>

<span class="c1"># 2. Setup dataloader and loss
</span><span class="n">train_dataloader</span> <span class="o">=</span> <span class="nc">DataLoader</span><span class="p">(</span><span class="n">train_examples</span><span class="p">,</span> <span class="n">shuffle</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">batch_size</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
<span class="n">train_loss</span> <span class="o">=</span> <span class="n">losses</span><span class="p">.</span><span class="nc">MultipleNegativesRankingLoss</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">)</span>

<span class="c1"># 3. Fine-tune the model
</span><span class="n">num_epochs</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">warmup_steps</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">train_dataloader</span><span class="p">)</span> <span class="o">*</span> <span class="n">num_epochs</span> <span class="o">*</span> <span class="mf">0.1</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Starting fine-tuning...</span><span class="sh">"</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span>
    <span class="n">train_objectives</span><span class="o">=</span><span class="p">[(</span><span class="n">train_dataloader</span><span class="p">,</span> <span class="n">train_loss</span><span class="p">)],</span>
    <span class="n">epochs</span><span class="o">=</span><span class="n">num_epochs</span><span class="p">,</span>
    <span class="n">warmup_steps</span><span class="o">=</span><span class="n">warmup_steps</span><span class="p">,</span>
    <span class="n">output_path</span><span class="o">=</span><span class="sh">'</span><span class="s">./my-finetuned-model</span><span class="sh">'</span><span class="p">,</span>
    <span class="n">show_progress_bar</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Fine-tuning complete. Model saved to </span><span class="sh">'</span><span class="s">./my-finetuned-model</span><span class="sh">'</span><span class="s">.</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <h2 id="using-your-new-model-with-langchain">Using Your New Model (with LangChain)</h2> <p>Your new, smarter model is now saved to the <code class="language-plaintext highlighter-rouge">./my-finetuned-model</code> directory. You can load it directly in <code class="language-plaintext highlighter-rouge">sentence-transformers</code> or, as you originally asked, in <code class="language-plaintext highlighter-rouge">LangChain</code> by simply pointing to the saved path.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">langchain_huggingface</span> <span class="kn">import</span> <span class="n">HuggingFaceEmbeddings</span>

<span class="c1"># Point to your local, fine-tuned model directory
</span><span class="n">model_path</span> <span class="o">=</span> <span class="sh">"</span><span class="s">./my-finetuned-model</span><span class="sh">"</span>

<span class="c1"># LangChain will load the model and its new tokenizer
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="nc">HuggingFaceEmbeddings</span><span class="p">(</span>
    <span class="n">model_name</span><span class="o">=</span><span class="n">model_path</span><span class="p">,</span>
    <span class="n">model_kwargs</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">device</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">cpu</span><span class="sh">'</span><span class="p">},</span> <span class="c1"># Use 'cuda' if available
</span>    <span class="n">encode_kwargs</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">normalize_embeddings</span><span class="sh">'</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>
<span class="p">)</span>

<span class="c1"># This text now generates a more accurate embedding
</span><span class="n">text</span> <span class="o">=</span> <span class="sh">"</span><span class="s">Tell me about the xigncode anti-cheat.</span><span class="sh">"</span>
<span class="n">query_embedding</span> <span class="o">=</span> <span class="n">embeddings</span><span class="p">.</span><span class="nf">embed_query</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Successfully created embedding with the new word!</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Vector dimension: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <h2 id="key-gotchas-case-sensitivity--subwords">Key Gotchas: Case Sensitivity &amp; Subwords</h2> <p>Our conversation highlighted a few critical points to remember:</p> <h3 id="xigncode-vs-xigncode-case-sensitivity"><code class="language-plaintext highlighter-rouge">XIGNCODE</code> vs. <code class="language-plaintext highlighter-rouge">xigncode</code> (Case Sensitivity)</h3> <ul> <li><strong>Uncased Models</strong> (like <code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code>): Automatically lowercase everything. You only need to add the lowercase <code class="language-plaintext highlighter-rouge">xigncode</code>. It will match <code class="language-plaintext highlighter-rouge">XIGNCODE</code>, <code class="language-plaintext highlighter-rouge">xigncode</code>, and <code class="language-plaintext highlighter-rouge">XignCode</code>.</li> <li><strong>Cased Models</strong> (like <code class="language-plaintext highlighter-rouge">bert-base-cased</code>): <strong>Treat <code class="language-plaintext highlighter-rouge">XIGNCODE</code> and <code class="language-plaintext highlighter-rouge">xigncode</code> as two different words.</strong> If you add <code class="language-plaintext highlighter-rouge">XIGNCODE</code>, it will <em>only</em> match that exact capitalization. For cased models, it’s safer to add all common variations you expect to see.</li> </ul> <h3 id="unk-vs-subwords"><code class="language-plaintext highlighter-rouge">[UNK]</code> vs. Subwords</h3> <ul> <li><strong>Why do this at all?</strong> To prevent your new word (e.g., <code class="language-plaintext highlighter-rouge">xigncode</code>) from being split into <code class="language-plaintext highlighter-rouge">['xig', '##nco', '##de']</code>. This splitting loses the unique meaning of the term.</li> <li><strong>Will it become <code class="language-plaintext highlighter-rouge">[UNK]</code>?</strong> <strong>Almost never</strong> in modern models (like BERT, RoBERTa, etc.). These models are subword-based and are designed to split <em>any</em> unknown word into its component pieces. The <code class="language-plaintext highlighter-rouge">[UNK]</code> (Unknown) token is mostly a relic of older, word-based models (like Word2Vec) or is only used if the input contains a character that is <em>completely</em> outside the tokenizer’s character set.</li> </ul> <p>By adding the token and fine-tuning, you are telling the model: “Stop splitting this word! Treat it as one thing, and <em>this</em> is what it means.”</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
</code></pre></div></div>]]></content><author><name></name></author><category term="deep-learning"/><category term="embeddings"/><category term="fine-tuning"/><category term="nlp"/><summary type="html"><![CDATA[A step-by-step guide on adding new vocabulary to a Hugging Face sentence-transformer model, including fine-tuning and using it in LangChain.]]></summary></entry></feed>