Mert Özen Her Satırda Daha İleri
Yapay Zeka Makine Öğrenmesine Giriş — #05

Feature Engineering: Normalization, One-Hot Encoding, Text Featurization ve Eksik Değerler

Mert Özen 15 Kas 2026 14 dk 8 görüntülenme
Feature Engineering: Normalization, One-Hot Encoding, Text Featurization ve Eksik Değerler

Feature engineering, ham kolonları modelin kullanabileceği sayılara çevirmek ve iş bilgisini bu sayılara taşımaktır; Bookstore teslim süresinde en büyük kazanç algoritmadan değil, sipariş saatinden türetilen tek bir domain feature'ından geldi. Normalization'ın SDCA'yı nasıl kurtardığını, one-hot ve hashing'i, CustomMapping ile tarih feature'larını, FeaturizeText'i, eksik değerleri ve training-serving skew'i cross-validation ölçümleriyle gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.ML 6.0.0 (ML.NET 6.0) ve Microsoft.ML.FastTree 6.0.0 ile derlenip çalıştırılmıştır. Veri, serinin seed'li generator'ıyla üretilmiş sentetik Bookstore verisidir.

Önce Tahmin Et

Önceki yazıda sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için teslim süresini SDCA (Stochastic Dual Coordinate Ascent; linear modeller için bir optimizasyon yöntemi) ile tahmin eden bir linear model eğittik ve feature'ları NormalizeMinMax ile 0–1 aralığına çektik. Normalization'ı çıkarırsan hata ne kadar artar? Feature listesine sipariş tutarını (OrderTotal, 150 ile 40 bin TL arasında bir sayı) normalization olmadan eklersen ne olur? Peki aynı deneyi ağaç tabanlı FastTree ile yaparsan?

Bu yazıda feature'ları grup grup ekleyip her adımı cross-validation ile ölçüyoruz. Cevaplar ilk tabloda; bazıları beklediğin gibi çıkmayacak.

Ölçüm Düzeneği

Her deneyde aynı şeyi yapıyoruz: bir feature grubu ekliyor, pipeline'ı hem SDCA hem FastTree ile kuruyor ve müşteriye göre gruplanmış 5-fold cross-validation'ın ortalama MAE'sini (ortalama mutlak hata, gün cinsinden) yazdırıyoruz. Yapı taşları küçük method'lar:

/// <summary>04. yazının kolonları: one-hot kategoriler, 0/1 boolean'lar, iki sayı.</summary>
private static IEstimator<ITransformer> Base() =>
    Ml.Transforms.Categorical.OneHotEncoding(
            [new("CarrierKey", "Carrier"), new("MethodKey", "ShippingMethod"), new("WarehouseKey", "Warehouse")])
        .Append(Ml.Transforms.Conversion.ConvertType(
            [new("Campaign", "IsCampaign"), new("Backorder", "HasBackorderItem")], DataKind.Single));
private static readonly string[] BaseColumns =
    ["CarrierKey", "MethodKey", "WarehouseKey", "Campaign", "Backorder", "DistanceKm", "ItemCount"];
public static IEstimator<ITransformer> OrderTime() =>
    Ml.Transforms.CustomMapping<OrderTimeInput, OrderTimeFeatures>(OrderTimeMappingFactory.Map, "OrderTime")
        .Append(Ml.Transforms.Categorical.OneHotEncoding("LocalDayKey", "LocalDay"));
public static IEstimator<ITransformer> Weight() =>
    Ml.Transforms.IndicateMissingValues("WeightMissing", "TotalWeightKg")
        .Append(Ml.Transforms.Conversion.ConvertType("WeightMissing", outputKind: DataKind.Single))
        .Append(Ml.Transforms.ReplaceMissingValues("Weight", "TotalWeightKg",
            MissingValueReplacingEstimator.ReplacementMode.Mean));
public static IEstimator<ITransformer> Note() =>
    Ml.Transforms.Text.FeaturizeText("NoteFeatures", "CustomerNote");
