본문 바로가기

공부 일지 #37 | 머신러닝: 차원 축소와 추천 알고리즘

@studying:)2025. 9. 14. 15:55

학습날짜: 2025.09.11


1. 차원 축소(Dimension Reduction)

  • 차원의 저주
    • 고차원 데이터 다룰 때 발생하는 현상임
    • 동일한 데이터 양에서도 과적합 잘 생기고, 일반화 어려움
    • 데이터 밀도 유지하려면 필요한 데이터 양이 기하급수적으로 늘어남
  • 차원의 증가가 과적합에 미치는 영향
    • 노이즈를 신호로 잘못 학습함
    • 다중공선성 발생
    • 데이터 희소화, 편중
  • Occam’s Razor (오컴의 면도날)
    • "불필요한 가정 세우지 말고, 최소한의 변수로 설명 가능한 모델이 바람직함"
  • Feature Selection vs Extraction
    • Feature Selection: 중요한 변수만 선택 (Forward/Backward Selection)
    • Feature Extraction: 변수를 변환하거나 규제해 단순화 (PCA가 대표적, Ridge·Lasso·Elastic Net도 변수 영향력을 줄여 과적합 완화에 사용됨)

1.1. SFS (Sequential Forward Selection) 

  • Forward & Backward Selection 의 한 방법.
  • 실무에서는 잘 쓰지 않음, 확인 용도로만 쓰임.
  • 단계: 
    1. 가장 좋은 feature 선택
    2. 다른 feature 추가 → pair 검증
    3. triplet 생성 후 검증
    4. 반복해서 feature set 완성
# SFS 실습; iris 데이터 사용

!pip install mlxtend --upgrade # SFS 구현을 위해 사용

from sklearn.neighbors import KNeighborsClassifier
from mlxtend.feature_selection import SequentialFeatureSelector as SFS

# 분류 모델 생성 (KNN, k=5 사용)
knn = KNeighborsClassifier(n_neighbors=5)

# Sequential Feature Selector 설정
sfs1 = SFS(knn,
           k_features=3, # 현재 특징 4개인데, 3개로 만들고 싶다.
           forward=True, # True면 전진, False면 후진
           floating=False, # 전진이면 무조건 이거 넣어야 함. 후진이면 삭제
           verbose=2, # log level
           scoring='accuracy', # 이것밖에 없음
           cv = 0
           )
           
# 모델 학습 (특징 선택 수행)           
sfs1 = sfs1.fit(X, y)
'''
[Parallel(n_jobs=1)]: Done   4 out of   4 | elapsed:    0.0s finished
[2025-09-11 00:33:13] Features: 1/3 -- score: 0.96[Parallel(n_jobs=1)]: Done   3 out of   3 | elapsed:    0.0s finished
 → 첫 번째 feature 선택 후 정확도
[2025-09-11 00:33:13] Features: 2/3 -- score: 0.9666666666666667[Parallel(n_jobs=1)]: Done   2 out of   2 | elapsed:    0.0s finished
 → 두 번째 feature 추가 후 정확도
[2025-09-11 00:33:13] Features: 3/3 -- score: 0.9733333333333334
 → 세 번째 feature 추가 후 정확도
'''

# 어떤 feature가 들어가있길래 정확도가 향상될까??
sfs1.subsets_
'''
{1: {'feature_idx': (3,),  → 첫 번째 선택된 feature는 인덱스 3번(=petal width)
  'cv_scores': array([0.96]),
  'avg_score': np.float64(0.96),
  'feature_names': ('3',)},
 2: {'feature_idx': (0, 3), → 0번(sepal length)과 3번(petal width)
  'cv_scores': array([0.96666667]),
  'avg_score': np.float64(0.9666666666666667),
  'feature_names': ('0', '3')},
 3: {'feature_idx': (0, 2, 3), → 최종 선택된 3개 feature는 sepal length, petal length, petal width
  'cv_scores': array([0.97333333]),
  'avg_score': np.float64(0.9733333333333334),
  'feature_names': ('0', '2', '3')}}
'''
  
sfs1.k_score_ # np.float64(0.9733333333333334)

'''
Note:
mlxtend는 버전 업데이트가 잘 안 됨.
sklearn에도 있으니 그걸 사용해보자!
'''

# sklearn, loading과정이 자세히 보이진 않지만, 심플하게 사용 가능
from sklearn.feature_selection import SequentialFeatureSelector

# foward 선택
sfs_foward = SequentialFeatureSelector(knn, n_features_to_select=2, direction='forward')
sfs_foward.fit(X,y)

sfs_foward.get_support()
'''
array([False, False,  True,  True])
즉, petal length(2), petal width(3)가 선택됨
 '''

# backward 선택
sfs_backward = SequentialFeatureSelector(knn, n_features_to_select=2, direction='backward')
sfs_backward.fit(X,y)

