<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>min_max0315.log</title>
        <link>https://velog.io/</link>
        <description>매일 꾸준히</description>
        <lastBuildDate>Wed, 30 Sep 2026 12:50:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>min_max0315.log</title>
            <url>https://velog.velcdn.com/images/min_max0315/profile/f2e918a8-6075-4a08-adfb-8dc2fe53cee7/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. min_max0315.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/min_max0315" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TIL — Momentum Optimizer와 Kaiming Initialization]]></title>
            <link>https://velog.io/@min_max0315/TIL-Momentum-Optimizer%EC%99%80-Kaiming-Initialization</link>
            <guid>https://velog.io/@min_max0315/TIL-Momentum-Optimizer%EC%99%80-Kaiming-Initialization</guid>
            <pubDate>Wed, 30 Sep 2026 12:50:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘의 핵심<br>오늘은 어제까지 공부한 <strong>GD → SGD → Mini-batch SGD</strong> 흐름에서 이어서 <strong>Momentum</strong>을 공부했고,<br>초기화에서는 <strong>Zero / Constant / Random → Xavier → Kaiming</strong>으로 이어지는 이유를 정리했다.</p>
<p>특히 오늘은 단순히 공식을 외우기보다,</p>
<ul>
<li>왜 Momentum이 필요한지</li>
<li>Momentum을 기하학적으로 어떻게 이해해야 하는지</li>
<li>Xavier와 Kaiming의 차이가 왜 생기는지</li>
<li>ReLU의 어떤 특성이 Kaiming을 필요하게 만드는지</li>
<li>초기화의 목표가 왜 <code>Var(W)</code> 자체가 아니라 activation / gradient scale 유지인지</li>
</ul>
<p>를 연결해서 이해하는 데 집중했다.</p>
</blockquote>
<hr>
<h1 id="1-지금까지-최적화-흐름">1. 지금까지 최적화 흐름</h1>
<h2 id="📌-gradient-descent">📌 Gradient Descent</h2>
<p>Gradient Descent는 현재 Parameter에서 Loss가 가장 빠르게 증가하는 방향의 반대쪽으로 이동하는 방법이다.</p>
<p>$$
\theta_{t+1}
=
\theta_t
-
\eta \nabla_\theta L
$$</p>
<p>여기서:</p>
<ul>
<li>$\theta$ : Weight, Bias 등 학습 가능한 Parameter</li>
<li>$\eta$ : Learning Rate</li>
<li>$\nabla_\theta L$ : Loss를 Parameter로 미분한 Gradient</li>
</ul>
<p>Gradient는 단순히 &quot;얼마나 움직여라&quot;는 값이 아니라,</p>
<blockquote>
<p><strong>현재 위치에서 Loss가 어느 방향으로, 얼마나 가파르게 증가하는지</strong></p>
</blockquote>
<p>를 나타낸다.</p>
<p>Learning Rate는 그 방향으로 실제로 얼마나 이동할지 결정한다.</p>
<hr>
<h2 id="📌-batch-gradient-descent">📌 Batch Gradient Descent</h2>
<p>전체 데이터가 $N$개라면:</p>
<p>$$
L(\theta)
=
\frac{1}{N}
\sum_{i=1}^{N}
L_i(\theta)
$$</p>
<p>이고 전체 Gradient는:</p>
<p>$$
\nabla_\theta L
=
\frac{1}{N}
\sum_{i=1}^{N}
\nabla_\theta L_i
$$</p>
<p>이다.</p>
<p>즉:</p>
<pre><code class="language-text">전체 데이터
↓
평균 Loss
↓
전체 데이터 기준 Gradient
↓
Parameter 1회 Update</code></pre>
<hr>
<h2 id="📌-pure-sgd">📌 Pure SGD</h2>
<p>SGD는 데이터 하나를 사용해 Gradient를 계산하고 바로 Parameter를 업데이트한다.</p>
<p>$$
\theta_{t+1}
=
\theta_t
-
\eta\nabla_\theta L_i
$$</p>
<p>즉:</p>
<pre><code class="language-text">데이터 1개
↓
Loss
↓
Gradient
↓
바로 Update</code></pre>
<p>전체 Gradient를 정확히 계산하지 않고 하나의 Sample Gradient로 추정하므로 빠르지만 Gradient가 많이 흔들릴 수 있다.</p>
<hr>
<h2 id="📌-mini-batch-sgd">📌 Mini-batch SGD</h2>
<p>실제 딥러닝에서는 Pure SGD보다 Mini-batch를 가장 많이 사용한다.</p>
<p>Batch Size가 $B$라면:</p>
<p>$$
L_B
=
\frac{1}{B}
\sum_{i\in\mathcal B}L_i
$$</p>
<p>이고:</p>
<p>$$
\nabla_\theta L_B
=
\frac{1}{B}
\sum_{i\in\mathcal B}
\nabla_\theta L_i
$$</p>
<p>이다.</p>
<p>즉:</p>
<blockquote>
<p><strong>각 데이터의 Loss를 계산한 뒤 평균 Loss를 만들고, 그 평균 Loss를 Backward하면 결과적으로 각 데이터 Gradient의 평균을 얻는다.</strong></p>
</blockquote>
<hr>
<h1 id="2-mini-batch와-gradient-variance">2. Mini-batch와 Gradient Variance</h1>
<p>Mini-batch Gradient를:</p>
<p>$$
g_B
=
\frac{1}{B}
\sum_{i=1}^{B}g_i
$$</p>
<p>라고 하자.</p>
<p>각 Sample Gradient가 서로 어느 정도 독립이라고 단순하게 가정하면:</p>
<p>$$
Var(g_B)
\approx
\frac{Var(g_i)}{B}
$$</p>
<p>따라서:</p>
<p>$$
Var(g_B)\propto\frac{1}{B}
$$</p>
<p>이다.</p>
<p>즉:</p>
<pre><code class="language-text">Batch Size ↑
↓
더 많은 Sample Gradient를 평균
↓
개별 데이터 때문에 튀는 영향 감소
↓
Gradient Variance ↓
↓
전체 Gradient에 가까운 안정적인 추정</code></pre>
<blockquote>
<p>💡 여기서 분산은 &quot;입력 데이터 값의 분산&quot;이 아니라,<br><strong>Mini-batch를 다르게 뽑았을 때 Gradient 추정값이 얼마나 흔들리는가</strong>를 의미한다.</p>
</blockquote>
<hr>
<h1 id="3-momentum은-왜-필요한가">3. Momentum은 왜 필요한가?</h1>
<p>Mini-batch SGD는 Gradient가 noisy하다.</p>
<p>예를 들어 Loss Surface가 길고 좁은 골짜기 형태라면 SGD는:</p>
<pre><code class="language-text">↘
  ↙
    ↘
      ↙
        ↘</code></pre>
<p>처럼 가파른 방향으로 계속 지그재그 진동할 수 있다.</p>
<p>이때 자연스럽게 다음 질문이 생긴다.</p>
<blockquote>
<p><strong>현재 Gradient만 보지 말고, 이전에 이동하던 방향도 기억하면 더 안정적으로 움직일 수 있지 않을까?</strong></p>
</blockquote>
<p>이 아이디어가 Momentum이다.</p>
<hr>
<h1 id="4-momentum-수식">4. Momentum 수식</h1>
<p>가장 기본적인 Momentum 형태는:</p>
<p>$$
v_t
=
\beta v_{t-1}
+
g_t
$$</p>
<p>$$
\theta_{t+1}
=
\theta_t
-
\eta v_t
$$</p>
<p>이다.</p>
<p>여기서:</p>
<ul>
<li>$g_t$ : 현재 Step의 Gradient</li>
<li>$v_t$ : 과거 Gradient 방향이 누적된 Velocity</li>
<li>$\beta$ : 과거 방향을 얼마나 유지할지 결정하는 Momentum 계수</li>
<li>$\eta$ : Learning Rate</li>
</ul>
<p>보통 $\beta=0.9$ 정도를 많이 사용한다.</p>
<hr>
<h2 id="📌-momentum의-직관">📌 Momentum의 직관</h2>
<p>SGD:</p>
<pre><code class="language-text">현재 Gradient만 사용</code></pre>
<p>Momentum:</p>
<pre><code class="language-text">현재 Gradient
+
과거에 계속 가던 방향
↓
새로운 Update 방향</code></pre>
<p>즉 Momentum은 물리적인 <strong>관성</strong>과 비슷한 직관을 가진다.</p>
<hr>
<h1 id="5-momentum을-기하학적으로-이해하기">5. Momentum을 기하학적으로 이해하기</h1>
<p>Momentum에서 가장 중요한 기하학적 이해는 다음과 같다.</p>
<p>Loss Surface가 한쪽 방향으로는 가파르고, 다른 방향으로는 완만하다고 하자.</p>
<p>SGD는 가파른 방향의 Gradient 부호가 계속 바뀌면서:</p>
<pre><code class="language-text">왼쪽 ↔ 오른쪽
왼쪽 ↔ 오른쪽</code></pre>
<p>으로 진동할 수 있다.</p>
<p>반면 실제 Minimum을 향하는 방향의 Gradient는 여러 Step 동안 비슷한 방향을 유지할 수 있다.</p>
<p>Momentum에서는:</p>
<pre><code class="language-text">계속 방향이 바뀌는 Gradient
→ 서로 상쇄
→ Oscillation 감소

계속 같은 방향으로 나오는 Gradient
→ Velocity에 누적
→ 이동 가속</code></pre>
<p>이 발생한다.</p>
<blockquote>
<p>🎯 <strong>Momentum의 핵심은 가파른 축의 불필요한 진동은 줄이고, 일관된 진행 방향은 가속하는 것이다.</strong></p>
</blockquote>
<hr>
<h1 id="6-momentum을-전개해보면">6. Momentum을 전개해보면</h1>
<p>$$
v_t
=
\beta v_{t-1}+g_t
$$</p>
<p>를 계속 펼치면:</p>
<p>$$
v_t
=
g_t
+
\beta g_{t-1}
+
\beta^2g_{t-2}
+
\beta^3g_{t-3}
+\cdots
$$</p>
<p>가 된다.</p>
<p>즉:</p>
<pre><code class="language-text">현재 Gradient      → 가중치 1
1 Step 전 Gradient → 가중치 β
2 Step 전 Gradient → 가중치 β²
3 Step 전 Gradient → 가중치 β³</code></pre>
<p>과거 Gradient일수록 영향력이 지수적으로 감소한다.</p>
<p>따라서 Momentum은 본질적으로:</p>
<blockquote>
<p><strong>과거 Gradient의 지수적으로 감소하는 가중 누적</strong></p>
</blockquote>
<p>이라고 볼 수 있다.</p>
<hr>
<h1 id="7-momentum에서-꼭-기억할-점">7. Momentum에서 꼭 기억할 점</h1>
<h3 id="📌-momentum은-learning-rate를-크게-만드는-것이-아니다">📌 Momentum은 Learning Rate를 크게 만드는 것이 아니다</h3>
<p>Learning Rate:</p>
<p>$$
\eta
$$</p>
<p>는 <strong>Update의 전체 Scale</strong>을 조절한다.</p>
<p>Momentum:</p>
<p>$$
v_t
$$</p>
<p>는 <strong>과거 방향 정보를 이용해 Update 방향을 Smooth하게 만들고 일관된 방향을 가속</strong>한다.</p>
<p>둘의 역할은 다르다.</p>
<hr>
<h3 id="📌-momentum-핵심-5줄">📌 Momentum 핵심 5줄</h3>
<ol>
<li>Mini-batch Gradient는 noisy해서 Parameter Update가 지그재그로 움직일 수 있다.</li>
<li>Momentum은 과거 Gradient 방향을 Velocity에 누적한다.</li>
<li>방향이 계속 바뀌는 Gradient 성분은 서로 상쇄된다.</li>
<li>계속 같은 방향의 Gradient는 누적되어 가속된다.</li>
<li>즉 Momentum은 <strong>Oscillation 감소 + 일관된 방향 가속</strong>을 위한 방법이다.</li>
</ol>
<hr>
<h1 id="8-지금까지-초기화-흐름">8. 지금까지 초기화 흐름</h1>
<p>초기화에서 가장 먼저 배운 핵심은 두 가지였다.</p>
<blockquote>
<p><strong>1. Weight들이 서로 다른 값을 가져야 한다.</strong><br><strong>2. Weight의 Scale이 적절해야 한다.</strong></p>
</blockquote>
<hr>
<h2 id="📌-zero--constant-initialization">📌 Zero / Constant Initialization</h2>
<p>모든 Weight를 동일하게 초기화하면:</p>
<pre><code class="language-text">같은 Weight
↓
같은 Activation
↓
같은 Gradient
↓
같은 Update
↓
같은 Feature 학습</code></pre>
<p>이 발생할 수 있다.</p>
<p>이를 <strong>Symmetry Problem</strong>이라고 한다.</p>
<p>따라서 Hidden Layer의 Weight 전체를 같은 값으로 초기화하는 것은 좋지 않다.</p>
<hr>
<h2 id="📌-random-initialization">📌 Random Initialization</h2>
<p>Uniform / Normal Distribution에서 Weight를 랜덤하게 뽑으면 각 뉴런이 서로 다른 Weight로 시작할 수 있다.</p>
<p>즉 Symmetry는 깨진다.</p>
<p>하지만 새로운 문제가 생긴다.</p>
<blockquote>
<p><strong>Random하게 뽑기는 했는데, 얼마나 큰 값으로 뽑아야 하는가?</strong></p>
</blockquote>
<hr>
<h1 id="9-초기화에서-분산이-중요한-이유">9. 초기화에서 분산이 중요한 이유</h1>
<p>한 뉴런의 Pre-activation을:</p>
<p>$$
z
=
w_1x_1+w_2x_2+\cdots+w_nx_n
$$</p>
<p>라고 하자.</p>
<p>여기서:</p>
<ul>
<li>$x_i$ : 이전 Layer의 Activation</li>
<li>$w_i$ : Weight</li>
<li>$n$ : Input Feature 수, 즉 fan-in</li>
<li>$z$ : Activation Function 적용 전 값</li>
</ul>
<p>일반적으로:</p>
<p>$$
Var(z)
=
\sum_iVar(w_ix_i)
+
2\sum_{i&lt;j}Cov(w_ix_i,w_jx_j)
$$</p>
<p>이다.</p>
<p>초기화 이론에서는 단순화를 위해:</p>
<ul>
<li>Weight들이 독립</li>
<li>입력들이 대략 비상관</li>
<li>평균이 0에 가까움</li>
<li>Weight와 Activation이 독립</li>
</ul>
<p>등을 가정한다.</p>
<p>그러면:</p>
<p>$$
Var(z)
\approx
\sum_iVar(w_ix_i)
$$</p>
<p>이고 더 단순화하면:</p>
<p>$$
\boxed{
Var(z)
\approx
fan_{in}Var(w)Var(x)
}
$$</p>
<p>가 된다.</p>
<hr>
<h1 id="10-초기화의-진짜-목표">10. 초기화의 진짜 목표</h1>
<p>초기화의 목표는:</p>
<pre><code class="language-text">Var(W)를 최대한 작게 만들기 ❌</code></pre>
<p>가 아니다.</p>
<p>목표는:</p>
<blockquote>
<p><strong>Layer를 지나도 Activation과 Gradient의 Scale이 너무 커지거나 작아지지 않도록 유지하는 것</strong></p>
</blockquote>
<p>이다.</p>
<p>즉 대략:</p>
<p>$$
Var(z_{l+1})
\approx
Var(x_l)
$$</p>
<p>와 같은 상태를 좋은 출발점으로 만들고 싶은 것이다.</p>
<p>Weight Variance를 조절하는 것은 이 목표를 달성하기 위한 <strong>수단</strong>이다.</p>
<hr>
<h1 id="11-weight-scale과-variance">11. Weight Scale과 Variance</h1>
<p>중요한 성질:</p>
<p>$$
Var(aW)
=
a^2Var(W)
$$</p>
<p>이다.</p>
<p>따라서 Weight 전체 Scale을 절반으로 줄이면:</p>
<p>$$
Var(0.5W)
=
0.25Var(W)
$$</p>
<p>가 된다.</p>
<p>즉:</p>
<blockquote>
<p><strong>Weight 값을 동일한 비율로 작게 만들면 Weight 분포의 분산도 자연스럽게 작아진다.</strong></p>
</blockquote>
<p>그래서 fan-in이 많을수록 각 $w_ix_i$의 분산이 많이 더해지므로 Weight Scale을 줄일 필요가 있다.</p>
<hr>
<h1 id="12-xavier-initialization">12. Xavier Initialization</h1>
<p>Xavier는 Forward와 Backward에서 Signal / Gradient Scale을 동시에 고려한다.</p>
<p>Forward 관점에서:</p>
<p>$$
Var(w)
\approx
\frac{1}{fan_{in}}
$$</p>
<p>정도가 필요하다.</p>
<p>Backward 관점에서는:</p>
<p>$$
Var(w)
\approx
\frac{1}{fan_{out}}
$$</p>
<p>정도가 필요하다.</p>
<p>하지만 일반적으로:</p>
<p>$$
fan_{in}\neq fan_{out}
$$</p>
<p>이므로 두 조건을 동시에 정확히 만족하기 어렵다.</p>
<p>Xavier는 둘 사이를 절충해:</p>
<p>$$
\boxed{
Var(w)
=
\frac{2}
{fan_{in}+fan_{out}}
}
$$</p>
<p>을 사용한다.</p>
<hr>
<h2 id="📌-xavier의-의미">📌 Xavier의 의미</h2>
<pre><code class="language-text">Forward Activation Scale
        ↘
         적절한 Weight Variance
        ↗
Backward Gradient Scale</code></pre>
<p>즉:</p>
<blockquote>
<p><strong>Forward와 Backward 모두 한쪽으로 크게 망가지지 않도록 중간 지점을 잡는 초기화</strong></p>
</blockquote>
<p>라고 볼 수 있다.</p>
<hr>
<h1 id="13-xavier는-학습-내내-분산을-유지하는가">13. Xavier는 학습 내내 분산을 유지하는가?</h1>
<p>아니다.</p>
<p>Xavier는:</p>
<blockquote>
<p><strong>학습 시작 시 Activation과 Gradient Scale이 폭발하거나 소실되지 않도록 좋은 초기 조건을 만드는 방법</strong></p>
</blockquote>
<p>이다.</p>
<p>학습이 시작되면:</p>
<p>$$
W
\leftarrow
W
-
\eta\nabla_WL
$$</p>
<p>에 의해 Weight가 계속 바뀌므로 Xavier가 처음 만든 분포가 그대로 유지되지는 않는다.</p>
<p>또 독립성, 평균 0 등 여러 가정이 사용되므로 실제 네트워크에서 분산이 정확히 보존되는 것도 아니다.</p>
<hr>
<h1 id="14-kaiming-initialization은-왜-필요한가">14. Kaiming Initialization은 왜 필요한가?</h1>
<p>Xavier를 이해한 다음 ReLU를 생각하면 새로운 문제가 생긴다.</p>
<p>ReLU:</p>
<p>$$
ReLU(z)
=
\max(0,z)
$$</p>
<p>는 음수 입력을 모두 0으로 만든다.</p>
<p>$z$가 0을 중심으로 대칭적인 분포라고 가정하면:</p>
<pre><code class="language-text">z &gt; 0 → 그대로 통과
z &lt; 0 → 0</code></pre>
<p>이므로 대략 절반의 Activation이 0이 된다.</p>
<blockquote>
<p>🚨 여기서 0이 되는 것은 Weight의 절반이 아니라 <strong>Pre-activation / Activation의 음수 절반</strong>이다.</p>
</blockquote>
<hr>
<h1 id="15-relu의-중요한-특성">15. ReLU의 중요한 특성</h1>
<p>Kaiming 관점에서 ReLU의 중요한 특성은 두 가지다.</p>
<h3 id="📌-forward">📌 Forward</h3>
<p>$$
a=ReLU(z)
$$</p>
<p>에서 음수 Signal이 0이 된다.</p>
<p>0 중심 대칭 분포라는 가정 아래:</p>
<p>$$
E[ReLU(z)^2]
\approx
\frac{1}{2}E[z^2]
$$</p>
<p>즉 Signal의 Second Moment가 대략 절반으로 감소한다.</p>
<hr>
<h3 id="📌-backward">📌 Backward</h3>
<p>ReLU의 미분은:</p>
<p>$$
ReLU&#39;(z)
=
\begin{cases}
1 &amp; z&gt;0\
0 &amp; z&lt;0
\end{cases}
$$</p>
<p>이다.</p>
<p>따라서 음수 영역에서는 Gradient 역시 0이 된다.</p>
<p>즉 ReLU는:</p>
<pre><code class="language-text">Forward
→ 음수 Activation 제거

Backward
→ 해당 위치 Gradient 제거</code></pre>
<p>라는 특성을 가진다.</p>
<hr>
<h1 id="16-왜-weight를-xavier보다-크게-잡는가">16. 왜 Weight를 Xavier보다 크게 잡는가?</h1>
<p>ReLU 때문에 Forward Signal이 대략 절반 줄어든다고 생각하면:</p>
<p>$$
\text{출력 Signal Scale}
\approx
\frac12
fan_{in}Var(w)Var(x)
$$</p>
<p>이다.</p>
<p>우리는 입력과 비슷한 Scale을 유지하고 싶으므로:</p>
<p>$$
\frac12
fan_{in}Var(w)Var(x)
\approx
Var(x)
$$</p>
<p>라고 둔다.</p>
<p>$Var(x)$를 약분하면:</p>
<p>$$
\frac12fan_{in}Var(w)
\approx1
$$</p>
<p>따라서:</p>
<p>$$
\boxed{
Var(w)
\approx
\frac{2}{fan_{in}}
}
$$</p>
<p>이 나온다.</p>
<p>즉:</p>
<blockquote>
<p><strong>ReLU가 Signal 일부를 0으로 없애므로, 그 손실을 보상하기 위해 Xavier보다 Weight Scale을 더 크게 잡을 수 있다.</strong></p>
</blockquote>
<hr>
<h1 id="17-zero-centered-가정은-어디에-필요한가">17. Zero-centered 가정은 어디에 필요한가?</h1>
<p>&quot;ReLU가 대략 절반을 0으로 만든다&quot;는 말은 아무 분포에서나 성립하는 것이 아니다.</p>
<p>Pre-activation $z$가:</p>
<blockquote>
<p><strong>0을 중심으로 대략 대칭적인 분포</strong></p>
</blockquote>
<p>라고 가정해야 한다.</p>
<p>이 유도와 잘 맞도록 Weight를 보통 평균 0의 대칭적인 Normal / Uniform Distribution에서 초기화한다.</p>
<p>즉 흐름은:</p>
<pre><code class="language-text">Weight Distribution이 Zero-centered
↓
Pre-activation z도 0 주변 대칭이라고 가정
↓
z의 절반 정도가 음수
↓
ReLU가 그 절반을 0으로 만듦
↓
Signal 감소
↓
Weight Variance를 키워 보상</code></pre>
<hr>
<h1 id="18-왜-kaiming은-fan_in만-쓰는가">18. 왜 Kaiming은 fan_in만 쓰는가?</h1>
<p>여기서 내가 가장 헷갈렸던 질문:</p>
<blockquote>
<p><strong>Xavier는 fan_in과 fan_out을 같이 보는데, 왜 Kaiming은 fan_in만 쓰는가?</strong></p>
</blockquote>
<p>정확히는:</p>
<blockquote>
<p><strong>Kaiming이 fan_in만 쓸 수 있는 것은 아니다.</strong></p>
</blockquote>
<p>Forward Scale을 유지하려면:</p>
<p>$$
\boxed{
Var(w)
\approx
\frac{2}{fan_{in}}
}
$$</p>
<p>을 사용한다.</p>
<p>Backward Gradient Scale을 유지하려면:</p>
<p>$$
\boxed{
Var(w)
\approx
\frac{2}{fan_{out}}
}
$$</p>
<p>을 사용할 수 있다.</p>
<p>PyTorch에서도:</p>
<pre><code class="language-python">nn.init.kaiming_normal_(
    weight,
    mode=&quot;fan_in&quot;,
    nonlinearity=&quot;relu&quot;
)</code></pre>
<p>또는:</p>
<pre><code class="language-python">nn.init.kaiming_normal_(
    weight,
    mode=&quot;fan_out&quot;,
    nonlinearity=&quot;relu&quot;
)</code></pre>
<p>가 가능하다.</p>
<hr>
<h1 id="19-왜-둘-중-하나를-선택해야-하는가">19. 왜 둘 중 하나를 선택해야 하는가?</h1>
<p>Forward를 정확히 유지하려는 조건과 Backward를 정확히 유지하려는 조건이 일반적으로 다르기 때문이다.</p>
<p>예를 들어:</p>
<p>$$
fan_{in}=100
$$</p>
<p>$$
fan_{out}=50
$$</p>
<p>이라면:</p>
<p>Forward 기준:</p>
<p>$$
Var(w)=\frac{2}{100}=0.02
$$</p>
<p>Backward 기준:</p>
<p>$$
Var(w)=\frac{2}{50}=0.04
$$</p>
<p>이다.</p>
<p>하나의 Weight Tensor는 하나의 초기 분산만 가질 수 있으므로:</p>
<p>$$
0.02
$$</p>
<p>와</p>
<p>$$
0.04
$$</p>
<p>를 동시에 정확히 만족할 수 없다.</p>
<p>그래서:</p>
<pre><code class="language-text">fan_in
→ Forward Activation Scale 유지 우선