// Her adım aynı seed'le yeni bir MLContext alır; adımlar yalnız feature'larıyla birbirinden ayrılır.
void Step(string name, Func<IEstimator<ITransformer>> features, string[] columns, string normalize = "MinMax", bool tree = true)
{
    Ml = new MLContext(seed: 1);
    IEstimator<ITransformer> f = features().Append(Ml.Transforms.Concatenate("Features", columns));
    if (normalize == "MinMax") f = f.Append(Ml.Transforms.NormalizeMinMax("Features"));
    if (normalize == "MeanVariance") f = f.Append(Ml.Transforms.NormalizeMeanVariance("Features"));
    string sdca = Cv(data, f.Append(Sdca())).ToString("F3");
    string ft = tree ? Cv(data, f.Append(Tree())).ToString("F3") : "  -  ";
    Console.WriteLine($"{name,-44} SDCA {sdca}   FastTree {ft}");
}
private static double Cv(IDataView data, IEstimator<ITransformer> pipeline) =>
    Ml.Regression.CrossValidate(data, pipeline, numberOfFolds: 5, labelColumnName: "DeliveryDays",
        samplingKeyColumnName: "CustomerId", seed: 7).Average(f => f.Metrics.MeanAbsoluteError);

Her adımın yeni bir MLContext almasının nedeni şu: aynı context'i kullanan ardışık deneyler, context'in rastgele sayı üretecini sırayla tükettiği için birbirinden etkileniyor. İlk denememde aynı konfigürasyon tablonun farklı satırlarında 0,850 ve 0,855 gibi iki farklı sonuç vermişti. Her adım kendi context'iyle başlayınca adımlar arasındaki fark yalnız feature'lardan geliyor. Tablonun tamamı şu:

dotnet run --project src/Bookstore.ML.Lab -- features
04 columns, no normalization                 SDCA 0.839   FastTree 0.850
04 columns, NormalizeMinMax                  SDCA 0.844   FastTree 0.850
04 columns, NormalizeMeanVariance            SDCA 0.841   FastTree   -  
04 columns + OrderTotal, no normalization    SDCA 4.729   FastTree 0.862
04 columns + OrderTotal, NormalizeMinMax     SDCA 0.844   FastTree 0.862
+ City (one-hot, 16)                         SDCA 0.849   FastTree 0.850
+ District (one-hot)                         SDCA 0.838   FastTree 0.844
+ District (hash, 32 slots)                  SDCA 0.833   FastTree 0.847
+ LocalDay, LocalHour                        SDCA 0.637   FastTree 0.541
+ HoursUntilProcessing                       SDCA 0.525   FastTree 0.531
+ CustomerNote (FeaturizeText)               SDCA 0.519   FastTree 0.530
+ TotalWeightKg (mean + missing flag)        SDCA 0.520   FastTree 0.535

Tablo yukarıdan aşağı okunuyor: her satır bir öncekinin feature'larını içeriyor, normalization satırları hariç. Şimdi grup grup bakalım.

Numeric Feature'lar ve Normalization

İlk üç satır sürpriz: önceki yazının yedi kolonunda normalization SDCA'yı neredeyse hiç etkilemiyor (0,839, 0,844, 0,841). Mesafe 20 ile 1.600 arasında, kitap sayısı 1 ile 120 arasında; geri kalanı 0 ve 1. SDCA bu ölçek farkıyla başa çıkabiliyor.

Dördüncü satırda tablo değişiyor. Aynı kolonlara OrderTotal'ı normalization olmadan ekleyince SDCA'nın hatası 4,729 güne fırlıyor; model ortalama tahmin etmekten bile kötü. Aynı kolonlar NormalizeMinMax'ten geçince hata yine 0,844. Linear modellerin optimizasyonu her feature'ın weight'ini adım adım günceller. Bir feature'ın değerleri diğerlerinden on binlerce kat büyükse bu adımlar dengesizleşir ve model ya çok yavaş ya da hiç yakınsamaz. Normalization bütün feature'ları benzer aralığa çekerek bunu önler: NormalizeMinMax 0–1 aralığına, NormalizeMeanVariance ortalaması 0, standart sapması 1 olacak şekilde.

