Add GPCEncoder.py and main.py

main.py is a code that analysis the accuracy between various encoders.
This commit is contained in:
2026-01-08 23:56:43 +09:00
committed by GitHub
parent eb75ddf1e4
commit 838e7e1354
2 changed files with 268 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
# Import
import numpy as np
import pandas as pd
# Define functions
def make_random_projection_matrix(K: int, d: int, rng: np.random.Generator) -> np.ndarray:
"""
K차원 -> d차원으로 줄이는 랜덤 투영 행렬 R (shape: K x d)
- gaussian: N(0, 1)/sqrt(d)
"""
R = rng.normal(0.0, 1.0, size=(K, d)) / np.sqrt(d)
return R
# Define GPC Encoder (Geoemetry Preserving Category Encoder)
def gpc_encoder(
X_train: pd.DataFrame, X_test: pd.DataFrame, cat_cols: list[str], d: int = 16, r: float = 1.0, seed: int = 42,) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
train 기준으로 각 cat_col의 'category -> vector' 매핑을 만들고
train/test 모두 동일 매핑으로 변환한 뒤, d차원 벡터를 컬럼으로 펼쳐 반환.
"""
rng = np.random.default_rng(seed)
Xtr_num = X_train.drop(columns=cat_cols)
Xte_num = X_test.drop(columns=cat_cols)
tr_blocks = [Xtr_num]
te_blocks = [Xte_num]
for col in cat_cols:
# (중요) 결측을 하나의 범주로 처리해 매핑 일관성 유지
tr = X_train[col].astype("object")
te = X_test[col].astype("object")
# 4.1 (train 기준) 범주 목록 확정
cats = pd.unique(tr)
K = len(cats)
if K < 2:
# 범주가 1개면 분리 정보가 없으니 상수 벡터로 처리
const_vec = np.zeros(d, dtype=float)
const_vec[0] = r
mapping = {cats[0]: const_vec}
fallback = const_vec
else:
# 4.1 e_i: K차원 표준기저를 직접 만들 필요는 없지만,
# 수식 흐름을 그대로 반영하기 위해 I(K)를 사용한다.
I = np.eye(K, dtype=float) # e_i 들이 행(row)로 들어있다고 보면 됨
ones = np.ones((K,), dtype=float) / K # (1/K)*1 벡터
# 4.2 u_i: 중심을 원점으로(평행이동) -> u_i = e_i - (1/K)1
U = I - ones[None, :] # shape (K, K)
# 4.3 v_i: 길이를 r로 맞추는 스케일
# ||u_i|| = sqrt((K-1)/K) 이므로, v_i = r * sqrt(K/(K-1)) * u_i
scale = r * np.sqrt(K / (K - 1))
V = scale * U # shape (K, K), 각 row가 v_i
# 4.4 Random Projection: K -> d
# (컬럼별로 다른 R을 쓰고 싶으면 col별 seed를 섞음)
col_seed = int(rng.integers(0, 2**31 - 1))
R = make_random_projection_matrix(K=K, d=d, rng=np.random.default_rng(col_seed))
Z = V @ R # shape (K, d)
# 5.4 투영 후 다시 길이를 r로 맞춤(정규화)
norms = np.linalg.norm(Z, axis=1, keepdims=True)
Z = (Z / norms) * r
# 5.5 category -> vector 매핑 테이블 생성
mapping = {cat: Z[i] for i, cat in enumerate(cats)}
# unseen category 처리용 fallback (중립 벡터: 평균 후 정규화)
mean_vec = Z.mean(axis=0)
mv = np.linalg.norm(mean_vec)
if mv < 1e-12:
tmp = np.random.default_rng(col_seed).normal(size=d)
mean_vec = (tmp / np.linalg.norm(tmp)) * r
else:
mean_vec = (mean_vec / mv) * r
fallback = mean_vec
# (train/test) 변환: 값 -> 벡터 -> 컬럼 펼치기
def to_matrix(series: pd.Series) -> np.ndarray:
out = np.zeros((len(series), d), dtype=float)
for i, v in enumerate(series):
out[i] = mapping.get(v, fallback)
return out
Ztr = to_matrix(tr)
Zte = to_matrix(te)
new_cols = [f"{col}__gpce_{j}" for j in range(d)]
tr_blocks.append(pd.DataFrame(Ztr, columns=new_cols, index=X_train.index))
te_blocks.append(pd.DataFrame(Zte, columns=new_cols, index=X_test.index))
Xtr_out = pd.concat(tr_blocks, axis=1)
Xte_out = pd.concat(te_blocks, axis=1)
return Xtr_out, Xte_out
+169
View File
@@ -0,0 +1,169 @@
# Import standard libraries
import os
import numpy as np
import pandas as pd
from tqdm import tqdm
import matplotlib.pyplot as plt
# Import model selection
from sklearn.model_selection import train_test_split
# Import preprocessing and pipelines
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
# Import classifiers
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
# Import metrics
from sklearn.metrics import accuracy_score
# Import encoder
from gpc_encoder import gpc_encoder
from sklearn.preprocessing import LabelEncoder
from category_encoders import TargetEncoder, OneHotEncoder
# CONFIG
TARGET_COL = 'is_fake'
CSV_FILE_NAME = 'fake_real_job_postings.csv'
DATA_LEN_LIMIT = 10000
RANDOM_SEED = 42
MODELS = {"LogReg": Pipeline([("scaler", StandardScaler()), ("clf", LogisticRegression(max_iter=2000))]),
"DecisionTree": DecisionTreeClassifier(random_state=42),
"RandomForest": RandomForestClassifier(n_estimators=300, random_state=42, n_jobs=-1),
"KNN": Pipeline([("scaler", StandardScaler()), ("clf", KNeighborsClassifier(n_neighbors=15))]),
"SVM(RBF)": Pipeline([("scaler", StandardScaler()), ("clf", SVC(kernel="rbf"))]),
"MLP": Pipeline([("scaler", StandardScaler()), ("clf", MLPClassifier(hidden_layer_sizes=(64, 32), max_iter=500, random_state=42))])}
ENCODERS= ["GPCE", "Label", "OneHot", "Target"]
D = 16
R = 1.0
TEST_SIZE = 0.2
# Load data
df = pd.read_csv(f"./datasets/{CSV_FILE_NAME}")
if DATA_LEN_LIMIT:
df = df.sample(frac=1).reset_index(drop=True)
df = df[:DATA_LEN_LIMIT]
tar_dtype = df[TARGET_COL].dtype
if tar_dtype == 'object' or tar_dtype == 'category':
df[TARGET_COL] = LabelEncoder().fit_transform(df[TARGET_COL])
cat_cols = df.drop(TARGET_COL, axis=1).select_dtypes(include=['object', 'category']).columns.tolist()
df = df.dropna().copy()
# Split X and y
X = df.drop(columns=[TARGET_COL])
y = df[TARGET_COL].to_numpy()
# Split train and test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=y if len(np.unique(y)) > 1 else None)
# Encode categorical features
def get_encoded_data(method, X_train, X_test, y_train, cat_cols):
if method == "GPCE":
X_train_enc, X_test_enc = gpc_encoder(X_train=X_train, X_test=X_test, cat_cols=cat_cols, d=D, r=R, seed=RANDOM_SEED)
return X_train_enc, X_test_enc
elif method == "Label":
X_train_enc, X_test_enc = X_train.copy(), X_test.copy()
for col in cat_cols:
le = LabelEncoder()
full_data = pd.concat([X_train_enc[col], X_test_enc[col]]).astype(str)
le.fit(full_data)
X_train_enc[col] = le.transform(X_train_enc[col].astype(str))
X_test_enc[col] = le.transform(X_test_enc[col].astype(str))
return X_train_enc, X_test_enc
elif method == "OneHot":
ohe = OneHotEncoder(cols=cat_cols, use_cat_names=True, handle_unknown='value')
X_train_enc = ohe.fit_transform(X_train, y_train)
X_test_enc = ohe.transform(X_test)
return X_train_enc, X_test_enc
elif method == "Target":
te = TargetEncoder(cols=cat_cols)
X_train_enc = te.fit_transform(X_train, y_train)
X_test_enc = te.transform(X_test)
return X_train_enc, X_test_enc
return X_train, X_test
results = {}
for encoder in tqdm(
ENCODERS,
desc="Encoding method",
position=0
):
results[encoder] = {}
X_train_enc, X_test_enc = get_encoded_data(method=encoder, X_train=X_train, X_test=X_test, y_train=y_train, cat_cols=cat_cols)
for name, model in tqdm(
MODELS.items(),
desc=f"Models ({encoder})",
position=1,
leave=False
):
# Train the model
model.fit(X_train_enc, y_train)
# Evaluate the model
pred = model.predict(X_test_enc)
accuracy = accuracy_score(y_test, pred)
# Save the result
results[encoder][name] = float(accuracy)
results_df = pd.DataFrame(results).T
print("========================== Accuracy Result =========================")
print(results_df)
# Visualize
models = results_df.columns.tolist()
encoders = results_df.index.tolist()
n_models = len(models)
n_encoders = len(encoders)
x = np.arange(n_models)
bar_width = 0.8 / n_encoders
gray_levels = np.linspace(0.85, 0.25, len(encoders))
min_y = max(results_df.min().min() - 0.1, 0)
max_y = min(results_df.max().max() + 0.1, 1)
plt.figure(figsize=(12, 5))
for i, (enc, gray) in enumerate(zip(encoders, gray_levels)):
offsets = x - 0.4 + (i + 0.5) * bar_width
plt.bar(
offsets,
results_df.loc[enc].values,
width=bar_width,
color=str(gray_levels[i]),
edgecolor="black",
label=enc
)
plt.xticks(x, models, rotation=0)
plt.xlabel("Model")
plt.ylabel("Accuracy")
plt.title(f"Comparison of Model Accuracy Across Encoders ({CSV_FILE_NAME})")
plt.ylim(min_y, max_y)
plt.legend(title="Encoder", ncol=min(n_encoders, 4))
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
output_dir = "results"
file_name = f"{CSV_FILE_NAME.replace(".csv", "")}.png"
full_path = os.path.join(output_dir, file_name)
os.makedirs(output_dir, exist_ok=True)
plt.savefig(full_path, dpi=500)
plt.show()