fan_out
→ Backward Gradient Scale 유지 우선</code></pre>
<p>중 하나를 선택할 수 있다.</p>
<hr>
<h1 id="20-xavier와-kaiming의-관점-차이">20. Xavier와 Kaiming의 관점 차이</h1>
<h3 id="📌-xavier">📌 Xavier</h3>
<p>Forward와 Backward의 요구 조건 사이에서 <strong>절충</strong>한다.</p>
<p>$$
Var(w)
=
\frac{2}
{fan_{in}+fan_{out}}
$$</p>
<h3 id="📌-kaiming">📌 Kaiming</h3>
<p>ReLU의 Signal / Gradient gating 특성을 고려한 뒤 한 방향의 Scale 보존을 우선한다.</p>
<p>대표적으로 Forward를 우선하면:</p>
<p>$$
Var(w)
=
\frac{2}{fan_{in}}
$$</p>
<p>을 사용한다.</p>
<blockquote>
<p>💡 Kaiming이 &quot;Forward만 중요해서&quot; fan_in을 쓰는 것이 아니라,<br><strong>fan_in과 fan_out 조건을 동시에 정확히 만족할 수 없기 때문에 어떤 방향을 우선할지 선택하는 것이다.</strong></p>
</blockquote>
<hr>
<h1 id="21-xavier와-kaiming은-언제-사용하는가">21. Xavier와 Kaiming은 언제 사용하는가?</h1>
<p>큰 기준으로는 다음처럼 기억하면 된다.</p>
<table>
<thead>
<tr>
<th>Activation</th>
<th>대표 초기화</th>
</tr>
</thead>
<tbody><tr>
<td>Linear</td>
<td>Xavier</td>
</tr>
<tr>
<td>tanh</td>
<td>Xavier</td>
</tr>
<tr>
<td>Sigmoid 계열</td>
<td>Xavier 계열을 고려</td>
</tr>
<tr>
<td>ReLU</td>
<td>Kaiming</td>
</tr>
<tr>
<td>LeakyReLU</td>
<td>Kaiming</td>
</tr>
</tbody></table>
<p>핵심 차이는 ReLU의 특성이다.</p>
<p>ReLU는:</p>
<pre><code class="language-text">음수 Activation → 0
음수 영역 Gradient → 0</code></pre>
<p>으로 Signal 전달 특성을 바꾸므로 이를 초기화에 반영한 것이 Kaiming이다.</p>
<blockquote>
<p>🚨 단, &quot;ReLU가 아니면 무조건 Xavier&quot;라고 외우면 안 된다.<br>GELU, SiLU 등 현대 Activation은 구조와 구현에 따라 다른 초기화 전략을 사용할 수 있다.</p>
</blockquote>
<hr>
<h1 id="22-xavier-vs-kaiming-핵심-비교">22. Xavier vs Kaiming 핵심 비교</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>Xavier</th>
<th>Kaiming</th>
</tr>
</thead>
<tbody><tr>
<td>핵심 목적</td>
<td>Forward/Backward Scale 절충</td>
<td>ReLU 특성을 반영한 Scale 유지</td>
</tr>
<tr>
<td>대표 Activation</td>
<td>Linear, tanh</td>
<td>ReLU, LeakyReLU</td>
</tr>
<tr>
<td>대표 Variance</td>
<td>$\frac{2}{fan_{in}+fan_{out}}$</td>
<td>$\frac{2}{fan_{in}}$</td>
</tr>
<tr>
<td>Forward 우선</td>
<td>절충</td>
<td><code>fan_in</code></td>
</tr>
<tr>
<td>Backward 우선</td>
<td>절충</td>
<td><code>fan_out</code> 선택 가능</td>
</tr>
<tr>
<td>핵심 아이디어</td>
<td>양방향 균형</td>
<td>ReLU가 제거하는 Signal 보상</td>
</tr>
</tbody></table>
<hr>
<h1 id="23-지금까지-전체-학습-흐름">23. 지금까지 전체 학습 흐름</h1>
<h2 id="📌-1단계--gradient">📌 1단계 — Gradient</h2>
<pre><code class="language-text">미분
↓
현재 Parameter에서 Loss의 변화율
↓
Gradient
↓
Gradient 반대 방향으로 이동</code></pre>
<p>$$
\theta
\leftarrow
\theta-\eta\nabla L
$$</p>
<hr>
<h2 id="📌-2단계--gd--sgd--mini-batch">📌 2단계 — GD / SGD / Mini-batch</h2>
<pre><code class="language-text">Batch GD
→ 전체 데이터의 Gradient

Pure SGD
→ 데이터 1개의 Gradient

Mini-batch SGD
→ 일부 데이터의 평균 Gradient</code></pre>
<p>Mini-batch는 계산 효율과 Gradient 안정성 사이의 균형을 잡는다.</p>
<hr>
<h2 id="📌-3단계--gradient-variance">📌 3단계 — Gradient Variance</h2>
<pre><code class="language-text">Batch Size ↑
↓
Gradient 평균에 포함되는 Sample 수 ↑
↓
Gradient Noise ↓
↓
Gradient Variance ↓</code></pre>
<p>$$
Var(g_B)
\propto
\frac1B
$$</p>
<hr>
<h2 id="📌-4단계--momentum">📌 4단계 — Momentum</h2>
<pre><code class="language-text">Mini-batch Gradient는 여전히 noisy
↓
현재 Gradient만 보면 지그재그 이동
↓
과거 이동 방향을 기억하자
↓
Momentum</code></pre>
<p>$$
v_t
=
\beta v_{t-1}+g_t
$$</p>
<p>$$
\theta_{t+1}
=
\theta_t-\eta v_t
$$</p>
<p>결과:</p>
<pre><code class="language-text">Oscillation 감소
+
일관된 방향 가속</code></pre>
<hr>
<h2 id="📌-5단계--초기화">📌 5단계 — 초기화</h2>
<pre><code class="language-text">Zero / Constant
↓
Symmetry 문제

Random Initialization
↓
Symmetry 해결

하지만 Weight Scale은?
↓
Activation / Gradient Scale 문제</code></pre>
<hr>
<h2 id="📌-6단계--분산-유지">📌 6단계 — 분산 유지</h2>
<p>$$
Var(z)
\approx
fan_{in}Var(w)Var(x)
$$</p>
<p>fan-in이 커질수록 여러 $w_ix_i$ 항의 분산이 누적되므로 Weight Scale을 적절하게 줄일 필요가 있다.</p>
<p>목표는:</p>
<pre><code class="language-text">Weight Variance를 작게 만드는 것 ❌

Layer 간 Activation / Gradient Scale을
안정적으로 유지하는 것 ✅</code></pre>
<p>이다.</p>
<hr>
<h2 id="📌-7단계--xavier">📌 7단계 — Xavier</h2>
<pre><code class="language-text">Forward 안정성
+
Backward 안정성
↓
둘 사이의 절충</code></pre>
<p>$$
Var(w)
=
\frac{2}
{fan_{in}+fan_{out}}
$$</p>
<hr>
<h2 id="📌-8단계--kaiming">📌 8단계 — Kaiming</h2>
<pre><code class="language-text">ReLU 사용
↓
음수 Signal / Gradient 제거
↓
Signal Scale 감소
↓
Weight Scale을 키워 보정</code></pre>
<p>Forward 기준:</p>
<p>$$
Var(w)
=
\frac{2}{fan_{in}}
$$</p>
<p>Backward 기준:</p>
<p>$$
Var(w)
=
\frac{2}{fan_{out}}
$$</p>
<hr>
<h1 id="24-오늘의-핵심-질문과-답">24. 오늘의 핵심 질문과 답</h1>
<h3 id="📌-q-momentum은-수식만-알면-되는가">📌 Q. Momentum은 수식만 알면 되는가?</h3>
<p>아니다.</p>
<p>기하학적으로:</p>
<blockquote>
<p><strong>가파른 방향의 Oscillation을 줄이고, 일관된 방향의 이동을 가속한다</strong></p>
</blockquote>
<p>는 의미를 이해해야 한다.</p>
<hr>
<h3 id="📌-q-kaiming에서-weight-절반이-0이-되는가">📌 Q. Kaiming에서 Weight 절반이 0이 되는가?</h3>
<p>아니다.</p>
<blockquote>
<p>ReLU에 의해 <strong>Pre-activation / Activation의 음수 절반 정도가 0이 된다.</strong></p>
</blockquote>
<p>Weight 자체의 절반이 0이 되는 것이 아니다.</p>
<hr>
<h3 id="📌-q-초기화의-목적은-variance를-0으로-만드는-것인가">📌 Q. 초기화의 목적은 Variance를 0으로 만드는 것인가?</h3>
<p>아니다.</p>
<blockquote>
<p><strong>Layer를 지나도 Activation / Gradient Scale이 안정적으로 유지되도록 적절한 Weight Variance를 정하는 것이 목적이다.</strong></p>
</blockquote>
<hr>
<h3 id="📌-q-xavier와-kaiming의-가장-큰-차이는">📌 Q. Xavier와 Kaiming의 가장 큰 차이는?</h3>
<p>Xavier:</p>
<blockquote>
<p><strong>Forward와 Backward Scale의 절충</strong></p>
</blockquote>
<p>Kaiming:</p>
<blockquote>
<p><strong>ReLU의 gating 특성을 고려해 Forward 또는 Backward Scale 보존을 우선</strong></p>
</blockquote>
<hr>
<h3 id="📌-q-relu에서는-왜-weight를-더-크게-잡는가">📌 Q. ReLU에서는 왜 Weight를 더 크게 잡는가?</h3>
<p>ReLU가 음수 Signal을 0으로 만들어 Signal의 Second Moment를 줄이기 때문이다.</p>
<p>이를 보정하기 위해:</p>
<p>$$
Var(w)
\approx
\frac{2}{fan_{in}}
$$</p>
<p>처럼 Weight Scale을 키운다.</p>
<hr>
<h1 id="25-최종-핵심-정리">25. 최종 핵심 정리</h1>
<blockquote>
<p><strong>Optimization에서는 Gradient를 어떻게 더 안정적이고 효율적으로 사용할지를 고민한다.</strong><br>GD → SGD → Mini-batch → Momentum으로 갈수록 계산 효율과 Gradient Noise를 다루는 방식이 발전한다.</p>
<p><strong>Initialization에서는 학습 시작 시 Signal이 어떻게 안정적으로 전달되게 만들지를 고민한다.</strong><br>Random Initialization → Xavier → Kaiming으로 갈수록 Layer 구조와 Activation Function의 특성까지 초기 Weight Scale에 반영한다.</p>
<p>결국 두 흐름 모두 같은 목표를 가진다.</p>
<p><strong>Gradient와 Activation이 지나치게 폭발하거나 사라지지 않도록 만들어, 신경망이 안정적으로 학습될 수 있게 하는 것.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL — BCE, Mini-batch Gradient, Xavier Initialization, Entropy·KL Divergence·Mutual Information]]></title>
            <link>https://velog.io/@min_max0315/TIL-BCE-Mini-batch-Gradient-Xavier-Initialization-EntropyKL-DivergenceMutual-Information</link>
            <guid>https://velog.io/@min_max0315/TIL-BCE-Mini-batch-Gradient-Xavier-Initialization-EntropyKL-DivergenceMutual-Information</guid>
            <pubDate>Tue, 29 Sep 2026 14:19:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘의 핵심<br>오늘은 단순히 개념을 외우는 수준보다, <strong>왜 이런 식이 나오는지 수식적으로 연결하는 것</strong>에 집중했다.<br>흐름은 크게 다음과 같다.</p>
<p><strong>BCE → Mini-batch Gradient → 분산 → Xavier Initialization → Entropy → Cross Entropy → KL Divergence → Mutual Information</strong></p>
</blockquote>
<hr>
<h2 id="1-binary-cross-entropy는-무엇을-보는가">1. Binary Cross Entropy는 무엇을 보는가?</h2>
<h3 id="📌-binary-classification의-출력">📌 Binary Classification의 출력</h3>
<p>Binary Classification에서 실제 정답은 보통</p>
<p>$$
y \in {0,1}
$$</p>
<p>이다.</p>
<p>예를 들어 스팸 분류라면:</p>
<ul>
<li>$y=1$ : 스팸</li>
<li>$y=0$ : 스팸 아님</li>
</ul>
<p>하지만 모델은 바로 <code>0</code> 또는 <code>1</code>을 출력하는 것이 아니라, 보통</p>
<p>$$
p = P(y=1\mid x)
$$</p>
<p>즉 <strong>입력 $x$가 class 1일 확률</strong>을 예측한다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">p = 0.8</code></pre>
<p>이면 모델은 해당 입력이 class 1일 확률을 80%로 보고 있다는 뜻이다.</p>
<p>최종 class는 필요하다면 threshold를 적용해 결정한다.</p>
<pre><code class="language-text">p &gt;= 0.5  →  class 1
p &lt; 0.5   →  class 0</code></pre>
<hr>
<h3 id="📌-bce의-목적">📌 BCE의 목적</h3>
<p>Binary Cross Entropy는</p>
<blockquote>
<p><strong>실제 정답 $y\in{0,1}$과 모델이 예측한 확률 $p\in[0,1]$이 얼마나 잘 맞는지</strong></p>
</blockquote>
<p>를 측정하는 Loss다.</p>
<p>수식은 다음과 같다.</p>
<p>$$
L
=
-\left[
y\log p
+
(1-y)\log(1-p)
\right]
$$</p>
<p>여기서:</p>
<ul>
<li>$y$ : 실제 정답</li>
<li>$p$ : 모델이 예측한 class 1의 확률</li>
</ul>
<p>정답이 1이면:</p>
<p>$$
L=-\log p
$$</p>
<p>정답이 0이면:</p>
<p>$$
L=-\log(1-p)
$$</p>
<p>따라서 모델이 정답 클래스에 높은 확률을 줄수록 Loss가 작아진다.</p>
<hr>
<h2 id="2-cross-entropy에서-bce로-내려오기">2. Cross Entropy에서 BCE로 내려오기</h2>
<p>일반적인 Cross Entropy는</p>
<p>$$
L
=
-\sum_{k=1}^{K} y_k \log p_k
$$</p>
<p>이다.</p>
<p>Binary Classification은 class가 두 개뿐이므로 $K=2$라고 생각하면 된다.</p>
<p>class 1의 확률을 $p$라고 하면 class 0의 확률은</p>
<p>$$
1-p
$$</p>
<p>이고, 정답도 하나의 binary 값 $y$로 표현하면</p>
<p>$$
y_1=y,\qquad y_0=1-y
$$</p>
<p>가 된다.</p>
<p>따라서 그대로 대입하면</p>
<p>$$
L
=
-\left[
y\log p
+
(1-y)\log(1-p)
\right]
$$</p>
<p>즉 BCE가 나온다.</p>
<blockquote>
<p>💡 <strong>BCE는 Cross Entropy를 Binary Classification에 맞게 표현한 형태라고 볼 수 있다.</strong></p>
</blockquote>
<hr>
<h2 id="3-왜-마지막-activation으로-sigmoid를-사용하는가">3. 왜 마지막 Activation으로 Sigmoid를 사용하는가?</h2>
<h3 id="📌-linear-출력은-확률이-아니다">📌 Linear 출력은 확률이 아니다</h3>
<p>마지막 Linear Layer는 보통</p>
<p>$$
z = Wx+b
$$</p>
<p>를 출력한다.</p>
<p>이때 $z$의 범위는</p>
<p>$$
-\infty &lt; z &lt; \infty
$$</p>
<p>이므로 그대로는 확률로 해석할 수 없다.</p>
<p>Sigmoid는</p>
<p>$$
\sigma(z)
=
\frac{1}{1+e^{-z}}
$$</p>
<p>이고 출력 범위는</p>
<p>$$
0 &lt; \sigma(z) &lt; 1
$$</p>
<p>이다.</p>
<p>즉:</p>
<pre><code class="language-text">Linear
↓
logit z
↓
Sigmoid
↓
class 1일 확률 p
↓
BCELoss</code></pre>
<p>의 흐름이 만들어진다.</p>
<hr>
<h3 id="📌-왜-relu는-마지막-activation으로-부적절한가">📌 왜 ReLU는 마지막 Activation으로 부적절한가?</h3>
<p>ReLU는</p>
<p>$$
ReLU(z)=\max(0,z)
$$</p>
<p>이고 출력 범위는</p>
<p>$$
[0,\infty)
$$</p>
<p>이다.</p>
<p>하지만 BCE에서 $p$는 확률이므로</p>
<p>$$
0 \le p \le 1
$$</p>
<p>이어야 한다.</p>
<p>따라서 <code>2</code>, <code>10</code> 같은 값도 출력할 수 있는 ReLU는 BCE 앞의 최종 Activation으로 부적절하다.</p>
<blockquote>
<p>🎯 <strong>BCE가 원하는 것은 class 1의 확률이고, ReLU는 확률 범위를 보장하지 않는다.</strong></p>
</blockquote>
<hr>
<h2 id="4-bceloss와-bcewithlogitsloss">4. BCELoss와 BCEWithLogitsLoss</h2>
<h3 id="📌-bceloss">📌 BCELoss</h3>
<p><code>BCELoss</code>는 이미 확률로 변환된 값을 입력으로 받는다.</p>
<pre><code class="language-python">model = nn.Sequential(
    nn.Linear(..., 1),
    nn.Sigmoid()
)