FastTree'nin sütununa bak: normalization olsun olmasın 0,850 ve 0,862. Karar ağaçları "mesafe 650'den büyük mü?" gibi eşik sorularıyla bölünür; bir kolonun bütün değerlerini aynı oranda büyütmek eşiğin yerini değiştirir ama hangi siparişin hangi tarafa düştüğünü değiştirmez. Bu yüzden ağaç tabanlı modeller ölçekten etkilenmez, linear modeller, k-means gibi mesafe tabanlı algoritmalar ve neural network'ler etkilenir. Pratik kural: pipeline'a normalization'ı her durumda koy, ağaç tabanlı modellerde bir zararı yok.

Categorical: One-Hot ve Hashing

Şehri eklemek iki modelde de bir şey kazandırmıyor (0,849 ve 0,850). Şaşırtıcı değil: şehir ile depo arasındaki mesafe zaten bir feature, şehrin kendisi ek bilgi taşımıyor.

İlçe farklı bir durum. 312 farklı ilçe var ve one-hot encoding her biri için ayrı bir slot açıyor. SDCA'da hata 0,838'e, FastTree'de 0,844'e iniyor; bazı ilçeler sistematik olarak daha geç teslimat alıyor ve model bunu yakalıyor. Bu kadar çok değeri olan kolonlara yüksek cardinality'li denir ve iki sorunları vardır: feature vektörünü büyütürler ve az örneği olan değerler için model güvenilir bir şey öğrenemez. Alternatif hashing:

Step("+ District (hash, 32 slots)", () => City().Append(
    Ml.Transforms.Categorical.OneHotHashEncoding("DistrictKey", "District", numberOfBits: 5)), district);

OneHotHashEncoding her ilçe adını bir hash fonksiyonundan geçirip numberOfBits: 5 ile açılan 32 slottan birine yerleştiriyor. Farklı ilçeler aynı slota düşebiliyor (collision). Buna rağmen SDCA'da hata 0,833 ile one-hot'tan biraz daha iyi, FastTree'de 0,847 ile biraz daha kötü. Vektör 312 slottan 32'ye indi, kayıp neredeyse yok. Hashing'in bir avantajı daha var: eğitimde hiç görülmemiş yeni bir ilçe geldiğinde one-hot onu tamamen yok sayar, hashing ise bir slota yerleştirir. Yayınevi, şehir-ilçe çifti, kitap ISBN'i (uluslararası kitap numarası) gibi sürekli büyüyen kategoriler için hashing iyi bir varsayılandır.

Tarih ve Saat: Domain Bilgisiyle Feature

Asıl büyük sıçrama tarihten geliyor. Ham OrderedAtUtc değerini modele vermek işe yaramaz. Ama bir siparişin hangi gün ve saatte verildiğini bilmek çok şey anlatır. Önce UTC'den (eşgüdümlü evrensel saat) yerel saate, yani Türkiye saatine (UTC+3) çevirip haftanın gününü ve saati çıkarıyoruz. Bu dönüşümü pipeline'ın içinde bir CustomMapping olarak yazıyoruz:

