개인 활동(공부)/강의

Kaggle 데이터를 활용한 개인화 추천시스템(4)

분석가 황규진 2024. 12. 5. 16:50

추천알고리즘 성능 고도화

  • 일반적으로 베이스라인 모델을 빠르게 구축하고 성능 테스트를 진행/확인한 이후 이를 베이스로하여 성능 고도화를 위한 노력을 진행합니다.
  • 데이터를 늘리거나 줄이거나 새로운 데이터를 수집해서 실험해보거나 전처리/후처리 조건 등을 변경하는 작업이 필수입니다.
  • 위 내용은 실무에서 가능하고 도메인 지식과 밀접히 연결되어 있어 이번 강의에서는 스킵하고 추천 알고리즘을 변경해보는 방향을 위주로 성능 고도화를 진행합니다.
  • 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 및 아웃라이어에 큰 영향을 받지 않음. 대부분 실제 환경에서 변수가 간 관계는 비선형 관계를 가짐
    • 계산 비용이 높고, 수식이 상대적으로 복잡하며 다소 블랙박스 형태의 모델이므로 예측 과정 및 이유에 대한 설명력이 높지 않음

https://www.geeksforgeeks.org/linear-regression-vs-neural-networks-understanding-key-differences/

 

모델 생성

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