criterion = nn.BCELoss()</code></pre>
<p>즉:</p>
<pre><code class="language-text">logit
↓
Sigmoid
↓
probability
↓
BCELoss</code></pre>
<p>이다.</p>
<h3 id="📌-bcewithlogitsloss">📌 BCEWithLogitsLoss</h3>
<p>PyTorch에서는 보통 다음 형태를 더 많이 사용한다.</p>
<pre><code class="language-python">model = nn.Linear(..., 1)
criterion = nn.BCEWithLogitsLoss()</code></pre>
<p>이 경우 마지막에 <code>Sigmoid()</code>를 직접 붙이지 않는다.</p>
<p><code>BCEWithLogitsLoss</code>가 내부적으로 <strong>Sigmoid + BCE를 수치적으로 더 안정적인 방식으로 계산</strong>하기 때문이다.</p>
<blockquote>
<p>🚨 <code>BCEWithLogitsLoss</code>를 사용할 때 마지막에 Sigmoid를 중복해서 넣으면 안 된다.</p>
</blockquote>
<hr>
<h2 id="5-mini-batch-gradient-descent">5. Mini-batch Gradient Descent</h2>
<h3 id="📌-gd와-sgd-복습">📌 GD와 SGD 복습</h3>
<p>전체 데이터가 $N$개라고 하자.</p>
<p>전체 Loss는</p>
<p>$$
L(\theta)
=
\frac{1}{N}
\sum_{i=1}^{N}
L_i(\theta)
$$</p>
<p>이다.</p>
<p>여기서:</p>
<ul>
<li>$\theta$ : 모델의 모든 Parameter</li>
<li>$L_i$ : i번째 데이터의 Loss</li>
</ul>
<p>Batch Gradient Descent는 전체 데이터를 사용한다.</p>
<p>$$
g
=
\nabla_\theta L
=
\frac{1}{N}
\sum_{i=1}^{N}
\nabla_\theta L_i
$$</p>
<p>Pure SGD는 데이터 하나만 사용한다.</p>
<p>$$
g_i
=
\nabla_\theta L_i
$$</p>
<hr>
<h3 id="📌-mini-batch는-둘의-중간">📌 Mini-batch는 둘의 중간</h3>
<p>Batch Size가 $B$라면</p>
<p>$$
L_B
=
\frac{1}{B}
\sum_{i\in \mathcal{B}}
L_i
$$</p>
<p>를 계산한다.</p>
<p>그리고</p>
<p>$$
g_B
=
\nabla_\theta L_B
$$</p>
<p>를 사용한다.</p>
<p>미분은 선형이므로</p>
<p>$$
\nabla_\theta
\left(
\frac{1}{B}
\sum_{i\in\mathcal{B}}L_i
\right)
=
\frac{1}{B}
\sum_{i\in\mathcal{B}}
\nabla_\theta L_i
$$</p>
<p>이다.</p>
<blockquote>
<p><strong>각 데이터의 Gradient를 직접 하나씩 구해서 평균내는 것과, 평균 Loss를 한 번 Backward 하는 것은 같은 결과를 준다.</strong></p>
</blockquote>
<hr>
<h2 id="6-pytorch에서는-평균-loss를-통해-gradient-평균이-만들어진다">6. PyTorch에서는 평균 Loss를 통해 Gradient 평균이 만들어진다</h2>
<p>PyTorch의 많은 Loss 함수는 기본적으로</p>
<pre><code class="language-python">reduction=&quot;mean&quot;</code></pre>
<p>을 사용한다.</p>
<p>예를 들어:</p>
<pre><code class="language-python">criterion = nn.MSELoss()
loss = criterion(y_hat, y)
loss.backward()</code></pre>
<p>이면:</p>
<pre><code class="language-text">각 데이터 Loss 계산
↓
Batch 평균 Loss
↓
Backward
↓
결과적으로 Batch Gradient의 평균</code></pre>
<p>이 만들어진다.</p>
<hr>
<h2 id="7-batch-size와-gradient-variance">7. Batch Size와 Gradient Variance</h2>
<p>Mini-batch Gradient를</p>
<p>$$
g_B
=
\frac{1}{B}
\sum_{i=1}^{B}g_i
$$</p>
<p>라고 하자.</p>
<p>각 데이터의 Gradient가 서로 어느 정도 독립이라고 단순하게 가정하면</p>
<p>$$
Var(g_B)
\approx
\frac{Var(g_i)}{B}
$$</p>
<p>이므로</p>
<p>$$
Var(g_B)\propto\frac{1}{B}
$$</p>
<p>이다.</p>
<p>즉:</p>
<pre><code class="language-text">Batch Size ↑
↓
더 많은 Gradient를 평균
↓
개별 데이터 때문에 튀는 정도 감소
↓
Gradient Variance ↓</code></pre>
<blockquote>
<p>💡 여기서 분산은 <strong>Mini-batch를 다르게 뽑았을 때 Gradient 추정값이 얼마나 흔들리는가</strong>를 의미한다.</p>
</blockquote>
<hr>
<h2 id="8-mini-batch의-분산과-초기화의-분산은-다른-이야기다">8. Mini-batch의 분산과 초기화의 분산은 다른 이야기다</h2>
<p>Mini-batch에서는</p>
<p>$$
g_B
=
\frac{g_1+\cdots+g_B}{B}
$$</p>
<p>처럼 <strong>평균</strong>을 낸다.</p>
<p>반면 초기화에서는 다음 Layer의 Pre-activation이</p>
<p>$$
z
=
w_1x_1+w_2x_2+\cdots+w_nx_n
$$</p>
<p>처럼 여러 값을 <strong>합</strong>한다.</p>
<p>여기서:</p>
<ul>
<li>$x_i$ : 이전 Layer의 Activation</li>
<li>$w_i$ : Weight</li>
<li>$n$ : Input Feature 수, 즉 fan-in</li>
<li>$z$ : Activation Function을 통과하기 전 값</li>
</ul>
<hr>
<h2 id="9-분산variance이란">9. 분산(Variance)이란?</h2>
<p>분산은 값 자체가 크냐 작냐가 아니라</p>
<blockquote>
<p><strong>값들이 자신의 평균 주변에서 얼마나 퍼져 있는가</strong></p>
</blockquote>
<p>를 나타낸다.</p>
<p>$$
Var(X)
=
E[(X-E[X])^2]
$$</p>
<p>예를 들어:</p>
<pre><code class="language-text">[1000, 1000, 1000]</code></pre>
<p>은 값은 크지만 모두 같으므로 분산은 0이다.</p>
<p>반면:</p>
<pre><code class="language-text">[-1000, 0, 1000]</code></pre>
<p>은 평균 주변에서 크게 퍼져 있으므로 분산이 크다.</p>
<h3 id="📌-scale과-분산">📌 Scale과 분산</h3>
<p>중요한 성질은</p>
<p>$$
Var(aX)
=
a^2Var(X)
$$</p>
<p>이다.</p>
<p>즉 Weight 전체의 Scale을 줄이면 Weight 분포의 분산도 자연스럽게 줄어든다.</p>
<hr>
<h2 id="10-초기화에서-왜-fan-in이-커지면-weight를-작게-해야-하는가">10. 초기화에서 왜 fan-in이 커지면 Weight를 작게 해야 하는가?</h2>
<p>다음 식을 생각하자.</p>
<p>$$
z
=
\sum_{i=1}^{n}w_ix_i
$$</p>
<p>일반적으로는</p>
<p>$$
Var(z)
=
\sum_iVar(w_ix_i)
+
2\sum_{i&lt;j}Cov(w_ix_i,w_jx_j)
$$</p>
<p>이다.</p>
<p>초기화 이론에서는 단순화를 위해 다음과 같은 가정을 둔다.</p>
<ul>
<li>Weight들이 서로 독립</li>
<li>입력 Activation들도 대략 비상관</li>
<li>평균이 0에 가까움</li>
<li>$w_i$와 $x_i$가 독립</li>
</ul>
<p>그러면 Covariance 항을 무시할 수 있고</p>
<p>$$
Var(z)
\approx
\sum_iVar(w_ix_i)
$$</p>
<p>가 된다.</p>
<p>또한</p>
<p>$$
Var(w_ix_i)
\approx
Var(w_i)Var(x_i)
$$</p>
<p>라고 두면</p>
<p>$$
\boxed{
Var(z)
\approx
nVar(w)Var(x)
}
$$</p>
<p>가 된다.</p>
<p>즉:</p>
<pre><code class="language-text">fan-in n ↑
↓
더 많은 w_i x_i 항의 분산이 더해짐
↓
Var(z) ↑ 가능
↓
Weight Scale / Var(w)를 줄일 필요가 있음</code></pre>
<hr>
<h2 id="11-좋은-초기화의-핵심">11. 좋은 초기화의 핵심</h2>
<p>좋은 초기화에는 두 가지 목표가 있다.</p>
<h3 id="📌-1-symmetry-breaking">📌 1. Symmetry Breaking</h3>
<p>모든 Weight가 동일하면 뉴런들이 같은 Feature를 학습할 수 있다.</p>
<p>따라서 Weight들이 서로 다른 값을 가져야 한다.</p>
<h3 id="📌-2-적절한-scale">📌 2. 적절한 Scale</h3>
<p>Weight가 너무 크면 Activation과 Gradient가 불안정해질 수 있고, 너무 작으면 Signal이 지나치게 작아질 수 있다.</p>
<blockquote>
<p>🎯 <strong>좋은 초기화 = 서로 다른 Weight + 적절한 Weight Scale</strong></p>
</blockquote>
<hr>
<h2 id="12-xavier-initialization">12. Xavier Initialization</h2>
<h3 id="📌-forward-관점">📌 Forward 관점</h3>
<p>Forward에서</p>
<p>$$
Var(z)
\approx
fan_{in}Var(w)Var(x)
$$</p>
<p>이다.</p>
<p>Layer를 지나도 Activation의 분산을 대략 유지하고 싶다면</p>
<p>$$
Var(z)
\approx
Var(x)
$$</p>
<p>를 목표로 둘 수 있다.</p>
<p>따라서</p>
<p>$$
fan_{in}Var(w)Var(x)
\approx
Var(x)
$$</p>
<p>이고 $Var(x)\neq0$라면</p>
<p>$$
Var(w)
\approx
\frac{1}{fan_{in}}
$$</p>
<p>이 된다.</p>
<hr>
<h3 id="📌-backward-관점">📌 Backward 관점</h3>
<p>Backward에서는 대략</p>
<p>$$
Var(\delta_{prev})
\approx
fan_{out}Var(w)Var(\delta_{next})
$$</p>
<p>라고 볼 수 있다.</p>
<p>Gradient의 분산을 유지하려면</p>
<p>$$
Var(w)
\approx
\frac{1}{fan_{out}}
$$</p>
<p>이 필요하다.</p>
<p>Forward와 Backward의 요구가 다를 수 있으므로 Xavier는 둘을 절충해</p>
<p>$$
\boxed{
Var(w)
=
\frac{2}{fan_{in}+fan_{out}}
}
$$</p>
<p>를 사용한다.</p>
<hr>
<h2 id="13-xavier-normal">13. Xavier Normal</h2>
<p>Xavier Normal은</p>
<p>$$
w
\sim
\mathcal{N}
\left(
0,
\frac{2}{fan_{in}+fan_{out}}
\right)
$$</p>
<p>이다.</p>
<p>표준편차는</p>
<p>$$
std
=
\sqrt{
\frac{2}{fan_{in}+fan_{out}}
}
$$</p>
<p>이다.</p>
<p>PyTorch:</p>
<pre><code class="language-python">nn.init.xavier_normal_(model.weight)</code></pre>
<hr>
<h2 id="14-xavier-uniform">14. Xavier Uniform</h2>
<p>Uniform Distribution에서</p>
<p>$$
w\sim U(-a,a)
$$</p>
<p>이면</p>
<p>$$
Var(w)=\frac{a^2}{3}
$$</p>
<p>이다.</p>
<p>Xavier의 목표 분산과 맞추면</p>
<p>$$
\frac{a^2}{3}
=
\frac{2}{fan_{in}+fan_{out}}
$$</p>
<p>이므로</p>
<p>$$
a
=
\sqrt{
\frac{6}{fan_{in}+fan_{out}}
}
$$</p>
<p>이다.</p>
<p>즉</p>
<p>$$
w
\sim
U\left(
-\sqrt{\frac{6}{fan_{in}+fan_{out}}},
+\sqrt{\frac{6}{fan_{in}+fan_{out}}}
\right)
$$</p>
<p>이다.</p>
<p>PyTorch:</p>
<pre><code class="language-python">nn.init.xavier_uniform_(model.weight)</code></pre>
<hr>
<h2 id="15-xavier는-학습-내내-분산을-유지하는-방법인가">15. Xavier는 학습 내내 분산을 유지하는 방법인가?</h2>
<p>아니다.</p>
<p>Xavier는</p>
<blockquote>
<p><strong>학습 시작 시 Activation과 Gradient가 너무 커지거나 작아지지 않도록 안정적인 초기 조건을 만들어주는 초기화 방법</strong></p>
</blockquote>
<p>이다.</p>
<p>학습이 시작되면</p>
<p>$$
W
\leftarrow
W
-
\eta\nabla_WL
$$</p>
<p>에 의해 Weight가 계속 바뀐다.</p>
<p>따라서 Xavier가 처음 설정한 분산이 학습 내내 유지된다는 보장은 없다.</p>
<p>또 Xavier의 유도에는 독립성, 평균 0, 비슷한 분산 등의 가정이 들어가므로 실제 네트워크에서 정확히 분산이 보존되는 것도 아니다.</p>
<blockquote>
<p>💡 Xavier의 목적은 <strong>학습 가능한 좋은 출발점</strong>을 만드는 것이다.</p>
</blockquote>
<hr>
<h1 id="16-entropy--정보-하나의-정보량">16. Entropy — 정보 하나의 정보량</h1>
<p>어떤 사건 $x$가 발생할 확률이 $p(x)$라고 하자.</p>
<p>그 사건의 정보량(Self-information)은</p>
<p>$$
I(x)
=
-\log_2p(x)
$$</p>
<p>로 정의한다.</p>
<p>확률이 높은 사건은 자주 발생하므로 정보량이 작고, 확률이 낮은 사건은 드물기 때문에 정보량이 크다.</p>
<p>예를 들어:</p>
<p>$$
p(x)=\frac12
$$</p>
<p>이면</p>
<p>$$
I(x)
=
-\log_2\frac12
=
1\text{ bit}
$$</p>
<p>이다.</p>
<p>또</p>
<p>$$
p(x)=\frac18
$$</p>
<p>이면</p>
<p>$$
I(x)
=
3\text{ bits}
$$</p>
<p>이다.</p>
<hr>
<h2 id="17-왜-로그-밑이-2인가">17. 왜 로그 밑이 2인가?</h2>
<p>Binary Code 관점에서 정보를 표현한다면 밑이 2인 로그를 사용한다.</p>
<p>$$
I(x)
=
-\log_2p(x)
$$</p>
<p>이 경우 단위는 bit이다.</p>
<p>로그 밑을 $e$로 사용하면 단위는 nat이다.</p>
<blockquote>
<p>💡 로그 밑이 2인 이유는 정보가 반드시 이진이라서가 아니라, <strong>이진 부호화 관점에서 정보량의 단위를 bit로 표현하기 위해서</strong>다.</p>
</blockquote>
<hr>
<h2 id="18-entropy">18. Entropy</h2>
<p>Entropy는 각 사건의 정보량을 확률적으로 평균낸 값이다.</p>
<p>$$
\boxed{
H(X)
=
-\sum_xp(x)\log_2p(x)
}
$$</p>
<p>또는 기대값으로</p>
<p>$$
H(X)
=
E_{x\sim P}
[-\log_2p(x)]
$$</p>
<p>라고 쓸 수 있다.</p>
<p>즉:</p>
<blockquote>
<p><strong>Entropy는 실제 확률분포 $P$를 알고 있을 때, 그 분포에서 발생하는 데이터를 표현하는 데 필요한 평균 정보량이다.</strong></p>
</blockquote>
<p>정보이론적으로는 긴 symbol sequence를 이상적으로 부호화할 때 얻을 수 있는 <strong>최적 평균 부호 길이의 이론적 하한과 연결되는 값</strong>이다.</p>
<blockquote>
<p>🚨 Entropy와 정확히 같은 길이의 단일 symbol code가 항상 존재한다는 뜻은 아니다. 긴 sequence를 효율적으로 부호화할수록 평균 code length를 Entropy에 가깝게 만들 수 있다.</p>
</blockquote>
<hr>
<h2 id="19-cross-entropy">19. Cross Entropy</h2>
<p>실제 데이터 분포를 $P$라고 하고, 우리가 모델링한 예측 분포를 $Q$라고 하자.</p>
<p>Cross Entropy는</p>
<p>$$
\boxed{
H(P,Q)
=
-\sum_xp(x)\log_2q(x)
}
$$</p>
<p>또는</p>
<p>$$
H(P,Q)
=
E_{x\sim P}
[-\log_2q(x)]
$$</p>
<p>이다.</p>
<p>중요한 점은:</p>
<ul>
<li>실제 데이터는 $P$에서 발생</li>
<li>정보량 계산에는 $Q$를 사용</li>
</ul>
<p>한다는 것이다.</p>
<p>즉:</p>
<blockquote>
<p><strong>실제 데이터가 $P$를 따르는데, 우리가 $Q$라는 확률 모델을 사용해 데이터를 표현할 때 필요한 기대 정보량</strong></p>
</blockquote>
<p>이라고 볼 수 있다.</p>
<hr>
<h2 id="20-entropy와-cross-entropy의-차이">20. Entropy와 Cross Entropy의 차이</h2>
<p>Entropy:</p>
<p>$$
H(P)
=
-\sum_xp(x)\log_2p(x)
$$</p>
<p>Cross Entropy:</p>
<p>$$
H(P,Q)
=
-\sum_xp(x)\log_2q(x)
$$</p>
<p>즉:</p>
<pre><code class="language-text">Entropy       : 실제 분포 P를 사용
Cross Entropy : 실제 데이터는 P지만 예측 분포 Q를 사용</code></pre>
<p>모델이 실제 분포를 정확히 맞히면</p>
<p>$$
Q=P
$$</p>
<p>이므로</p>
<p>$$
H(P,Q)=H(P)
$$</p>
<p>가 된다.</p>
<hr>
<h2 id="21-kl-divergence">21. KL Divergence</h2>
<p>Cross Entropy와 Entropy의 차이가 KL Divergence다.</p>
<p>$$
\boxed{
D_{KL}(P\parallel Q)
=
H(P,Q)-H(P)
}
$$</p>
<p>이를 풀어쓰면</p>
<p>$$
D_{KL}(P\parallel Q)
=
\sum_x
p(x)
\log_2
\frac{p(x)}{q(x)}
$$</p>
<p>이다.</p>
<h3 id="📌-의미">📌 의미</h3>
<p>Entropy $H(P)$는 실제 분포를 알고 있을 때의 최적 기대 정보량이고,
Cross Entropy $H(P,Q)$는 $Q$를 사용했을 때의 기대 정보량이다.</p>
<p>따라서</p>
<p>$$
H(P,Q)-H(P)
$$</p>
<p>는</p>
<blockquote>
<p><strong>잘못되거나 불완전한 분포 $Q$를 사용해서 생기는 추가적인 기대 정보 비용</strong></p>
</blockquote>
<p>으로 해석할 수 있다.</p>
<p>즉:</p>
<pre><code class="language-text">최적 기대 비용 H(P)
+
분포를 잘못 가정해서 생긴 추가 비용 D_KL(P||Q)
=
실제 기대 비용 H(P,Q)</code></pre>
<p>이다.</p>
<hr>
<h3 id="📌-중요한-성질">📌 중요한 성질</h3>
<p>$$
D_{KL}(P\parallel Q)\ge0
$$</p>
<p>이고, 두 분포가 같으면</p>
<p>$$
D_{KL}(P\parallel Q)=0
$$</p>
<p>이다.</p>
<p>하지만 일반적인 거리함수와 달리</p>
<p>$$
D_{KL}(P\parallel Q)
\neq
D_{KL}(Q\parallel P)
$$</p>
<p>일 수 있다.</p>
<p>즉 KL Divergence는 대칭적이지 않다.</p>
<hr>
<h2 id="22-cross-entropy-loss와-kl-divergence의-관계">22. Cross Entropy Loss와 KL Divergence의 관계</h2>
<p>실제 데이터 분포 $P$가 고정되어 있다면</p>
<p>$$
H(P)
$$</p>
<p>는 모델 Parameter와 무관한 상수다.</p>
<p>그리고</p>
<p>$$
H(P,Q)
=
H(P)
+
D_{KL}(P\parallel Q)
$$</p>
<p>이므로 Cross Entropy를 최소화하는 것은 결국</p>
<p>$$
D_{KL}(P\parallel Q)
$$</p>
<p>를 최소화하는 것과 같다.</p>
<p>즉:</p>
<blockquote>
<p><strong>Cross Entropy Loss를 줄인다는 것은 모델의 예측 분포 $Q$를 실제 데이터 분포 $P$에 가깝게 만드는 것이라고 볼 수 있다.</strong></p>
</blockquote>
<hr>
<h1 id="23-mutual-information">23. Mutual Information</h1>
<p>이제 KL Divergence를 이용해 두 변수 $X$, $Y$가 얼마나 서로 의존하는지를 측정할 수 있다.</p>
<p>두 변수가 독립이라면</p>
<p>$$
P(X,Y)
=
P(X)P(Y)
$$</p>
<p>이다.</p>
<p>따라서 다음 두 분포를 비교하면 된다.</p>
<p>실제 Joint Distribution:</p>
<p>$$
P_{XY}(x,y)
$$</p>
<p>독립이라고 가정했을 때의 분포:</p>
<p>$$
P_X(x)P_Y(y)
$$</p>
<p>이 둘의 차이를 KL Divergence로 측정한 것이 Mutual Information이다.</p>
<p>$$
\boxed{
I(X;Y)
=
D_{KL}
\left(
P_{XY}
\parallel
P_XP_Y
\right)
}
$$</p>
<hr>
<h2 id="24-mutual-information의-수식">24. Mutual Information의 수식</h2>
<p>풀어쓰면</p>
<p>$$
\boxed{
I(X;Y)
=
\sum_{x,y}
p(x,y)
\log
\frac{p(x,y)}{p(x)p(y)}
}
$$</p>
<p>이다.</p>
<h3 id="📌-독립이면">📌 독립이면?</h3>
<p>독립이라면</p>
<p>$$
p(x,y)=p(x)p(y)
$$</p>
<p>이므로</p>
<p>$$
\frac{p(x,y)}{p(x)p(y)}=1
$$</p>
<p>이고</p>
<p>$$
\log1=0
$$</p>
<p>이다.</p>
<p>따라서</p>
<p>$$
I(X;Y)=0
$$</p>
<p>이다.</p>
<p>즉:</p>
<blockquote>
<p><strong>Mutual Information이 0이면 두 변수는 독립이다.</strong></p>
</blockquote>
<hr>
<h3 id="📌-의존성이-강하면">📌 의존성이 강하면?</h3>
<p>$X$를 알았을 때 $Y$에 대한 불확실성이 크게 줄어든다면 Mutual Information이 커진다.</p>
<p>따라서:</p>
<blockquote>
<p><strong>Mutual Information은 $X$와 $Y$가 서로 공유하는 정보의 양</strong></p>
</blockquote>
<p>이라고 해석할 수 있다.</p>
<hr>
<h2 id="25-mutual-information과-entropy의-관계">25. Mutual Information과 Entropy의 관계</h2>
<p>Mutual Information은 다음과 같이도 쓸 수 있다.</p>
<p>$$
\boxed{
I(X;Y)
=
H(X)+H(Y)-H(X,Y)
}
$$</p>
<p>또는</p>
<p>$$
\boxed{
I(X;Y)
=
H(X)-H(X\mid Y)
}
$$</p>
<p>이며</p>
<p>$$
I(X;Y)
=
H(Y)-H(Y\mid X)
$$</p>
<p>이기도 하다.</p>
<p>여기서 $H(X)$는 원래 $X$에 대한 불확실성이고,
$H(X\mid Y)$는 $Y$를 알고 난 뒤에도 남아 있는 $X$의 불확실성이다.</p>
<p>따라서</p>
<p>$$
H(X)-H(X\mid Y)
$$</p>
<p>는</p>
<blockquote>
<p><strong>Y를 알게 됨으로써 X에 대한 불확실성이 얼마나 감소했는가</strong></p>
</blockquote>
<p>를 나타낸다.</p>
<hr>
<h1 id="26-전체-정보이론-흐름">26. 전체 정보이론 흐름</h1>
<p>오늘 배운 정보이론 내용은 다음처럼 하나의 흐름으로 연결할 수 있다.</p>
<pre><code class="language-text">어떤 사건이 얼마나 놀라운가?
↓
Self-Information

I(x) = -log p(x)

↓
그 정보량을 평균내면?

Entropy

H(P) = E[-log P(x)]

↓
실제 분포 P가 아니라
예측 분포 Q를 사용하면?

Cross Entropy

H(P,Q) = E_P[-log Q(x)]

↓
최적 기대 비용과의 차이는?

KL Divergence

D_KL(P||Q) = H(P,Q) - H(P)

↓
두 변수가 독립이라고 가정한 분포와
실제 Joint Distribution의 차이는?

Mutual Information

I(X;Y) = D_KL(P_XY || P_X P_Y)</code></pre>
<hr>
<h1 id="27-오늘의-최종-연결">27. 오늘의 최종 연결</h1>
<h3 id="📌-optimization">📌 Optimization</h3>
<pre><code class="language-text">GD
→ 전체 데이터 Gradient

SGD
→ 데이터 하나의 Gradient

Mini-batch SGD
→ 일부 데이터의 평균 Gradient</code></pre>
<p>Batch Size가 증가하면</p>
<p>$$
Var(g_B)\propto\frac{1}{B}
$$</p>
<p>이므로 Gradient 추정이 더 안정적이 된다.</p>
<hr>
<h3 id="📌-initialization">📌 Initialization</h3>
<p>다음 Layer의 Pre-activation은</p>
<p>$$
z
=
\sum_iw_ix_i
$$</p>
<p>이고, 독립성 등의 단순한 가정 아래</p>
<p>$$
Var(z)
\approx
fan_{in}Var(w)Var(x)
$$</p>
<p>이다.</p>
<p>따라서 fan-in이 증가하면 Weight 분산을 작게 조절할 필요가 있다.</p>
<p>Xavier는 Forward / Backward를 함께 고려해</p>
<p>$$
\boxed{
Var(w)
=
\frac{2}{fan_{in}+fan_{out}}
}
$$</p>
<p>을 사용한다.</p>
<hr>
<h3 id="📌-information-theory">📌 Information Theory</h3>
<p>Self-Information:</p>
<p>$$
I(x)=-\log_2p(x)
$$</p>
<p>Entropy:</p>
<p>$$
H(P)
=
E_P[-\log_2P(x)]
$$</p>
<p>Cross Entropy:</p>
<p>$$
H(P,Q)
=
E_P[-\log_2Q(x)]
$$</p>
<p>KL Divergence:</p>
<p>$$
D_{KL}(P\parallel Q)
=
H(P,Q)-H(P)
$$</p>
<p>Mutual Information:</p>
<p>$$
I(X;Y)
=
D_{KL}
\left(
P_{XY}
\parallel
P_XP_Y
\right)
$$</p>
<p>즉:</p>
<blockquote>
<p><strong>Entropy는 불확실성 / 평균 정보량을, Cross Entropy는 다른 확률모델을 사용했을 때의 기대 비용을, KL Divergence는 그 추가 비용을, Mutual Information은 두 변수가 독립에서 얼마나 벗어나 서로 정보를 공유하는지를 나타낸다.</strong></p>
</blockquote>
<hr>
<h1 id="28-오늘의-핵심-한-문장">28. 오늘의 핵심 한 문장</h1>
<blockquote>
<p><strong>딥러닝의 Loss, Optimization, Initialization은 서로 떨어진 개념이 아니라 확률·미분·분산이라는 수학적 구조로 연결되어 있으며, 정보이론에서는 Entropy → Cross Entropy → KL Divergence → Mutual Information으로 이어지는 하나의 흐름으로 이해할 수 있다.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL — Gradient Descent, SGD 그리고 Weight Initialization]]></title>
            <link>https://velog.io/@min_max0315/TIL-Gradient-Descent-SGD-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Weight-Initialization</link>
            <guid>https://velog.io/@min_max0315/TIL-Gradient-Descent-SGD-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Weight-Initialization</guid>
            <pubDate>Mon, 28 Sep 2026 12:46:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘의 핵심<br><strong>Gradient Descent가 Parameter를 어떻게 업데이트하는지 이해하고, 초기 Weight를 왜 적절하게 설정해야 하는지 학습했다.</strong></p>
