<jupyter_start><jupyter_text>"심층분해머신"> "d2l.ai 16장의 특징 기반 추천 시스템을 구현하여 보았다."- toc: true- badges: true- author: 단호진- categories: [recommender] 앱이나 웹에서 사용자가 광고를 클릭할 것인지 예측하는 것은 광고주 입장에서 중요한 사안이다. 사용자에 맞는 광고를 노출시키는데 도움이 되기 때문이다. 여기에서 다루는 문제는 테이블 문제인데 부스팅 류의 알고리즘이 성공적으로 적용되어 왔다. 추천 시스템을 이용하면 테이블 문제를 다른 관점에서 볼 수 있게된다. 이 포스트에서 살펴볼 분해머신은 행렬분해추천 시스템의 개념을 확장한 것이며 유저도 아이템의 위치로 옮겨 두었다고 생각하면 좋다. 코드 구현의 목적은 다음과 같다.- d2l mxnet 코드를 참고하여 pytorch 코드로 이식하면서 특징 기반의 문제에서 심층분해머신을 구현한다.- polars dataframe 패키지를 사용한다.- Catboost를 사용하여 베이스 라인을 계산하여 본다. 데이터각 특징 컬럼은 사생활 보호 등의 사유로 세부 사항을 알 수 없다. 모든 특징 변수는 범주형으로 임베딩 처리를 위하여 특징 열별로 0부터 시작하는 새로운 아이디를 부여하였다.<jupyter_code>from pathlib import Path
import d2l.torch as d2l

d2l.DATA_HUB['ctr'] = (d2l.DATA_URL + 'ctr.zip',
                       'e18327c48c8e8e5c23da714dd614e390d369843f')

data_dir = d2l.download_extract('ctr')
dh = Path(data_dir)
import numpy as np
import polars as pl
import pandas as pd
def pl_df(p, lookup=None):
    df = pd.read_csv(p, sep='\t', header=None)
    df = pl.from_pandas(df)
    for name in df.columns:
        df[name] = df[name].cast(pl.UInt64)
    df['key'] = list(range(len(df)))
    df['name'] = [p.stem] * len(df)
    df = df[df.columns[-2:] + df.columns[:-2]]
#     df = df.melt(id_vars=['key', 'name', '0'], value_vars=df.columns[1:-2])
    return df

df = pl_df(dh/'train.csv')
df = df.vstack(pl_df(dh/'test.csv'))
# df.null_count()
df.head()
from collections import OrderedDict

def encoding_df(df: pl.DataFrame):
    cols = df.columns[3:]
    for col in cols:
        d = {v:k for k, v in enumerate(df[col].unique())}
        df[col] = list(map(lambda x: d[x], df[col]))
        
    num_features = OrderedDict([(col, df[col].n_unique()) for col in cols])
    return df, num_features

train, num_features = encoding_df(df)
test = train.filter(pl.col('name') == 'test').drop('name')
train = train.filter(pl.col('name') == 'train').drop('name')
len(train), len(test)
num_features
train.head()
from torch.utils.data import Dataset, DataLoader

class CTRDataset(Dataset):
    def __init__(self, df: pl.DataFrame):
        super().__init__()
        self.df = df
        
    def __len__(self):
        return len(self.df)
    
    def __getitem__(self, index):
        return {
            'target': np.array([self.df[index, 1]], dtype=np.float32),
            'features': self.df[index, 2:].to_numpy()[0]
        }
ds_tr = CTRDataset(train)
len(ds_tr)
ds_tr[0]<jupyter_output><empty_output><jupyter_text>모델 16.9.1. 2원 분해 머신\begin{equation}\hat y(x) = w_0 + \sum_{i=1}^d w_i x_i + \sum_{i=1}^d \sum_{j=i+1}^d  x_i x_j\end{equation}- $x \in \mathbb{R}^d $, 특징 변수의 임베딩- $w_i \in \mathbb{R}^d $, 각 특징 변수에 대한 Linear 레이어- $V \in \mathbb{R}^{d \times k} $, 특징 변수의 레코드 임베딩아래 모델 코드에는 전역 바이스 고려가 되어 있지 않다. 차후 수정되어야 한다.<jupyter_code>import torch
import torch.nn as nn
import torch.nn.functional as F
import pytorch_lightning as ptl
from itertools import chain


