İlk ML.NET Modeli: MLContext, IDataView, Pipeline ve Teslim Süresi Tahmini
Bir ML.NET modeli, MLContext üzerinde kurulan ve Fit ile train verisinden öğrenen bir estimator zinciridir; sonuç, kaydedilip yüklenebilen tek bir transformer'dır. Bookstore teslim süresi için input class'ı, lazy IDataView'ı, boş hücrelerin varsayılan olarak 0 okunması tuzağını, SDCA ile ilk regression modelini, PredictionEngine ile tek tahmini, modeli .zip olarak saklamayı ve float/double, kolon adı ve thread-safety hatalarını gerçek çıktılarla 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
İki sorum var. Birincisi: ML.NET'te LoadFromTextFile çağrısı döndüğünde 24 bin satırlık CSV (virgülle ayrılmış düz metin) dosyası belleğe okunmuş olur mu? İkincisi: dosyada boş bırakılmış bir sayı hücresi, örneğin iptal edilmiş bir siparişin teslim süresi, ML.NET'e hangi değerle girer?
İlk sorunun cevabını bilmek performans açısından önemli. İkincisinin cevabını bilmemek ise sessizce yanlış bir model eğitmek demek. Bu yazıda bir önceki yazıda kutu olarak kullandığımız pipeline'ı açıyoruz ve sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için ilk ML.NET modelini adım adım kuruyoruz.
Proje ve Paketler
Serinin bütün deneyleri Bookstore.ML.Lab adlı bir .NET 10 console uygulamasında duruyor. ML.NET'in çekirdeği Microsoft.ML paketi; ağaç tabanlı trainer'lar (FastTree, FastForest) ayrı bir pakette. Bu yazıyı yazdığım gün NuGet'teki son kararlı sürüm 6.0.0, 11 Kasım 2026'da yayımlanmış:
dotnet add src/Bookstore.ML.Lab package Microsoft.ML --version 6.0.0
dotnet add src/Bookstore.ML.Lab package Microsoft.ML.FastTree --version 6.0.0
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Analysis" Version="0.24.0" />
<PackageReference Include="Microsoft.ML" Version="6.0.0" />
<PackageReference Include="Microsoft.ML.FastTree" Version="6.0.0" />
</ItemGroup>
</Project>
Her ML.NET kodu bir MLContext ile başlar. Veri yükleme, dönüşümler, trainer'lar, değerlendirme ve model kaydetme hep bu nesnenin altındaki catalog'lardan çağrılır: ml.Data, ml.Transforms, ml.Regression, ml.Model. Constructor'a verilen seed, karıştırma ve rastgele başlangıç değeri kullanan her işlemin tekrarlanabilir olmasını sağlar. Ama aşağıda göreceğimiz gibi tek başına yetmez.
Input Class'ı ve [LoadColumn]
ML.NET'e verinin şeklini bir C# class'ı ile anlatıyoruz. Her property bir kolon; [LoadColumn] attribute'u CSV'deki kaçıncı kolondan okunacağını söylüyor. Tahminin okunacağı output class'ı ise trainer'ın ürettiği Score kolonunu kendi property'sine bağlıyor:
/// <summary>data/orders_clean.csv'de teslim süresi modellerinin kullandığı kolonlar.</summary>
public class DeliveryInput
{
[LoadColumn(0)] public string OrderId { get; set; } = "";
[LoadColumn(1)] public string CustomerId { get; set; } = "";
[LoadColumn(2)] public DateTime OrderedAtUtc { get; set; }
[LoadColumn(3)] public string City { get; set; } = "";
[LoadColumn(4)] public string District { get; set; } = "";
[LoadColumn(5)] public string Warehouse { get; set; } = "";
[LoadColumn(6)] public float DistanceKm { get; set; }
[LoadColumn(7)] public string Carrier { get; set; } = "";
[LoadColumn(8)] public string ShippingMethod { get; set; } = "";
[LoadColumn(9)] public float ItemCount { get; set; }
[LoadColumn(10)] public float TotalWeightKg { get; set; }
[LoadColumn(11)] public float OrderTotal { get; set; }
[LoadColumn(13)] public bool IsCampaign { get; set; }
[LoadColumn(14)] public string PaymentMethod { get; set; } = "";
[LoadColumn(15)] public bool HasBackorderItem { get; set; }
[LoadColumn(16)] public string CustomerNote { get; set; } = "";
[LoadColumn(17)] public bool IsCancelled { get; set; }
[LoadColumn(18)] public float DeliveryDays { get; set; }
[LoadColumn(19)] public float TrackingEventCount { get; set; }
}
public sealed class DeliveryPrediction
{
[ColumnName("Score")]
public float PredictedDays { get; set; }
}
Üç ayrıntıya dikkat et. Sayı kolonları float: ML.NET'in sayı tipi Single ve trainer'ların büyük kısmı double kabul etmiyor. Kolon adları property adlarından geliyor; pipeline'da kolonlara bu adlarla erişeceğiz. Ve LoadFromTextFile<T> class'taki her property için bir [LoadColumn] bekliyor. Önceki yazıdaki deneylerde bu class'a dosyada olmayan bir property eklediğimde yükleme şu hatayla durmuştu: Property 'CustomerAvgDays' is missing the LoadColumnAttribute attribute. Dosyada olmayan kolonları ayrı bir class'a koymak bu yüzden gerekli oldu.
IDataView: Lazy ve Schema'lı
Veriyi yükleyip ilk on kolonun schema'sına bakalım:
MLContext ml = new(seed: 1);
// 1) Yükleme lazy: biri satırları dolaşana kadar hiçbir şey okunmaz.
// MissingRealsAsNaNs: boş bir sayı 0 yerine NaN olur (varsayılan 0).
IDataView data = ml.Data.LoadFromTextFile<DeliveryInput>("data/orders_clean.csv", new TextLoader.Options
{
HasHeader = true,
Separators = [','],
MissingRealsAsNaNs = true,
});
Console.WriteLine($"row count known before reading? {data.GetRowCount()?.ToString() ?? "no (null)"}");
foreach (DataViewSchema.Column column in data.Schema.Take(10))
{
Console.WriteLine($" {column.Index,2} {column.Name,-16} {column.Type}");
}
dotnet run --project src/Bookstore.ML.Lab -- first-model
row count known before reading? no (null)
0 OrderId String
1 CustomerId String
2 OrderedAtUtc DateTime
3 City String
4 District String
5 Warehouse String
6 DistanceKm Single
7 Carrier String
8 ShippingMethod String
9 ItemCount Single
İlk satır açılıştaki birinci sorunun cevabı: GetRowCount() null döndü, çünkü dosya henüz okunmadı. IDataView bir tablo değil, tablonun nasıl okunacağının tarifi. Elinde schema var, yani kolon adları ve tipleri, ama satırlar ancak biri onları dolaşmaya başladığında okunuyor. Pipeline'ın her adımı da aynı şekilde çalışıyor: veriyi kopyalamıyor, bir öncekinin üstüne yeni kolonlar ekleyen bir görünüm üretiyor. Bu sayede belleğe sığmayacak kadar büyük bir dosyayla da çalışabilirsin. Bedeli şu: bir dönüşümdeki hata, veri gerçekten okunana kadar ortaya çıkmayabilir.
LINQ (C#'ta koleksiyonlar üzerinde sorgu yazma) serisini okuduysan bu davranış tanıdık gelecek. IEnumerable'daki deferred execution gibi, iş ancak sonuç istendiğinde yapılıyor.
Boş Hücre: 0 mı, NaN mı?
Teslim süresi modeli iptal edilen siparişleri kullanmamalı; onların label'ı boş. FilterRowsByMissingValues, belirtilen kolonu eksik (NaN) olan satırları atıyor:
// 2) İptal edilen siparişlerin label'ı boş (NaN): bu satırları çıkar
IDataView delivered = ml.Data.FilterRowsByMissingValues(data, "DeliveryDays");
Console.WriteLine($"rows with a label: {delivered.GetColumn<float>("DeliveryDays").Count():N0}");
DataOperationsCatalog.TrainTestData split = ml.Data.TrainTestSplit(
delivered, testFraction: 0.2, samplingKeyColumnName: "CustomerId", seed: 7);
rows with a label: 23,105
23.105 satır kaldı, beklediğimiz sayı. Ama bu sonuç yükleme kodundaki MissingRealsAsNaNs = true satırına bağlı. ML.NET dokümanına göre bu ayarın varsayılan değeri false ve o durumda boş bir sayı alanı 0 olarak okunuyor. Varsayılan loader ile aynı filtreyi denersen olan şu:
// 3) Boş hücreler: varsayılan loader onları eksik değil 0 olarak okur
IDataView labels = ml.Data.FilterRowsByMissingValues(data, "DeliveryDays");
float[] days = [.. labels.GetColumn<float>("DeliveryDays")];
float[] weights = [.. labels.GetColumn<float>("TotalWeightKg")];
Console.WriteLine($"[empty] default loader: {days.Length:N0} rows survive the filter, " +
$"{days.Count(d => d == 0):N0} have DeliveryDays = 0, {weights.Count(w => w == 0):N0} have weight 0");
dotnet run --project src/Bookstore.ML.Lab -- pitfalls
[empty] default loader: 24,180 rows survive the filter, 1,075 have DeliveryDays = 0, 756 have weight 0
Filtre hiçbir satırı atmadı. 1.075 iptal edilmiş sipariş "0 günde teslim edildi" olarak eğitime girdi; ağırlığı bilinmeyen 756 sipariş de "0 kilo" oldu. Açılıştaki ikinci sorunun cevabı bu. Ben de ilk denememde bu ayarı koymamıştım: aynı pipeline test setinde 0,934 gün MAE (ortalama mutlak hata) ve 0,167 R² vermişti ve test setinde de sıfır günlük sahte teslimatlar vardı. Hiçbir exception yok, hiçbir uyarı yok; yalnızca daha kötü bir model ve yanlış ölçülmüş bir metric. Boş değer içerebilecek her CSV'yi bu ayarla oku ve yükledikten sonra bir kolonun kaç tane 0 içerdiğini kontrol et.
Estimator, Transformer ve Pipeline
ML.NET'te iki temel kavram var. Estimator, veriden bir şey öğrenmesi gereken bir adımın tarifidir; henüz hiçbir şey öğrenmemiştir. Fit çağrıldığında veriyi okur ve bir transformer üretir. Transformer ise öğrenilmiş bir dönüşümdür; Transform ile veriye uygulanır. Pipeline, estimator'ları Append ile art arda bağlayan bir zincir:
// 3) Pipeline: estimator'lardan bir zincir, henüz hiçbir şey öğrenilmedi
var pipeline = 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))
.Append(ml.Transforms.Concatenate("Features",
"CarrierKey", "MethodKey", "WarehouseKey", "Campaign", "Backorder", "DistanceKm", "ItemCount"))
.Append(ml.Transforms.NormalizeMinMax("Features"))
.Append(ml.Regression.Trainers.Sdca(new SdcaRegressionTrainer.Options
{
LabelColumnName = "DeliveryDays",
FeatureColumnName = "Features",
NumberOfThreads = 1, // her çalıştırmada aynı sonuç
}));
Console.WriteLine($"pipeline: {pipeline.GetType().Name}");
// 4) Fit; one-hot sözlüklerini, min/max değerlerini ve weight'leri öğrenir
Stopwatch sw = Stopwatch.StartNew();
ITransformer model = pipeline.Fit(split.TrainSet);
Console.WriteLine($"model: {model.GetType().Name}, trained in {sw.ElapsedMilliseconds} ms");
IDataView scored = model.Transform(split.TestSet);
RegressionMetrics metrics = ml.Regression.Evaluate(scored, labelColumnName: "DeliveryDays");
Console.WriteLine($"test MAE {metrics.MeanAbsoluteError:F3} days, RMSE {metrics.RootMeanSquaredError:F3}, " +
$"R2 {metrics.RSquared:F3}");
pipeline: EstimatorChain`1
model: TransformerChain`1, trained in 1973 ms
test MAE 0.873 days, RMSE 1.830, R2 0.229
Zincirdeki adımlar sırayla şunu yapıyor. OneHotEncoding her kategorik kolonu, her farklı değer için bir slot açan bir vektöre çeviriyor; hangi değerlerin var olduğunu Fit sırasında train verisinden öğreniyor. ConvertType boolean kolonları 0 ve 1'e çeviriyor. Concatenate bütün feature'ları trainer'ın beklediği tek bir Features vektöründe topluyor. NormalizeMinMax her slotu 0–1 aralığına çekiyor; min ve max değerlerini yine train verisinden öğreniyor. Son adım trainer.
Trainer olarak SDCA'yı (Stochastic Dual Coordinate Ascent; linear modelleri büyük veride hızlı eğiten bir optimizasyon yöntemi) seçtik. Linear bir model, her feature'a bir weight verip bunların toplamıyla tahmin yapar. Ne öğrendiğini ve neden iyi ya da kötü çalıştığını altıncı yazıda ayrıntılı göreceğiz.
Çıktıdaki tip adlarına bak: pipeline bir EstimatorChain, Fit sonrası elimizdeki model bir TransformerChain. Model dediğimiz şey yalnız trainer'ın weight'leri değil, one-hot sözlükleri ve normalization değerleri dahil bütün zincir. Bu ayrım önemli; bir sonraki yazıda training-serving skew konusunda tekrar karşımıza çıkacak.
NumberOfThreads = 1 satırının da bir hikâyesi var. Bu satır olmadan aynı kodu üç kez çalıştırdığımda üç farklı sonuç aldım: 0,882, 0,883 ve 0,884. MLContext'e seed vermiş olmama rağmen SDCA birden çok thread'le çalışırken güncellemelerin sırası her seferinde değişiyor. Tek thread'le sonuç her çalıştırmada aynı: 0,873. Fark küçük, ama iki modeli karşılaştırırken 0,001'lik farkın modelden mi şanstan mı geldiğini bilmek istersin.
İlk Sonuçlar
Model test setinde ortalama 0,873 gün yanılıyor. Çıktıda iki metric daha var: RMSE (hataların karelerinin ortalamasının karekökü) ve R². Bunları altıncı yazıda ayrıntılı açacağız; R²'nin 0,229 gibi düşük bir değerde kalmasının büyük ölçüde haftalar sonra bulunan kayıp kargolardan kaynaklandığını orada ölçeceğiz. Karşılaştırma için: ilk yazıdaki kural 1,05 gün, önceki yazıdaki FastTree modeli rastgele split'te 0,860 gün yanılıyordu. Bu yazıdaki test seti müşteriye göre gruplanmış farklı bir split, bu yüzden sayılar birebir karşılaştırılabilir değil. Ama yön belli: yedi kolonla eğitilmiş basit bir linear model, elle yazılmış kuralı açık farkla geçiyor.
PredictionEngine ile Tek Tahmin
Değerlendirme bütün test setini bir seferde skorladı. Uygulamada ise tek bir sipariş için tek bir tahmin istiyoruz. Bunun için PredictionEngine var:
// 5) Tek seferde tek sipariş
PredictionEngine<DeliveryInput, DeliveryPrediction> engine =
ml.Model.CreatePredictionEngine<DeliveryInput, DeliveryPrediction>(model);
DeliveryInput toVan = new()
{
Carrier = "CarrierC", ShippingMethod = "Standard", Warehouse = "ANK-1",
DistanceKm = 1220, ItemCount = 2,
};
DeliveryInput inIstanbul = new()
{
Carrier = "CarrierA", ShippingMethod = "Express", Warehouse = "IST-1",
DistanceKm = 25, ItemCount = 1,
};
Console.WriteLine($"Ankara -> Van, CarrierC, 2 books : {engine.Predict(toVan).PredictedDays:F2} days");
Console.WriteLine($"Istanbul, Express, 1 book : {engine.Predict(inIstanbul).PredictedDays:F2} days");
Console.WriteLine($"same, but one item is back-ordered : " +
$"{engine.Predict(new DeliveryInput { Carrier = "CarrierA", ShippingMethod = "Express", Warehouse = "IST-1", DistanceKm = 25, ItemCount = 1, HasBackorderItem = true }).PredictedDays:F2} days");
Ankara -> Van, CarrierC, 2 books : 3.83 days
Istanbul, Express, 1 book : 1.10 days
same, but one item is back-ordered : 6.25 days
Ankara deposundan Van'a CarrierC ile giden iki kitaplık bir sipariş için 3,83 gün, İstanbul içinde Express bir sipariş için 1,10 gün. Aynı İstanbul siparişinde bir kitap stokta yoksa tahmin 6,25 güne çıkıyor. Linear model bunu "stok yoksa yaklaşık 5,15 gün ekle" diye öğrenmiş: ilk yazıdaki kuralda 5 gün yazmıştık, model veriden benzer bir sayı buldu.
Örnekleri oluştururken yalnız modelin kullandığı property'leri doldurduk. OrderId, City gibi diğer property'ler boş string olarak kaldı ve modeli etkilemedi, çünkü pipeline onları hiç okumuyor.
Modeli Kaydetmek ve Yüklemek
Eğitilmiş model bir dosyaya kaydedilip başka bir process'te, başka bir MLContext ile yüklenebiliyor:
// 6) Bütün zinciri input schema'sıyla birlikte kaydet
Directory.CreateDirectory("models");
ml.Model.Save(model, split.TrainSet.Schema, ModelPath);
Console.WriteLine($"saved {ModelPath} ({new FileInfo(ModelPath).Length / 1024.0:F1} KB)");
MLContext other = new();
ITransformer loaded = other.Model.Load(ModelPath, out DataViewSchema inputSchema);
PredictionEngine<DeliveryInput, DeliveryPrediction> engine2 =
other.Model.CreatePredictionEngine<DeliveryInput, DeliveryPrediction>(loaded);
Console.WriteLine($"loaded model, Ankara -> Van : {engine2.Predict(toVan).PredictedDays:F2} days " +
$"(input schema has {inputSchema.Count} columns)");
saved models/delivery-sdca.zip (6.4 KB)
loaded model, Ankara -> Van : 3.83 days (input schema has 19 columns)
6,4 KB'lık bir zip dosyası. İçinde bütün transformer zinciri ve Save'e verdiğimiz input schema var. Yüklenen model aynı siparişe aynı cevabı veriyor. Schema'yı birlikte kaydetmenin faydası şu: modeli kullanacak uygulama, modelin hangi kolonları hangi tiplerle beklediğini dosyanın kendisinden öğrenebiliyor.
Bu dosya bir veri dosyası değil, çalıştırılabilir bir dönüşüm zinciri. Microsoft Learn'deki Web API dokümanı modelleri yalnız güvenilir kaynaklardan yüklemeyi açıkça öneriyor. Kullanıcıdan gelen bir dosyayı model olarak yüklemek, kullanıcıdan gelen kodu çalıştırmak gibidir. Bu dosyayı ASP.NET Core'da nasıl servis edeceğimizi, versiyonlayacağımızı ve güvenle değiştireceğimizi onuncu yazıda göreceğiz.
Sık Düşülen Tuzaklar
İlk hafta karşına çıkacak üç hatayı bilerek üretelim. Birincisi tip uyuşmazlığı: modeli float ile eğitip tahmin için double property'li bir class kullanmak.
/// <summary>Aynı input, ama DistanceKm double olarak tanımlanmış.</summary>
public sealed class DoubleInput
{
public string Carrier { get; set; } = "";
public string ShippingMethod { get; set; } = "";
public string Warehouse { get; set; } = "";
public double DistanceKm { get; set; }
public float ItemCount { get; set; }
public bool IsCampaign { get; set; }
public bool HasBackorderItem { get; set; }
}
// 1) float yerine double
try
{
var engine = ml.Model.CreatePredictionEngine<DoubleInput, DeliveryPrediction>(model);
Console.WriteLine(engine.Predict(new DoubleInput { DistanceKm = 450 }).PredictedDays);
}
catch (Exception ex)
{
Console.WriteLine($"[double] {ex.GetType().Name}: {ex.Message}");
}
[double] ArgumentOutOfRangeException: Schema mismatch for input column 'DistanceKm': expected Single, got Double (Parameter 'inputSchema')
İkincisi kolon adı yazım hatası. DistanceKm yerine DistanceKM yazmak derleme sırasında yakalanmıyor; kolon adları string ve hata ancak Fit veriyi okumaya başladığında geliyor:
// 2) Kolon adındaki yazım hatası ancak Fit çalışınca fark edilir
IDataView data = ml.Data.LoadFromTextFile<DeliveryInput>("data/orders_clean.csv", hasHeader: true, separatorChar: ',');
var typo = ml.Transforms.Concatenate("Features", "DistanceKM", "ItemCount")
.Append(ml.Regression.Trainers.Sdca(labelColumnName: "DeliveryDays"));
try
{
typo.Fit(data);
}
catch (Exception ex)
{
Console.WriteLine($"[typo] {ex.GetType().Name}: {ex.Message}");
}
[typo] ArgumentOutOfRangeException: Could not find input column 'DistanceKM' (Parameter 'inputSchema')
İki mesaj da aynı exception tipini kullanıyor ve hangi kolonun sorunlu olduğunu açıkça söylüyor. Kolon adlarını string olarak tekrar tekrar yazmak yerine nameof(DeliveryInput.DistanceKm) kullanmak bu hatanın büyük kısmını derleme zamanına taşır.
Üçüncüsü en tehlikelisi, çünkü her zaman exception fırlatmıyor. PredictionEngine thread-safe değil. Tek bir engine'i birden çok thread'den kullanınca olan şu:
// 4) Birçok thread'in paylaştığı tek bir PredictionEngine
PredictionEngine<DeliveryInput, DeliveryPrediction> shared =
ml.Model.CreatePredictionEngine<DeliveryInput, DeliveryPrediction>(model);
DeliveryInput[] orders = [.. Enumerable.Range(0, 2000).Select(i => new DeliveryInput
{
Carrier = "CarrierA", ShippingMethod = "Standard", Warehouse = "IST-1", DistanceKm = 20 + i % 1500, ItemCount = 1 + i % 5,
})];
float[] expected = [.. orders.Select(o => shared.Predict(o).PredictedDays)];
int wrong = 0, failed = 0;
string? firstError = null;
Parallel.For(0, orders.Length, i =>
{
try
{
if (shared.Predict(orders[i]).PredictedDays != expected[i]) Interlocked.Increment(ref wrong);
}
catch (Exception ex)
{
Interlocked.Increment(ref failed);
Interlocked.CompareExchange(ref firstError, ex.GetType().Name, null);
}
});
Console.WriteLine($"[threads] 2000 parallel predictions on one engine: {wrong} wrong, {failed} exceptions (first: {firstError})");
[threads] 2000 parallel predictions on one engine: 383 wrong, 115 exceptions (first: ArgumentException)
2.000 paralel tahminin 435'i yanlış bir sayı döndürdü, 124'ü exception fırlattı. Sayılar her çalıştırmada değişiyor; benim denemelerimde yanlış sonuç sayısı 350 ile 435 arasında oynadı. Yanlış sonuçlar exception'lardan daha kötü: hiçbir log'da görünmüyorlar. Microsoft Learn'deki doküman da PredictionEngine'in thread-safe olmadığını söylüyor ve ASP.NET Core'da PredictionEnginePool kullanmayı öneriyor. C# Asenkron serisindeki paylaşılan durum konusunu hatırla: bir web uygulamasında her request ayrı bir thread'de çalışabilir, bu yüzden engine'i singleton olarak kaydetmek bu hatayı üretime taşır. Onuncu yazıda pool ile çözeceğiz.
Bunlara bir de bu yazının ortasındaki tuzağı ekle: MissingRealsAsNaNs olmadan boş sayı hücreleri 0 olarak okunur ve bunu sana kimse söylemez.
Küçük Bir Deneme
★☆☆ Pipeline'dan NormalizeMinMax satırını çıkar ve first-model komutunu tekrar çalıştır. MAE ne kadar değişti? Sonucu bir kenara yaz; beşinci yazıda normalization'ın SDCA için ne zaman kritik olduğunu göreceğiz.
★★☆ Concatenate'teki kolon adlarını nameof ile yaz ve bilerek bir property adını değiştir. Hata artık nerede yakalanıyor?
★★★ Pitfalls'taki paralel testi her thread için ayrı bir PredictionEngine oluşturacak şekilde değiştir (örneğin ThreadLocal<T> ile). Yanlış sonuç ve exception sayısı sıfıra iniyor mu? Engine oluşturmanın maliyetini Stopwatch ile ölç.
Bir sonraki yazıda feature engineering'e geçiyoruz: normalization'ın hangi algoritmada fark yarattığı, şehir ve ilçe gibi categorical feature'lar için one-hot encoding ile hashing, tarih ve saatten domain bilgisiyle feature üretmek, müşteri notundan text feature'ları, eksik değerler ve bütün bunları pipeline'ın içinde tutmanın neden zorunlu olduğu.