</blockquote>
<hr>
<h2 id="1-gradient란">1. Gradient란?</h2>
<h3 id="📌-gradient의-의미">📌 Gradient의 의미</h3>
<p>함수</p>
<p>$$
f(x)=x^2
$$</p>
<p>를 미분하면</p>
<p>$$
f&#39;(x)=2x
$$</p>
<p>이고 $x=3$이라면</p>
<p>$$
f&#39;(3)=6
$$</p>
<p>이다.</p>
<p>여기서 <code>6</code>은 <strong>x를 1 증가시키면 y가 정확히 6 증가한다는 의미가 아니다.</strong></p>
<blockquote>
<p>💡 <strong>현재 $x=3$ 근처에서 x가 아주 조금 변했을 때, 함수값은 그 변화량의 약 6배만큼 변한다는 의미다.</strong></p>
</blockquote>
<p>즉,</p>
<p>$$
\Delta f \approx f&#39;(x)\Delta x
$$</p>
<p>이므로 $x=3$에서</p>
<p>$$
\Delta f \approx 6\Delta x
$$</p>
<p>이다.</p>
<p>예를 들어 $x$가 <code>0.01</code> 변하면</p>
<p>$$
\Delta f \approx 6\times0.01=0.06
$$</p>
<p>정도 변한다.</p>
<hr>
<h3 id="📌-gradient가-크다는-것은">📌 Gradient가 크다는 것은?</h3>
<p>Gradient의 절댓값이 크다는 것은</p>
<blockquote>
<p><strong>현재 Parameter가 조금만 변해도 Loss가 크게 변할 수 있다는 뜻이다.</strong></p>
</blockquote>
<p>즉 현재 위치의 경사가 가파르다는 의미다.</p>
<p>🚨 하지만</p>
<pre><code class="language-text">Gradient가 크다
≠
최솟값에서 멀리 떨어져 있다</code></pre>
<p>이다.</p>
<p>Gradient는 어디까지나 <strong>현재 위치에서의 순간적인 변화율</strong>이다.</p>
<hr>
<h2 id="2-gradient-descent">2. Gradient Descent</h2>
<p>우리는 Loss를 최소화하고 싶다.</p>
<p>Gradient는 함수가 <strong>가장 빠르게 증가하는 방향</strong>을 알려주므로, 반대 방향으로 이동하면 된다.</p>
<p>$$
\theta_{new}
=
\theta_{old}
-
\eta\nabla_\theta L
$$</p>
<ul>
<li>$\theta$: Weight, Bias 등의 Parameter</li>
<li>$L$: Loss</li>
<li>$\nabla_\theta L$: Parameter에 대한 Gradient</li>
<li>$\eta$: Learning Rate</li>
</ul>
<h3 id="📌-gradient와-learning-rate의-역할">📌 Gradient와 Learning Rate의 역할</h3>
<p>Gradient는</p>
<blockquote>
<p><strong>어느 방향으로 이동해야 하는지 + 현재 경사가 얼마나 가파른지</strong></p>
</blockquote>
<p>를 알려준다.</p>
<p>Learning Rate는</p>
<blockquote>
<p><strong>그 방향으로 실제로 얼마나 이동할지</strong></p>
</blockquote>
<p>결정한다.</p>
<p>즉,</p>
<p>$$
\Delta\theta
=
-\eta\nabla_\theta L
$$</p>
<p>이다.</p>
<hr>
<h2 id="3-learning-rate가-너무-크면">3. Learning Rate가 너무 크면?</h2>
<p>예를 들어 Gradient가 <code>6</code>이고 Learning Rate가 <code>0.1</code>이라면</p>
<p>$$
\Delta x=-0.1\times6=-0.6
$$</p>
<p>현재 위치가 $x=3$이라면</p>
<p>$$
x_{new}=3-0.6=2.4
$$</p>
<p>가 된다.</p>
<p>Gradient가 크고 LR까지 크면:</p>
<pre><code class="language-text">큰 Gradient
    ×
큰 Learning Rate
    ↓
Parameter가 크게 이동
    ↓
Minimum을 지나침
    ↓
Loss 증가
    ↓
더 큰 Gradient
    ↓
발산</code></pre>
<p>이 발생할 수 있다.</p>
<hr>
<h2 id="4-입력-데이터의-scale과-gradient">4. 입력 데이터의 Scale과 Gradient</h2>
<p>선형회귀를 생각하면</p>
<p>$$
\hat y_i = wx_i+b
$$</p>
<p>MSE는</p>
<p>$$
L=
\frac1N
\sum_{i=1}^{N}
(\hat y_i-y_i)^2
$$</p>
<p>이고 Weight에 대한 Gradient는</p>
<p>$$
\frac{\partial L}{\partial w}
=
\frac{2}{N}
\sum_i
x_i(\hat y_i-y_i)
$$</p>
<p>이다.</p>
<p>여기서</p>
<ul>
<li>$x_i$: i번째 입력</li>
<li>$y_i$: 실제 정답</li>
<li>$\hat y_i$: 모델 예측값</li>
<li>$w$: Weight</li>
</ul>
<p>즉 Gradient 안에 <strong>입력 $x_i$가 직접 곱해진다.</strong></p>
<p>키 데이터가 <code>155 ~ 190</code>처럼 크고 예측 오차도 크면 Gradient 역시 커질 수 있다.</p>
<h3 id="📌-그래서-scaling을-사용한다">📌 그래서 Scaling을 사용한다</h3>
<p>Standardization:</p>
<p>$$
x&#39;=\frac{x-\mu}{\sigma}
$$</p>
<p>등을 사용하면 입력값의 Scale을 조절할 수 있다.</p>
<pre><code class="language-text">입력 Scale 안정화
↓
Gradient Scale 안정화
↓
Parameter Update 안정화
↓
학습이 쉬워짐</code></pre>
<blockquote>
<p>🚨 정규화를 하지 않으면 무조건 발산하는 것은 아니다.<br>작은 Learning Rate를 사용하면 학습할 수도 있지만, Scaling을 하면 최적화가 훨씬 쉬워지는 경우가 많다.</p>
</blockquote>
<hr>
<h1 id="5-batch-gradient-descent">5. Batch Gradient Descent</h1>
<p>Parameter가 $w,b$이고 데이터가 철수, 영희, 윤수 3개 있다고 하자.</p>
<p>각 데이터에서:</p>
<pre><code class="language-text">철수 → dw철수, db철수
영희 → dw영희, db영희
윤수 → dw윤수, db윤수</code></pre>
<p>가 계산된다.</p>
<p>GD에서는 Parameter별로 전체 데이터의 Gradient를 평균낸다.</p>
<p>$$
\frac{\partial L}{\partial w}
=
\frac{
dw_{\text{철수}}
+
dw_{\text{영희}}
+
dw_{\text{윤수}}
}{3}
$$</p>
<p>$$
\frac{\partial L}{\partial b}
=
\frac{
db_{\text{철수}}
+
db_{\text{영희}}
+
db_{\text{윤수}}
}{3}
$$</p>
<p>그리고 한 번 업데이트한다.</p>
<blockquote>
<p>💡 <strong>Parameter끼리 Gradient를 평균내는 것이 아니다.</strong><br>각 Parameter가 자신의 Gradient를 가지며, 그 Parameter에 대한 여러 데이터의 Gradient를 평균내는 것이다.</p>
</blockquote>
<hr>
<h1 id="6-mnist에서-gradient-descent">6. MNIST에서 Gradient Descent</h1>
<p>MNIST 학습 데이터가 60,000장이고</p>
<pre><code class="language-python">model = nn.Linear(784, 10)</code></pre>
<p>이라면 Parameter는</p>
<pre><code class="language-text">Weight = 784 × 10 = 7,840
Bias   = 10

총 7,850개</code></pre>
<p>이다.</p>
<p>Batch GD라면:</p>
<pre><code class="language-text">MNIST 60,000장
↓
Forward
↓
Loss 계산
↓
60,000장 전체를 기준으로
7,850개 Parameter의 Gradient 계산
↓
Parameter 전체 Update 1회</code></pre>
<p>즉 <strong>한 Epoch에 Update가 한 번</strong> 일어난다.</p>
<hr>
<h1 id="7-sgd">7. SGD</h1>
<p>SGD는 전체 데이터를 기다리지 않고 <strong>데이터 하나를 사용해 Gradient를 계산하고 바로 Parameter를 업데이트한다.</strong></p>
<p>$$
\frac{\partial L}{\partial w}
=
\frac{\partial L_i}{\partial w}
$$</p>
<p>여기서 $L_i$는 i번째 데이터 하나에 대한 Loss다.</p>
<p>MNIST라면:</p>
<pre><code class="language-text">1번째 이미지 → Gradient → Update
2번째 이미지 → Gradient → Update
3번째 이미지 → Gradient → Update
...
60,000번째 이미지 → Gradient → Update</code></pre>
<p>따라서 순수 SGD라면:</p>
<p>$$
1\ Epoch = 60,000\ Updates
$$</p>
<p>이다.</p>
<h3 id="📌-왜-사용하는가">📌 왜 사용하는가?</h3>
<p>Batch GD는 정확한 전체 Gradient를 얻지만, 업데이트 한 번을 위해 모든 데이터를 계산해야 한다.</p>
<p>SGD는 데이터 일부로 전체 Gradient를 <strong>확률적으로 추정</strong>한다.</p>
<p>그래서 계산 하나당 훨씬 자주 Parameter를 업데이트할 수 있다.</p>
<hr>
<h1 id="8-mini-batch-sgd">8. Mini-batch SGD</h1>
<p>실제 딥러닝에서는 순수 SGD보다 <strong>Mini-batch SGD</strong>를 훨씬 많이 사용한다.</p>
<p>예를 들어:</p>
<pre><code class="language-python">DataLoader(
    dataset,
    batch_size=100,
    shuffle=True
)</code></pre>
<p>라면 100개의 데이터에 대해</p>
<p>$$
g_B
=
\frac1{100}
\sum_{i\in B}g_i
$$</p>
<p>를 계산하고 Parameter를 한 번 업데이트한다.</p>
<p>MNIST 60,000장이라면:</p>
<p>$$
\frac{60000}{100}=600
$$</p>
<p>이므로 한 Epoch에 600번 Update한다.</p>
<table>
<thead>
<tr>
<th>방법</th>
<th align="right">한 번에 사용하는 데이터</th>
<th align="right">60,000개 기준 Update</th>
</tr>
</thead>
<tbody><tr>
<td>Batch GD</td>
<td align="right">60,000</td>
<td align="right">1</td>
</tr>
<tr>
<td>Mini-batch SGD</td>
<td align="right">100</td>
<td align="right">600</td>
</tr>
<tr>
<td>Pure SGD</td>
<td align="right">1</td>
<td align="right">60,000</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-batch-size는-클수록-좋은가">9. Batch Size는 클수록 좋은가?</h2>
<p>Batch Size가 커지면 일반적으로 여러 데이터의 Gradient가 평균되므로</p>
<p>$$
Batch\ Size\uparrow
\Rightarrow
Gradient\ Variance\downarrow
$$</p>
<p>한다.</p>
<p>즉 전체 데이터 Gradient에 더 가까운 <strong>안정적인 Gradient</strong>를 얻을 수 있다.</p>
<p>하지만 무조건 크게 하는 것도 좋은 것은 아니다.</p>
<pre><code class="language-text">작은 Batch
→ Gradient가 noisy
→ Update를 자주 함

큰 Batch
→ Gradient가 안정적
→ Update 횟수가 줄어듦

Full Batch
→ 가장 정확한 전체 Gradient
→ Epoch당 Update 1회</code></pre>
<blockquote>
<p>🎯 <strong>Batch Size는 Gradient의 안정성, Update 빈도, GPU 효율 사이의 Trade-off다.</strong></p>
</blockquote>
<hr>
<h1 id="10-weight-initialization이-필요한-이유">10. Weight Initialization이 필요한 이유</h1>
<p>신경망을 학습하기 전에 Weight의 시작값을 결정해야 한다.</p>
<p>좋은 초기화에서 중요한 것은 크게 두 가지다.</p>
<h3 id="📌-1-weight들이-서로-다른-값을-가져야-한다">📌 1. Weight들이 서로 다른 값을 가져야 한다</h3>
<p>그래야 각 뉴런이 서로 다른 Feature를 학습할 수 있다.</p>
<h3 id="📌-2-weight가-적절한-크기를-가져야-한다">📌 2. Weight가 적절한 크기를 가져야 한다</h3>
<p>Weight가 너무 크거나 너무 작으면 Layer를 지나면서 Activation과 Gradient의 Scale이 불안정해질 수 있다.</p>
<blockquote>
<p>🎯 <strong>좋은 초기화 = Symmetry를 깨면서 Activation과 Gradient의 Scale을 안정적으로 유지하는 것</strong></p>
</blockquote>
<hr>
<h1 id="11-zero-initialization">11. Zero Initialization</h1>
<p>모든 Weight를 0으로 초기화한다.</p>
<p>$$
W_{ij}=0
$$</p>
<p>PyTorch:</p>
<pre><code class="language-python">nn.init.zeros_(model.weight)</code></pre>
<p>문제는 모든 뉴런이 동일하게 시작한다는 것이다.</p>
<pre><code class="language-text">같은 Weight
↓
같은 출력
↓
같은 Gradient
↓
같은 Update
↓
계속 같은 Weight</code></pre>
<p>이를 <strong>Symmetry Problem</strong>이라고 한다.</p>
<p>결국 여러 뉴런을 만들어도 비슷한 Feature만 학습하게 된다.</p>
<h3 id="📌-bias는">📌 Bias는?</h3>
<p>Bias는 0으로 초기화하는 경우가 흔하다.</p>
<pre><code class="language-python">nn.init.zeros_(model.bias)</code></pre>
<p>따라서</p>
<blockquote>
<p><strong>Deep Network의 Weight 전체를 Zero Initialization하는 것이 문제이지, 모든 Parameter를 절대 0으로 만들면 안 된다는 뜻은 아니다.</strong></p>
</blockquote>
<hr>
<h1 id="12-constant-initialization">12. Constant Initialization</h1>
<p>모든 Weight를 같은 특정 값으로 초기화한다.</p>
<p>$$
W_{ij}=c
$$</p>
<p>예:</p>
<pre><code class="language-python">nn.init.constant_(model.weight, 0.5)</code></pre>
<p>Zero가 아니므로 괜찮아 보이지만 여전히 문제가 있다.</p>
<pre><code class="language-text">Neuron 1 = [0.5, 0.5, 0.5]
Neuron 2 = [0.5, 0.5, 0.5]</code></pre>
<p>두 뉴런이 똑같은 상태로 시작한다.</p>
<p>따라서:</p>
<pre><code class="language-text">같은 출력
→ 같은 Gradient
→ 같은 Update</code></pre>
<p>가 반복된다.</p>
<blockquote>
<p>💡 문제는 <code>0</code>이라는 숫자가 아니라 <strong>뉴런들의 Weight가 동일하다는 것</strong>이다.</p>
</blockquote>
<hr>
<h1 id="13-uniform-initialization">13. Uniform Initialization</h1>
<p>Weight를 Uniform Distribution에서 랜덤하게 뽑는다.</p>
<p>$$
W_{ij}\sim U(a,b)
$$</p>
<p>예:</p>
<p>$$
W_{ij}\sim U(-0.1,0.1)
$$</p>
<p>PyTorch:</p>
<pre><code class="language-python">nn.init.uniform_(
    model.weight,
    a=-0.1,
    b=0.1
)</code></pre>
<p>각 Weight가 서로 다른 값을 가지므로 Symmetry가 깨진다.</p>
<pre><code class="language-text">서로 다른 Weight
↓
서로 다른 Activation
↓
서로 다른 Gradient
↓
서로 다른 Feature 학습</code></pre>
<p>Uniform Distribution의 평균과 분산은</p>
<p>$$
E[W]=\frac{a+b}{2}
$$</p>
<p>$$
Var(W)=\frac{(b-a)^2}{12}
$$</p>
<p>이다.</p>
<hr>
<h1 id="14-normal-initialization">14. Normal Initialization</h1>
<p>Normal Distribution에서 Weight를 랜덤하게 뽑는다.</p>
<p>$$
W_{ij}
\sim
\mathcal N(\mu,\sigma^2)
$$</p>
<p>PyTorch:</p>
<pre><code class="language-python">nn.init.normal_(
    model.weight,
    mean=0.0,
    std=0.01
)</code></pre>
<p>보통 평균은 0 주변으로 두고 Standard Deviation을 통해 Weight의 Scale을 결정한다.</p>
<p>Uniform과 달리 Normal Distribution은:</p>
<pre><code class="language-text">          값 많음
             ↓
            /\
          /    \
--------        --------
             0</code></pre>
<p>처럼 평균 근처의 값이 많이 나온다.</p>
<hr>
<h1 id="15-uniform-vs-normal">15. Uniform vs Normal</h1>
<table>
<thead>
<tr>
<th>Uniform</th>
<th>Normal</th>
</tr>
</thead>
<tbody><tr>
<td>특정 범위 안에서 고르게 추출</td>
<td>평균 근처 값이 많이 등장</td>
</tr>
<tr>
<td>범위를 벗어나지 않음</td>
<td>큰 값도 낮은 확률로 등장</td>
</tr>
<tr>
<td><code>U(a,b)</code></td>
<td><code>N(μ,σ²)</code></td>
</tr>
</tbody></table>
<p>하지만 더 중요한 것은</p>
<blockquote>
<p><strong>Uniform과 Normal 중 무엇을 선택했느냐보다 Weight의 Scale과 Variance를 적절하게 설정했느냐이다.</strong></p>
</blockquote>
<p>실제로 Xavier와 Kaiming 모두 Uniform / Normal 버전이 존재한다.</p>
<hr>
<h1 id="16-weight의-크기는-왜-중요한가">16. Weight의 크기는 왜 중요한가?</h1>
<p>뉴런 하나를 생각해보자.</p>
<p>$$
z
=
w_1x_1+w_2x_2+\cdots+w_nx_n+b
$$</p>
<p>여기서:</p>
<ul>
<li>$x_i$: 이전 Layer에서 들어오는 입력</li>
<li>$w_i$: Weight</li>
<li>$b$: Bias</li>
<li>$z$: Activation Function 적용 전 값, <strong>Pre-activation</strong></li>
</ul>
<p>그리고:</p>
<p>$$
a=\phi(z)
$$</p>
<p>이다.</p>
<p>여기서:</p>
<ul>
<li>$z$: Activation 이전 값</li>
<li>$\phi$: ReLU, Sigmoid 등의 Activation Function</li>
<li>$a$: Activation Function을 통과한 출력</li>
</ul>
<p>전체 흐름:</p>
<pre><code class="language-text">이전 Layer의 출력 x
↓
z = Wx + b
↓
Activation Function
↓
a = φ(z)
↓
다음 Layer</code></pre>
<hr>
<h1 id="17-입력-개수가-많으면-왜-weight를-작게-해야-할까">17. 입력 개수가 많으면 왜 Weight를 작게 해야 할까?</h1>
<p>$$
z=
\sum_{i=1}^{n}w_ix_i
$$</p>
<p>에서 $n$개의 값을 계속 더한다.</p>
<p>입력이 2개인 경우:</p>
<pre><code class="language-text">w1x1 + w2x2</code></pre>
<p>입력이 1000개인 경우:</p>
<pre><code class="language-text">w1x1 + w2x2 + ... + w1000x1000</code></pre>
<p>Weight의 Scale을 똑같이 사용한다면 입력 개수가 많을수록 $z$의 Scale이 커질 가능성이 있다.</p>
<p>단순한 독립성 등의 가정 아래에서는 대략:</p>
<p>$$
Var(z)
\approx
nVar(w)Var(x)
$$</p>
<p>라고 생각할 수 있다.</p>
<p>따라서</p>
<p>$$
n\uparrow
$$</p>
<p>일수록 일반적으로 Weight의 Scale을 어느 정도 줄여줄 필요가 있다.</p>
<p>이때 들어오는 입력 뉴런의 개수를 <strong>fan-in</strong>이라고 한다.</p>
<hr>
<h1 id="18-weight가-너무-크거나-작으면">18. Weight가 너무 크거나 작으면?</h1>
<h3 id="weight가-너무-큰-경우">Weight가 너무 큰 경우</h3>
<pre><code class="language-text">큰 Weight
↓
큰 Pre-activation
↓
Activation Scale 불안정
↓
다음 Layer
↓
Scale이 계속 커질 가능성</code></pre>
<p>Backward에서도 Weight가 반복해서 관여하기 때문에 Gradient Scale 역시 불안정해질 수 있다.</p>
<h3 id="weight가-너무-작은-경우">Weight가 너무 작은 경우</h3>
<pre><code class="language-text">작은 Weight
↓
작은 Pre-activation
↓
작은 Activation
↓
Layer를 거치며 Signal 감소 가능</code></pre>
<p>Backward에서도 Gradient가 지나치게 작아질 가능성이 있다.</p>
<p>🚨 다만:</p>
<blockquote>
<p><code>Activation이 크다 → Gradient가 반드시 크다</code></p>
</blockquote>
<p>는 것은 아니다.</p>
<p>Activation Function에 따라 다르다.</p>
<p>예를 들어 Sigmoid는 입력 절댓값이 너무 커지면 포화되어 오히려 미분값이 0에 가까워지고 <strong>Gradient Vanishing</strong>이 발생할 수도 있다.</p>
<hr>
<h1 id="19-초기화의-핵심-정리">19. 초기화의 핵심 정리</h1>
<p>초기화 방법의 흐름은 다음과 같다.</p>
<pre><code class="language-text">Zero Initialization
↓
모든 뉴런이 동일
↓
Symmetry 문제


Constant Initialization
↓
0이 아니어도 동일한 값
↓
여전히 Symmetry 문제


Random Initialization
       ↓
 ┌─────────────┐
 ↓             ↓
Uniform      Normal
 ↓             ↓
뉴런마다 서로 다른 Weight
       ↓