class TwoWayFM(ptl.LightningModule):
    def __init__(self, num_factors, num_features, hiddens=None, drop_rate=0.1):
        super().__init__()
        self.num_features = sorted(num_features.items(), key=lambda x: int(x[0]))
        self.num_features = OrderedDict(self.num_features)
        self.item = nn.Embedding(len(self.num_features), num_factors)
        self.vs = nn.ModuleList([
            nn.Embedding(cats, num_factors) for cats in self.num_features.values()
        ])
        self.fcs = nn.ModuleList([
            nn.Linear(num_factors, 1, bias=True) for _ in range(len(self.num_features))
        ])
        self.mlp = None
        self.hiddens = hiddens
        if self.hiddens:
            self.mlp = [[
                nn.Linear(fin, fout),
                nn.ReLU(),
                nn.Dropout(drop_rate),
            ] for fin, fout in zip(
                [num_factors * len(self.num_features)] + self.hiddens,
                self.hiddens + [1]
            )]
            self.mlp = nn.Sequential(*[l for l in chain.from_iterable(self.mlp)])
        
    def forward(self, features):  # b x d
        d = len(self.num_features)
        x = torch.unsqueeze(
            self.item(torch.arange(d, device=self.device)),
            axis=0) # 1 x d x f
        v = torch.cat([
            torch.unsqueeze(self.vs[k](features[:, k]), axis=1) for k in range(d)
        ], axis=1)  # b x d x f
        
        hat_y = torch.sum(torch.cat([
            torch.unsqueeze(self.fcs[k](x[:, k, :]), axis=1) for k in range(d)  #
        ], axis=1), axis=2)  #  b x d
        
        square_of_sum = torch.sum(v * x, axis=1) ** 2
        sum_of_square = torch.sum(v ** 2 * x ** 2, axis=1)  # b x f
        hat_y = (torch.sum(hat_y, axis=1, keepdim=True)
                 + 0.5 * torch.sum(
                     square_of_sum - sum_of_square, axis=1, keepdim=True))  # b x 1
        if self.mlp:
            hat_y = hat_y + self.mlp(v.view(hat_y.shape[0], -1))
        return torch.sigmoid(hat_y)
    
    def training_step(self, batch, batch_index):
        target = batch['target']
        features = batch['features']
        hat_y = self.forward(features)
        loss = F.mse_loss(hat_y, target)
        self.log('train_loss', loss)
        return loss
    
    def validation_step(self, batch, batch_index):
        loss = self.training_step(batch, batch_index)
        self.log('val_loss', loss)
    
    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=0.001, weight_decay=1e-5)
        return optimizer
ds_tr = CTRDataset(train)
ds_va = CTRDataset(test)
dl_tr = DataLoader(ds_tr, batch_size=256, shuffle=True, num_workers=12)
dl_va = DataLoader(ds_va, batch_size=256, shuffle=False, num_workers=12)

model = TwoWayFM(10, num_features, [30, 20, 10])

trainer = ptl.Trainer(
    gpus=1,
    max_epochs=200,
)
trainer.fit(model, dl_tr, dl_va)
gt = []
preds = []
model.eval()
for va in iter(dl_va):
    t = va['target']
    f = va['features']
    gt.append(t)
    p = model(f) > 0.5
    p = p.type_as(t)
    preds.append(p)
gt = torch.cat(gt, axis=0)
preds = torch.cat(preds, axis=0)
from sklearn.metrics import accuracy_score

accuracy_score(gt, preds)<jupyter_output><empty_output><jupyter_text>mlp를 제하면 정확도가 0.87정도 된다. 이 값이 어떤 수준인지 알기 위하여 catboost로 베이스 라인을 만들어 비교해 보자.  Catboost를 이용한 검토<jupyter_code>X_tr = train.drop(['key', '0']).to_numpy()
y_tr = train['0'].to_numpy()
X_va = test.drop(['key', '0']).to_numpy()
y_va = test['0'].to_numpy()
cat_features = np.arange(34)
from catboost import CatBoostClassifier, Pool, cv
from sklearn.metrics import accuracy_score
model = CatBoostClassifier(
    custom_loss=['Accuracy'],
    random_seed=42,
    logging_level='Silent'
)
model.fit(
    X_tr, y_tr,
    cat_features=cat_features,
    eval_set=(X_va, y_va),
#     logging_level='Verbose',  # you can uncomment this for text output
    plot=True,
);
model.best_score_<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1392810362392665, 'fraction_numerical': 0.018483481298209866, 'mean_word_length': 2.871549295774648, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '352537', 'license': 'Apache-2.0', 'n_tokens_mistral': 3133, 'n_tokens_neox': 3324, 'n_words': 631, 'path': '_notebooks/2021-08-07-factorization-machine.ipynb', 'repo_name': 'danhojin/jupyter-blog', 'size': '6871'}