sfs_backward.get_support()
array([False, False,  True,  True])
전진 선택과 동일하게 petal length(2), petal width(3)가 선택됨
 '''

1.2. PCA (주성분 분석)

  • 대표적 차원 축소 기법
  • 공분산 큰 방향(정보량 많은 방향)으로 축 설정
  • 데이터 정사영하여 차원 축소
  • 첫 번째 주성분 → 이후 축은 직교(orthogonal)하게 결정
  • 차원 과도 축소 시 정보 손실 발생
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# iris 데이터 준비; 스케일링
X = iris['data']
iris_z_score = StandardScaler().fit_transform(X)

# 주성분 분석 : 4차원 → 2차원으로 축소
pca = PCA(n_components=2)
pca.fit(iris_z_score)

# 주성분 찾기 : 고유벡터(축)
print(pca.components_.shape)
print(pca.components_)
'''
(2, 4)
[[ 0.52106591 -0.26934744  0.5804131   0.56485654]
 [ 0.37741762  0.92329566  0.02449161  0.06694199]]
 '''
 
# projection (정사영: 원 데이터를 새 축으로 변환)
X_pca = pca.transform(iris_z_score)

X_pca
'''
array([[-2.26470281,  0.4800266 ],
       [-2.08096115, -0.67413356],
		...
       [ 1.37278779,  1.01125442],
       [ 0.96065603, -0.02433167]])
⇒ 원래 4차원 데이터가 2차원 좌표(pca_com1, pca_com2)로 변환됨
⇒ 어떤 feature가 어떤 의미인지 직접 해석 불가 (PCA의 단점)       
'''

# 각 주성분이 분산을 얼마나 잘 설명하는지?
np.sum(pca.explained_variance_ratio_)
'''
np.float64(0.9581320720000166)
⇒ 원래 정보의 약 95.8% 보존됨, 나머지 약 4.2% 손실
'''

# PCA 결과 DataFrame 정리
pca_cols = ['pca_com1', 'pca_com2']
pca_df = pd.DataFrame(data=X_pca, columns=pca_cols)
pca_df['label'] = iris['target']
pca_df.head(2)
'''
pca_com1	pca_com2	label
0	-2.264703	0.480027	0
1	-2.080961	-0.674134	0
'''

# 시각화(원본 feature vs PCA 변환 결과 비교)
fig, ax = plt.subplots(ncols = 2)

# 왼쪽: 원본 feature (sepal length, sepal width)
sns.scatterplot(data = iris_df,
                x = 'sepal length (cm)',
                y = 'sepal width (cm)',
                hue = 'label',
                ax = ax[0])
                
# 오른쪽: PCA 결과 (주성분 1, 주성분 2)                
sns.scatterplot(data=pca_df,
                x='pca_com1',
                y='pca_com2',
                hue = 'label',
                ax = ax[1])
plt.show()


2. Association Rule

  • 개념
    • 추천 시스템, 장바구니 분석 등에서 활용
    • 추천 알고리즘의 하나
    • 규칙 형태: if A → B (A=선행, B=후행)
  • 주요 용어
    • Support (지지도): X와 Y가 함께 발생할 확률 → 빈발성 척도
    • Confidence (신뢰도): X 발생 시 Y도 발생할 조건부 확률 → 효율성, 유효성 척도
    • Lift (향상도): X와 Y가 독립적인지 여부 판단
      • Lift = 1 → 독립적
      • Lift > 1 → 양의 상관관계
      • Lift < 1 → 음의 상관관계
  • 예시
    • Support(X=식빵·버터) = 3/10
    • Support(Y=우유) = 4/10
    • Support(X→Y) = 3/10
    • Confidence = 1.0
    • Lift = 2.5 → 양의 상관관계

2.1.알고리즘

2.1.1. Apriori 알고리즘

  • 전제: “빈번한 아이템셋의 부분집합도 빈번하다”
  • 단계적 후보 생성 → 지지도 낮은 규칙 제거
# Apriori 알고리즘 실습 (장바구니 분석)

# 1. 데이터 준비; 각 행(row)은 하나의 거래(transaction), 각 원소는 구매된 품목
data = np.array([
  ['식빵', '버터', '우유'],
  ['바게트빵', '소금', '올리브유'],
  ['식빵', '버터', '우유', '잼'],
  ['치즈케익', '바게트빵', '식빵'],
  ['식빵', '잼', '샐러드유'],
  ['치즈케익', '잼', '치즈'],
  ['치즈', '우유'],
  ['식빵'],
  ['식빵', '버터', '우유'],
  ['바게트빵']
], dtype=object)

# 2. 데이터를 희소행렬로 변환
from mlxtend.preprocessing import TransactionEncoder

trans_enc = TransactionEncoder()
trans_arr = trans_enc.fit_transform(data) # True/False 형태의 sparse matrix로 변환

df = pd.DataFrame(trans_arr, columns=trans_enc.columns_)
df.head(2)
'''
바게트빵	버터	샐러드유	소금	식빵	올리브유	우유	잼	치즈	치즈케익
0	False	True	False	False	True	False	True	False	False	False
1	True	False	False	True	False	True	False	False	False	False
'''

# 3. Apriori 알고리즘 적용
from mlxtend.frequent_patterns import apriori

# 최소 지지도(min_support=0.3, 즉 거래의 30% 이상에서 등장하는 아이템셋만 남김)
result = apriori(df, min_support=0.3, use_colnames=True)
result
'''
	support	itemsets