Symmetry Breaking</code></pre>
<p>하지만 여기서 끝이 아니다.</p>
<pre><code class="language-text">Random하게 만들었다
↓
그런데 Weight를 얼마나 크게 해야 하지?
↓
너무 크면 Scale 불안정
너무 작으면 Signal 감소
↓
입력 개수까지 고려해서
적절한 Weight Variance를 결정하자
↓
Xavier / Kaiming Initialization</code></pre>
<hr>
<h1 id="20-오늘의-핵심">20. 오늘의 핵심</h1>
<h3 id="📌-gradient-descent">📌 Gradient Descent</h3>
<p>$$
\theta
\leftarrow
\theta-\eta\nabla L
$$</p>
<ul>
<li>Gradient: 방향 + 현재 경사의 가파른 정도</li>
<li>Learning Rate: 실제 Parameter 이동 크기</li>
<li>Batch GD: 전체 데이터로 Gradient 계산 후 Update</li>
</ul>
<h3 id="📌-sgd">📌 SGD</h3>
<ul>
<li>Pure SGD: 데이터 1개마다 Update</li>
<li>Mini-batch SGD: 일부 데이터를 평균내어 Update</li>
<li>실제 딥러닝에서는 Mini-batch 방식이 일반적</li>
</ul>
<h3 id="📌-weight-initialization">📌 Weight Initialization</h3>
<p>좋은 초기화의 핵심은 두 가지다.</p>
<blockquote>
<p><strong>1. Weight들이 서로 다른 값을 가져 Symmetry를 깨야 한다.</strong><br><strong>2. Weight가 적절한 Scale을 가져 Activation과 Gradient가 안정적으로 전달되어야 한다.</strong></p>
</blockquote>
<p>그리고 입력 개수 <code>fan-in</code>이 많아질수록 같은 Weight Scale을 그대로 사용하는 것이 적절하지 않을 수 있다.</p>
<p>이 문제를 해결하기 위해 다음으로 배우게 되는 것이:</p>
<ul>
<li><strong>Xavier / Glorot Initialization</strong></li>
<li><strong>Kaiming / He Initialization</strong></li>
</ul>
<p>이다.</p>
<blockquote>
<p>🎯 <strong>오늘 가장 중요한 연결</strong></p>
<p><code>Gradient</code>를 이해하면 → <code>GD / SGD</code>를 이해할 수 있고<br><code>Activation과 Gradient의 Scale</code>을 이해하면 → <code>Xavier / Kaiming</code>이 왜 필요한지 이해할 수 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - PyTorch 기초부터 Autograd, nn.Module, Torchviz까지]]></title>
            <link>https://velog.io/@min_max0315/TIL-PyTorch-%EA%B8%B0%EC%B4%88%EB%B6%80%ED%84%B0-Autograd-nn.Module-Torchviz%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@min_max0315/TIL-PyTorch-%EA%B8%B0%EC%B4%88%EB%B6%80%ED%84%B0-Autograd-nn.Module-Torchviz%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 24 Sep 2026 13:52:49 GMT</pubDate>
            <description><![CDATA[<p>오늘은 PyTorch를 공부하면서 <strong>Tensor indexing → Autograd → <code>nn.Module</code> → 모델 구성 → Torchviz → Parameter → Activation Function</strong>까지 이어서 정리했다.</p>
<p>단순히 API 사용법을 외우기보다는,</p>
<blockquote>
<p><strong>PyTorch가 내부에서 어떤 방식으로 Tensor와 계산 그래프를 관리하는가?</strong></p>
</blockquote>
<p>를 중심으로 이해해보자.</p>
<hr>
<h2 id="1-numpy와-pytorch-tensor의-차이">1. NumPy와 PyTorch Tensor의 차이</h2>
<h3 id="📌-numpy는-cpu-tensor는-gpu">📌 NumPy는 CPU, Tensor는 GPU?</h3>
<p>큰 흐름에서는 맞지만 정확하게는 조금 다르다.</p>
<ul>
<li><code>NumPy ndarray</code><ul>
<li>주로 CPU 기반 수치 연산</li>
<li>데이터 과학, 행렬 계산 등에 널리 사용</li>
</ul>
</li>
<li><code>PyTorch Tensor</code><ul>
<li>CPU에서도 사용 가능</li>
<li>GPU(CUDA) 연산 가능</li>
<li>Autograd를 통한 자동 미분 지원</li>
<li>딥러닝에 필요한 연산과 결합되어 있음</li>
</ul>
</li>
</ul>
<p>즉 Tensor는 단순히 <strong>GPU용 NumPy</strong>가 아니다.</p>
<blockquote>
<p>💡 <strong>PyTorch Tensor는 NumPy와 비슷한 수치 연산 기능에 GPU 연산과 자동 미분 기능을 추가한 구조라고 이해하면 편하다.</strong></p>
</blockquote>
<p>딥러닝 파이프라인 안에서는 굳이 NumPy로 갔다가 다시 Tensor로 돌아오기보다 Tensor를 계속 유지하는 경우가 많다.</p>
<hr>
<h2 id="2-와-torchmatmul">2. <code>@</code>와 <code>torch.matmul()</code></h2>
<p>PyTorch에서</p>
<pre><code class="language-python">A @ B</code></pre>
<p>와</p>
<pre><code class="language-python">torch.matmul(A, B)</code></pre>
<p>는 기본적으로 같은 행렬곱을 의미한다.</p>
<p>즉 <code>@</code>는 <code>torch.matmul()</code>의 연산자 표현이라고 생각하면 된다.</p>
<hr>
<h2 id="3-tensor-indexing">3. Tensor Indexing</h2>
<pre><code class="language-python">a = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
])</code></pre>
<p>shape은 <code>(2, 3)</code>이다.</p>
<pre><code class="language-text">axis 0 → 행 방향
axis 1 → 열 방향</code></pre>
<h3 id="📌-a01과-a0-1">📌 <code>A[0][1]</code>과 <code>A[0, 1]</code></h3>
<pre><code class="language-python">A[0][1]</code></pre>
<p>은:</p>
<pre><code class="language-python">temp = A[0]
result = temp[1]</code></pre>
<p>처럼 <strong>한 번 가져온 뒤 다시 indexing</strong>하는 방식이다.</p>
<p>반면:</p>
<pre><code class="language-python">A[0, 1]</code></pre>
<p>은:</p>
<pre><code class="language-text">axis 0 → index 0
axis 1 → index 1</code></pre>
<p>을 한 번에 지정한다.</p>
<blockquote>
<p>💡 <code>A[0][1]</code> = 가져오고 다시 indexing<br>💡 <code>A[0,1]</code> = 각 axis의 index를 한 번에 지정</p>
</blockquote>
<hr>
<h2 id="4-쉼표-가-중요하다">4. 쉼표 <code>,</code>가 중요하다</h2>
<pre><code class="language-python">a[index0, index1, index2]</code></pre>
<p>는:</p>
<pre><code class="language-text">index0 → axis 0
index1 → axis 1
index2 → axis 2</code></pre>
<p>를 의미한다.</p>
<p>예를 들어:</p>
<pre><code class="language-python">a[
    torch.tensor([0]),
    torch.tensor([1])
]</code></pre>
<p>은 <code>(0,1)</code> 좌표를 가져오므로:</p>
<pre><code class="language-python">tensor([2])</code></pre>
<p>가 된다.</p>
<hr>
<h2 id="5-tensor-하나만-index로-넣으면">5. Tensor 하나만 index로 넣으면?</h2>
<pre><code class="language-python">idx = torch.tensor([
    [0,1],
    [1,1]
])

a[idx]</code></pre>
<p>여기서 <code>idx</code>가 2차원이라고 해서:</p>
<pre><code class="language-text">첫 번째 차원 → axis 0
두 번째 차원 → axis 1</code></pre>
<p>이 되는 것은 아니다.</p>
<p><code>a[idx]</code>에서 <strong>index가 하나</strong>이므로 <code>idx</code> 전체가 axis 0을 indexing한다.</p>
<pre><code class="language-text">0 → [1,2,3]
1 → [4,5,6]

1 → [4,5,6]
1 → [4,5,6]</code></pre>
<p>결과:</p>
<pre><code class="language-python">tensor([
    [[1,2,3],
     [4,5,6]],

    [[4,5,6],
     [4,5,6]]
])</code></pre>
<blockquote>
<p>🚨 <strong>Index Tensor의 차원 수가 어느 axis를 접근할지 결정하는 것이 아니다.</strong></p>
</blockquote>
<p>핵심:</p>
<pre><code class="language-text">a[idx]
→ axis 0 하나를 indexing

a[idx1, idx2]
→ axis 0과 axis 1을 indexing</code></pre>
<hr>
<h2 id="6-여러-tensor-index를-넣으면-좌표가-된다">6. 여러 Tensor Index를 넣으면 좌표가 된다</h2>
<pre><code class="language-python">a[
    torch.tensor([0,1]),
    torch.tensor([1,2])
]</code></pre>
<p>은 같은 위치끼리 묶어서:</p>
<pre><code class="language-text">(0,1) → 2
(1,2) → 6</code></pre>
<p>을 가져온다.</p>
<p>결과:</p>
<pre><code class="language-python">tensor([2,6])</code></pre>
<hr>
<h2 id="7--ellipsis">7. <code>...</code> Ellipsis</h2>
<pre><code class="language-python">g = torch.randn(2,3,4,5,6)</code></pre>
<p>에서:</p>
<pre><code class="language-python">g[1,2,...]</code></pre>
<p>는 사실상:</p>
<pre><code class="language-python">g[1,2,:,:,:]</code></pre>
<p>와 같다.</p>
<p>즉:</p>
<pre><code class="language-text">axis 0 → index 1
axis 1 → index 2
axis 2 → 전부
axis 3 → 전부
axis 4 → 전부</code></pre>
<p>이므로 결과 shape은:</p>
<pre><code class="language-python">torch.Size([4,5,6])</code></pre>
<p>이다.</p>
<blockquote>
<p>💡 <code>...</code> = 나머지 모든 axis를 그대로 가져와라.</p>
</blockquote>
<hr>
<h2 id="8-autograd와-requires_grad">8. Autograd와 <code>requires_grad</code></h2>
<pre><code class="language-python">x = torch.tensor([1.], requires_grad=True)

y = x ** 2

y.backward()

print(x.grad)</code></pre>
<p>결과:</p>
<pre><code class="language-python">tensor([2.])</code></pre>
<p>수식은:</p>
<p>$$
y=x^2
$$</p>
<p>이므로:</p>
<p>$$
\frac{dy}{dx}=2x
$$</p>
<p>현재 <code>x=1</code>이므로:</p>
<p>$$
\frac{dy}{dx}=2
$$</p>
<p>이다.</p>
<p><code>requires_grad=True</code>는:</p>
<blockquote>
<p><strong>이 Tensor가 참여하는 연산을 Autograd가 추적하도록 하겠다.</strong></p>
</blockquote>
<p>라는 의미다.</p>
<hr>
<h2 id="9-leaf-tensor">9. Leaf Tensor</h2>
<p>Leaf Tensor는 단순히 입력 Tensor를 뜻하는 것이 아니다.</p>
<p>정확히는:</p>
<blockquote>
<p><strong>Autograd가 추적하는 다른 연산의 결과로 만들어진 것이 아니라 직접 생성된 Tensor</strong></p>
</blockquote>
<p>이다.</p>
<p>예:</p>
<pre><code class="language-python">z = torch.tensor([1.], requires_grad=True)

x = z * 2
y = x ** 2</code></pre>
<pre><code class="language-text">z → leaf
↓
×2
↓
x → non-leaf
↓
square
↓
y → non-leaf</code></pre>
<p>따라서:</p>
<pre><code class="language-python">z.is_leaf   # True
x.is_leaf   # False</code></pre>
<p>이다.</p>
<hr>
<h2 id="10-gradient는-어디에-저장되는가">10. Gradient는 어디에 저장되는가?</h2>
<p>Backward 과정에서 중간 Tensor의 gradient도 계산된다.</p>
<p>하지만 기본적으로 <code>.grad</code>에 저장되는 것은 leaf Tensor다.</p>
<pre><code class="language-python">z = torch.tensor([1.], requires_grad=True)

x = z * 2
y = x ** 2

y.backward()</code></pre>
<p>이 경우:</p>
<pre><code class="language-python">z.grad</code></pre>
<p>에는 값이 저장되지만:</p>
<pre><code class="language-python">x.grad</code></pre>
<p>는 기본적으로 <code>None</code>이다.</p>
<p>중간 Tensor의 gradient도 보고 싶다면:</p>
<pre><code class="language-python">x.retain_grad()</code></pre>
<p>를 사용한다.</p>
<hr>
<h2 id="11-forward에서-무엇을-저장하는가">11. Forward에서 무엇을 저장하는가?</h2>
<p>Forward 과정에서 <strong>미분값을 미리 저장하는 것이 아니다.</strong></p>
<p>Backward에 필요한:</p>
<pre><code class="language-text">activation
입력값
출력값
중간값
연산 정보</code></pre>
<p>등을 저장한다.</p>
<p>예를 들어:</p>
<p>$$
y=xW+b
$$</p>
<p>에서:</p>
<p>$$
\frac{\partial L}{\partial W}
=
\frac{\partial L}{\partial y}
\frac{\partial y}{\partial W}
$$</p>
<p>를 계산하려면 forward에서 사용했던 <code>x</code>가 필요하다.</p>
<p>따라서 흐름은:</p>
<pre><code class="language-text">Forward
↓
Activation / 중간값 저장

Backward
↓
저장된 값을 이용해 local gradient 계산
↓
Chain Rule
↓
이전 layer로 gradient 전달</code></pre>
<p>이다.</p>
<hr>
<h2 id="12-gradient는-누적된다">12. Gradient는 누적된다</h2>
<p>PyTorch의 <code>.grad</code>는 기본적으로 누적된다.</p>
<p>그래서 학습에서는:</p>
<pre><code class="language-python">optimizer.zero_grad()</code></pre>
<p>를 사용해 이전 gradient를 초기화한다.</p>
<hr>
<h2 id="13-transfer-learning">13. Transfer Learning</h2>
<p>Transfer Learning은:</p>
<blockquote>
<p><strong>이미 다른 데이터로 학습된 모델의 지식을 새로운 문제에 재사용하는 방법론</strong></p>
</blockquote>
<p>이다.</p>
<p>장점:</p>
<pre><code class="language-text">학습 시간 감소
데이터 요구량 감소
초기 성능 향상
이미 학습한 feature 재사용</code></pre>
<p>Transfer Learning 안에는 크게:</p>
<pre><code class="language-text">Transfer Learning
│
├─ Feature Extraction
│   └─ Backbone Freeze
│      Classifier만 학습
│
└─ Fine-Tuning
    └─ Backbone 일부 또는 전체까지 재학습</code></pre>
