추천알고리즘 성능 고도화
- 일반적으로 베이스라인 모델을 빠르게 구축하고 성능 테스트를 진행/확인한 이후 이를 베이스로하여 성능 고도화를 위한 노력을 진행합니다.
- 데이터를 늘리거나 줄이거나 새로운 데이터를 수집해서 실험해보거나 전처리/후처리 조건 등을 변경하는 작업이 필수입니다.
- 위 내용은 실무에서 가능하고 도메인 지식과 밀접히 연결되어 있어 이번 강의에서는 스킵하고 추천 알고리즘을 변경해보는 방향을 위주로 성능 고도화를 진행합니다.
- 3강의 베이스라인 모델의 성능을 개선할 방향성은 크게 아래 2가지입니다.
- (1) DeepLearning 계열 - DNN, NCF via TensorFlow
- (2) Regression 계열 Model (Linear Regression, Ridge, Random Forest, Gradient Boosting 등) via Scikit-learn
회귀 모델
- Linear Regression
- 선형 관계를 가정, Scale 및 아웃라이어에 민감, 데이터가 선형적이면 매우 효과적이고 계산 비용이 적으나
- 실제 환경에서 선형관계를 가지는 데이터셋을 모델링하는 경우가 흔하지 않음. 예측 성능이 낮은 경향이 있고, 변수의 독립성 등 가정을 잘 지켜야 함
- Tree-based Ensemble
- 비선형 관계를 가정, Scale 및 아웃라이어에 큰 영향을 받지 않음. 대부분 실제 환경에서 변수가 간 관계는 비선형 관계를 가짐
- 계산 비용이 높고, 수식이 상대적으로 복잡하며 다소 블랙박스 형태의 모델이므로 예측 과정 및 이유에 대한 설명력이 높지 않음
모델 생성
Regression
# DNN, NCF 모델을 이용하여 1차 성능 고도화 시도를 진행합니다.
# Regression 계열 모델을 이용하여 2차 성능 고도화를 진행합니다.
from sklearn.metrics import mean_squared_error, mean_absolute_error
from keras.models import Model
from keras.layers import Input, Embedding, Flatten, Concatenate, Dense, BatchNormalization, Dropout
# Preparing user and item embeddings
n_users = df_mf['user_id'].nunique()
n_items = df_mf['isbn'].nunique()
# Creating user and item IDs mapping
user_mapping = {user: idx for idx, user in enumerate(df_mf['user_id'].unique())}
item_mapping = {item: idx for idx, item in enumerate(df_mf['isbn'].unique())}
# Mapping user and item IDs
df_mf['user_id_mapped'] = df_mf['user_id'].map(user_mapping)
df_mf['isbn_mapped'] = df_mf['isbn'].map(item_mapping)
# Stratified split
split = StratifiedShuffleSplit(n_splits=1, test_size=0.3, random_state=42)
for train_idx, test_idx in split.split(df_mf, df_mf['user_id']):
strat_train_set = df_mf.iloc[train_idx] # Use iloc for positional indexing
strat_test_set = df_mf.iloc[test_idx]
# Splitting the data
X_train = strat_train_set[['user_id_mapped', 'isbn_mapped']]
y_train = strat_train_set['rating']
X_test = strat_test_set[['user_id_mapped', 'isbn_mapped']]
y_test = strat_test_set['rating']
DNN ( Deep Neural Network )
from keras.models import Sequential
from keras.layers import Dense, Dropout, BatchNormalization
from keras.optimizers import Adam
from keras.callbacks import EarlyStopping, ReduceLROnPlateau
# Define the model
model = Sequential()
# 입력층
model.add(Dense(128, activation='relu', input_shape=(X_train.shape[1],))) # fully connected, 레이어 입력층 선언
model.add(BatchNormalization()) # 레이어의 출력을 정규화하여 학습을 안정적으로 만듦
model.add(Dropout(0.2)) # 랜덤하게 20% 가중치 삭제
# 히든 레이어
model.add(Dense(64, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(32, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(16, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.2))
# 출력층
model.add(Dense(1)) #출력층으로, 1개의 노드만 가짐. 출력값이 하나(연속형 값)로
# Compile the model
# learning_rate=0.01 학습 속도, 학습이 진행될수록 decay=0.01를 통해 학습률이 점점 감소
optimizer = Adam(learning_rate=0.01, decay=0.01)
model.compile(optimizer=optimizer, loss='mean_squared_error')
# Define early stopping and learning rate reduction
early_stopping = EarlyStopping(monitor='val_loss', patience=5, min_delta=0.001) #검증 데이터의 손실 값(val_loss)이 5번의 에포크 동안 개선되지 않으면 학습 중단. 5번 에포크수 동안, 손실 값이 0.001 미만으로 개선될 경우 변화로 카운팅 제외
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=0.0001) #학습률 조절. 검증 손실이 3번의 에포크 동안 개선되지 않으면 학습률을 50% 감소, 최소 학습률은 0.0001
# Train the model
model.fit(X_train, y_train, epochs=20, batch_size=256, validation_split=0.2,
callbacks=[early_stopping, reduce_lr])
y_pred = model.predict([X_test, X_test])
# Calculate RMSE and MAE
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'DNN RMSE: {rmse:.4f}')
mae = mean_absolute_error(y_test, y_pred) # No square root for MAE
print(f'DNN MAE: {mae:.4f}')
# DNN RMSE: 1.8160
# DNN MAE: 1.4520
dnn_results = {'Algorithm': 'DNN', 'RMSE': rmse, 'MAE': mae}
results_df = pd.concat([results_df, pd.DataFrame([dnn_results])], ignore_index=True)
재현율 코드
def get_precision_recall_from_testdata(test_data, top_n, threshold, y_pred):
top_k = top_n
predictions_df = test_data.copy()
predictions_df = predictions_df[['user_id', 'isbn', 'rating']]
predictions_df['est_rating'] = y_pred
predictions_df['pred_rank'] = predictions_df.groupby('user_id')['est_rating'].rank(method='first', ascending=False)
predictions_df['true_rank'] = predictions_df.groupby('user_id')['rating'].rank(method='first', ascending=False)
predictions_df['is_relevant'] = predictions_df['true_rank'] <= top_k
predictions_df = predictions_df.sort_values(by='user_id')
predicted_top_k = predictions_df[predictions_df['pred_rank'] <= top_k]
precision_per_user = predicted_top_k.groupby('user_id')['is_relevant'].sum()
total_relevant_items = predictions_df.query("rating < @threshold").groupby("user_id").size()
precision_at_k = precision_per_user / top_k
recall_at_k = precision_per_user / total_relevant_items
return precision_at_k.mean(), recall_at_k.mean()
DNN 성능 측정 코드
# Dictionary to store models and their predictions
models_predictions = {}
# Evaluate models and get hit ratios
top_n_values = [1, 2, 3, 4, 5, 6, 7]
dnn_precision_results = []
dnn_recall_results = []
threshold = 7
model_name = 'DNN'
# Get precision for different Top-N values
for top_n in top_n_values:
dnn_precision, dnn_recall = get_precision_recall_from_testdata(strat_test_set, top_n, threshold, y_pred)
dnn_precision_results.append({'model': model_name, 'topk': top_n, 'precision': dnn_precision})
dnn_recall_results.append({'model': model_name, 'topk': top_n,'recall': dnn_recall})
precision_results = pd.concat([precision_results, pd.DataFrame(dnn_precision_results)])
recall_results = pd.concat([recall_results, pd.DataFrame(dnn_recall_results)])
precision_results.pivot_table(index='model', columns='topk', values='precision').round(3)
recall_results.pivot_table(index='model', columns='topk', values='recall').round(3)
Neural Collaborative Filtering (NCF)
- 기본적인 모델인 Collaborative Filtering을 신경망 구조로 확장하여, 사용자와 아이템 간의 비선형 관계를 모델링할 수 있도록 설계된 방법입니다.
- NCF는 Matrix Factorization처럼 사용자의 잠재 요인(latent factors)과 아이템의 잠재 요인을 학습합니다.
- 이 과정에서 신경망의 비선형 활성 함수를 사용하여 복잡한 패턴을 학습합니다.
- 전통적인 협업 필터링 기법은 선형 모델에 의존하지만(주로 dot product), NCF는 비선형 활성 함수(예: MLP)를 사용하여 사용자와 아이템 간의 복잡한 관계를 포착합니다.
# Neural Collaborative Filtering model
n_latent_factors = 64 # 사용자와 아이템의 잠재 요인 수를 정의
user_input = Input(shape=(1,))
item_input = Input(shape=(1,))
user_embedding = Embedding(n_users, n_latent_factors)(user_input) # 임베딩 진행
user_vec = Flatten()(user_embedding) # 임베딩된 벡터를 1차원 배열로 변환 - dense layer 입력용
item_embedding = Embedding(n_items, n_latent_factors)(item_input)
item_vec = Flatten()(item_embedding)
# Concatenate user and item embeddings
concat = Concatenate()([user_vec, item_vec]) #사용자 벡터와 아이템 벡터를 연결하여 하나의 벡터로 통합
# Adding fully connected layers with batch normalization and dropout
dense = Dense(256, activation='relu')(concat)
dense = BatchNormalization()(dense)
dense = Dropout(0.2)(dense)
dense = Dense(128, activation='relu')(dense)
dense = BatchNormalization()(dense)
dense = Dropout(0.2)(dense)
output = Dense(1)(dense)
# Compile model
model = Model([user_input, item_input], output)
model.compile(optimizer='rmsprop', loss='mean_squared_error')
# Fit the model
model.fit([X_train['user_id_mapped'], X_train['isbn_mapped']], y_train, epochs=2, batch_size=128, validation_split=0.2)
# Predict and evaluate
y_pred = model.predict([X_test['user_id_mapped'], X_test['isbn_mapped']])
# Calculate RMSE and MAE
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'NCF RMSE: {rmse:.4f}')
mae = mean_absolute_error(y_test, y_pred) # No square root for MAE
print(f'NCF MAE: {mae:.4f}')
ncf_results = {'Algorithm': 'NCF', 'RMSE': rmse, 'MAE': mae}
results_df = pd.concat([results_df, pd.DataFrame([ncf_results])], ignore_index=True)
# Dictionary to store models and their predictions
models_predictions = {}
# Evaluate models and get hit ratios
top_n_values = [1, 2, 3, 4, 5, 6, 7]
ncf_precision_results = []
ncf_recall_results = []
threshold = 7
model_name = 'NCF'
# Get precision for different Top-N values
for top_n in top_n_values:
ncf_precision, ncf_recall = get_precision_recall_from_testdata(strat_test_set, top_n, threshold, y_pred)
ncf_precision_results.append({'model': model_name, 'topk': top_n, 'precision': ncf_precision})
ncf_recall_results.append({'model': model_name, 'topk': top_n,'recall': ncf_recall})
precision_results = pd.concat([precision_results, pd.DataFrame(ncf_precision_results)])
recall_results = pd.concat([recall_results, pd.DataFrame(ncf_recall_results)])
precision_results.pivot_table(index='model', columns='topk', values='precision').round(3)\
recall_results.pivot_table(index='model', columns='topk', values='recall').round(3)
Machine Learning(Regression) Model
- 딥러닝 모델의 경우 비정형 데이터(이미지, 오디오, 텍스트 등)에서 스스로 피처를 추출하고 복잡한 관계를 학습하는데 특화된 반면 간단한 데이터나 Tabular 데이터셋에는 잘 맞지 않는 경향이 있습니다.
- 이 경우 Linear Regression 과 같은 전통적인 구조가 간단하여 속도가 빠르고 설명력이 높고 리소스 및 운영 코스트 효율이 높은 머신러닝 모델을 주로 이용합니다.
- 수많은 Regression 알고리즘 중에서 몇개를 선정하여 아래와 같이 딕셔너리를 선언하여 각 성능을 체크해봅니다.
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, VotingRegressor, ExtraTreesRegressor
from sklearn.neural_network import MLPRegressor
from xgboost import XGBRegressor
from sklearn.linear_model import LinearRegression, Ridge, ElasticNet
from sklearn.metrics import mean_absolute_error, mean_squared_error
# Regression 알고리즘 후보 (이중 높은 성능을 보이는 모델을 실험을 통해 빠르게 찾는 과정이 중요)
models = {
'Linear Regression': LinearRegression(),
'Ridge': Ridge(),
'ElasticNet': ElasticNet(),
'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, random_state=42),
'ExtraTreesRegressor': ExtraTreesRegressor(),
}
models
# Preprocessing
df_mf = df[['user_id', 'isbn', 'rating']].drop_duplicates()
user_list = df_mf.groupby("user_id").size().reset_index()
user_list.columns = ['user_id', 'review_cnt']
user_list = user_list[user_list['review_cnt'] < 30]
df_mf = df_mf[df_mf['user_id'].isin(user_list.user_id)]
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.feature_extraction.text import TfidfVectorizer
split = StratifiedShuffleSplit(n_splits=1, test_size=0.3, random_state=42)
for train_idx, test_idx in split.split(df_mf, df_mf['user_id']):
strat_train_set = df_mf.iloc[train_idx]
strat_test_set = df_mf.iloc[test_idx]
user_info = df[['user_id', 'age']].drop_duplicates()
user_info.head()
def encode_categorical_columns(df):
categorical_columns = ['isbn', 'book_author', 'category']
for column in categorical_columns:
df[f'{column}_encoded'] = df[column].astype('category').cat.codes
return df
def prepare_data(df, book_info, user_info):
merged_df = df.merge(book_info, on=['isbn'])\
.merge(user_info[['user_id', 'age']].drop_duplicates(), on='user_id')
encoded_df = encode_categorical_columns(merged_df)
return encoded_df
strat_train_set = prepare_data(strat_train_set, book_info, user_info)
strat_test_set = prepare_data(strat_test_set, book_info, user_info)
def split_data(df):
features = ['age', 'isbn_encoded', 'book_author_encoded', 'year_of_publication',
'rating_mean', 'rating_count', 'category_encoded']
X = df[features]
y = df['rating']
return X, y
X_train, y_train = split_data(strat_train_set)
X_test, y_test = split_data(strat_test_set)
print(X_train.shape) # (132995, 7)
print(y_train.shape) # (132995,)
print(X_test.shape) # (56998, 7)
print(y_test.shape) # (56998, )
results = {}
# Iterate over the models and calculate the MAE and RMSE values
for name, model in models.items():
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
#print(name, model)
results[name] = {'MAE': mae, 'RMSE': rmse}
Feature 기반의 모델이 성능이 좋으나 항상 Feature 정보가 있는 것은 아니며,
Feature Quailty에 의해 성능이 크게 영향을 받는다는 단점이 존재합니다. (예, 장애 및 수집오류로 인한 이슈)
상황에 맞춰 두 방향성을 모두 고려하여 최적화하여 이용하는 것이 필요합니다. (예, ML 모델의 보안으로 cf, mf 이용 등)
최종 모델 예측 및 저장
# Dictionary to store models and their predictions
models_predictions = {}
# Evaluate models and get hit ratios
top_n_values = [1, 2, 3, 4, 5, 6, 7]
ml_precision_results = []
ml_recall_results = []
threshold = 7
for model_name, model in models.items():
print(f"Evaluating {model_name}...")
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
models_predictions[model_name] = {
'model': model,
'predictions': y_pred
}
# Get precision for different Top-N values
for top_n in top_n_values:
ml_precision, ml_recall = get_precision_recall_from_testdata(strat_test_set, top_n, threshold, y_pred)
ml_precision_results.append({'model': model_name, 'topk': top_n, 'precision': ml_precision})
ml_recall_results.append({'model': model_name, 'topk': top_n,'recall': ml_recall})
ml_precision_results = pd.DataFrame(ml_precision_results)
ml_recall_results = pd.DataFrame(ml_recall_results)
ml_precision_results.pivot_table(index='model', columns='topk', values='precision')
ml_recall_results.pivot_table(index='model', columns='topk', values='recall')
[출처] 메타코드M
'코드 및 쿼리문 > 강의 - 메타코드M' 카테고리의 다른 글
Kaggle 데이터를 활용한 개인화 추천시스템(6) (0) | 2024.12.10 |
---|---|
Kaggle 데이터를 활용한 개인화 추천시스템(5) (1) | 2024.12.10 |
Kaggle 데이터를 활용한 개인화 추천시스템(3) (2) | 2024.12.05 |
Kaggle 데이터를 활용한 개인화 추천시스템(2) (2) | 2024.12.05 |
Kaggle 데이터를 활용한 개인화 추천시스템(1) (1) | 2024.12.02 |