0	0.3	(바게트빵)
1	0.3	(버터)
2	0.6	(식빵)
3	0.4	(우유)
4	0.3	(잼)
5	0.3	(버터, 식빵)
6	0.3	(버터, 우유)
7	0.3	(우유, 식빵)
8	0.3	(버터, 우유, 식빵)
'''

# 4. 연관 규칙 생성
from mlxtend.frequent_patterns import association_rules

result_df = association_rules(result,
                       metric="confidence", # 신뢰도 사용
                       min_threshold=0.5,	# 신뢰도가 50% 이상인 규칙만 생성
                       support_only=False) # 신뢰도 사용하니까 False로 두기

# lift 기준으로 정렬 (연관 강도가 높은 규칙 우선)
sort_df = result_df.sort_values(by = "lift", ascending=False)
sort_df.head()
'''
	antecedents	consequents	antecedent support	consequent support	support	confidence	lift	representativity	leverage	conviction	zhangs_metric	jaccard	certainty	kulczynski
8	(우유, 식빵)	(버터)		0.3	0.3	0.3	1.00	3.333333	1.0	0.21	inf	1.000000	1.00	1.000000	1.000
9	(버터)		(우유, 식빵)	0.3	0.3	0.3	1.00	3.333333	1.0	0.21	inf	1.000000	1.00	1.000000	1.000
2	(버터)		(우유)		0.3	0.4	0.3	1.00	2.500000	1.0	0.18	inf	0.857143	0.75	1.000000	0.875
7	(버터, 식빵)	(우유)		0.3	0.4	0.3	1.00	2.500000	1.0	0.18	inf	0.857143	0.75	1.000000	0.875
3	(우유)		(버터)		0.4	0.3	0.3	0.75	2.500000	1.0	0.18	2.8	1.000000	0.75	0.642857	0.875

해석:
- {우유, 식빵} → {버터}, lift=3.33 → 우유와 식빵을 같이 산 고객은 버터도 함께 살 확률이 3.33배 높음
- {버터} → {우유, 식빵}, confidence=1.0 → 버터 구매자는 항상 우유+식빵을 같이 샀음
'''

2.1.2. FP-Growth 알고리즘

  • Apriori 개선 버전
  • FP-Tree 구조 활용 → DB 접근 횟수 줄여 효율 ↑
# FP-Growth 알고리즘 실습
from mlxtend.frequent_patterns import fpgrowth

# 1. FP-Growth 적용
# - apriori와 마찬가지로 TransactionEncoder로 변환한 희소행렬 df를 입력
# - min_support=0.3 : 거래의 30% 이상에서 등장하는 아이템셋만 남김
result = fpgrowth(df, min_support=0.3, use_colnames=True) 

# 2. 연관 규칙 생성
result_df = association_rules(result,
                       metric="confidence", # 신뢰도 사용
                       min_threshold=0.5,
                       support_only=False) # 신뢰도 사용하니까 False로 두기

# 3. lift 기준 정렬
sort_df = result_df.sort_values(by = "lift", ascending=False)
sort_df.head()
'''
	antecedents	consequents	antecedent support	consequent support	support	confidence	lift	representativity	leverage	conviction	zhangs_metric	jaccard	certainty	kulczynski
8	(우유, 식빵)	(버터)		0.3	0.3	0.3	1.00	3.333333	1.0	0.21	inf	1.000000	1.00	1.000000	1.000
9	(버터)		(우유, 식빵)	0.3	0.3	0.3	1.00	3.333333	1.0	0.21	inf	1.000000	1.00	1.000000	1.000
2	(버터)		(우유)		0.3	0.4	0.3	1.00	2.500000	1.0	0.18	inf	0.857143	0.75	1.000000	0.875
7	(버터, 식빵)	(우유)		0.3	0.4	0.3	1.00	2.500000	1.0	0.18	inf	0.857143	0.75	1.000000	0.875
3	(우유)		(버터)		0.4	0.3	0.3	0.75	2.500000	1.0	0.18	2.8	1.000000	0.75	0.642857	0.875
'''
studying:)
@studying:) :: what i studied

studying:) 님의 학습 여정을 기록하는 블로그입니다.

목차