<p>이 있다.</p>
<p>즉:</p>
<blockquote>
<p><strong>Transfer Learning = 더 큰 개념</strong><br><strong>Fine-tuning = Transfer Learning의 한 방식</strong></p>
</blockquote>
<p>이다.</p>
<hr>
<h2 id="14-nnmodule">14. <code>nn.Module</code></h2>
<p>PyTorch에서 모델은 보통:</p>
<pre><code class="language-python">class CustomModel(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        ...</code></pre>
<p>형태로 만든다.</p>
<h3 id="📌-__init__">📌 <code>__init__()</code></h3>
<p>모델이 사용할 layer를 정의한다.</p>
<pre><code class="language-python">self.fc1 = nn.Linear(10,20)
self.fc2 = nn.Linear(20,1)</code></pre>
<h3 id="📌-super__init__">📌 <code>super().__init__()</code></h3>
<p>부모 클래스인 <code>nn.Module</code>의 <code>__init__()</code>을 호출한다.</p>
<p>이를 통해 PyTorch가:</p>
<pre><code class="language-text">Parameter 관리
Submodule 관리
state_dict
.to(&quot;cuda&quot;)
.train()
.eval()</code></pre>
<p>등을 위한 내부 상태를 준비한다.</p>
<blockquote>
<p>부모 method를 사용할 수 있게 되는 것은 <strong>상속 자체</strong>이고,<br><code>super().__init__()</code>은 부모 클래스의 초기 상태를 설정하는 것이다.</p>
</blockquote>
<hr>
<h2 id="15-nnsequential">15. <code>nn.Sequential</code></h2>
<pre><code class="language-python">fc1 = nn.Linear(1,3)
fc2 = nn.Linear(3,1)</code></pre>
<p>에서:</p>
<pre><code class="language-python">model = fc2(fc1(x))</code></pre>
<p>는 모델이 아니라 이미 계산된 Tensor 결과다.</p>
<p>하나의 모델로 만들려면:</p>
<pre><code class="language-python">model = nn.Sequential(
    fc1,
    fc2
)</code></pre>
<p>로 만든다.</p>
<p>그러면:</p>
<pre><code class="language-python">model(x)</code></pre>
<p>는 내부적으로:</p>
<pre><code class="language-python">fc2(fc1(x))</code></pre>
<p>를 수행한다.</p>
<hr>
<h2 id="16-list-is-not-a-module-subclass">16. <code>list is not a Module subclass</code></h2>
<p>잘못된 예:</p>
<pre><code class="language-python">nn.Sequential([
    nn.Linear(10,20),
    nn.ReLU()
])</code></pre>
<p><code>nn.Sequential</code>은 <code>nn.Module</code>들을 기대하지만 Python list 하나가 들어갔기 때문에 에러가 난다.</p>
<p>올바른 방식:</p>
<pre><code class="language-python">nn.Sequential(
    nn.Linear(10,20),
    nn.ReLU()
)</code></pre>
<p>또는:</p>
<pre><code class="language-python">layers = [
    nn.Linear(10,20),
    nn.ReLU()
]

nn.Sequential(*layers)</code></pre>
<hr>
<h2 id="17-layer-자체와-실행-결과">17. Layer 자체와 실행 결과</h2>
<pre><code class="language-python">self.linear1</code></pre>
<p>은 <strong>Layer 자체</strong>다.</p>
<p>반면:</p>
<pre><code class="language-python">self.linear1(x)</code></pre>
<p>은 <strong>x를 해당 Layer에 넣어 실제 연산한 결과</strong>다.</p>
<p>따라서 <code>forward()</code>에서는:</p>
<pre><code class="language-python">def forward(self, x):
    x = self.linear1(x)
    x = self.linear2(x)
    return x</code></pre>
<p>처럼 사용해야 한다.</p>
<hr>
<h2 id="18-클래스와-객체">18. 클래스와 객체</h2>
<pre><code class="language-python">model = CustomModel</code></pre>
<p>은 클래스 자체다.</p>
<p>실제 모델 객체를 만들려면:</p>
<pre><code class="language-python">model = CustomModel()</code></pre>
<p>이어야 한다.</p>
<pre><code class="language-text">CustomModel
→ 설계도

CustomModel()
→ 실제 생성된 모델 객체</code></pre>
<hr>
<h2 id="19-학습-가능한-parameter-수">19. 학습 가능한 Parameter 수</h2>
<p>모델 구조:</p>
<pre><code class="language-python">Linear(1,3)
Sigmoid
Linear(3,4)
Sigmoid

Linear(4,4)
Sigmoid
Linear(4,1)
Sigmoid</code></pre>
<p><code>Linear(in, out)</code>의 parameter는:</p>
<p>$$
Weight = out \times in
$$</p>
<p>$$
Bias = out
$$</p>
<p>이다.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th align="right">Weight</th>
<th align="right">Bias</th>
<th align="right">Parameter</th>
</tr>
</thead>
<tbody><tr>
<td>Linear(1,3)</td>
<td align="right">3</td>
<td align="right">3</td>
<td align="right">6</td>
</tr>
<tr>
<td>Linear(3,4)</td>
<td align="right">12</td>
<td align="right">4</td>
<td align="right">16</td>
</tr>
<tr>
<td>Linear(4,4)</td>
<td align="right">16</td>
<td align="right">4</td>
<td align="right">20</td>
</tr>
<tr>
<td>Linear(4,1)</td>
<td align="right">4</td>
<td align="right">1</td>
<td align="right">5</td>
</tr>
</tbody></table>
<p>총:</p>
<p>$$
6+16+20+5=47
$$</p>
<p>개다.</p>
<p>PyTorch에서:</p>
<pre><code class="language-python">sum(
    p.numel()
    for p in model.parameters()
    if p.requires_grad
)</code></pre>
<p>로 확인할 수 있다.</p>
<hr>
<h2 id="20-bias도-학습되는가">20. Bias도 학습되는가?</h2>
<p>그렇다.</p>
<pre><code class="language-python">nn.Linear(1,3)</code></pre>
<p>은 기본적으로:</p>
<pre><code class="language-python">nn.Linear(1,3,bias=True)</code></pre>
<p>이다.</p>
<p>따라서 <code>weight</code>, <code>bias</code> 모두 학습 대상이다.</p>
<p>반면 <code>Sigmoid</code>에는 학습 가능한 parameter가 없다.</p>
<hr>
<h2 id="21-torchviz">21. Torchviz</h2>
<p><code>torchviz</code>는:</p>
<blockquote>
<p><strong>PyTorch의 Autograd 계산 그래프를 시각화하는 라이브러리</strong></p>
</blockquote>
<p>이다.</p>
<p>예:</p>
<pre><code class="language-python">x = torch.tensor([4.], requires_grad=True)

y = x ** 2
z = y + 1
f = z * 4
g = f ** 2 + z</code></pre>
<pre><code class="language-python">make_dot(g)</code></pre>
<p>를 실행하면 <code>g</code>가 어떤 연산으로 만들어졌는지 계산 그래프로 볼 수 있다.</p>
<p>&lt; 예시 이미지 &gt; </p>
<p><img src="https://velog.velcdn.com/images/min_max0315/post/90d64c4b-b251-40c0-98a3-484559104d5c/image.png" alt=""></p>
<hr>
<h2 id="22-make_dot">22. <code>make_dot()</code></h2>
<p><code>make_dot()</code>에는 <strong>시각화하고 싶은 최종 Tensor</strong>를 넣는다.</p>
<pre><code class="language-python">make_dot(g)</code></pre>
<p>는 <code>g</code>에서 역으로 따라가면서 Autograd graph를 보여준다.</p>
<p>중간 Tensor를 넣으면:</p>
<pre><code class="language-python">make_dot(z)</code></pre>
<p><code>z</code>까지의 계산 그래프만 보여준다.</p>
<h3 id="📌-왜-make_dotmodel은-안-되는가">📌 왜 <code>make_dot(model)</code>은 안 되는가?</h3>
<p><code>model</code>은 <code>nn.Module</code> 객체일 뿐, 실제 계산 결과 Tensor가 아니다.</p>
<p>먼저:</p>
<pre><code class="language-python">result = model(x)</code></pre>
<p>로 forward를 수행한 다음:</p>
<pre><code class="language-python">make_dot(result)</code></pre>
<p>을 사용해야 한다.</p>
<p>Parameter 이름까지 보고 싶다면:</p>
<pre><code class="language-python">make_dot(
    result,
    params=dict(model.named_parameters())
)</code></pre>
<p>처럼 사용할 수 있다.</p>
<hr>
<h2 id="23-graphviz-dot-에러">23. Graphviz <code>dot</code> 에러</h2>
<p>다음 에러:</p>
<pre><code class="language-text">ExecutableNotFound:
failed to execute PosixPath(&#39;dot&#39;)</code></pre>
<p>는 PyTorch 문제가 아니라 시스템에 Graphviz의 <code>dot</code> 실행파일이 없어서 발생한 것이다.</p>
<p>Mac에서는:</p>
<pre><code class="language-bash">brew install graphviz</code></pre>
<p>설치 후:</p>
<pre><code class="language-bash">dot -V</code></pre>
<p>로 확인할 수 있다.</p>
<blockquote>
<p><code>pip install graphviz</code> = Python 패키지<br><code>brew install graphviz</code> = 실제 Graphviz 실행 프로그램</p>
</blockquote>
<hr>
<h2 id="24-torchviz에서-accumulategrad">24. Torchviz에서 <code>AccumulateGrad</code></h2>
<p>다음 연산:</p>
<pre><code class="language-python">x = torch.tensor([4.], requires_grad=True)

y = x ** 2
z = y + 1
f = z * 4
g = f ** 2 + z</code></pre>
<p>구조:</p>
<pre><code class="language-text">x
↓
y = x²
↓
z = y + 1
├────────────────┐
↓                │
f = z × 4        │
↓                │
f²               │
↓                │
└────── + ←──────┘
        ↓
        g</code></pre>
<p><code>z</code>에서 두 경로로 분기된다.</p>
<p>Backward에서는 두 경로의 gradient가 합쳐진다.</p>
<p>현재:</p>
<p>$$
x=4, \quad y=16, \quad z=17, \quad f=68
$$</p>
<p>첫 번째 경로:</p>
<p>$$
\frac{\partial g}{\partial f}=2f=136
$$</p>
<p>$$
\frac{\partial f}{\partial z}=4
$$</p>
<p>따라서:</p>
<p>$$
136 \times 4 = 544
$$</p>
<p>두 번째 경로에서는:</p>
<p>$$
\frac{\partial g}{\partial z}=1
$$</p>
<p>따라서:</p>
<p>$$
544+1=545
$$</p>
<p>이다.</p>
<h3 id="📌-왜-z에-accumulategrad가-없는가">📌 왜 <code>z</code>에 <code>AccumulateGrad</code>가 없는가?</h3>
<blockquote>
<p><strong><code>AccumulateGrad</code>는 분기된 gradient를 합치는 노드가 아니다.</strong></p>
</blockquote>
<p><code>AccumulateGrad</code>는:</p>
<blockquote>
<p><strong>Leaf Tensor의 <code>.grad</code>에 최종 gradient를 누적해서 저장하는 역할</strong></p>
</blockquote>
<p>이다.</p>
<p><code>z</code>는 연산 결과로 만들어진 non-leaf Tensor이므로 기본적으로 <code>z.grad</code>를 저장하지 않는다.</p>
<p>반면 <code>x</code>는 leaf Tensor이므로 <code>x.grad</code>에 최종 gradient를 저장한다.</p>
<p>최종적으로:</p>
<p>$$
\frac{\partial g}{\partial z}=545
$$</p>
<p>이고:</p>
<p>$$
z=x^2+1
$$</p>
<p>이므로:</p>
<p>$$
\frac{\partial z}{\partial x}=2x=8
$$</p>
<p>따라서:</p>
<p>$$
\frac{\partial g}{\partial x}
=
545 \times 8
=
4360
$$</p>
<p>그래서:</p>
<pre><code class="language-python">g.backward()
print(x.grad)</code></pre>
<p>결과는:</p>
<pre><code class="language-python">tensor([4360.])</code></pre>
<p>이다.</p>
<hr>
<h2 id="25-sigmoid는-element-wise-activation">25. Sigmoid는 Element-wise Activation</h2>
<p>Sigmoid는 각 element에 독립적으로 적용된다.</p>
<p>$$
x=[x_1,x_2,x_3]
$$</p>
<p>라면:</p>
<p>$$
[\sigma(x_1),\sigma(x_2),\sigma(x_3)]
$$</p>
<p>처럼 계산한다.</p>
<p>ReLU, GELU, SiLU 등의 일반적인 activation도 대부분 element-wise 방식이다.</p>
<hr>
<h2 id="26-다른-element-관계를-고려하는-activation">26. 다른 Element 관계를 고려하는 Activation</h2>
<p>전통적인 activation은:</p>
<p>$$
y_i=f(x_i)
$$</p>
<p>처럼 현재 element만 본다.</p>
<p>하지만 현대 연구에서는:</p>
<pre><code class="language-text">다른 channel
주변 spatial feature
global context</code></pre>
<p>등을 이용해 activation을 동적으로 결정하는 방법들도 연구되고 있다.</p>
<p>개념적으로:</p>
<p>$$
y_i=f(x_i \mid \text{context})
$$</p>
<p>처럼 볼 수 있다.</p>
<pre><code class="language-text">현재 feature
   +
주변 feature / 다른 channel 정보
   ↓
activation 결정</code></pre>
<p>다만 이런 방식은 아직 ReLU나 GELU처럼 기본값으로 널리 쓰이는 것은 아니고, <strong>dynamic activation / gating / channel interaction / attention</strong> 등의 형태로 연구되고 있다.</p>
<hr>
<h2 id="27-오늘-배운-pytorch-전체-흐름">27. 오늘 배운 PyTorch 전체 흐름</h2>
<pre><code class="language-text">Tensor
↓
Indexing / Shape 이해
↓
nn.Module로 모델 정의
↓
Forward
↓
Tensor 연산 수행
↓
Autograd Graph 생성
↓
중간 Activation 저장
↓
Loss
↓
Backward
↓
Chain Rule
↓
Gradient 계산
↓
Leaf Tensor / Parameter의 .grad에 저장
↓
Optimizer
↓
Parameter Update</code></pre>
<p>Torchviz는 이 과정 중:</p>
<pre><code class="language-text">Forward
↓
Autograd Graph</code></pre>
<p>를 눈으로 확인할 수 있게 해주는 도구다.</p>
<hr>
<h2 id="28-오늘-반드시-기억할-핵심">28. 오늘 반드시 기억할 핵심</h2>
<blockquote>
<p>💡 <strong>Tensor indexing에서 axis를 결정하는 것은 Index Tensor의 차원이 아니라 <code>a[idx1, idx2, ...]</code>에서 쉼표로 구분된 index의 위치다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>requires_grad=True</code>는 연산 그래프를 추적하게 만들고, <code>backward()</code>는 그래프를 역으로 따라가며 gradient를 계산한다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong>Forward에서는 미분값을 저장하는 것이 아니라 backward에 필요한 activation과 중간값을 저장한다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong>중간 Tensor의 gradient도 backward 과정에서 계산되지만 기본적으로 <code>.grad</code>에는 leaf Tensor의 gradient만 저장된다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>nn.Module</code>은 모델의 구조를 관리하고, <code>forward()</code>는 실제 Tensor가 어떤 연산을 거칠지를 정의한다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>super().__init__()</code>은 부모인 <code>nn.Module</code>이 필요한 내부 상태를 초기화한다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>model</code>과 <code>model(x)</code>는 다르다. 전자는 Module이고 후자는 실제 forward 연산 결과 Tensor다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>make_dot()</code>은 모델 자체가 아니라 실제 연산 결과 Tensor를 받아 Autograd Graph를 시각화한다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong><code>AccumulateGrad</code>는 분기된 gradient를 합치는 노드가 아니라 leaf Tensor의 <code>.grad</code>에 gradient를 누적하는 역할이다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong>Linear에서는 weight뿐 아니라 bias도 기본적으로 학습 가능한 parameter다.</strong></p>
</blockquote>
<blockquote>
<p>💡 <strong>전통적인 activation은 element-wise지만, feature/channel/context 간 관계를 고려하는 activation이나 gating 방식도 연구되고 있다.</strong></p>
</blockquote>
<hr>
<h2 id="마무리">마무리</h2>
<p>오늘은 단순히 PyTorch API를 사용하는 법보다 <strong>PyTorch 내부에서 Tensor → Forward → Computational Graph → Backward → Gradient가 어떻게 연결되는지</strong>를 깊게 다뤘다.</p>
<p>핵심 흐름은 다음과 같다.</p>
<pre><code class="language-text">Tensor가 들어온다
→ Module이 Tensor를 연산한다
→ 연산 그래프가 만들어진다
→ Forward 중 필요한 값이 저장된다
→ Backward가 그래프를 역으로 탄다
→ Chain Rule로 Gradient가 계산된다
→ Parameter의 .grad에 저장된다
→ Optimizer가 Parameter를 업데이트한다</code></pre>
<p>이 흐름이 잡히면 이후에 배우게 될 <strong>Loss Function, Optimizer, CNN, Transformer, Fine-Tuning, Gradient Checkpointing</strong>도 하나의 학습 시스템 안에서 연결해서 이해할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - Maximum Likelihood, NLL, Cross Entropy와 데이터 전처리]]></title>
            <link>https://velog.io/@min_max0315/TIL-Maximum-Likelihood-NLL-Cross-Entropy%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@min_max0315/TIL-Maximum-Likelihood-NLL-Cross-Entropy%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Tue, 22 Sep 2026 12:04:40 GMT</pubDate>
            <description><![CDATA[<p>오늘은 딥러닝에서 자주 사용하는 <strong>Maximum Likelihood, NLL, Cross Entropy, Softmax + Cross Entropy, MSE</strong>와 데이터 전처리에서 사용하는 <strong>정규화, 표준화, Whitening</strong>에 대해 정리한다.</p>
<p>단순히 수식을 외우는 것보다 <strong>&quot;왜 이걸 사용하는가?&quot;</strong>를 중심으로 이해해보자.</p>
<hr>
<h2 id="1-maximum-likelihood란">1. Maximum Likelihood란?</h2>
<h3 id="📌-우리가-원하는-것은-무엇일까">📌 우리가 원하는 것은 무엇일까?</h3>
<p>분류 모델을 학습한다고 생각해보자.</p>
<p>입력 데이터와 정답은 다음과 같이 표현할 수 있다.</p>
<p>$$
(x_i, y_i)
$$</p>
<ul>
<li>$x_i$: i번째 입력 데이터</li>
<li>$y_i$: i번째 데이터의 <strong>정답 클래스</strong></li>
<li>$i$: 데이터의 인덱스</li>
<li>$\theta$: 모델이 가지고 있는 모든 weight, bias 등의 파라미터</li>
</ul>
<p>예를 들어 MNIST라면,</p>
<pre><code class="language-text">x_i = 숫자 3 이미지
y_i = 3</code></pre>
<p>이다.</p>
<blockquote>
<p>🚨 여기서 $y_i$는 One-Hot Encoding의 <code>0</code>, <code>1</code> 값이 아니다.<br><strong>정답 클래스의 번호</strong>를 의미한다.</p>
</blockquote>
<hr>
<h3 id="📌-모델이-정답에-얼마나-높은-확률을-줬는가">📌 모델이 정답에 얼마나 높은 확률을 줬는가?</h3>
<p>모델은 입력 $x_i$가 들어왔을 때 각 클래스에 대한 확률을 계산한다.</p>
<p>$$
P(y_i \mid x_i, \theta)
$$</p>
<p>이 식의 의미는:</p>
<blockquote>
<p><strong>현재 모델 $\theta$에서 입력 $x_i$가 들어왔을 때 실제 정답 $y_i$가 나올 확률</strong></p>
</blockquote>
<p>이다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">정답: class 3

모델 출력:
[0.01, 0.02, 0.05, 0.80, 0.12]
                  ↑
               class 3</code></pre>
<p>이면,</p>
<p>$$
P(y_i=3 \mid x_i,\theta)=0.8
$$</p>
<p>이다.</p>
<p>우리는 당연히 이 정답 확률을 높이고 싶다.</p>
<hr>
<h2 id="2-maximum-likelihood-estimation">2. Maximum Likelihood Estimation</h2>
<p>데이터가 하나가 아니라 $N$개라면 모든 데이터에 대해 정답 확률을 높이고 싶다.</p>
<p>그래서 다음과 같이 확률을 곱한다.</p>
<p>$$
L(\theta)
=
\prod_{i=1}^{N}
P(y_i \mid x_i,\theta)
$$</p>
<p>여기서:</p>
<ul>
<li>$N$: 전체 학습 데이터 개수</li>
<li>$P(y_i|x_i,\theta)$: i번째 데이터에서 실제 정답에 준 확률</li>
<li>$L(\theta)$: 현재 모델이 실제 데이터를 얼마나 잘 설명하는지를 나타내는 Likelihood</li>
</ul>
<p>그리고 우리의 진짜 목적은:</p>
<p>$$
\theta^*
=
\arg\max_{\theta}L(\theta)
$$</p>
<p>이다.</p>
<p>즉,</p>
<blockquote>
<p><strong>실제 정답들이 나올 확률을 가장 크게 만드는 모델 파라미터 $\theta$를 찾자.</strong></p>
</blockquote>
<p>이것이 <strong>Maximum Likelihood Estimation, MLE</strong>이다.</p>
<h3 id="📌-간단한-예시">📌 간단한 예시</h3>
<p>현재 모델이 세 데이터의 정답에 다음 확률을 줬다고 하자.</p>
<pre><code class="language-text">데이터 1 → 0.6
데이터 2 → 0.7
데이터 3 → 0.5</code></pre>
<p>Likelihood는:</p>
<p>$$
0.6\times0.7\times0.5=0.21
$$</p>
<p>그런데 학습 후:</p>
<pre><code class="language-text">데이터 1 → 0.8
데이터 2 → 0.9
데이터 3 → 0.7</code></pre>
<p>이 되었다면:</p>
<p>$$
0.8\times0.9\times0.7=0.504
$$</p>
<p>Likelihood가 증가했다.</p>
<blockquote>
<p>💡 <strong>Maximum Likelihood의 핵심</strong><br>모델이 우리가 실제로 관측한 정답에 높은 확률을 주도록 모델의 weight를 찾는 것.</p>
</blockquote>
<hr>
<h2 id="3-왜-log를-사용하는가">3. 왜 Log를 사용하는가?</h2>
<p>Maximum Likelihood에는 한 가지 문제가 있다.</p>
<p>데이터가 많아지면 작은 확률들을 계속 곱해야 한다.</p>
<pre><code class="language-text">0.8 × 0.7 × 0.9 × 0.6 × 0.8 × ...</code></pre>
<p>확률은 1보다 작기 때문에 계속 곱하면 값이 매우 작아진다.</p>
<p>또한 곱셈은 미분하기도 상대적으로 불편하다.</p>
<p>그래서 <strong>log</strong>를 사용한다.</p>
<p>로그의 중요한 성질은:</p>
<p>$$
\log(ab)=\log a+\log b
$$</p>
<p>이다.</p>
<p>따라서:</p>
<p>$$
\log L(\theta)
=
\sum_{i=1}^{N}
\log P(y_i|x_i,\theta)
$$</p>
<p>가 된다.</p>
<p>곱셈이 덧셈으로 변경되었다.</p>
<p>이것을 <strong>Log-Likelihood</strong>라고 한다.</p>
<hr>
<h2 id="4-nll---negative-log-likelihood">4. NLL - Negative Log Likelihood</h2>
<p>Maximum Likelihood는 값을 <strong>최대화</strong>해야 한다.</p>
<p>하지만 딥러닝에서는 일반적으로 Loss를 <strong>최소화</strong>하는 방식으로 학습한다.</p>
<p>따라서 Log-Likelihood에 음수를 붙인다.</p>
<p>$$
L_{NLL}
=
-\sum_{i=1}^{N}
\log P(y_i|x_i,\theta)
$$</p>
<p>이것이 <strong>Negative Log Likelihood(NLL)</strong> 이다.</p>
<p>즉:</p>
<pre><code class="language-text">Likelihood 최대화
        ↓
Log-Likelihood 최대화
        ↓
Negative Log-Likelihood 최소화</code></pre>
<p>세 개는 결국 <strong>같은 목적</strong>을 다른 형태로 표현한 것이다.</p>
<blockquote>
<p>🎯 <strong>NLL을 왜 쓰는가?</strong><br>Maximum Likelihood라는 통계적 목표를 딥러닝에서 사용하기 편한 <strong>Loss 최소화 문제</strong>로 바꾸기 위해 사용한다.</p>
</blockquote>
<hr>
<h2 id="5-cross-entropy">5. Cross Entropy</h2>
<p>Cross Entropy는 <strong>정답 확률 분포와 모델이 예측한 확률 분포의 차이</strong>를 측정한다.</p>
<p>다중 클래스 분류에서:</p>
<p>$$
L_{CE}
=
-\sum_{k=1}^{C}
t_k\log p_k
$$</p>
<p>여기서:</p>
<ul>
<li>$C$: 클래스 개수</li>
<li>$k$: 클래스의 인덱스</li>
<li>$t_k$: 정답의 One-Hot 값</li>
<li>$p_k$: 모델이 k번째 클래스에 예측한 확률</li>
</ul>
<p>예를 들어 정답이 class 3이라면:</p>
<pre><code class="language-text">정답 t:
[0, 0, 0, 1, 0]

예측 p:
[0.01, 0.02, 0.05, 0.80, 0.12]</code></pre>
<p>Cross Entropy는:</p>
<p>$$
-\left(
0\log0.01+
0\log0.02+
0\log0.05+
1\log0.8+
0\log0.12
\right)
$$</p>
<p>결국:</p>
<p>$$
L=-\log0.8
$$</p>
<p>만 남는다.</p>
<hr>
<h2 id="6-nll과-cross-entropy는-왜-같은가">6. NLL과 Cross Entropy는 왜 같은가?</h2>
<p>NLL은:</p>
<p>$$
-\log P(y_i|x_i,\theta)
$$</p>
<p>즉 <strong>정답 클래스에 모델이 준 확률</strong>만 사용한다.</p>
<p>Cross Entropy는:</p>
<p>$$
-\sum_k t_k\log p_k
$$</p>
<p>이지만 One-Hot Encoding에서는 정답 클래스만 $t_k=1$이고 나머지는 0이다.</p>
<p>따라서:</p>
<p>$$
-\log p_{\text{정답}}
$$</p>
<p>만 남는다.</p>
<p>즉 <strong>One-Hot + 다중 클래스 분류 환경에서는</strong></p>
<p>$$
NLL = Cross\ Entropy
$$</p>
<p>가 된다.</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>관점</th>
</tr>
</thead>
<tbody><tr>
<td>NLL</td>
<td>실제 정답이 나올 확률을 최대화하자</td>
</tr>
<tr>
<td>Cross Entropy</td>
<td>정답 분포와 예측 분포의 차이를 줄이자</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <strong>NLL은 Likelihood 관점, Cross Entropy는 확률분포 관점에서 시작하지만 One-Hot 분류에서는 같은 식으로 귀결된다.</strong></p>
</blockquote>
<hr>
<h2 id="7-softmax">7. Softmax</h2>
<p>Neural Network의 마지막 출력은 바로 확률이 아니다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">z = [2.0, 1.0, 0.5]</code></pre>
<p>이런 값을 출력할 수 있다.</p>
<p>이를 <strong>Logit</strong>이라고 한다.</p>
<p>Softmax는 logit을 확률로 변환한다.</p>
<p>$$
p_i=
\frac{e^{z_i}}
{\sum_j e^{z_j}}
$$</p>
<p>여기서:</p>
<ul>
<li>$z_i$: i번째 클래스의 logit</li>
<li>$p_i$: i번째 클래스의 예측 확률</li>
<li>$j$: 모든 클래스를 순회하기 위한 인덱스</li>
</ul>
<blockquote>
<p>🎯 <strong>Softmax를 왜 쓰는가?</strong><br>모델이 출력한 임의의 실수값을 <strong>합이 1인 클래스 확률분포</strong>로 만들기 위해서다.</p>
</blockquote>
<hr>
<h2 id="8-softmax--cross-entropy가-좋은-이유">8. Softmax + Cross Entropy가 좋은 이유</h2>
<p>Softmax + Cross Entropy를 함께 사용하는 중요한 이유 중 하나는 <strong>gradient가 매우 깔끔해지기 때문</strong>이다.</p>
<p>정답이 $k$번째 클래스라고 하자.</p>
<p>Cross Entropy:</p>
<p>$$
L=-\log p_k
$$</p>
<p>Softmax는:</p>
<p>$$
p_k=
\frac{e^{z_k}}
{\sum_j e^{z_j}}
$$</p>
<p>이므로 합치면:</p>
<p>$$
L
=
-\log
\left(
\frac{e^{z_k}}
{\sum_j e^{z_j}}
\right)
$$</p>
<p>로그 성질을 사용하면:</p>
<p>$$
L
=
-z_k+
\log
\left(
\sum_j e^{z_j}
\right)
$$</p>
<hr>
<h2 id="9-loss를-각-logit에-대해-미분">9. Loss를 각 logit에 대해 미분</h2>
<p>우리가 원하는 것은:</p>
<p>$$
\frac{\partial L}{\partial z_i}
$$</p>
<p>이다.</p>
<p>여기서:</p>
<ul>
<li>$L$: Loss</li>
<li>$z_i$: i번째 클래스의 logit</li>
<li>$\frac{\partial L}{\partial z_i}$: $z_i$를 조금 바꿨을 때 Loss가 얼마나 변하는지를 나타내는 gradient</li>
</ul>
<h3 id="📌-정답-클래스인-경우">📌 정답 클래스인 경우</h3>
<p>정답 클래스가 $i=k$라면:</p>
<p>$$
-z_k
$$</p>
<p>를 $z_k$로 미분하면:</p>
<p>$$
-1
$$</p>
<p>그리고:</p>
<p>$$
\log\left(\sum_j e^{z_j}\right)
$$</p>
<p>를 $z_k$로 미분하면:</p>
<p>$$
\frac{e^{z_k}}
{\sum_j e^{z_j}}
$$</p>
<p>가 된다.</p>
<p>그런데 이것은 바로 Softmax의 $p_k$이다.</p>
<p>따라서:</p>
<p>$$
\frac{\partial L}{\partial z_k}
=
p_k-1
$$</p>
<h3 id="📌-정답-클래스가-아닌-경우">📌 정답 클래스가 아닌 경우</h3>
<p>정답이 아닌 클래스 $i$에서는 $-z_k$와 관계가 없으므로 미분하면 0이다.</p>
<p>따라서:</p>
<p>$$
\frac{\partial L}{\partial z_i}
=
p_i
$$</p>
<p>가 된다.</p>
<hr>
<h2 id="10-결국-gradient는-p---t">10. 결국 Gradient는 <code>p - t</code></h2>
<p>One-Hot 정답을 사용하면 이를 하나의 식으로 표현할 수 있다.</p>
<p>$$
\frac{\partial L}{\partial z_i}
=
p_i-t_i
$$</p>
<p>여기서:</p>
<ul>
<li>$p_i$: i번째 클래스의 예측 확률</li>
<li>$t_i$: i번째 클래스의 One-Hot 정답값</li>
<li>$z_i$: i번째 클래스의 Softmax 이전 logit</li>
</ul>
<p>예를 들어:</p>
<pre><code class="language-text">예측:
p = [0.23, 0.63, 0.14]

정답:
t = [0, 1, 0]</code></pre>
<p>이라면:</p>
<pre><code class="language-text">gradient = p - t
         = [0.23, -0.37, 0.14]</code></pre>
<p>이다.</p>
<p>Gradient Descent에서는:</p>
<p>$$
z_{\text{new}}
=
z-\eta\frac{\partial L}{\partial z}
$$</p>
<p>여기서 $\eta$는 learning rate다.</p>
<p>정답 클래스는 gradient가 음수이므로 logit이 증가하고, 오답 클래스는 gradient가 양수이므로 logit이 감소한다.</p>
<blockquote>
<p>🎯 <strong>Softmax + Cross Entropy를 사용하면 모델이 정답 확률은 올리고 오답 확률은 내리는 방향의 gradient가 매우 직접적으로 만들어진다.</strong></p>
</blockquote>
<hr>
<h2 id="11-mse">11. MSE</h2>
<p>MSE 계열의 제곱 오차는 <strong>예측값과 정답값의 거리</strong>를 보는 방식이다.</p>
<p>분류 출력을 One-Hot과 비교한다고 하면:</p>
<p>$$
E
=
\frac{1}{2}
\sum_{k=1}^{C}
(y_k-t_k)^2
$$</p>
<p>여기서:</p>
<ul>
<li>$C$: 클래스 개수</li>
<li>$y_k$: k번째 클래스의 예측값</li>
<li>$t_k$: k번째 클래스의 정답 One-Hot 값</li>
</ul>
<p>예를 들어:</p>
<pre><code class="language-text">예측:
[0.1, 0.2, 0.7]

정답:
[0, 0, 1]</code></pre>
<p>이면 3개 값 <strong>모두</strong> 계산한다.</p>
<p>$$
\frac12
\left[
(0.1-0)^2
+
(0.2-0)^2
+
(0.7-1)^2
\right]
$$</p>
<table>
<thead>
<tr>
<th>MSE</th>
<th>Cross Entropy</th>
</tr>
</thead>
<tbody><tr>
<td>예측 벡터와 정답 벡터의 거리</td>
<td>정답 클래스의 확률</td>
</tr>
<tr>
<td>모든 출력의 오차를 계산</td>
<td>One-Hot에서는 정답 항만 남음</td>
</tr>
<tr>
<td>회귀에서 주로 사용</td>
<td>분류에서 주로 사용</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <strong>MSE는 &quot;얼마나 멀리 틀렸는가?&quot;를 보고, Cross Entropy는 &quot;정답에 얼마나 높은 확률을 줬는가?&quot;를 본다고 이해하면 쉽다.</strong></p>
</blockquote>
<hr>
<h2 id="12-데이터-전처리는-왜-할까">12. 데이터 전처리는 왜 할까?</h2>
<p>모델에 들어오는 feature마다 값의 크기가 크게 다르면 학습이 불안정하거나 느려질 수 있다.</p>
<p>그래서 모델이 다루기 쉬운 형태로 데이터를 변환한다.</p>
<p>대표적으로:</p>
<ul>
<li>정규화</li>
<li>표준화</li>
<li>Whitening</li>
</ul>
<p>이 있다.</p>
<hr>
<h2 id="13-정규화---normalization">13. 정규화 - Normalization</h2>
<p>여기서는 흔히 사용하는 <strong>Min-Max Scaling</strong> 기준으로 생각한다.</p>
<p>$$
x&#39;
=
\frac{x-x_{\min}}
{x_{\max}-x_{\min}}
$$</p>
<p>여기서:</p>
<ul>
<li>$x$: 원래 값</li>
<li>$x_{\min}$: 데이터의 최소값</li>
<li>$x_{\max}$: 데이터의 최대값</li>
<li>$x&#39;$: 변환된 값</li>
</ul>
<p>예를 들어 이미지 픽셀은:</p>
<pre><code class="language-text">0 ~ 255</code></pre>
<p>이므로:</p>
<p>$$
x&#39;=\frac{x}{255}
$$</p>
<p>를 하면 <code>0 ~ 1</code> 범위로 변경된다.</p>
<blockquote>
<p>🎯 <strong>왜 쓰는가?</strong><br>데이터의 값 범위를 비슷하게 맞춰 모델이 특정 큰 값에 과도하게 영향을 받는 것을 줄이기 위해서다.</p>
</blockquote>
<hr>
<h2 id="14-표준화---standardization">14. 표준화 - Standardization</h2>
<p>표준화는 데이터를 <strong>평균 0, 표준편차 1</strong>에 가깝게 만든다.</p>
<p>$$
x&#39;
=
\frac{x-\mu}{\sigma}
$$</p>
<p>여기서:</p>
<ul>
<li>$x$: 원래 값</li>
<li>$\mu$: 평균</li>
<li>$\sigma$: 표준편차</li>
<li>$x&#39;$: 표준화된 값</li>
</ul>
<blockquote>
<p>🎯 <strong>왜 쓰는가?</strong><br>Feature마다 평균과 분산이 다른 문제를 줄여서 학습을 안정적으로 만들기 위해서다.</p>
</blockquote>
<hr>
<h2 id="15-whitening">15. Whitening</h2>
<p>Whitening은 표준화보다 한 단계 더 나아간다.</p>
<p>표준화는 각각의 feature의 스케일을 맞추지만, feature끼리의 <strong>상관관계는 남아 있을 수 있다.</strong></p>
<p>Whitening은:</p>
<blockquote>
<p><strong>분산을 맞추고 feature 간 상관관계까지 제거</strong></p>
</blockquote>
<p>하는 것이 목적이다.</p>
<p>먼저 평균을 제거한다.</p>
<p>$$
X_c=X-\mu
$$</p>
<p>여기서:</p>
<ul>
<li>$X$: 전체 데이터</li>
<li>$\mu$: feature별 평균</li>
<li>$X_c$: 평균이 제거된 데이터</li>
</ul>
<p>그리고 공분산 행렬:</p>
<p>$$
\Sigma
=
\frac1N X_c^T X_c
$$</p>
<p>를 구한다.</p>
<p>이를 고유값 분해하면:</p>
<p>$$
\Sigma=U\Lambda U^T
$$</p>
<p>여기서:</p>
<ul>
<li>$U$: 데이터의 새로운 방향을 나타내는 고유벡터</li>
<li>$\Lambda$: 각 방향의 분산을 나타내는 고유값</li>
</ul>
<p>PCA Whitening은:</p>
<p>$$
X_{\text{white}}
=
X_c U\Lambda^{-1/2}
$$</p>
<p>와 같이 수행할 수 있다.</p>
<p>결과적으로:</p>
<p>$$
Cov(X_{\text{white}})\approx I
$$</p>
<p>가 된다.</p>
<p>즉:</p>
<pre><code class="language-text">각 feature의 분산 ≈ 1
feature 사이 상관관계 ≈ 0</code></pre>
<p>가 된다.</p>
<blockquote>
<p>🎯 <strong>왜 쓰는가?</strong><br>Feature 간 중복된 상관관계를 제거하고 각각의 방향을 비슷한 스케일로 만들어 데이터를 더 독립적으로 다루기 위해서다.</p>
</blockquote>
<hr>
<h2 id="16-정규화--표준화--whitening-비교">16. 정규화 / 표준화 / Whitening 비교</h2>
<table>
<thead>
<tr>
<th>방법</th>
<th>핵심 목적</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>정규화</td>
<td>값의 범위를 맞춤</td>
<td>예: <code>0~1</code></td>
</tr>
<tr>
<td>표준화</td>
<td>평균과 분산을 맞춤</td>
<td>평균 <code>0</code>, 표준편차 <code>1</code></td>
</tr>
<tr>
<td>Whitening</td>
<td>스케일 + feature 상관관계 제거</td>
<td>공분산 ≈ 단위행렬</td>
</tr>
</tbody></table>
<p>이미지 딥러닝에서는 흔히:</p>
<pre><code class="language-text">픽셀값 0~255
   ↓
0~1 Scaling
   ↓
채널별 Mean / Std 표준화
   ↓
Neural Network</code></pre>
<p>와 같은 형태를 사용한다.</p>
<hr>
<h2 id="17-전체-흐름-정리">17. 전체 흐름 정리</h2>
<pre><code class="language-text">Model
  ↓
Logit z
  ↓
Softmax
  ↓
Probability p
  ↓
Cross Entropy
  ↓
Loss
  ↓
Gradient 계산
  ↓
Weight Update</code></pre>
<p>Cross Entropy의 배경을 따라가면:</p>
<pre><code class="language-text">실제 정답이 나올 확률을 높이고 싶다.
             ↓
Maximum Likelihood
             ↓
Log-Likelihood
             ↓
Negative Log-Likelihood
             ↓
One-Hot Classification
             ↓
Cross Entropy</code></pre>
<p>그리고 모델에 데이터를 넣기 전에는:</p>
<pre><code class="language-text">Raw Data
   ↓
Normalization / Standardization
   ↓
필요하다면 Whitening
   ↓
Model Input</code></pre>
<p>과 같은 전처리 과정을 사용할 수 있다.</p>
<hr>
<h2 id="18-한-줄-정리">18. 한 줄 정리</h2>
<blockquote>
<p>💡 <strong>Maximum Likelihood는 실제 정답의 확률을 최대화하자는 원칙이고, NLL은 이를 Loss 최소화 형태로 바꾼 것이며, One-Hot 다중분류에서는 Cross Entropy와 같은 형태가 된다. Softmax + Cross Entropy는 gradient가 $p-t$로 깔끔하게 계산되어 분류 학습에 매우 적합하다.</strong></p>
</blockquote>
<p>그리고 전처리는:</p>
<blockquote>
<p>💡 <strong>정규화는 범위, 표준화는 평균과 분산, Whitening은 여기에 feature 간 상관관계까지 조정하는 과정이다.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Python ] Closure와 Decorator]]></title>
            <link>https://velog.io/@min_max0315/Python-Closure%EC%99%80-Decorator</link>
            <guid>https://velog.io/@min_max0315/Python-Closure%EC%99%80-Decorator</guid>
            <pubDate>Tue, 22 Sep 2026 03:58:23 GMT</pubDate>
            <description><![CDATA[<h1 id="til---python-closure와-decorator">TIL - Python Closure와 Decorator</h1>
<p>오늘은 Python의 <strong>Closure(클로저)</strong> 와 <strong>Decorator(데코레이터)</strong> 에 대해 정리한다.</p>
<hr>
<h2 id="1-closure는-왜-사용할까">1. Closure는 왜 사용할까?</h2>
<h3 id="📌-함수의-namespace는-언제까지-유지될까">📌 함수의 Namespace는 언제까지 유지될까?</h3>
<p>Python에서 함수를 호출하면 해당 함수만의 <strong>Local Namespace</strong>가 만들어진다.</p>
<pre><code class="language-python">def example(x):
    y = x + 1
    return y</code></pre>
<p><code>example()</code>이 실행되는 동안에는 <code>x</code>, <code>y</code> 같은 지역 변수가 존재하지만,
함수 실행이 끝나면 일반적으로 해당 호출의 Local Namespace도 더 이상 유지할 필요가 없어진다.</p>
<p>그렇다면 이런 상황은 어떨까?</p>
<blockquote>
<p>💡 <strong>함수가 실행될 때 만들어진 상태를 나중에도 기억해서 사용하고 싶다면?</strong></p>
</blockquote>
<p>이럴 때 사용할 수 있는 개념이 <strong>Closure</strong>다.</p>
<hr>
<h2 id="2-closure란">2. Closure란?</h2>
<h3 id="📌-외부-함수의-값을-기억하는-내부-함수">📌 외부 함수의 값을 기억하는 내부 함수</h3>
<p>Closure는 쉽게 말하면,</p>
<blockquote>
<p><strong>내부 함수가 자신이 만들어질 당시 외부 함수의 변수를 기억하는 구조</strong></p>
</blockquote>
<p>라고 이해할 수 있다.</p>
<pre><code class="language-python">def outer(x):
    def inner():
        return x

    return inner</code></pre>
<p>사용해보면 다음과 같다.</p>
<pre><code class="language-python">func = outer(10)

print(func())
# 10</code></pre>
<p><code>outer(10)</code>의 실행은 이미 끝났지만,
반환된 <code>inner</code> 함수는 자신이 사용하고 있는 <code>x=10</code>을 계속 기억한다.</p>
<h3 id="🚨-정확히는-외부-함수의-namespace-전체를-저장하는-것은-아니다">🚨 정확히는 외부 함수의 Namespace 전체를 저장하는 것은 아니다.</h3>
<p>내부 함수가 <strong>실제로 참조하는 외부 변수(Free Variable)</strong> 를 Closure가 기억한다.</p>
<p>즉,</p>
<pre><code class="language-python">def outer(x):
    a = 100

    def inner():
        return x

    return inner</code></pre>
<p>위 코드에서 <code>inner()</code>는 <code>x</code>만 사용하므로,
Closure에서 중요한 값은 <code>x</code>다.</p>
<hr>
<h2 id="3-closure를-사용하는-이유">3. Closure를 사용하는 이유</h2>
<p>Closure를 사용하면 함수가 특정 상태를 기억하도록 만들 수 있다.</p>
<pre><code class="language-python">def make_multiplier(n):

    def multiply(x):
        return x * n

    return multiply</code></pre>
<pre><code class="language-python">double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))
# 20

