<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>inhwaaa_v.log</title>
        <link>https://velog.io/</link>
        <description>얼렁뚱땅 바보 학부생...</description>
        <lastBuildDate>Thu, 01 Jan 2026 15:28:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>inhwaaa_v.log</title>
            <url>https://velog.velcdn.com/images/inhwaaa_v/profile/a36d6a6e-5811-44db-8709-0a1487503af4/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. inhwaaa_v.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/inhwaaa_v" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[KNN, K-Means]]></title>
            <link>https://velog.io/@inhwaaa_v/KNN-K-Means</link>
            <guid>https://velog.io/@inhwaaa_v/KNN-K-Means</guid>
            <pubDate>Thu, 01 Jan 2026 15:28:33 GMT</pubDate>
            <description><![CDATA[<h1 id="knn-알고리즘-k-nearest-neighbor">kNN 알고리즘 (k-Nearest Neighbor)</h1>
<p>&nbsp;kNN(k-Nearest Neighbor) 알고리즘은 k개의 가장 가까운 이웃 데이터를 찾고, 그 이웃들의 레이블을 기반으로 분류/회귀 문제를 해결하는 알고리즘을 의미한다.</p>
<p>&nbsp;kNN 알고리즘은 새 데이터가 입력으로 들어오면, 이 포인트가 어떤 클래스에 속해있는지를 찾는다. 이를 위해 <strong>k개의 이웃을 찾</strong>고, <strong>이웃 중 가장 많은 클래스에 속한 레이블을 해당하는 데이터 포인트에 속하는 클래스라고 지정</strong>한다. 이때, <strong>k 값에 따라 결과가 달라질 수 있</strong>으며, 데이터의 분포와 패턴이 명확할 때 더 잘 작동하는 경향이 있다. k 값은 일반적으로 홀수를 선택한다. (짝수로 선택하게 되면 Voting 시 동점이 발생할 수 있기 때문이다.)</p>
<p>&nbsp;kNN 알고리즘은 데이터 포인트가 가장 많이 속한 클래스로 분류를 수행한다. 따라서 특징 공간에 있는 모든 데이터에 대한 정보가 필요하다. 이러한 점 때문에 모든 학습 데이터를 저장해야 해서 메모리의 양이 크고, 기존 모든 포인트와의 모든 거리를 계산해야 해서 계산 복잡도가 크다는 문제점이 있어 데이터와 클래스가 많을수록 효율성이 낮아진다. 하지만, 직관적이고 이해하기 쉬우며, 사전 학습 시간(학습 시간)이 필요하지 않다는 장점이 있다.</p>
<ul>
<li><p><strong>핵심</strong> : 거리 기반 이웃 찾기, <strong>kNN은 학습하는 알고리즘이 아니다! 거리 기반 계산임.</strong>
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/07dfe52c-7b32-4de8-8a9e-136b8f1cd44e/image.png" alt=""></p>
</li>
<li><p><strong>정의</strong> : 새로운 샘플 x에 대해 학습 데이터의 가장 가까운 k개 이웃의 다수결(분류) 또는 평균(회귀)으로 예측하는 비모수, 메모리 기반(lazy) 알고리즘</p>
<ul>
<li><strong>비모수(non-parameter)</strong> : 모집단이 특정 분포를 따른다는 가정을 하지 않고 통계적 분석을 하는 방법. 다시 말해, 데이터의 분포나 함수 형태에 대해 특정한 가정을 두지 않는 것.</li>
<li>예를 들어 선형 회귀 모델의 경우엔 $y=ax+b$와 같이 미리 정해진 형태를 가정하므로 모수적이다. 하지만, kNN은 모델 파라미터가 고정되어 있지 않고, 어떠한 함수 형태나 파라미터를 학습하지 않으므로 비모수적이라고 할 수 있다.</li>
</ul>
</li>
<li><p><strong>거리 척도</strong> : 기본은 유클리드 거리($ℓ_2$)임. 대안 : 맨해튼($ℓ_1$), 민코프스키($ℓ_p$), 코사인 등.</p>
</li>
</ul>
<p>$$
d(\mathbf{x}, \mathbf{x_i}) = \sqrt{\sum_{j=1}^{d} (x_j - x_{ij})^2}
$$</p>
<ul>
<li><strong>가중 kNN</strong> : <code>weights=&#39;distance&#39;</code>로 가까운 이웃에 더 큰 가중치를 부여하는 것</li>
<li><strong>하이퍼파라미터</strong> : <code>k</code>, <code>metric</code>(거리 측정 방법), <code>weights</code>(가중치 결정 방법, uniform - 동일 가중치, distance - 거리에 반비례하는 가중치), <code>p</code>(민코프스키 차수)<ul>
<li><code>weights</code>를 distance로 부여하면, 거리에 반비례하는 가중치를 부여하므로 예측값은 더 가까운 이웃에 더 큰 영향을 받게 됨. (가까운 거리에 더 큰 가중치를 부여하게 되므로)</li>
<li>Bias-Variance Trade-off : k↓ → 경계 세밀(분산↑, 과적합), k↑ → 경계 매끈 (편향↑, 과소적합)</li>
<li>전처리 : 거리 기반이므로 스케일 영향이 큼 → 표준화/정규화 권장</li>
<li>복잡도 : 학습 비용 작음, 예측 시 $O(n⋅d)$<ul>
<li>대량 데이터면 KD-Tree/볼-트리/근사 최근접 탐색 고려</li>
</ul>
</li>
</ul>
</li>
<li><strong>유클리드 거리</strong> : 각 차원의 차를 제곱해서 사용하는 것으로, n차원에서 두 점 사이의 거리를 구할 경우 다음과 같이 나타냄.</li>
</ul>
<p>$$
\sqrt{(a_1 - b_1)^2 + (a_2 - b_2)^2 + \cdots + (a_n - b_n)^2}
$$</p>
<ul>
<li><strong>맨해튼 거리</strong> : 각 차원의 차를 제곱해서 사용하는 것이 아닌, 절댓값을 바로 합산하는 것을 의미하며, 맨해튼 거리는 항상 유클리드 거리보다 크거나 같다.</li>
</ul>
<p>$$
|a_1 - b_1| + |a_2 - b_2| + \cdots + |a_n - b_n|
$$</p>
<ul>
<li><strong>민코프스키 거리</strong> : 유클리드 거리와 맨해튼 거리를 일반화한 거리로, 차이의 절댓값에 거듭제곱을 취하고 다시 루트를 적용한 형태임.<ul>
<li>p=1일 경우 맨해튼 거리, p=2일 경우 유클리드 거리, p=$\infty$일 경우 체비쇼프 거리와 동일함.</li>
</ul>
</li>
</ul>
<p>$$
(|a_1 - b_1|^p + |a_2 - b_2|^p + \cdots + |a_n - b_n|^p)^{\frac{1}{p}}
$$</p>
<pre><code>knn = KNeighborsClassifier(n_neighbors=6)
knn.fit(X_train, y_train)

y_pred = knn.predict(X_test)
scores = metrics.accuracy_score(y_test, y_pred)</code></pre><pre><code># kNN은 전처리의 영향을 많이 받는다.
# 표준화 전이 표준화 후보다 성능이 더 높음.
# 표준화는 무조건 해야 한다 X, 성능을 보고 무엇(하는 것 or 안 하는 것)이 더 좋은지 판단해야 함.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score, KFold

# shuffle은 데이터를 섞는 것으로, 학습 시 순서에 의존하는 것을 방지해줌.
cv = KFold(n_splits=5, shuffle=True, random_state=42) # n_splits=5이므로 5-Fold CV
raw_model = KNeighborsClassifier(n_neighbors=5)

# cross_val_score(estimator, X, y, cv=None) : 교차 검증을 통해 점수를 평가하는 교차 평가 함수
score_raw = cross_val_score(raw_model, X, y, cv=cv).mean() # 정규화 전 CV Score

# 파이프라인이란, 전처리 → 학습 → 평가로 이어지는 일련의 과정들을 하나로 묶는 것임.
pipe = Pipeline([(&#39;scaler&#39;, StandardScaler()), (&#39;knn&#39;, KNeighborsClassifier(n_neighbors=5))]) # 정규화 후 knn
score_scaled = cross_val_score(pipe, X, y, cv = cv).mean() # 정규화 후 CV Score

print(&quot;CV 정확도 (무스케일) : &quot;, score_raw)
print(&quot;CV 정확도 (표준화) : &quot;, score_scaled)</code></pre><h2 id="머신러닝-성능-평가">머신러닝 성능 평가</h2>
<h3 id="confusion-matrix-혼동-행렬">Confusion Matrix (혼동 행렬)</h3>
<ul>
<li><strong>혼동 행렬</strong> : 예측값과 실제값 사이 관계를 행렬 형태로 표현한 것</li>
<li>F1 Score는 정밀도와 재현율의 조화평균을 의미하며, 한쪽 클래스에 치우친(클래스 편향이 있을 때) 예측 성능을 과대평가하지 않도록 하여 불균형 데이터에서의 평가 편향을 완화한다. (→ 편향된 모델을 좋은 모델로 평가하지 않는다)</li>
<li>정밀도와 재현율 중 하나라도 작으면, 그 값의 역수가 커져 조화평균(F1)이 크게 감소한다. F1은 Precision과 Recall 중 작은 값에 의해 지배된다.</li>
<li>조화 평균은 역수의 산술평균의 역수를 의미한다. 역수의 차원에서 평균을 구하고, 다시 역수를 취해 원래 차원의 값으로 되돌리는 것이다. $x = \frac {2ab}{a+b}$와 같이 표현한다.</li>
</ul>
<p><strong>결과 해석</strong></p>
<ul>
<li><strong>정확도</strong> : 전체 문제 중에서 정답을 맞춘 비율<ul>
<li>Accuracy = TP + TN / TP + TN + FP + FN</li>
</ul>
</li>
<li><strong>정밀도 (Precision)</strong> : True라고 분류한 것 중 실제로 True인 것의 비율 (Positive 정답률)<ul>
<li>Precision = TP(True Positive) / (TP(True Positive) + FP(False positive))</li>
</ul>
</li>
<li><strong>재현율 (Recall)</strong> : 실제 True인 것 중에서 True라고 예측한 것의 비율<ul>
<li>Recall = TP(True Positive) / (TP(True Positive) + FN(False Negative))</li>
<li>Sensitivity 혹은 Hit Rate라고도 불림.</li>
</ul>
</li>
<li><strong>F1-score</strong> : Precision과 Recall은 상호보완적이기 때문에, Recall을 올리면 Precision이 내려가고, Precision을 올리면 Recall이 내려갈 수 밖에 없다. 이를 보완하기 위해 생겨난 것이 Recall과 Precision의 조화평균인 F1 score이다.<ul>
<li>Precision과 Recall의 조화평균으로 0.0~ 1.0 사이의 값을 가지며, 값이 1에 가까울수록 좋은 모델이다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/5fe00924-063e-40fc-abba-8faeebff9f34/image.png" alt=""></p>
<h3 id="분류-리포트">분류 리포트</h3>
<ul>
<li><strong>macro avg</strong> : 각 클래스의 성능 지표를 <strong>동일한 가중치로 평균</strong>한 값<ul>
<li>모든 클래스를 동일하게 중요하게 취급하므로 클래스 불균형이 심하면 낮게 나오는 경향</li>
</ul>
</li>
<li><strong>weighted avg</strong> : 각 클래스 성능을 <strong>해당 클래스 샘플 수(support)로 가중 평균</strong><ul>
<li>데이터 분포를 반영한 평균이므로 클래스 불균형에 둔감함.</li>
</ul>
</li>
</ul>
<pre><code># Confusion Matrix를 판단할 땐, 대각 성분을 봐야 한다.
# Confusion Matrix의 x축은 모델, y축은 정답을 의미함.
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.metrics import ConfusionMatrixDisplay

digits = load_digits() # 데이터 로드
n_samples = len(digits.images) # 데이터 개수
# knn은 이미지 형태 그대로 처리가 불가능함. (거리 계산을 해야 하기 때문)
# 따라서 reshape을 통해 이미지를 1차원 벡터로 펼침.
# shape 출력 결과를 보면 알 수 있듯이, 8*8 이미지 -&gt; 64 길이 벡터로 처리하여 knn이 처리 가능하도록 함
print(digits.images.shape)
data = digits.images.reshape((n_samples, -1))
print(data.shape)

Xd_tr, Xd_te, yd_tr, yd_te = train_test_split(data, digits.target, test_size=0.2, random_state=0) # train, test 데이터 분할
knn_d = KNeighborsClassifier(n_neighbors=6) # knn 알고리즘
knn_d.fit(Xd_tr, yd_tr) # 학습
yd_pred = knn_d.predict(Xd_te) # 예측
print(&quot;Digits 정확도 : &quot;, accuracy_score(yd_te, yd_pred))

# ConfusionMatrixDisplay.from_estimator(추정치, X, y) : 추정치, 데이터, 레이블이 주어졌을 때 혼동 행렬을 그리는 함수
# 실제 레이블과 예측 레이블만이 주어졌을 땐 ConfusionMatrixDisplay.from_predictions(y_true, y_pred)
disp = ConfusionMatrixDisplay.from_estimator(knn_d, Xd_te, yd_te) # knn 예측 결과(추정치) 기반 실제 정답과 비교한 혼동 행렬을 그림
plt.title(&quot;Digits Confusion Matrix (KNN)&quot;)
plt.show()</code></pre><h1 id="k-means-클러스터링">K-Means 클러스터링</h1>
<p>&nbsp;K-Means 클러스터링은 레이블이 없는 데이터를 학습하는 비지도 학습의 가장 대표적인 알고리즘이다. 이는 주어진 n개의 관측 값을 k개의 클러스터로 분할하며, 관측 값들은 거리가 최소인 클러스터로 분류된다.</p>
<p>&nbsp;K-Means 클러스터링 알고리즘은 초기 중심점 위치에 따라 결과가 크게 달라지고, 때로는 도메인 지식에 따라서도 결과가 달라질 수 있다. 따라서 여러 번 중심점(centroid)를 설정해 최적의 값을 찾는데, 이것의 대표적인 방법에는 Elbow method와 Silhouette analysis가 있다.</p>
<p>&nbsp;이는 많은 데이터셋에 대해서도 클러스터링 성능이 좋다는 장점이 있다. 그러나, 클러스터 모양이 복잡하거나 크기가 다르면 제한적인 성능을 보이므로 다른 알고리즘을 고려해야 한다.</p>
<p>&nbsp;K-means 클러스터링은 군집 중심점(centroid)라는 특정한 k개의 임의 지점을 선택해 해당 중심에 가장 가까운 포인트들을 선택하는 군집화 기법으로, 다음과 같은 과정에 의해 수행된다.</p>
<ol>
<li>초기 중심점 설정</li>
<li>중심점을 선택된 포인트들의 평균 지점으로 이동함.</li>
<li>이동된 중심점에서 다시 가까운 포인트를 선택함.</li>
<li>다시 중심점을 평균 지점으로 이동하는 프로세스를 반복적으로 수행함.</li>
</ol>
<ul>
<li><p><strong>정의</strong> : k개의 중심(centroid)를 추정하여 각 샘플을 가장 가까운 중심에 할당하고, 중심을 재계산하는 과정을 반복하여 군집 내 제곱합(SSE)를 최소화함.</p>
</li>
<li><p><strong>목표 함수</strong></p>
<p>  $$
  J = \sum_{i=1}^{k} \sum_{x_j \in C_i} | \mathbf{x}_j - \boldsymbol{\mu}_i |^2 
  $$</p>
</li>
</ul>
<ul>
<li><strong>알고리즘</strong> : 초기 중심 설정 → 할당 단계 (closest centroid) → 업데이트 단계 (평균 재계산) → 수렴 시 종료<ul>
<li><strong>주의</strong> : 초기값 민감성(기본 k-means++), 비구형/밀도 가변 데이터에서 한계 → <strong>대안</strong> : DBSCAN, GMM</li>
</ul>
</li>
<li>kNN 알고리즘은 학습을 하지 않는 알고리즘이지만, K-Means 알고리즘은 학습을 함.</li>
</ul>
<pre><code>import numpy as np
from sklearn.cluster import KMeans

X_simple = np.array([
    [6,3], [11, 15], [17,12], [24,10], [20,25], [22, 30],
    [85,70], [71,81], [60, 79], [56,52], [81, 91], [80, 81]
]) # 데이터

# kMeans 알고리즘, k(중심점)=2
# 두 개의 클러스터 중심점을 반복 계산해 학습
kmeans2 = KMeans(n_clusters=2, random_state=0).fit(X_simple)
print(&quot;중심점 : \n&quot;, kmeans2.cluster_centers_) # 학습 이후 찾은 중심점 좌표 출력

# 원본 데이터 출력
plt.figure()
plt.scatter(X_simple[:, 0], X_simple[:,1]) # 원본 데이터 표시
plt.title(&quot;Raw Points for K-Means&quot;)
plt.xlabel(&#39;x1&#39;)
plt.ylabel(&#39;x2&#39;)
plt.show()

# K-Means 알고리즘 수행 후 결과
plt.figure()
# 각 데이터에 지정한 클러스터에 따라 표시
plt.scatter(X_simple[:, 0], X_simple[:,1], c = kmeans2.labels_)
# centroid 좌표 표시
plt.scatter(kmeans2.cluster_centers_[:, 0], kmeans2.cluster_centers_[:,1], marker=&#39;x&#39;, s=200) 
plt.title(&quot;K-Means Result (k=2)&quot;)
plt.xlabel(&#39;x1&#39;)
plt.ylabel(&#39;x2&#39;)
plt.show()</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[주성분분석과 군집화]]></title>
            <link>https://velog.io/@inhwaaa_v/%EC%A3%BC%EC%84%B1%EB%B6%84%EB%B6%84%EC%84%9D%EA%B3%BC-%EA%B5%B0%EC%A7%91%ED%99%94</link>
            <guid>https://velog.io/@inhwaaa_v/%EC%A3%BC%EC%84%B1%EB%B6%84%EB%B6%84%EC%84%9D%EA%B3%BC-%EA%B5%B0%EC%A7%91%ED%99%94</guid>
            <pubDate>Thu, 01 Jan 2026 15:22:37 GMT</pubDate>
            <description><![CDATA[<h1 id="주성분분석-pca">주성분분석 (PCA)</h1>
<h2 id="차원의-저주">차원의 저주</h2>
<p>&nbsp;차원의 저주(Curse of dimensionality)란, 학습 데이터에 비해 입력 차원의 수가 큰 경우 일정 차원을 기점으로 학습 능력이 급격히 감소하는 현상을 의미한다. 다시 말해, 특징 공간의 차원이 증가하면서 학습 데이터의 수가 특징 공간의 차원의 수보다 적어져 성능이 저하되는 것이다.</p>
<p>&nbsp;차원이 증가할 수록 특징 공간의 부피가 커지고, 개별 차원 내에서의 데이터의 밀도가 희소해지며, 이에 따라 거리 함수가 제대로 작동하지 않고, 계산 비용이 증가하는 등의 문제가 발생하는데, 이러한 문제를 차원의 저주라고 한다.</p>
<p>&nbsp;그러나, 입력 차원 수가 증가한다고 반드시 차원의 저주가 발생하는 것은 아니며, 학습 데이터보다 입력 차원의 수가 많아지는 경우에 차원의 저주 문제가 발생한다. 공간이 희소해짐에 따라, 저차원 데이터에서 패턴을 파악하는 것보다 고차원 데이터에서 패턴을 파악하는데 더 많은 데이터가 필요해지기 때문이다.</p>
<p>&nbsp;이러한 차원의 저주는 차원이 증가함에 따라 동일한 데이터 개수로는 공간을 충분히 채울 수 없게 되어 데이터의 분포가 희소해지기에 발생한다. 입력 데이터의 차원이 증가하면, 특징 공간의 부피가 차원에 따라 기하급수적로 증가하여 데이터 간 거리가 멀어지고, 학습 데이터의 밀도가 낮아진다.</p>
<p>&nbsp;이러한 차원의 저주 문제를 해결하기 위한 이론적인 해결책은 훈련 샘플의 밀도가 충분히 높아질 때까지 데이터를 모아서 훈련 세트의 크기를 키우는 것이다. 그러나, 일정 밀도에 도달하기 위해 필요한 훈련 샘플 수는 차원의 수가 커짐에 따라 기하급수적으로 늘어난다는 문제가 존재한다. 따라서 PCA, SVD 등과 같은 차원 축소 기법들을 통해 학습 결과에 영향을 미치지 않는 불필요한 축을 줄임으로써 차원의 저주를 완화하기도 한다.</p>
<blockquote>
<p>“The curse of dimensionality refers to various phenomena that arise when analyzing and organizing data in high-dimensional spaces (often with hundreds or thousands of dimensions) that do not occur in low-dimensional settings such as the three-dimensional physical space of everyday experience.</p>
</blockquote>
<h2 id="차원-축소-방법">차원 축소 방법</h2>
<h3 id="특징-선택-feature-selection">특징 선택 (Feature Selection)</h3>
<ul>
<li>변수들 중 중요한 변수만 몇 개 고르고 나머지는 버리는 방법으로, 변수 간 중첩이 있는지, 어떤 변수가 중요한 변수인지, 어떤 변수가 종속 변수에 영향을 크게 주는 변수인지를 분석할 필요가 있음.</li>
<li>변수 간의 중첩을 확인하는 방법으로 상관관계를 주로 사용함.</li>
</ul>
<h3 id="특징-추출-feature-extraction">특징 추출 (Feature Extraction)</h3>
<ul>
<li>변수들을 조합해 데이터를 잘 표현하는 주성분을 가진 새로운 변수를 추출하는 방법</li>
<li>(ex) 주성분분석 (PCA, Principal Component Analysis)</li>
</ul>
<h3 id="선형-판별-분석-lda--linear-discrimination-analysis">선형 판별 분석 (LDA : Linear Discrimination Analysis)</h3>
<ul>
<li>학습 과정 중 클래스를 가장 잘 구분하는 축(axis)을 학습하는 방법</li>
</ul>
<h2 id="pca-principal-component-analysis">PCA (Principal Component Analysis)</h2>
<p>&nbsp;PCA는 데이터의 분산을 최대한 보존하면서 서로 직교하는 주성분을 찾아 고차원 공간의 표본들을 저차원 공간으로 변환하는 기법을 의미한다. 이를 위해 데이터를 잘 표현하는 초평면을 정의한 뒤, 데이터를 이 초평면에 projection하여 분산이 최대로 보존되는 축을 선택한다.</p>
<p>&nbsp;다시 말해, PCA는 다변량 데이터의 차원을 축소하면서 정보 손실을 최소화하는 방법으로, 데이터의 분산을 최대한 보존하는 새로운 축(주성분)을 찾아 원래 데이터를 이 주성분에 투영함으로써 차원을 축소하는 방법이다. 이를 통해 데이터의 중요한 정보를 유지하면서 차원을 줄이고, 시각화 및 기계 학습 알고리즘의 성능을 향상시킬 수 있다.</p>
<p>&nbsp;이러한 PCA의 과정은 다음과 같다.</p>
<ol>
<li><strong>데이터 전처리</strong> : 데이터를 표준화(평균 0, 표준편차 1)하거나 정규화(최소값 0, 최댓값 1)하여 스케일 조정</li>
<li><strong>공분산 행렬 계산</strong> : 데이터의 공분산 행렬을 계산합니다. 공분산 행렬은 변수 간의 선형 관계를 나타내며, 이를 통해 데이터의 분포와 구조를 파악할 수 있습니다.</li>
<li><strong>고윳값 및 고유벡터 계산</strong> : 공분산 행렬의 고윳값과 고유벡터를 계산합니다. 고윳값은 데이터의 분산을 나타내고, 고유벡터는 주성분의 방향을 나타냅니다. (입력 데이터들의 공분산 행렬에 대한 고유값 분해)<ol>
<li>고유벡터 : 주성분 벡터. 데이터 분포에서 분산이 큰 방향.</li>
<li>고유값 : 분산의 크기</li>
</ol>
</li>
<li><strong>주성분 선택</strong> : 고유값이 큰 순서대로 주성분을 선택합니다. 주성분 개수를 선택하는 방법으로, 누적 설명 분산 비율(cumulative explained variance ratio)을 활용할 수 있습니다. 누적 설명 분산 비율이 일정 수준(예: 95% 또는 99%) 이상인 주성분까지 선택하여 차원 축소를 수행합니다.</li>
<li><strong>주성분으로 원본 데이터 변환</strong> : 선택된 주성분(고유 벡터)을 이용하여 원본 데이터를 변환합니다. 이 과정에서 원본 데이터의 차원이 축소되며, 새로운 주성분 축에 투영된 데이터를 얻게 됩니다.</li>
</ol>
<h3 id="주성분을-찾는-방법">주성분을 찾는 방법</h3>
<p>&nbsp;주성분을 찾기 위해선 특이값 분해(Singular Value Decomposition, SVD)를 사용한다. 이는 정방행렬이 아닌 경우(비정방행렬)에 대해서도 행렬 분해를 수행하기 위해 활용하는 방법이다. 이를 위해 임의의 행렬을 $A=UΣV^T$의 형태 행렬로 분해한다.</p>
<ul>
<li>$A=UΣV^T$<ul>
<li>$A$ : 𝑚 × 𝑛 matrix (주어진 𝑚 × 𝑛 행렬)</li>
<li>$U$ : 𝑚 × 𝑚 orthogonal matrix (직교행렬)</li>
<li>$Σ$ : 𝑚 × 𝑛 diagonal matrix (대각행렬)</li>
<li>$V$ : 𝑛 × 𝑛 orthogonal matrix (직교행렬)</li>
</ul>
</li>
</ul>
<p>&nbsp;이때, 고유값은 행렬 A가 직사각행렬이면 입출력 차원이 달라 고유값을 직접 정의할 수 없다. 따라서 길이(스케일 크기)만 추출하기 위해 $AA^T$, $A^TA$의 결과가 항상 정방 행렬이고 대칭행렬인 점을 활용한다.</p>
<ul>
<li><p>$U$ (왼쪽 특이행렬) :  $AA^T$의 고유 벡터를 열에 배치한 n × n 행렬</p>
</li>
<li><p>$Σ$  (특이값 리스트) : $AA^T$의 고유값의 제곱근을 대각선에 배치한 대각 행렬 (n × m 행렬)</p>
</li>
<li><p>$V^T$ (특이행렬) : $A^TA$의 고유 벡터를 열에 배치한 m × m 행렬</p>
<p>  $$
  C = \frac{1}{n} X^T X = V \left( \frac{\Sigma^2}{n} \right) V^T ;\Rightarrow; \text{PCA 고유벡터} = V, \quad \text{고유값} = \frac{\Sigma^2}{n}
  $$</p>
</li>
</ul>
<h3 id="고유값-분해-eigen-decomposition"><strong>고유값 분해 (Eigen-Decomposition)</strong></h3>
<p>&nbsp;고유값 분해 (Eigen-Decomposition)란 행렬을 고유값과 고유 벡터를 통해 분해하는 기법으로, 어떤 정방행렬 A를 $A = QΛQ^{-1}$의 형태로 쪼개는 것을 의미한다. 이때, Q는 A의 고유벡터를 열에 배치한 행렬, Λ는 고유값을 대각선에 배치한 대각행렬을 의미한다.</p>
<p>&nbsp;이렇게 고유값 분해를 수행하면 행렬을 그 행렬만의 좌표계(고유 벡터)로 바꾸어 단순 대각 형태로 만들어 준다. 이때, 고유값이 크다는 것은 그 방향에 데이터의 정보량이 많다는 의미이기에 정보량이 큰 데이터만 남기고 차원을 축소할 수 있기 때문에 PCA에서 주로 활용된다.</p>
<ul>
<li>대각화 가능하다 = 행렬 A를 고유 벡터와 고유값으로 분해할 수 있다</li>
<li>다시 말해, 고유값 분해가 가능하고, $A = QΛQ^{-1}$의 형태로 표현이 가능하다는 것</li>
</ul>
<p>&nbsp;고유값 분해를 해주면, 대각행렬 $Λ$가 단순 스케일 행렬임을 볼 수 있다. (이는 표준 기저에서 봤을 땐 복잡한 변환이었는데, 좌표계를 고유벡터 기준으로 바꿨더니 A가 단순히 각 좌표 성분을 $\lambda_i$배 해주는 행렬로 보이더라! 라는 느낌으로 이해하면 된다.)</p>
<p>&nbsp;$Q$의 열벡터들을 고유벡터로 구성한 이유는 결국엔 이 벡터들이 만드는 좌표계가 고유기저이기 때문이고, 결국 고유값 분해는 이 고유 기저를 기준으로 행렬을 분해하는 것을 의미한다고 볼 수 있다.</p>
<p><strong>고유값 분해 예시</strong></p>
<ul>
<li><strong>고유값</strong> : $det(A - \lambda I) = 0$</li>
<li><strong>고유벡터</strong> : $(A - \lambda I) V = 0$</li>
<li><strong>고유값들의 합</strong> : 대각 요소들의 합</li>
<li><strong>고유값들의 곱</strong> : 행렬식</li>
<li><strong>정규직교벡터</strong> : 서로 수직이고, 각각 길이가 1인 벡터들<ul>
<li>$Q^TQ = 1, Q^{-1} = Q^T$</li>
</ul>
</li>
</ul>
<h1 id="군집화-clustering">군집화 (Clustering)</h1>
<p>&nbsp;군집화는 유사한 속성들을 갖는 관측치들을 묶어 전체 데이터를 몇 개의 군집(cluster)로 나누는 것을 의미한다. 다시 말해, 어떠한 레이블 없이 데이터 내에서 거리가 가까운 것들끼리 각 군집들로 분류하여 데이터 내에 숨어있는 패턴 혹은 그룹을 파악해 서로 묶는 것이다. 이때, 군집(cluster)은 비슷한 특성을 가진 데이터의 집합을 의미한다. 군집화는 군집 내 응집도와 군집 간 분리도를 최대화하는 것을 목적으로 하며, 이러한 군집화 알고리즘에는 K-Means Clustering, Mean Shift, Gaussian Mixture Model, DBSCAN, Agglomerative Clustering 등이 있다.</p>
<h2 id="거리-척도">거리 척도</h2>
<h3 id="유클리드-거리">유클리드 거리</h3>
<ul>
<li>각 차원의 차를 제곱해서 사용하는 것으로, n차원에서 두 점 사이의 거리를 구할 경우 다음과 같이 나타냄.</li>
</ul>
<p>$$
d_E(x, y) = \left( \sum_{i=1}^{n} |x_i - y_i|^2 \right)^{\frac{1}{2}} = \sqrt{ \sum_{i=1}^{n} |x_i - y_i|^2 }
$$</p>
<h3 id="맨하탄-거리">맨하탄 거리</h3>
<ul>
<li>각 차원의 차를 제곱해서 사용하는 것이 아닌, 절댓값을 바로 합산하는 것을 의미하며, 맨해튼 거리는 항상 유클리드 거리보다 크거나 같다.</li>
</ul>
<p>$$
d_M(x, y) = \sum_{i=1}^{n} |x_i - y_i|
$$</p>
<h3 id="마할라노비스-거리">마할라노비스 거리</h3>
<ul>
<li>변수들의 공분산(상관관계)을 고려해 거리를 측정하는 방식</li>
<li>$S^{-1}$ : covariance matrix</li>
</ul>
<p>$$
d_{\text{Mahalanobis}}(X,Y) = \sqrt{(X - Y)^T S^{-1} (X - Y)}
$$</p>
<p><strong>공분산 행렬 (Covariance Matrix)</strong></p>
<ul>
<li>각 확률 변수 사이의 공분산을 모두 구해 행렬화한 것</li>
<li>대각선에는 각 변수의 분산, 그 외 위치에는 변수 쌍들의 공분산이 표현됨.</li>
</ul>
<h2 id="k-means-알고리즘">K-means 알고리즘</h2>
<p>&nbsp;<strong>K-Means</strong> 클러스터링은 클러스터링에서 가장 일반적으로 사용되는 알고리즘으로, 군집 중심점(centroid)이라는 특정한 임의의 지점을 선택해 해당 중심에 가장 <strong>가까운</strong> 포인트들을 선택하는 군집화 기법이다. K-Means이므로 K개의 centroid를 지정할 수 있고, 이때 가장 가까운 포인트를 선택한다는 점에서 K-Means는 <strong>거리 기반 군집화 방법</strong>임을 알 수 있다.</p>
<p>&nbsp;이처럼 K-means 클러스터링은 군집 중심점(centroid)라는 특정한 k개의 임의 지점을 선택해 해당 중심에 가장 가까운 포인트들을 선택하는 군집화 기법으로, 다음과 같은 과정에 의해 수행된다.</p>
<ol>
<li>초기 중심점 설정</li>
<li>중심점을 선택된 포인트들의 평균 지점으로 이동함.</li>
<li>이동된 중심점에서 다시 가까운 포인트를 선택함.</li>
<li>다시 중심점을 평균 지점으로 이동하는 프로세스를 반복적으로 수행함.</li>
<li>즉, 초기 중심 설정 → 할당 단계 (closest centroid) → 업데이트 단계 (평균 재계산) → 수렴 시 종료의 과정을 반복함.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/cfeb3645-3244-4eff-b7ef-4780bf61d040/image.png" alt=""></p>
<h3 id="inertia-sse">inertia (=SSE)</h3>
<ul>
<li>Centroid와 Sample들 사이의 거리 제곱 합</li>
<li>K-Means 알고리즘은 k개의 중심(centroid)를 추정하여 각 샘플을 가장 가까운 중심에 할당하고, 중심을 재계산하는 과정을 반복하여 군집 내 제곱합(SSE)를 최소화함.</li>
</ul>
<p><strong>참고 - Rule of Thumb</strong></p>
<ul>
<li><p>일상적인 상황에서 간단하게 적용할 수 있는 규칙이나 원리</p>
</li>
<li><p>k : cluster의 개수이고, N : sample 개수일 때,</p>
<p>  $$
  k = \sqrt{\frac{N}{2}}
  $$</p>
</li>
</ul>
<h3 id="silhouette-score">Silhouette Score</h3>
<ul>
<li>최적의 cluster 개수를 찾는 방법</li>
<li>클러스터링의 품질을 정량적으로 계산해 주는 방법으로, 데이터의 응집도를 나타내는 값인 $a^{(i)}$, 클러스터 간 분리도를 나타내는 값인 $b^{(i)}$를 통해 실루엣 계수 $s^{(i)}$를 계산함.<ul>
<li>$a_i$ : i번째 샘플이 속한 클러스터 내부의 평균 거리</li>
<li>$b_i$ : i번째 샘플과 가장 가까운 클러스터 샘플들과의 평균 거리</li>
</ul>
</li>
</ul>
<p>$$
s^{(i)} = \frac{b^{(i)} - a^{(i)}}{\max { a^{(i)}, b^{(i)} }}
$$</p>
<ul>
<li>실루엣 계수는 클러스터 내 데이터 포인트 간의 거리는 가깝고, 클러스터 간 거리는 멀면 높은 값을 지니며, -1~+1 사이의 값을 지님.</li>
<li>클러스터 개수가 최적화되어 있다면 분리도의 값은 커지고, 응집도의 값은 작아지기에 실루엣 계수는 1에 가까워짐. 결국 실루엣 계수가 1에 가까울 수록 클러스터 개수가 최적화된 것임.</li>
</ul>
<h2 id="mean-shift-알고리즘">Mean-Shift 알고리즘</h2>
<p>&nbsp;<strong>평균 이동(Mean Shift)</strong>은 K-means와 유사하게 중심을 군집의 중심으로 지속적으로 움직이면서 군집화를 수행한다. 그러나 K-means 방법이 중심에 소속된 데이터의 평균 거리 중심으로 이동을 한다면, Mean Shift는 중심을 데이터가 모여 있는 <strong>밀도가 가장 높은 곳</strong>으로 이동시킨다는 점에서 그 차이가 있다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/a0998a84-d177-440b-a0bb-4d0491c518b7/image.png" alt=""></p>
<p>&nbsp;Mean Shift는 특정 대역폭(bandwidth;region of interest)을 가지고 최초의 확률 밀도 중심 내에서 데이터의 <strong>확률 밀도</strong>가 더 높은 곳으로 중심을 이동한다. (centroid-based algorithm라고 한다)</p>
<p>&nbsp;Mean Shift는 데이터 간의 거리가 아닌 데이터의 분포도를 이용해 군집 중심점을 잡는다. 군집 중심점은 데이터 포인트가 모여 있는 곳이라는 아이디어에 착안한 것이며, 이를 위해 <strong>확률 밀도 함수를 이용</strong>한다. 일반적으로 <strong>확률 밀도 함수를 찾기 위해 KDE(Kernel Density Estimation)를 이용</strong>한다. Mean Shift 알고리즘은 <strong>임의의 포인트에서 시작해 데이터 내의 확률 밀도 peak 포인트를 찾을 때까지 KDE를 반복적으로 적용하며 군집화를 수행</strong>한다.</p>
<p>&nbsp;Mean Shift 알고리즘은 K-Means와 다르게 군집의 개수를 지정할 필요가 없다. 다만 Bandwidth의 크기에 따라 알고리즘 자체에서 군집의 개수를 최적화하므로, Bandwidth의 최적화가 필요하다</p>
<p>&nbsp;또한, Mean Shift 알고리즘은 K-평균과 달리 초기 중심점을 무작위로 선택하는 방식이 아니므로 초기값 설정에 따른 결과의 변화가 거의 없고, 초기화 과정에서 모든 데이터 포인트를 잠재적인 클러스터 중심으로 설정한다. 따라서 K-Means와 달리 랜덤 초기화에 의존하지 않으며, 이로 인해 실행할 때마다 결과가 항상 동일하다는 장점이 존재한다. (모든 데이터 포인트에서 각각이 밀도 높은 방향으로 올라가다 보면 자연스럽게 같은 peak로 수렴하는 시작점들이 생김 → 그때 같은 peak에 도착한 시작점들을 하나의 cluster로 묶음. 따라서 Mean Shift의 클러스터 개수 = 데이터 밀도 분포의 피크(Mode)의 개수)</p>
<p><strong>정리</strong></p>
<ul>
<li><strong>Mean Shift 알고리즘</strong> : 주어진 Bandwidth 내에서 KDE(Kernel Density Estimation)를 이용해 데이터의 밀도 분포를 추정하고, 밀도가 높은 방향으로 중심점을 반복적으로 이동시키면서 군집화를 수행하는 모델</li>
<li>각 샘플은 주변 데이터의 밀도가 가장 높은 방향으로 이동하게 되며, 이러한 이동 과정이 수렴하게 되면 그 점은 하나의 클러스터 중심이 된다. 즉, Mean Shift는 데이터 밀도 기반으로 군집 중심을 자동 탐색하는 알고리즘이다.</li>
<li>중심을 군집의 중심으로 지속적으로 움직이면서 군집화를 수행한다는 점이 K-Means 알고리즘과 유사하나, K-Means 알고리즘은 중심에 소속된 데이터의 평균 거리 중심으로 이동하는 데 반해, Mean-Shift 알고리즘은 중심을 데이터가 많이 모여 있는, 밀도가 가장 높은 곳으로 이동한다.</li>
<li>즉, Mean-Shift 알고리즘은 데이터의 분포도를 통해 군집 중심점을 찾는 알고리즘으로, <strong>가장 집중적으로 데이터가 모여있어 확률 밀도 함수가 피크인 점을 군집 중심점으로 선정</strong>하며, 일반적으로 <strong>주어진 모델의 확률 밀도 함수를 찾기 위해 KDE(Kernel Density Estimation)를 이용</strong>한다.</li>
<li>Mean Shift 알고리즘에서의 <strong>Bandwidth</strong>는 <strong>각 데이터 포인트 주변에서 밀도를 계산할 때 고려할 이웃 데이터의 반경</strong>을 정의하며. <strong>Bandwidth 크기에 따라 군집 수가 결정</strong>된다. 이때,  탐색 반경이 너무 크면 정확한 중심 위치를 찾을 수 없고, 너무 작으면 local minimum에 빠지기 쉽다.</li>
</ul>
<h3 id="kde-kernel-density-estimation">KDE (Kernel Density Estimation)</h3>
<ul>
<li>커널(kernel) 함수를 통해 어떤 변수의 확률 밀도 함수를 추정하는 대표적인 방법</li>
<li>KDE는 확률 변수의 확률 밀도 함수를 사용하여 각 반복에서 데이터 밀도가 높은 영역을 식별한다.</li>
<li>개별 데이터 포인트에 커널 함수(Gaussian 등)를 적용한 값들을 모두 합한 후 평균을 구하여 확률 밀도 함수를 추정하는 방식</li>
<li>다시 말해, KDE는 각 데이터 포인트에 Gaussian 같은 커널을 올리고, 모든 커널을 합쳐 확률 밀도 함수를 만든 뒤 Mean Shift가 이 density에서 가장 높은 지점을 찾도록 돕는 방법</li>
</ul>
<h1 id="코드">코드</h1>
<ul>
<li><strong>PCA 수행</strong> : <code>class PCA(n_components=None, svd_solver=&#39;auto&#39;)</code><ul>
<li><strong>유지할 주성분의 수 (또는 비율) :</strong> <code>n_components</code><ul>
<li><strong>수 지정</strong> : <code>PCA(n_components=2)</code></li>
<li><strong>비율 지정</strong> : <code>PCA(n_components=0.95)</code></li>
</ul>
</li>
<li><strong>특이값 분해 수행 방식 결정 :</strong> <code>svd_solver:{‘auto’, ‘full’, ‘arpack’, ‘randomized’}</code><ul>
<li>PCA에서 계산 시 어떤 방식으로 특이값 분해를 수행할지 결정하는 옵션</li>
<li>전체 데이터를 사용할 건지, 일부 성분을 근사적으로 뽑아 빠르게 계산할 건지 결정<ul>
<li><code>full</code> : full SVD(특이 분해) 실행</li>
<li><code>arpack</code> : n_components로 잘린 SVD 실행</li>
<li><code>randomized</code> : Halko 등의 방법으로 무작위 SVD 실행</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>KMeans 수행</strong> : <code>class KMeans(n_clusters=8, init=&#39;k-means++’, n_init=10, max_iter=300, random_state=None, algorithm=&#39;auto&#39;)</code><ul>
<li><code>n_clusters</code> : cluster의 개수</li>
<li><code>init</code> : {&#39;k-means++&#39;, &#39;random’}<ul>
<li>k-means++ : 수렴 속도를 높이기 위한 스마트한 방식으로, 첫 번째 중심은 아무거나 선택하고, 나머지 중심들은 <strong>이미 선택된 중심들과 최대한 멀리</strong> 있도록 선택함.</li>
</ul>
</li>
<li><code>n_init</code> : 다른 centroid로 시도할 횟수</li>
<li><code>algorithm</code> : {“auto”, “full”, “elkan”}<ul>
<li><code>full</code> : classical EM-style algorithm<ul>
<li>EM(Expectation Maximization) 알고리즘</li>
</ul>
</li>
<li><code>elkan</code> : ****삼각부등식으로 거리 계산을 줄이는 최적화</li>
<li><code>auto</code> : dense면 elkan, sparse면 full</li>
</ul>
</li>
</ul>
</li>
<li><strong>실루엣 점수 계산</strong> : <code>silhouette_score(X, labels, metric= &#39;euclidean&#39;)</code></li>
<li><strong>Mean-Shift 수행</strong> : <code>class MeanShift(bandwidth=None, seeds=None, n_jobs=None, max_iter=300)</code><ul>
<li><code>bandwidth</code> : float value used in the RBF kernel</li>
<li><code>seeds</code> : used to initialize kernels</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[앙상블과 랜덤 포레스트]]></title>
            <link>https://velog.io/@inhwaaa_v/%EC%95%99%EC%83%81%EB%B8%94%EA%B3%BC-%EB%9E%9C%EB%8D%A4-%ED%8F%AC%EB%A0%88%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@inhwaaa_v/%EC%95%99%EC%83%81%EB%B8%94%EA%B3%BC-%EB%9E%9C%EB%8D%A4-%ED%8F%AC%EB%A0%88%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 01 Jan 2026 15:18:26 GMT</pubDate>
            <description><![CDATA[<h1 id="앙상블-ensemble-learning">앙상블 (Ensemble Learning)</h1>
<p>&nbsp;앙상블 기법이란, 여러 개 분류기를 생성하고, 그 예측을 결합함으로써 보다 정확한 예측을 도출하는 기법으로, Strong Classifier를 사용하는 대신 Weak Classifier를 조합하여 더 정확한 예측을 수행한다.</p>
<p>&nbsp;이는 단일 모델의 예측을 평가하는 것보다 더 많은 계산이 필요하므로 더 많은 연산 능력을 활용해 더 좋은 예측력을 가지게 된다.</p>
<h2 id="앙상블-기법의-유형">앙상블 기법의 유형</h2>
<ul>
<li>보팅 (Voting) : 여러 개 분류기가 투표를 통해 최종 예측 결과를 결정하는 방식</li>
<li>배깅 (Bagging : Bootstrap Aggregating) : 데이터 샘플링을 통해 모델을 학습시키고 결과를 집계하는 방식</li>
<li>부스팅 (Boosting) : 여러 개 분류기가 순차적으로 학습을 수행하는 방식 (다음 분류기에게 가중치 부스팅)</li>
<li>스태킹 (Stacking : Stacked Generalization) : 여러 개 분류기 결과를 취합하는 마지막 예측기(블렌더 또는 메타 학습기)를 학습하는 방법</li>
</ul>
<h2 id="앙상블-기법의-특징">앙상블 기법의 특징</h2>
<ul>
<li>앙상블 모델은 Heterogeneous하고 Independent한 특성을 지니고, 낮은 상관관계를 지녀야 성능이 좋아짐. (즉, 모델의 종류나 학습 방식이 서로 다르고, 독립적이어야 함.)</li>
<li>이러한 특성을 지녀야 서로 다른 종류의 오차를 생성할 가능성이 높고, 높은 정확도를 얻을 수 있으며, 성능을 분산시키므로 과적합 감소 효과를 얻을 수 있음.</li>
<li>앙상블이 잘 작동하려면 모델들이 같은 실수를 반복하지 않는 것이 중요한데, 모든 모델이 같은 방향으로 틀리면 앙상블 효과가 떨어짐. (하지만, 모델들이 서로 다른 관점에서 판단하면 투표나 평균을 취했을 때 앙상블 효과가 높아지므로 정확도가 높아질 가능성이 높음.)</li>
<li>즉, 오차가 서로 상관되지 않는 것이 중요함.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/fc39cb6f-1937-4d64-b591-422590998982/image.png" alt=""></p>
<h2 id="보팅-voting">보팅 (Voting)</h2>
<p>&nbsp;<strong>Voting은 서로 다른 알고리즘을 가진 분류기 중 투표를 통해 최종 예측 결과를 결정하는 방식</strong>으로, 최종 결과 선정 방식에 따라 Hard Voting과 Soft Voting으로 분류된다.</p>
<h3 id="하드-보팅-hard-voting">하드 보팅 (Hard Voting)</h3>
<ul>
<li>분류기 각각의 예측 결과를 활용하는 방법으로, <strong>다수의 분류기가 예측한 결과 값을 최종 결과로 선정</strong>하므로 직접 투표에 해당함.</li>
<li>다시 말해, 각각의 분류기 결과값 중 가장 많은 것을 따르는 방식임.</li>
<li>확률의 신뢰도가 낮거나 모델들이 확률 출력을 제공하지 않는 경우 사용됨.</li>
</ul>
<h3 id="소프트-보팅-soft-voting">소프트 보팅 (Soft Voting)</h3>
<ul>
<li>예측 결과의 확률값을 활용하는 방법으로, 모든 분류기가 예측한 레이블 값의 결정 확률 평균을 계산해 가장 확률이 높은 레이블 값을 최종 결과로 선정하므로 간접 투표에 해당함.</li>
<li>분류기의 확률을 더하고 각각 평균을 내서 <strong>확률이 제일 높은 값을 선정</strong>하는 방식임.</li>
<li>확률 예측이 가능한 모델들(즉, 확률의 신뢰도가 높음)이고 모델의 성능 차이가 큰 경우 사용됨.</li>
<li>하드 보팅에 비해 성능이 좋은 편에 속함.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/37a594d4-aa2c-4f57-96ab-6d9b9831bbdd/image.png" alt=""></p>
<pre><code>from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score

X_train, X_test, y_train, y_test = train_test_split(X, y) # 데이터 분할

dt_clf = DecisionTreeClassifier(max_depth=4) # Decision Tree 객체 생성 (트리 최대 깊이 = 4)
knn_clf = KNeighborsClassifier(n_neighbors=7) # KNN 분류기 생성 (예측 시 참고할 주변 데이터 포인트 개수 = 7)
svm_clf = SVC(gamma=0.1, probability=True) # SVC 객체 생성 (probability=True이므로 클래스 확률 출력 제공)

models = [(&#39;dt&#39;, dt_clf), (&#39;knn&#39;, knn_clf), (&#39;svc&#39;, svm_clf)] # 모델 정의
voting_clf = VotingClassifier(estimators=models, voting=&#39;soft&#39;, weights=[2, 1, 2]) # 소프트 보팅 객체 생성 (weight는 각 모델에 대한 가중치 비율/중요도를 지정함.)
voting_clf.fit(X_train, y_train)

# 여러 분류기를 한 번에 학습시키고 성능 비교
for clf in (dt_clf, knn_clf, svm_clf, voting_clf):
    clf.fit(X_train, y_train) # 학습
    y_pred = clf.predict(X_test) # 예측
    print(clf.__class__.__name__, accuracy_score(y_test, y_pred)) # 성능 측정 (정확도)</code></pre><h2 id="배깅-bagging-boostrap-aggregating">배깅 (Bagging, Boostrap Aggregating)</h2>
<p>&nbsp;배깅이란, <strong>부트스트랩으로 생성된 여러 데이터셋을 통해 Weak Learner를 훈련시키고, 그 결과를 Voting하여 최종 예측</strong>을 만드는 것을 의미한다.</p>
<p>&nbsp;다시 말해, 데이터 샘플링(Bootstrap)을 통해 모델을 학습시키고, 결과를 집계(Aggregating)하는 방법으로, <strong>모두 같은 유형의 알고리즘 기반 분류기를 활용</strong>한다. 즉, 원본 데이터를 여러 번 랜덤 샘플링 해서 각기 다른 학습 데이터를 만들고, <strong>동일한 알고리즘의 여러 모델을 학습</strong>시킨 뒤 예측을 평균 또는 투표로 결합하는 방법을 배깅이라고 한다.</p>
<ul>
<li><strong>부트스트랩(bootstrap)</strong> : 통계학에서 사용하는 용어로, 무작위 샘플링을 통해 새로운 데이터셋을 생성하는 것을 의미함.
  (ex) 한 식자재 마트에 들어오는 상추의 신선도를 알기 위해 마트에 입고되는 모든 상추 중 임의로 100개를 뽑아 상추의 신선도 평균을 구하는 것</li>
<li>부트스트랩은 raw data의 분포를 추정할 때 사용할 수 있음.
  예를 들어, 측정된 데이터 중에서 중복을 허용한 복원 추출로 n개를 뽑고, 뽑은 n개의 평균을 구하는 것을 m번 반복하여 모으게 되면 평균에 대한 분포를 구할 수 있게 되고, 이로부터 샘플 평균에 대한 신뢰 구간을 추정할 수 있게 됨. 이를 통해 데이터가 부족해도 여러 데이터셋을 가진 것처럼 모델을 반복 학습하는 효과를 낼 수 있음.</li>
<li>즉, <strong>배깅(Bagging)은 부트스트랩(bootstrap)을 집계(Aggregating)하여</strong> 학습 데이터가 충분하지 않더라도 충분한 학습효과를 주어 높은 bias의 underfitting 문제나, 높은 variance로 인한 overfitting 문제를 해결하는데 도움을 줌.</li>
<li>일반적으로 배깅에선 데이터 분할 시 중복을 허용하는 방식을 활용하며, 만약 중복을 허용하지 않는다면 그 방식을 페이스팅(Pasting)이라고 한다.</li>
<li>집계 방법에는 <strong>다수결 투표 방식으로 결과를 집계하는 Categorical Data 방법</strong>과 <strong>평균값을 집계하는 Continuous Data</strong>가 있음.</li>
<li>대표적인 Bagging 알고리즘에는 랜덤 포레스트(Random Forest)가 있음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/8cf15527-2557-47a8-9309-60f22befb905/image.png" alt=""></p>
<pre><code>from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

dt_clf = DecisionTreeClassifier() # 결정 트리 객체 생성
# 배깅 객체 생성, 결정 트리 모델을 기본으로 사용하며, 내부적으로는 이를 500번 복제하여 사용
# 다시 말해, Decision Tree 500개를 랜덤하게 학습시키고, 그 결과를 투표해서 최종 예측하는 배깅 앙상블 모델임.
# 각 트리는 서로 다른 부트스트랩 샘플로 학습되며, 원본 데이터에서 중복 허용하여 랜덤하게 뽑은 데이터로 각 트리를 학습함.
# 트리마다 조금씩 다른 데이터 -&gt; 서로 다른 결정 경계 -&gt; 오차 상관 감소 -&gt; 앙상블 효과 증가
bag_clf = BaggingClassifier(dt_clf, n_estimators=500)
bag_clf.fit(X_train, y_train) # 학습

# 단일 Decision Tree 모델과 배깅 분류기 성능 차이 비교
# 배깅은 여러 트리의 결과를 활용하므로 분산을 줄이고 일반화 성능을 향상시킴.
for clf in (dt_clf, bag_clf):
    clf.fit(X_train, y_train) # 학습
    y_pred = clf.predict(X_test) # 예측
    print(clf.__class__.__name__, accuracy_score(y_test, y_pred)) # 성능 측정 (정확도)</code></pre><pre><code>from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

X, y = make_moons(n_samples=500, noise=0.30, random_state=42) # 랜덤 데이터 500개 생성
X_train, X_test, y_train, y_test = train_test_split(X, y) # 데이터 분할

# 대표적인 Bagging 알고리즘 - 랜덤 포레스트(Random Forest)
clf = RandomForestClassifier(n_estimators=500) # 랜덤 포레스트 객체 생성
clf.fit(X_train, y_train) # 학습

y_pred = clf.predict(X_test) # 예측
print(f&quot;{clf.__class__.__name__} : {accuracy_score(y_test, y_pred):.4f}&quot;) # 성능 측정 (정확도)</code></pre><h2 id="부스팅-boosting">부스팅 (Boosting)</h2>
<p>&nbsp;<strong>순차적으로 모델을 학습시키고, 이전 모델이 잘못 예측한 부분을 다음 모델이 개선할 수 있도록 하는 방법</strong>으로, <strong>각 모델의 예측 결과를 가중 평균하여 최종 예측</strong>을 만들어 낸다.</p>
<p>&nbsp;다시 말해, 이전 분류기가 틀린 예측을 한 데이터에 대해 올바른 예측이 가능하도록 다음 분류기에게 가중치를 부여하면서 학습과 예측을 진행해 계속해서 가중치를 부스팅하며 학습을 진행한다.</p>
<p>&nbsp;이는 배깅과 유사한 매커니즘을 지니나, <strong>배깅과는 다르게 순차적으로 진행</strong>된다는 차이가 있다. 앞선 Bagging의 경우 각각의 분류기들이 학습시 상호영향을 주지 않은 상황에서(독립적) 학습이 끝난 다음 결과를 종합하는 기법이라면, Boosting은 이전 분류기의 학습 결과를 토대로 다음 분류기의 학습 데이터의 샘플 가중치를 조정해 학습을 진행하는 방법이므로 먼저 생성된 모델을 꾸준히 개선해 나가는 방식으로 학습이 진행된다.</p>
<p>&nbsp;따라서 오차에 대해 높은 가중치를 부여하므로 높은 정확도를 나타내고, 배깅에 비해 성능이 좋으나, 속도가 느리고 과적합 가능성이 있으며, 이상치에 취약할 수 있다. 이러한 Boosting 기법을 활용한 <strong>대표적인 알고리즘은 XGBoost, AdaBoost, GBM, LightBoost 등</strong>이 있다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/d7573726-fb47-450c-91e2-d5b329c07300/image.png" alt=""></p>
<h3 id="adaboost">AdaBoost</h3>
<ul>
<li>가장 초기의 부스팅 알고리즘 중 하나로, 약한 학습기(weak learners)를 순차적으로 추가하여 학습시키고, 이전 학습기에서 잘못 분류된 샘플에 더 많은 가중치를 부여하여 이후 학습기가 이를 더 잘 맞추도록 함. (오차 보정을 위해 데이터들에 가중치를 부여하며 동작함)</li>
<li>단순한 모델을 결합해 높은 예측 성능을 내는 방법</li>
</ul>
<pre><code>from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import accuracy_score

X, y = make_moons(n_samples=500, noise=0.30, random_state=42) # 랜덤 데이터 500개 생성
X_train, X_test, y_train, y_test = train_test_split(X, y) # 데이터 분할

# AdaBoost = Adaptive Boosting
# 앞에서 틀린 것을 뒤에서 계속 보완해 나가면서 강한 분류기를 만들어 가는 부스팅 알고리즘으로,
# 약한 분류기 여러 개를 순차적으로 학습시키면서 못 맞춘 샘플에 점점 더 가중치를 많이 줘서
# 어려운 샘플에 집중할 수 있도록 하는 방식임.
# 즉, 데이터에 대한 가중치를 적응적으로 바꾸면서 학습해 나가는 방식임.
clf = AdaBoostClassifier(n_estimators=500, learning_rate=0.1) # AdaBoost 객체 생성
clf.fit(X_train, y_train) # 학습

y_pred = clf.predict(X_test) # 예측
print(f&quot;{clf.__class__.__name__} : {accuracy_score(y_test, y_pred):.4f}&quot;) # 성능 측정 (정확도)</code></pre><h3 id="gradient-boosting-machine-gbm">Gradient Boosting Machine (GBM)</h3>
<ul>
<li>Gradient Descent를 사용하여 loss function이 줄어드는 방향으로 weak learner들을 반복적으로 결합함으로써 성능을 향상시키는 boosting 알고리즘</li>
<li>이전 예측기가 만든 잔여 오차(Residual Error)를 예측하는 새로운 예측기를 만들고 학습시키며, 이전 모델의 잔차를 통해 약한 학습기를 강화함.</li>
<li>즉, 순차적으로 모델을 학습시키면서 각 단계에서 이전 모델의 오차를 줄이는 방향으로 새로운 모델을 추가하는 방식임.</li>
<li>계산 시간이 오래 걸릴 수 있고, 과적합 문제가 발생할 수 있음.</li>
<li>가중치 업데이트<ul>
<li>AdaBoost : 가중치를 단순히 증가 또는 감소</li>
<li>GBM : 경사하강법</li>
</ul>
</li>
</ul>
<pre><code>from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score

X, y = make_moons(n_samples=500, noise=0.30, random_state=42) # 랜덤 데이터 500개 생성
X_train, X_test, y_train, y_test = train_test_split(X, y) # 데이터 분할

# GBM은 오차(잔차/Residual)를 줄이기 위해 이전 모델이 틀린 부분을 점점 보완하면서 모델을 쌓아가는 부스팅 알고리즘
# 잔차 기반 학습을 진행하는 알고리즘임.
clf = GradientBoostingClassifier(n_estimators=500, learning_rate=0.1) # GBM 객체 생성
clf.fit(X_train, y_train) # 학습

y_pred = clf.predict(X_test) # 예측
print(f&quot;{clf.__class__.__name__} : {accuracy_score(y_test, y_pred):.4f}&quot;) # 성능 측정 (정확도)</code></pre><h2 id="스태킹-stacking">스태킹 (Stacking)</h2>
<p>&nbsp;스태킹은 <strong>여러 개의 서로 다른 모델(베이스 모델)을 학습시키고, 그 예측 결과를 조합해 최종 예측을 만드는 방식</strong>이며, 이 방법의 핵심 요소는 <strong>Meta Learner</strong>이다.</p>
<p>&nbsp;Stacking은 여러 모델의 예측값을 새로운 입력으로 사용해서 또 다른 모델(Meta Learner)을 학습시키는 앙상블 방법이다. 개별 모델이 예측한 데이터를 다시 Meta dataset으로 사용해 학습한다는 컨셉의 접근법으로, Stacking을 위해선 개별 모델들(Base Learner)과 최종 모델(Meta Learner)이 필요하다.</p>
<p>&nbsp;이때, 스태킹에선 여러 예측기에서 각각 다른 값을 예측하면, 마지막 예측기에서 이 예측을 입력으로 받아 최종 예측을 수행한다.</p>
<p>&nbsp;스태킹의 장점은 다양한 모델의 강점을 활용하여 전체적인 성능을 향상시킬 수 있다는 것이며, 스테이지 0에서 k-폴드 교차 검증을 사용함으로써 과적합을 방지하고 견고한 성능을 보장할 수 있다.</p>
<h3 id="스테이지-0-base-model">스테이지 0 (Base Model)</h3>
<ul>
<li>여러 개의 서로 다른 모델(베이스 모델)을 k-fold Cross Validation을 통해 학습시킴.</li>
<li>각 폴드에서 베이스 모델은 검증 세트에 대한 예측을 수행함.</li>
<li>이러한 예측 결과를 모아 새로운 데이터셋을 만들고, 이러한 데이터셋은 메타 모델이 베이스 모델이 학습한 데이터를 직접 보지 않도록 하여 과적합 방지에 도움을 줌.</li>
</ul>
<h3 id="스테이지-1-meta-learner">스테이지 1 (Meta Learner)</h3>
<ul>
<li>베이스 모델의 예측 결과로 구성된 새로운 데이터셋을 통해 메타 모델을 학습시킴.</li>
<li>메타 모델은 이 예측 결과를 조합해 최종 출력을 만듦.</li>
</ul>
<h2 id="blending">Blending</h2>
<ul>
<li>Stacking에서 k-fold 교차 검증을 생략하고, hold out 방식을 사용하는 방식</li>
<li>Stacking에서 사용하는 k-fold 교차 검증을 생략하고, <strong>데이터를 훈련-검증-테스트 세트로 나누어 각 ML 모델을 한 번씩 훈련시킨 후, 예측된 값을 사용해 메타 모델을 학습</strong>하는 방식 (즉 데이터를 Train / Hold-out / Test로 나누고 훈련된 모델을 Hold-out set에 대해 예측하여 Meta Feature를 생성함. 이후 이 Meta Feature를 입력으로 하는 메타 모델을 학습해 최종적으로 Test set에 대해 예측하여 결과를 생성함.)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/5fadbc94-b3f7-413a-9c70-c12c03497868/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서포트 벡터 머신과 결정 트리]]></title>
            <link>https://velog.io/@inhwaaa_v/SVM</link>
            <guid>https://velog.io/@inhwaaa_v/SVM</guid>
            <pubDate>Thu, 01 Jan 2026 15:08:10 GMT</pubDate>
            <description><![CDATA[<h1 id="svm-support-vector-machine">SVM (Support Vector Machine)</h1>
<p> 서포트 벡터머신은 선형/비선형 분류, 회귀, 이상치 탐색 등 복잡한 분류 문제에 적합한 다목적 기계학습 모델로, 중간 이하 크기의 데이터셋에 적합한 방법이다. 이는 분류를 위한 최적의 결정 경계를 찾는 것을 목표로 하며, 최적의 결정 경계라는 것은 다수의 결정 경계 후보들 중에 최대 마진을 갖는 결정 경계를 의미한다.</p>
<p> 이때, 서포트 벡터(support vectors)는 마진 결정에 영향을 끼치는 샘플 데이터로, 결정 경계와 가장 가까운 데이터 포인트를 의미한다. 마진(margin)은 결정 경계와 서포트 벡터 사이의 거리(minus-plane과 plus-plane 사이의 거리)로, 결정 경계와 서포트 벡터 사이에 직교하는 수직 선분을 그려 거리가 같아지는 지점을 의미한다.</p>
<p> SVM은 데이터를 가장 잘 구분하는 결정 경계선을 찾아 분류하는 지도 학습 알고리즘으로, 다수의 결정 경계 후보 중 최대 마진을 지니는 결정 경계를 찾는 것을 목적으로 한다.</p>
<ul>
<li><strong>plus-plane</strong> : $w^Tx + b = 1$</li>
<li><strong>minus-plane</strong> : $w^Tx + b = -1$</li>
<li><strong>decision boundary</strong> : $w^Tx + b = 0$
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/2d7118b0-b8c0-4461-8435-2b2bde779cb5/image.png" alt=""></li>
</ul>
<blockquote>
<p><strong>결정 경계(Decision Boundary)</strong>란, 머신러닝의 특징 공간 안에서 분류 모델이 각기 다른 클래스(범주)의 데이터를 나누는 기준이 되는 선, 면, 혹은 초평면을 의미한다. (샘플들의 카테고리를 구분할 수 있는 초평면) 쉽게 말해, 서로 다른 클래스에 속하는 데이터들을 구분하기 위한 경계라고 할 수 있다.</p>
<ul>
<li>N차원 공간상의 결정 경계 차원은 N-1이다. (2차원 - 직선, 3차원 - 평면, 3차원 이상 - 초평면 등)</li>
<li>원점에서 시작하는 벡터 w와 직교하고, 거리가 b인 직선의 방정식 ($w^Tx + b = 0$)</li>
<li>결정 경계는 입력 공간을 둘로 나누는 하이퍼플레인이며, 하이퍼플레인은 <strong>N차원 입력이라면 N-1차원의 표면</strong>이 됨. 그 경계는 보통 <strong>$w^T x + b = 0$</strong>으로 나타나며, 이는 <strong>벡터 w에 직교하는 하이퍼플레인</strong>임.</li>
</ul>
</blockquote>
<blockquote>
<p>참고 )</p>
<ul>
<li>최적의 결정 경계선을 찾기 위해선 독립 변수가 k개일 때, 최소 k+1개의 서포트 벡터가 필요하다.</li>
</ul>
</blockquote>
<h2 id="라그랑주-승수법-lagrange-multiplier-method">라그랑주 승수법 (Lagrange multiplier method)</h2>
<p> 라그랑주 승수법은 제약이 있는 최적화 문제를 푸는 방법을 의미한다. 즉, 목적 함수와 제약사항이 존재할 때, 제약사항을 목적 함수로 옮겨줌으로써 제약이 없는 단순한 문제로 바꿔주는 방법이다. 이를 위해 모든 제약식에 라그랑주 승수(Lagrange Multiplier) λ를 곱하고 등식 제약이 있는 문제를 제약이 없는 문제로 바꾸어 문제를 해결한다.</p>
<p> 예를 들어 $\frac{1}{2(w*x - y)^2}$가 목적 함수이고, 이 함수를 minimize해야 최적화되는 것이며, w가 무조건 0 ~ 0.3 사이여야 한다고 했을 때, 0 &lt;= w &lt;= 0.3이라는 조건은 제약사항이다.</p>
<p> 제약사항은 equality constraints, inequality constraints로 나눌 수 있으며, equality constraints는 h(x) = 0으로 나타낼 수 있는 constraint(h(x))를, inequality constraints는 h(x) &lt;= 0을 만족하는 것을 의미한다.</p>
<p> 제약사항을 만족시키는 목적 함수 값을 구하기 위해선 목적 함수와 제약사항 함수가 만나는 지점을 찾아야 하는데, 이를 위해 제약사항 수식도 목적 함수에 녹여 하나의 식으로 표현하는 것이 좋다. (-&gt; 라그랑주 승수법)</p>
<p> <img src="https://velog.velcdn.com/images/inhwaaa_v/post/8fa9615a-7193-4d14-aa47-695198cae42b/image.png" alt=""></p>
<ul>
<li>목적 함수 f(x, y, z, ...)와 제약 조건 g(x, y, z, ...) = 0이 있는 경우 Lagrangian은 다음과 같이 정의됨.<ul>
<li>L(x, y, z, …, λ) = f(x, y, z, …) + λ⋅g(x, y, z,…)</li>
<li>f(x, y, z, …) : 목적 함수</li>
<li>g(x, y, z, …) = 0 : 제약 조건</li>
<li>λ : 라그랑주 승수</li>
</ul>
</li>
<li>제약사항을 포함한 새로운 함수를 만들고, 모든 변수에 대해 미분하고 이 값을 전부 0과 동치시키는 연립 방정식을 풀고 최종적인 x, y 결과를 얻는 방식 = 라그랑주 승수법</li>
<li>SVM은 부등식 제약이 있는 최적화 문제임.</li>
</ul>
<h3 id="kkt-condition"><strong>KKT Condition</strong></h3>
<p> 부등식 제약이 있는 최적화 문제에서 라그랑주 승수법을 적용하기 위해 만족해야 하는 조건</p>
<ol>
<li>라그랑주 함수 L에 대해 모든 독립 변수 $x_i$에 대한 미분 값이 0이다. (Stationarity)</li>
<li>모든 라그랑주 승수 $λ_i$와 제약 조건 부등식의 곱은 0이다. (Complementary Slackness)</li>
<li>모든 라그랑주 승수는 0보다 크거나 같아야 한다. (Dual Feasibility)</li>
</ol>
<h2 id="마진과-조건부-최적화-문제">마진과 조건부 최적화 문제</h2>
<p> 마진은 결정 경계와 가장 가까운 점(서포트 벡터) 사이의 수직 거리(직교 거리)를 의미한다. 이때, 결정 경계의 수식은 $w^Tx + b = 0$이므로 직교 방향이 w의 방향이며, 서포트 벡터는 w 방향으로 ±1만큼 떨어져 있다.</p>
<p> <img src="https://velog.velcdn.com/images/inhwaaa_v/post/199fe0da-ed93-4b6a-8efe-25acf85050c1/image.png" alt=""></p>
<p> SVM의 목적은 이러한 마진을 최대화하는 결정 경계를 찾는 것이다. 따라서 다음과 같은 최적화 문제로 변환할 수 있다. 원래 SVM의 제약 조건은 $w^Tx_i + b ≥ 1$과 $w^Tx_i + b &lt; 1$ 두 가지이다. 이 두 가지를 한 번에 표현하기 위해선 $y_i ∈ {+1, -1}$이라는 성질을 이용할 수 있다. 따라서 두 조건을 포함하여 $y_i(w^Tx_i + b) ≥ 1$로 표현할 수도 있다. 그리고 이를 $y_i(w^Tx_i + b -1 ) ≥ 0$로 변형할 수 있다.</p>
<p> <img src="https://velog.velcdn.com/images/inhwaaa_v/post/3119126b-999a-410d-8f31-3f1fea89f199/image.png" alt=""></p>
<h2 id="hinge-손실-함수">hinge 손실 함수</h2>
<p> hinge 손실 함수는 SVM에서 주로 사용되는 손실 함수로, 다음과 같은 수식으로 표현한다.</p>
<p>$$
Loss=max(0,1−(y′∗y))
$$</p>
<p> hinge 손실은 예측이 잘못된 경우에만 패널티를 부여하며, 올바르게 분류된 경우 손실 값이 0이 된다. 분류가 잘 되는 margin 바깥 부분의 관측값이라면 손실을 무시(loss = 0)하고, 분류가 잘 되지 않는 margin 내의 관측값이라면 손실이 증가하도록 유도한다.</p>
<p>$$
L(w) = \frac{1}{2} | w |^2 + C \sum_{i} \max(0,, 1 - y_i (w^\top x_i) )
$$</p>
<h2 id="소프트-마진soft-margin과-하드-마진-hard-margin"><strong>소프트 마진(Soft Margin)과 하드 마진 (Hard Margin)</strong></h2>
<p> 결정 경계선을 나눌 때, 이상치를 허용하지 않을 경우 과적합 문제가 발생할 수 있다. 따라서 두 범주를 정확하게 나누지는 않지만 마진을 최대화하여 과적합을 방지하는 것을 소프트 마진, 이상치를 허용하지 않는 것을 하드 마진이라고 한다.</p>
<p> 하드 마진의 경우엔 마진이 매우 작아지고, 개별적인 학습 데이터들을 모두 다 놓치지 않으려 하기에 오버피팅 문제가 발생할 수 있다. 반면, 소프트 마진의 경우엔 기준을 너그럽게 잡으므로 어느 정도 이상치를 마진 안에 포함하도록 기준을 설정할 수 있어 오버피팅을 방지할 수 있다는 장점이 있으나, 너무 많은 이상치를 허용할 경우 언더피팅 문제가 발생할 수 있다.</p>
<p> 소프트 마진과 하드 마진을 조정해 주는 매개변수로 C와 Gamma가 사용되며, C는 마진 강화 정도(오류 허용 정도의 역수)를, Gamma는 커널 계수, 즉 얼마나 weight를 주는지를 의미한다. C 값과 Gamma 값은 작을수록 좋다. 그 이유는 C 값이 작으면 오류의 허용 범위가 커져 이상치에 덜 민감해지고, Gamma 값이 작으면 결정 경계가 유연해지는 경향성이 생긴다.</p>
<p> Gamma는 하나의 관측치가 영향력을 행사하는 거리를 조정해 주는 매개변수로, 작은 Gamma 값은 넓은 영향력을 의미하여 더 부드럽고 넓은 결정 경계선을 만들어 내고, 반대의 경우엔 좁은 영향력을 지니며, 더 민감한 결정 경계선을 형성하는데, 이 경우에는 과적합이 발생할 수 있다.</p>
<h2 id="커널-트릭">커널 트릭</h2>
<p> 현실 세계의 데이터는 대부분 Feature가 여러 개이고, 선형적으로만 데이터가 존재하지 않는다. 따라서 SVM으로 Non-Linear Data를 분류하기 위해서는 커널을 통해 차원을 변경해야 한다. 따라서 SVM은 커널 함수(ex. Linear Kernel, Polynomial Kernal 등)를 통해 데이터를 고차원 공간으로 매핑해 선형적으로 구분할 수 있게 만든다. 이는 데이터를 고차원 공간으로 직접적으로 매핑시키는 것이 아니다. 커널을 통해 복잡한 저차원 데이터를 선형 분리가 가능한 고차원으로 매핑시켜 처리하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/80750406-8b51-4c6f-a2e4-a643b758a6bb/image.png" alt=""></p>
<blockquote>
<p><strong>다항 회귀 (Polynomial Regression)</strong> : 비선형 데이터를 학습하기 위해 각 특성의 거듭제곱을 새로운 특성으로 추가하고, 해당 데이터셋에 대해 선형 모델을 학습시키는 방법으로, 각 특성을 주어진 Degree에 따라 제곱해 새로운 특성으로 추가한다.</p>
</blockquote>
<ul>
<li><code>PolynomialFeature(degree=2)</code></li>
<li>하지만, 이런 Feature를 추가하는 방식은 모델 성능에 영향을 미칠 수 있다.  (모델이 느려짐)</li>
<li>이를 해결하기 위해 커널 트릭을 사용한다. 커널 트릭은 높은 차수의 특성을 추가한 것과 같은 효과를 가지면서도 실제로 특성을 추가하지 않는다.<blockquote>
</blockquote>
</li>
</ul>
<h3 id="rbf-커널">RBF 커널</h3>
<ul>
<li>RBF Function은 기존 벡터와 입력 벡터의 유사도를 측정하는 함수로, 가우시안 함수 형태를 지닌다.</li>
</ul>
<p>$$
K(x,z)=exp(−β∥x−z∥2)
$$</p>
<ul>
<li>데이터를 고차원으로 보낼 때, 특정 점들과 얼마나 가까운지(유사한지)를 새로운 특징으로 추가하는 방법</li>
<li>RBF 커널은 모든 데이터 점 x에 대해 기준점(랜드마크) z₁, z₂, z₃…와의 거리 유사도를 Feature로 추가함.</li>
<li>즉, 고차원 벡터 자체가 아닌 고차원 벡터들 간의 dot product만을 계산하여 고차원으로 매핑하는 함수를 직접 만들지 않아도 그 효과를 사용할 수 있게 해준다.</li>
</ul>
<p><strong>정리</strong></p>
<ul>
<li>커널(K)만 있으면 고차원 feature φ(x)의 내적을 직접 계산할 필요 없음</li>
<li>RBF 커널은 &quot;가우시안 거리 기반 유사도&quot;</li>
<li>랜드마크들과의 거리 → 새로운 feature가 되어 고차원으로 올라감</li>
<li>그 공간에서는 직선으로 분리 가능</li>
<li>SVM은 결국 $K(x_i, x_j)$만 이용해 분류함</li>
<li>즉, Feature를 추가하긴 하는데 고차원 함수 계산이 아니라 유사도 계산을 통한 비선형성 추가</li>
</ul>
<h1 id="결정-트리-decision-tree">결정 트리 (Decision Tree)</h1>
<p> 결정 트리란, 일련의 분류 규칙을 통해 데이터를 분류/회귀하는 지도학습 모델 중 하나로, 결과 모델이 트리 구조를 지녀 Decison Tree라는 이름을 지닌다. 아래의 그림처럼 특정 기준(질문)에 따라 데이터를 구분하는 모델을 의사 결정 트리 모델이라고 하며, 한 번의 분기마다 변수 영역을 두 개로 구분한다.</p>
<p> 결정 트리에서 질문이나 정답을 노드(Node)라고 하고, 맨 처음 분류 기준을 Root Node, 중간 분류 기준을 Intermediate Node, 맨 마지막 노드를 Terminal Node 혹은 Leaf Node라고 한다. 이러한 결정 트리의 기본 아이디어는 Leaf Node가 가장 섞이지 않은 상태로 분류되는 것, 즉 복잡성(Entropy)이 낮아지는 방향으로 만드는 것이다</p>
<p> <img src="https://velog.velcdn.com/images/inhwaaa_v/post/151d7b2c-0c1c-4832-a3fc-4362183b7916/image.png" alt=""></p>
<h2 id="불순도-impurity">불순도 (Impurity)</h2>
<p> 불순도(Impurity)란 해당 범주 안에 서로 다른 데이터가 얼마나 섞여 있는지 복잡성을 의미하며, 다양한 개체들이 섞여 있을수록 불순도가 높아진다.</p>
<p> 결정 트리에서 분기 기준을 설정할 땐 현재 노드의 불순도에 비해 자식 노드의 불순도가 감소되도록 설정해야 하며, 현재 노드의 불순도와 자식 노드 불순도의 차이를 Information Gain(정보 이득)이라고 한다.</p>
<p> 이러한 불순도를 수치적으로 나타내기 위한 함수에는 지니 계수(Gini)와 엔트로피(Entropy)가 있다.</p>
<ul>
<li><p><strong>지니 계수(Gini)</strong> : 데이터의 혼합 정도(불순도)를 정량화하는 지표</p>
<ul>
<li>0에서 0.5 사이의 값을 지니며, 값이 낮을수록 데이터의 순도가 낮음을 나타냄.</li>
<li>지니 계수가 0일 경우, 모든 데이터가 동일한 클래스에 속해 매우 순수한 상태임을 나타내고, 0.5에 가까운 값은 데이터가 여러 클래스에 혼합되어 높은 불순도를 지닌다는 것을 의미함.</li>
<li>지니 계수는 주로 의사결정트리에서 노드의 분할 기준으로 사용되며, 각 노드를 가능한 한 순수하게 만들기 위해 Gini 계수를 최소화하는 방향으로 학습이 이루어짐.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/2edd80f4-3d76-4926-8e68-fbd00c4e9886/image.png" alt=""></li>
</ul>
</li>
<li><p><strong>엔트로피 (Entropy)</strong> : 정보 이론에서 유래한 개념으로, 불확실성의 정도를 정령화하는 척도</p>
<ul>
<li>엔트로피는 데이터의 혼합 정도, 즉 데이터의 무질서성을 표현하며, 의사결정트리에서 정보 이득(특정 속성을 기준으로 데이터를 분할했을 때 전체 데이터의 불확실성이 얼마나 감소했는지를 측정하는 지표)을 계산하는데 사용됨. 의사결정트리에서는 분기 시 이런 불순도 값이 줄어드는 방향으로 트리를 형성해야 함.</li>
<li>엔트로피 값은 0에서 $log_2(C)$ 사이의 값을 지니며, 클래스가 두 개인 경우 최댓값은 1임.</li>
<li><strong>엔트로피가 0이라는 것은 모든 데이터가 동일한 클래스에 속해 불확실성이 없음</strong>을 의미하며, <strong>엔트로피가 높아질수록 데이터의 혼합 정도가 증가하고, 불확실성도 커짐.</strong></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/af348feb-cd36-456d-8939-942fbb40f990/image.png" alt=""></p>
<h2 id="정보-이득-information-gain">정보 이득 (Information Gain)</h2>
<p> 정보 이득이란, 분기 이전의 불순도와 분기 이후의 불순도 차이(Parent Node와 Child Node의 불순도 차이)를 의미한다. 예를 들어, 불순도가 1인 상태에서 0.7인 상태로 바뀌었다면 정보 이득은 0.3이다.</p>
<h2 id="결정-트리-구성-단계">결정 트리 구성 단계</h2>
<ol>
<li>Root Node의 불순도 계산</li>
<li>나머지 속성에 대해 분할 후 Child Node의 불순도 계산</li>
<li>각 속성에 대한 Information Gain 계산 후 Information Gain(Root노드와 자식노드의 불순도 차이)이 최대가 되는 분기 조건을 찾아 분기</li>
<li>모든 Leaf Node의 불순도가 0이 될때까지 2,3을 반복 수행한다.</li>
</ol>
<h2 id="가지치기-pruning">가지치기 (pruning)</h2>
<p> 가지치기란, 결정 트리의 특정 노드 및 하부 트리를 제거해 일반화 성능을 높이는 것을 의미한다. 쉽게 말해, 데이터로부터 생성된 복잡한 트리 구조를 단순화하기 위해 가지를 많이 내려 생성된 나무로부터 몇 개의 가지를 쳐내 트리 구조를 단순화하는 것이다. 이러한 가지치기는 사전 가지치기와 사후 가지치기로 나눌 수 있다.</p>
<p> 사전 가지치기는 의사결정트리의 분류 정지 조건을 사전에 설정해 분할을 멈추는 것을 의미하며, 트리 생성 시 트리의 깊이나 사용되는 Feature 개수, Leaf Node Sample 수 등과 같이 관련 하이퍼파라미터를 직접 설정하여 수행한다.</p>
<p> 사후 가지치기는 Full Tree를 생성한 후 모델에 대한 해석과 평가가 완화되는 방향으로 가지를 결합하는 것을 의미한다. 대표적인 사후 가지치기 방법에는 Cost Complexity Pruning이 있으며, 이는 오차 제곱합(SSE)이 최소가 되는 트리를 찾는 방법이다.</p>
<p> 아래의 식에서 오른쪽 두 번째 항은 일종이 패널티로써, 나무의 크기가 클수록 그 값이 커진다. 여기서 $\alpha$는 complexity 파라미터로, 이 값이 클수록 가지치기하는 노드의 수가 많아져 모델이 단순해지고, 작을 수록 복잡한 데이터를 표현할 수 있는 모델이 된다.</p>
<p>$$
Cost(T) = ERR(T) + \alpha L(T)
$$</p>
<ul>
<li>ERR(T) : 검증 데이터에 대한 오분류율</li>
<li>L(T) : Leaf Node 개수 (구조 복잡성)</li>
</ul>
<h2 id="결정-트리-단점">결정 트리 단점</h2>
<ul>
<li>과적합으로 알고리즘 성능이 떨어질 수 있다.<ul>
<li>이를 극복하기 위해서 트리의 크기를 사전에 제한하는 튜닝이 필요하다.</li>
</ul>
</li>
<li>한 번에 하나의 변수만 고려하므로 변수 간 상호작용을 파악하기가 어렵다.</li>
<li>약간의 차이에 따라(레코드의 개수의 약간의 차이) 트리의 모양이 많이 달라질 수 있다.<ul>
<li>두 변수가 비슷한 수준의 정보력을 갖는다고 했을 때, 약간의 차이에 의해 다른 변수가 선택되면 이 후의 트리 구성이 크게 달라질 수 있다.</li>
</ul>
</li>
<li>계단 모양의 결정 경계를 만들기에 학습 데이터의 회전에 민감하다.</li>
<li><strong>이같은 문제를 극복하기 위해 등장한 모델이 바로 랜덤 포레스트이다.</strong><ul>
<li>같은 데이터에 대해 결정 트리를 여러 개 만들어 그 결과를 종합해 예측 성능을 높이는 기법</li>
</ul>
</li>
</ul>
<hr>
<h1 id="코드">코드</h1>
<ul>
<li><strong>SVC</strong> : <code>SVC(*, C=1.0, kernel=&#39;rbf&#39;, degree=3, gamma=&#39;scale’, coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape=&#39;ovr’, break_ties=False, random_state=None)</code><ul>
<li><code>kernel</code> : 커널 유형 지정<ul>
<li>{‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’}</li>
</ul>
</li>
<li><code>C</code> : 오류 허용 정도의 역수로 작을 수록 오류를 많이 허용함. (이상치 덜 민감)</li>
<li><code>gamma</code> : Kernel 계수 (커널의 유연성 : 계수로 조정)<ul>
<li>for ‘rbf’, ‘poly’, ‘sigmoid’</li>
</ul>
</li>
</ul>
</li>
<li><strong>Decision Tree 시각화</strong> : <code>plot_tree(decision_tree, max_depth=None, feature_names=None, class_names=None, filled=False, proportion=False, rounded=False, precision=3, fontsize=None)</code><ul>
<li><code>max_depth</code> : if None, the tree is fully generated.</li>
<li><code>feature_names</code> : list of strings</li>
<li><code>class_names</code> : list of str or bool</li>
<li><code>proportion</code> : True로 지정하면, 값 대신 비율로 표현</li>
<li><code>precision</code> : number of digits of precision</li>
<li><code>filled</code>, <code>rounded</code>, <code>fontsize</code> : 노드의 모양과 폰트크기 설정</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eigen-Decomposition, PCA, SVD]]></title>
            <link>https://velog.io/@inhwaaa_v/Eigen-Decomposition-PCA-SVD</link>
            <guid>https://velog.io/@inhwaaa_v/Eigen-Decomposition-PCA-SVD</guid>
            <pubDate>Sun, 19 Oct 2025 18:38:20 GMT</pubDate>
            <description><![CDATA[<h1 id="벡터와-행렬">벡터와 행렬</h1>
<h2 id="벡터와-행렬의-개념">벡터와 행렬의 개념</h2>
<ul>
<li><p><strong>벡터</strong> : 방향과 크기를 갖는 대상</p>
</li>
<li><p><strong>행렬</strong> : 여러 벡터를 열로 담은 구조</p>
<ul>
<li>행렬은 <strong>교환법칙이 성립하지 않</strong>는다. ($AB ≠ BA$)</li>
<li>행렬은 <strong>분배법칙과 결합법칙이 성립</strong>한다.</li>
<li>$A(B + C) = AB + AC$ (분배법칙), $A(BC) = (AB)C$ (결합법칙)</li>
<li><strong>행렬의 곱셈을 위해선 행렬 A의 열과 행렬 B의 행의 개수가 같아야 한다.</strong></li>
</ul>
</li>
<li><p><strong>텐서</strong> : 3차원 이상의 구조를 갖는 배열</p>
</li>
<li><p><strong>설계 행렬 (Design Matrix)</strong> : 학습 데이터를 담은 행렬 (n samples * d features)</p>
</li>
<li><p>전치 $A^T$, 역행렬 $A^{-1}$, 단위 행렬 $I$</p>
</li>
<li><p>행렬식 $det(A)$ : 선형 변환의 부피 스케일링 (부호는 방향성)</p>
</li>
<li><p><strong>특수한 행렬들</strong></p>
<p>  <img src="https://velog.velcdn.com/images/inhwaaa_v/post/0bbba6bd-1ad8-48fd-a0c3-cef0aadb988c/image.png" alt=""></p>
</li>
</ul>
<h2 id="역행렬-inverse-matrix">역행렬 (Inverse Matrix)</h2>
<p>&nbsp;행렬 A와 곱하면 단위 행렬 I가 나오는 행렬 $A^{-1}$를 역행렬이라고 하며, 이는 <strong>정방행렬(행과 열의 수가 같은 행렬)에 대해서만 정의</strong>되고, 역행렬이 없으면 특이행렬(Singular Matrix)이라고 한다.</p>
<p>$$
\mathbf{A} = \begin{bmatrix}a &amp; b \c &amp; d\end{bmatrix}, \quad\mathbf{A}^{-1} = \frac{1}{ad - bc}\begin{bmatrix}d &amp; -b \-c &amp; a\end{bmatrix}
$$</p>
<p>&nbsp;역행렬은 <strong>곱했을 때 항등 행렬을 만들어 내는 성질</strong>을 지닌다.</p>
<p>$$
AA^{-1} = A^{-1}A = I
$$</p>
<pre><code class="language-python">A = np.array([[1., 2., 3.,],
             [3., 4., 5.],
             [7., 8., 10.]])

print(&quot;A = \n&quot;, A) # 기본 행렬 출력
print(&quot;A^T = \n&quot;, A.T) # 전치된 행렬 출력
print(&quot;det(A) = &quot;, npl.det(A)) # 행렬식 계산
print(&quot;A inverse (via scipy) : \n&quot;, sl.inv(A)) # 역행렬 계산
# 역행렬은 곱했을 때 항등행렬을 만들어 내는 성질을 지님. (AA^{-1} = A^{-1}A = I)
print(&quot;A * A^{-1} = l : \n&quot;, A @ sl.inv(A))</code></pre>
<h2 id="전치행렬-transpose-matrix">전치행렬 (Transpose Matrix)</h2>
<p>&nbsp;전치행렬이란, 행렬의 행과 열을 교환한 행렬을 의미한다.</p>
<p>$$
\mathbf{A} =\begin{bmatrix}1 &amp; 2 &amp; 3 \4 &amp; 5 &amp; 6 \7 &amp; 8 &amp; 9\end{bmatrix}\Rightarrow\mathbf{A}^{\mathrm{T}} =\begin{bmatrix}1 &amp; 4 &amp; 7 \2 &amp; 5 &amp; 8 \3 &amp; 6 &amp; 9\end{bmatrix}
$$</p>
<p>$$
\mathbf{B} =\begin{bmatrix}x &amp; y \z &amp; w\end{bmatrix}\Rightarrow\mathbf{B}^{\mathrm{T}} =\begin{bmatrix}x &amp; z \y &amp; w\end{bmatrix}
$$</p>
<p>$$
\mathbf{C} =\begin{bmatrix}1 &amp; 1 &amp; 1 &amp; 1 \-3 &amp; 5 &amp; -2 &amp; 7\end{bmatrix}\Rightarrow\mathbf{C}^{\mathrm{T}} =\begin{bmatrix}1 &amp; -3 \1 &amp; 5 \1 &amp; -2 \1 &amp; 7\end{bmatrix}
$$</p>
<h2 id="행렬식determinant과-행렬식의-기하학">행렬식(Determinant)과 행렬식의 기하학</h2>
<p>&nbsp;행렬식이란, 행렬 공간을 얼마나 늘이거나 줄이는지, 선형 변환의 부피 스케일링을 의미하는 값이다. 다시 말해, 선형 변환의 부피 변화율을 나타내는 값이다.</p>
<p>$$
A =\begin{bmatrix}a &amp; b \c &amp; d\end{bmatrix}
$$</p>
<p>$$
\text{det}(A) = ad - bc
$$</p>
<p>&nbsp;행렬식은 어떤 행렬의 역행렬 존재 여부에 대한 판별값이 되기도 하며, 행렬식의 값이 0이면 역행렬이 존재하지 않는다. 행렬식은 정방 행렬에 대해서만 정의되며, 기하학적으로는 2차원에서는 2개의 행 백터가 이루는 평행사변형의 넓이를, 3차원에서는 3개의 행 벡터가 이루는 평행 사각 기둥의 부피를 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/41c62169-65dc-4146-98ca-86b064159703/image.png" alt=""></p>
<h3 id="예시-코드">예시 코드</h3>
<p>&nbsp;$A = [a_1, a_2]$ (열벡터 $a_1, a_2$) 일 때, $|det(A)|$는 $a_1, a_2$가 만드는 평행사변형의 면적이다.</p>
<pre><code class="language-python"># 선형 변환 결과 확인하는 코드
# 행렬식 det(A)가 선형 변환의 부피 스케일링 의미하는 거니까
# 결국엔 det(A)만큼 선형변환 적용한 결과를 시각화한 그래프 !
# 그래프 Before, After 보면 각 단위 면적이 3배 커진 걸 확인 가능함.
A2 = np.array([[2., 1.,],
               [1., 2.,]])

print(&quot;det(A2) =&quot;, npl.det(A2)) # A2 행렬식 계산

# 격자와 변환된 격자 시각화
# (11, 11, 2) -&gt; (121, 2)
# np.stack() -&gt; 새로운 차원(축) 추가해 배열 합침
# np.meshgrid() -&gt; 2차원 평면 위에 격자의 좌표망 만듦
grid = np.stack(np.meshgrid(np.linspace(-1, 1, 11), np.linspace(-1, 1, 11)), axis=-1).reshape(-1, 2)
grid_T = grid @ A2.T # 모든 격자점에 선형 변환 적용 (행렬곱)

plt.figure(figsize=(5,5))
plt.scatter(grid[:,0], grid[:,1], s=10) # 원래 점 표시 / s = 점 크기
plt.scatter(grid_T[:,0], grid_T[:,1], s=10, marker=&#39;x&#39;) # 선형 변환 적용된 점 표시
plt.axis(&#39;equal&#39;); plt.title(&quot;Before (•) / After (x) by A2&quot;) # plt.axis(&#39;equal&#39;); x, y축 스케일 맞춤
plt.show()</code></pre>
<h1 id="고유값고유벡터">고유값/고유벡터</h1>
<h2 id="고유값고유벡터--정의와-직관">고유값/고유벡터 : 정의와 직관</h2>
<p>&nbsp;$n<em>n$ 정방 행렬 A에 대해 $Av = \lambda v$를 만족하는 $v ≠ 0$를 A의 고유 벡터, $\lambda$를 고유값이라고 한다. 다시 말해, *</em>고유 벡터<strong>는 행렬 A를 선형 변환했을 때, 선형 변환 A에 의한 변환 결과가 자기 자신의 상수배가 되는 0이 아닌 벡터를 의미하고, **고유값</strong>은 그 상수배 값(고유벡터의 변화되는 스케일 정도)를 의미한다. 이때, A의 고유 벡터는 A에 선형 변환을 취해도 방향이 변하지 않고, 크기만 변한다.</p>
<p>&nbsp;대칭행렬은 $A = A^T$이므로 항상 실수 고유값을 지니고, 고유벡터를 만드는 행렬 A가 대칭행렬일 때 서로 다른 고유값을 가지는 고유벡터는 서로 직교한다. (모든 대칭행렬의 고유벡터는 서로 직교(orthogonal)한다)</p>
<p>&nbsp;여기서 직교는 고유벡터의 값이 서로 수직이어서 두 벡터의 내적 결과가 0이 나온다는 것을 의미한다.</p>
<ul>
<li><p><strong>키워드 : 불변 방향, 크기 스케일링, 대칭행렬, 직교</strong></p>
</li>
<li><p><strong>고유값</strong> : $det(A - \lambda I) = 0$.</p>
<p>  $$
  \begin{vmatrix}a - \lambda &amp; b \c &amp; d - \lambda\end{vmatrix}= \lambda^2 - (a + d)\lambda + (ad - bc) = 0
  $$</p>
</li>
<li><p><strong>고유벡터</strong> : $(A - \lambda I) V = 0$</p>
</li>
</ul>
<h3 id="예시-코드---고유값-분해와-av--λv-검증">예시 코드 - 고유값 분해와 Av = λv 검증</h3>
<pre><code class="language-python">A = np.array([[4., 2.],
              [3., 5.]])
vals, vecs = npl.eig(A) # 고유값 분해
print(&quot;A = \n&quot;, A) # A 행렬 출력
print(&quot;eigenvalues =&quot;, vals) # A의 고유값
print(&quot;eigenvectors (columns) = \n&quot;, vecs) # A의 고유벡터

# 검증 Av = λv
for i in range(len(vals)):
  v = vecs[:, i] # i번째 고유 벡터
  Iv = vals[i] * v # i번째 고유값 * 고유벡터 (λv)
  Av = A @ v # Av 계산
  # Av - λv = 0이 성립하면 Av = λv인 것.
  print(f&quot;i = {i} : ||Av - λv|| = &quot; , npl.norm(Av - Iv))</code></pre>
<h3 id="예시-코드---2d-변환에서의-시각적-의미">예시 코드 - 2D 변환에서의 시각적 의미</h3>
<p>&nbsp;임의의 A가 평면 전체의 방향을 뒤섞어도 고유 벡터의 방향은 그대로 남는다.</p>
<pre><code class="language-python">A = np.array([[2.,1.],
              [0.5,1.5]])

vals, vecs = npl.eig(A) # 고유값 고유벡터 계산
# (289, 2)
grid = np.stack(np.meshgrid(np.linspace(-2,2,17), np.linspace(-2,2,17)), axis=-1).reshape(-1, 2)
grid_T = grid @ A.T # 모든 격자점에 선형 변환 적용 (행렬곱)

plt.figure(figsize=(5,5))
plt.scatter(grid[:,0], grid[:,1], s=8)
plt.scatter(grid_T[:,0], grid_T[:,1], s=8, marker=&#39;x&#39;)
for i in range(2):
  v = vecs[:,i].real # i번째 고유벡터 실수부 (대칭행렬이 아니라 고유벡터가 복소수일 수 있어서)
  # 벡터 크기를 1로 정규화
  v = v/npl.norm(v) # np.linalg.norm(v) : 벡터 v의 길이 계산
  t = np.linspace(-2,2,100)[:,None] # -2 ~ 2까지 100개 점 만들고, (100, 1) 형태로 만듦
  line = t*v[None,:] # (100, 1) (1, 2) -&gt; (100, 2) / 고유벡터 방향으로 -2~2배까지 점 찍음
  plt.plot(line[:,0], line[:,1])
plt.axis(&#39;equal&#39;); plt.title(&quot;Eigen-directions are invariant (lines)&quot;)
plt.show()</code></pre>
<h2 id="고유-벡터의-직교">고유 벡터의 직교</h2>
<p>&nbsp;대칭 행렬의 고유 벡터 집합은 서로 직교하나, 비대칭 행렬에서는 일반적으로 고유벡터가 직교하지 않는다.</p>
<ul>
<li>고유벡터들이 직교하면 $Q^TQ = I$가 성립한다.</li>
</ul>
<pre><code class="language-python">import numpy as np
import numpy.linalg as npl

# (1) 대칭행렬 : 고유벡터 직교성 확인
S = np.array([[2., -1., 0.],
              [-1., 2., -1.],
              [0., -1., 2.]])

valsS, vecsS = npl.eigh(S) # 대칭전용 -&gt; 실수 / 직교 보장 (QR) / 다시 말해, npl.eigh(S)는 고유값과 고유벡터가 실수이고 고유벡터들이 직교하도록 보장함
# 고유벡터들이 직교하면 Q^TQ = I가 성립함.
print(&quot;Symmetric eigenvectors orthogonality (Q^T Q ≈ I) : \n&quot;, vecsS.T @ vecsS)

# (2) 비대칭행렬 : 일반적으로 직교 아님 (Q^T Q ≠ I)
# -&gt; 다르게 표현하면 Off-diagonal 값이 0이 아님 (직교하지 않음)
B = np.array([[1., 2.],
              [0., 3.]])

valsB, vecsB = npl.eig(B)
print(&quot;Nonsymmetric eigenvectors dot products : \n&quot;, vecsB.T @ vecsB)</code></pre>
<h2 id="파워-반복power-iteration과-rayleigh-quotient">파워 반복(Power Iteration)과 Rayleigh Quotient</h2>
<p>&nbsp;Power Iteration이란, 행렬의 가장 큰 고유값과 그에 대응하는 고유벡터를 근사적으로 찾는 알고리즘이다. (행렬을 계속 곱하다 보면 가장 큰 고유값과 고유벡터만 살아남는다는 것)</p>
<p>&nbsp;고유값이 크다 = 정보량이 많다(해당 방향이 데이터의 분산이 크다)는 의미이며, PCA에서는 고유값이 큰 고유벡터만 남기고 지우는 방식으로 차원 축소한다. 따라서 고유값이 큰 순서대로 몇 개의 고유벡터만 남기고 작은 건 버려야 하는데, 고유값이 큰 걸 찾을 수 있는 알고리즘 중 하나가 Power Iteration이다.</p>
<p>&nbsp;Rayleigh Quotient이란, 벡터 x를 A로 변환했을 때 방향/크기의 비율을 측정하는 것이다. 따라서 x가 고유벡터라면, Rayleigh Quotient를 통해 고유값을 계산할 수 있다.</p>
<p>$$
x_{k+1} \leftarrow \frac{A x_k}{| A x_k |} \quad \text{→ 지배적 고유벡터로 수렴.}
$$</p>
<p>$$
R(x) = \frac{x^{\mathrm{T}} A x}{x^{\mathrm{T}} x}\quad \text{→ } x가 \text{ 고유벡터일 때 } R(x) = \lambda.
$$</p>
<pre><code class="language-python">def power_iteration(A, num_iter=30):
  x = rng.normal(size=(A.shape[0], )) # 행렬 A 크기에 맞춰 초기 벡터 난수 생성
  x = x / npl.norm(x) # 정규화
  history = [] # Rayleigh quotient 기록하는 리스트
  # 행렬(A)을 계속 곱하다 보면 가장 큰 고유값과 고유벡터만 살아남는다 -&gt; power_iteration
  # Av = λv이므로 계속 곱하면 그 벡터의 방향은 변하지 않고 크기만 고유값만큼 커지므로
  # 계속 곱하면 가장 큰 고유값을 가진 방향으로 수렴함.
  for _ in range(num_iter):
    x = A @ x # Ax
    x = x / npl.norm(x)
    r = (x @ (A @ x)) / (x @ x) # Rayleigh quotient 계산
    history.append(r)
  return x, np.array(history) # 가장 큰 고유값에 대응하는 고유벡터로 추정된 벡터 return

A = np.array([[4., 2., 0.],
              [2., 3., 1.],
              [0., 1., 2.]])

x_star, rq_hist = power_iteration(A, num_iter=25) # Power Iteration 실행
vals, vecs = npl.eigh(A) # 고유값, 고유벡터 구하기
idx = np.argmax(vals) # 가장 큰 고유값 인덱스 추출
print(&quot;Power-iter dominant eigenvalue (approx) = &quot;, rq_hist[-1]) # 마지막 반복에서의 Rayleigh Quotient (행렬 A의 가장 큰 고유값)
print(&quot;True dominant eigenvalue = &quot;, vals[idx])

plt.figure()
plt.plot(rq_hist)
plt.xlabel(&quot;iteration&quot;); plt.ylabel(&quot;Rayleigh quotient&quot;)
plt.title(&quot;Convergence of Rayleigh quotient to dominant λ&quot;)
plt.show()</code></pre>
<h1 id="고유값-분해-eigen-decomposition">고유값 분해 (Eigen-Decomposition)</h1>
<p>&nbsp;고유값 분해 (Eigen-Decomposition)란, 행렬을 고유값과 고유벡터를 통해 분해하는 기법으로, 어떤 정방행렬 A를 $A = QΛQ^{-1}$의 형태로 쪼개는 것을 의미한다. 이때, Q는 A의 고유벡터를 열에 배치한 행렬, Λ는 고유값을 대각선에 배치한 대각행렬을 의미한다.</p>
<p>&nbsp;이렇게 고유값 분해를 수행하면 행렬을 그 행렬만의 좌표계(고유 벡터)로 바꾸어 단순 대각 형태로 만들어 주며, 이는 PCA에서 주로 활용된다. 고유값이 크다는 것은 그 방향에 데이의 정보량이 많다는 의미이기에 정보량이 큰 데이터만 남기고 차원을 축소할 수 있기 때문이다.</p>
<ul>
<li>대각화 가능하다 = 행렬 A를 고유벡터와 고유값으로 분해할 수 있다</li>
<li>다시 말해, 고유값 분해가 가능하고, $A = QΛQ^{-1}$의 형태로 표현이 가능하다는 것을 의미함.</li>
</ul>
<p>&nbsp;고유값 분해를 해주면, 대각행렬 Λ가 단순 스케일 행렬임을 볼 수 있다. (이는 표준 기저에서 봤을 땐 복잡한 변환이었는데, 좌표계를 고유벡터 기준으로 바꿨더니 A가 단순히 각 좌표 성분을 $\lambda_i$배 해주는 행렬로 보이더라! 라는 느낌으로 이해하면 된다.)</p>
<p>&nbsp;Q의 열벡터들을 고유벡터로 구성한 이유는 결국엔 이 벡터들이 만드는 좌표계가 고유기저이기 때문이고, 결국 고유값 분해는 이 고유 기저를 기준으로 행렬을 분해하는 것을 의미한다고 볼 수 있다.</p>
<pre><code class="language-python"># 일반 행렬의 고유값 분해
A = np.array([[4.,2.],
              [3.,5.]])

vals, vecs = npl.eig(A) # 고유값과 고유벡터 계산
V = vecs
L = np.diag(vals) # 대각행렬 만드는 함수
A_rec = V @ L @ npl.inv(V) # A = VΛV^{-1}
print(&quot;||A - VΛV^{-1}||&quot;, npl.norm(A - A_rec)) # A와 고유값 분해된 값 비교

# 대칭 행렬의 고유값 분해
# 대칭 (스펙트럴 정리(Spectral Theorem) - 대칭행렬은 항상 직교대각화 가능하다는 것) 확인
S = np.array([[2.,-1.],
              [-1.,3.]])
valsS, QS = npl.eigh(S) # 대칭전용 -&gt; 실수 / 직교 보장 (QR)
# 직교행렬이면 역행렬이 전치행렬
# QQ^{T} = Q^{T}Q = I이고, QQ^{-1} = I이므로 Q^{-1} = Q^T
S_rec = QS @ np.diag(valsS) @ QS.T # A = QΛQ^{-1} = QΛQ^{T}
print(&quot;||S - QΛQ^T|| =&quot;, npl.norm(S - S_rec)) # S와 고유값 분해된 값 비교</code></pre>
<h2 id="pca--공분산의-고유-분해">PCA : 공분산의 고유 분해</h2>
<h3 id="pca의-절차-code-example">PCA의 절차 (Code Example)</h3>
<ol>
<li>데이터의 평균을 0으로 정규화</li>
<li>공분산행렬 계산 - 공분산행렬은 대칭행렬이기에 항상 고유값 분해가 가능하다. 또한, 공분산은 데이터의 분산 구조를 담는 행렬이기에 공분산 행렬의 고유 벡터는 데이터의 분산이 최대/최소인 방향을 나타낸다. 이를 주성분 벡터라 부를 수 있고, 고유값은 그 방향에서의 분산 크기(정보량)를 의미한다.</li>
<li>고유값 분해</li>
<li>주성분 선택</li>
<li>차원 축소 및 원래 데이터 X를 선택된 주성분 벡터 W에 투영 (Z=XW)</li>
</ol>
<pre><code class="language-python">from sklearn.datasets import load_digits

digits = load_digits()
X = digits.data.astype(float)
X = X - X.mean(axis = 0, keepdims=True) # 정규화
n = X.shape[0] # 데이터 샘플 개수
C = (X.T @ X) / n # 공분산행렬

vals, vecs = npl.eigh(C) # 대칭전용 -&gt; 실수 / 직교 보장 (QR)
idx = np.argsort(vals)[::-1] # 고유값 내림차순 정렬 후, 가장 큰 고유값이 첫 번째가 되도록 순서 바꿈
vals, vecs = vals[idx], vecs[:, idx] # 고유벡터도 같은 순서 정렬

explained = vals / vals.sum() # 고유값이 전체 분산에서 차지하는 비율
print(&quot;Top-10 explained variance ratio : \n&quot;, explained[:10])

def pca_project(X, vecs, k):
  W = vecs[:, :k] # 앞에서부터 k개 주성분 벡터 선택 (64*k)
  Z = X @ W # Z = XW (원래 데이터 X를 선택된 주성분 벡터 W에 투영)
  return Z, W

def pca_reconstruct(Z, W):
  return Z @ W.T # ZW^T (축소된 데이터 Z를 원래 차원으로 복원)

# k = 주성분 개수
for k in [5, 10, 20, 40]:
  Z, W = pca_project(X, vecs, k)
  # digits.data.mean(axis=0, keepdims=True) - 원래 X에서 평균을 뺐었으므로 다시 더해줘서 복원
  Xk = pca_reconstruct(Z, W) + digits.data.mean(axis=0, keepdims=True)
  fig = plt.figure(figsize=(6, 2))
  for i in range(8):
    ax = plt.subplot(1, 8, i+1)
    ax.imshow(Xk[i].reshape(8,8), interpolation=&quot;nearest&quot;)
    ax.set_xticks([]); ax.set_yticks([])
  plt.suptitle(f&quot;PCA reconstruction with k = {k}&quot;)
  plt.tight_layout()
  plt.show()</code></pre>
<h1 id="특이값-분해-singular-value-decomposition">특이값 분해 (Singular Value Decomposition)</h1>
<p>&nbsp;고유값 분해는 정방 행렬만 분해 가능하다. 따라서 정방 행렬이 아닌 경우(비정방행렬)에 대해서도 분해를 수행하기 위해선 특이값 분해를 활용해야 한다. 이는 임의의 행렬을 $A=UΣV^T$의 형태로 행렬을 쪼개는 것을 의미한다.</p>
<p>&nbsp;이때, 고유값은 행렬 A가 직사각행렬이면 입출력 차원이 달라 상수배(고유값)을 직접 정의할 수 없다. 따라서 길이(스케일 크기)만 추출하기 위해 $AA^T$, $A^TA$의 결과가 항상 정방 행렬이고 대칭행렬인 점을 활용한다.</p>
<ul>
<li><p>$U$ (왼쪽 특이행렬) :  $AA^T$의 고유 벡터를 열에 배치한 n*n 행렬</p>
</li>
<li><p>$S$  (특이값 리스트) : $AA^T$의 고유값의 제곱근을 대각선에 배치한 대각 행렬 (n*m 행렬)</p>
</li>
<li><p>$V^T$ (특이행렬) : $A^TA$의 고유 벡터를 열에 배치한 m*m 행렬</p>
<p>  $$
  C = \frac{1}{n} X^T X = V \left( \frac{\Sigma^2}{n} \right) V^T ;\Rightarrow; \text{PCA 고유벡터} = V, \quad \text{고유값} = \frac{\Sigma^2}{n} 
  $$</p>
</li>
</ul>
<p>&nbsp;실제 차원 축소(PCA)의 구현에는 비정방행렬에도 적용이 가능한 보다 일반적인 방법인 SVD가 주로 사용된다.</p>
<pre><code class="language-python"># SVD로 얻은 주성분 벡터랑 공분산 고유분해로 얻은 주성분 벡터가 같은지 검증하는 코드

# PCA의 목표는 데이터가 가장 많이 퍼져있는 방향을 찾는 거
# 공분산 이용하는 이유 -&gt; 데이터의 분산 구조를 담는 행렬이기 때문
# 따라서 공분산 행렬의 고유벡터 = 데이터 분산이 최대/최소인 방향
# = 주성분 벡터
# 고유값 = 그 방향에서의 분산 크기

# SVD로 얻은 V와 공분산 고유벡터 비교
from sklearn.datasets import load_digits
digits = load_digits()
Xraw = digits.data.astype(float)
Xc = Xraw - Xraw.mean(axis=0, keepdims=True)

# 행렬 Xc에 대해 SVD 분해
# U : 왼쪽 특이행렬 / S : 특이값 리스트 / VT : 오른쪽 특이행렬
U, S, VT = npl.svd(Xc, full_matrices=False)
V = VT.T
C = (Xc.T @ Xc) / Xc.shape[0] # 공분산
valsC, vecsC = npl.eigh(C) # 고유값, 고유벡터
idx = np.argsort(valsC)[::-1] # 가장 큰 고유값
vecsC = vecsC[:, idx] # 가장 큰 고유값에 대응하는 고유벡터

# 주성분 부호/순서 불확정성을 감안해 상관을 비교
# V = SVD에서 나온 오른쪽 특이벡터들 -&gt; 공분산 행렬의 고유벡터와 같아야 함.
# C = 공분산행렬을 직접 고유분해해서 얻은 고유벡터
# V, C는 둘 다 직교행렬이므로 두 직교행렬을 곱하면 직교 행렬이 되며,
# 두 값이 완전히 같으면 곱한 값이 단위행렬이 됨.
# 따라서 SVD의 V와 공분산 고유벡터가 같은지 확인
# SVD 결과 = PCA임을 증명 가능함. (오른쪽 특이벡터랑 공분산 행렬이 같아야 PCA 결과 = SVD 결과가 성립)
corr = np.abs(np.diag(V.T @ vecsC))
print(&quot;Diagonal of |V^T * eigenvector(C)| (shold be near 1) : \n&quot;, corr[:10])</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[과대적합, 규제, Bias-Variance Trade-off]]></title>
            <link>https://velog.io/@inhwaaa_v/%EB%AA%A8%EB%8D%B8-%EC%84%A0%ED%83%9D%EA%B3%BC-%EA%B3%BC%EB%8C%80%EC%A0%81%ED%95%A9-%EB%B0%8F-%EA%B7%9C%EC%A0%9C</link>
            <guid>https://velog.io/@inhwaaa_v/%EB%AA%A8%EB%8D%B8-%EC%84%A0%ED%83%9D%EA%B3%BC-%EA%B3%BC%EB%8C%80%EC%A0%81%ED%95%A9-%EB%B0%8F-%EA%B7%9C%EC%A0%9C</guid>
            <pubDate>Sun, 19 Oct 2025 15:43:46 GMT</pubDate>
            <description><![CDATA[<h1 id="모델-선택과-과대적합">모델 선택과 과대적합</h1>
<h2 id="과소적합과-과대적합">과소적합과 과대적합</h2>
<p>&nbsp;과소적합(underfitting)이란, 모델이 훈련 데이터를 충분히 학습할 만큼 복잡하지 않고 단순하거나, 학습이 충분히 이뤄지지 않아 데이터에 존재하는 패턴들을 제대로 학습하지 못하는 것을 의미한다. 다시 말해, 훈련 데이터의 복잡도에 비해 모델의 형태가 단순하여 발생하는 문제이다. 이러한 과소적합 문제가 발생한 경우, 모델이 학습 데이터셋(Training Dataset)과 테스트 데이터셋(Test Dataset) 모두 현저히 낮은 성능을 보인다. </p>
<p>&nbsp;일반적으로 과소적합 문제는 모델의 복잡도가 낮거나, 학습 시간이 부족하거나, Feature Engineering이 미흡하여 발생한다. 따라서 복잡한 모델을 사용하거나, 에폭 수를 증가시켜 더 오래 학습시키거나, Feature Engineering을 보강하는 방법을 통해 해결한다.</p>
<p>&nbsp;과대적합(overfitting)이란, 모델이 지나치게 복잡해 학습 데이터의 패턴 뿐만 아니라 노이즈까지 학습하여 훈련 데이터가 아닌 다른 데이터에 대해선 일반화 성능이 떨어지는 현상을 의미한다. 이는 훈련 데이터의 복잡도에 비해 모델의 형태가 너무 복잡하여 훈련 데이터의 패턴 뿐만 아니라 노이즈까지 학습한 경우에 발생한다. 이러한 과대적합이 발생한 경우, 모델이 학습 데이터셋(Training Dataset)에서는 성능이 매우 높지만, 테스트 데이터(Test Dataset)에 대해서는 현저히 낮은 성능을 보인다.</p>
<p>&nbsp;일반적으로 과대적합 문제는 모델의 복잡도가 지나치거나, 데이터의 양이 적거나, 정규화가 부족할 때 발생한다. 따라서 L1, L2 등의 정규화 기법을 적용하거나, Dropout, Early Stopping, Data Augmentation, 모델 단순화 등의 방법들을 통해 해결한다.</p>
<p>&nbsp;과소적합과 과대적합 모두 일반화 성능이 떨어진다는 점에선 공통점을 지니나, 과소적합은 모델이 너무 단순해 패턴을 제대로 학습하지 못해 발생한 문제를, 과대적합은 모델이 너무 복잡해 훈련 데이터의 패턴과 노이즈까지 모두 학습해 버린 문제를 의미한다는 차이가 있다.</p>
<p>&nbsp;수능으로 예를 들면, 과소적합은 공부를 하지 않아 교과서 개념도 제대로 못 익혀 기출 문제도 제대로 못 푸는 경우를 의미하고, 과대적합은 모의고사 문제 등 특정 시험에만 맞춰져 새로운 유형이 나오는 실제 수능에선 시험을 잘 못 보는 경우를 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/765d9c7d-fd5b-49ca-bf27-49b40b40892b/image.png" alt=""></p>
<h2 id="bias-variance-trade-off">Bias-Variance Trade-off</h2>
<h3 id="bias와-variance">Bias와 Variance</h3>
<p>&nbsp;Bias는 모델의 단순함으로 인한 오차, 다시 말해  모델의 예측값이 실제 값과 얼마나 멀리 떨어져 있는지를 나타내며, Bias가 크다는 것은 모델이 틀린 가정 위에서 학습한다는 것을 의미한다. (ex) 비선형 관계를 가진 데이터를 선형 회귀로 학습하는 것</p>
<p>&nbsp;Variance는 데이터 변화에 민감하게 반응하는 정도(같은 데이터 분포에서 다른 학습 데이터 샘플을 사용했을 때 모델이 얼마나 다르게 학습되는지)를 의미한다. 다시 말해, 훈련 데이터가 조금만 바뀌어도 모델의 예측값이 크게 변하는 정도, 즉 불안정성을 의미하며, Variance가 크다는 것은 모델이 훈련 데이터에 과하게 민감하다는 것을 의미한다. (ex) 다항식 차수가 매우 높은 모델은 학습 데이터에 대해선 성능이 좋으나, 데이터가 조금만 달라져도 정확도가 낮아지는 과적합 문제가 발생함.</p>
<p>&nbsp;따라서 Bias가 크면 모델이 너무 단순해 패턴을 제대로 못 잡는 Underfitting 문제가 발생하고, 이로 인해 항상 일정한 방향으로 오차가 남는다.  Variance가 크면 모델이 너무 복잡해져 데이터에 들어있는 작은 변화나 노이즈까지 민감하게 반응하는 현상인 Overfitting 문제가 발생하며, 데이터셋이 조금만 바뀌어도 모델의 결과가 크게 달라진다. 이러한 Bias-Variance는 Trade-off 관계에 있으며, 이는 모든 모델의 한계가 된다. 따라서 이를 해결하기 위해선, 두 요소 사이의 균형점을 찾는 것이 중요하다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/d286b421-6d39-4679-8e4a-0d244d1e72d6/image.png" alt=""></p>
<h3 id="bias-variance-trade-off-1">Bias-Variance Trade-off</h3>
<p>&nbsp;편향-분산 트레이드오프(Bias-Variance Trade-off)란, 모델이 복잡해질 수록 편향은 감소하고 분산은 증가하는 경향이 있다는 것이다. 다시 말해, 복잡한 모델은 데이터의 노이즈까지 학습하려 하므로 편향은 줄지만(과소적합 감소), 데이터가 조금만 바뀌어도 예측이 크게 변동하여(분산 증가) 과대적합될 위험이 커진다는 것이다. 이러한 상황에서 일반화 성능이 좋은 모델, 다시 말해 테스트 데이터에 대해 높은 성능을 보이는 모델을 만들기 위해선 결국 가능한 <strong>작은 바이어스와 낮은 분산을 가지도록 하는 모델을 선택</strong>하는 것이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/7ce20975-9810-409b-95f5-0b76f4109cbd/image.png" alt=""></p>
<h3 id="bias-variance-trade-off-수식-표현">Bias-Variance Trade-off 수식 표현</h3>
<ul>
<li>Bias의 제곱은 실제값과 예측값들의 평균의 차이를 의미하며, 낮은 Bias는 평균적으로 우리가 실제값에 근사하게 예측을 수행한다는 것을 의미하며, 높은 Bias는 실제값과 예측값의 평균이 멀리 떨어져있는 poor match를 의미함. (모델의 예측값이 실제 값과 얼마나 떨어져 있는가)</li>
<li>Variance는 예측값들의 평균으로부터 특정 예측값이 어느 정도 퍼져있는가를 의미하며, 낮은 Variance는 들어오는 데이터에 따라 예측 값이 크게 바뀌지 않으나, 높은 Variance는 들어오는 데이터에 따라 예측 값이 크게 바뀌므로 poor match를 의미함. (예측 모델의 복잡도)</li>
<li>Bias-Variance는 Trade-off 관계이므로, 이 세 가지 값을 다 더한 값이 최소가 되는 모델이 일반화 성능이 좋은 가장 좋은 모델임.</li>
</ul>
<p>$$
Expected  Error = (Bias^2) + Variance + \sigma^2
$$</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/49006f1c-1754-4d13-9ebc-2bcbd48b2de4/image.png" alt=""></p>
<blockquote>
<p><strong>일반화 성능이 좋은 모델이란?</strong>
  → 학습 데이터셋과 테스트 데이터셋에 대한 성능이 비슷한 모델을 의미함.</p>
</blockquote>
<blockquote>
</blockquote>
<p><strong>데이터셋의 구분</strong></p>
<ul>
<li>Training - 모델을 학습할 때 사용하는 데이터</li>
<li>Vaildation - 모델 학습 하이퍼파라미터 최적화와 과적합 방지, 모델 학습 중 모델 일반화 성능 검증을 위해 활용되는 데이터<ul>
<li>데이터의 양이 부족한 경우, 검증 데이터를 마련하기 어려울 수 있는데, 이 경우 k-fold CV를 활용함.</li>
<li>이는 Training set을 k개의 fold로 나누어 k-1개의 fold를 train set, 1개의 fold를 validation set으로 설정해 모델의 성능을 검증하는 방식을 의미하며, 절차는 다음과 같다.<ol>
<li>데이터를 k개(=fold)로 나눈 뒤, k-1개를 학습용으로 사용하고, 남은 한 개를 검증용으로 사용하여 훈련 및 평가를 진행함. (k-1개로 학습, 1개로 검증)</li>
<li>이 과정을 fold마다 번갈아 가며 k번 반복함. (모든 데이터가 한 번씩은 검증용으로 사용됨)</li>
<li>마지막에 k번의 검증 성능을 평균 내어 최종 성능으로 활용함.</li>
</ol>
</li>
</ul>
</li>
<li>Test - 실제 평가 시 사용되는 데이터로 최종적으로 모델 성능을 평가하는 데이터이므로 학습에 관여하지 않음.<ul>
<li>테스트 데이터는 최종 평가용으로만 활용하고, 모델 학습 과정에는 절대 관여해선 안 된다. 테스트 데이터를 검증 데이터로 활용하지 않는 이유도 이 이유에서이다. 모델 개발 과정에서 테스트 데이터를 검증 용도로 사용하면, 모델이 그 데이터를 직접적으로 학습하지 않았더라도 하이퍼파라미터 최적화나 모델 구조 선택에 간접적으로 활용되어 실제 새로운 데이터에 대한 일반화 성능을 왜곡한다.</li>
<li>테스트 데이터는 반드시 모델이 본 적 없는 완전히 새로운 데이터를 가정하고 평가해야 한다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>파라미터와 하이퍼파라미터의 구분</strong></p>
</blockquote>
<ul>
<li><strong>파라미터</strong> : 모델이 학습하는 변수</li>
<li><strong>하이퍼파라미터 :</strong> 학습을 위해 초기에 설정되어야 하는 상수나 변수 (파라미터를 위한 파라미터)</li>
</ul>
<h1 id="규제">규제</h1>
<h2 id="규제란">규제란?</h2>
<p>&nbsp;규제(Regularization)란, 모델의 복잡도를 낮추어 과적합을 방지하고 일반화 성능을 향상시키기 위해 사용되는 기법으로, 모델이 학습 데이터에만 특화되지 않고, 일반화가 가능하도록 일종의 규제를 가하는 것을 의미한다.  복잡한 모델은 작은 데이터 변화에도 쉽게 모델이 바뀔 수 있고, 이로 인해 일반화 성능이 떨어지기 때문에 규제를 적용한다. 가장 일반적인 규제 기법으로는 L1 규제(Lasso)와 L2 규제(Ridge)가 있다.</p>
<p>&nbsp;L1, L2 규제는 모델의 과적합 방지를 위해 사용하는 기술로, 모델의 손실 함수에 각각 L1 Loss Function, L2 Loss Function을 더해 주는 것을 의미한다. 다시 말해, 손실 함수에 패널티 항을 추가하는 것이다.</p>
<p>$$
E(b)<em>{new} = E(b)</em>{org} + penalty
$$</p>
<p>&nbsp;L1 규제는 손실 함수에 가중치의 절댓값을 더하는 형태를 지닌다. 따라서, 모델이 너무 큰 가중치를 가지면 손실이 커지게 되고, 학습 과정에서 손실을 최소화하기 위해 가중치를 줄이는 방향으로 업데이트가 이루어진다.</p>
<p>&nbsp;즉, 가중치가 클수록 비용이 커지고, 이를 줄이기 위해 가중치가 0을 향해 수렴하도록 학습된다. 그 결과, 큰 가중치는 억제되고 중요하지 않은 가중치는 완전히 0으로 수렴하여, 과적합이 줄고 더 단순하고 안정적인 모델을 만들 수 있다. (Feature Selection → 모델이 희소(sparse)해져 해석이 쉬워짐)</p>
<blockquote>
</blockquote>
<ul>
<li>Feature selection : 모델 예측에 불필요한 변수를 제거하는 과정</li>
<li>L1 규제에서의 기울기 범위는 [-1, 1], 즉 상수 범위로 일정하다. 따라서 w가 크면 업데이트 되는 w 값이 작아지고, w가 작으면 업데이트 되는 w 값이 커진다. 이러한 이유에 가중치들이 0 근처로 수렴하도록 규제할 수 있다.</li>
</ul>
<p>$$
\text{Cost} = \text{Loss} + \lambda \sum_{i=1}^{n} |w_i|
$$</p>
<p>$$
w^{(t+1)} = w^{(t)} - \eta \left( \frac{\partial \text{Loss}}{\partial w} + \lambda \cdot \text{sign}(w) \right)
$$</p>
<p>&nbsp;L2 규제는 손실 함수에 가중치 제곱의 합을 더하는 형태로, 큰 가중치를 더 강하게 억제할 수 있고, 모든 가중치가 조금씩 줄어들어 비슷한 크기의 값으로 퍼지므로(spread out) 파라미터가 분산되는 효과를 얻을 수 있다. 하지만, 가중치의 제곱 값을 활용하기에 L1 Regulazation에 비해 이상치에 더 민감하게 반응한다는 특징이 있다.</p>
<p>$$
\text{Cost} = \text{Loss} + \lambda \sum_{i=1}^{n} w_i^2
$$</p>
<p>$$
w_i^{(t+1)} = w_i^{(t)} - \eta \left( \frac{\partial \text{Loss}}{\partial w_i} + 2\lambda w_i^{(t)} \right)</p>
<p>$$</p>
<blockquote>
<p>*<em>참고 ) *</em>
 선형 회귀 모델에서 L1 규제를 주는 것을 Lasso, L2 규제를 주는 것을 Ridge라고 한다.</p>
</blockquote>
<h2 id="명시적-규제">명시적 규제</h2>
<p>&nbsp;명시적 규제란, 모델에 직접적으로 부하를 주는 기법들을 의미한다.</p>
<h3 id="가중치-감쇠-weight-decay">가중치 감쇠 (Weight Decay)</h3>
<ul>
<li>가중치를 강제로 축소하는 방법 (위에서 말한 L1, L2 와 같규제 항을 추가하는 것)</li>
<li>가중치를 작게 유지함으로써 일반화 능력을 향상시킴.</li>
</ul>
<h2 id="암시적-규제">암시적 규제</h2>
<p>&nbsp;암시적 규제란, 모델에 직접적으로 부하를 주는 것이 아닌, 모델을 조기에 종료하거나, 모델을 여러 개 사용하는 등의 방법들을 의미한다. 모델을 직접적으로 변형시키거나, 부하를 주진 않는 방법, 다시 말해 손실 함수에 명시적인 제약항을 추가하지 않고 모델의 학습 과정이나 구조적 특성으로 일반화를 유도하는 기법이다.</p>
<h3 id="드롭아웃-dropout">드롭아웃 (Dropout)</h3>
<p>&nbsp;드롭아웃이란, 학습 단계마다 무작위로 일부 뉴런을 비활성화하여 신경망이 특정 뉴런에만 과하게 의존하는 것을 방지하기 위한 기법을 의미한다. 이는 과적합을 줄이고 일반화 성능을 향상시키는 효과가 있는데, 그 이유는 학습 과정에서 특정 가중치/뉴런에 과하게 의존하지 않고, 다양한 경로를 학습할 수 있게 하기 때문이다.</p>
<p>&nbsp;다시 말해, Dropout은 co-adaptation(신경망에서 특정 뉴런들이 서로 강하게 의존되는 현상)을 방지하는 효과가 있다. 특정 뉴런이 특정 feature만 잘 잡아내서 다른 뉴런이 그 feature를 잡아내지 않고, 다른 역할만 하면 뉴런들이 독립적으로 일반화된 표현을 학습하지 못 하는 현상이 발생할 수 있는데, Dropout은 이를 방지할 수 있고, 이를 통해, 각 뉴런이 다른 뉴런에 과도하게 의존하지 않고 자신만의 독립적 표현을 학습할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/1d636c46-d86d-4aa5-8a8c-64b52edc1a83/image.png" alt=""></p>
<h3 id="early-stopping">Early Stopping</h3>
<ul>
<li>모델이 과적합되기 전 훈련을 멈추는 정규화 기법으로, 모델 훈련 중 검증 데이터의 성능을 모니터링하고, 검증 데이터의 손실이 일정 횟수 동안 개선되지 않으면 학습을 중단함.</li>
<li>다시 말해, 검증 데이터의 오류가 최저인 지점을 만나면 조기에 학습을 종료하는 기법을 의미함. 이를 통해 모델이 훈련 데이터에 과적합되기 전에 일반화 성능이 가장 좋은 지점에서 종료 가능함.</li>
</ul>
<h3 id="data-augmentation">Data Augmentation</h3>
<ul>
<li>학습을 위한 정보의 양을 늘리기 위해/더 많은 특징을 뽑아내기 위해 Data Augmentation 기법을 활용할 수 있으며, 이는 기존의 데이터를 변형하여 새로운 데이터를 생성하는 기법임.</li>
</ul>
<h3 id="ensemble">Ensemble</h3>
<p>&nbsp;앙상블 학습의 핵심은 여러 개의 Weak Classifier를 결합해 Strong Classifier를 만드는 것이며, 이를 위해 Bagging이나 Boosting 등의 방법을 활용할 수 있다. </p>
<ul>
<li>Bagging : 샘플을 여러 번 뽑아 각 모델을 학습시켜 결과물을 집계하는 방법, 다시 말해 같은 모델 구조를 여러 번 학습시키되, 데이터를 무작위로 다르게 샘플링해 학습시키는 방</li>
<li>Boosting : 가중치를 활용해 Weak Classifier를 Strong Classifier를 만드는 것으로, 모델의 예측 결과에 따라 가중치를 부여하여 다음 모델에 영향을 줌.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/9bde946f-7ecc-457e-91bd-a5e88417e42d/image.png" alt=""></p>
<ul>
<li>Voting : 서로 다른 여러 모델의 결과를 결합하는 방법<ul>
<li>hard Voting : 각 분류기가 뽑은 최종 클래스에 대해 다수결 투표로 결정하는 것</li>
<li>Soft Voting : 각 분류기가 예측한 확률을 평균 내고, 그 중 가장 확률이 높은 클래스를 선택하는 것</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/de0e8c9e-516c-4d73-a979-af26526f9ce5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[특징 공간과 차원의 저주]]></title>
            <link>https://velog.io/@inhwaaa_v/%ED%8A%B9%EC%A7%95-%EA%B3%B5%EA%B0%84%EA%B3%BC-%EC%B0%A8%EC%9B%90%EC%9D%98-%EC%A0%80%EC%A3%BC</link>
            <guid>https://velog.io/@inhwaaa_v/%ED%8A%B9%EC%A7%95-%EA%B3%B5%EA%B0%84%EA%B3%BC-%EC%B0%A8%EC%9B%90%EC%9D%98-%EC%A0%80%EC%A3%BC</guid>
            <pubDate>Sun, 19 Oct 2025 12:54:02 GMT</pubDate>
            <description><![CDATA[<h1 id="특징-공간과-결정-공간">특징 공간과 결정 공간</h1>
<p>&nbsp;특징 공간(Feature Space)이란, 데이터의 각 샘플이 가진 여러 특징(feature)들을 좌표로 표현하는 다차원 공간이다. 쉽게 말해, 특징들이 수치회된 벡터 공간이라고 할 수 있다. 머신러닝에서는 데이터가 특징 공간 내의 점(벡터)으로 표현되며, 모델이 패턴과 관계, 군집 등을 찾아내고 학습할 수 있도록 한다.</p>
<p>&nbsp;이러한 특징 변수가 1개이면 1차원의 특징 공간, 2개이면 2차원의 공간, 세 개면 3차원의 공간, N개이면 N차원의 공간이 형성된다. 이러한 특징 공간을 표현할 땐, 특징과 목푯값을 함께 축으로 표시할 수도 있고, 특징만을 축으로 표현할 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/11f41ccf-0e00-4bec-a1a3-8c3101abc92b/image.png" alt=""></p>
<p>&nbsp;결정 공간(Decision Space)이란, 결과 값에 따라 나눠질 수 있는 공간으로, 모델이 입력 x를 받아 결과를 도출하는 공간을 의미한다. 이는 최종 Output 결정하는 공간이며, 특징 공간의 점이 어떤 클래스에 속하는지 표현한다. 이러한 결정 공간 안에서 클래스를 구분하기 위해선 결정 경계를 활용한다.</p>
<p>&nbsp;결정 경계(Decision Boundary)란, 머신러닝의 특징 공간 안에서 분류 모델이 각기 다른 클래스(범주)의 데이터를 나누는 기준이 되는 선, 면, 혹은 초평면을 의미한다. 쉽게 말해, 서로 다른 클래스에 속하는 데이터들을 구분하기 위한 경계라고 할 수 있다.</p>
<p>&nbsp;이러한 결정 경계는 특징 공간의 차원에 따라 다른 이름으로 불린다.</p>
<ul>
<li><strong>결정선 (Decision Line)</strong> : 2차원 결정 공간에서 사용되며, 결정 경계가 선 형태로 나타남.</li>
<li><strong>결정면 (Decision Plane)</strong> : 3차원 결정 공간에서 사용되며, 결정 경계가 평면 형태로 나타남.</li>
<li><strong>결정 초평면 (Decision Hyperplane)</strong> : 3차원보다 큰 결정 공간에서 사용되며, 시각적으로 그릴 순 없지만, 수학적으로는 n-1차원의 초평면이 존재하여 n차원 공간 데이터를 나눔. 다시 말해, 결정선과 결정면을 고차원으로 일반화한 개념임.</li>
</ul>
<p>&nbsp;이러한 특징 공간과 결정 공간은, 같은 공간일수도 있고, 아닐 수도 있다. 모델의 각 층이 만들어 내는 특징 공간이 연속적으로 이어지다가 마지막 층에서 결정 공간으로 귀결되기도 하고, 최종 특징 공간에서의 표현이 결정 함수에 의해 변환되어 특징 공간과 결정 공간이 다르게 표현되기도 하기 때문이다.</p>
<p>&nbsp;분류 모델의 목표는 결국 최적의 결정 경계를 찾는 것이며, 모델이 학습을 한다는 것은 주어진 데이터를 가장 잘 나눌 수 있는 결정 경계(선, 면, 초평면)의 위치와 형태를 찾아가는 과정이라고 할 수 있다.</p>
<blockquote>
<p><strong>정리 )</strong></p>
</blockquote>
<ul>
<li>특징 공간 : 입력 데이터가 존재하는 공간으로, 각 축이 입력 변수에 대응함.</li>
<li>결정 공간 : 모델이 특징 벡터 x를 결정 함수 f(x)를 통해 출력으로 사상시킨 공간 (다시 말해, 모델이 내린 분류 결과들이 표현된 공간)</li>
<li>결정 경계 : f(x) 값이 클래스 간 임계점에 해당하는 지점들의 집합</li>
</ul>
<h1 id="차원의-저주-curse-of-dimensionality">차원의 저주 <strong>(Curse of dimensionality)</strong></h1>
<p>&nbsp;차원의 저주(Curse of dimensionality)란, 학습 데이터에 비해 입력 차원의 수가 큰 경우 일정 차원을 기점으로 학습 능력이 급격히 감소하는 현상을 의미한다. 다시 말해, 특징 공간의 차원이 증가하면서 학습 데이터의 수가 특징 공간의 차원의 수보다 적어져 성능이 저하되는 것이다.</p>
<p>&nbsp;차원이 증가할 수록 특징 공간의 부피가 커지고, 개별 차원 내에서의 데이터의 밀도가 희소해지며, 이에 따라 거리 함수가 제대로 작동하지 않고, 계산 비용이 증가하는 등의 문제가 발생하는데, 이러한 문제를 차원의 저주라고 한다.</p>
<p>&nbsp;그러나, 입력 차원 수가 증가한다고 반드시 차원의 저주가 발생하는 것은 아니며, 학습 데이터보다 입력 차원의 수가 많아지는 경우에 차원의 저주 문제가 발생한다. 공간이 희소해짐에 따라, 저차원 데이터에서 패턴을 파악하는 것보다 고차원 데이터에서 패턴을 파악하는데 더 많은 데이터가 필요해지기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/acd9504b-9a30-44fc-b265-b03962723008/image.png" alt=""></p>
<p>&nbsp;이러한 차원의 저주는 차원이 증가함에 따라 동일한 데이터 개수로는 공간을 충분히 채울 수 없게 되어 데이터의 분포가 희소해지기에 발생한다. 입력 데이터의 차원이 증가하면, 특징 공간의 부피가 차원에 따라 기하급수적로 증가하여 데이터 간 거리가 멀어지고, 학습 데이터의 밀도가 낮아진다.</p>
<p>&nbsp;이러한 차원의 저주 문제를 해결하기 위한 이론적인 해결책은 훈련 샘플의 밀도가 충분히 높아질 때까지 데이터를 모아서 훈련 세트의 크기를 키우는 것이다. 그러나, 일정 밀도에 도달하기 위해 필요한 훈련 샘플 수는 차원의 수가 커짐에 따라 기하급수적으로 늘어난다는 문제가 존재한다. 따라서 PCA, SVD 등과 같은 차원 축소 기법들을 통해 학습 결과에 영향을 미치지 않는 불필요한 축을 줄임으로써 차원의 저주를 완화하기도 한다.</p>
<blockquote>
<p><strong>정리 )</strong></p>
</blockquote>
<ul>
<li>“A function defined in high-dimensional space is likely to be much more complex than a function defined in a lower-dimensional space, and those complications are harder to discern” – Friedman</li>
<li>This means that a more complex target function requires denser sample points to learn it well!
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/62912703-b199-4e92-bc28-b7589439f33e/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Fuzzy Logic]]></title>
            <link>https://velog.io/@inhwaaa_v/Fuzzy-Logic</link>
            <guid>https://velog.io/@inhwaaa_v/Fuzzy-Logic</guid>
            <pubDate>Sat, 18 Oct 2025 18:45:39 GMT</pubDate>
            <description><![CDATA[<h1 id="1-퍼지논리">1. 퍼지논리</h1>
<h2 id="11-퍼지-논리fuzzy-logic란">1.1 퍼지 논리(Fuzzy Logic)란</h2>
<p>&nbsp;Fuzzy라는 단어는, 흐릿한, 불분명한이라는 의미를 가진다. 여기서도 알 수 있듯, <strong>퍼지 논리(Fuzzy Logic)</strong>는 명확하게 구분되지 않는 개념을 수학적으로 다루는 방법, 다시 말해 정확하지 않거나(imprecise) 불완전한(incomplete) 정보를 처리하는 데 유용한 논리 체계를 의미한다. 이는 1965년 L. A. Zadeh가 퍼지 집합 이론을 제시하면서 등장했고, 이후 퍼지 명제나 규칙을 다루기 위한 퍼지 논리로 발전하였다.</p>
<h2 id="12-크리스프-집합-vs-퍼지-집합">1.2 크리스프 집합 vs 퍼지 집합</h2>
<ul>
<li><strong>크리스프 집합 (Crisp Set)</strong> : 원소가 집합에 속하면 1, 아니면 0</li>
<li><strong>퍼지 집합 (Fuzzy Set)</strong> : 원소가 집합에 속하는 정도를 0~1 사이 값으로 표현</li>
<li>$키 큰 사람 = {(170, 0.3), (175, 0.5), (180, 0.95), (190, 1.0)}$</li>
<li>명제 논리 == 기존 집합 (크리스프 집합), 퍼지 논리 == 퍼지 집합</li>
</ul>
<blockquote>
<p>원소가 속하거나 속하지 않는 두 가지로만 구분되는 크리스프 집합과는 달리, 퍼지 집합에서는 원소가 특정 집합에 속하는 정도를 표현함.
그렇다고 해서, 퍼지 논리가 애매한 논리인 것은 아니다. 퍼지 논리는 애매함을 다루는 질서정연한 논리이다.</p>
</blockquote>
<h2 id="13-퍼지-연산자">1.3 퍼지 연산자</h2>
<ul>
<li>교집합 (AND) : $min(μ_A(x), μ_B(x))$</li>
<li>합집합 (OR) : $max(μ_A(x), μ_B(x))$</li>
<li>여집합 (NOT) : $1 - μ_A(x)$</li>
</ul>
<h2 id="14-퍼지-추론-과정">1.4 퍼지 추론 과정</h2>
<ul>
<li><strong>퍼지화 (Fuzzification)</strong> : 입력값을 소속 함수로 변환</li>
<li><strong>규칙 적용 (Rule Application)</strong> : 각 규칙의 조건에 맞게 소속 함수 값을 계산</li>
<li><strong>결합 (Aggregation)</strong> : 여러 규칙의 결과를 결합</li>
<li><strong>역퍼지화(Defuzzification)</strong> : 퍼지 결과를 실제 수치로 변환</li>
</ul>
<h3 id="141-헤지">1.4.1 헤지</h3>
<ul>
<li>퍼지 집합의 모양을 바꾸는 퍼지 집합 한정사 (퍼지 집합의 의미를 강화하거나 약화시키는 연산자처럼 작동)</li>
<li>(ex) 매우, 얼마간, 꽤, 다소, 조금 등</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/a76d28fe-7bec-4326-92d1-86d084cf23e0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/f763a88f-0e2e-4d89-b258-19c6e87de832/image.png" alt=""></p>
<h3 id="142-퍼지-규칙">1.4.2 퍼지 규칙</h3>
<ul>
<li>IF x가 A THEN y는 B에서 x, y는 언어 변수, A, B는 각각 논의 영역 X와 Y의 퍼지 집합에서 결정된 언어 값</li>
<li>규칙 후건의 출력 값이나 소속도는 전건의 소속도에서 직접 추정할 수 있음.</li>
<li>퍼지 규칙의 전건은 여러 개일 수 있으며, 전건의 모든 부분은 동시에 계산되고, 숫자 하나로 결정됨. (-&gt; 이를 위해 퍼지 집합 연산을 사용함)</li>
<li>후건 역시 여러 개일 수 있음. (ex) IF 기온이 높다 THEN 뜨거운 물은 줄어든다 시원한 물은 늘어난다</li>
</ul>
<h3 id="143-퍼지-추론-기법">1.4.3 퍼지 추론 기법</h3>
<p><strong>맘다니형 추론 : 가장 흔히 쓰이는 퍼지 추론 기법</strong></p>
<blockquote>
<p>1단계 : 입력 변수의 퍼지화 
  2단계 : 규칙 평가 
  3단계 : 출력으로 나온 규칙의 통합
  4단계 : 역퍼지화</p>
</blockquote>
<p><strong>맘다니형 추론 예시 - 입력 2개, 출력 1개, 규칙 3개 문제 예시</strong></p>
<ul>
<li>x, y, z는 언어 변수 (프로젝트 자금, 인력, 위험도)</li>
<li>A1, A2, A3는 논의 영역 x 상의 퍼지 집합에서 정해지는 언어 값 (부족하다, 한계 수익점에 있다, 충분하다)</li>
<li>B1, B2는 논의 영역 y 상의 퍼지 집합에서 정해지는 언어 값 (적다, 많다)</li>
<li>C1, C2, C3는 논의 영역 z 상의 퍼지 집합에서 정해지는 언어 값 (낮다, 중간이다, 높다)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/aad72db7-fd9c-4ba2-8494-854fee69f858/image.png" alt=""></p>
<p><strong>1단계 : 입력 변수의 퍼지화</strong></p>
<ul>
<li>크리스프 입력 x1(프로젝트 자금), y1(프로젝트 인력)을 받고, 이를 적합한 퍼지 집합에 각각 어느 정도로 속할지 결정</li>
<li>논의 영역 : X, Y</li>
</ul>
<p><strong>2단계 : 규칙 평가</strong></p>
<ul>
<li>퍼지 입력 $μ<em>{(x=A1)} = 0.5$, $μ</em>{(x=A2)} = 0.2$, $μ<em>{(x=B1)} = 0.1$, $μ</em>{(x=B2)} = 0.7$을 받아 퍼지 규칙의 전건에 적용</li>
<li>주어진 퍼지 규칙에 전건이 여러 개 있다면 퍼지 연산자를 사용해 전건의 평가 결과를 나타내는 숫자 하나를 얻고, 이 숫자를 후건의 소속 함수에 적용함.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/90c32f32-54eb-421d-a72c-e5601cf84a69/image.png" alt=""></p>
<p><strong>3단계 : 출력으로 나온 규칙의 통합</strong></p>
<ul>
<li>여러 개 규칙이 동시에 발화되면 각 규칙마다 후건부 퍼지 집합이 나오는데, 이 결과들을 하나의 최종 퍼지 집합으로 합치는 것</li>
</ul>
<p><strong>4단계 : 역퍼지화</strong></p>
<ul>
<li>최종 퍼지 집합을 단일 수치로 바꾸는 것</li>
<li>대표적인 방법 -&gt; 무게 중심법</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/c1813c8f-e6de-4f24-90b5-26f7ae555d44/image.png" alt=""></p>
<p><strong>스게노형 추론 : 퍼지 집합 대신 입력 변수에 대한 수학 함수를 사용하는 추론 방법</strong></p>
<ul>
<li>맘다니형과 거의 비슷하나, 후건부가 입력 변수에 대한 수학 함수라는 차이가 존재함.</li>
<li>낮다, 보통이다 등의 언어적 표현이 아닌 상수로 값을 표현함.</li>
<li>(ex) IF 온도가 높음 THEN 팬 속도 = 0.5 * 온도 + 20</li>
<li>규칙 후건부를 단일체로 표현할 수 있음. (규칙 후건을 통합할 때 연속적인 것이 아닌 개별적인 값들로써 역퍼지화가 가능함. / 다시 말해 단일체의 가중평균으로 출력을 구함.)</li>
<li>효율적인 계산 / 빠른 역퍼지화<blockquote>
<p>스게노형 퍼지 규칙 형식
IF x가 A 
AND y가 B
THEN Z는 f(x, y) 
// x, y, z : 언어 변수
// A, B : 논의 영역을 x, y로 하는 퍼지 집합 
// f(x, y) : 수학 함수</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/51925bc9-a78e-4e2b-a891-c6628f3c9a1a/image.png" alt="">
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/ae5279f4-2fd9-4fb4-9e7a-f885f348fc28/image.png" alt=""></p>
<p><strong>맘다니형 추론과 스게노형 추론의 차이</strong></p>
<ul>
<li>맘다니형 추론 방법은 전문 지식을 더욱 객관적이고 인간의 방식으로 설명할 수 있다는 장점이 있으나, 퍼지 집합을 다루기에 계산 비용이 많이 듦.</li>
<li>스게노형 추론 방법은 계산을 효율적으로 할 수 있고, 최적화나 적응형 기법과 함께 잘 작동함.</li>
</ul>
<h2 id="15-함축-연산자-처리">1.5 함축 연산자 처리</h2>
<ul>
<li>IF-THEN 부분을 연결하는 방법으로, IF 부분의 소속도 값과 THEN 부분의 소속도 값을 이용해 출력을 결정한다.</li>
<li>(ex) IF (0.7) THEN (aircon is &quot;high&quot;) -&gt; min(0.7, &quot;high&quot;)</li>
</ul>
<h1 id="code-example">Code Example</h1>
<h2 id="팬-속도-제어">팬 속도 제어</h2>
<ul>
<li>입력 : 온도 (0~40℃)</li>
<li>출력 : 팬 속도 (0~100%)</li>
<li>규칙 예시 :<ul>
<li>IF 온도가 낮음 → 팬 속도 낮음</li>
<li>IF 온도가 보통 → 팬 속도 보통</li>
<li>IF 온도가 높음 → 팬 속도 높음</li>
</ul>
</li>
</ul>
<pre><code>!pip install scikit-fuzzy matplotlib --quiet

import numpy as np
import skfuzzy as fuzz
import matplotlib.pyplot as plt

# -----------------------------------------------------
# 1. 변수 정의
# -----------------------------------------------------
x_temp = np.arange(0, 41, 1) # 0부터 40까지 숫자 배열 (온도)
x_fan = np.arange(0, 101, 1) # 0부터 100까지 숫자 배열 (팬 속도)

# -----------------------------------------------------
# 2. 소속 함수 정의 (Membership function를 정의)
# -----------------------------------------------------
# fuzz.trimf : Triangular membership function
# syntax : y = trimf(x,params)
# (입력값, [삼각형 소속 함수 세 꼭짓점 좌표])
# (왼쪽 시작점, 꼭짓점(최대 소속도=1), 오른쪽 끝점)
temp_low = fuzz.trimf(x_temp, [0, 0, 20])
temp_med = fuzz.trimf(x_temp, [10, 20, 30])
temp_high = fuzz.trimf(x_temp, [20, 40, 40])

fan_low = fuzz.trimf(x_fan, [0, 0, 50])
fan_med = fuzz.trimf(x_fan, [20, 50, 75])
fan_high = fuzz.trimf(x_fan, [50, 100, 100])

# -----------------------------------------------------
# 3. 입력값 설정
# -----------------------------------------------------
# fuzz.interp_membership(x, xmf, xx) : 특정 입력값에 대한 소속도(멤버십 값) 계산
# x : 입력 범위, xmf : 퍼지 집합의 멤버십 함수, xx : 평가 원하는 값
temp_value = 28

temp_level_low = fuzz.interp_membership(x_temp, temp_low, temp_value) # 범위밖 -&gt; 소속도 : 0
temp_level_med = fuzz.interp_membership(x_temp, temp_med, temp_value) # (c - x) / (c - b) -&gt; (30 - 28) / (30 - 20) -&gt; 소속도 : 0.2
temp_level_high = fuzz.interp_membership(x_temp, temp_high, temp_value) # (x - a) / (b - a) -&gt; (28 - 20) / (40 - 20) -&gt; 소속도 : 0.4

print(&quot;소속도(28도) : 낮음 = %.2f, 보통 = %.2f, 높음 = %.2f&quot; % (temp_level_low, temp_level_med, temp_level_high))

# -----------------------------------------------------
# 4. 규칙 적용
# -----------------------------------------------------
fan_activation_low = np.fmin(temp_level_low, fan_low) # IF 온도가 낮음 → 팬 속도 낮음
fan_activation_med = np.fmin(temp_level_med, fan_med) # IF 온도가 보통 → 팬 속도 보통
fan_activation_high = np.fmin(temp_level_high, fan_high) # IF 온도가 높음 → 팬 속도 높음

# -----------------------------------------------------
# 5. 결과 결합 및 역퍼지화
# -----------------------------------------------------
# np.fmax(a, b) : 두 값 중 큰 값 반환 (OR 연산)
# aggregated : 소속도가 가장 큰 값 찾는 거
aggregated = np.fmax(fan_activation_low, np.fmax(fan_activation_med, fan_activation_high))
# fuzz.defuzz(x, mf, &#39;centroid&#39;) : 퍼지값을 실제 값으로 반환
# &#39;centroid&#39; : 집합의 평균 계산
# &#39;bisector&#39; : 곡선을 반으로 나눈 값
# &#39;mom/som/lom&#39; : 최대 소속도를 갖는 값들 중 각각 중앙/최소/최대값
print(aggregated)
fan_result = fuzz.defuzz(x_fan, aggregated, &#39;centroid&#39;)
print(&quot;결정된 팬 속도 : %.2f %%&quot; % fan_result)

numerator = np.sum(x_fan * aggregated)
denominator = np.sum(aggregated)
fan_result = numerator / denominator
print(numerator, denominator, fan_result)

# -----------------------------------------------------
# 6. 시각화
# -----------------------------------------------------
plt.figure(figsize = (8, 4))
plt.plot(x_fan, fan_low, &#39;b&#39;, linewidth=1.5, label=&#39;Low&#39;)
plt.plot(x_fan, fan_med, &#39;g&#39;, linewidth=1.5, label=&#39;Medium&#39;)
plt.plot(x_fan, fan_high, &#39;r&#39;, linewidth=1.5, label=&#39;High&#39;)
plt.axvline(fan_result, color=&#39;k&#39;, linestyle=&#39;--&#39;, label=&quot;Output (defuzzified)&quot;)
plt.legend()
plt.title(&quot;Fuzzy Inference Result (Fan Speed)&quot;)
plt.show()</code></pre><h2 id="팁-주기-문제">팁 주기 문제</h2>
<ul>
<li>입력 : 서비스 (0-10), 음식 맛 (0-10)</li>
<li>출력 : 팁 퍼센트 (0-30%)</li>
<li>규칙 예시 :<ul>
<li>IF 서비스가 형편없다 OR 음식이 맛없다 → 팁 적게</li>
<li>IF 서비스가 보통이다 → 팁 보통</li>
<li>IF 서비스가 훌륭하다 OR 음식이 맛있다 → 팁 많이</li>
</ul>
</li>
</ul>
<pre><code># -----------------------------------------------------
# 1. 변수 정의
# -----------------------------------------------------
x_service = np.arange(0, 11, 1) # 0부터 10까지 숫자 배열 (서비스)
x_food = np.arange(0, 11, 1) # 0부터 10까지 숫자 배열 (음식 맛)
x_tip = np.arange(0, 31, 1) # 0부터 30까지 숫자 배열 (팁 퍼센트)

# -----------------------------------------------------
# 2. 소속 함수 정의
# -----------------------------------------------------
# fuzz.trimf : Triangular membership function
# (입력값, [삼각형 소속 함수 세 꼭짓점 좌표])

# 서비스 : 0 ~ 10
service_low = fuzz.trimf(x_service, [0, 0, 5])
service_med = fuzz.trimf(x_service, [0, 5, 10])
service_high = fuzz.trimf(x_service, [5, 10, 10])

# 음식 맛 : 0 ~ 10
food_bad = fuzz.trimf(x_food, [0, 0, 5])
food_ok = fuzz.trimf(x_food, [0, 5, 10])
food_good = fuzz.trimf(x_food, [5, 10, 10])

# 팁 : 0 ~ 30
tip_low = fuzz.trimf(x_tip, [0, 0, 15])
tip_med = fuzz.trimf(x_tip, [0, 15, 20])
tip_high = fuzz.trimf(x_tip, [15, 30, 30])

# -----------------------------------------------------
# 3. 입력값 (서비스 = 7, 음식 = 8)
# -----------------------------------------------------
# fuzz.interp_membership(x, xmf, xx) : 특정 입력값에 대한 소속도(멤버십 값) 계산
# x : 입력 범위, xmf : 퍼지 집합의 멤버십 함수, xx : 평가 원하는 값

service_val = 7
food_val = 8


service_level_low = fuzz.interp_membership(x_service, service_low, service_val) # 범위 밖
service_level_med = fuzz.interp_membership(x_service, service_med, service_val) # 높음
service_level_high = fuzz.interp_membership(x_service, service_high, service_val) # 높음

print(&quot;소속도(서비스 = 7) : 낮음 = %.2f, 높음 = %.2f, 높음 = %.2f&quot; % (service_level_low, service_level_med, service_level_high))


food_level_bad = fuzz.interp_membership(x_food, food_bad, food_val) # 범위 밖
food_level_ok = fuzz.interp_membership(x_food, food_ok, food_val) # 높음
food_level_good = fuzz.interp_membership(x_food, food_good, food_val) # 높음

print(&quot;소속도(음식 = 8) : 낮음 = %.2f, 높음 = %.2f, 높음 = %.2f&quot; % (food_level_bad, food_level_ok, food_level_good))

# -----------------------------------------------------
# 4. 규칙 적용
# -----------------------------------------------------
rule1 = np.fmax(service_level_low, food_level_bad) # 서비스가 나쁘거나 음식이 별로면
activation_rule1 = np.fmin(rule1, tip_low) # 팁 조금

rule2 = service_level_med # 서비스 중간
activation_rule2 = np.fmin(rule2, tip_med) # 팁 중간

rule3 = np.fmax(service_level_high, food_level_good) # 서비스도 좋고 음식도 좋음
activation_rule3 = np.fmin(rule3, tip_high) # 팁 많이


# -----------------------------------------------------
# 5. 결과 결합 및 역퍼지화
# -----------------------------------------------------
aggregate = np.fmax(activation_rule1, np.fmax(activation_rule1, activation_rule2))
print(aggregate)
tip_result = fuzz.defuzz(x_tip, aggregate, &#39;centroid&#39;)
print(&quot;결정된 팁 : %.2f %%&quot; % tip_result)

numerator = np.sum(x_tip * aggregate)
denominator = np.sum(aggregate)
fan_result = numerator / denominator
print(numerator, denominator, fan_result)

# -----------------------------------------------------
# 6. 시각화
# -----------------------------------------------------
plt.figure(figsize = (8, 4))
plt.plot(x_tip, tip_low, &#39;b&#39;, linewidth=1.5, label=&#39;Low&#39;)
plt.plot(x_tip, tip_med, &#39;g&#39;, linewidth=1.5, label=&#39;Medium&#39;)
plt.plot(x_tip, tip_high, &#39;r&#39;, linewidth=1.5, label=&#39;High&#39;)
plt.axvline(tip_result, color=&#39;k&#39;, linestyle=&#39;--&#39;, label=&quot;Output (defuzzified)&quot;)
plt.legend()
plt.title(&quot;Fuzzy Inference Result (Tip Percentage)&quot;)
plt.show()</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Knowledge Representation]]></title>
            <link>https://velog.io/@inhwaaa_v/Knowledge-Representation</link>
            <guid>https://velog.io/@inhwaaa_v/Knowledge-Representation</guid>
            <pubDate>Sat, 18 Oct 2025 18:10:56 GMT</pubDate>
            <description><![CDATA[<h1 id="1-지식-표현-개요">1. 지식 표현 개요</h1>
<p>&nbsp;지식 표현(Knowledge Representation, KR)은 현실 세계의 사실/규칙/관계를 기호적으로 기술하여 추론이 가능하도록 하는 방식을 의미한다. 다시 말해, 인간이나 컴퓨터가 지식을 어떻게 표현하는지, 그 방법에 관한 내용을 다룬다.</p>
<p>&nbsp;지식 표현의 대표적인 방법에는 규칙, 의미망, 프레임, 시맨틱 웹/온톨로지가 있다.</p>
<ul>
<li>규칙 (Rules) : <code>IF &lt;전제&gt; THEN &lt;결론&gt;</code>과 같은 형태를 지니며, 작업 메모리(현재의 사실들)에 적용해 새로운 사실을 도출함.</li>
<li>의미망 (Semantic Network) : 개념 간의 관계를 <strong>방향 그래프</strong>로 표현하며, 이때 노드(개념/객체)와 간선(관계)로 의미 구조를 표현함.</li>
<li>프레임 (Frame) : 객체/개념을 슬롯(slot : 속성)과 값(value)으로 조직화하며, 슬롯에 프로시저(메소드)를 부착할 수 있음.<ul>
<li>이때, 프레임을 객체로, 슬롯을 field(속성)으로, 프로시저를 method(함수)로 볼 수 있음.</li>
</ul>
</li>
<li>시맨틱 웹/온톨로지 (OWL/RDF) : 웹 상에서 공유 가능한 의미/관계를 형식적으로 서술함.</li>
</ul>
<h1 id="2-규칙-기반-표현과-점화-forward-chaining">2. 규칙 기반 표현과 점화 (Forward Chaining)</h1>
<p>&nbsp; 규칙은 전제(Premise)와 결론(Conclusion)으로 구성되며, 규칙의 전제 조건을 만족하면 결론이 실행된다. 이를 규칙의 전제 조건이 일치하는 경우, 규칙은 점화되고 결론은 실행된다는 말로 표현하기도 한다.</p>
<p>&nbsp;규칙을 사용하는 시스템에는 작업 메모리라고도 하는 데이터베이스가 포함되며, 작업 메모리(Working Memory)에는 현재 관측된 사실(Observed focus), 상태(State), 지식(Knowledge)이 저장된다.</p>
<p>&nbsp;&#39;IF 오늘은 휴강이다 THEN 기분이 좋다&#39;와 같이 IF-THEN 구조로 표현되는 것을 규칙이라고 한다.</p>
<pre><code># 러닝 환경 준비
!pip -q install sympy networkx # Symbolic 수학 라이브러리, 그래프 분석 라이브러리 설치

import itertools
from typing import List, Set, Tuple, Dict, Any
import sympy as sp
import networkx as nx
import matplotlib.pyplot as plt

# 전방향 추론기(forward-chaining) 예시
from dataclasses import dataclass, field

# 데이터들을 담는 클래스 - __init__(), __repr__(), __eq__()과 같은 메소드들 자동으로 추가해 줌!
# __repr__ : 객체를 출력할 때 사람이 읽기 쉽게 보여주는 문자열 반환
@dataclass
class Rule:
  premises : List[str] # 전제들의 리스트 (모두 참이어야 함; OR는 별도 규칙으로 분해)
  conclusion : str

# 코드 비워져 있을 때 채울 수 있는지 요기
@dataclass
class RuleEngine:
  rules : List[Rule] # 규칙들의 리스트
  # facts를 facts: Set[str] = set()와 같은 형태로 쓰지 않는 이유는
  # list, set, dict과 같은 변경 가능한 자료형을 위와 같은 형태로 사용하면
  # 객체가 같은 기본값을 공유하는 문제가 생김. (다시 말해, 모든 객체가 같은 set 인스턴스를 가리킴)
  # 따라서 아래와 같은 형식을 통해 인스턴스마다 set()을 실행시켜 각 인스턴스마다 서로 다른 집합 가질 수 있게 함
  facts : Set[str] = field(default_factory=set) # 사실들의 집합 / field는 기본 값 공유 문제를 해결하기 위해 dataclasses에서 제공하는 함수임.

  # 전방향 추론 (forward chaining)
  def infer(self, max_steps: int = 50) -&gt; Set[str]: # max_steps - 규칙이 새로운 사실을 계속 만들어 내면 무한루프 돌 수 있으므로
    &quot;&quot;&quot;단순 전방향 추론 : 적용 가능한 규칙의 결론을 사실에 추가&quot;&quot;&quot;
    changed = True
    steps = 0
    while changed and steps &lt; max_steps:
      changed = False
      steps += 1
      for r in self.rules:
        if all(p in self.facts for p in r.premises): # 모든 전제가 충족되고
          if r.conclusion not in self.facts: # 사실이 결론에 포함되어 있지 않으면
            self.facts.add(r.conclusion) # 결론을 사실 집합에 추가
            changed = True # 규칙에 새로운 사실이 추가됐으므로 True
    return self.facts # 사실 집합 반환

# 규칙과 사실 예시
rules = [
    Rule([&quot;비가온다&quot;], &quot;우산을가져간다&quot;),
    Rule([&quot;버그가없다&quot;], &quot;프로그램은 올바르게 동작한다.&quot;),
    # (A OR B) -&gt; C 는 두 규칙으로 분해 -&gt; IF 습도가 높다 OR 온도&gt;=30 THEN 에어컨가동
    # A OR B → C는 (A → C) ∧ (B → C)와 논리적 동치를 이루므로 두 규칙으로 나누면 동일한 의미가 됨.
    Rule([&quot;습도가 높다&quot;], &quot;에어컨가동&quot;),
    Rule([&quot;온도&gt;=30&quot;], &quot;에어컨가동&quot;),
]

engine = RuleEngine(rules=rules, facts={&quot;비가온다&quot;, &quot;온도&gt;=30&quot;})
# 비가 온다 -&gt; 우산을 가져간다
# 온도 &gt;= 30 -&gt; 에어컨 가동
engine.infer()
engine.facts</code></pre><h1 id="3-의미망-semantic-network">3. 의미망 (Semantic Network)</h1>
<p>&nbsp;의미망은 <strong>방향 그래프</strong>(<strong>Directed Graph</strong>)를 통해 개념 간의 관계를 나타내는 방법이며, 그래프는 노드와 간선으로 이뤄진다. 이때, 개념/객체를 노드로, 관계를 간선(ex. is-a, has, inst-of 등)으로 표현한다.</p>
<p>&nbsp;의미망은 매우 복잡한 개념이나 인과 관계를 잘 표현할 수 있지만, 지식의 양이 커지면 너무 복잡해져 조작이 어렵다는 단점이 존재한다.</p>
<pre><code># 동물-조류-펭귄의 부분 계층과 속성
G = nx.DiGraph()
# 노드
G.add_nodes_from([&quot;Animal&quot;, &quot;Bird&quot;, &quot;Penguin&quot;, &quot;Wings&quot;, &quot;Antarctica&quot;]) # 노드 정의
# 관계 (라벨 포함)
G.add_edge(&quot;Penguin&quot;, &quot;Bird&quot;, relation=&quot;is-a&quot;) # Penguin is a Bird
G.add_edge(&quot;Bird&quot;, &quot;Animal&quot;, relation=&quot;is-a&quot;) # Bird is a Animal
G.add_edge(&quot;Bird&quot;, &quot;Wings&quot;, relation=&quot;has&quot;) # Bird has Wings
G.add_edge(&quot;Penguin&quot;, &quot;Antarctica&quot;, relation=&quot;habitat&quot;) # Penguin habitat Antarctica

pos = nx.spring_layout(G, seed=42) # 그래프 모양 + 위치 담은 변수
plt.figure(figsize=(6,4))
nx.draw(G, pos, with_labels=True) # 노드와 엣지를 그림
# pos는 노드들의 좌표 정보이고,
# G는 무엇이 연결되어 있는지(엣지 정보)를 담은 그래프 구조
# G.edges(data=True) : 모든 엣지 + 속성 반환
# u 시작 노드 / v 끝 노드 / d는 relation들 (속성) 반환함
edge_labels = {(u, v) : d[&quot;relation&quot;] for u,v,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels) # relation label 그림
plt.title(&quot;Semantic Network Example&quot;)
plt.show()</code></pre><h1 id="프레임-frame---슬롯프로시저">프레임 (Frame) - 슬롯/프로시저</h1>
<p>&nbsp;프레임은 슬롯과 값으로 구성되며, 슬롯에 이벤트 기반 프로시저를 추가할 수 있다.</p>
<ul>
<li><code>if-added</code> : 새 값이 추가될 때 실행</li>
<li><code>if-deleted</code> : 값이 삭제될 때 실행</li>
<li><code>if-needed</code> : 값이 필요하지만 비어 있을 때 호출 (지연 계산)</li>
</ul>
<p>&nbsp;<strong>인공지능 분야에서는 프레임으로 &#39;객체&#39;를 나타내고, 프레임의 슬롯으로 객체의 &#39;필드(속성)&#39;을 나타내며, 슬롯에 붙은 프로시저로 객체의 &#39;메소드(함수)&#39;를 나타낸다.</strong> 또한, 프레임을 인스턴스 프레임과 클래스 프레임으로 나눌 수도 있다.</p>
<p>&nbsp;이러한 프레임은 개념을 구조화해 표현할 수 있기에 개념과 개념 간 관계, 속성, 기능 등을 명확하게 표현할 수 있고, 지식의 재사용성이 높아 이미 만들어진 프레임을 조합해 새로운 프레임을 만들어 지식을 쉽게 확장 가능하며, 지식의 일관성을 유지할 수 있다는 장점이 존재한다.</p>
<pre><code>class Frame: # 프레임 - 객체
  def __init__(self, name):
    self.name = name # 프레임 이름
    self.slots = {} # 슬롯 - attribute 저장하는 딕셔너리
    self.procs = {&quot;if-added&quot;: {}, &quot;if-deleted&quot; : {}, &quot;if-needed&quot; : {}} # 프로시저 - method / 프로시저는 슬롯에 붙음.

  def set_slot(self, slot, value):
    # 슬롯에 값을 넣음
    self.slots[slot] = value
    # 프로시저가 if-added
    # (ex) 슬롯에 값이 추가될 때, on_color_added 함수가 자동으로 호출되게 함.
    # penguin = Frame(&quot;펭귄&quot;)
    # penguin.procs[&quot;if-added&quot;][&quot;color&quot;] = on_color_added
    if slot in self.procs[&quot;if-added&quot;]:
      self.procs[&quot;if-added&quot;][slot](self, value) # 함수 실행

  def get_slot(self, slot):
    if slot not in self.slots and slot in self.procs[&quot;if-needed&quot;]: # 슬롯이 비어있고 프로시저가 if-needed
      # 해당 슬롯이 아직 존재하지 않을 때, 필요할 때만 계산하는 함수를 실행해 값을 생성함.
      # 지연 계산 (값이 정말 필요할 때까지 계산을 미룸)
      self.slots[slot] = self.procs[&quot;if-needed&quot;][slot](self)
    # dict.get(key, default)
    return self.slots.get(slot, None) # 저장된 값 반환

  def del_slot(self, slot):
    if slot in self.slots: # 슬롯이 존재하면
      val = self.slots.pop(slot) # 값 꺼내고 슬롯에서 제거
      # 슬롯에 삭제될 때 실행할 프로시저가 등록되어 있으면 실행
      if slot in self.procs[&quot;if-deleted&quot;]: # 슬롯에 if-delete 프로시저 존재하면
        self.procs[&quot;if-deleted&quot;][slot](self, val) # 함수 실행

  def attach_proc(self, kind, slot, func):
    # kind가 &quot;if-added&quot;, &quot;if-deleted&quot;, &quot;if-needed&quot; 중 하나인지 검증 (논리적 가정 검증)
    assert kind in self.procs # assert는 kind가 True가 아니면 오류 발생시킴
    self.procs[kind][slot] = func # 슬롯에 프로시저 함수 연결 (slot 이름에 함수 매핑) &quot;if-added&quot;: { &quot;age&quot;: &lt;lambda함수&gt; } &lt; 요 느낌으로 저장됨

# 예시 : 사람 프레임
person = Frame(&quot;Kim&quot;)
# 프로시저 함수 붙임
person.attach_proc(&quot;if-added&quot;, &quot;age&quot;, lambda self, v : print(f&quot;[if-added] {self.name}.age := {v}&quot;))
person.attach_proc(&quot;if-needed&quot;, &quot;is_adult&quot;, lambda self: (self.slots.get(&quot;age&quot;, 0) &gt;= 19))
# age 슬롯에 20 저장
person.set_slot(&quot;age&quot;, 20)
# 나이가 19살 이상이면 is_adult = True
print(&quot;is_adult? -&gt; &quot;, person.get_slot(&quot;is_adult&quot;))</code></pre><h1 id="정리---규칙-의미망-프레임-비교-및-장단점">정리 - 규칙, 의미망, 프레임 비교 및 장단점</h1>
<ul>
<li>규칙은 전제와 결론으로 구성된 지식 표현 방식을 의미하며, <code>IF &lt;전제&gt; THEN &lt;결론&gt;</code>의 구조를 지닌다. 이때, 전제 조건을 만족하면 결론이 실행되는데, 이를 점화되었다고도 표현한다. (ex) <code>IF 오늘은 축제이다 THEN 교수님이 수업을 일찍 마치신다</code><ul>
<li>규칙을 사용해 지식을 표현하면 직관적이고 단순하게 지식을 표현할 수 있다는 장점이 존재한다. 또한, 어떤 규칙에 따라 이러한 결과가 도출되었는지 파악하기 용이하고, 규칙을 추가하거나 수정하기도 용이하다는 장점이 있다.</li>
<li>그러나, 지식이 많아질 수록 전제와 결론의 수가 많아져 관리가 어렵고, 복잡한 관계를 표현하기 비효율적이며, 현실 세계의 모든 가능한 상황들을 규칙으로 표현하기엔 한계가 있다는 단점이 존재한다.</li>
</ul>
</li>
<li>의미망은 방향 그래프를 이용해 개념 간의 관계를 나타내는 지식 표현 방식이며, 각 노드는 개념을, 엣지는 개념 간 관계를 나타낸다.<ul>
<li>이러한 의미망은 객체 간 관계를 직관적으로 표현하기 쉽고, &#39;is-a&#39;, &#39;has&#39; 등 객체 간 인과관계를 표현할 수도 있고, 새로운 개념이나 관계를 노드와 엣지를 통해 추가하는 것도 어렵지 않다는 장점이 있다.</li>
<li>그러나, 의미망은 표준 지침이 없어 시스템에 따라 의미망의 형태가 달라 시스템마다 의미망의 형태가 다를 수 있고, 규모가 커지면 복잡해져 조작이 어려울 수 있고, 각 노드나 관계가 무엇을 의미하는지 명확히 밝힐 수 있는 의미 체계가 없다는 단점이 있다.</li>
</ul>
</li>
<li>프레임은 특정 객체와 그 속성을 묶어 하나로 조직화하는 지식 표현 방식으로, 객체지향 프로그래밍에서의 객체와 유사한 특성을 지닌다.<ul>
<li>이러한 프레임은 개념을 구조화해 표현할 수 있고, 유사한 개념을 쉽게 만들 수 있으므로 지식 재사용성이 높고, 개념을 구조화해 표현하기 때문에 지식의 일관성을 유지한다는 장점이 있다.</li>
<li>그러나, 슬롯과 값에 대한 표준이 없고, 다른 지식 표현 방법들에 비해 표현 방법이 어려우며, 사전에 정의되지 않은 새로운 상황이나 속성을 쉽게 반영할 수 없다는 단점이 있다.</li>
</ul>
</li>
</ul>
<h1 id="참고---논리">참고 - 논리</h1>
<p>&nbsp;전통적인 논리에는 명제 논리, 1차 술어 논리, 2차 술어 논리가 있다.</p>
<ul>
<li>명제 논리(Propositional Logic)는 명제와 명제 간의 논리적 관계를 다루며,
AND, OR 등의 논리 연산자를 이용해 명제를 결합하거나 변형한다.</li>
<li>1차 술어 논리(First-Order Predicate Logic) 는 술어(predicate)와 변수(variable)를 사용하여 대상을 좀 더 정교하게 표현한다. 예를 들어, 사람(x)이나 학생(x)처럼 “x가 사람이다”, “x가 학생이다”라는 형태로 표현한다.</li>
<li>2차 술어 논리(Second-Order Predicate Logic) 는 1차 술어 논리를 확장하여 술어 자체를 객체로 다루는 논리이다. 즉, 술어들 사이의 관계나 속성을 표현할 수 있어 더 높은 수준의 구조적 관계를 표현할 수 있다.</li>
</ul>
<p>&nbsp;이러한 술어 논리는 수학적 근거를 바탕으로 논리 개념을 자연스럽게 표현 가능하고, 지식의 정형화 영역에 적합(ex. 정리 증명 기법 사용)하며, 지식의 첨가와 삭제가 용이하고, 비교적 단순하다는 장점이 있다. 하지만, 절차적인 지식 표현이 어렵고 사실의 구성 법칙이 부족하므로 실세계의 복잡한 구조를 표현하기 어렵다는 단점이 존재한다.</p>
<h1 id="5-명제-논리-propositional-logic">5. 명제 논리 (Propositional Logic)</h1>
<ul>
<li>기호 논리학에서 명제는 참 혹은 거짓을 판별할 수 있는 문장이다.</li>
<li>명제 논리는 주어와 술어를 구분하지 않고, 전체를 하나의 명제로 처리해 참 또는 거짓을 판별한다.</li>
<li>단순 명제를 논리 연산자를 통해 결합하여 복합 명제를 구성할 수 있다.</li>
<li>명제 논리의 장점은 명제 간 결합이 단순하고 체계적이라는 것이다. 그러나,
명제가 최소 단위이기에 내부 구조에 대한 분석이 어렵고, 지식 표현을 일반화하기 어렵다는 단점이 존재한다.</li>
<li>명제 : P, Q, R, ...는 참(T) 또는 거짓(F).</li>
<li>연산자 : ¬,∧,∨,→,↔ 등</li>
</ul>
<h2 id="51-함축-implication">5.1 함축 (Implication)</h2>
<ul>
<li><p>두 명제 사이의 조건부 관계를 표현하는 논리 연산자 (IF-THEN)</p>
<pre><code>C = 오늘은 휴일이다.
D = 오늘은 수업이 없다.
E = C -&gt; D</code></pre></li>
</ul>
<h2 id="52-모더스-포넌스-부정-논법-삼단-논법">5.2 모더스 포넌스, 부정 논법, 삼단 논법</h2>
<p>&nbsp;모더스 포넌스(Modus Ponens, affirming method)는 A -&gt; B라는 규칙이 있고, A가 사실이면 B가 결론이 되는 구조를 의미한다. 예를 들어, 만약 &quot;인화가 놀고 있다면 -&gt; 수업을 짼 것이다&quot; 에서 인화가 놀고 있다를 A, 수업을 짼 것이다를 B로 보고, 인화가 놀고 있다는 명제가 참이라면, 수업을 짼 것이라는 결론도 참이 된다.</p>
<p>&nbsp;따라서 아래와 같은 규칙을 지닌다고 할 수 있다.</p>
<pre><code>  규칙 | A -&gt; B
  사실 | A
  ---------------------|
  결론 | B</code></pre><p>&nbsp;부정 논법(Modus Tollens, denying method)은 A -&gt; B라는 규칙이 있고, B가 사실이 아니면 A는 사실이 아니라는 결론을 도출해 내는 것을 의미한다. 예를 들어, &quot;인화가 자퇴를 한다면 -&gt; 인화는 새 진로를 찾은 것이다&quot;에서 인화가 자퇴를 한다를 A, 인화는 새 진로를 찾은 것이다를 B로 보고, 인화가 새 진로를 찾은 것이라는 B가 거짓이면, 인화가 자퇴를 한다는 A도 거짓이 되어 인화는 자퇴를 하지 않는다는 결론이 나오는 것이다.</p>
<p>&nbsp;따라서 아래와 같은 규칙을 지닌다고 할 수 있다.</p>
<pre><code>  규칙 | A -&gt; B
  사실 | NOT B
  ---------------------|
  결론 | NOT A</code></pre><p>&nbsp;삼단 논법(syllogism, Based on two propositions)은 A-&gt;B이고 B-&gt;C이면 A-&gt;라는 결론을 도출해 내는 것을 의미한다. 예를 들어, &quot;소크라테스는 인간이다&quot;라는 규칙이 있고, &quot;인간은 모두 죽는다&quot;라는 사실이 있을 때, &quot;소크라테스는 죽는다&quot;라는 결론이 나오는 것과 같은 것이 삼단 논법의 예시이다.</p>
<p>&nbsp;따라서 아래와 같은 규칙을 지닌다고 할 수 있다.</p>
<pre><code>  규칙 | A -&gt; B
  사실 | B -&gt; C
  ---------------------|
  결론 | A -&gt; C</code></pre><pre><code># SymPy 논리 심볼과 식
P, Q, R = sp.symbols(&#39;P Q R&#39;) # 명제를 나타내는 심볼 생성
expr = sp.Implies(P &amp; sp.Not(Q), sp.Or(sp.Not(P), Q)) # expr = (P ∧ ¬Q) → (¬P ∨ Q)

def truth_table(expr, symbols): # 진리표 만들기
  rows = []
  # itertools.product()는 데카르트 곱(Cartesian product), 즉 모든 가능한 조합을 만들어주는 함수임.
  # itertools.product(iterable, repeat=n)
  # iterable: 조합을 만들 값들의 집합 (예: [False, True])
  # repeat=n: 몇 번 반복할지
  for vals in itertools.product([False, True], repeat=len(symbols)): # 모든 True/False 조합 만들고
    env = dict(zip(symbols, vals)) # 각 심볼에 True/False 매핑하는 dict 생성
    # expr.subs(env) : expr((P ∧ ¬Q) → (¬P ∨ Q))에 해당 값들 대입해서 결과를 계산함.
    rows.append((vals, bool(expr.subs(env)))) # 결과 저장
  return rows

tt = truth_table(expr, [P, Q]) # P, Q의 모든 값에 따른 진리표 생성
for env, val in tt: # env -&gt; (P, Q) 값, val -&gt; expr 결과 값 출력
  print(env, &quot;=&gt;&quot;, val)</code></pre><h2 id="53-추론-규칙-예시">5.3 추론 규칙 예시</h2>
<pre><code># 모더스 포넌스는
# 규칙 A-&gt; B
# 사실 A
# ------------
# 결론 B

def modus_ponens(A, B, facts: Set[str]) -&gt; bool:
  &quot;&quot;&quot;A-&gt;B와 A가 facts에 있으면 B를 추가&quot;&quot;&quot;
  changed = False
  if (f&quot;{A}-&gt;{B}&quot; in facts) and (A in facts) and (B not in facts): # A-&gt;B가 사실이고, A가 사실에 포함되어 있고, B가 포함되어 있지 않으면
    facts.add(B); changed = True # B를 사실에 추가
  return changed

facts = {&quot;A-&gt;B&quot;, &quot;A&quot;} # 조건 충족 -&gt; True
changed = modus_ponens(&quot;A&quot;, &quot;B&quot;, facts)
facts, changed</code></pre><h1 id="6-술어-논리-predicate-logic">6. 술어 논리 (Predicate Logic)</h1>
<p>&nbsp;명제 논리는 전체 명제가 참이냐 거짓이냐만 판단할 수 있다. 따라서 복잡한 문제를 해결하기엔 어려움이 존재한다.</p>
<p>&nbsp;이에 반해, 술어 논리는 하나의 명제를 술어와 그 술어의 수식을 받는 객체로 분리하여 &quot;술어(객체)&quot;의 형태로 표현한 것으로, 명제 논리보다 더 구체적으로 지식을 표현한다. 이때, 객체는 상수 기호로, 관계는 술어 기호로 나타내며, 술어 논리에서는 하나의 명제를 변수와 한정자를 사용해 표현할 수 있다.</p>
<ul>
<li>전칭 한정자 ∀(All)는 어떤 명제가 모든 대상에 대해 참임을 나타내며, $∀x[Human(x) -&gt; Die(x)]$와 같은 예시가 이에 해당한다.</li>
<li>존재 한정자 ∃(Exist)는 어떤 명제가 적어도 하나 이상의 대상에 대해 참임을 의미하며, $∃x[undergraduate(x) ∧ skipclass(x, inhwa)]$와 같은 예시가 이에 해당한다.</li>
</ul>
<p>&nbsp;술어 논리에서의 추론을 위한 첫 번째 방법은 술어 논리식을 명제 논리식으로 변환한 후, 명제 논리의 추론 기법을 적용하는 것이고, 두 번째는 논리 융합이다.</p>
<p>&nbsp;논리 융합(Logical Inference)을 사용하기 위해서는 모든 지식이 정형식(formal expression)으로 표현되어야 하며, 정형식은 기본적인 명제나 개념들로 구성된 논리식이다. 이를 논리 연산자(logical operator)나 한정사(quantifier)를 통해 확장함으로써 더 복잡한 지식 구조를 표현할 수 있다.</p>
<pre><code>A, B, C = sp.symbols(&#39;A B C&#39;)
formula = sp.Implies(A &amp; sp.Implies(B, C), C)  # (A ∧ (B→C)) → C
# CNF 과정
# 1. 함축 기호 제거 - P → Q는 ¬P ∨ Q 와 동치
# ¬(A ∧ (¬B ∨ C)) ∨ C
# 2. 부정 기호를 드모르간 법칙을 통해 기초 공식 안으로 이동
# (¬A ∨ (B ∧ ¬C)) ∨ C
# 3. 분배 법칙 - P∨(Q∧R)≡(P∨Q)∧(P∨R)
# ((¬A∨C∨B)∧(¬A∨C∨¬C)) - 이때 (C ∨ ¬C)는 항상 참이므로
# (¬A∨B∨C)
cnf_form = sp.to_cnf(formula, simplify=True)
print(&quot;Original : &quot;, formula)
print(&quot;CNF      : &quot;, cnf_form)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Genetic Algorithm]]></title>
            <link>https://velog.io/@inhwaaa_v/Genetic-Algorithm</link>
            <guid>https://velog.io/@inhwaaa_v/Genetic-Algorithm</guid>
            <pubDate>Sat, 18 Oct 2025 14:28:11 GMT</pubDate>
            <description><![CDATA[<h1 id="유전자-알고리즘-genetic-algorithm">유전자 알고리즘 (Genetic Algorithm)</h1>
<p>&nbsp;유전자 알고리즘이란, 인간의 유전 현상을 모델링하는 알고리즘으로, 자연 진화 과정을 모방하여 모델링하고, 최적해를 찾는다.</p>
<p>&nbsp;이때, 각 세대마다 선택(selection), 교차(crossover), 돌연변이(mutation) 과정을 거치며 더 나은 해를 찾아가며, 이 과정에서 전역 최적점을 찾을 수 있고, 적합도 평가에 기울기 정보를 필요로 하지 않으므로 함수의 연속이나 미분가능성 등의 제약을 받지 않는다는 장점이 존재한다.</p>
<h2 id="염색체와-유전자">염색체와 유전자</h2>
<p>&nbsp;<strong>염색체</strong> (<strong>chromosome</strong>)(<strong>=individual</strong>)는 여러 개의 <strong>유전자</strong> (<strong>gene</strong>)로 구성되며, 각 유전자는 문제 공간의 한 부분이다. 전체 염색체는 문제 공간을 염색체 공간으로 인코딩한 것을 의미한다.</p>
<p>&nbsp;<strong>세대</strong> (<strong>generation</strong>)는 여러 개의 염색체로 구성된 하나의 집합을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/040ef65b-ae6b-4b27-85fd-4600207b4656/image.png" alt=""></p>
<h2 id="평가-함수와-적합도">평가 함수와 적합도</h2>
<p>&nbsp;유전자 알고리즘의 평가 함수(Evaluation function)는 현재의 염색체가 얼마나 문제를 잘 해결하고 있는지를 나타내는 적합도(Fitness)를 반환한다.</p>
<p>&nbsp;유전자 알고리즘에서 적합도는 염색체의 성능을 나타내며, 높은 적합도(최적해에 가까운 것)를 가지는 염색체가 다음 세대에 더 많이 전달된다.</p>
<h2 id="유전자-알고리즘의-절차">유전자 알고리즘의 절차</h2>
<ol>
<li>Initialization : 초기 집단(population)을 랜덤하게 생성하는 단계</li>
<li>Selection : 현재 집단에서 적합도가 높은 개체를 선택해 다음 세대에 전달하는 과정</li>
<li>Crossover : 선택된 두 부모 염색체를 결합하여 새로운 염색체(자식 염색체)를 생성하는 단계</li>
<li>Mutation : 일정 확률로 유전자 값을 무작위로 변경하는 단계</li>
<li>Evaluation : 새로운 집단의 적합도를 평가하는 단계</li>
<li>Check the termination conditions : 종료 조건을 확인하는 단계</li>
</ol>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/4935b974-7104-478e-a5d2-f4c438a4da44/image.png" alt=""></p>
<h2 id="선택-연산자">선택 연산자</h2>
<ol>
<li>Rootlet Wheel Selection : 각 개체가 선택될 확률이 그 개체의 적합도에 비례함.<ul>
<li>평가 함수를 통해 염색체의 적합도를 계산한 뒤, 적합도에 비례해 룰렛 휠에 염색체를 배당하여 적합도가 높은 염색체가 더 많이 선택될 수 있도록 하는 것</li>
<li>적합도 크기에 따라 비율이 결정됨.</li>
</ul>
</li>
<li>Rank-based Selection : 개체들의 적합도에 따라 랭크를 매겨서 상위 개체들을 선택함.<ul>
<li>상위 개체들만 계속 선택되므로 룰렛 휠이나 토너먼트 방식에 비해 다양성이 적음.</li>
</ul>
</li>
<li>Tournamant Selection : 일정 수의 개체를 선택하고 토너먼트를 진행해 적합도가 높은 것을 선택함.<ul>
<li>무작위로 k개 개체를 선택하고, 이 중 적합도가 가장 높은 개체를 다음 세대로 선택함.</li>
<li>이 과정을 반복해 새로운 population을 구성함.</li>
</ul>
</li>
</ol>
<h2 id="교차-연산자">교차 연산자</h2>
<p>&nbsp;선택된 부모 개체(염색체)로부터 자손(offspring)을 생성하는 과정을 의미한다.</p>
<ul>
<li><strong>multipoint CX</strong> : 부모 염색체를 여러 지점에서 나눠 교환하는 방법으로, 교차점이 1개면 1-point crossover, 2개면 2-point crossover와 같이 표현함.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/903bd538-97e1-49b7-b036-213365221413/image.png" alt=""></p>
<ul>
<li><strong>uniform CX</strong> : 교차점을 따로 정하지 않고, 각 위치마다 확률적으로 부모 중 한쪽의 값을 선택하는 방법으로, 유전자 단위로 독립적 교환이 일어남. (Mask 설정 필요)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/a3aae517-e4ac-4a1c-a65c-4d614671f859/image.png" alt=""></p>
<h2 id="돌연변이-연산자">돌연변이 연산자</h2>
<p>&nbsp;돌연변이 연산자는 개체의 유전자를 일정 확률로 변화시켜 새로운 염색체를 만드는 과정을 의미한다. 이는 개체군의 다양성을 유지하는 것을 목적으로 하는 연산자로, 유전자의 위치를 바꾸거나 값을 수정하는 형태로 진행하여, 지역 최적해가 아닌 전역 최적해를 찾을 수 있도록 해준다.</p>
<h2 id="elitism">ELITISM</h2>
<p>&nbsp;ELITISM이란, 현재 세대에서 가장 적합한 염색체를 수정 없이 다음 세대에 그대로 복사하는 방법으로 교차나 돌연변이 과정에도 손실되지 않고 보존된다는 특징이 있다.</p>
<p>&nbsp;이는 지금까지의 결과를 계속 활용할 수 있으므로 결과를 더 빨리 찾아낼 수 있으나, 결과의 다양성을 보장하지 못한다는 단점이 존재한다. (가장 적합한 방안이 계속 유전 -&gt; 다른 해를 탐색할 기회 감소 -&gt; 최적이 답이 아니더라도 최종 해결 방안이 될 가능성이 존재함)</p>
<br>

<h1 id="genetic-algorithm-example-1">Genetic Algorithm Example 1</h1>
<pre><code>import random

# 파라미터
POPULATION_SIZE = 4 # 개체 집단 크기
MUTATION_RATE = 0.1 # 돌연변이 확률
SIZE = 5 # 하나의 염색체에서 유전자(비트) 개수

# 염색체 클래스
class Chromosome:
    def __init__(self,g = None):
      # 유전자는 리스트 (0/1)로 구현
      # copy() 메소드 사용 이유는 새로운 염색체의 유전자 리스트가 원본 리스트와 독립적으로 동작하도록 하기 위함
      # 다시 말해, 새 염색체에서 genes를 변경해도 원본 리스트가 함께 바뀌지 않게 하기 위함임.
      self.genes = g.copy() if g else []
      self.fitness = 0 # 적합도 초기값

      # 초기 염색체면 무작위로 0/1 채움
      if len(self.genes) == 0:
        for _ in range(SIZE):
          # 0~1 사이 무작위 난수 생성하고, 이 난수가 0.5 이상이면 1, 그렇지 않으면 0을 유전자로 추가
          self.genes.append(1 if random.random() &gt;= 0.5 else 0)

    def cal_fitness(self):
      &quot;&quot;&quot;적합도 : 이진수 -&gt; 10진수 값&quot;&quot;&quot;
      # 유전자를 하나의 이진수로 보고 십진수로 변환해 적합도로 사용
      # (ex) [1, 0, 1, 1, 1] -&gt; value = 16 + 0 + 4 + 2 + 1 = 23
      value = 0
      for i in range(SIZE): # SIZE = 5 / i = 0, 1, 2, 3, 4
        value += self.genes[i] * (2 ** (SIZE - 1 - i))
      self.fitness = value
      return self.fitness

    def __str__(self): # 객체를 문자열로 표현한 것을 반환하는 함수 (출력 시 보기 좋은 형태 위해)
      # 이 함수가 없으면 &lt;__main__.Chromosome object at 0x78eb5a744e00&gt;와 같이 메모리 주소가 출력됨
      return str(self.genes)


# 보조 함수 : 개체 집단 출력
def print_p(pop):
  for i, x in enumerate(pop):
    print(&quot;염색체 #&quot;, i, &quot;=&quot;, x, &quot; 적합도 =&quot;, x.cal_fitness())
  print(&quot;&quot;)

# 선택 연산 (룰렛 휠)
def select(pop):
  # 총 적합도 합 (개체의 적합도 계산)
  max_value = sum(c.cal_fitness() for c in pop)
  # 모두 0인 극단 상황 방지 - 모든 개체 적합도가 0이면 랜덤 선택
  # 이 과정 안 해주면 pick = random.uniform(0, max_value)에서 0만 선택 -&gt; 아무 개체도 선택 x
  if max_value == 0:
    return random.choice(pop)
  # 룰렛 휠에서 화살표가 멈추는 지점
  # max_value = sum(c.cal_fitness() for c in pop) 요기서 전체 적합도 합 구하고
  # 밑의 코드에서 그 합 범위 안에서 임의 실수 하나를 뽑으니까
  # 결론적으로 적합도에 비례한 룰렛 휠 구현이 됨
  pick = random.uniform(0, max_value) # 0 이상 max_value 이하 임의 실수 반환
  current = 0 # 누적 적합도 합
  # 룰렛 휠에서 화살표가 어떤 조각(개체)에 속하는지 찾아 반환
  for c in pop:
    current += c.cal_fitness()
    if current &gt; pick: # 누적합 &gt; pick이 되는 지점(구간 안에 들어왔을 때), 그 지점 값 반환
      return c
  return pop[-1] # pick이 마지막 구간에 떨어진 경우(위의 루프에서 아무 것도 선택 안 된 경우), 마지막 개체 반환

# 교차/돌연변이 연산
def crossover(pop):
  # 부모 선택 연산 (룰렛 휠)
  father = select(pop)
  mother = select(pop)
  index = random.randint(1, SIZE - 1) # 1 ~ SIZE -1, 교차점 정하기
  # 교차점 기준 교차 연산
  child1 = father.genes[:index] + mother.genes[index:]
  child2 = mother.genes[:index] + father.genes[index:]
  return (child1, child2)

def mutate(c):
  # 각 비트에 대해 MUTATION_RATE로 무작위 치환
  for i in range(SIZE):
    # MUTATION_RATE = 0.1, random.random() -&gt; 0.0 이상, 1.0 미만 실수 반환
    if random.random() &lt; MUTATION_RATE: # 돌연변이 발생 여부 결정
      c.genes[i] = 1 if random.random() &lt; 0.5 else 0 # 새 유전자 값 결정

# 메인 루프
population = [] # 세대

# 초기 염색체 생성하여 개체 집단 구성
for _ in range(POPULATION_SIZE):# POPULATION_SIZE = 4
  population.append(Chromosome()) # 개체 집단 정의

# 세대 번호 및 출력
count = 0
population.sort(key=lambda x : x.cal_fitness(), reverse=True) # 적합도 순 정렬
print(&quot;세대 번호 = &quot;, count)
print_p(population)

count = 1
while population[0].cal_fitness() &lt; 31:
  new_pop = []
  # 선택 + 교차로 자식 생성 (부모 2 -&gt; 자식 2, 인구 수 채울 때까지)
  # 한 번의 교차로 자식 2개 생기니까 전체 염색체 수 유지하려고 POPULATION_SIZE // 2
  # 나누지 않으면 세대별 염색체 수 8개 됨
  for _ in range(POPULATION_SIZE // 2):
    c1, c2 = crossover(population)
    new_pop.append(Chromosome(c1))
    new_pop.append(Chromosome(c2))

  # 자식 세대로 교체 (깊은 복사 성격 유지)
  population = new_pop.copy()

  # 돌연변이 수행
  for c in population:
    mutate(c)

  # 출력 전 정렬 (내림차순 : 적합도 큰 순)
  population.sort(key = lambda x : x.cal_fitness(), reverse=True)
  print(&quot;세대 번호 = &quot;, count)
  print_p(population)

  count += 1
  if count &gt; 100: # 종료 조건 확인
    break</code></pre><h1 id="genetic-algorithm-example-2">Genetic Algorithm Example 2</h1>
<pre><code># Core imports
import random
import math
import statistics
import itertools
from typing import List, Tuple, Callable, Optional, Sequence, Any

import matplotlib.pyplot as plt

# For reproducibility during demos
def set_seed(seed : int = 42):
  random.seed(seed)

set_seed(1234)

# Parameter (modifiable)
# 개체 집단 크기, 최소 2개 이상(교차 위해), 짝수가 좋음 (세대 크기 유지 위해서 -&gt; 앞에서 // 2 한 것 처럼)
POPULATION_SIZE = 50  # population size (&gt;= 2, preferably even)
GENE_SIZE = 16 # # chromosome length (bits)
# ELITISM : 상위 몇 개의 개체는 수정 없이 그대로 다음 세대로 넘기는 것
ELITISM = 2 # 그 세대에서 가장 좋은 애는 남기는 거, Number of elites to carry over unchanged
MAX_GENERATIONS = 200

# Selection / crossover / mutation options
SELECTION_METHOD = &quot;roulette&quot; # &#39;roulette&#39; or &#39;tournament&#39;
TOURNAMENT_K = 3 # if selection = tournament
CROSSOVER_METHOD = &quot;single&quot; # &#39;single&#39;(하나의 교차점 기준 교차) or &#39;uniform&#39;(균등 선택)
CROSSOVER_RATE = 0.9 # probability to crossover a pair, 교차 확률
MUTATION_RATE = 0.01 # per-gene mutation probability, 돌연변이 확률
# 돌연변이 확률을 세대가 진행됨에 따라 점진적으로 감소시킬지 여부
ADAPTIVE_MUTATION = False # if True, linearly reduces mutation over tim

# Fitness goal : maximize integer value of binary, or use a custom function (이진수 정수 값 최대화 or 사용자 정의 함수)
# 이진 리스트 -&gt; 정수
# &lt;&lt; : left shift
# 현재 비트(b)를 이전까지의 비트 연산 결과(value)에 붙여서 누적 정수를 만들어가는 과정
def binary_to_int(bits : Sequence[int]) -&gt; int:
  &quot;&quot;&quot;Convert list of bit (MSB first) to integer.&quot;&quot;&quot;
  value = 0
  for b in bits: # b = 현재 비트
    value = (value &lt;&lt; 1) | (1 if b else 0) # 비트 연산 -&gt; 결과는 정수
  return value

# 비트열의 정수값을 그대로 적합도로 사용함. (이진수가 클 수록 더 적합함)
def fitness_func(bits : Sequence[int]) -&gt; float:
  &quot;&quot;&quot;Example fitness : value of the 16-bit number (0..65535).&quot;&quot;&quot;
  return float(binary_to_int(bits))

# 비트열로 포함 가능한 최대값 계산 (목표 최댓)
# GENE_SIZE = 16
TARGET_MAX = (1 &lt;&lt; GENE_SIZE) - 1 # e.g., 65535 for 16 bits

# Individual &amp; Population
class Individual:
  &quot;&quot;&quot;Chromosome represented as a list of 0/1 integers.&quot;&quot;&quot;
  def __init__(self, genes: Optional[List[int]] = None):
    # 유전자 리스트 (염색체 생성)
    if genes is None:
      self.genes = [1 if random.random() &lt; 0.5 else 0 for _ in range(GENE_SIZE)]
    else:
      self.genes = genes.copy()
    # 적합도 초기값
    self.fitness: float = 0.0
    self.evaluate() # 적합도 계산

  def evaluate(self) -&gt; float:
    self.fitness = fitness_func(self.genes)
    return self.fitness

  # 현재 개체와 똑같은 유전자를 가진 새 개체 반환 (깊은 복사, 새 인스턴스 만들어 반환하므로)
  def copy(self) -&gt; &#39;Individual&#39;:
    return Individual(self.genes)

  # 출력 형식
  def __repr__(self) -&gt; str:
    return f&quot;Individual(fitness = {self.fitness:.1f}, genes={self.genes})&quot;

# 처음 세대 만드는 함수 (초기 집단(population)을 랜덤하게 생성하는 단계)
def init_population(n:int) -&gt; List[int]:
  return [Individual() for _ in range(n)]

# Selection Operators
def selection_roulette(pop: List[Individual]) -&gt; Individual:
  total = sum(ind.fitness for ind in pop) # 모든 개체 적합도 합
  # Handle edge case : if all fitness are zero, pick uniformly
  # 모든 적합도가 0일 때 예외처리
  if total == 0:
    return random.choice(pop).copy()
  # 룰렛 화살표 위치 정하기
  pick = random.uniform(0, total)
  # 룰렛 휠 구성 (매커니즘은 Original에서와 같음)
  acc = 0.0
  for ind in pop:
    acc += ind.fitness
    if acc &gt;= pick:
      return ind.copy()
  # Fallback
  return pop[-1].copy()

def select_tournament(pop: List[Individual], k:int = 3) -&gt; Individual:
  # pop : 개체 집단, k : 한 번의 토너먼트에 참여할 개체 수
  # 전체 population에서 무작위로 염색체 k개 선택
  sample = random.sample(pop, k)
  best = max(sample, key=lambda x : x.fitness) # 가장 적합도 높은 염색체 선택
  return best.copy() # 가장 적합도 높은 염색체 복사 (이유는 앞에서 다룬 copy 사용 이유와 같음)

# select method에 따른 함수 호출
def select(pop: List[Individual]) -&gt; Individual:
  if SELECTION_METHOD == &quot;tournament&quot;:
    return select_tournament(pop, TOURNAMENT_K)
  return selection_roulette(pop)

# Crossover operators
def crossover_single(p1: Individual, p2: Individual) -&gt; Tuple[Individual, Individual]:
  # pick a split in [1, GENE_SIZE-1]
  # 하나의 교차점 기준 교차 (앞에서 다룬 매커니즘과 같음)
  point = random.randint(1, GENE_SIZE - 1)
  c1 = p1.genes[:point] + p2.genes[point:]
  c2 = p2.genes[:point] + p1.genes[point:]
  return Individual(c1), Individual(c2)

# 균등 교차 - 모든 유전자 기준, 랜덤 확률 -&gt; 일정 확률 이상이면 교차
def crossover_uniform(p1 : Individual, p2 : Individual) -&gt; Tuple[Individual, Individual]:
  c1, c2 = [], [] # 자식 염색체 유전자 리스트 초기화
  for a, b in zip(p1.genes, p2.genes):
    # 랜덤 난수 생성 -&gt; 50% 확률로 교차
    if random.random() &lt; 0.5:
      c1.append(a); c2.append(b) # 교차 X
    else:
      c1.append(b); c2.append(a) # 교차
  return Individual(c1), Individual(c2)

def do_crossover(p1 : Individual, p2: Individual) -&gt; Tuple[Individual, Individual]:
  # CROSSOVER_RATE = 0.9
  # CROSSOVER_RATE 기준에 못 미치면 교차를 수행하지 않고, 기준에 미치면 교차 수행
  if random.random() &gt; CROSSOVER_RATE:
    # no crossover -&gt; copy parents
    return p1.copy(), p2.copy()
  # crossover method에 따른 호출 (uniform, multipoint)
  if CROSSOVER_METHOD == &quot;uniform&quot;:
    return crossover_uniform(p1, p2)
  return crossover_single(p1, p2)

# Mutation operators
def mutate(ind : Individual, gen_idx : int, max_gen : int) -&gt; None:
  # adaptive mutation : linearly decay over generations
  # 돌연변이 확률을 세대가 진행됨에 따라 점진적으로 감소시킬지 여부에 따른 분기
  if ADAPTIVE_MUTATION:
    rate = MUTATION_RATE * max(0.01, 1.0 - gen_idx / max(1, max_gen))
  else:
    rate = MUTATION_RATE
  for i in range(GENE_SIZE):
    if random.random() &lt; rate:
      # 비트 반전 (1 - 0, 1 - 1 이런 형태가 되니까 비트 반전됨)
      ind.genes[i] = 1 - ind.genes[i]
  ind.evaluate() # 돌연변이 후 적합도 재계산

# GA Main Loop
def run_ga(max_generations: int = MAX_GENERATIONS, verbose : bool = False):
  pop = init_population(POPULATION_SIZE) # 개체 집단 초기화
  pop.sort(key=lambda x : x.fitness, reverse=True) # 적합도 높은 순 정렬

  best_history = [pop[0].fitness] # 세대별 최고 적합도
  mean_history = [statistics.mean(ind.fitness for ind in pop)] # 세대 평균 적합도
  generation = 0

  # 조건 : 세대 수가 최대에 도달하지 않고, 목표 적합도에 도달하지 않음
  while generation &lt; max_generations and pop[0].fitness &lt; TARGET_MAX:
    new_pop : List[Individual] = [] # 새 세
    # Elitism : carry top ELITISM individuals
    # ELITISM 개수만큼 상위 개체 그대로 다음 세대에 복사해서 유지
    elites = [pop[i].copy() for i in range(min(ELITISM, len(pop)))]
    new_pop.extend(elites)

    # Fill the rest with offspring / 나머지 자식 채우기
    while len(new_pop) &lt; POPULATION_SIZE:
      # 부모 선택
      p1 = select(pop)
      p2 = select(pop)
      # 교차 연산
      c1, c2 = do_crossover(p1, p2)
      # 돌연변이 연산 및 자식 추가
      mutate(c1, generation, max_generations)
      if len(new_pop) &lt; POPULATION_SIZE:
        new_pop.append(c1)
      if len(new_pop) &lt; POPULATION_SIZE:
        mutate(c2, generation, max_generations)
        new_pop.append(c2)

    # 세대 교체 및 정렬
    pop = new_pop
    pop.sort(key=lambda x : x.fitness, reverse=True)

    # Track stats / 통계 갱신 및 세대 수 증가
    best_history.append(pop[0].fitness)
    mean_history.append(statistics.mean(ind.fitness for ind in pop))
    generation += 1

    # verbose가 참일 때, 10세대마다, 또는 목표 적합도 도달 시 출력하도록 함
    if verbose and (generation % 10 == 0 or pop[0].fitness == TARGET_MAX):
      print(f&quot;Generation {generation} | Best : {pop[0].fitness} mean = {mean_history[-1]:.1f}&quot;)

  return pop[0], best_history, mean_history, generation


# Quick smoke test
best, best_hist, mean_hist, gens = run_ga(verbose=True)
print(&quot;Best fitness : &quot;, best.fitness, &quot;in&quot;, gens, &quot;generations&quot;)
</code></pre><h3 id="hyperparameter-sweep-분석">Hyperparameter Sweep 분석</h3>
<p>&nbsp;Example 1과 Example 2는 Selection 연산의 구현 방식(토너먼트, 룰렛 휠), ELITISM 존재 여부, 교차 및 돌연변이 확률을 결정하는 하이퍼파라미터 여부 등의 차이가 있다. (코드에 대한 자세한 설명은 주석에 있으므로 생략한다.)</p>
<p>&nbsp;여기서는 CROSSOVER_RATE, MUTATION RATE 값 차이에 따른 결과 변화에 대해 알아본다.</p>
<pre><code># c_rate=(0.6, 0.8, 0.95) : 실험할 CROSSOVER_RATE
# m_rates=(0.001, 0.01, 0.05): 실험할 MUTATION_RATE
# repeats=5 : 각 조합을 몇 번 반복할지
# max_generations=150 : 최대 세대 수
from re import M
def experiment_sweep(c_rate=(0.6, 0.8, 0.95), m_rates=(0.001, 0.01, 0.05), repeats=5, max_generations=150):
  &quot;&quot;&quot;
    Run multiple GA trials over combinations of crossover and mutation rates.
    Returns summary dict with average generations to reach target (or last gen best).
  &quot;&quot;&quot;
  global CROSSOVER_RATE, MUTATION_RATE
  results = {}
  for c in c_rate:
    for m in m_rates:
      gens_list = []
      best_at_end = []
      for _ in range(repeats):
        # reset seed each run for fair comparison
        set_seed(random.randint(1, 10_000_000)) # 랜덤 시드 고정
        # CROSSOVER_RATE, MUTATION_RATE 설정
        CROSSOVER_RATE = c
        MUTATION_RATE = m
        # 실행
        best, best_hist, mean_hist, g = run_ga(max_generations=max_generations, verbose=False)
        gens_list.append(g)
        best_at_end.append(best.fitness)
      results[(c,m)] = {
          &#39;avg_generations&#39; : sum(gens_list)/len(gens_list),
          &#39;avg_best_fitness&#39; : sum(best_at_end)/len(best_at_end),
          &#39;runs&#39; : len(gens_list)
      } # 실험 결과 저장
  return results

# Run a quick sweep (reduce repeats if slow)
sweep_results = experiment_sweep()
for (c, m), stats in sweep_results.items():
  print(f&quot;CROSSOVER={c:.2f}, MUTATION={m:.3f} -&gt; avg_gens={stats[&#39;avg_generations&#39;]:.1f}, avg_best={stats[&#39;avg_best_fitness&#39;]:.1f}&quot;)</code></pre><p><strong>분석 결과</strong>
&nbsp;교차율(CROSSOVER_RATE)이 너무 높으면, 탐색이 과도해져 수렴이 늦어지고, 0.6과 같이 적당할 때 빠르게 안정화되었다. 또한, 돌연변이율(MUTATION_RATE)은 너무 작으면 다양성이 부족하고, 너무 크면 무작위성이 커져 수렴 속도가 느려진다.
&nbsp;아래의 실행 결과를 살펴보면, CROSSOVER=0.95, MUTATION=0.001일 때, 다시 말해 교차율이 너무 높고, 돌연변이율이 너무 작을 때 탐색이 과도해져 수렴 속도가 느려졌다.</p>
<pre><code>CROSSOVER=0.60, MUTATION=0.001 -&gt; avg_gens=22.8, avg_best=65535.0
CROSSOVER=0.60, MUTATION=0.010 -&gt; avg_gens=24.6, avg_best=65535.0
CROSSOVER=0.60, MUTATION=0.050 -&gt; avg_gens=22.2, avg_best=65535.0
CROSSOVER=0.80, MUTATION=0.001 -&gt; avg_gens=40.2, avg_best=65535.0
CROSSOVER=0.80, MUTATION=0.010 -&gt; avg_gens=35.8, avg_best=65535.0
CROSSOVER=0.80, MUTATION=0.050 -&gt; avg_gens=31.2, avg_best=65535.0
CROSSOVER=0.95, MUTATION=0.001 -&gt; avg_gens=42.6, avg_best=65535.0
CROSSOVER=0.95, MUTATION=0.010 -&gt; avg_gens=29.2, avg_best=65535.0
CROSSOVER=0.95, MUTATION=0.050 -&gt; avg_gens=27.8, avg_best=65535.0</code></pre><pre><code># Visualize sweep (heatmap-like via scatter)
# We&#39;ll map x=CROSSOVER_RATE, y=MUTATION_RATE, size=avg_best_fitness, annotation=avg_generations
# 점이 클 수록 적합도가 높고, 숫자가 작을 수록 빠르게 수렴한 것임.
xs, ys, sizes, texts = [], [], [], [] # x축(crossover_rate), y축(mutation_rate), size(평균 최고 적합도, 점 크기), texts(평균 세대 수)
for (c, m), st in sweep_results.items():
  xs.append(c)
  ys.append(m)
  sizes.append(st[&#39;avg_best_fitness&#39;])
  texts.append(st[&#39;avg_generations&#39;])

plt.figure()
plt.scatter(xs, ys, s=[v for v in sizes])
for x, y, t in zip(xs, ys, texts):
  plt.text(x, y, f&quot;{t:.0f}&quot;, ha=&#39;center&#39;, va=&#39;bottom&#39;)
plt.title(&quot;Sweep : size=avg best fitness, label=avg generations&quot;)
plt.xlabel(&quot;CROSSOVER_RATE&quot;)
plt.ylabel(&quot;MUTATION_RATE&quot;)
plt.show()</code></pre><p><strong>분석 결과</strong></p>
<p>&nbsp;여기서 점이 클 수록 적합도가 높고, 숫자가 작을 수록 빠르게 수렴한 것이다.</p>
<p>&nbsp;모든 실험 조합이 전역 최적해(65535)에 도달했지만, 교차율과 돌연변이율의 조합은 수렴 속도에 뚜렷한 차이를 보였다.</p>
<p>&nbsp;특히, 교차율이 지나치게 높거나 돌연변이율이 너무 낮을 경우 탐색이 과도하거나 다양성이 부족해 수렴이 지연되었으며, 반대로 교차율 0.6, 돌연변이율 0.05 부근에서 가장 빠르고 안정적인 수렴을 보였다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/22d92bc8-66ad-4c85-8f0b-6ad94d99a090/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Expert System]]></title>
            <link>https://velog.io/@inhwaaa_v/Expert-System</link>
            <guid>https://velog.io/@inhwaaa_v/Expert-System</guid>
            <pubDate>Sat, 18 Oct 2025 13:53:32 GMT</pubDate>
            <description><![CDATA[<h1 id="전문가-시스템-expert-system">전문가 시스템 (Expert System)</h1>
<p>&nbsp;전문가 시스템이란, 특정 분야 전문가의 지식을 모방하고, 이러한 지식을 컴퓨터 프로그램으로 구현한 것으로, 주어진 특정 문제를 해결하거나 조언을 제공하기 위해 사용된다.</p>
<p>&nbsp;전문가 시스템은 특정 영역 전문가 수준의 문제 해결 능력을 지닌 컴퓨터 프로그램이며, 이를 위해 전문가의 지식을 &quot;규칙&quot;이라는 형태로 지식 베이스에 저장하고, 이 규칙들을 통해 추론을 수행한다. 이는 기존의 절차적 코드가 아닌 규칙으로 표현되는 지식을 통해 추론함으로써 복잡한 문제를 해결하도록 설계되었으며, 인공지능 소프트웨어 최초의 성공적 형태이다.</p>
<p>&nbsp;이러한 전문가 시스템은 의료 진단, 주식 투자 조언, 기계 고장 진단 등에 활용된다.</p>
<p>&nbsp;전문가 시스템은 전문가의 지식을 모방해 비전문가도 전문가 수준의 결정을 내릴 수 있고, 일관된 판단을 제공하며 지식의 확산과 보존이 용이하다는 장점이 있다. 또한, IF THEN을 사용하는 규칙은 인간 전문가의 지식을 표현하는 자연스러운 방식이며, 지식 베이스와 추론 엔진이 분리되므로 다른 영역에도 쉽게 적용할 수 있다.</p>
<p>&nbsp;하지만, 복잡한 지식 베이스 작성이 필요하고, 전문가의 직관적인 판단을 완벽히 반영하기 어려우며, 환경 변화에 따른 지식 업데이트가 필요하다는 단점이 존재하기도 한다. 또한, 지식을 학습할 수 없고, 탐색이 비효율적이며, 규칙이 많아지면 유지보수가 어려워진다는 단점이 존재한다.</p>
<br>

<h2 id="전문가-시스템의-구성-요소">전문가 시스템의 구성 요소</h2>
<ul>
<li><strong>지식 베이스 (Knowledge Base)</strong> : 전문가의 지식을 규칙 형태로 저장하는 부분<ul>
<li>전문가의 지식을 저장하는 장소</li>
<li>규칙, 사실, 전략 등 다양한 형태로 저장 가능함.</li>
</ul>
</li>
<li><strong>추론 엔진 (Inference Engine)</strong> : 지식 베이스의 규칙들을 바탕으로 문제 해결을 위한 추론을 수행하는 부분<ul>
<li>지식 베이스의 정보를 활용해 실제 추론 및 결정을 내리는 장소</li>
</ul>
</li>
<li><strong>사용자 인터페이스</strong> : 사용자와 시스템 간의 상호작용을 위한 부분</li>
</ul>
<br>

<h3 id="지식-베이스-knowledge-base">지식 베이스 (Knowledge Base)</h3>
<p>&nbsp;지식 베이스는전문가의 지식과 경험을 형식화하여 저장한다. 이때, 지식의 획득은 전문가로부터 직접적인 인터뷰나 문서 분석을 통해 이뤄질 수 있고, 시간이 지나면서 바뀌는 지식 또는 새로운 정보는 업데이트가 필요하다.</p>
<h4 id="구성-요소">구성 요소</h4>
<ul>
<li>규칙 (Rules) : &quot;만약 ~라면 ~하다&quot;<ul>
<li>규칙은 생성 규칙(Production Rule)이라고도 한다.</li>
<li>규칙에 AND, OR, 수학 연산자 등을 사용할 수 있다.</li>
</ul>
</li>
<li>팩트 (Facts) : 현재 상황이나 문제에 대한 정보, 사용자로부터 입력 받은 정보나 시스템이 이미 알고 있는 정보 등</li>
</ul>
<h4 id="지식-표현-방법">지식 표현 방법</h4>
<ul>
<li>생성 규칙 : if A then B, 특정 조건이 충족될 때 해당되는 동작이나 결정을 나타내는 형식</li>
<li>술어 논리 : 객체나 객체 사이의 관계나 속성을 표현하는 술어</li>
<li>의미망 : 개념과 개념 사이의 관계를 그래프 형식으로 나타낸 것으로 node는 개념, edge는 relationship</li>
<li>프레임 : 관련 정보, 속성을 묶어서 표현하는 자료구조</li>
<li>개념 그래프 : 지식의 의미적 관계를 그래프로 표현한 방법</li>
</ul>
<br>

<h3 id="추론-엔진-inference-engine">추론 엔진 (Inference Engine)</h3>
<p>&nbsp;추론 엔진에서는 지식 베이스에 저장된 규칙과 팩트를 활용해 문제 해결을 위한 추론을 수행한다.</p>
<p>&nbsp;추론 엔진은 지식 베이스의 지식을 활용해 추론 및 결정을 수행하는 모듈로, 두 가지 구성 요소로 구성된다.</p>
<ul>
<li>장기 기억 장치 : 전문가의 지식이 전략 형태로 저장되는 장소</li>
<li>단기 기억 장치 : 현재 문제 상황에서 판단에 필요한 정보를 임시로 저장하는 공간</li>
</ul>
<h4 id="추론-방식">추론 방식</h4>
<ul>
<li><strong>정방향 추론 (Forward Chaining)</strong> : 현재 팩트들로부터 새로운 팩트를 도출해 나가는 방식으로, &quot;이 사실들이 주어졌을 때 어떤 결론을 낼 수 있을까?&quot;라는 방식으로 추론함.<ul>
<li>정방향 추론은 초기 상태에서 시작해 규칙을 적용함으로써 목표 상태에 도달하는 방식의 추론 방식으로, 사실들을 기반으로 규칙을 도출하고, 새로운 사실이나 목표를 도출하는 과정이다.</li>
<li>주어진 사실을 기반으로 명확한 추론을 시작할 수 있고, 다양한 결과와 사실을 도출해 낼 수 있다는 장점이 있으나, 특정 목표를 향해 가지 않기에 불필요한 규칙 적용이 발생할 수도 있고, 이에 따라 추론 과정이 길어질 수 있다는 단점이 존재한다.</li>
</ul>
</li>
<li><strong>역방향 추론 (Backward Chaining)</strong> : 원하는 결론을 설정하고, 그 결론을 만족시키는 팩트나 규칙을 찾아가는 방식으로 &quot;이 결론을 내리려면 어떤 사실들이 필요할까?&quot;라는 방식으로 추론함.<ul>
<li>역방향 추론은 목표 상태에서 시작해 이를 만족시키기 위한 규칙을 역으로 추적해 나가는 방식의 추론 방식을 의미한다. 다시 말해, 원하는 결과나 목표를 얻기 위해 필요한 조건이 무엇인지 찾아나가는 과정이다.</li>
<li>명확한 목표를 가지고 추론을 수행하므로 효율적이고, 필요한 규칙만을 적용하므로 전체 추론 과정이 간결하다는 장점이 있으나, 초기 상태 정보가 부족하면 추론 시작조차 어렵고, 특정 목표에 대한 규칙이 없으면 추론이 불가능하다는 단점이 존재한다.</li>
</ul>
</li>
</ul>
<h4 id="추론-엔진의-작동-순서">추론 엔진의 작동 순서</h4>
<ol>
<li>팩트 확인 : 사용자로부터 입력받은 정보나 이미 알고 있는 정보를 팩트로 간주함.</li>
<li>규칙 적용 : 해당 팩트에 적용 가능한 규칙을 찾아냄.</li>
<li>추론 수행 : 적용 가능한 규칙을 바탕으로 새로운 팩트나 결론을 도출함.</li>
</ol>
<br>

<h2 id="code-example">Code Example</h2>
<pre><code># 지식 베이스와 추론 엔진이 어떻게 설계되는지
class ExpertSystem: # 추론 엔진
  def __init__(self):
    self.knowledge_base = KnowledgeBase() # 지식 베이스 객체 초기화

  # 진단 규칙을 평가하는 함수로, 규칙 조건이 사용자로부터 받은 증상과 일치하는지 검사
  # 만약 조건이 AND나 OR로 연결된 여러 개의 증상들로 구성되어 있다면, check_condition 함수는 자신을 재귀적으로 호출하여 각 하위 조건을 평가함.
  def check_condition(self, condition, selected_symptoms):
    # &quot;condition&quot; : &quot;stomach_pain&quot; 와 같이 단일 증상인지 확인
    if isinstance(condition, str): # condition이 문자열 인스턴스면
      return condition in selected_symptoms # 사용자가 입력한 증상이 conditiion에 포함되는지 확인

    op = condition.get(&quot;op&quot;) # 딕셔너리에서 op의 value만 추출
    if op == &quot;AND&quot;:
      # &quot;conds&quot; : [&quot;fever&quot;, &quot;cough&quot;, &quot;sore_throat&quot;]과 같은 증상들
      # 증상이 모두 있어야 True
      return all(self.check_condition(sub_cond, selected_symptoms) for sub_cond in condition[&quot;conds&quot;])
    elif op == &quot;OR&quot;:
      # 증상 중 하나라도 있으면 True
      return any(self.check_condition(sub_cond, selected_symptoms) for sub_cond in condition[&quot;conds&quot;])
  # 사용자로부터 입력받은 증상들을 바탕으로 check_condition 함수를 사용하여 가장 적합한 진단을 반환하는 함수
  # True/False 결과 받아서 규칙에 맞는지 확인하고, 맞으면 진단명과 설명 return
  def diagnose(self, selected_symptoms): # 증상에 따른 진단
    for rule in self.knowledge_base.rules: # 규칙 하나씩 꺼내보면서 (독감, 감기, 위염, 편두통 등)
      if self.check_condition(rule[&quot;condition&quot;], selected_symptoms): # 사용자의 증상이 지식 베이스에 포함되는지 확인
        return rule[&quot;diagnosis&quot;], rule.get(&quot;explanation&quot;, &quot;&quot;) # 진단명과 설명 return
    return &quot;No specific diagnosis.&quot;, &quot;Consult a doctor.&quot; # 기본 답변 return

class KnowledgeBase: # 지식 베이스
  def __init__(self):
    # 질문 시에 활용할 거 (descriptions)
    # 다시 말해, 증상들의 간략한 설명을 담고 있는 딕셔너리로, 이를 통해 사용자에게 어떤 증상을 물어볼지 결정함.
    self.symptom_descriptions = {
        &quot;fever&quot; : &quot;a fever&quot;,
        &quot;cough&quot; : &quot;a cough&quot;,
        &quot;sore_throat&quot; : &quot;a sore throat&quot;,
        &quot;stomach_pain&quot; : &quot;stomach pain&quot;,
        &quot;headache&quot; : &quot;a headache&quot;,
        &quot;runny_nose&quot; : &quot;a runny nose&quot;,
        &quot;itchy_eyes&quot; : &quot;itchy eyes&quot;
    }

    # rule 정의 - 진단 규칙 (AND, OR 조건을 포함한 복잡한 진단 규칙을 모델링함)
    # 각 규칙은 특정 조건과 그 조건이 만족될 때의 진단, 설명으로 구성되며,
    # 조건은 단순히 하나의 증상을 기반으로 할 수도 있고, AND, OR 등 논리 연산자를 포함해 여러 증상의 조합을 기반으로 할 수도 있음.
    self.rules = [
        {
            &quot;condition&quot; : {&quot;op&quot;: &quot;AND&quot;, &quot;conds&quot; : [&quot;fever&quot;, &quot;cough&quot;, &quot;sore_throat&quot;]},
            &quot;diagnosis&quot; : &quot;You might have the flu (당신은 독감일 수 있습니다.)&quot;,
            &quot;explanation&quot; : &quot;Flu symptoms include fever, cough, and sore throat. (독감의 증상에는 발열, 기침, 목 아픔이 포함됩니다.)&quot;
        },
        {
            &quot;condition&quot; : {&quot;op&quot; : &quot;AND&quot;, &quot;conds&quot; : [&quot;runny_nose&quot;, &quot;sore_throat&quot;]},
            &quot;diagnosis&quot; : &quot;You might have a cold. (당신은 감기일 수 있습니다.)&quot;,
            &quot;explanation&quot; : &quot;Cold symptoms include runny nose and sore throat. (감기의 증상에는 콧물과 목 아픔이 포함됩니다.)&quot;
        },
        {
            &quot;condition&quot; : &quot;stomach_pain&quot;,
            &quot;diagnosis&quot; : &quot;You might have gastritis. (당신은 위염일 수 있습니다.)&quot;,
            &quot;explanation&quot; : &quot;Pain in the stomach can be a sympotom of gastritis. (위의 통증은 위염의 증상일 수 있습니다.)&quot;
        },
        {
            &quot;condition&quot; : &quot;headache&quot;,
            &quot;diagnosis&quot; : &quot;You might have a migraine. (당신은 편두통일 수 있습니다.)&quot;,
            &quot;explanation&quot; : &quot;A servere headache can be a symptom of migraine. (심한 두통은 편두통의 증상일 수 있습니다.)&quot;
        },
        {
            &quot;condition&quot; : &quot;itchy_eyes&quot;,
            &quot;diagnosis&quot; : &quot;You might have an allergy. (당신은 알레르기일 수 있습니다.)&quot;,
            &quot;explanation&quot; : &quot;Itchy eyes can be a symptom of an allergy. (눈이 가렵다면 알레르기의 증상일 수 있습니다.)&quot;
        }
    ]

# 사용자에게 각 증상에 대해 직접 물어보고, 사용자의 응답을 바탕으로 선택된 증상들의 리스트를 반환
# &#39;yes&#39;로 응답한 증상만 반환 리스트에 포함
def get_symptoms_from_user():
  symptom_descriptions = KnowledgeBase().symptom_descriptions
  selected_symptoms = []

  print(&quot;Please answer the following (yes or no):&quot;)
  for symptom, description in symptom_descriptions.items():
    answer = input(f&quot;Do you have {description}? &quot;).lower()
    if answer == &#39;yes&#39;:
      selected_symptoms.append(symptom)
  return selected_symptoms

if __name__ == &quot;__main__&quot;:
  selected_symptoms = get_symptoms_from_user() # 사용자가 증상 선택
  system = ExpertSystem()
  diagnosis, explanation = system.diagnose(selected_symptoms) # 진단 (진단명, 설명 or 기본 답변 return)
  print(diagnosis)
  print(explanation)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Minimax Alogorithm과 Alpha-Beta Pruning]]></title>
            <link>https://velog.io/@inhwaaa_v/Minimax-Alogorithm%EA%B3%BC-Alpha-Beta-Pruning</link>
            <guid>https://velog.io/@inhwaaa_v/Minimax-Alogorithm%EA%B3%BC-Alpha-Beta-Pruning</guid>
            <pubDate>Sat, 18 Oct 2025 13:01:07 GMT</pubDate>
            <description><![CDATA[<h1 id="minimax-알고리즘">Minimax 알고리즘</h1>
<p>&nbsp;Minimax 알고리즘이란, 최대 최소 전략을 사용해 최선의 의사결정을 하는 데 사용되는 알고리즘으로, 상대가 최선의 수를 둔다고 가정하고, 그 상황에서 내가 얻을 수 있는 최선의 결과를 선택하는 알고리즘이다. 다시 말해, 상대가 최선을 다할 것이라고 가정하고, 그 상황에서도 나의 최선을 선택하는 전략이다.</p>
<p>&nbsp;상대방은 나에게 최대한 불리한 점수(최소 점수)를 선택하고, 나는 그 중에서도 가능한 한 최대 점수를 얻는 선택을 하므로 최소를 최대화한다는 의미에서 Minimax 알고리즘이라고 불린다. (MAX는 자신의 점수를 최대화하고,
MIN은 상대의 점수를 최소화한다. (다시 말해, MAX의 손해를 극대화하는 것이다))</p>
<ul>
<li>미니맥스 알고리즘은 유한한 탐색 트리 안에 해답이 존재하면 반드시 찾을 수 있으므로 완결될 수 있다.</li>
<li>미니맥스 알고리즘은 최적성을 지닌다.</li>
<li>트리의 최대 값이가 m이고, 각 노드에서 가능한 수가 b개일 때, 시간 복잡도와 공간 복잡도는 $O(b^m)$이며, 모든 경우의 수를 탐색하기에 시간 복잡도가 지수적으로 증가하므로 이를 줄이기 위해 알파-베타 가지치기(Alpha-Beta Pruning)가 등장했다.</li>
</ul>
<pre><code># 보드는 1차원 리스트로 구현한다.
game_board = [&#39; &#39;, &#39; &#39;, &#39; &#39;,
              &#39; &#39;, &#39; &#39;, &#39; &#39;,
              &#39; &#39;, &#39; &#39;, &#39; &#39;]


# 비어 있는 칸을 찾아서 리스트로 반환한다.
def empty_cells(board):
    cells = [] # 비어있는 칸 저장할 리스트
    for x, cell in enumerate(board):
            if cell == &#39; &#39;: # board가 비어있으면
                cells.append(x) # append
    return cells

# 비어 있는 칸에는 놓을 수 있다.
def valid_move(x):
    return x in empty_cells(game_board) # x가 비어있는 셀 안에 포함되어 있으면 True / else False

# 위치 x에 놓는다.
def move(x, player):
    if valid_move(x): # 비어있는 칸이면
        game_board[x] = player # 위치 x에 놓고
        return True
    return False # 아니면 False

# 현재 게임 보드를 그린다.
def draw(board):
    for i, cell in enumerate(board):
        if i%3 == 0:
            print(&#39;\n----------------&#39;)
        print(&#39;|&#39;, cell , &#39;|&#39;, end=&#39;&#39;)
    print(&#39;\n----------------&#39;)

# 보드의 상태를 평가한다.
def evaluate(board):
    if check_win(board, &#39;X&#39;):
        score = 1
    elif check_win(board, &#39;O&#39;):
        score = -1
    else:
        score = 0
    return score

# 1차원 리스트에서 동일한 문자가 수직선이나 수평선, 대각선으로 나타나면
# 승리한 것으로 한다.
def check_win(board, player):
    win_conf = [
        [board[0], board[1], board[2]], # 가로
        [board[3], board[4], board[5]],
        [board[6], board[7], board[8]],
        [board[0], board[3], board[6]], # 세로
        [board[1], board[4], board[7]],
        [board[2], board[5], board[8]],
        [board[0], board[4], board[8]], # 대각선
        [board[2], board[4], board[6]],
    ]
    return [player, player, player] in win_conf # Player = &#39;O&#39; or &#39;X&#39;이고, 이거에 포함되면 True

# 1차원 리스트에서 동일한 문자가 수직선이나 수평선, 대각선으로 나타나면
# 승리한 것으로 한다.
def game_over(board):
    return check_win(board, &#39;X&#39;) or check_win(board, &#39;O&#39;)

# 미니맥스 알고리즘을 구현한다.
# 이 함수는 순환적으로 호출된다.
def minimax(board, depth, maxPlayer):
    pos = -1 # 초기값 / 최대 혹은 최소 위치 저장하는 변수
    # 단말 노드이면 보드를 평가하여 위치와 평가값을 반환한다.
    if depth == 0 or len(empty_cells(board)) == 0 or game_over(board): # depth=0이거나 비어있는 칸이 없거나 둘 중 한명이 이긴 경우
        return -1, evaluate(board)

    if maxPlayer:
        value = -10000  # 음의 무한대
        # 자식 노드를 하나씩 평가하여서 최선의 수를 찾는다.
        # 모든 비어있는 셀 p에 대해서 평가해 최선의 위치를 찾음.
        for p in empty_cells(board):
            board[p] = &#39;X&#39;        # 보드의 p 위치에 &#39;X&#39;을 놓는다.

            # 경기자를 교체하여서 minimax()를 순환호출한다.
            x, score = minimax(board, depth-1, False) # depth-1은 재귀호출이니까 depth 점차 줄여나가는 거 / 제한하지 않으면 계속 찾아서 들어감
            board[p] = &#39; &#39;        # 재귀 반복이 끝나면 보드는 원 상태로 돌린다.

            if score &gt; value:
                value = score     # 최대값을 취한다.
                pos = p        # 최대값의 위치를 기억한다.
    else: # if minPlayer
        value = +10000  # 양의 무한대
        # 자식 노드를 하나씩 평가하여서 최선의 수를 찾는다.
        for p in empty_cells(board):
            board[p] = &#39;O&#39;        # 보드의 p 위치에 &#39;O&#39;을 놓는다.

            # 경기자를 교체하여서 minimax()를 순환호출한다.
            x, score = minimax(board, depth-1, True)
            board[p] =  &#39; &#39;        # 보드는 원 상태로 돌린다.

            if score &lt; value:
                value = score     # 최소값을 취한다.
                pos = p        # 최소값의 위치를 기억한다.

    return pos, value    # 위치와 값을 반환한다.

player=&#39;X&#39;
# 메인 프로그램
while True:
    draw(game_board)
    if len(empty_cells(game_board)) == 0 or game_over(game_board): # 단말 노드
        break
    i, v = minimax(game_board, 9, player==&#39;X&#39;) # 최대 or 최소 위치와 값을 반환하면
    move(i, player) # 이동하도록 해서 최적의 값 찾아 나가도록
    if player==&#39;X&#39;:
        player=&#39;O&#39;
    else:
        player=&#39;X&#39;

if check_win(game_board, &#39;X&#39;):
    print(&#39;X 승리!&#39;)
elif check_win(game_board, &#39;O&#39;):
    print(&#39;O 승리!&#39;)
else:
    print(&#39;비겼습니다!&#39;)</code></pre><br>

<h1 id="알파베타-가지치기-alpha-beta-pruning">알파베타 가지치기 (Alpha-Beta Pruning)</h1>
<p>&nbsp;Minimax의 성능을 향상시키기 위한 최적화 기법으로, 더 이상 유망하지 않은 가지는 탐색하지 않고 잘라내는 방식으로 작동한다.</p>
<p>&nbsp;알파-베타 가지치기의 핵심 아이디어는, <strong>탐색 트리의 어떤 부분은 제외하여도 결과에 영향을 주지 않는다</strong>는 것이다. 따라서 탐색을 더 깊게 진행할 필요가 없을 때 탐색을 중단(알파-베타 컷 오프)하여도 결과는 Minimax 알고리즘에서의 결과와 동일하다.</p>
<p>&nbsp;알파-베타 가지치기(Alpha-Beta Pruning)의 핵심은 필요없는 분기를 미리 배제함으로써 탐색 범위를 줄이는 것이다. 이를 위해 두 가지 값을 유지한다.</p>
<ul>
<li>Alpha : MAX가 현재까지 선택한 최댓값 (이 값보다 낮으면 볼 필요가 없음)</li>
<li>Beta : MIN이 현재까지 선택한 최솟값 (이 값보다 높으면 볼 필요가 없음)</li>
<li><strong>β ≤ α이면, 이후 탐색은 무의미하므로 가지치기함.</strong></li>
</ul>
<p>&nbsp;트리를 탐색하며 MAX는 Alpha 값을, MIN은 Beta 값을 갱신하며, Beta ≤ Alpha가 되는 시점부터 해당 노드의 탐색을 중지한다. 따라서 전체 트리 탐색 과정 중 많은 부분을 스킵할 수 있고, 시간 복잡도 또한 $O(b^{d/2})$까지 개선이 가능하다.</p>
<pre><code># 주요 변경 사항은 minimax 함수에 추가된 alpha와 beta 매개변수와
# 해당 함수 내에서 alpha와 beta 값을 업데이트하는 코드입니다.
# 이로 인해 알파베타 가지치기가 가능해져, 탐색 트리의 일부 브랜치를 건너뛰게 됩니다.

import random

game_board = [&#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;, &#39; &#39;]

def empty_cells(board):
    cells = []
    for x, cell in enumerate(board):
        if cell == &#39; &#39;:
            cells.append(x)
    return cells

def valid_move(x):
    return x in empty_cells(game_board)

def move(x, player):
    if valid_move(x):
        game_board[x] = player
        return True
    return False

def draw(board):
    for i, cell in enumerate(board):
        if i % 3 == 0:
            print(&#39;\n----------------&#39;)
        print(&#39;|&#39;, cell , &#39;|&#39;, end=&#39;&#39;)
    print(&#39;\n----------------&#39;)

def evaluate(board):
    if check_win(board, &#39;X&#39;):
        return 1
    elif check_win(board, &#39;O&#39;):
        return -1
    return 0

def check_win(board, player):
    win_conf = [
        [board[0], board[1], board[2]],
        [board[3], board[4], board[5]],
        [board[6], board[7], board[8]],
        [board[0], board[3], board[6]],
        [board[1], board[4], board[7]],
        [board[2], board[5], board[8]],
        [board[0], board[4], board[8]],
        [board[2], board[4], board[6]],
    ]
    return [player, player, player] in win_conf

def game_over(board):
    return check_win(board, &#39;X&#39;) or check_win(board, &#39;O&#39;)

def minimax(board, depth, alpha, beta, maxPlayer):  # 알파와 베타 매개변수 추가
    pos = -1
    if depth == 0 or len(empty_cells(board)) == 0 or game_over(board):
        return -1, evaluate(board)

    if maxPlayer:
        value = -10000
        for p in empty_cells(board):
            board[p] = &#39;X&#39;
            x, score = minimax(board, depth-1, alpha, beta, False)
            board[p] = &#39; &#39;
            if score &gt; value:
                value = score
                pos = p
            # Alpha = MAX가 현재까지 선택한 최댓값 (이 값보다 낮으면 볼 필요가 없음)
            alpha = max(alpha, value)  # 알파 업데이트
            # β ≤ α이면, 이후 탐색은 무의미하므로 가지치기
            if alpha &gt;= beta:  # 알파-베타 가지치기 조건
                break
    else:
        value = +10000
        for p in empty_cells(board):
            board[p] = &#39;O&#39;
            x, score = minimax(board, depth-1, alpha, beta, True)
            board[p] =  &#39; &#39;
            if score &lt; value:
                value = score
                pos = p
            # Beta = MIN이 현재까지 선택한 최솟값 (이 값보다 높으면 볼 필요가 없음)
            beta = min(beta, value)  # 베타 업데이트
            # β ≤ α이면, 이후 탐색은 무의미하므로 가지치기
            if alpha &gt;= beta:  # 알파-베타 가지치기 조건
                break

    return pos, value

player = random.choice([&#39;X&#39;, &#39;O&#39;]) # 플레이어 랜덤 선택
first_move = True

while True:
    draw(game_board)
    if len(empty_cells(game_board)) == 0 or game_over(game_board):
        break
    if first_move:
        move(random.choice(empty_cells(game_board)), player) # 첫 번째 수 무작위, 첫 번째 플레이어 수 둔 것
        first_move = False
    else:
        # 최적의 수를 찾아 이동
        i, v = minimax(game_board, len(empty_cells(game_board)), -10000, 10000, player == &#39;X&#39;)  # 알파와 베타 값 초기화
        move(i, player)
    if player == &#39;X&#39;:
        player = &#39;O&#39;
    else:
        player = &#39;X&#39;

if check_win(game_board, &#39;X&#39;):
    print(&#39;X 승리!&#39;)
elif check_win(game_board, &#39;O&#39;):
    print(&#39;O 승리!&#39;)
else:
    print(&#39;비겼습니다!&#39;)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[탐색]]></title>
            <link>https://velog.io/@inhwaaa_v/%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@inhwaaa_v/%ED%83%90%EC%83%89</guid>
            <pubDate>Fri, 17 Oct 2025 19:40:09 GMT</pubDate>
            <description><![CDATA[<h1 id="탐색-search">탐색 (Search)</h1>
<ul>
<li><strong>탐색 (Search)</strong> : 상태 공간에서 시작 상태(초기 상태)에서 목표 상태까지의 경로</li>
<li><strong>상태 공간 (State Space)</strong> : 상태들이 모여있는 공간</li>
<li><strong>연산자</strong> : 다음 상태를 생성하는 것</li>
</ul>
<br>

<h2 id="blind-search-맹목적-탐색-uninformed-search">Blind Search (맹목적 탐색, Uninformed Search)</h2>
<p>&nbsp;Blind Search란, 현재 상태에서 목표 상태까지의 Step 개수(Path Cost)를 모르는 것을 의미한다. 다시 말해, Blind Search에서는 현재 상태에서 목표 상태까지 얼마나 남았는지 보여주는 함수인 H(n)을 계산하지 않는다. 이러한 특성으로 인해 최적 해에 도달하기 어렵다는 단점이 존재한다.</p>
<p>&nbsp;Blind Search는 경험적으로 분석을 할 수 없기에 Goal State로 탐색이 진행될 때 각 Search들만의 규칙을 가지고 탐색하긴 하지만, 현재 State에서 Goal까지의 Path Cost를 구할 수 없기에 모든 방향으로 뻗어나가고, 무한 루프에 빠질 가능성이 존재한다.</p>
<p>&nbsp;Blind Search의 예시에는, Breath-First Search (너비 우선 탐색), Uniform-Cost Search (일정 비용 탐색), Depth-First Search (깊이 우선 탐색), Depth-Limited Search (깊이 제한 선택), Iterative Deepening Search (점차적 깊이 제한 탐색), Bidirectional Search (양방향 탐색) 등이 존재한다.</p>
<br>

<h2 id="heuristic-search-경험적-탐색-informed-search">Heuristic Search (경험적 탐색, Informed Search)</h2>
<p>&nbsp;Heuristic Search란, 어떤 상태가 목표 상태로 가는 데 적합한지를 아는 것을 의미한다. 이는  맹목적 탐색의 단점을 극복하기 위해 탄생한 탐색법으로, Solution을 좀 더 효율적으로 찾을 수 있다는 장점이 존재하며, 대체로 Uninformed Search보다 효율적이다.
&nbsp;Best-First Search, A* Search, Hill Climbing이 Heuristic Search의 대표적인 예시이며, 이때, Best-First Search가 가장 기본적인 접근 방법이며, 대다수의 경험적 탐색 알고리즘의 기반이 된다.</p>
<ul>
<li>Best-First Search는 평가 함수 F(n)을 기반으로 한 Tree/Graph Search의 확장으로, 평가 함수는 G(n)과 H(n)으로 구성되며, 이는 n부터 Goal State까지의 비용(=거리)을 의미함.<ul>
<li>G(n) : 시작 노드에서 현재 노드까지 걸린 Path Cost</li>
<li>H(n) : 현재 노드에서 목표 노드까지의 가장 빠른 거리</li>
<li>(ex) 현재 위치 = 목표 노드, H(n) = 0</li>
</ul>
</li>
<li>Greedy Best-First Search에서 그리디는 &#39;탐욕적인&#39;이라는 의미로, 현재 상태에서 가장 좋아보이는 것을 선택해 최적인지 아닌지를 항상 확인하는 알고리즘이며, 평가 함수는 오직 H(n)만으로만 구성됨. (G(n)으로만 구성되면 Uniform-Cost Search)</li>
<li>A* Search의 평가 함수식은 F(n) = H(n) + G(n)으로 구성되며, 결과적으로 Optimal하고 Complete함.</li>
<li>Hill Climbing Search는 주변 값을 탐색하여 점차적으로 좋은 값으로 올라간다는 점에서 그 이름이 붙여졌으며, 현재 위치에서 주변 값을 탐색하여 후보 값들을 선정한 뒤, 그 중 하나를 골라 점차적으로 값을 업데이트 하는 방식임.<ul>
<li>대표적인 Local Search Algorithm으로, 전체를 한 번에 탐색하는 것이 아닌, 주변(local)을 계속 탐색하면서 optimum을 찾아 나가는 방식임.</li>
<li>순수한 Hill Climbing 기법은 OPEN, CLOSED 리스트를 사용하지 않고, 오직 H(n) 값만을 사용해 탐색함.</li>
<li>현재 상태에서 인접한 상태들(neighbors)을 살펴보고, 그 중 가장 좋은 방향으로 한 단계씩 이동하는 탐색 방법으로, 전체 탐색 공간을 한 번에 보지 않고 위치 주변에서 조금씩 더 나은 해로 올라가는 탐색임. (이러한 특성 때문에 Local Search Algorithm의 대표적인 예시라고 함)</li>
<li>초기 상태 선택 → 초기 상태의 이웃 상태 평가 → 이웃 중 가장 좋은 상태로 이동 → 더 나은 상태가 없을 때까지 반복하여 local optimum 도달 시 종료됨.</li>
<li>&quot;휴리스틱하다&quot;는 것은 최적 해를 반드시 찾는다는 것이 아닌, 제한된 정보로 최대한 효율적인 근사 해를 찾는 방법임을 의미함. 따라서 Hill Climbing Search가 Global Optimum을 보장하지 않아도, 현재 상태를 평가해서 더 나은 방향을 선택한다는 점에서 휴리스틱한 탐색이라고 할 수 있음. (A* Search와 같이 Optimal하거나 Complete하지 않으나, Heuristic함.)</li>
<li>실제로 가장 유용하고 많이 사용되는 것은 A* 알고리즘이며, 이러한 Heuristic Search는 길찾기 알고리즘에 많이 사용됨.</li>
</ul>
</li>
</ul>
<p>** 맹목적 탐색과 경험적 탐색은 여행에서 길을 찾는 것과 고향에서 길을 찾는 것의 차이로 이해할 수 있음. (여행 - 지도 앱을 통해 길을 따라감 / 고향 - 어떤 버스가 빠르고, 어디서 환승하는 게 더 나은지에 대한 판단이 가능함)</p>
<br>

<h2 id="탐색-알고리즘-성능-측정">탐색 알고리즘 성능 측정</h2>
<ul>
<li><strong>완결성 (completeness)</strong> : 문제에 해답이 있다면, 반드시 해답을 찾을 수 있는지 여부 (탐색 알고리즘이 무한 루프에 빠지지 않고 언젠가 목표 상태를 찾아낼 수 있으면 완결성이라고 함)</li>
<li><strong>최적성 (optimality)</strong> : 가장 비용이 낮은 해답을 찾을 수 있는지 여부</li>
<li>*<em>시간 복잡도 (time complexity) *</em>: 해답을 찾는 데 걸리는 시간 (탐색 완료까지 걸리는 시간)</li>
<li><strong>공간 복잡도 (space complexity)</strong> : 탐색을 수행하는 데 필요한 메모리 양<ul>
<li>b : 탐색 트리의 최대 분기 개수 (자식 수)</li>
<li>d : 목표 노드의 깊이</li>
<li>m : 트리의 최대 깊이</li>
</ul>
</li>
</ul>
<br>

<h2 id="blind-search-예시">Blind Search 예시</h2>
<h3 id="8-puzzle-문제">8-puzzle 문제</h3>
<ul>
<li>8-puzzle은 3×3 격자 위에 8개의 숫자 타일과 1개의 빈칸(blank) 이 있는 슬라이딩 퍼즐 문제로, 타일을 상/하/좌/우로 움직여 목표 상태(Goal State)에 도달하는 것을 목표로 함.</li>
<li>Initial State → Intermediate States → Goal State</li>
<li>상태 (state) : 길이 9의 리스트 (0은 빈칸)</li>
<li>연산자 (operator) : 빈칸을 상/하/좌/우로 이동</li>
<li>상태 공간 (state space) : 가능한 모든 보드 배치 집합</li>
</ul>
<pre><code>from collections import deque
import math
import time
import matplotlib.pyplot as plt

class State:
  __slots__ = {&#39;board&#39;, &#39;depth&#39;, &#39;goal&#39;, &#39;parent&#39;, &#39;move&#39;}
  def __init__(self, board, goal, depth=0, parent=None, move=None):
    # 왜 튜플로 묶이는지를 이해하기 위해선 자료형에 대한 이해 필요
    # -&gt; 튜플은 &quot;수정 불가능한 순서 있는 집합&quot;
    # 탐색 과정에서 여러 노드들이 같은 리스트 객체를 공유하면, 하나를 수정했을 때 다른 것도 바뀌는 문제가 발생할 수 있기에 tuple을 사용함.
    # 또한, 해시를 활용하기 위해선 불변성이 필요한데, 리스트나 딕셔너리 같은 변경 가능한 객체들은 Hashable하지 않으므로 수정 불가능한 tuple을 사용함.
    self.board = tuple(board) # 길이 9 리스트 (현재 퍼즐 상태)
    self.depth = depth # 시작으로부터의 깊이, g(n)
    self.goal = tuple(goal) # 목표 상태
    self.parent = parent # 경로 복원을 위한 부모 포인터
    self.move = move # 현재 상태를 만들게 한 이동

  # 해시 -&gt; 어떤 데이터를 숫자(정수)로 반환한 값으로, 객체를 set, dict과 같은 자료구조에서 빠르게 찾을 수 있도록 하기 위해 사용함.
  # set과 dict는 내부적으로 해시 테이블(Hash Table) 구조로 되어 있고, 각 객체는 hash() 함수를 통해 정수 해시값을 얻고 그 값을 테이블의 인덱스(버킷 위치) 로 사용해 저장함.
  # 이로 인해 비교 대상이 매우 적어져 시간 복잡도가 줄어듦.
  # 여기서는 State 객체를 visited 집합에 넣기 위해 해시값을 사용함. (중복 체크 빠르게 하고, State 객체 해시 가능하게 만들기 위해)
  def __hash__(self):
    return hash(self.board)

  def __eq__(self, other):
    return isinstance(other, State) and self.board == other.board

  def __lt__(self, other):
    return self.depth &lt; other.depth

  def is_goal(self):
    return self.board == self.goal

  def index0(self):
    return self.board.index(0) # 보드에서 값이 값이 0인 칸 인덱스 번호 반환

  def expand(self):
    i = self.index0()
    # divmod(a, b) -&gt; (몫, 나머지) 튜플 반환
    # 여기서는 divmod(index0, 3)이므로 보드에서 값이 0인 칸 인덱스 번호가 몇 열에 있는지 알 수 있음.
    # (ex) i = 4라면, divmod의 결과값은 (1, 1)이고, 다음과 같이 인덱스 값이 대응됨.
    # 즉, 이를 통해 값이 0인 퍼즐 위치를 찾아낼 수 있음.
    # 0 1 2   →   (0,0) (0,1) (0,2)
    # 3 4 5   →   (1,0) (1,1) (1,2)
    # 6 7 8   →   (2,0) (2,1) (2,2)
    r, c = divmod(i, 3)
    children = []
    def swap_and_make(nr, nc, move): # nc, nr -&gt; 0을 옮길 위치의 좌표
      j = nr * 3 + nc # 2차원 좌표를 1차원 인덱스로 변환함 (ex. (2,1) -&gt; 7)
      new_board = list(self.board) # 현재 board는 튜플이라 불변하므로 list로 만들기
      new_board[i], new_board[j] = new_board[j], new_board[i] # 위치 옮기고
      return State(new_board, self.goal, self.depth+1, self, move)  # 새로운 상태로 State 객체 생성
    if c &gt; 0:
      children.append(swap_and_make(r, c-1, &#39;L&#39;)) # 왼쪽
    if r &gt; 0:
      children.append(swap_and_make(r-1, c, &#39;U&#39;)) # 위
    if c &lt; 2:
      children.append(swap_and_make(r, c+1, &#39;R&#39;)) # 오른쪽
    if r &lt; 2:
      children.append(swap_and_make(r+1, c, &#39;D&#39;)) # 아래
    return children

  def pretty(self):
    b = list(self.board)
    return (f&quot;{b[0:3]}\n{b[3:6]}\n{b[6:9]}\n&quot;)

# 탐색 끝난 뒤 부모 포인터 따라가면서 목표 상태까지 온 경로(탐색 경로) 복원
def reconstruct_path(s: State):
  path = []
  while s is not None:
    path.append(s) # 현재 상태 path 추가
    s = s.parent # 부모 상태로 이동 (이걸 부모가 없는 시작점까지 반복)
  path.reverse() # 순서 뒤집기 (자식부터 거꾸로 추가했으니까)
  return path

def print_path(path):
  for i, st in enumerate(path):
    print(f&quot;Step {i}  (move={st.move})\n{st.pretty()}&quot;)
</code></pre><blockquote>
<h4 id="list-dictionary-set-tuple의-정리">List, Dictionary, Set, Tuple의 정리</h4>
</blockquote>
<ul>
<li>List : 파이썬에서의 배열 표현으로, 크기 조정/서로 다른 데이터 타입 가능<ul>
<li>modifiable, resizeable, different data type</li>
</ul>
</li>
<li>Dictionary : (key, value) 쌍으로 이루어진 표현<ul>
<li>존재하지 않는 키 값 사용 시 keyError가 발생하므로 get() 함수 활용</li>
<li>값 삭제 : del 변수[key]</li>
<li>key, value unpacking : 변수.items()</li>
</ul>
</li>
<li>Set : 고유한 원소들의 순서 없는 집합<ul>
<li>add(), remove() 활용</li>
<li>distinct(고유한), Unordered</li>
</ul>
</li>
<li>Tuple : 수정 불가능한 순서 있는 집합으로 리스트와 비슷하나 딕셔너리의 키, Set의 element와 같은 역할<ul>
<li>immatable(수정 불가능한), Ordered list</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="hashable하려면-값이-불변해야-하는-이유">Hashable하려면 값이 불변해야 하는 이유</h4>
</blockquote>
<ul>
<li>해시는 해시값을 키로 사용해 데이터를 저장하는데, 해시 값이 변하면 그 객체의 저장 위치가 변하거나 위치를 잘못 참조해 데이터 무결성이 깨질 수 있음. (<a href="https://velog.io/@naringst/python-hashable">https://velog.io/@naringst/python-hashable</a>)</li>
</ul>
<br>

<h3 id="bfs-breath-first-search">BFS (Breath-First Search)</h3>
<ul>
<li>분기 계수 b가 유한하면 너비 우선 탐색은 반드시 해답을 발견할 수 있음.</li>
<li>시간 복잡도와 공간 복잡도는 모두 지수 복잡도 $O(b^d)$</li>
</ul>
<pre><code>def bfs(start_board, goal_board):
  start = State(start_board, goal_board) # 시작 상태 생성
  if start.is_goal(): # 시작 상태가 목표 상태면 그대로 반환
    return reconstruct_path(start), 0, 0 # path, expanded, t1 - t0
  q = deque([start])
  visited = {start} # 방문한 상태
  expanded = 0 # 확장한 노드
  t0 = time.time()
  while q:
    cur = q.popleft() # 큐에서 맨 앞 노드 꺼냄
    expanded += 1
    for child in cur.expand(): # 가능한 모든 자식 상태 생성
      if child in visited: # 현재 상태에서 나온 자식 이미 봤으면 넘기기
        continue
      if child.is_goal(): # 목표 상태 도달
        t1 = time.time()
        path = reconstruct_path(child)
        return path, expanded, t1 - t0 # 리턴
      visited.add(child)
      q.append(child)
  return None, expanded, time.time() - t0 # 목표 상태에 도달하지 못함 -&gt; 시작 노드 = 목표 노드이므로 기존 Path가 없음 -&gt; None</code></pre><blockquote>
<h4 id="dequedouble-ended-queue를-사용한-큐-관리">deque(Double-Ended Queue)를 사용한 큐 관리</h4>
</blockquote>
<ul>
<li><p>양쪽에서 요소를 추가하거나 제거 가능한 데이터 구조로, &#39;양방향 큐&#39;라고도 한다.</p>
</li>
<li><p>스택처럼 사용할 수도 있고, 큐 처럼 사용할 수도 있다.</p>
</li>
<li><p>일반적인 리스트(list)가 이러한 연산에 O(n)이 소요되는 데 반해, deque는 O(1)로 접근하기 때문에 리스트보다 빠른 추가/제거 연산이 가능하며, push/pop 연산이 빈번한 알고리즘에서 리스트보다 월등한 속도를 자랑하기 때문에 자주 사용된다.</p>
</li>
<li><p>deque는 스레딩과 무관하며, 기본적으로 스레드 안전하지 않다. 그러나 리스트보다 <strong>빠른 큐 작업</strong>을 제공하며, <strong>회전(Rotation) 및 슬라이스(Slicing) 같은 추가적인 기능</strong>도 지원하기 때문에 주로 사용한다.</p>
</li>
<li><p>주요 메서드</p>
<ul>
<li><strong>append(item)</strong> : 끝에 요소 추가</li>
<li><strong>appendleft(item)</strong> : 앞에 요소 추가</li>
<li><strong>remove(item)</strong> : 특정 요소 제거</li>
<li><strong>rotate(num)</strong> : 요소를 num만큼 회전해 앞뒤를 변경 (양수면 오른쪽 회전, 음수면 왼쪽 회전)</li>
</ul>
<br> 

</li>
</ul>
<h3 id="dfs-depth-first-search">DFS (Depth-First Search)</h3>
<ul>
<li>무한 상태 공간에서의 깊이 우선 탐색은 무한 경로를 따라 끝없이 나가므로 완결적이지 않음.</li>
<li>최적성 또한 없으며, 가장 왼쪽(leftmost)에 있는 해답만을 발견함.</li>
<li>시간 복잡도는 $O(b^m)$, 공간 복잡도는 $O(bm)$이므로 공간 복잡도는 선형 복잡도만을 지님.</li>
</ul>
<pre><code>def dfs(node:State, limit, visited, expanded):
  if node.is_goal(): # 현재 노드 == 목표 상태
    return reconstruct_path(node), expanded # (path, expanded)
  if limit == 0:
    return None, expanded # 목표 도달도 전에 깊이 제한 도달 &amp; 깊이 제한이 0이라 루트 노드 이상 탐색 X
  visited.add(node) # 노드 방문 기록
  for child in node.expand(): # 자식 탐색
    if child in visited:
      continue
    expanded += 1
    res, expanded = dfs(child, limit - 1, visited, expanded) # 자식 상태에 대해 재귀적 실행
    if res is not None: # res -&gt; reconstruct_path(node)로 반환받은 값
      return res, expanded
  return None, expanded # 목표 상태에 도달 못함

# 깊이 제한 탐색 : 특정 깊이 이상으로 들어가지 못하게 하는 dfs
def dfs_with_depth_limit(start_board, goal_board, limit=20):
  start = State(start_board, goal_board)
  t0 = time.time()
  path, expanded = dfs(start, limit, set(), 0) # set() -&gt; visited 집합
  return path, expanded, time.time() - t0</code></pre><br>

<h3 id="iddfs-점증적-깊이-증가-dfs-iterative-deepening-dfs">IDDFS (점증적 깊이 증가 DFS, Iterative Deepening DFS)</h3>
<ul>
<li>한계 깊이를 차례로 늘려가며서 제한 탐색을 진행하는 것</li>
<li>깊이 우선 탐색의 공간 효율성과 너비 우선 탐색의 완전성을 결합함.</li>
<li>BFS처럼 최단 경로를 찾을 수 있음. (최적성)</li>
<li>노드를 여러 번 방문할 가능성이 있어 비효율적일 수 있고, 간단한 문제에 대해서는 BFS, DFS가 더 쉬울 수 있음.</li>
</ul>
<pre><code>def iddfs(start_board, goal_board, max_depth=30):
  start = State(start_board, goal_board)
  total_expanded = 0 # 탐색 과정에서 확장한 전체 노드 수
  t0 = time.time()
  # max_depth까지 하나씩 늘려가면서 DFS 실행
  for depth in range(max_depth + 1):
    path, expanded = dfs(start, depth, set(), 0)
    total_expanded += expanded
    if path is not None: # goal 도달
      return path, total_expanded, time.time() - t0
  return None, total_expanded, time.time() - t0</code></pre><br>

<h2 id="heuristic-search-예시">Heuristic Search 예시</h2>
<h3 id="heuristic-정의">Heuristic 정의</h3>
<pre><code>def h_misplaced(board, goal):
  return sum(1 for i in range(9) if board[i] != 0 and board[i] != goal[i]) # 잘못 놓인 타일 수 반환

def h_manhattan(board, goal):
  pos = {v: i for i, v in enumerate(goal)} # 목표 상태 위치 인덱스 딕셔너리 형태 저장
  dist = 0 # 전체 맨해튼 거리 합
  for i, v in enumerate(board):
    if v == 0: # 0은 빈칸이니까 무시
      continue
    gi = pos[v] # 현재 값 v가 목표 상태에서 어디에 있어야 하는지 확인
    r1, c1 = divmod(i, 3) # 현재 위치
    r2, c2 = divmod(gi, 3) # 목표 위치
    dist += abs(r1 - r2) + abs(c1 - c2) # h(n) 거리 계산
  return dist</code></pre><br>

<h3 id="a-search-fn--gn--hn">A* Search (f(n) = g(n) + h(n))</h3>
<p>&nbsp;A* Search는 지금까지 온 거리 + 남은 거리를 합산해 총 예상 비용이 낮은 후보부터 탐색해 나가는 알고리즘을 의미하며, A* 알고리즘이 Optimal하고 Complete하게 동작하려면, 휴리스틱 조건을 만족해야 한다.</p>
<ul>
<li>Admissibility : 휴리스틱 비용이 0≤h(n)≤h∗(n)의 조건을 만족해야 한다는 것. 다시 말해, 휴리스틱 값은 항상 실제 최단 거리보다 크지 않아야 함. (낙관적인 추정을 통해 Optimal한 해를 추정하기 위해) (이때, h*(n)은 실제 최단 거리, 즉 true cost를 의미함)<ul>
<li>0 이상 : 휴리스틱이 0보다 작으면 오히려 잘못된 방향으로 탐색이 유도될 수 있음.</li>
<li>h*(n) 이하 : 실제 남은 거리보다 작거나 같은 값만큼만 추정해야 탐색이 너무 비관적이지도 않고, 너무 낙관적이지도 않은 “올바른 방향으로의 탐색”이 가능함.</li>
<li>다시 말해, 과대평가하지 않는 범위에서 가장 현실적인 추정치가 최적의 휴리스틱이라고 할 수 있음.</li>
</ul>
</li>
<li>Consistency (일관성) : 경로를 따라가면서 휴리스틱 값이 일관되게 감소해야 한다는 것으로, h(n)≤c(n,n&#39;)+h(n&#39;)의 조건을 만족함. (이때, c(n,n&#39;)는 n에서 n&#39;으로 이동할 때의 실제 비용, h(n&#39;)는 다음 상태의 휴리스틱 값임)<ul>
<li>즉, 연결된 두 노드 간의 휴리스틱 값의 차이는 실제 이동 비용보다 크지 않아야 한다. (c(n, n&#39;) &gt;= h(n) - h(n&#39;))</li>
<li>두 노드를 건너는 실제 비용보다 휴리스틱 값의 차이가 크면, 휴리스틱이 일관되지 않게 되어 탐색이 불안정해지고 중복 탐색이 발생할 수 있다.</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="heapq">heapq</h4>
</blockquote>
<ul>
<li>순위가 가장 높은 자료(data)를 가장 먼저 꺼내는 우선순위 큐를 구현한 모듈 (<a href="https://docs.python.org/3/library/heapq.html">https://docs.python.org/3/library/heapq.html</a>)</li>
</ul>
<pre><code>import heapq

def astar(start_board, goal_board, h_func=h_manhattan):
  start = State(start_board, goal_board)
  if start.is_goal(): # 시작 상태 = 목표 상태
    return reconstruct_path(start), 0, 0
  open_heap = [] # 우선 순위 큐 (F 값 작은 순서대로 탐색해 나가야 하니까)
  gscore  = {start.board: 0}
  f0 = h_func(start.board, start.goal) # 시작 상태의 f(n) -&gt; 0 + h(start)
  heapq.heappush(open_heap, (f0, 0, start)) # 우선순위 큐에 시작 상태 저장, g = 0
  closed = set() # set은 중복 방지 &amp; 빠른 탐색(해시 기반 구조)을 위해 사용됨.
  expanded = 0
  t0 = time.time()

  while open_heap:
    f, g, cur = heapq.heappop(open_heap) # F(n) 값이 가장 작은 상태 pop
    if cur.board in closed: # 이미 처리된 것들 (visited 처리)
      continue
    if cur.is_goal(): # 목표 상태 도달
      t1 = time.time()
      return reconstruct_path(cur), expanded, t1-t0
    closed.add(cur.board)
    expanded += 1
    for child in cur.expand(): # 현재 상태에서 이동할 수 있는 모든 자식 상태 생성
      tentative_g = g+1 # 자식 G(n)
      # tentative_g -&gt; 시작 노드에서 현재 노드까지 걸린 Path Cost, gscore.get(child.board, math.inf) -&gt; 지금까지 발견된 child 최단 경로 g
      if child.board in closed and tentative_g &gt;= gscore.get(child.board, math.inf): # 닫힌 경로 + 경로 별로
        continue
      if tentative_g &lt; gscore.get(child.board, math.inf): # 더 짧은 경로 찾음
        gscore[child.board] = tentative_g # gscore 갱신 (G(n))
        fchild = tentative_g + h_func(child.board, child.goal) # F(n) = H(n) + G(n)
        heapq.heappush(open_heap, (fchild, tentative_g, child)) # open_heap에 넣음 (F(n), G(n), 현재 상태)
  return None, expanded, time.time() - t0</code></pre><br>

<h3 id="기본-보드를-통한-각-알고리즘-비교">기본 보드를 통한 각 알고리즘 비교</h3>
<pre><code>START = [2, 8, 3, 1, 6 , 4, 7, 0, 5]
GOAL = [1, 2, 3, 8, 0, 4, 7, 6, 5]

def run_all(start=START, goal=GOAL):
  results = {}
  # BFS
  path, expd, t = bfs(start, goal)
  results[&#39;BFS&#39;] = (len(path)-1 if path else None, expd, t)

  # DFS (depth-limited) - 무한 상태 공간에서의 깊이 우선 탐색은 무한 경로를 따라 끝없이 나가므로 완결적이지 않음 (expanded = 5369)
  path, expd, t = dfs_with_depth_limit(start, goal, limit=20)
  results[&#39;DFS(d&lt;=20)&#39;] = (len(path)-1 if path else None, expd, t)

  # IDDFS - 노드를 여러 번 방문할 가능성이 있어 비효율적일 수 있음 (expanded = 91)
  path, expd, t = iddfs(start, goal, max_depth=30)
  results[&#39;IDDFS&#39;] = (len(path)-1 if path else None, expd, t)

  # misplaced와 manhattan은 모두 휴리스틱 함수 H(n)로 사용될 수 있으며,
  # 어떤 휴리스틱 함수를 넘기느냐에 따라 탐색 효율성이 다르다는 것을 결과를 통해 확인 가능함.
  # manhattan -&gt; 계산은 조금 더 복잡하지만 정보량이 많음.
  # 그럼에도 두 A* 모두 완결성과 최적성은 동일하게 유지됨.
  # A* (misplaced)
  path, expd, t = astar(start, goal, h_func=h_misplaced)
  results[&#39;A*(misplaced)&#39;] = (len(path)-1 if path else None, expd, t)

  # A* (manhattan)
  path, expd, t = astar(start, goal, h_func=h_manhattan)
  results[&#39;A*(manhattan)&#39;] = (len(path)-1 if path else None, expd, t)
  return results

res = run_all()
for k, v in res.items():
  print(f&quot;{k:16s} | path_len={v[0]} expanded={v[1]} time={v[2]:.4f}s&quot;)</code></pre><h4 id="코드-실행-결과">코드 실행 결과</h4>
<pre><code>BFS              | path_len=5 expanded=26 time=0.0002s
DFS(d&lt;=20)       | path_len=17 expanded=5369 time=0.0167s
IDDFS            | path_len=5 expanded=91 time=0.0002s
A*(misplaced)    | path_len=5 expanded=6 time=0.0001s
A*(manhattan)    | path_len=5 expanded=5 time=0.0001s</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Group Normalization]]></title>
            <link>https://velog.io/@inhwaaa_v/Group-Normalization</link>
            <guid>https://velog.io/@inhwaaa_v/Group-Normalization</guid>
            <pubDate>Tue, 14 Oct 2025 09:57:20 GMT</pubDate>
            <description><![CDATA[<h1 id="group-normalization">Group Normalization</h1>
<h2 id="1-introduction">1. Introduction</h2>
<p><strong>배치 정규화(Batch Norm, BN)</strong> : (미니)배치 내에서 계산된 평균과 분산으로 특성 정규화</p>
<ul>
<li><p>배치 통계의 확률적 불확실성은 일반화를 향상시키는 정규화로 작용하기도 함.</p>
</li>
<li><p>배치 차원에서 정규화하는 독특한 동작으로 인해 충분히 큰 크기의 배치가 필요(ex. 작업자당 32개 이미지)하며, 배치 크기를 줄이면 모델 오류가 증가한다는 단점이 존재함.</p>
</li>
<li><p>최근 많은 모델들은 메모리를 많이 소비하는 non-trivial batch size 크기로 훈련
  → BN에 대한 과한 의존 ⇒ 메모리 제한으로 인해 큰 용량의 모델 탐지 어려움</p>
</li>
<li><p>Detection, Segmentation, Video Recognition와 같은 컴퓨터 비전 작업 → 배치 크기 제한 필요</p>
<ul>
<li>(ex) Fast/er and Mask R-CNN → batch size 1 or 2 images (높은 해상도 위해)</li>
<li>이 경우, BN을 linear layer로 변환하고, frozen함.
  → 작은 배치 사이즈로 쓰면 힘드니까 평균, 분산을 고정시키고 선형 레이어로 쓴다는 의미</li>
</ul>
</li>
<li><p>3D convolution을 활용한 video classification</p>
<p>  → spatial-temporal feature가 존재해 temporal length와 batch size 사이의 trade-off 발생</p>
<p>  → 즉, temporal length를 길게 가져가면 batch size를 줄여야 하고, batch size를 늘리면 temporal length를 짧게 가져가야 하는 상황이 발생</p>
</li>
<li><p>BN의 사용은 종종 이러한 시스템이 모델 설계와 배치 크기 간의 타협을 해야 함을 요구함.</p>
</li>
</ul>
<br>

<p><strong>Group Normalization(GN)</strong> </p>
<ul>
<li>채널을 그룹으로 나눈 다음, 각 그룹 내에서 평균과 분산을 계산해 정규화 수행</li>
<li>GN의 계산은 배치 크기와 무관하며, 배치 크기의 폭 넓은 범위에서 정확도가 안정적임.</li>
<li>넓은 범위의 배치 크기에서 매우 안정적으로 동작함.</li>
</ul>
<br>

<p><strong>Group Normalizatioon (GN)이 왜 좋은지?</strong></p>
<ul>
<li>SIFT 및 HOG와 같은 많은 고전적 feature들이 그룹 단위 특징을 지니며, 그룹 단위 정규화를 포함함.</li>
<li>(ex) HOG 벡터는 여러 공간 셀의 결과로, 각 셀은 정규화된 방향 히스토그램으로 표현함.</li>
<li>마찬가지로, GN의 채널을 여러 개의 그룹으로 나누고 각 그룹 내의 feature를 정규화하는 레이어 제안<ul>
<li>이러한 GN은 배치 차원을 활용하지 않으며, 그 계산도 배치 크기와 독립적이며, 다양한 배치 크기에서도 안정적으로 동작함.</li>
<li>배치 크기에 의존적이지 않기에 배치 크기가 변경되더라도 pre-training에서 fine-tuning으로 자연스럽게 전이될 수 있음.</li>
<li>COCO object detection, segmentation을 위한 Mask R-CNN이나 Kinetics 비디오 분류를 위한 3D 컨볼루션 네트워크에서도 더 나은 결과를 보임.</li>
<li>LN 및 IN 대신해 사용될 수 있으며, Sequential 또는 Generative 모델에도 적용할 수 있음.</li>
</ul>
</li>
</ul>
<br>

<p><strong>배치 차원에서 정규화 하지 않는 기존의 방법</strong></p>
<ul>
<li>Layer Normalization</li>
<li>Instance Normalization</li>
<li>이 방법들은 Sequential models(RNN/LSTM) 또는 Generative models(GANs) 훈련에 효과적이나, visual recognition에서 제한된 성공 (→ GN이 더 나은 결과를 보임)</li>
</ul>
<br>

<h2 id="2-related-work">2. Related Work</h2>
<p><strong>Normalization</strong></p>
<ul>
<li>입력 데이터 정규화 → 학습 속도 빨라짐</li>
<li><strong>Local Response Normalization (LRN)</strong> : 각 픽셀에 대해 작은 이웃 영역 내에서만 통계를 계산함.</li>
<li><strong>Batch Normalization (BN)</strong> : Batch dimension을 따라 보다 Global Normalization을 수행하며, 모든 layer에 대해 이를 수행하도록 제안됨.<ul>
<li>“배치(batch)”라는 개념이 항상 존재하는 것은 아니며, 경우에 따라 변경될 수 있음.</li>
<li>(ex) inference 시에는 학습 데이터에서 미리 계산된 평균과 분산이 사용되며, 주로 running average 방식을 통해 계산되고, 테스트 시엔 정규화가 수행되지 않음.</li>
<li>타겟 데이터 분포가 변경될 경우, 미리 계산된 통계값도 변경될 수 있는데, 이로 인해 학습, 전이, 테스트 시의 일관성 문제가 발생함.</li>
</ul>
</li>
<li>배치 차원을 활용하지 않는 정규화 방법들<ul>
<li><strong>Layer Normalization (LN)</strong><ul>
<li>채널 차원(channel dimension)을 따라 정규화 수행</li>
</ul>
</li>
<li><strong>Instance Normalization (IN)</strong><ul>
<li>각 샘플별로 Batch Normalization과 유사한 연산을 수행</li>
</ul>
</li>
<li><strong>Weight Normalization (WN)</strong><ul>
<li>feature가 아니라 filter weights를 정규화하는 방식</li>
</ul>
</li>
</ul>
</li>
<li>이러한 방법들은 batch dimension에서 발생하는 문제를 겪지 않지만, 여전히 많은 visual recognition 작업에서 BN의 정확도를 따라가지 못함.</li>
</ul>
<p>.</p>
<p><strong>Addressing small batches</strong></p>
<ul>
<li><strong>Batch Renormalization (BR)</strong><ul>
<li>두 개의 추가 매개변수를 도입하여 BN의 평균과 분산 추정치를 특정 범위 내로 제한해 배치 크기가 작을 때의 변동을 줄임.</li>
<li>작은 배치 환경에서 BN보다 더 나은 정확도를 보이나, BR도 배치에 의존적이며, 배치 크기가 줄어들 때 여전히 정확도가 저하된다는 문제 존재</li>
</ul>
</li>
<li><strong>Synchronized BN (SyncBN)</strong><ul>
<li>작은 배치를 사용하지 않으려는 방법</li>
<li>여러 개의 GPU에서 계산된 배치 통계를 공유해 배치 크기를 키운 것처럼 동작하도록 함.</li>
<li>이 방법은 작은 배치 문제를 해결하는 것이 아니라, 문제를 하드웨어 및 엔지니어링 요구사항으로 전가하는 것에 불과함. (이러한 이유에서 더 많은 하드웨어 자원을 필요로 함)</li>
<li>비동기 솔버 ASGD(Asynchronous Stochastic Gradient Descent)를 사용할 수 없음.</li>
<li>이러한 문제로 인해 Synchronized BN (SyncBN)의 활용 범위가 제한됨.</li>
</ul>
</li>
<li>기존 연구들 → 배치 통계 계산 방식을 조정하는 방식이었다면 GN은 애초에 배치 통계를 계산할 필요가 없음. (배치 통계 방식을 회피)</li>
</ul>
<p><strong>Group-wise computation</strong></p>
<ul>
<li>Group convolutions은 AlexNet에서 모델을 두 개의 GPU에 분산시키기 위해 처음 제안되었고, 이후 그룹을 모델 설계의 한 차원으로 활용하는 개념이 활발히 연구되기 시작함.</li>
<li>ResNeXt : 네트워크의 depth, width, grous 간의 trade-off 조사<ul>
<li>비슷한 연산량을 유지하면서도 그룹 수를 늘리면 정확도 향상</li>
</ul>
</li>
<li>MobileNet 및 Xception : channel-wise convolution 활용<ul>
<li>그룹 수가 채널 수와 같은 특수한 형태의 그룹 합성곱</li>
</ul>
</li>
<li>ShuffleNet<ul>
<li>channel shuffle 연산 제안</li>
<li>그룹으로 나뉜 특징 맵의 축을 재배열하는 방식 도입</li>
</ul>
</li>
<li>이러한 방법들은 모두 채널 차원을 그룹으로 나누어 활용하는 공통점 존재</li>
<li>GN은 이러한 방법들과 관련이 있긴 하지만, group convolution을 필요로 하지 않음.</li>
</ul>
<br>

<h2 id="3-group-normalization">3. Group Normalization</h2>
<ul>
<li>SIFT, HOG, GIST와 같은 고전적 feature들은 설계상 group-wise 표현 방식을 따름.<ul>
<li>즉, 특정 기준으로 그룹을 만들어 특징을 표현함.</li>
<li>또한, 이러한 특징들은 <strong>히스토그램 기반 표현 방식</strong>을 사용함.<ul>
<li>HOG : 이미지의 각 작은 영역에서 Gradient의 방향 분포를 히스토그램으로 나타냄.</li>
<li>SIFT : 이미지의 특정 키포인트 주변에서 경사도 방향 히스토그램을 계산하는 방식으로 Feature를 만듦.</li>
<li>GIST : 이미지를 여러 스케일과 방향으로 필터링한 후, 각 필터 응답을 지역적인 통계량(히스토그램)으로 요약함.</li>
</ul>
</li>
</ul>
</li>
<li>이러한 feature들은 각 히스토그램 또는 각 방향에 대해 group-wise normalization을 적용해 처리됨.<ul>
<li>HOG : 작은 블록별 경사도 크기 정규화</li>
<li>SIFT : 키포인트 주변의 히스토그램을 정규화해 조명 변화 줄임.</li>
</ul>
</li>
<li>고차원 feature 표현 방법인 VLAD 및 Fisher Vectors (FV)도 group-wise feature
  → 이 경우 그룹은 특정 클러스터와 관련된 서브 벡터로 해석 가능함.
  → 즉, 특정 클러스터(ex. k-means clustering에서 생성된 중심점)과 관련된 서브 벡터를 의미함.
  → VLAD : 각 특징점이 어떤 클러스터에 속하는지를 기반으로 특징 벡터 구성
  → Fisher Vector : GMM(Gaussian Mixture Model)을 기반으로 특징을 클러스터별로 정리한 후, 각 클러스터에 대해 통계적인 정보를 표현함.</li>
</ul>
<blockquote>
<p>정리하면, 기존 로컬 특징이든, 고차원 특징이든 그룹을 기준으로 묶어서 표현하는 방식이 많음.</p>
</blockquote>
<ul>
<li><p>Deep Neural Network feature를 비구조적인 벡터로 생각할 필요가 없음.
→ 어떤 필터와 그 필터의 수평 반전은 유사한 필터 응답 분포를 가질 가능성이 높음.
→ 다시 말해, 어떤 방향으로 된 엣지를 감지하는 필터가 있다면, 그 필터를 좌우로 뒤집은 필터도 비슷한 역할을 할 수 있음. (유사한 필터 응답 분포를 지님)
→ 따라서 이 두 개의 필터는 서로 비슷한 특징을 가지므로 함께 정규화 가능함.
→ 이러한 자연스러운 관계를 통해 필터 그룹을 정의하고 정규화하면 성능 향상 가능
→ 더 깊은 계층에선 주파수, 모양, 질감, 조명 변화 등 다양한 기준을 사용해 그룹을 만들 수 있고, 이러한 요소들은 서로 상관관계를 가질 수 있으며, 상호 의존적인 특징을 형성함.</p>
<blockquote>
<p>CNN에서 학습된 필터들이 단순한 개별적인 벡터가 아니라, 서로 구조적으로 연관될 수 있음.</p>
</blockquote>
</li>
<li><p>신경과학 연구에서도 뉴런의 반응을 정규화하는 패턴이 존재하며, 이를 딥러닝에도 적용 가능함.</p>
</li>
</ul>
<blockquote>
<p>&nbsp;요약하면, CNN 필터들을 다양한 기준에서 살펴봤을 때, 이들은 구조적인 관계성을 지님.
&nbsp;&nbsp;필터들이 가지는 구조적 관계성을 그룹으로 만들고, 그룹 단위 정규화 ⇒ Group Normalization</p>
</blockquote>
<hr>
<h1 id="정리">정리</h1>
<p>&nbsp;기존 배치 정규화는 배치 단위로 정규화해서, 배치 사이즈의 영향을 많이 받고, RNN이나 LSTM과 같은 모델에서 타임 스텝이 아닌 배치를 통해 통계를 내고 고려해서 최적이 아닌 분산과 평균으로 정규화 될 수 있다! 이런 문제점이 존재함.</p>
<p>&nbsp;이걸 해결하기 위해 나온게 LN, IN과 같은 방법들임.</p>
<p>&nbsp;LN은 같은 샘플, 모든 채널을 기준으로 IN은 하나 샘플, 개별 채널을 기준으로 정규화를 하니까 배치 사이즈의 영향을 받지 않고, 타임 스텝마다 정규화가 가능해서 생성형 모델이나 시퀀셜 모델에 사용하기 좋았는데, 단점은 Visual recognition task에서 그렇게 좋은 결과를 내지 못했다는 것.</p>
<p>&nbsp;이를 해결하는 것이 GN.</p>
<p>&nbsp;이 GN이 가능한 이유는 CNN 필터가 학습하는 특징들 간 관계가 어느 정도 연관성을 가지고 있어서 이것들을 그룹화할 수 있어서임.</p>
<p>&nbsp;그래서 정해진 그룹별로 정규화를 수행하는 게 GN이고 Visual recognition task에서 IN, LN보다 좋은 성능을 보였고, 배치 사이즈에 독립적이라서 LSTM, RNN과 같은 IN, LN이 사용되는 것들에서도 GN을 사용하면 좋은 성능을 보일 수 있음.</p>
<p>&nbsp;만약 그룹이 단 하나의 채널만 지닌다면, GN=IN이고, 그룹이 모든 채널을 포함하면 GN=LN이 되는 관계성을 지님. 
→ 즉, GN은 IN과 LN을 일반화한 방식이라 상황에 따라 적절한 그룹 수를 조절하면 됨</p>
<hr>
<h1 id="간단-요약">간단 요약</h1>
<p><strong>** Normalization (”정규화”, “feature scaling”)</strong></p>
<ul>
<li>다양한 측도의 데이터들을 <strong>공통의 스케일</strong>로 변환하는 작업</li>
<li>데이터의 범주를 바꾸는 데이터 스케일링 관점</li>
<li>Data Input을 정규화함으로써 weight, bias의 학습을 안정적이고 빠르게 할 수 있음.</li>
<li>(ex) z-score normalization</li>
</ul>
<br>

<p><strong>** Regularization (”정규화”)</strong></p>
<ul>
<li>모델 학습에 패널티를 부여해 <strong>모델의 복잡도를 낮추는</strong> 과정</li>
<li>손실 함수에 규제항을 추가해 과적합 제어</li>
<li>(ex) Regularizer(L1, L2), Dropout, Early-stopping, etc…</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/9be0b023-bde6-49c5-8468-af3b6fe85a6c/image.png" alt=""></p>
<br>

<p><strong>** Batch Normalization</strong> : Normalization 효과를 Batch Normalization으로 확장한 것</p>
<ul>
<li>각 레이어의 Input을 Batch 단위로 Normalization</li>
<li>더 깊은 신경망에서 안정적이고 빠르게 학습</li>
<li>Data의 internal covariate shift를 해소함으로써 얻을 수 있음.</li>
<li><strong>covariate shift</strong> : test input distribution이 train input distribution에서 변화해 달라지는 현상 → 모델 일반화 성능에 부정적 영향</li>
<li><strong>Internal covariate shift</strong> : Deep Neural Network 내 hidden layer의 관점에서 나타나는 Covariate shift 현상</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/b56278b6-58b7-4074-bf3c-b78c6fe0aa57/image.png" alt=""></p>
<br>

<p><strong>** Batch Normalization의 한계</strong></p>
<ul>
<li><p>전체 데이터가 대상이 아닌 배치 단위 통계량 활용
→ 해결 : Group Normalization</p>
</li>
<li><p>RNN 구조에선 하나의 배치가 매 time-step마다 서로 다른 통계량을 지녀  배치 대상 정규화가 매 time-step, 매 layer마다 새로 적용되어야 함.
→ 해결 : Layer Normalization, Instance Normalization
→ Layer Normalization : 각 타임 스텝에서 전체 채널 기준 정규화
→ Instance Normalization : 각 타입 스텝에서 개별 채널 기준 정규화
→ 시퀀스가 긴 경우에도 각 타임 스텝을 독립적으로 정규화하기에 RNN, LSTM 같은 구조에서도 안정적으로 작동함.</p>
</li>
</ul>
<br>

<p><strong>** Group Normalization</strong></p>
<ul>
<li>BN은 채널을 공유하는 모든 배치가 함께 정규화되나, LN, IN, GN은 배치가 아닌 채널 단위로 묶여서 정규화됨.</li>
</ul>
<br>

<p><strong>** 왜 채널을 중심으로 그룹화?</strong></p>
<ul>
<li>The channels of visual representations are not entirely independent.</li>
<li>예를 들어 CNN Feature Map → 다양한 특징들. 독립적으로 존재하는 것이 아니기에 함께 묶여서 분석될 수 있음.</li>
</ul>
<br>

<p><strong>** Formulation</strong></p>
<ul>
<li>4가지 Normalization 공식은 같고, Normalization 대상만 다름.<ul>
<li>$x$ = the feature computed by a layer</li>
<li>$S_i$ = the set of pixels in which the mean and std are computed</li>
<li>m = the size of set</li>
</ul>
</li>
<li><strong>Batch Normalization</strong> : Pixels <strong>sharing the same channel</strong> index are normalized together (N, H, W)</li>
<li><strong>Layer Normalization</strong> : Pixels on the <strong>same sample</strong> are nomalized together (C, H, W)<ul>
<li>레이어의 모든 채널이 similar contributions를 지닌다고 가정</li>
</ul>
</li>
<li><strong>Instance Normalization</strong> : Pixels on the <strong>same sample and channel</strong> are normalized together (H, W)<ul>
<li>오직 spatial dimension에만 의존하므로 채널 의존성 활용 기회를 놓침</li>
</ul>
</li>
<li><strong>Group Normalization</strong> : Pixels on the same group are normalized together (group of channels * (H,W))<ul>
<li>[G = C] : Group has only 1 channel (GN = IN)</li>
<li>[G = 1] : GN = LN (그룹이 모든 채널을 포함)</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/d9dbe502-3bd3-4b1e-9c8d-f0b7d4c929fc/image.png" alt="">
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/e2d6c5dd-2fb1-4542-894e-a239af99cfe4/image.png" alt=""></p>
<br>

<p>** Code</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/82ab9525-8973-4bc1-9f8b-9db51edbe840/image.png" alt=""></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Efficientnet]]></title>
            <link>https://velog.io/@inhwaaa_v/Efficientnet</link>
            <guid>https://velog.io/@inhwaaa_v/Efficientnet</guid>
            <pubDate>Tue, 14 Oct 2025 09:28:30 GMT</pubDate>
            <description><![CDATA[<h2 id="efficientnet">Efficientnet</h2>
<ul>
<li>기존 연구 : depth, width, resolution 중 하나를 확장해 정확도를 높이는 것이 일반적이고, 두 개 또는 세 개 차원을 임의로 확장할 수도 있으나, 수많은 수작업 조정 과정의 필요 &amp; 최적이 아닌 성능이나 효율성을 낼 가능성이 존재함.</li>
<li>핵심 질문 : “is there a principled method to scale up ConvNets that can achieve better accuracy and efficiency?”</li>
<li>깊이, 너비, 해상도의 모든 차원을 고정된 계수로 균일하게 확장함으로써 차원 사이 균형을 맞추는 간단하지만 효과적인 compound coefficient를 사용해 균일하게 확장하는 스케일링 방법 제안함.</li>
<li>모델 확장 효과가 기본이 되는 네트워크 구조에 크게 의존하므로 Neural Architecture Search(NAS)를 통해 새로운 기본 네트워크를 설계함. → 이 네트워크를 확장한 것이 EfficientNet</li>
<li>(ex) 계산 자원을 $2^N$배로 늘리고 싶다면, 깊이는 $α^N$, 너비는 $β^N$, 해상도는 $γ^N$만큼 증가시키면 되며, 이때, $α, β, γ$는 원래 작은 모델에서 grid search를 통해 정해진 상수임.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/bdae6bc6-ae8a-4885-833b-5e18a1e5f847/image.png" alt=""></li>
</ul>
<br>


<h3 id="problem-formulation">Problem Formulation</h3>
<ul>
<li><p>ConvNet의 레이어 $i$ → $Y_i = F_i(X_i)$</p>
<ul>
<li>이때 입력 텐서 $X_i$의 shape은 $(H_i, W_i, C_i)$</li>
<li>하나의 ConvNet $N$은 여러 레이어가 순차적으로 합성된 구조로 표현될 수 있음.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/6672707a-fdc8-4bda-bb12-79617f8096b9/image.png" alt=""></li>
<li>실제론, ConvNet 레이어들이 여러 개 stage로 나뉘고, 각 stage 내 레이어들은 동일한 구조를 가지는 경우가 많으므로 다음과 같이 표현될 수 있음.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/38793737-9803-4ddd-bacf-a3397ea6cbe3/image.png" alt=""></li>
</ul>
</li>
<li><p>기존 ConvNet 설계는 주로 각 레이어 구조 $F_i$를 어떻게 설계할지를 중심으로 하지만, 모델 스케일링은 $F_i$를 고정하고, 레이어 개수($L_i$), 채널 수(너비, $C_i$), 해상도($H_i, W_i$)를 확장함.</p>
</li>
<li><p>또한, $L_i, C_i, H_i, W_i$ 조합을 조정할 수 있는 design space를 줄이기 위해, 모든 레이어를 동일한 비율로 스케일하도록 제한함.</p>
</li>
<li><p>목표 : 주어진 resource constraints에 대해 모델 정확도를 최대화하는 것으로, 다음과 같은 최적화 문제로 공식화될 수 있음.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/851220dd-c9da-4fa7-8eb3-4176d6a14b53/image.png" alt=""></p>
<br>

<h3 id="scaling-dimensions">Scaling Dimensions</h3>
<p><strong>Observation 1</strong> : 네트워크의 너비, 깊이, 해상도 중 어느 차원이든 확장하면 정확도가 향상되나, 모델이 커질수록 그 이득은 줄어든다.</p>
<ol>
<li><strong>Depth (d)</strong> : 가장 일반적인 방법으로, 복잡한 특징 학습 가능 / 일반화 성능 좋음<ul>
<li>깊은 네트워크는 Gradient Vanishing으로 인해 학습이 어렵기에 이를 해결하기 위해 skip connection이나 batch normalization과 같은 기법들이 도입되었으나, 그럼에도 불구하고 너무 깊은 모델은 성능 향상이 거의 없거나 오히려 나빠짐. (ex) ResNet, Inception 등</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/139339d9-865f-4a2b-9739-fdfaee3ddd4f/image.png" alt=""></p>
<ol start="2">
<li><strong>Width (w)</strong> : 너비가 넓으면 더 세밀한 특징 포착이 가능하고, 학습도 더 쉬워지는데, 작은 모델에서는 괜찮으나, 너무 넓고 얕은 네트워크는 높은 수준 특징을 포착하지 못함. (ex) MobileNet, ShuffleNet 등</li>
</ol>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/40239d44-11f8-4dd6-afbc-5abaaedd6883/image.png" alt=""></p>
<ol>
<li><strong>Resolution (r)</strong> : 해상도가 높으면 좀 더 세밀한 패턴 포착이 가능하고, 초기엔 보통 224*224 해상도를 사용했으나, 최근엔 더 높은 정확도를 위해 299x299, 331x331, 480x480, 600x600 등이 사용됨.<ul>
<li>r = 1.0은 224x224, r = 2.5는 560x560 해상도</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/c6856692-c6f4-4f05-b13e-c09fd49e84d1/image.png" alt=""></p>
<br>

<h3 id="compound-scaling">Compound Scaling</h3>
<p><strong>Observation 2</strong> : 더 나은 정확도와 효율성을 달성하려면, ConvNet 스케일링 시 너비, 깊이, 해상도 세 차원을 균형 있게 조정해야 한다. (스케일링 차원들이 서로 독립적이지 않으므로)</p>
<ul>
<li>다양한 깊이, 해상도에서 너비만 확장했을 때의 성능 비교</li>
<li>d = 1.0, r = 1.0 → 너비만 키우면 정확도가 금방 포화됨</li>
<li>d = 2.0, r = 2.0 → 같은 FLOPS 조건에서도 정확도가 크게 향상</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/8c6f9f25-d4e4-43ed-9c4b-caf0ccf6c68d/image.png" alt=""></p>
<br>

<h3 id="compound-scaling-method">compound scaling method</h3>
<ul>
<li>복합 계수 φ를 사용해 세 가지 차원을 동시에, 일관되게 확장하는 방법</li>
<li>$α, β, γ$는 각각 깊이, 너비, 해상도에 얼마만큼 자원을 분배할지 결정하는 상수값으로, small grid search를 통해 찾으며, φ는 사용자가 지정하는 계수로, 얼마나 많은 추가 자원을 사용할 것인지를 결정함. (ex) φ=1 → 기존보다 2배 많은 자원 사용</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/3896d925-5d70-4b52-9297-ed2e03afe000/image.png" alt=""></p>
<ul>
<li>FLOPS ∝ $d · w² · r²$<ul>
<li>FLOPS는 일반적으로 $d · w² · r²$에 비례하므로 깊이를 2배 늘리면 FLOPS는 2배 증가하고, 너비나 해상도를 2배 늘리면 FLOPS는 4배 증가하므로 논문에선 다음과 같이 스케일링함.</li>
<li>FLOPS ∝ $(d · w² · r²)^ φ$</li>
<li>이때, $d · w² · r²$ ≈ 2 → φ 1만큼 증가 시 FLOPS는 $2^φ$만큼 증가하도록 설정함.</li>
</ul>
</li>
</ul>
<br>

<h3 id="efficientnet-architecture">EfficientNet Architecture</h3>
<p>&nbsp;모델 스케일링은 기본 네트워크의 레이어 연산자를 변경하지 않기에 좋은 baseline network를 선택하는 것도 매우 중요하므로 모바일 사이즈 기본 네트워크를 별도 개발함. (EfficientNet)</p>
<h4 id="efficientnet-b0">EfficientNet-B0</h4>
<ul>
<li>정확도와 FLOPS를 줄이도록 두 가지를 동시에 고려하는 multi-objective NAS를 활용함.</li>
<li>특정 하드웨어를 타깃으로 하지 않으므로 latency가 아닌 FLOPS를 최적화하는 목적 함수를 최적화 대상으로 사용함<ul>
<li>$ACC(m) × [FLOPS(m) / T]^w$</li>
<li>이때, ACC(m)는 정확도, T는 목표 FLOPS, w는 정확도와 FLOPS 간 trade-off 조절 하이퍼파라미터</li>
<li>이 NAS 탐색 결과로 얻은 네트워크를 EfficientNet-B0라고 하며, 네트워크의 기본 블록은 Mobile Inverted Bottleneck MBConv이며, 여기에 SE 최적화가 추가됨.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/2feea13a-8246-4d1c-ab26-e1fc68a8f964/image.png" alt=""></p>
<h4 id="efficientnet-b1b7--efficientnet-b0을-시작으로-compound-scaling-방법을-두-단계에-거쳐-적용">EfficientNet-B1~B7 : EfficientNet-B0을 시작으로 compound scaling 방법을 두 단계에 거쳐 적용</h4>
<ul>
<li><p><strong>Step 1</strong> : 스케일링 계수 결정</p>
<ul>
<li>φ = 1 (자원이 2배 주어진 상황)을 가정하고, 식 2, 3 기반 α, β, γ 값을 grid search로 찾음.</li>
<li>B0에 가장 적합한 값 : <strong>α = 1.2</strong>, <strong>β = 1.1</strong>, <strong>γ = 1.15 (</strong>이때, <strong>α · β² · γ² ≈ 2</strong> 조건 만족)</li>
</ul>
</li>
<li><p><strong>Step 2</strong> : 계수 고정 후 모델 확장</p>
<ul>
<li>찾은 α, β, γ 값을 고정된 상수로 사용하며, φ 값을 점진적으로 증가시키며 네트워크를 확장해 B1~B7까지의 모델을 생성함.</li>
</ul>
</li>
<li><p>이때, 큰 모델에서 α, β, γ 값을 탐색하면 높은 성능을 낼 수 있으나, 탐색 비용이 기하급수적으로 증가하므로 작은 모델(B0)에서 한 번만 계수를 탐색(Step 1)하고, 이후에는 동일한 계수를 사용해 모든 확장 모델을 자동으로 생성(Step 2)하는 방법을 활용함.</p>
</li>
</ul>
<pre><code class="language-python"># MBConv + SE
# 일반 Bottleneck : 입력 -&gt; 채널 줄이기 -&gt; 연산 -&gt; 다시 늘리기
# Inverted Bottleneck(MBConv) : 입력 -&gt; 채널 늘리기 -&gt; depthwise conv -&gt; 채널 줄이기 -&gt; 출력
# SE 구조 : Squeeze (GAP 각 채널 정보 요약) -&gt; Excitation (채널별 가중치를 통해 중요한 채널은 크게, 아닌 채널은 작게) -&gt; Rescale (원래 Feature map에 가중치 곱해 반영)

(2): Sequential(
      (0): MBConv(
        (block): Sequential(
            # 채널 확장
          (0): Conv2dNormActivation(
            (0): Conv2d(16, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
            (2): SiLU(inplace=True)
          )
          # Depthwise 3*3 conv
          (1): Conv2dNormActivation(
              # 채널마다 독립적으로 3*3 convolution (groups=96)
            (0): Conv2d(96, 96, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), groups=96, bias=False)
            (1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
            (2): SiLU(inplace=True)
          )
          # Squeeze and Excitation -&gt; SE 모듈
          (2): SqueezeExcitation(
              # Squeeze
            (avgpool): AdaptiveAvgPool2d(output_size=1)
            # Excitation (중요 채널 학습)
            (fc1): Conv2d(96, 4, kernel_size=(1, 1), stride=(1, 1))
            (fc2): Conv2d(4, 96, kernel_size=(1, 1), stride=(1, 1))
            (activation): SiLU(inplace=True)
            (scale_activation): Sigmoid() # 각 채널 사이 가중치 0~1 사이 조정 -&gt; 즉, 채널별 가중치 부여
          )
          # 채널 축소
          (3): Conv2dNormActivation(
            (0): Conv2d(96, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          )
        )
        # stochastic_depth : residual 블록을 학습 중에 확률적으로 drop하는 방식
        # mode값이 row면 샘플 단위, batch면 배치 전체 단위로 블록 drop
        (stochastic_depth): StochasticDepth(p=0.0125, mode=row)
      )</code></pre>
<blockquote>
<p>SiLU : 입력 x에 sigmoid를 곱한 형태 ($x⋅sigmoid(x)$)
→ 항상 연속적이고, 부드러우며 음수도 일부 통과시키는 형태로, EfficientNet에서 SiLU를 통해 ReLU 대비 더 높은 정확도를 얻었으며, NAS 구조 탐색 결과도 성능 좋은 조합으로 SiLU 선택
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/932d02eb-a5cb-4504-b70d-74332d121955/image.png" alt=""></p>
</blockquote>
<blockquote>
<ul>
<li>논문 : <a href="https://arxiv.org/pdf/1905.11946">https://arxiv.org/pdf/1905.11946</a></li>
</ul>
</blockquote>
<ul>
<li>NAS 관련 Survey : <a href="https://arxiv.org/pdf/1808.05377">https://arxiv.org/pdf/1808.05377</a></li>
</ul>
<blockquote>
<p>간단한 정리 )</p>
</blockquote>
<ul>
<li>MobileNet → 너비, 해상도를 사용자가 따로 조정함.</li>
<li>EfficientNet → 너비, 깊이, 해상도를 공식 기반으로 균형 확장함. (셋 다 조절)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MobileNet]]></title>
            <link>https://velog.io/@inhwaaa_v/MobileNet</link>
            <guid>https://velog.io/@inhwaaa_v/MobileNet</guid>
            <pubDate>Tue, 14 Oct 2025 09:14:21 GMT</pubDate>
            <description><![CDATA[<h2 id="mobilenet">MobileNet</h2>
<ul>
<li>기존의 작고 효율적인 신경망 구축 방법 : 사전 학습된 네트워크 압축 or distillation (큰 네트워크가 작은 네트워크를 가르치는 방식)<ul>
<li>후자의 방법은 MobileNet 접근과 상호보완적임.</li>
</ul>
</li>
<li>MobileNet : 모델 개발자가 직접 작은 네트워크 선택할 수 있도록 해주는 아키텍처 구조로, latency 최적화에 중점을 두지만, 모델 크기도 작게 유지됨.</li>
<li>depthwise separable convolution을 기반으로 한 간결한 아키텍처를 사용해 경량 DNN 구성</li>
<li>latency, accuracy 간 효율적 균형을 위해 두 가지 전역 하이퍼파라미터 도입 (width multiplier, resolution multiplier)</li>
<li>모바일 및 임베디드 비전 애플리케이션 설계 요구 사항에 맞춰 쉽게 조정 가능한 모델</li>
</ul>
<br>

<h3 id="depthwise-separable-convolution">Depthwise Separable Convolution</h3>
<ul>
<li>일반적인 합성곱을 depthwise convolution과 1*1 pointwise convolution으로 나눈 것</li>
<li>MobileNet에선 depthwise convolution은 각 입력 채널마다 하나의 필터만 적용하고, 그 뒤에 pointwise convolution이 1*1 합성곱을 통해 depthwise convolution의 출력을 결합함.
  <img src="https://velog.velcdn.com/images/inhwaaa_v/post/f9bb8bdd-b685-4671-9b64-f7c71c974b3c/image.png" alt=""></li>
</ul>
<pre><code class="language-python">    (1): InvertedResidual(
      (conv): Sequential(
        (0): Conv2dNormActivation(
            # group=32이므로 입력 채널 1개마다 별도 필터를 써 채널별로 독립적으로 convolution (Depthwise convolution)
          (0): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=32, bias=False)
          (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          # ReLU6 -&gt; 출력을 6으로 클리핑
          (2): ReLU6(inplace=True)
        )
        # pointwise convolution
        (1): Conv2d(32, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )</code></pre>
<ul>
<li><p>Standard convolutions cost : $D_K * D_K * M * N * D_F * D_F$ (이때, $D_K$는 커널 크기, $D_F$는 피쳐맵 크기, M은 입력 채널 수, N은 출력 채널 수)</p>
</li>
<li><p>MobileNet cost : $D_K * D_K * M * D_F * D_F * D_F + M * N * D_F * D_F$ (Depthwise + Pointwise)</p>
</li>
<li><p>다음과 같은 연산량 절감 효과가 있으며, Standard Convolution보다 8~9배 더 적으면서도 정확도 손실은 작음.</p>
<p>  ** 참고로, 추가적인 공간 차원의 인수 분해 방법은 depthwise convolution 자체가 계산량이 적기에 큰 절감 효과를 주지 못함.</p>
<p>  <img src="https://velog.velcdn.com/images/inhwaaa_v/post/da4d337f-c8b0-480d-8081-dafd47ab7633/image.png" alt=""></p>
</li>
</ul>
<br>    

<h3 id="network-structure-and-training">Network Structure and Training</h3>
<ul>
<li>depthwise separable convolution을 기반으로 하지만, 첫 번재 레이어는 예외적으로 일반 합성곱을 사용하며, 모든 레이어는 BatchNorm, ReLU를 따르고 마지막 FC Layer만 비선형 함수 없이 Softmax 분류기로 연결됨.</li>
<li>Downsampling은 depthwise convolution에서 stride를 통해 수행되며, 마지막에선 Average Pooling을 통해 공간 해상도를 1로 줄인 뒤 FC Layer로 연결함.</li>
<li>구조화되지 않은 희소 행렬 연산은 일정 수준 이상의 희소성 확보 전까진 밀집 행렬보다 연산이 느린 경우가 많은데, MobileNet은 대부분 연산을 dense 1*1 conv에 집중시키므로 im2col 없이 GEMM으로구현 가능함.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/11786964-f289-46f8-8aa7-e9c17a067eb2/image.png" alt=""></p>
<br>

<h3 id="width-multiplier-thinner-models">Width Multiplier: Thinner Models</h3>
<ul>
<li>특정 용도에 따라 필요한 더 작고 빠른 모델을 만들기 위해 width multiplier라 불리는 파라미터 α 도입</li>
<li><strong>너비 계수 α</strong> : 네트워크의 모든 레이어에서 채널 수를 균일하게 줄임.</li>
<li>너비 계수가 적용된 depthwise separable convolution의 계산 비용
$$
D_K * D_K * αM * D_F * DF + αM * αN * D_F * D_F
$$</li>
<li>α의 범위는 (0, 1]이며, 일반적으로 1, 0.75, 0.5, 0.25 등이 사용됨.</li>
<li>α=1일 땐 기본 MobileNet, α&lt;1일 땐 축소된 MobileNet 구조임.</li>
<li>너비 계수 사용 시 계산량과 파라미터 수가 대략 $α^2$에 비례해 줄어듦.</li>
<li>소형 모델 정의에 좋으나, 축소된 구조는 처음부터 새로 학습이 필요함. 
(ex) 어떤 레이어에 너비 계수 α를 적용하면, 입력 채널 수 M은 αM, 출력 채널 수 N은 αN으로 감소</li>
</ul>
<br>

<h3 id="resolution-multiplier-reduced-representation">Resolution Multiplier: Reduced Representation</h3>
<ul>
<li><p>해상도 계수 ρ는 입력 이미지에 적용되며, 모든 레이어의 내부 표현 크기도 동일한 비율로 줄어듦.</p>
</li>
<li><p>실제로는 입력 해상도를 설정함으로써 ρ 값을 간접적으로 결정하며, 너비 계수와 해상도 계수를 적용한 depthwise separable convolution의 연산 비용은 다음과 같음.</p>
<p>  $$
  D_K × D_K × αM × ρD_F × ρD_F + αM × αN × ρD_F × ρD_F
  $$</p>
</li>
<li><p>이때, ρ 값의 범위는 (0, 1]이고, 일반적으로 입력 해상도를 224, 192, 160, 128 중 하나로 설정해 ρ 값을 간접적으로 정하며, ρ = 1일 땐 기본 MobileNet, ρ &lt; 1인 경우 계산량이 더 줄어든 MobileNet</p>
<ul>
<li><p>해상도 계수 적용 시 연산량이 $ρ^2$ 비율로 감소함.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/c2d533c7-aa46-4bea-8e59-beab5a1e7724/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<blockquote>
<ul>
<li>논문 : <a href="https://arxiv.org/pdf/1704.04861">https://arxiv.org/pdf/1704.04861</a></li>
</ul>
</blockquote>
<blockquote>
<p>참고 )</p>
</blockquote>
<ul>
<li>너비 계수는 모델의 전체 채널 수를, 해상도 계수는 모델 입력 이미지 크기를 조절해 조절함.</li>
<li>distillation : 작은 모델이 정답 레이블이 아니라 큰 모델의 출력을 모방하도록 학습되는 방식으로, 라벨이 없는 대규모 데이터셋에서도 학습이 가능함.</li>
<li>Triplet Loss : 한 학습 샘플 = (Anchor, Positive, Negative)일 때, anchor를 기준으로 positive는 가까이, negative는 멀리 배치하는 방법
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/25a79df3-f585-4a9e-a1bf-5435e9e3627a/image.png" alt=""></li>
<li>FaceNet 논문 : <a href="https://arxiv.org/pdf/1503.03832">https://arxiv.org/pdf/1503.03832</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Xception]]></title>
            <link>https://velog.io/@inhwaaa_v/Xception</link>
            <guid>https://velog.io/@inhwaaa_v/Xception</guid>
            <pubDate>Tue, 14 Oct 2025 09:03:57 GMT</pubDate>
            <description><![CDATA[<h2 id="xception">Xception</h2>
<ul>
<li><strong>Inception 모듈</strong> : 일반적인 합성곱 연산-Depthwise Separable Convolution의 중간 단계</li>
<li><strong>Depthwise Separable Convolution</strong> : 매우 많은 수의 tower를 가진 Inception 모듈<ul>
<li>tower : Inception에서 각기 다른 필터 크기를 적용하는 하나의 병렬 경로</li>
</ul>
</li>
<li><strong>Xception</strong> : 기존의 Inception 모듈을 Depthwise Separable Convolution으로 대체함.<ul>
<li>이때, <strong>“합성곱 신경망의 Feature map 내에서 채널 간 상관관계(cross-channel correlations)와 공간적 상관관계(spatial correlations)는 완전히 분리하여 학습할 수 있다.”</strong>는 가정에 기반함.</li>
<li>기존 Inception 모듈은 1x1 → 채널 간 상관관계, 3x3, 5x5 → 공간적 상관관계 학습의 구조로 부분적으로 연산을 분리하는 구조였다면, 이를 채널 간 상관관계와 공간적 상관관계를 완전히 분리한 Depthwise Separable Convolution 구조 기반으로 바꾼 것이 Xception. ⇒ 이를 통해 Inception 모듈을 완전히 대체함.</li>
<li>가정이 Inception에서보다 더 강하므로 <strong>Extreme Inception의 약자 = Xception</strong>.</li>
<li>depthwise separable convolution 층을 잔차 연결로 쌓은 선형 구조</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/22937d8b-388a-42a2-9e20-b8e7f0ecc833/image.png" alt=""></p>
<blockquote>
<ul>
<li>Xception 모델의 구현 : <a href="https://deep-learning-study.tistory.com/548">https://deep-learning-study.tistory.com/548</a></li>
</ul>
</blockquote>
<ul>
<li>논문 : <a href="https://arxiv.org/pdf/1610.02357v3">https://arxiv.org/pdf/1610.02357v3</a></li>
</ul>
<blockquote>
<p>참고 )</p>
</blockquote>
<ul>
<li>일반 convolution이 채널과 공간을 동시에 처리한다면, Depthwise Separable Convolution은 채널과 공간을 분리해서 처리하므로 파라미터 수가 적음.</li>
<li>depth multiplier = 1, 하나의 입력 채널 당 몇 개의 depthwise 필터를 사용할 것인가?</li>
<li><blockquote>
<p>depth_multiplier는 입력 채널 하나당 몇 개의 depthwise 필터를 적용할지를 결정하는 하이퍼파라미터로, 입력 채널이 3개이고, depth_multiplier = 1이면 각 채널당 하나의 필터가 적용되어 총 3개의 출력 채널이 생성된다. 반면 depth_multiplier = 2이면 각 채널에 2개의 필터가 적용되어 총 6개의 출력 채널이 생성된다.
&nbsp;&nbsp;일반적으로 Xception에서는 depth_multiplier = 1을 사용하며, 이는 각 입력 채널마다 하나의 필터만 적용한다는 의미이다. (Depthwise Separable Convolution은 입력 채널별로 필터를 독립적으로 적용함)</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GoogLeNet]]></title>
            <link>https://velog.io/@inhwaaa_v/GoogLeNet</link>
            <guid>https://velog.io/@inhwaaa_v/GoogLeNet</guid>
            <pubDate>Tue, 14 Oct 2025 08:55:54 GMT</pubDate>
            <description><![CDATA[<h2 id="googlenet">GoogLeNet</h2>
<ul>
<li>네트워크 내부의 컴퓨팅 자원 활용을 크게 개선한 모델</li>
<li>성능 최적화를 위해 Hebbian principle와 multi-scale processing(여러 크기 필터 동시 적용)을 적용함.</li>
<li>이 당시까지의 최신 트렌드는, 레이어 수를 늘리고, 레이어 크기를 키우는 방향으로 발전했고, 이 과정에서 과적합 해결을 위해 드롭아웃도 많이 사용함.</li>
<li>Inception 모델은 모든 필터가 학습되며, Inception 레이어가 여러 번 반복되며, 22층 깊이의 네트워크가 됨.</li>
</ul>
<h3 id="motivation-and-high-level-considerations">Motivation and High Level Considerations</h3>
<p>&nbsp;DNN 성능 향상의 가장 직관적인 방법은 레이어 수와 레이어 당 유닛 수를 늘리는 것인데, 이는 네트워크 크기가 커질수록 파라미터 수가 급격히 증가하고, 네트워크 크기가 균일하게 증가할 경우 연산량이 급격히 늘어난다는 단점이 존재함.
    - 완전 연결 대신 희소 연결 구조를 사용하면, 근본적 해결 가능하고, 이는 Hebbian 이론과도 일맥상통하나, 현재의 컴퓨팅 인프라에선 비효율적이며, CPU, GPU 등도 밀집 행렬에 최적화되어서 연산량이 많더라도 밀집 행렬 곱셈이 희소 행렬 곱셈보다 빠를 수 있음.
    - 공간적 도메인에서 희소성을 어느 정도 활용 중임 (ex) 컨볼루션
    - 현재 하드웨어의 밀집 행렬 연산 이점을 살리면서도 필터 수준에서의 희소성을 일부 활용할 수 있는 중간 단계의 아키텍처가 가능한가? ⇒ 희소 행렬을 상대적으로 밀집한 submatrix로 클러스터링하면 우수한 성능을 얻을 수 있음.
    - 이러한 아이디어를 기반으로 시작해 희소 구조를 실제로 구현하되, 밀집된 기존 컴포넌트로 이를 대체한 것이 Inception 아키텍처이며(즉, 희소 연결처럼 작동하는 컨볼루션 블록들을 dense하게 만듦(여러 개 컨볼루션 결과를 합치는 형태)), 이는 local optimality를 지님.</p>
<p>** <strong>Contrast Normalization</strong> : 이미지 밝기/대비 균일하게 맞추는 것</p>
<br>

<h3 id="architectural-details">Architectural Details</h3>
<p>&nbsp;Inception의 주요 아이디어는 <strong>컨볼루션 비전 네트워크에서 최적의 local sparse structure를 어떻게 쉽게 이용 가능한 dense 컴포넌트로 근사하고 덮을 수 있을까</strong>를 찾는 데 있으며, 이는 <strong>translation invariance</strong>를 가정하기에 컨볼루션 블록들로 구성되므로 최적의 국소 구조를 찾아내 이를 공간적으로 반복해야 함.</p>
<ul>
<li>레이어별로 네트워크를 구축해 활성화 값 분석 → 비슷한 유닛끼리 묶고, 필터 뱅크를 통해 특징 추출 → 다음 레이어 구성</li>
<li>입력에 가까운 초기 레이어는 상관관계가 높은 유닛들이 대체로 이미지의 국소적인 영역을 보므로 1*1 컨볼루션으로 처리해도 충분하나(상관 있는 유닛들이 좁은 영역에 몰려있으므로), 어떤 유닛들은 더 넓은 영역에 걸쳐 활성화가 비슷할 수 있으므로 더 큰 필터가 필요함.</li>
<li>패치 정렬 문제(패치 기반 처리 방식에서 패치들이 서로 잘 안 맞는 문제)를 피하기 위해 1<em>1, 3</em>3, 5*5 필터 크기만을 사용하며, 서로 다른 크기 필터 레이어들을 조합해 각 레이어의 출력 필터 뱅크를 하나의 출력 벡터로 concatenate해 다음 단계의 입력으로 삼음. + 각 단계마다 병렬로 풀링 경로도 추가함.<ul>
<li>이러한 Inception 모듈을 층층히 쌓으면 출력 활성화 간 상관 통계는 변하고, 높은 레이어로 갈수록 더 높은 수준의 추상적 특징을 포착함.</li>
</ul>
</li>
<li>하지만, 이러한 구조는 5*5 컨볼루션의 연산량이 매우 비싸고, 풀링 계층 추가 시 출력 필터 수가 이전 레이어와 같아 단계가 진행될수록 출력 수가 폭발적으로 증가한다는 단점이 존재함.<ul>
<li>이러한 문제 해결을 위해 적절한 지점마다 차원 축소와 projection을 적용함.</li>
<li>저차원 임베딩이라도 비교적 큰 이미지 패치의 많은 정보를 담을 수 있으나, 임베딩은 정보를 압축된 형태로 표현하기 때문에, 모델링이 더 어려워진다는 단점이 있어, 희소 표현을 유지하되, 다수 신호를 한꺼번에 집계해야할 때만 압축하는 것이 목표임</li>
<li>1<em>1 컨볼루션을 통해 차원 축소 수행 후, 비용이 많이 드는 3</em>3, 5*5 컨볼루션을 적용함.</li>
<li>1*1 컨볼루션은 차원 축소 외에도 ReLU 활성화를 함께 사용해 이중 기능을 함.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/d6a0b0aa-ed6d-46b7-816b-4058565d60ad/image.png" alt=""></li>
</ul>
</li>
<li>이와 같은 모듈들을 층층히 쌓은 구조가 Inception Network이고, 중간중간 stride가 2인 max pooling 레이어를 추가해 그리드의 해상도를 절반으로 줄이기도 함.</li>
</ul>
<br>

<h3 id="googlenet-1">GoogLeNet</h3>
<ul>
<li>Inception 모듈 내부를 포함해 모든 컨볼루션 연산에는 ReLU 활성화 함수가 사용됐으며, 네트워크에서 사용하는 receptive field 크기는 224*224이고, 입력은 mean subtraction을 적용한 RGB 컬러 채널</li>
<li><strong>#</strong>3×3 reduce와 #5×5 reduce는 3<em>3과 5</em>5 컨볼루션 이전에 적용되는 1*1 차원 축소 레이어의 필터 수</li>
<li>pool proj은 풀링 이후 적용되는 1*1 프로젝션 레이어의 필터 수</li>
<li>reduction/projection 레이어도 ReLU 활성화를 사용함.
  <img src="https://velog.velcdn.com/images/inhwaaa_v/post/75a5fd77-e66b-43b7-b87f-8904486ad299/image.png" alt=""></li>
<li>초기 단계부터 판별력을 높이고, 역전파되는 Gradient를 고려하고, 추가적인 정규화 효과 제공을 위해 auxiliary classifier를 추가하고, 학습 과정에선 loss가 전체 네트워크 손실에 가중치 0.3을 곱해 합산되나, 추론 시엔 auxiliary classifier를 제거하고 사용함.</li>
<li>이러한 auxiliary classifier는 필터 크기 5<em>5, stride 3인 평균 풀링 레이어, 차원 축소를 위한 필터 수 128개의 1</em>1 컨볼루션, 유닛 수 1024개의 FCN, 70% 드롭아웃 비율을 지닌 드롭아웃 레이어, 선형 레이어+소프트맥스 손실 함수로 구성됨.
  <img src="https://velog.velcdn.com/images/inhwaaa_v/post/875d5ec1-29b5-4fd4-b7b5-92e502d724ff/image.png" alt=""></li>
</ul>
<blockquote>
<p>Polyak averaging : 모델 파라미터들의 이동 평균(지금까지 지나온 파라미터 평균)을 계산하는 방법</p>
</blockquote>
<ul>
<li>일부 구현에선 5x5 대신 3x3을 두 번 포함해 5x5 컨볼루션의 효과를 냄</li>
<li>이후 버전들에선 연산량 절감을 위해 3x3 연산을 1x3, 3x1 커널로 나누어 연산하기도 함.</li>
</ul>
<pre><code class="language-python">GoogLeNet(
  # 앞쪽 -&gt; local한 특징 추출 + 계산량 줄임
  (conv1): BasicConv2d(
    (conv): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) 
    (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (maxpool1): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)  
  (conv2): BasicConv2d(
    (conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
    (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (conv3): BasicConv2d(
    (conv): Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (maxpool2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)

  # Global한 특징 통합
  # 희소 구조를 밀집하게 근사하기 위해 브랜치를 나누고, 여러 스케일 정보를 뽑기 위해
  # 다양한 종류의 필터 사용
  (inception3a): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(192, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(192, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(96, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(96, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(192, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(16, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(192, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )

  )</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[DenseNet]]></title>
            <link>https://velog.io/@inhwaaa_v/DenseNet</link>
            <guid>https://velog.io/@inhwaaa_v/DenseNet</guid>
            <pubDate>Tue, 14 Oct 2025 08:48:20 GMT</pubDate>
            <description><![CDATA[<h2 id="densenet">DenseNet</h2>
<ul>
<li>DenseNet은  각 층을 피드포워드 방식으로 다른 모든 층과 직접 연결함.</li>
<li>일반적인 합성곱 신경망은 L개의 층이 존재할 때, N개의 연결(즉, 각 층이 그 다음 층과만 연결됨)만을 지니는 반면, DenseNet은 L(L+1)/2 개의 직접 연결을 지님. (즉, 각 층은 이전 층의 모든 Feature map을 입력으로 사용하고, 자신의 Feature map을 모든 이후 층의 입력으로 전달함)</li>
<li>다시 말해, 동일한 Feature map 크기를 갖는 모든 층을 서로 직접 연결함.</li>
<li>ResNet과 달리 feature 간 연결을 sum이 아닌 concatenate를 통해 수행하며, ResNet은 많은 층 중 일부가 학습 중 무작위로 생략될 수 있으나(unrolled RNN처럼), DenseNet은 새롭게 추가되는 정보와 보존되는 정보 명확하게 구분함.
→ DenseNet의 각 층이 출력하는 Feature map 수는 매우 적음. (대신 이전 층들의 Feature map 정보를 모두 보존해 입력에 포함)
→ 이로 인해 정보/기울기 흐름이 원활해지고 암묵적인 deep supervision 효과 / 정규화 효과를 얻을 수 있으며, 이를 통해 훈련 데이터가 적은 경우에도 과적합 줄이기 가능</li>
<li>Gradient Vanishing 완화 / feature propagation 강화 / feature reuse 촉진 / 모델 파라미터 수 감소의 효과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/0deda0a7-00dc-4cd8-a4d8-ec22ffd43548/image.png" alt=""></p>
<br>

<h3 id="resnet">ResNet</h3>
<ul>
<li>skip connection을 도입해 비선형변환을 우회하는 identity function을 추가함.<ul>
<li>$x_l = H_l(x_{l-1}) + x_{l-1}$</li>
<li>기울기가 항등 경로를 통해 뒷부분 층에서 앞부분 층까지 직접 흐를 수 있으나, $H_l$의 출력과 입력이 sum으로 결합되므로 정보 흐름 제약 가능성이 존재함.</li>
<li>(ex) $H_l(x_{l-1}) = [-1, 2, 3], x_{l-1} = [1, -2, 1] =&gt; x_l = [0, 0, 4]$ 라면, 일부 정보가 손실되는 것을 확인 가능함</li>
<li>residual connection이 Gradient 흐름은 도울 수 있으나, feature reuse에는 한계가 있음을 시사함.</li>
</ul>
</li>
</ul>
<br>

<h3 id="dense-connectivity">Dense Connectivity</h3>
<ul>
<li>정보 흐름 향상을 위해 모든 이전 층에서 현재 층까지의 직접 연결을 도입함.<ul>
<li>$x_l = H_l([x_0, x_1, ..., x_{l-1}])$</li>
<li>이때, $[x_0, x_1, ..., x_{l-1}]$은 Feature map들을 채널 차원에서 concatenation한 것이며, 실제 구현 시엔 여러 입력을 하나의 텐서로 연결해 대입함.</li>
<li>밀집 연결 방식이므로 <strong>Dense Convolutional Network (DenseNet)</strong>이라고 부름.</li>
</ul>
</li>
</ul>
<pre><code class="language-python">    def forward(self, init_features: Tensor) -&gt; Tensor:
        features = [init_features]
        for name, layer in self.items():
            new_features = layer(features) # features 입력 -&gt; 새 출력
            features.append(new_features) # 새 출력 append -&gt; 다음 레이어 전달
        return torch.cat(features, 1) # 채널 차원 dim=1에서 concat</code></pre>
<h3 id="composite-function-h_l">Composite function ($H_l$)</h3>
<ul>
<li>Batch Normalization / ReLU / 3*3 convolution 3단계 연산으로 정의함.</li>
</ul>
<br>

<h3 id="pooling">Pooling</h3>
<ul>
<li>$x_l = H_l([x_0, x_1, ..., x_{l-1}])$에서 사용된 concatenation에 사용된 연결은 feature map 크기가 같을 때만 가능하나, 합성곱 신경망에선 featuer map의 크기를 변경하는 down sampling 계층이 핵심 구성 요소이므로, 전체 네트워크를 여러 개의 dense block으로 나누고, 그 사이에 transition layer를 배치함.<ul>
<li>transition layer는 Batch Normalization, 1x1 conv, 2x2 Average Pooling으로 구성됨.</li>
<li>이를 통해 feature map의 크기를 줄이면서도 다음 dense block에 맞게 적절히 연결되도록 도움.</li>
</ul>
</li>
</ul>
<br>

<h3 id="growth-rate">Growth rate</h3>
<ul>
<li>각 $H_l$이 k개의 Feature map을 생성한다 하면, $l$번째 층은 총 $k_0 + k * (l-1)$개의 feature map을 입력으로 받으며, 이때의 k를 Growth rate라고 하고, 이는 각 층이 네트워크 전체 상태에 얼마나 많은 새로운 정보를 추가하는지를 조절하는 하이퍼파라미터임.</li>
<li>DenseNet에선 Feature map 전체를 네트워크의 global-state로 간주할 수 있고, 각 층은 이 상태에 자신의 Feature map k개만 덧붙이고, 이후의 모든 층에서 이 정보를 중복 없이 공유하므로 정보를 매 층마다 복사할 필요가 없음.</li>
</ul>
<br>

<h3 id="bottleneck-layers">Bottleneck layers</h3>
<ul>
<li>출력 k개보다 많은 수의 Feature map을 받는 경우, 3<em>3 합성곱 연산 이전에 1</em>1 합성곱을 bottleneck layer로 도입하면 입력 Feature map 수를 줄이고 계산 효율성을 높일 수 있음</li>
<li>(ex) DenseNet-B : BN → ReLU →  Conv1(1<em>1) → BN → ReLU → Conv(3</em>3)</li>
</ul>
<pre><code class="language-python">def forward(self, input: Tensor) -&gt; Tensor:  # noqa: F811
        # 단일 Tensor -&gt; list, else 그대로 사용
        if isinstance(input, Tensor):
            prev_features = [input]
        else:
            prev_features = input

        if self.memory_efficient and self.any_requires_grad(prev_features):
            if torch.jit.is_scripting():
                raise Exception(&quot;Memory Efficient not supported in JIT&quot;)
                        # 중간값 계산 안 하고 역전파 시 계산 -&gt; 메모리 효율
            bottleneck_output = self.call_checkpoint_bottleneck(prev_features)
        else:
            bottleneck_output = self.bn_function(prev_features)

        new_features = self.conv2(self.relu2(self.norm2(bottleneck_output)))
        if self.drop_rate &gt; 0:
            new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
        return new_features</code></pre>
<pre><code class="language-python"># bottleneck conv 처리
    def bn_function(self, inputs: List[Tensor]) -&gt; Tensor:
        concated_features = torch.cat(inputs, 1)
        bottleneck_output = self.conv1(self.relu1(self.norm1(concated_features)))  # noqa: T484
        return bottleneck_output</code></pre>
<br>

<h3 id="compression">Compression</h3>
<ul>
<li>모델의 Compactness를 높이기 위해 transition layer에서 feature map 수를 줄일 수 있음.</li>
<li>(ex) dense block이 m개의 feature map만 지닌다면, 다음 전이 계층은 θ⋅m개의 feature map만 출력하도록 압축 계수(θ)를 사용하며, 0 &lt; θ ≤ 1이고, θ=1이면 feature map 수는 줄어들지 않음. </li>
</ul>
<pre><code class="language-python">import torchvision.models as models

model = models.densenet121(weights=&#39;IMAGENET1K_V1&#39;)
print(model)</code></pre>
<br>

<h3 id="정리">정리</h3>
<p>&nbsp;DenseNet은 이전의 모든 레이어의 결과인 feature map을 이후의 모든 레이어에 concat해서, 중복 없이 효율적으로 학습하고, gradient vanishing 문제도 완화할 수 있으나, concat 구조 특성상 연산량이 빠르게 늘어나기 때문에, 이를 줄이기 위해 1×1 conv를 포함한 bottleneck 구조를 활용하고, transition layer를 통해 feature map 크기와 채널 수를 압축하여 전체 모델을 메모리 효율적으로 만든다는 특징이 존재함.</p>
<p>&nbsp;또한, ResNet은 sum을 통한 연결이기에 메모리 걱정이 그나마 덜하지만 정보 흐름이 제한될 수 있음. 즉, 일정 부분의 정보들이 무시될 수 있는데 이에 반해 DenseNet은 이전 레이어의 모든 출력을 활용하기에 feature reuse가 뛰어남. (+ 높은 파라미터 효율성 → 과적합 억제)</p>
<blockquote>
<p>논문 : <a href="https://arxiv.org/pdf/1608.06993">https://arxiv.org/pdf/1608.06993</a></p>
</blockquote>
<blockquote>
<p>참고 )
<strong>deep supervision</strong> : 중간 층마다 auxiliary classifier 또는 보조 손실 함수를 달아서, 그 층에서도 직접 학습이 일어나게 만드는 방식으로, DenseNet은 별도로 중간에 loss를 두진 않으나, 모든 층이 이전 층의 Feature map을 직접 입력으로 받아 출력층 loss가 거의 모든 층에 직접 영향을 주기에 Implicit Deep Supervision라고 이야기함.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAG]]></title>
            <link>https://velog.io/@inhwaaa_v/RAG</link>
            <guid>https://velog.io/@inhwaaa_v/RAG</guid>
            <pubDate>Tue, 07 Oct 2025 06:44:11 GMT</pubDate>
            <description><![CDATA[<h1 id="rag-시스템">RAG 시스템</h1>
<p>&nbsp;RAG는 각 질의에 대해 적절한 컨택스트를 구성하는 방법에 관한 것으로, 모델이 외부 데이터 소스에서 관련 정보를 검색하여 모델의 생성을 향상시키는 기술이다.</p>
<h2 id="청킹-chunking">청킹 (Chunking)</h2>
<p>&nbsp;RAG 시스템은 외부 메모리 소스에서 정보를 검색하는 검색기와 검색된 정보를 기반으로 응답을 생성하는 생성 모델로 구성된다.</p>
<p>&nbsp;외부 메모리 소스의 문서는 10개의 토큰이나 100만 개의 토큰일 수 있으므로 문서 전체를 그냥 검색하면 컨택스트가 지나치게 길어질 수 있다. 이를 방지하기 위해 각 문서를 더 관리하기 쉬운 청크(Chunk)로 분리하며, 이를 <strong>청킹 (Chunking</strong>)이라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/6493ebaf-29ec-4dfd-9807-1b5e1644516e/image.png" alt=""></p>
<h3 id="청킹-전략">청킹 전략</h3>
<p>&nbsp;청킹의 가장 단순한 전략은 특정 단위를 기준으로 문서를 동일한 길이의 청크로 나누는 것이다. 주로 문자나 단어, 문장, 단락 같은 단위를 사용한다.</p>
<ul>
<li>(ex) 각 문서를 2,048자 또는 512단어 청크로 나눔. / 각 청크가 고정된 수의 문장(예를 들어 20문장)을 포함하게 함. / 각 단락을 하나의 청크로 삼음 등의 방식으로 문서를 분할할 수 있다.</li>
</ul>
<p>&nbsp;또한, 각 청크가 최대 청크 크기 안에 들어올 때까지 점점 작은 단위를 사용해 문서를 재귀적으로 분할할 수도 있다.</p>
<ul>
<li>(ex) 문서를 먼저 여러 절로 나누고, 절이 너무 길면 단락으로, 단락이 길면 문장으로 나누는 방식</li>
</ul>
<p>&nbsp;그리고 생성 모델의 토크나이저가 나누는 토큰을 기준으로 문서를 청킹하는 방법도 있다. 이는 token limit을 효율적으로 관리하므로 모델과의 작업이 더 수월해진다는 장점이 있으나, 다른 토크나이저를 사용하는 생성 모델로 바꾸면 데이터를 처음부터 다시 색인화해야 한다는 단점이 있다.</p>
<p>&nbsp;하지만, 이러한 청킹 전략에 정답은 없다. 너무 작은 청크는 문맥이 부족할 수 있고, 너무 큰 청크는 검색 정확도가 떨어질 수 있기 때문이다. 따라서 실험을 통해 적절한 청크 크기와 방식을 찾는 것이 중요하다.</p>
<blockquote>
<p>&nbsp;<strong>청크 간 겹침 (Chunk Overlap)</strong>이란, 문서를 청크로 나누는 과정에서 문맥이 손실되는 문제를 해결하기 위한 방식으로, 인접한 청크 사이에 일부 내용을 겹치게 만들어 문맥을 보존하는 방법을 의미한다. 다시 말해, 청크를 나눌 때 중복 구간을 유지하게 하는 것을 의미한다.</p>
</blockquote>
<p>&nbsp;예를 들어, “트랜스포머 모델은 어텐션 메커니즘을 기반으로 하며, RNN의 순차적 한계를 극복하기 위해 제안되었다. 특히, 셀프 어텐션 구조는 문맥 정보를 효과적으로 통합한다.”라는 문장을 청크화하여  트랜스포머 모델은 어텐션 메커니즘을 기반으로 하며(청크1) / RNN의 순차적 한계를 극복하기 위해 제안되었다.(청크2) / 특히, 셀프 어텐션 구조는 문맥 정보를 효과적으로 통합한다. (청크3)로 나누었을 때, 청크1이 없는 채로 청크 2를 보면 청크2에서 제안된 것이 무엇인지 알 수 없다. 따라서 이를 해결하기 위해 청크를 나눌 때 중복 구간을 유지하여 청크 1의 마지막 부분이 청크 2의 시작 부분에 같이 들어가도록 하여 두 청크가 연결된 문맥을 일부 공유할 수 있도록 한다.</p>
<h2 id="임베딩-모델">임베딩 모델</h2>
<p>&nbsp;임베딩이란, 원본 데이터의 의미적 특성을 보존한 채 텍스트(단어, 문장, 문서 등)를 숫자 벡터로 표현하는 과정이다.</p>
<p>&nbsp;임베딩 모델은 이러한 임베딩 과정을 수행하기 위한 별도의 모델로, 텍스트 데이터를 고차원 벡터 공간에 매핑하기 위한 알고리즘을 의미한다. 주로 단어 또는 문장 같은 언어 단위를 수치적으로 표현하는 데 사용되며, RAG 시스템에선 임베딩 모델이 제대로 작동하지 않으면 임베딩 기반 검색도 제대로 작동하지 않으므로 중요하게 여겨진다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/feb2228b-e167-4ecd-a3b4-3cbb36fcbda1/image.png" alt=""></p>
<p>&nbsp;임베딩 모델은 크게 단어 임베딩과 텍스트 임베딩으로 나눌 수 있으며, 단어 임베딩은 특정 단어를 고정된 차원의 벡터로 변환하여 단어 간 의미적 유사성을 반영하는 방법(Word2Vec, GloVe 등)을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/45c3eb65-e7d3-4ea6-84a9-735a58cf8916/image.png" alt=""></p>
<p>&nbsp;텍스트 임베딩은 문장이나 문단과 같은 더 큰 텍스트를 벡터로 변환하며, BERT나 Sentence-BERT와 같은 모델을 통해 전체 문맥을 고려하여 문장 간 의미적 관계를 포착하는 데 중점을 둔다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/65278a5d-cfbf-4fb3-adef-205fddca94e3/image.png" alt=""></p>
<h2 id="검색-시스템">검색 시스템</h2>
<p>&nbsp;검색은 주어진 질의에 대한 문서들의 관련성을 기준으로 순위를 매기는 방식으로 작동하며, 검색 알고리즘은 관련성 점수를 계산하는 방법에 따라 달라진다.</p>
<h3 id="용어-기반-검색-어휘적-검색-lexical-retrieval">용어 기반 검색 (어휘적 검색, lexical retrieval)</h3>
<ul>
<li>질의와 관련된 문서를 찾기 위해 키워드를 사용하는 방법</li>
<li>자주 사용되는 용어 기반 솔루션에는 엘라스틱서치와 BM25가 있다.</li>
</ul>
<h4 id="엘라스틱-서치">엘라스틱 서치</h4>
<ul>
<li>엘라스틱 서치는 역색인(Inverted index)이라는 데이터 구조를 활용하며, 이는 용어를 문서로 매핑하는 사전이다.</li>
<li>이러한 역색인은 텍스트를 단어(토큰) 단위로 나누어 각 단어가 어떤 문서에 나타나는지 기록하는 구조이다. (문서에 번호가 지정된 것이 아닌 단어마다 어느 문서에 들어가 있는지 기록하므로 역색인이라고 한다)</li>
<li>이러한 정보는 TF-IDF 점수 계산 시 유용하게 쓰인다.
<img src="https://velog.velcdn.com/images/inhwaaa_v/post/abc99b29-04a2-441b-90c8-f728fa5ccf5c/image.png" alt=""></li>
</ul>
<blockquote>
<h4 id="참고--tf-idf-term-frequency-inverse-document-frequency">참고 : TF-IDF (Term Frequency-Inverse Document Frequency)</h4>
<p>&nbsp;TF-IDF란, 단어마다 중요도를 고려하여 가중치를 주는 통계적인 단어 표현 방법이다.
&nbsp;&nbsp;TF-IDF는 문맥이나 단어 간의 의미를 고려하지 않지만, 단어의 빈도와 희소성을 기반으로 문서의 중요성을 평가하는 데 유용하게 사용된다.
&nbsp;&nbsp;TF-IDF의 한계(문맥이나 단어 간 의미를 고려하지 않음)를 극복하기 위해 단어 간 관계를 학습하는 Word Embedding 기법이나 BERT를 사용할 수 있다.</p>
</blockquote>
<hr>
>
- TF-IDF는 문서 내에서 많이 나온 단어의 중요도는 높게, 많은 문서에서 등장하는 조사 등의 단어의 중요도는 낮게 평가한다.
- 즉, 한 문장에서 자주 사용되는 단어는 중요하다 판단하지만, 다른 문장에서도 흔하게 사용하면 중요하지 않다고 단어를 판단하는 알고리즘이다.
- TF-IDF 가중치 계산 방법 : TF-IDF=TF∗IDF
- 단어 출현빈도(Term Frequency, TF) : **특정 문서**에서 특정 단어의 출현 빈도
- TF(t,d) = 해당 단어 t의 등장 횟수 / 문서 d의 총 단어 수
- 역 문서 빈도(Inverse Document Frequency, IDF) : 특정 단어가 **전체 문서** 집합에서 얼마나 희귀한지를 나타내는 것
- IDF(t) = log(전체 문서 수 / 문서 수 d에서 t가 등장하는 횟수)
#### **코드 예시**
```
from collections import Counter
import math
>
# Step 1 : Sample documents
# Each document is a string, representing some text content.
# 샘플 문서 생성
documents = [
    "text mining algorithms analyze text",
    "text data mining finds patterns in text",
    "patterns and algorithms are used in text mining",
]
>
# TF(t,d) = 해당 단어 t의 등장 횟수 / 문서 d의 총 단어 수
# IDF(t) = log(N / DF) (N : 총 문서 수, DF : 단어가 등장한 문서 수)
>
# Step 2 : Preprocess documents and calculate term frequencies (TF)
# For each document, split it into words and count occurrences using Counter.
# 각 문서를 단어로 분리하고, Counter를 통해 각 단어 출현 빈도 계산 (t)
tf = [Counter(doc.split()) for doc in documents]
>
# Step 3 : Calculate document frequency (DF) for each term
# DF counts the number of documents containing each term.
df = Counter() # Counter 객체 df 생성
for doc_tf in tf:
  df.update(doc_tf.keys()) # 전체 문서에서의 key 빈도(DF) 계산
>
# Step 4 : Calculate TF-IDF for each term in each document
# Use the formula TF-IDF = TF * log(N/DF), where N is the total number of documents.
N = len(documents) # N = 총 문서 수
tf_idf = [] # TF-IDF 점수를 저장할 리스트
>
# 각 문서에 대해 반복
for doc_index, doc_tf in enumerate(tf):
  doc_tfidf = {} # 현재 문서의 TF-IDF 점수를 저장할 딕셔너리
  for term, count in doc_tf.items(): # 현재 문서의 각 단어에 대해 반복
    idf = math.log(N / df[term]) # Inverse Document Frequency (IDF) / IDF 계산
    doc_tfidf[term] = count * idf # 현재 단어의 TF-IDF 점수 저장
  tf_idf.append(doc_tfidf) # 현재 문서의 TF-IDF 점수(딕셔너리 형태)를 리스트에 추가
>
# Step 5 : Display TF-IDF scores
# Print TF-IDF scores for each documents.
# enumerate( ) 함수 : 리스트의 값을 추출할 때 인덱스를 붙여 함께 출력하는 방법
# for index, value in enumerate(iterable 객체)
for doc_index, scores in enumerate(tf_idf):
  print(f"Document {doc_index + 1} : ") # index는 0부터 시작하니까 index + 1
  for term, score in scores.items(): # tf_idf는 딕셔너리 형태의 정보가 리스트로 저장되어 있으므로 items() 사용
    print(f"  {term}:{score:.2f}")
```

<h4 id="bm25">BM25</h4>
<ul>
<li>BM25는 TF-IDF를 개선한 방식으로, TF-IDF와 달리 문서 길이를 고려해 용어 빈도 점수를 정규화한다. 긴 문서일 수록 특정 용어를 포함할 가능성이 높고, 용어 빈도 값도 자연히 높아지기 때문이다.</li>
<li>BM25는 다음과 같은 기준을 통해 문서와 질의 간 관련성을 평가한다.<ul>
<li>① 단어가 질의에 포함되어 있는가?</li>
<li>② 그 단어가 문서 안에서 얼마나 자주 등장하는가?</li>
<li>③ 문서가 너무 길지는 않은가?</li>
<li>④ 단어가 전체적으로 희귀한가?</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/b683b931-939b-4f66-9848-239022dbb61e/image.png" alt=""></p>
<blockquote>
<p>$k_i$ : TF(단어 빈도)에 대한 민감도
$b$ : 문서 길이에 대한 보정 정도</p>
</blockquote>
<h3 id="임베딩-기반-검색-의미-기반-검색-semantic-retrieval">임베딩 기반 검색 (의미 기반 검색, semantic retrieval)</h3>
<p>&nbsp;용어 기반 검색은 의미가 아닌 단어의 형태만 가지고 관련성을 계산한다. 하지만, 텍스트의 겉모습이 그 의미를 제대로 반영하지 못하는 경우도 있다.
(ex) &#39;트랜스포머 아키텍처&#39; 검색 -&gt; 결과 : 영화 &lt;트랜스포머&gt; 문서 반환</p>
<p>&nbsp;이와 달리, 임베딩 기반 검색기는 문서의 의미가 질의와 얼마나 가까운지를 기준으로 순위를 매기며, 색인화 과정에서 원본 데이터 청크를 임베딩으로 변환하는 과정까지 포함한다. 이렇게 생성된 임베딩을 저장하고 유사도 검색을 수행하는 데이터베이스를 <strong>Vector Database</strong>라고 한다.</p>
<p>&nbsp;이러한 임베딩 데이터는 VectorDB에 저장되어, 아래와 같은 절차를 통해 RAG(Retrieval-Augmented Generation) 구조로 활용될 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/inhwaaa_v/post/232ab327-0377-4eb3-857a-0291134e6091/image.png" alt=""></p>
<p>&nbsp;이때, 단순히 벡터를 저장하는 것은 쉬운 일이지만, 질의가 임베딩으로 변환되면 벡터 데이터베이스는 이 질의 벡터와 유사한 벡터들을 데이터베이스에서 찾아내는 벡터 검색을 수행해야 하는데, 이는 쉽지 않은 과정이다. 따라서 벡터들은 빠르고 효율적 검색이 가능한 방식으로 색인화되고 저장되어야 한다.</p>
<p>&nbsp;벡터 검색은 보통 최근접 이웃 문제로 접근하여 주어진 질의에 대해 k개의 가장 가까운 벡터를 찾는 방식으로 수행되며, 가장 기본적인 방법은 k-최근접 이웃 알고리즘이다.</p>
<blockquote>
<p>K-NN 알고리즘은 다음과 같이 작동한다.
&nbsp;&nbsp;&nbsp;1. 질의 임베딩과 데이터베이스의 모든 벡터 간 유사도 점수를 코사인 유사도와 같은 지표를 사용해 계산한다.
&nbsp;&nbsp;&nbsp;2. 모든 벡터를 유사도 점수에 따라 순위를 매긴다
&nbsp;&nbsp;&nbsp;3. 높은 유사도 점수를 가진 상위 k개 벡터를 반환한다.</p>
</blockquote>
<p>&nbsp;하지만, 이는 계산이 많이 필요하고 느리다. 따라서 작은 데이터셋에서는 K-NN을, 큰 데이터셋에서는 근사 최근접 알고리즘(ANN)으로 벡터 검색을 수행하며, 대표적인 벡터 검색 알고리즘에는 FAISS, ScaNN, Annoy, Hnswlib 등이 있다.</p>
<blockquote>
<p><strong>벡터 데이터베이스 선택 기준 가이드</strong>
<a href="https://discuss.pytorch.kr/t/2023-picking-a-vector-database-a-comparison-and-guide-for-2023/2625">https://discuss.pytorch.kr/t/2023-picking-a-vector-database-a-comparison-and-guide-for-2023/2625</a></p>
</blockquote>
<h3 id="검색기의-품질">검색기의 품질</h3>
<p>&nbsp;검색기의 품질은 검색되는 데이터의 품질로 평가할 수 있으며, RAG 평가 프레임워크에서 주로 사용되는 두 가지 지표는 컨택스트 정밀도(Context Precision)와 컨텍스트 재현율(Context Recall)이다.</p>
<ul>
<li><strong>컨택스트 정밀도 (Context Precision)</strong> : 검색된 모든 문서 중에서 실제로 질의와 관련된 문서의 비율은 얼마인가?</li>
<li><strong>컨텍스트 재현율 (Context Recall)</strong> : 질의와 관련된 모든 문서 중에서 실제로 검색된 문서의 비율은 얼마인가?</li>
</ul>
<p>&nbsp;이러한 지표를 계산하기 위해 테스트용 질의 목록과 각 질의에 해당하는 문서 집합으로 평가 세트를 구성하며, 각 테스트 질의에 대해 테스트 문서가 관련이 있는지 없는지 주석을 단다. (이 주석은 사람이나 AI 평가자가 달 수 있다) 그런 다음, 이 평가 세트에서 검색기의 정밀도와 재현율 점수를 계산한다.</p>
<blockquote>
<p>&nbsp;RAG 파이프라인은 개별적으로, 그리고 조합하여 평가해야 하는 여러 구성 요소로 구성되므로 일련의 평가 지표가 필요한데, 고품질 검증 데이터셋을 인간이 직접 생성하는 것은 어렵고 시간이 많이 걸리며 비용도 많이 든다.
&nbsp;&nbsp;그래서 이러한 RAG 시스템의 평가를 위해 RAGAs를 활용할 수도 있다. 이는 RAG 시스템 평가를 위한 오픈 소스 프레임워크이다.
<a href="https://jihan819.tistory.com/entry/AI-RAGAS-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9Cdocs-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0">https://jihan819.tistory.com/entry/AI-RAGAS-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9Cdocs-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</a>
<a href="https://medium.com/data-science/evaluating-rag-applications-with-ragas-81d67b0ee31a">https://medium.com/data-science/evaluating-rag-applications-with-ragas-81d67b0ee31a</a>
&nbsp;&nbsp;아래의 글은 RAG 평가 방법에 관한 글이다.
<a href="https://velog.io/@cathx618/RAG-%ED%8F%89%EA%B0%80-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC">https://velog.io/@cathx618/RAG-%ED%8F%89%EA%B0%80-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC</a></p>
</blockquote>
<p>&nbsp;만약 더 관련성 높은 문서가 상위에 나와야 해서 검색된 문서의 순위가 중요할 때는, 정규화된 할인 누적 이득 (NDCG), 평균 정밀도 (MAP), 평균 역순위(MRR)과 같은 지표들을 활용할 수도 있다.</p>
<h3 id="검색-알고리즘-결합하기">검색 알고리즘 결합하기</h3>
<p>&nbsp;검색 알고리즘마다 뚜렷한 장점을 지니기에 운영 환경의 검색 시스템은 보통 여러 접근 방법을 함께 결합해 사용하며, 용어 기반 검색과 임베딩 기반 검색을 결합하는 것을 <strong>하이브리드 검색(Hybrid Search)</strong>이라고 한다.</p>
<p>&nbsp;서로 다른 알고리즘은 순차적으로 사용될 수 있으므로 용어 기반 시스템처럼 비용이 적게 들지만 정확도가 낮은 검색기로 후보군을 추려내고, 그 다음 k-최근접 이웃과 같이 더 정확하지만 비용이 더 많이 드는 방식으로 이 후보 중에서 최상의 결과를 찾는다. 이 두 번째 단계를 재순위화(Re-ranking)이라고 한다.</p>
<blockquote>
<p>&nbsp;앞에서도 말했듯 용어 기반 검색의 단점은 &#39;트랜스포머 아키텍처&#39; 검색 -&gt; 결과 : 영화 &lt;트랜스포머&gt; 문서 반환과 같이 의미를 반영하지 않고 문서를 가져올 수도 있다는 것이다. 따라서 용어 기반 시스템으로 관련성 있는 후보군들을 추려내고, 여기서 의미적 중요도를 기반으로 한 번 더 걸러서 최상의 결과를 찾겠다는 것이다.</p>
</blockquote>
<p>&nbsp;또한, 서로 다른 알고리즘을 앙상블 기법으로 결합할 수도 있으며, 이는 여러 검색기를 동시에 사용해 후보를 가져온 다음, 이 다양한 순위들을 하나로 결합해 최종 순위를 생성하는 방법이다. 이렇게 서로 다른 순위를 결합하는 알고리즘을 <strong>역퓨전(RRF)</strong>이라고 한다. (각 검색기의 점수 체계가 달라도 순위만 있으면 결합 가능하며, 두 검색기가 모두 높은 순위를 매긴 문서가 최종적으로 상위에 오르게 된다.)</p>
<p>$$
Score(D) = \sum_{i=1}^{n} \frac {1}{k+r_i(D)}
$$</p>
<ul>
<li>$n$ = 순위 목록의 수, 각 검색기에 의해 생성</li>
<li>$r_i(D)$ : 검색기 i에 의한 문서의 순위</li>
<li>$k$ : 0으로 나누는 것을 방지하고 낮은 순위 문서의 영향력을 제어하기 위한 상수로, 일반적으로 60을 사용함.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>