FFMをxLearnで試す

機械学習

Field-aware Factorization Machines (FFM) を Python で試すために、xLearn を使ってみた。

環境によってインストールできたりできなかったりして、よく分からない。メンテナンスもされていなさそう。

とりあえず、分類モデルを作ってみる。

ライブラリの import

from pathlib import Path
import random

from matplotlib import pyplot as plt
import numpy as np
import polars as pl
from sklearn.metrics import roc_auc_score, roc_curve
from sklearn.model_selection import train_test_split
import xlearn as xl

適当にデータ生成。ユーザとアイテムの組み合わせに対してCV予測するようなのを想定。ユーザには年齢(簡単のため20歳以上60歳以下)、職業(’A’ から ‘J’ の10種類)の情報があり、アイテムにはカテゴリー(’A’ から ‘J’ の10種類)の情報がある。ちなみに Polars については、以前 Qiita で記事を書いている。

random.seed(1)
np.random.seed(1)

n_users = 1000
n_items = 100
users = [f'user{i:05}' for i in range(n_users)]
items = [f'item{i:04}' for i in range(n_items)]

df_users = pl.DataFrame({
    'user': users,
    'age': [random.randint(20, 60) for _ in range(n_users)],
    'occupation': [random.choice(list('ABCDEFGHIJ')) for _ in range(n_users)],
})

df_items = pl.DataFrame({
    'item': items,
    'category': [random.choice(list('ABCDEFGHIJ')) for _ in range(n_items)],
})

df = df_users.join(df_items, how='cross')


x = (df['age'] - 40) / 10
y = df['occupation'].apply(lambda x: ord(x) - 68)
z = df['category'].apply(lambda x: ord(x) - 72)

# 分布確認用
# plt.hist((x * y - x * z + y * z / 10 - 10).to_numpy()
#          + np.random.normal(scale=5, size=df.height))

df['cv'] = ((x * y - x * z + y * z / 10 - 10).to_numpy()
            + np.random.normal(scale=5, size=df.height)) > 0
df['cv'] = df['cv'].cast(pl.Int32)

train/valid/test に分割。valid は early stopping に使う。

dev_idx, test_idx = train_test_split(
    range(df.height), test_size=0.2, random_state=1, stratify=df['cv'])
train_idx, valid_idx = train_test_split(
    dev_idx, test_size=0.2, random_state=1, stratify=df[dev_idx]['cv'])

FFM は独特の format があるので、その変換のための関数を定義。上記データ用にハードコーディングしている。予測用データにはラベルはなくてもよい。あと for loop は分かりやすいので使っているが、当然ながら遅いので、大きいデータを扱うには向かない。例えば df を直接いじって to_csv(has_header=False, sep=' ') とかすれば速くなる。

def ffm_format(df: pl.DataFrame) -> str:
    """
    format df to
    label field_1:feature_1:value_1 field_2:feature_2:value_2 ...
    """

    txt_all = ''
    for i in range(df.height):
        # label
        txt = str(df[i, 'cv'])

        # field: age
        age = df[i, 'age'] - 20  # 20 -- 60 -> 0 -- 40
        txt += f' 0:{age}:1'

        # field: occupation
        occupation = ord(df[i, 'occupation']) - 65 + 41
        txt += f' 1:{occupation}:1'

        # field: category
        category = ord(df[i, 'category']) - 65 + 41 + 10
        txt += f' 2:{category}:1'

        txt += '\n'
        txt_all += txt

    return txt_all

適当にディレクトリを作って、フォーマットしたテキストデータを入れる。

Path('xlearn_ffm').mkdir(exist_ok=True)
train_path = 'xlearn_ffm/train.txt'
valid_path = 'xlearn_ffm/valid.txt'
test_path = 'xlearn_ffm/test.txt'
with open(train_path, 'w') as f:
    f.write(ffm_format(df[train_idx]))
with open(valid_path, 'w') as f:
    f.write(ffm_format(df[valid_idx]))
with open(test_path, 'w') as f:
    f.write(ffm_format(df[test_idx]))

学習。pathlib.Path は受け付けないので注意。

ffm_model = xl.create_ffm()

ffm_model.setTrain(train_path)
ffm_model.setValidate(valid_path)

param = {
    'task': 'binary',
    'metric': 'auc',
    'lr': 0.2,
    'lambda': 0.002,
    'k': 4,
    'epoch': 10,
}

model_path = 'xlearn_ffm/model.out'
ffm_model.fit(param, model_path)

予測。

predict_path = 'xlearn_ffm/predict.txt'
ffm_model.setSigmoid()
ffm_model.setTest(test_path)
ffm_model.predict(model_path, predict_path)

精度評価。AUC で 0.91 くらいになった。

y_true = pl.read_csv(
    test_path, sep=' ', has_header=False, columns=[0])[:, 0].to_numpy()
y_pred = pl.read_csv(
    predict_path, has_header=False)[:, 0].to_numpy()
print(f'AUC = {roc_auc_score(y_true, y_pred):.4f}')

ROC curve (https://note.nkmk.me/python-sklearn-roc-curve-auc-score/)

fpr, tpr, thresholds = roc_curve(y_true, y_pred)

plt.plot(fpr, tpr, marker='o')
plt.xlabel('FPR: False positive rate')
plt.ylabel('TPR: True positive rate')
plt.grid()

以上。

コメント

タイトルとURLをコピーしました