print(triple(10))
# 30</code></pre>
<p><code>double</code>은 <code>n=2</code>,
<code>triple</code>은 <code>n=3</code>이라는 상태를 각각 기억한다.</p>
<blockquote>
<p>🎯 <strong>Closure의 핵심</strong></p>
<p>함수가 만들어질 당시의 상태를 기억하고,
이후 호출에서도 그 상태를 계속 사용할 수 있다.</p>
</blockquote>
<p>이 덕분에 전역 변수를 사용하지 않고도
함수마다 독립적인 상태를 유지할 수 있다.</p>
<hr>
<h2 id="4-closure를-이용해-기존-함수에-기능-추가하기">4. Closure를 이용해 기존 함수에 기능 추가하기</h2>
<p>Closure를 활용하면 <strong>다른 함수를 입력받아서 기능을 추가한 새로운 함수를 반환</strong>할 수도 있다.</p>
<p>예를 들어 함수 실행 시간을 측정해보자.</p>
<pre><code class="language-python">import time

def time_decorator(func):

    def wrapper(*args, **kwargs):

        start_time = time.time()

        result = func(*args, **kwargs)

        total_time = time.time() - start_time

        print(f&quot;time : {total_time}&quot;)

        return result

    return wrapper</code></pre>
<p>그리고 원래 함수가 있다고 하자.</p>
<pre><code class="language-python">def custom_add(x, y):
    print(x + y)
    return x + y</code></pre>
<p>이 함수를 <code>time_decorator</code>에 전달한다.</p>
<pre><code class="language-python">external_func = time_decorator(custom_add)

external_func(4, 4)</code></pre>
<p>실행 흐름은 다음과 같다.</p>
<pre><code class="language-text">custom_add
    ↓
time_decorator(custom_add)
    ↓
wrapper 반환
    ↓
external_func = wrapper
    ↓
external_func(4, 4)
    ↓
wrapper(4, 4) 실행
    ↓
custom_add(4, 4) 실행 + 실행시간 측정</code></pre>
<p>여기서 <code>wrapper</code>는 외부 함수인 <code>time_decorator()</code>가 끝난 뒤에도
<code>func</code>, 즉 <code>custom_add</code>를 기억하고 있다.</p>
<p>따라서 <code>wrapper</code>는 <strong>Closure</strong>다.</p>
<blockquote>
<p>💡 <code>wrapper</code>라는 이름은 Python의 특별한 문법이 아니라,
기존 함수를 감싸는 함수에 관례적으로 자주 사용하는 이름이다.</p>
</blockquote>
<hr>
<h2 id="5-그런데-매번-이렇게-써야-할까">5. 그런데 매번 이렇게 써야 할까?</h2>
<p>현재 방식에서는 새로운 기능을 적용할 때마다 아래와 같이 작성해야 한다.</p>
<pre><code class="language-python">external_func = time_decorator(custom_add)
external_func(4, 4)</code></pre>
<p>Python에서는 이 패턴을 더 명확하고 간결하게 표현하기 위해
<strong>Decorator 문법</strong>을 제공한다.</p>
<hr>
<h2 id="6-decorator란">6. Decorator란?</h2>
<h3 id="📌-기존-함수를-수정하지-않고-기능을-추가하는-방법">📌 기존 함수를 수정하지 않고 기능을 추가하는 방법</h3>
<p>Decorator는 한 문장으로 정리하면,</p>
<blockquote>
<p><strong>기존 함수의 코드를 직접 수정하지 않고, 그 함수를 감싸서 새로운 기능을 추가하는 방법</strong></p>
</blockquote>
<p>이다.</p>
<p>다음과 같이 사용할 수 있다.</p>
<pre><code class="language-python">@time_decorator
def custom_mul(x, y):
    print(x * y)
    return x * y</code></pre>
<p>이 코드는 사실 다음 코드와 동일한 의미다.</p>
<pre><code class="language-python">def custom_mul(x, y):
    print(x * y)
    return x * y

custom_mul = time_decorator(custom_mul)</code></pre>
<p>즉,</p>
<pre><code class="language-python">@time_decorator</code></pre>
<p>는 단순히 보기 좋게 만드는 문법만이 아니라,</p>
<p><strong><code>custom_mul</code>을 <code>time_decorator</code>에 전달하고,
반환된 새로운 함수로 <code>custom_mul</code>을 교체한다</strong></p>
<p>는 의미다.</p>
<p>이제 다음과 같이 호출하면 된다.</p>
<pre><code class="language-python">custom_mul(3, 4)</code></pre>
<p>실제로는 <code>time_decorator</code>에서 반환된 <code>wrapper()</code>가 실행되고,
그 안에서 원래 <code>custom_mul()</code>이 실행된다.</p>
<hr>
<h2 id="7-왜-wrapper에서-args-kwargs를-사용할까">7. 왜 wrapper에서 <code>*args</code>, <code>**kwargs</code>를 사용할까?</h2>
<p>Decorator는 어떤 함수에 적용될지 미리 알 수 없다.</p>
<p>예를 들어,</p>
<pre><code class="language-python">def add(x, y):
    ...</code></pre>
<p>일 수도 있고,</p>
<pre><code class="language-python">def user_info(name, age, city=&quot;Seoul&quot;):
    ...</code></pre>
<p>일 수도 있다.</p>
<p>따라서 모든 형태의 인자를 받을 수 있도록 보통 다음과 같이 작성한다.</p>
<pre><code class="language-python">def wrapper(*args, **kwargs):
    return func(*args, **kwargs)</code></pre>
<ul>
<li><code>*args</code> : 위치 인자</li>
<li><code>**kwargs</code> : 키워드 인자</li>
</ul>
<p>이렇게 작성하면 다양한 함수에 동일한 Decorator를 적용할 수 있다.</p>
<hr>
<h2 id="8-함수형-decorator와-closure의-관계">8. 함수형 Decorator와 Closure의 관계</h2>
<p>함수형 Decorator는 보통 Closure 구조를 사용한다.</p>
<pre><code class="language-python">def decorator(func):

    def wrapper(*args, **kwargs):

        # 실행 전 추가 기능

        result = func(*args, **kwargs)

        # 실행 후 추가 기능

        return result

    return wrapper</code></pre>
<p><code>wrapper()</code>는 외부 함수의 <code>func</code>를 기억하고 있으므로 Closure다.</p>
<p>즉,</p>
<blockquote>
<p><strong>Decorator 자체가 Closure인 것은 아니지만,
함수형 Decorator를 구현할 때 Closure가 매우 자주 사용된다.</strong></p>
</blockquote>
<hr>
<h2 id="9-decorator를-class로-만들-수도-있다">9. Decorator를 Class로 만들 수도 있다</h2>
<p>Decorator가 기억해야 할 상태가 많아지면
함수형 Closure 대신 Class를 사용할 수도 있다.</p>
<p>예를 들어 함수가 몇 번 호출되었는지 기록하는 Decorator를 만들어보자.</p>
<pre><code class="language-python">class CallCounter:

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):

        self.count += 1

        print(f&quot;{self.count}번째 호출&quot;)

        return self.func(*args, **kwargs)</code></pre>
<p>사용 방법은 함수형 Decorator와 동일하다.</p>
<pre><code class="language-python">@CallCounter
def hello(name):
    print(f&quot;Hello {name}&quot;)</code></pre>
<pre><code class="language-python">hello(&quot;Min&quot;)
hello(&quot;Max&quot;)</code></pre>
<p>출력 결과는 다음과 같다.</p>
<pre><code class="language-text">1번째 호출
Hello Min

2번째 호출
Hello Max</code></pre>
<p><code>@CallCounter</code>는 사실 다음과 같다.</p>
<pre><code class="language-python">hello = CallCounter(hello)</code></pre>
<p>따라서 <code>hello</code>는 이제 원래 함수가 아니라
<code>CallCounter</code> 객체를 가리킨다.</p>
<p>하지만 <code>CallCounter</code> 클래스에 <code>__call__()</code>이 구현되어 있기 때문에</p>
<pre><code class="language-python">hello(&quot;Min&quot;)</code></pre>
<p>처럼 함수처럼 호출할 수 있다.</p>
<hr>
<h2 id="10-왜-class-decorator를-사용할까">10. 왜 Class Decorator를 사용할까?</h2>
<p>단순한 기능이라면 함수형 Decorator로 충분하다.</p>
<p>하지만 관리해야 할 상태가 많아진다면 Class가 더 편할 수 있다.</p>
<pre><code class="language-python">self.func
self.count
self.config
self.logger</code></pre>
<p>이런 상태들을 <code>self.xxx</code> 형태로 관리할 수 있고,
필요하다면 추가 메서드도 만들 수 있다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 방식</th>
</tr>
</thead>
<tbody><tr>
<td>간단한 기능 추가</td>
<td>함수형 Decorator</td>
</tr>
<tr>
<td>간단한 상태 기억</td>
<td>Closure</td>
</tr>
<tr>
<td>여러 상태를 관리</td>
<td>Class Decorator</td>
</tr>
<tr>
<td>여러 메서드와 복잡한 동작 필요</td>
<td>Class Decorator</td>
</tr>
</tbody></table>
<blockquote>
<p>🎯 Class Decorator만 할 수 있는 특별한 기능이라기보다는,
복잡도가 높아질수록 Class로 구조화하는 것이 관리하기 편해지는 것이다.</p>
</blockquote>
<hr>
<h2 id="11-closure와-decorator-정리">11. Closure와 Decorator 정리</h2>
<h3 id="📌-closure">📌 Closure</h3>
<pre><code class="language-text">외부 함수 실행
    ↓
내부 함수 생성
    ↓
내부 함수가 외부 변수 참조
    ↓
내부 함수 반환
    ↓
외부 함수가 끝난 이후에도 필요한 값을 기억</code></pre>
<p><strong>함수가 자신이 만들어질 당시의 외부 상태를 기억할 수 있게 해주는 구조</strong>다.</p>
<hr>
<h3 id="📌-decorator">📌 Decorator</h3>
<pre><code class="language-text">원래 함수
    ↓
Decorator에 전달
    ↓
기능을 추가한 함수 또는 객체 반환
    ↓
원래 함수 이름에 다시 연결</code></pre>
<p><strong>기존 함수 코드를 수정하지 않고 공통 기능을 추가하는 방법</strong>이다.</p>
<p>대표적으로 다음과 같은 기능에서 사용할 수 있다.</p>
<ul>
<li>실행 시간 측정</li>
<li>로그 기록</li>
<li>권한 검사</li>
<li>호출 횟수 기록</li>
<li>입력값 검증</li>
<li>캐싱</li>
</ul>
<hr>
<h2 id="12-한-줄-정리">12. 한 줄 정리</h2>
<blockquote>
<p>💡 <strong>Closure는 함수가 상태를 기억하게 만드는 방법이고, Decorator는 그 구조 등을 활용하여 기존 함수에 공통 기능을 추가하는 패턴이다.</strong></p>
</blockquote>
<hr>
<h2 id="추가로-알아두면-좋은-점">추가로 알아두면 좋은 점</h2>
<p>실제 Decorator를 작성할 때는 원래 함수의 이름과 문서 정보가 유지되도록
<code>functools.wraps</code>를 사용하는 경우가 많다.</p>
<pre><code class="language-python">from functools import wraps
import time

