Transformer bawaan scikit-learn mencakup kebutuhan yang paling umum, tetapi transformasi khas suatu data tidak selalu tersedia. scikit-learn menyediakan dua pintu untuk situasi ini, yaitu FunctionTransformer yang membungkus fungsi biasa menjadi transformer, dan penulisan kelas sendiri di atas BaseEstimator serta TransformerMixin. Topik ini membahas keduanya beserta syarat fungsi yang dapat disimpan bersama modelnya, sehingga transformasi buatan sendiri dapat dirangkai ke dalam Pipeline sebagaimana transformer bawaan.
FunctionTransformer untuk fungsi sederhana#
FunctionTransformer menerima sebuah fungsi dan mengembalikan objek transformer yang menjalankan fungsi tersebut pada setiap panggilan transform. Karena fungsi bawaannya tidak mempelajari apa pun dari data, metode fit pada objek ini praktis tidak melakukan apa pun dan langsung mengembalikan dirinya. Bentuk ini tepat untuk transformasi stateless, yaitu transformasi yang hasilnya hanya bergantung pada baris yang diproses, bukan pada statistik seluruh data latih.
Contoh yang umum adalah transformasi logaritmik untuk data yang condong ke kanan, yakni data dengan banyak nilai kecil dan sedikit nilai sangat besar. Transformasi ini memampatkan rentang nilai yang lebar tanpa menghilangkan urutannya. Fungsi np.log1p memetakan setiap nilai x menjadi log(1 + x), dan dipilih daripada np.log karena tetap aman untuk nilai nol.
import numpy as np
from sklearn.preprocessing import FunctionTransformer
# panjang dokumen sintetis dalam jumlah kata, condong ke kanan
panjang_dokumen = np.array([[120.0], [340.0], [980.0], [2500.0], [7400.0]])
# log1p memetakan setiap nilai x menjadi log(1 + x), kebalikannya expm1
log_transformer = FunctionTransformer(np.log1p, inverse_func=np.expm1)
hasil = log_transformer.fit_transform(panjang_dokumen)
print("Asli :", panjang_dokumen.ravel())
print("Log1p:", hasil.ravel().round(3))
print("Kembali:", log_transformer.inverse_transform(hasil).ravel().round(1))Asli : [ 120. 340. 980. 2500. 7400.]
Log1p: [4.796 5.832 6.889 7.824 8.909]
Kembali: [ 120. 340. 980. 2500. 7400.]Keluaran memperlihatkan pemampatan yang dihasilkannya. Jarak antara 120 dan 7400 kata yang semula 7280 unit menyusut menjadi selisih sekitar 4.1 pada skala logaritma, sehingga model yang peka terhadap skala tidak lagi didominasi oleh beberapa dokumen panjang. Baris ketiga menegaskan bahwa inverse_transform mengembalikan nilai ke satuan aslinya, dan kemampuan itu hanya muncul bila inverse_func diberikan. Tanpa parameter tersebut, inverse_func defaultnya adalah fungsi identitas, sehingga pemanggilan inverse_transform hanya mengulang nilai transformasinya.
Transformer kustom berbasis kelas#
Apabila transformasinya perlu parameter yang dapat diatur dari luar atau statistik yang dipelajari dari data, kelas sendiri menjadi tempatnya. Kontraknya pendek, yaitu metode fit(self, X, y=None) yang mengembalikan self, dan metode transform(self, X) yang mengembalikan data baru tanpa mengubah masukannya. Kelas diturunkan dari BaseEstimator yang menyediakan get_params dan set_params, sehingga objeknya dapat digandakan oleh cross-validation dan ditala lewat nama parameternya, serta dari TransformerMixin yang memberikan fit_transform secara otomatis.
Contoh berikut menuliskan transformer yang menambahkan kolom rasio antara dua fitur. Parameternya berupa indeks kolom pembilang dan penyebut, dan fit tidak mempelajari apa pun karena rasio dihitung per baris. Kelasnya diuji di dalam pipeline tiga langkah yang juga memuat scaling dan model.
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
class RasioTransformer(BaseEstimator, TransformerMixin):
"""Transformer yang menambahkan kolom rasio dua fitur."""
def __init__(self, indeks_pembilang=0, indeks_penyebut=1):
self.indeks_pembilang = indeks_pembilang
self.indeks_penyebut = indeks_penyebut
def fit(self, X, y=None):
# transformer ini tidak mempelajari parameter apa pun dari data
return self
def transform(self, X):
X = np.asarray(X, dtype=float)
rasio = X[:, [self.indeks_pembilang]] / X[:, [self.indeks_penyebut]]
return np.hstack([X, rasio])
# data sintetis: dua fitur positif, target bergantung pada rasionya
rng = np.random.default_rng(42)
n = 300
X = rng.uniform(0.5, 10.0, size=(n, 2))
rasio_sebenarnya = X[:, 0] / X[:, 1]
y = (rasio_sebenarnya + rng.normal(0, 0.4, n) > 1.5).astype(int)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
pipeline = make_pipeline(
RasioTransformer(indeks_pembilang=0, indeks_penyebut=1),
StandardScaler(),
LogisticRegression(max_iter=1000),
)
pipeline.fit(X_train, y_train)
print("Bentuk data sebelum transform:", X_train.shape)
print("Bentuk data sesudah transform:", pipeline[:-1].transform(X_train).shape)
print(f"Akurasi test: {pipeline.score(X_test, y_test):.3f}")Bentuk data sebelum transform: (225, 2)
Bentuk data sesudah transform: (225, 3)
Akurasi test: 0.840Keluaran menegaskan dua hal. Pertama, bentuk data bertambah satu kolom dari (225, 2) menjadi (225, 3) setelah transformer pertama bekerja, dan pemeriksaannya menggunakan pemotongan pipeline[:-1] untuk menjalankan praproses tanpa model. Kedua, model yang menerima kolom rasio mencapai akurasi 0.840 pada target yang memang dibangkitkan dari rasio tersebut, sedangkan StandardScaler di belakangnya memastikan skala ketiga fitur itu setara bagi LogisticRegression.
Perhatikan bahwa nilai parameter disimpan apa adanya pada __init__ tanpa dihitung di sana. Konvensi scikit-learn menuntut __init__ hanya menyalin parameter, sedangkan hasil pembelajaran dari data disimpan sebagai atribut pada fit. Konvensi lain menandai atribut hasil pembelajaran dengan garis bawah di akhir namanya, seperti mean_ pada StandardScaler.
Fungsi bernama, bukan lambda#
Objek model dan transformer disimpan ke berkas melalui serialisasi, yaitu proses menyimpan objek sebagai deretan byte yang dapat dimuat kembali, baik oleh pickle bawaan Python maupun joblib. Mekanisme keduanya tidak menyimpan kode fungsinya, melainkan hanya referensi nama modul dan nama fungsinya, lalu mencari fungsi tersebut kembali pada saat pemuatan. Fungsi bernama yang didefinisikan di tingkat modul karenanya ditemukan kembali dengan mulus.
import pickle
import numpy as np
from sklearn.preprocessing import FunctionTransformer
def pangkat_dua(X):
return X ** 2
# fungsi bernama dapat dicari pickle berdasarkan namanya
ft = FunctionTransformer(pangkat_dua)
data = pickle.dumps(ft)
ft_baru = pickle.loads(data)
print(ft_baru.func.__name__, ft_baru.transform(np.array([[3.0]])))pangkat_dua [[9.]]Keluaran menunjukkan objek hasil pemuatan tetap berfungsi dan fungsinya masih dapat dikenali namanya. Lambda tidak mendapat perlakuan yang sama karena lambda tidak memiliki nama modul dan nama yang dapat dirujuk, sehingga pickle.dumps(FunctionTransformer(lambda x: x)) gagal dengan error PicklingError. Kesalahan ini tidak terlihat selama pengujian dan baru muncul pada tahap penyimpanan model, padahal transformasinya sudah bekerja sejak awal. Perbaikannya selalu sama, yaitu menuliskan fungsi tersebut sebagai fungsi bernama.
Kesalahan umum#
- Lupa mengembalikan
selfpada fit. Metode fit yang mengembalikanNonemembuat rantaiPipelinegagal karena hasil fit tidak bisa diteruskan ke langkah berikutnya. Perbaikannya dengan menutup fit dengan pernyataanreturn self. - Mengubah masukan pada transform.
transformyang mengisi kolom pada array yang diberikan mengubah data milik pemanggilnya dan menyebabkan hasil yang bergantung urutan eksekusi. Perbaikannya dengan mengembalikan array baru, misalnya melaluinp.hstack. - Menghitung statistik dari masukan pada transform. Rata-rata yang dihitung dari batch yang sedang diproses menghasilkan transformasi yang berbeda di setiap panggilan. Perbaikannya dengan memindahkan perhitungan statistik ke fit, menyimpannya sebagai atribut, dan memakainya pada transform.
Ringkasan#
FunctionTransformermembungkus fungsi menjadi transformer stateless, dannp.log1pmenjadi contohnya untuk data yang condong ke kanan.- Transformer kustom diturunkan dari
BaseEstimatordanTransformerMixin, mengimplementasikan fit yang mengembalikanselfserta transform yang mengembalikan array baru. __init__hanya menyalin parameter, sedangkan hasil pembelajaran dari data disimpan pada fit sebagai atribut berakhiran garis bawah.- Fungsi pada
FunctionTransformerharus berupa fungsi bernama agar objeknya dapat diserialisasi denganpickleataujoblib.
Referensi#
- sklearn.preprocessing.FunctionTransformer — scikit-learn — Dokumentasi pembungkus fungsi menjadi transformer beserta parameter validate dan inverse_func.
- Pipelines and composite estimators — scikit-learn — Panduan resmi yang memuat bagian menulis transformer kustom di atas BaseEstimator dan TransformerMixin.
- Glossary of Common Terms and API Elements — scikit-learn — Definisi resmi kontrak fit, transform, dan istilah stateless yang digunakan pada topik ini.