public sealed class OrderTimeFeatures
{
    public string LocalDay { get; set; } = "";
    public float LocalHour { get; set; }
    /// <summary>Deponun siparişe başlayabileceği ana kadar geçen saat (15:00 kesim saati, hafta sonu vardiya yok).</summary>
    public float HoursUntilProcessing { get; set; }
}
[CustomMappingFactoryAttribute("OrderTime")]
public sealed class OrderTimeMappingFactory : CustomMappingFactory<OrderTimeInput, OrderTimeFeatures>
{
    private static readonly TimeSpan TurkeyOffset = TimeSpan.FromHours(3);
    public static void Map(OrderTimeInput input, OrderTimeFeatures output)
    {
        DateTime local = input.OrderedAtUtc + TurkeyOffset;
        output.LocalDay = local.DayOfWeek.ToString()[..3];
        output.LocalHour = local.Hour + local.Minute / 60f;
        DateTime start = local.Hour >= 15 ? local.Date.AddDays(1).AddHours(9) : local;
        while (start.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
        {
            start = start.Date.AddDays(1).AddHours(9);
        }
        output.HoursUntilProcessing = (float)(start - local).TotalHours;
    }
    public override Action<OrderTimeInput, OrderTimeFeatures> GetMapping() => Map;
}

Tablodaki "+ LocalDay, LocalHour" satırı hatayı SDCA'da 0,838'den 0,637'ye, FastTree'de 0,844'ten 0,541'e indiriyor. Bu serinin en büyük iyileşmesi. FastTree'nin farkı açması da anlamlı: "cuma ve saat 15'ten sonra" gibi iki feature'ın birlikte etkisini ağaçlar bölünmelerle kendiliğinden öğrenir, linear model ise her feature'a ayrı bir weight verdiği için bu etkileşimi yakalayamaz.

Üçüncü feature, HoursUntilProcessing, farklı türden bir bilgi. Depo ekibine sorduğumuzda iki kural öğrendiğimizi düşün: saat 15:00'ten sonra gelen sipariş ertesi sabaha kalıyor, hafta sonu depo çalışmıyor. Bu iki kuralı kullanarak siparişin işleme başlamadan önce kaç saat bekleyeceğini hesaplıyoruz. Etkisine bak: SDCA'da hata 0,637'den 0,525'e iniyor ve linear model ağacı yakalıyor. Ağaçta ise kazanç küçük (0,531), çünkü ağaç bu etkileşimi zaten gün ve saatten öğrenmişti. Feature engineering'in en değerli hâli budur: iş tarafının bildiği bir kuralı modelin doğrudan kullanabileceği tek bir sayıya çevirmek. Bu feature, basit ve hızlı bir modeli karmaşık bir modelin seviyesine taşıdı.

Kampanya haftası (IsCampaign) da aynı türden bir feature; kampanya takvimini pazarlama ekibi bilir. Tatil günleri, ay sonu, maaş günü gibi takvim bilgileri de çoğu zaman tarih kolonundan daha değerlidir.

Text: FeaturizeText ve Türkçe Metin

Müşterinin teslimat notu serbest metin. FeaturizeText metni normalize ediyor, kelimelere ve karakter üçlülerine bölüyor ve her biri için bir slot açıyor. Bir notun hangi slotlara düştüğüne bakalım:

// Tek bir not için birkaç text feature'ı ve değerleri
var note = Note().Fit(data);
IDataView one = note.Transform(Ml.Data.LoadFromEnumerable([new DeliveryInput { CustomerNote = "office address - weekdays only" }]));
VBuffer<ReadOnlyMemory<char>> slots = default;
one.Schema["NoteFeatures"].GetSlotNames(ref slots);
float[] values = one.GetColumn<float[]>("NoteFeatures").First();
string[] names = [.. slots.DenseValues().Select(s => s.ToString())];
Console.WriteLine("non-zero text features for 'office address - weekdays only':");
foreach ((string n, float v) in names.Zip(values).Where(x => x.Second > 0 && x.First.StartsWith("Word.")))
{
    Console.WriteLine($"  {n,-22} {v:F3}");
}
Console.WriteLine("stop word languages: " +
    string.Join(", ", Enum.GetNames<StopWordsRemovingEstimator.Language>()));
dotnet run --project src/Bookstore.ML.Lab -- inspect
non-zero text features for 'office address - weekdays only':
  Word.-                 0.333
  Word.office            0.333
  Word.office|address    0.333
  Word.address           0.333
  Word.address|-         0.333
  Word.-|weekdays        0.333
  Word.weekdays          0.333
  Word.weekdays|only     0.333
  Word.only              0.333
stop word languages: English, French, German, Dutch, Danish, Swedish, Italian, Spanish, Portuguese, Portuguese_Brazilian, Norwegian_Bokmal, Russian, Polish, Czech, Arabic, Japanese

"office address - weekdays only" notu dokuz kelime feature'ına dönüştü: tek kelimeler ve yan yana gelen kelime çiftleri (n-gram), her biri aynı ağırlıkta. Bir de karakter üçlüleri var; onları listeden süzdük. Bu notun teslimat süresine etkisi gerçek: ofis adresine hafta sonu teslimat yapılamıyor. Tabloda text feature'ı SDCA'yı 0,525'ten 0,519'a, FastTree'yi 0,531'den 0,530'a götürüyor. Küçük ama tutarlı bir kazanç.

Çıktının son satırı Türkçe metinle çalışacaksan önemli: ML.NET'in hazır stop word listesi (anlam taşımayan "ve", "bir" gibi kelimeler) olan diller arasında Türkçe yok. ML.NET'te stemming, yani kelimeyi köküne indirme de yok. Türkçede "kargo", "kargom", "kargoya", "kargodan" dört ayrı kelime olarak sayılır ve az veride model bunları birbirine bağlayamaz. Karakter üçlüleri bu sorunu kısmen hafifletir, çünkü dört kelime de "kar", "arg", "rgo" parçalarını paylaşır. Daha güçlü bir alternatif, metni bir embedding modeliyle anlamını taşıyan bir vektöre çevirip feature olarak kullanmak. Embedding'in ne olduğunu LLM Temelleri serisinin "LLM'lerde Embedding ve Benzerlik" yazısında gördük; burada yalnız fikri not ediyorum.

Eksik Değerler

Siparişlerin yaklaşık yüzde 3'ünde ağırlık boş. Önceki yazıdan biliyoruz ki MissingRealsAsNaNs = true ile bu değerler NaN olarak geliyor. Peki NaN bir feature'ı olduğu gibi SDCA'ya verirsek ne olur?

// ReplaceMissingValues olmadan eksik ağırlık: satırlara ve tahmine ne olur?
var raw = Ml.Transforms.Concatenate("Features", "DistanceKm", "ItemCount", "TotalWeightKg")
    .Append(Ml.Regression.Trainers.Sdca(new SdcaRegressionTrainer.Options
    {
        LabelColumnName = "DeliveryDays", NumberOfThreads = 1,
    }));
ITransformer rawModel = raw.Fit(data);
var rawEngine = Ml.Model.CreatePredictionEngine<DeliveryInput, DeliveryPrediction>(rawModel);
Console.WriteLine($"weight 1.2 kg -> {rawEngine.Predict(new DeliveryInput { DistanceKm = 450, ItemCount = 2, TotalWeightKg = 1.2f }).PredictedDays:F2} days, " +
                  $"weight missing -> {rawEngine.Predict(new DeliveryInput { DistanceKm = 450, ItemCount = 2, TotalWeightKg = float.NaN }).PredictedDays}");
float[] scores = rawModel.Transform(data).GetColumn<float>("Score").ToArray();
Console.WriteLine($"rows scored NaN in the training data: {scores.Count(float.IsNaN)} of {scores.Length}");
weight 1.2 kg -> 2.09 days, weight missing -> NaN
rows scored NaN in the training data: 720 of 23105

Model eğitimde sorun çıkarmadı, ama ağırlığı bilinmeyen bir sipariş için tahmin NaN. Train verisindeki 720 satırın skoru da NaN. Bu model üretimde ağırlığı eksik her siparişte ekrana "NaN gün" yazdırır. Çözüm Weight() builder'ında: ReplaceMissingValues eksik değeri train verisinden öğrenilen ortalamayla dolduruyor, IndicateMissingValues ise "bu değer aslında eksikti" bilgisini ayrı bir 0/1 kolonunda saklıyor. Eksikliğin kendisi bir sinyal olabilir; örneğin ağırlık hep belli bir depodan eksik geliyorsa model bunu bu kolondan öğrenir.

Tablonun son satırı bu veri için dürüst bir cevap veriyor: ağırlığı eklemek bir şey kazandırmıyor (SDCA 0,520, FastTree 0,535). Ağırlığın teslim süresine etkisi bu veride çok zayıf. Bir feature'ı eklemek için "elimizde var" yetmez, ölçüm onu desteklemeli.

Feature Sayısı ve Overfitting

Bütün grupları eklediğimizde feature vektörünün ne kadar büyüdüğüne bakalım:

// Her feature grubu kaç slot genişliğinde?
ITransformer full = Full(Sdca()).Fit(data);
IDataView t = full.Transform(data);
foreach (string c in new[] { "CityKey", "DistrictKey", "LocalDayKey", "NoteFeatures", "Features" })
{
    Console.WriteLine($"{c,-14} {t.Schema[c].Type}");
}
CityKey        Vector<Single, 16>
DistrictKey    Vector<Single, 312>
LocalDayKey    Vector<Single, 7>
NoteFeatures   Vector<Single, 173>
Features       Vector<Single, 524>

Başta 7 kolonla başladık, şimdi 524 slotumuz var: 312'si ilçe, 173'ü teslimat notu. 23 bin satırlık bir veri için bu henüz sorun değil, ama oran kötüleştikçe üçüncü yazıdaki overfitting eğrisinin sağ tarafına kayarsın. Her yeni feature grubu modele yeni bir ezber imkânı da verir. Az örnekli ilçeler, nadir geçen kelimeler, tek bir müşteriye özgü bir değer. Feature eklerken iki şeyi birlikte izle: cross-validation hatası düşüyor mu ve fold'lar arasındaki fark büyüyor mu?

Training-Serving Skew

Bütün parçaları tek bir pipeline'da topluyoruz. Altıncı yazıda farklı trainer'ları bu pipeline üzerinde karşılaştıracağız:

public static IEstimator<ITransformer> Full(IEstimator<ITransformer> trainer) =>
    Base()
        .Append(Ml.Transforms.Categorical.OneHotEncoding([new("CityKey", "City"), new("DistrictKey", "District")]))
        .Append(OrderTime())
        .Append(Note())
        .Append(Weight())
        .Append(Ml.Transforms.Concatenate("Features", [.. BaseColumns, "CityKey", "DistrictKey", "LocalDayKey", "LocalHour",
            "HoursUntilProcessing", "NoteFeatures", "Weight", "WeightMissing"]))
        .Append(Ml.Transforms.NormalizeMinMax("Features"))
        .Append(trainer);

Bu yapının en önemli faydası, feature'ların eğitimde ve tahminde aynı kodla hesaplanması. Bunun tersine training-serving skew denir: model bir şekilde hesaplanan feature'larla eğitilir, üretimde başka bir şekilde hesaplanan feature'larla çalışır. En sık görülen biçimi şudur. Veri bilimci feature'ları eğitimde bir script'le hesaplar, API'yi yazan geliştirici aynı mantığı kendi kodunda yeniden yazar ve küçük bir fark kaçar. Bir örnekle görelim. Pipeline'ı kaydedip yeni bir context'le yüklüyor, aynı siparişi iki kez tahmin ediyoruz; ikincisinde sistemin başka bir yerindeki bir bug yerel saati UTC'ymiş gibi gönderiyor:

// Zip, mapping'in contract adını ve assembly adını saklar; o assembly modelle birlikte deploy edilebilmeli
MLContext fresh = new();
ITransformer loaded = fresh.Model.Load("models/delivery-features.zip", out _);
var engine = fresh.Model.CreatePredictionEngine<DeliveryInput, DeliveryPrediction>(loaded);
// İstanbul'da perşembe 14:10 = 11:10 UTC. Başka bir yerdeki bug yerel saati UTC'ymiş gibi gönderiyor.
DeliveryInput order = new()
{
    OrderedAtUtc = new DateTime(2026, 10, 15, 11, 10, 0, DateTimeKind.Utc),
    City = "Ankara", District = "ANK-07", Warehouse = "ANK-1", DistanceKm = 20, Carrier = "CarrierA",
    ShippingMethod = "Standard", ItemCount = 1, TotalWeightKg = 0.6f, CustomerNote = "",
};
float correct = engine.Predict(order).PredictedDays;
order.OrderedAtUtc = new DateTime(2026, 10, 15, 14, 10, 0, DateTimeKind.Utc);
float skewed = engine.Predict(order).PredictedDays;
Console.WriteLine($"ordered 14:10 local, sent as UTC 11:10 : {correct:F2} days");
Console.WriteLine($"same order, local time sent as UTC      : {skewed:F2} days");
dotnet run --project src/Bookstore.ML.Lab -- skew
ordered 14:10 local, sent as UTC 11:10 : 1.27 days
same order, local time sent as UTC      : 2.07 days

Ankara'da perşembe 14:10'da verilen bir sipariş için doğru tahmin 1,27 gün. Aynı sipariş, saati üç saat kaymış hâlde gelince 2,07 gün; çünkü model artık siparişin 15:00'ten sonra verildiğini düşünüyor. Hiçbir exception yok, tahmin makul bir sayı ve yanlış. Saat dilimi dönüşümünü pipeline'ın içine koymak bu sınıftaki hataların yarısını önlüyor: API'nin elle bir feature hesaplaması gerekmiyor. Diğer yarısı, modele giden ham verinin kendisinin doğru olması, sözleşme ve test meselesi.

CustomMapping'in bir koşulu var. Modeli kaydedebilmek için mapping'e bir contract adı ("OrderTime") ve [CustomMappingFactoryAttribute] ile işaretli bir factory class'ı vermek gerekiyor. ML.NET'in kaynak koduna baktığımda zip dosyasının bu contract adını ve factory'nin bulunduğu assembly'nin adını sakladığını, yüklerken o assembly'yi kendisi yükleyip kaydettiğini gördüm. Bu yüzden yeni context'te ayrıca RegisterAssembly çağırmadan model yüklendi. Ama modeli kullanan uygulama bu assembly'yi referans etmek zorunda. Factory class'ını, modeli hem eğiten hem servis eden projelerin paylaştığı küçük bir library'ye koymak bu yüzden iyi bir fikir. Normalization ve eksik değer doldurma gibi adımlar da aynı nedenle pipeline'ın içinde: min, max ve ortalama değerleri modelin zip dosyasında saklanıyor ve üçüncü yazıdaki gibi yalnız train verisinden öğreniliyor.

Sık Düşülen Tuzaklar

İlk tuzak, normalization'ı split'ten önce bütün veriye uygulamak. Min ve max test verisinden de öğrenilirse küçük bir sızıntı oluşur; pipeline'ın içinde kalan normalization bunu kendiliğinden önler.

İkincisi, ölçek farkını görmezden gelmek. Bu yazıda tek bir normalization edilmemiş kolon SDCA'nın hatasını 0,84'ten 4,73'e çıkardı. Linear bir modelin sonucu anlamsızsa önce feature ölçeklerine bak.

Üçüncüsü, eğitimde ve tahminde farklı preprocessing. Feature'ları API'de elle tekrar hesaplama; dönüşümleri pipeline'a koy ve modeli pipeline'ıyla birlikte kaydet.

Dördüncüsü, eksik değerleri hiç düşünmemek. NaN bir feature tahmini sessizce NaN yapar; varsayılan loader ise eksik değeri 0'a çevirir. İkisi de exception fırlatmaz.

Beşincisi, ölçmeden feature eklemek. Ağırlık ve şehir bu veride hiçbir şey kazandırmadı. Her feature'ın bir bakım maliyeti var: veri kaynağı, eksik değer politikası, izleme.

Küçük Bir Deneme

★☆☆ İlçe için hashing'i numberOfBits: 3 (8 slot) ve numberOfBits: 10 (1.024 slot) ile dene. Collision arttıkça hata nasıl değişiyor?

★★☆ OrderTimeMappingFactory'ye kampanyadan bağımsız bir feature ekle: siparişin ayın kaçıncı günü verildiği. Ölçüm bir şey kazandırıyor mu? Kazandırmıyorsa bu sana veri hakkında ne söylüyor?

★★★ HoursUntilProcessing'i Express siparişler için ayrı bir kesim saatiyle hesapla: Express siparişlerin 17:00'ye kadar aynı gün işlendiğini varsay. Hata düşüyor mu? Düşüyorsa iş tarafına bu kuralı doğrulat; düşmüyorsa varsayımın yanlış olabilir.

Bir sonraki yazıda bu pipeline'ı sabit tutup trainer'ları değiştireceğiz: linear regression'ın nasıl öğrendiğini küçük bir gradient descent simülasyonuyla görecek, SDCA, FastTree, FastForest ve LightGBM'i aynı veride karşılaştıracak ve MAE, RMSE (hataların karelerinin ortalamasının karekökü) ve R²'nin her birinin neyi ölçtüğünü, residual analizini, outlier'ların RMSE'ye etkisini ve extrapolation'ı konuşacağız.