def time_decorator(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        start_time = time.time()

        result = func(*args, **kwargs)

        print(f&quot;time : {time.time() - start_time}&quot;)

        return result

    return wrapper</code></pre>
<p>이 부분은 Decorator의 기본 원리를 이해한 뒤 추가로 공부하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ TIL ] 네트워크 기초 지식]]></title>
            <link>https://velog.io/@min_max0315/TIL-206%EB%85%84-8%EC%9B%94-11%EC%9D%BC</link>
            <guid>https://velog.io/@min_max0315/TIL-206%EB%85%84-8%EC%9B%94-11%EC%9D%BC</guid>
            <pubDate>Fri, 21 Aug 2026 08:14:39 GMT</pubDate>
            <description><![CDATA[<h1 id="🚀-백엔드--네트워크-코어-지식-요약-노트">🚀 백엔드 &amp; 네트워크 코어 지식 요약 노트</h1>
<h2 id="1-비동기-프로그래밍-기초">1. 비동기 프로그래밍 기초</h2>
<h3 id="📌-프로세스와-스레드">📌 프로세스와 스레드</h3>
<ul>
<li><strong>프로그램 (Program):</strong> 하드디스크에 저장된 정적인 &#39;코드&#39;</li>
<li><strong>프로세스 (Process):</strong> RAM에 올라가 프로그램이 실제로 동작하는 상태. 프로세스는 OS로부터 자기만의 독립된 메모리 공간을 할당받는다. (서로 다른 프로세스는 독립된 메모리 공간에 존재)</li>
<li><strong>스레드 (Thread):</strong> 프로세스 안에서 실제로 일을 진행하는 &#39;일꾼&#39;. <ul>
<li>하나의 프로세스에는 반드시 메인 스레드 하나가 존재하며, 추가로 멀티 스레드를 사용할 수 있다.</li>
<li><strong>동일한 프로세스 내부에서 스레드들은 메모리를 공유한다.</strong></li>
<li>스레드는 그 환경 안에서 현재 어느 명령어를 실행하고 있는지 나타내는 <strong>실행 단위</strong>이다. (즉, CPU는 프로세스를 실행하는 게 아니라, 프로세스에 속한 스레드를 실행한다.)</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>스레드에 존재하는 독립적인 상태들</strong></p>
<ul>
<li><strong>PC (Program Counter):</strong> 다음에 실행할 CPU 명령어의 주소</li>
<li><strong>CPU Register 값:</strong> 현재 계산 중인 값과 호출 상태</li>
<li><strong>스택 포인터와 전용 스택:</strong> 함수 호출 기록, 지역 변수, 반환 주소 등</li>
<li><strong>스레드 ID</strong></li>
<li><strong>스케줄링 정보:</strong> 실행 가능, 실행 중, 대기 중 등의 상태와 우선순위에 대한 정보</li>
<li><strong>Thread Local Storage:</strong> 해당 스레드만 사용하는 데이터</li>
</ul>
</blockquote>
<p><strong>💡 스레드가 필요한 이유</strong>
실행 흐름이 단 하나라면 프로세스 하나로도 충분하지만, 실행 흐름이 여러 개이거나 한 작업이 디스크, 네트워크, 입력 등을 기다리게 되면 전체 작업이 멈추게 된다. 여러 프로세스를 사용하면 메모리 공간을 공유하지 못하는 단점이 있으므로, 스레드 단위로 나누어 하나의 프로세스 안에서 병렬로 작업을 실행하기 위해 필요하다.</p>
<ul>
<li><strong>컨텍스트 스위칭 (Context Switching):</strong> OS가 실행 중인 스레드를 멈추고 레지스터와 Instruction Pointer 등을 저장한 뒤, 다른 스레드에 저장된 상태를 CPU에 올려서 수행하는 기법. (수행 중인 스레드를 교체하는 작업)</li>
</ul>
<hr>
<h3 id="📌-동시성-제어-concurrency-control">📌 동시성 제어 (Concurrency Control)</h3>
<ul>
<li><strong>경쟁 상태 (Race Condition):</strong> 하나의 프로세스에서 여러 스레드가 메모리를 공유하다 보니, Read-Write 과정에서 원하지 않는 타이밍에 컨텍스트 스위칭이 발생해 동기화가 깨지는 현상. (두 스레드가 동시에 공유 메모리에 접근해 값을 변경하려고 할 때 발생하는 치명적인 문제)</li>
<li><strong>동기화 (Synchronization):</strong> Lock이나 Mutex를 사용하여 서로 다른 스레드가 항상 동일한 값을 읽어올 수 있도록 해주는 기법.<ul>
<li><strong>Lock / Mutex:</strong> 공유 메모리를 한 스레드가 사용하는 동안 자물쇠(Lock)를 걸어 다른 스레드가 접근하지 못하게 하여 데이터 오염을 막는다. (다른 스레드는 작업이 끝날 때까지 대기)</li>
<li><strong>Event:</strong> Lock이 끝나는 경우 <code>.set()</code>을 호출해 다른 스레드에게 사용해도 된다는 알림을 전달한다. (<code>.wait()</code> 중이던 스레드들의 작업 재개)</li>
</ul>
</li>
<li><strong>교착 상태 (Deadlock):</strong> A 스레드가 자원 a를 쥐고 b를 대기 중이고, B 스레드가 자원 b를 쥐고 a를 대기하는 경우 둘 다 영원히 대기만 하는 상황.</li>
</ul>
<blockquote>
<p><strong>💀 Deadlock이 발생하는 4가지 필수 조건 (모두 충족 시 발생)</strong></p>
<ol>
<li><strong>상호 배제 (Mutual Exclusion):</strong> 자원을 한 번에 하나의 스레드만 사용 가능해야 한다.</li>
<li><strong>점유와 대기 (Hold and Wait):</strong> 자원을 하나 쥔 상태에서 다른 자원을 기다려야 한다.</li>
<li><strong>비선점 (No Preemption):</strong> 남이 사용 중인 자원을 강제로 가져올 수 없다.</li>
<li><strong>순환 대기 (Circular Wait):</strong> A는 B 자원을, B는 A 자원을 원형으로 물고 기다려야 한다.</li>
</ol>
</blockquote>
<blockquote>
<p><strong>🛡️ Deadlock 해결 전략</strong></p>
<ol>
<li><strong>예방 (Prevention):</strong> 4가지 필수 조건 중 한 가지를 원천 차단하는 방법.</li>
<li><strong>회피 (Avoidance):</strong> 데드락을 미리 계산해서, 안전한 상태인 경우에만 Lock을 빌려준다.</li>
<li><strong>탐지 및 회복 (Detection &amp; Recovery):</strong> 스레드를 자유롭게 풀어두되, 중간중간 데드락 검사를 하여 걸린 스레드 중 하나를 강제 종료시킨다.</li>
</ol>
</blockquote>
<hr>
<h3 id="📌-스레드-상태-관리">📌 스레드 상태 관리</h3>
<p>스레드는 생성부터 소멸까지 효율적인 운영을 위해 OS가 상태 꼬리표를 달아 철저하게 관리한다.</p>
<ul>
<li><strong>생성 (New):</strong> 스레드가 막 만들어진 상태</li>
<li><strong>실행 대기 (Ready):</strong> 당장이라도 일할 준비가 되어 있지만, 아직 CPU를 배정받지 못해 줄을 서 있는 상태</li>
<li><strong>실행 (Running):</strong> 실제로 CPU를 할당받아 열심히 코드를 실행 중인 상태</li>
<li><strong>대기/블로킹 (Blocked/Waiting):</strong> I/O 작업을 요청했거나, 다른 스레드가 Lock을 걸어두어 강제로 일손을 멈추고 기다리는 상태</li>
<li><strong>종료 (Terminated):</strong> 모든 작업을 마치고 스레드가 소멸한 상태</li>
</ul>
<hr>
<h3 id="📌-코루틴과-비동기-제어">📌 코루틴과 비동기 제어</h3>
<ul>
<li><p><strong>코루틴 (Coroutine):</strong> 실행을 중간에 일시 중단했다가 나중에 그 지점부터 다시 재개할 수 있는 함수. (<code>async def</code>로 생성하고, 내부에서 <code>await</code> 키워드를 사용해 제어권을 넘긴다.)</p>
<ul>
<li><strong>vs 멀티 스레드:</strong> 멀티 스레드는 OS가 제어권을 강제로 뺏어가지만(스케줄링), 코루틴은 <strong>단일 스레드</strong> 안에서 애플리케이션(개발자)이 주체가 되어 자발적으로 제어권을 양보한다. 따라서 동기화 문제가 심각하지 않다.</li>
</ul>
</li>
<li><p><strong>GIL (Global Interpreter Lock):</strong> CPython 인터프리터가 &quot;파이썬 바이트코드는 프로세스당 한 번에 한 스레드만 실행한다&quot;고 강제하는 락(Lock). OS로부터 다중 코어를 할당받아도, 실행권은 단 하나의 스레드에게만 부여된다.</p>
<ul>
<li><em>작동 예시:</em> A 스레드가 GIL 통제권을 얻어 실행 중 I/O 작업을 만나면 GIL을 내려놓는다. 이때 B 스레드가 GIL을 가져가 실행된다. A가 I/O를 끝내도 B가 GIL을 반환할 때까지 대기해야 한다. 스레드들은 자신만의 독립적인 스택(메모리)을 가지므로 한 프로세스 내에서도 독립적인 수행이 가능하다.</li>
</ul>
</li>
<li><p><strong>블로킹 (Blocking) vs 논블로킹 (Non-blocking):</strong> 스레드의 &#39;제어권&#39;에 초점을 맞춘 개념.</p>
<ul>
<li><strong>블로킹:</strong> 스레드가 I/O 작업을 요청(System call)하면, 결과를 반환할 때까지 호출된 쪽이 제어권을 쥐고 돌려주지 않아 스레드가 아무 작업도 못 하고 멈춰버리는 상태.</li>
<li><strong>논블로킹:</strong> I/O 작업을 요청하고 OS가 계속 진행하라는 결과를 반환하면 곧바로 다음 코드를 진행.</li>
</ul>
</li>
<li><p><strong>동기 (Sync) vs 비동기 (Async):</strong> 작업의 &#39;순서와 결과&#39;에 초점을 맞춘 개념.</p>
<ul>
<li><strong>동기:</strong> 특정 결과를 받아야지만 다음 작업으로 진행되는 경우.</li>
<li><strong>비동기:</strong> 특정 결과를 기다리지 않고 다음 작업으로 넘어가는 경우.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>🐍 Python에서의 사용</strong>
일반 <code>def</code>는 동기(Sync)로 동작하며, <code>async def</code>와 <code>await</code> 키워드를 사용해 비동기를 처리한다. <code>await</code>는 내부적으로 <strong>Yield</strong> 메커니즘을 사용하여, 결과를 반환할 때까지 현재 진행 상황을 저장해 두고 다른 작업을 수행할 수 있도록 제어권을 넘겨준다.</p>
</blockquote>
<hr>
<h2 id="2-asgi-서버와-uvicorn의-정체">2. ASGI 서버와 Uvicorn의 정체</h2>
<ul>
<li><strong>WSGI (Web Server Gateway Interface):</strong> 과거 파이썬 웹 생태계(Flask, Django 등)에서 사용하던 동기(Sync) 방식의 규격. 클라이언트 1명당 1개의 스레드를 할당하여, DB 조회나 딥러닝 연산 등 I/O 대기 시 블로킹(대기)이 발생한다. 접속자가 많아지면 서버가 멈추거나 터지는 한계가 있었다.</li>
<li><strong>ASGI (Asynchronous Server Gateway Interface):</strong> 위 문제를 해결하기 위해 등장한 비동기 인터페이스 (예: FastAPI). 기다리는 시간 동안 스레드의 제어권을 넘겨 다른 클라이언트의 요청을 동시에 처리할 수 있다.</li>
<li><strong>Uvicorn:</strong> FastAPI 같은 ASGI 프레임워크로 작성된 코드를 실제로 실행해 주는 엔진(웹 서버).</li>
</ul>
<blockquote>
<p><strong>💡 네트워크 요청 처리 파이프라인 예시</strong></p>
<ol>
<li>사용자가 스마트폰 브라우저에서 로그인 버튼을 눌러 HTTP 요청서를 보낸다.
<code>(예: POST /login HTTP/1.1 Host: www.mysite.com {&quot;user_id&quot;: &quot;hello&quot;, &quot;password&quot;: &quot;1234&quot;})</code></li>
<li>특정 포트로 도착한 데이터를 OS가 받아 HTTP 요청서로 재조립한다.</li>
<li>이때 가장 먼저 마중 나오는 것이 <strong>웹 서버 (Uvicorn)</strong>이다.</li>
<li>Uvicorn이 날것의 HTTP 데이터를 파이썬이 다루기 쉬운 Dictionary 형태로 번역해 준다 (ASGI 규격).</li>
<li>포장된 데이터를 FastAPI에게 넘기면, 개발자가 작성한 <code>@app.post(&#39;/login&#39;)</code> 라우팅을 타고 로직이 실행된다.</li>
</ol>
</blockquote>
<hr>
<h2 id="3-네트워크--서버-인프라-핵심">3. 네트워크 &amp; 서버 인프라 핵심</h2>
<h3 id="📌-ip-주소와-포트">📌 IP 주소와 포트</h3>
<ul>
<li><strong>공인 IP (Public IP):</strong> 전 세계 어디서든 IP만 알면 정확히 찾아올 수 있는 고유한 대표 주소. (통신사에서 부여)</li>
<li><strong>사내/사설 IP (Private IP):</strong> 공유기 내부(가정, 회사)에서만 사용하는 내부망 주소 (예: <code>192.168.x.x</code>). 외부 인터넷망에서 직접 찾아올 수 없다.<ul>
<li><em>분리 이유:</em> 전 세계 IP 주소 고갈 문제를 해결하기 위해, 건물 대표 주소(공인 IP) 1개를 두고 공유기가 내부망에 내선 번호(사설 IP)를 뿌려주는 방식을 채택.</li>
</ul>
</li>
<li><strong>IP와 랜카드의 관계:</strong> PC 1대당 IP가 무조건 1개인 것은 아니다. 네트워크 인터페이스(랜카드)마다 1개씩 부여되므로 유선/무선 랜을 동시 사용 시 여러 개의 IP를 가질 수 있다.</li>
<li><strong>포트 (Port):</strong> 1개의 IP 안에는 서비스별로 연결할 수 있는 방(포트)이 65,535개 존재한다.</li>
</ul>
<h3 id="📌-포트-포워딩-port-forwarding">📌 포트 포워딩 (Port Forwarding)</h3>
<p>외부에서 공인 IP를 치고 들어왔을 때, 공유기가 수많은 사설 IP 중 목적지를 찾을 수 있도록 미리 설정해 두는 &#39;안내 장부&#39;.</p>
<ul>
<li><strong>동작 원리:</strong> 외부에서 <code>공인IP:8000</code> 접속 요청 ➡ 공유기가 수신 ➡ 설정된 규칙에 따라 <code>내부IP(192.168.0.10):8000</code>으로 요청 전달.</li>
<li><strong>설정 3요소:</strong> <code>외부 포트</code>(접속용 창구 번호), <code>내부 IP</code>(수신할 PC 주소), <code>내부 포트</code>(PC 안에서 대기 중인 방 번호).</li>
</ul>
<h3 id="📌-서버-오픈을-위한-3단계-inbound">📌 서버 오픈을 위한 3단계 (Inbound)</h3>
<ol>
<li><strong>포트 바인딩 (Listening):</strong> 파이썬 코드 등을 실행해 OS에게 &quot;이 포트 쓸 테니 열어줘!&quot;라고 요청하고 대기.</li>
<li><strong>방화벽 (Firewall) 허용:</strong> OS 자체의 보안 경호원인 방화벽(ufw 등)에서 해당 포트의 외부 접근을 허용하는 명령어 실행.</li>
<li><strong>포트 포워딩:</strong> 공유기를 거치는 환경일 경우 관리자 페이지에 포워딩 규칙 작성.</li>
</ol>
<h3 id="📌-데이터-이동-흐름-outbound">📌 데이터 이동 흐름 (Outbound)</h3>
<p>내 PC에서 외부 인터넷(구글 등)으로 데이터를 보낼 때의 흐름:</p>
<ol>
<li><strong>출발:</strong> <code>192.168.0.10</code> (내 PC에서 패킷 생성)</li>
<li><strong>안내데스크:</strong> 내부망을 타고 공유기(<code>192.168.0.1</code>) 도착</li>
<li><strong>NAT (주소 변환):</strong> 공유기가 사설 IP를 숨기고 &#39;공인 IP&#39;로 발신자 스티커를 교체</li>
<li><strong>도착:</strong> 인터넷 세상을 거쳐 목적지에 도달</li>
</ol>
<p><img src="https://velog.velcdn.com/images/min_max0315/post/28d758c0-1330-4e45-babc-aa2ee7986ab3/image.png" alt="네트워크 흐름도"></p>
<hr>
<h2 id="4-웹소켓과-상태-유지">4. 웹소켓과 상태 유지</h2>
<p>컴퓨터들이 소통하는 방식은 크게 무상태(Stateless)와 상태 유지(Stateful)로 나뉜다.</p>
<ul>
<li><strong>HTTP (Stateless):</strong> 매 요청마다 인증서나 신분증을 같이 제공해야 하며, 서버가 클라이언트의 상태를 기억하지 않는다. 단방향 통신. (80번/443번 포트 사용)</li>
<li><strong>WebSocket (Stateful):</strong> 서버가 클라이언트가 누구인지 지속적으로 기억하며, 실시간 양방향 통신이 가능하다. (HTTP와 동일하게 80번/443번 포트를 공유)</li>
</ul>
<blockquote>
<p><strong>🔄 웹소켓 연결 과정 (4단계)</strong></p>
<ol>
<li><strong>Handshake:</strong> 클라이언트가 서버에게 연결 요청 (처음엔 HTTP 방식 사용).</li>
<li><strong>연결 수락 및 업그레이드:</strong> 서버가 수락하면 프로토콜을 업그레이드하여 둘 사이에 전용 통로가 뚫린다.</li>
<li><strong>상태 유지 및 실시간 소통:</strong> 통로 끝에 누가 있는지 기억(상태 유지). 클라이언트가 새로고침(요청)을 하지 않더라도 서버가 통로를 통해 즉각적으로 데이터를 밀어 넣어줄 수 있다.</li>
<li><strong>연결 종료:</strong> 작업이 끝나면 연결된 통로를 끊는다.</li>
</ol>
</blockquote>
<h3 id="📌-기저의-통신-프로토콜-tcp--udp">📌 기저의 통신 프로토콜 (TCP / UDP)</h3>
<ul>
<li><strong>TCP (Transmission Control Protocol):</strong> 데이터를 가장 안전하고 확실하게 배달하는 통신 시스템. 웹 브라우징, 웹소켓 등 신뢰성이 중요한 곳에 쓰인다.<ul>
<li>3-Way Handshake를 통해 상대방의 준비 상태를 미리 확인.</li>
<li>데이터 유실 시 재전송 및 순서 조립 보장.</li>
</ul>
</li>
<li><strong>UDP (User Datagram Protocol):</strong> TCP와 다르게 연결 확인이나 재배송을 하지 않고 데이터를 빠르게 냅다 던지는 방식. 속도가 생명인 유튜브 라이브 스트리밍이나 온라인 FPS 게임 등에서 주로 사용한다.</li>
</ul>
<h2 id="5-파이썬-동시성--병렬성-심화-concurrency-vs-parallelism">5. 파이썬 동시성 &amp; 병렬성 심화 (Concurrency vs Parallelism)</h2>
<h3 id="📌-3가지-동시성-처리-방식의-본질적인-차이">📌 3가지 동시성 처리 방식의 본질적인 차이</h3>
<p>많은 개발자가 헷갈리는 &#39;비동기&#39;, &#39;멀티 스레드&#39;, &#39;멀티 프로세스&#39;의 개념은 <strong>작업자(스레드)의 수</strong>와 <strong>메모리 공유 여부</strong>, 그리고 <strong>주체</strong>에 따라 명확히 구분됩니다.</p>
<ol>
<li><strong>비동기 (Asyncio) : &quot;1인 주방의 달인&quot;</strong><ul>
<li><strong>스레드 1개</strong> 안에서 일어나는 기술입니다. </li>
<li>요리사(스레드)가 스스로 I/O 대기(물 끓이기 등) 시간을 파악하고, 기다리는 동안 멈춰있지 않고 다른 작업(야채 썰기)으로 <strong>스스로 제어권을 양보</strong>하며 일을 계속합니다.</li>
<li>OS가 개입하는 강제 교체(Context Switching)가 없어서 매우 가볍고 빠르며, 메모리 공유 문제(Race Condition)도 적습니다.</li>
</ul>
</li>
<li><strong>멀티 스레딩 (Multi-threading) : &quot;한 주방 안의 여러 요리사&quot;</strong><ul>
<li>하나의 프로세스(주방) 안에서 메모리(도마)를 공유하며 여러 스레드가 작업합니다.</li>
<li>OS가 강제로 스레드를 번갈아 가며 실행시킵니다.</li>
<li><strong>파이썬의 한계 (GIL):</strong> 파이썬은 GIL(Global Interpreter Lock) 때문에 코어가 아무리 많아도 파이썬 바이트코드는 한 번에 하나의 스레드만 실행할 수 있습니다. 즉, 진정한 병렬 처리가 아니라 아주 빠르게 번갈아 실행되는 <strong>&#39;가짜 동시성(Concurrency)&#39;</strong>만 지원합니다.</li>
</ul>
</li>
<li><strong>멀티 프로세싱 (Multi-processing) : &quot;주방 건물을 통째로 복제&quot;</strong><ul>
<li>스레드를 늘리는 것이 아니라, 독립된 메모리를 가진 <strong>프로세스 자체를 N개로 복제</strong>합니다.</li>
<li>프로세스마다 자신만의 GIL을 따로 가지므로, GIL의 통제를 완전히 우회하여 물리적인 <strong>&#39;진짜 병렬 처리(Parallelism)&#39;</strong>가 가능해집니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="📌-멀티-프로세싱의-황금률과-치명적-오해">📌 멀티 프로세싱의 황금률과 치명적 오해</h3>
<h4 id="🚨-1-싱글-코어cpu-1개에서-멀티-프로세싱을-쓰면-최악인-이유">🚨 1. 싱글 코어(CPU 1개)에서 멀티 프로세싱을 쓰면 최악인 이유</h4>
<p>CPU 코어가 1개일 때 멀티 프로세싱을 시도하면 병렬성의 장점은 사라지고 최악의 비효율이 발생합니다.</p>
<ul>
<li><strong>막대한 컨텍스트 스위칭 오버헤드:</strong> 코어(진짜 요리사)는 1명인데 주방 건물(프로세스)만 4개인 상황입니다. 요리사가 이 건물 저 건물을 뛰어다니느라(프로세스 교체 작업) 정작 요리할 시간을 다 버리게 됩니다. 스레드 교체보다 훨씬 무거운 비용이 발생합니다.</li>
<li><strong>극심한 메모리 낭비:</strong> 같은 프로그램을 복제하므로 RAM 용량만 헛되이 낭비하게 됩니다. 오히려 프로세스 1개로 순서대로 처리하는 것보다 속도가 느려집니다.</li>
</ul>
<h4 id="🎯-2-올바른-프로세스-개수-설정--코어-수에-맞춰라">🎯 2. 올바른 프로세스 개수 설정 : &quot;코어 수에 맞춰라!&quot;</h4>
<p>멀티 프로세싱의 성능을 100% 끌어내려면 <strong>생성하는 프로세스의 수를 내 컴퓨터의 논리적 CPU 코어 수와 일치시키는 것</strong>이 가장 이상적입니다.</p>
<ul>
<li><strong>프로세스 &lt; 코어 수:</strong> 놀고 있는 잉여 코어가 생겨 자원이 낭비됩니다.</li>
<li><strong>프로세스 &gt; 코어 수:</strong> 코어가 프로세스를 번갈아 맡아야 하므로 다시 컨텍스트 스위칭 오버헤드가 발생해 속도가 떨어집니다.</li>
</ul>
<blockquote>
<p><strong>💡 실전 개발 팁 (권장 프로세스 수 = 코어 수 - 1)</strong>
내 PC의 코어가 8개라고 프로세스를 딱 8개 띄우면, 파이썬이 CPU를 100% 점유해 버려 OS(윈도우 등)나 마우스 커서마저 멈추는 현상이 발생할 수 있습니다. 
따라서 <strong><code>multiprocessing.cpu_count() - 1</code></strong> 공식을 사용하여, 코어 1개 정도는 OS와 백그라운드 프로그램을 위해 숨통을 틔워주는 것이 실무적인 표준입니다.</p>
</blockquote>
<hr>
<h3 id="📌-동시성-제어-기술-한눈에-보기">📌 동시성 제어 기술 한눈에 보기</h3>
<table>
<thead>
<tr>
<th align="left">비교 항목</th>
<th align="left">멀티 스레딩 (Multi-threading)</th>
<th align="left">비동기 (Asyncio)</th>
<th align="left">멀티 프로세싱 (Multi-processing)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>작업자 수</strong></td>
<td align="left">1개 주방, <strong>여러 명</strong>의 요리사 (스레드 N개)</td>
<td align="left">1개 주방, <strong>단 1명</strong>의 요리사 (스레드 1개)</td>
<td align="left"><strong>주방 건물 N개</strong>, 요리사 N명 (프로세스 N개)</td>
</tr>
<tr>
<td align="left"><strong>메모리 상태</strong></td>
<td align="left"><strong>공유함</strong> (Race Condition 주의)</td>
<td align="left">공유함 (단일 스레드라 통제 쉬움)</td>
<td align="left"><strong>독립됨</strong> (데이터 공유 어렵고 RAM 소모 큼)</td>
</tr>
<tr>
<td align="left"><strong>적합한 작업</strong></td>
<td align="left">파일 읽기, 가벼운 대기 작업</td>
<td align="left"><strong>네트워크 API, DB 통신 (I/O 대기 최적화)</strong></td>
<td align="left">영상 처리, 모델 훈련 등 <strong>무거운 수학적 연산 (CPU 최적화)</strong></td>
</tr>
<tr>
<td align="left"><strong>핵심 원리</strong></td>
<td align="left">OS가 강제로 스레드를 번갈아 교체</td>
<td align="left">스레드 스스로 대기 시간에 다른 작업 수행</td>
<td align="left">물리적으로 여러 코어에서 진짜 동시 실행 (GIL 우회)</td>
</tr>
</tbody></table>
]]></description>
        </item>
    </channel>
</rss>