ML.net ile Veri Kalitesini Düşüren Problemlere Çözümler

Üzerinde çalışacağımız veri her zaman ML.net'e giriş örneğinde olduğu gibi tertemiz şekilde gelmeyecektir. Kötü veriler makinenin yanlış öğrenmesine veya öğrenme başarımının düşmesine sebep olabilirler. Bu yazıda bu kötü verilerin ne olduğundan ve ML.net ortamında nasıl başa çıkacağımızdan bahsedeceğim.

Çalışma Bilgileri

Çalışma sırasında bir csv dosyası yerine zaten bellekte olan veriler ile çalışma ortamı oluşturmayı tercih ettim. Çoğu zaman RAM'e sığdırabildiğimiz miktarda veriyle çalışırken veriyi LINQ kullanarak önceden işlemek büyük kolaylık sağlamaktadır. Çalışma ortamımız da bu yazıdaki örnekler için böyle olacak. Örneklerde aşağıdaki gibi bir sınıf kullanacağız:

public sealed class Kisi
{
    public int Id { get; set; }
    public string SacRengi { get; set; }
    public string EgitimDurumu { get; set; }
    public string Cinsiyet { get; set; }
    public double YillikGelir { get; set; }

    public Kisi()
    {

    }

    public Kisi(int id, string sacRengi, string egitimDurumu, string cinsiyet, double yillikGelir)
    {
        Id = id;
        SacRengi = sacRengi;
        EgitimDurumu = egitimDurumu;
        Cinsiyet = cinsiyet;
        YillikGelir = yillikGelir;
    }
}

Bu sınıftan örnek veriyi aşağıdaki gibi oluşturabilirsiniz:

var veriler = new[] {
                new Kisi(1,"E","Lise", "E" , 60000),
                new Kisi(2,"K","Lisans", "K" , double.NaN),
                new Kisi(3,"S","Y.Lisans", null ,300000),
                new Kisi(4,null ,"Lisans", "E", 200000)
            };

Bellekteki bir verinin ML.net'in anlayacağı stream yapısına dönüştürmek için de aşağıdaki metot yeterli olacaktır:

 var egitimVerisi = mlContext.Data.ReadFromEnumerable(veriler);

⚠️⚠️ Versiyon 0.10 dan önce bellekteki veriler için mlContext.CreateStreamingDataView(veriler) şeklinde kullanım olmaktaydı.

ML.net'de veri ön işlemeyle ilgili literatürdeki her yönteme karşılık gelen hazır metotlar bulunmuyor. Bu sebeple veriyi çoğu zaman belleğe alındığında veya dosya üzerinde işlemek gerekmekte. Bu yazıda ML.net'de doğrudan karşılığı olmayan yöntemler için elimizdeki bu diziyi bellek üzerinde LINQ kullanarak işleyeceğiz.

Eksik Veri

Nedir ?

Hemen her veride karşılaştığımız problemdir. Tanım olarak, bir satırda bir veya birden fazla nitelik için değer girilmemesidir.

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
2 K Lisans K
3 S Y.Lisans 300000
4 Lisans E 200000

Yukarıdaki tabloda 3. satır için Cinsiyet niteliği, 2. satır için YillikGelir niteliği ve 4. satırda SacRengi niteliği boş bırakılmıştır. Eğer araştırmayı bu eksik verilere göre yapmaya çalışırsak ya sınıflandırıcımız bir hata verecek ya da üçüncü bir cinsiyet varmış gibi işlem yapacak. Peki, bu eksik veriyle nasıl başa çıkabiliriz? Bunun net bir cevabı yok. Yapmak istediğimiz işe, ne kadar uğraşmak istediğimize ve kullanacağımız algoritmaya göre karar vermemiz gerekecek. Başlıca yöntemler şunlar:

Yok etme

Bu yöntemde eksik veri içeren satırlar eğitimde kullanılmazlar. Bu yöntemi Cinsiyet ve YillikGelir nitelikleri için uyguladığımızda elde edeceğimiz veri aşağıdaki gibi olacaktır.

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000

Bu yöntem hızlıdır. Fakat, yukarıdaki çıktıya bakınca oldukça fazla işe yarar bilgiyide ziyan ettiğimiz ortadadır. Yapmak istediğimiz işlemin, verilen bilgilere göre eğitim durumunu tahmin etmek olduğunu düşünün. Bu durumda SacRengi boş diye çok daha işe yarayacak bilgileri atmak doğru olmayacaktır.

Bu işlem için ML.Net'de eğitim verisini uygun bir "Filter" nesnesinden geçirmek yeterli olacaktır. LambdaFilter tıpkı LINQ'deki Where metodu gibi çalışarak veriden koşula uymayan satırları silecektir.

var mlContext = new MLContext();
var egitimVerisi = mlContext.Data.ReadFromEnumerable(veriler);

egitimVerisi = LambdaFilter.Create(mlContext, "EgitimDurumuYokEt", egitimVerisi, "EgitimDurumu", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

egitimVerisi = LambdaFilter.Create(mlContext, "SacRengiYokEt", egitimVerisi, "SacRengi", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

egitimVerisi = LambdaFilter.Create(mlContext, "CinsiyetYokEt", egitimVerisi, "Cinsiyet", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

egitimVerisi = LambdaFilter.Create(mlContext, "YillikGerlirYokEt", egitimVerisi, "YillikGelir", NumberType.R8, (in double t) => !double.IsNaN(t));

Sabit değer koyma

Bu yöntemde eksik veri yerine daha önce belirlenmiş sabit bir değer konur. Önemi düşük nitelikler için oldukça faydalıdır.

Örneğin, SacRengi için boş olduğu durumda "E" değeri verilsin, diğer niteliklerde ise satır yok edilsin. Verimiz şu hale gelecektir.

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
4 E Lisans E 200000

Bu işlem için veriyi aşağıdaki gibi temizleyebiliriz:

Parallel.ForEach(veriler.Where(veri => veri.SacRengi is null),
                    veri => veri.SacRengi = "E");

Ortalama değer koyma

Bu yöntemde boş nitelikler için niteliğin ortalama değeri verilir. Burada gürültü ya da sapan veriler de olabileceği için aritmetik ortalama yerine geometrik veya harmonik ortalama da kullanılabilir. Veri sayısına göre ortanca (medyan) değer de kullanabilirsiniz.

Ortalamalar ile ilgili Veriyi Anlamak - Ortalamalar yazımı inceleyebilirsiniz.

Eksik veri olduğu durumda sırasıyla SacRengi için "E" sabit değerini kabul ettiğimiz, YillikGelir için aritmetik ortalama değerini kabul ettiğimiz ve Cinsiyet için satırı yok ettiğimiz senaryoda verimiz aşağıdaki gibi olacaktır.

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
2 K Lisans K 168666
4 E Lisans E 200000

ML.Net'de bu işlem bir Transform uygulamamız gerekiyor.

var mlContext = new MLContext();
var egitimVerisi = mlContext.Data.ReadFromEnumerable(veriler);

var pipeline = mlContext.Transforms.ReplaceMissingValues("YillikGelir", replacementKind: MissingValueReplacingTransformer.ColumnInfo.ReplacementMode.Mean);
egitimVerisi = pipeline.Fit(egitimVerisi).Transform(egitimVerisi);

egitimVerisi = LambdaFilter.Create(mlContext, "EgitimDurumuYokEt", egitimVerisi, "EgitimDurumu", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

egitimVerisi = LambdaFilter.Create(mlContext, "SacRengiYokEt", egitimVerisi, "SacRengi", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

egitimVerisi = LambdaFilter.Create(mlContext, "CinsiyetYokEt", egitimVerisi, "Cinsiyet", TextType.Instance, (in ReadOnlyMemory<char> t) => !t.IsEmpty);

Grubun ortalamasını koyma

Üzerinde çalıştığımız veri hakkında bilgi sahibi oldukça daha tutarlı eksik veri tamamlama şansımız olacaktır. Eğitim durumunun yıllık gelir üzerinde etkisi olduğunu biliyorsam, bu durumda yıllık gelir için satırdaki eğitim durumunun ortalamasını kullanabilirim.

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
2 K Lisans K 200000
4 E Lisans E 200000

İki nitelik arasında ilişki olup olmadığı korelasyon analizi ile anlaşılabilir. Bunun için daha önce iki korelasyona değinmiştim. Veriyi Anlamak - Korelasyon 1 - Pearson ve Veriyi Anlamak - Korelasyon 2 - Spearman yazılarıma bakabilirsiniz.

Bu yöntem için henüz ML.net kolay bir fonksiyon içermiyor. Bunu başarabilmek için veriyi ML.net ile kullanmadan önce bir tur dönüp grup ortalamalarını almanız,ardından bir tur daha dönüp boş değerleri doldurmanız gerekir. Bellekteki veriler için aşağıdaki LINQ ifadesi işimizi görecektir.

var grupOrtalamalari = veriler.Where(x=> !double.IsNaN(x.YillikGelir))
                              .GroupBy(x => x.EgitimDurumu)
                              .ToDictionary(x => x.Key,
                                            x => x.Average(y => y.YillikGelir));

Parallel.ForEach(veriler.Where(veri => double.IsNaN(veri.YillikGelir)),
        veri => veri.YillikGelir = grupOrtalamalari[veri.EgitimDurumu]);

var mlContext = new MLContext();
//...

En sık tekrar eden değeri koyma

Bu yöntemde ilgili nitelik için en sık tekrar eden değer (mod) kullanılır. Veride birden fazla mod bulunabilir. Eğer tek mod değeri varsa "unimodal", iki mod değeri varsa "bimodal", üç mod değeri varsa "trimodal" olarak isimlendirilir. Genellikle ikiden fazla mod olan durumlarda "multimodal" ismi kullanılmaktadır. Birden fazla mod olan durumlarda karar yine size kalmaktadır. Sabit olarak modlardan birisini seçebilirsiniz veya rastgele bir mod koyabilirsiniz. Örneğin "Cinsiyet" niteliği için verimiz "unimodal"dır. Mod değerimiz ise "E"'dir. Buna göre verimizi şekillendirmek istersek aşağıdaki gibi olacaktır:

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
2 K Lisans K 200000
3 S Y.Lisans E 300000
4 E Lisans E 200000

Bu durumu hızlıca veri üzerinde çözebiliriz, örneğin:

var cinsiyetMod = veriler.Where(x => x.Cinsiyet != null)
                              .GroupBy(x => x.Cinsiyet)
                              .OrderByDescending(x => x.Count())
                              .Select(x => x.Key)
                              .First();

Parallel.ForEach(veriler.Where(veri => veri.Cinsiyet is null),
        veri => veri.Cinsiyet = cinsiyetMod);

var mlContext = new MLContext();
//...

Artık eğitim için elimdeki tüm satırları kullanabiliyor olacağım.

Tahmin edilen değeri koyma

Bu yöntemde analiz içinde analiz yapılmaktadır. Örneğin, YıllıkGelir için EğitimDurumu, Cinsiyet nitelikleri ile bir Regresyon analizi yöntemiyle bir değer tahmin edilip kayıp değer yerine konulabilir. Biraz daha uğraşmak isterseniz Bayesian Inference yöntemini kullanabilirsiniz.

Sapan Veri

Nedir ?

Sapan veriler (Outliers) için "uç değer" veya "aykırı gözlem" tabirleri de kullanılmaktadır. Ben yazıda "sapan veri" tabirini kullanacağım. Sapan veri, bir niteliğin bazı satırlar için diğer gözlem değerlerine göre oldukça farklı değer içermesidir. Bu değerler yanlış da girilmiş olabilir, doğru da olabilir fakat sayıca az olmaları ve çoğunluğu yansıtmadığı için genellikle öğrenme sürecini kötü etkilemektedirler.

Bu örnekte eğitim verimizde eksik değer yok ama 2 satırda bir problemimiz var:

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 190000
2 K Lisans K 3
3 S Y.Lisans E 200000
4 S Lisans E 187000
 var veriler = new[] {
                new Kisi(1,"E","Lise", "E" , 190000),
                new Kisi(2,"K","Lisans", "K" , 3),
                new Kisi(3,"S","Y.Lisans", null ,200000),
                new Kisi(4,"E" ,"Lisans", "E", 187000),
                new Kisi(5,"E" ,"Lise", "E", 179000),
                new Kisi(6,"E" ,"Lisans", "K", 186000),
                new Kisi(7,"E" ,"Lisans", "E", 200000),
                new Kisi(8,"E" ,"Lisans", "E", 193000),
                new Kisi(9,"K" ,"Lisans", "E", 120000),
            };

Bu kişi için yıllık gelir garip bir değer içermektedir. Bu değerin doğru mu yanlış mı olduğunu bilmiyoruz. Fakat bu değer doğru olsa bile öğrenmeyi negatif yönde etkileyecektir. Bu sebeple bu veri için bir işlem yapmalıyız. Kayıp değerler için yaptığımız işlemlerin aynılarını bu değerler için de yapabiliriz. Lakin, milyonlarca verinin arasından bunları nasıl bulacağız?

Sapan verilerin tespit edilmesi

Sapan verilerin varlığı kişisel deneyimler veya hesaplamalar ile bulunabilir.

Veri hakkında bilgilerin kullanılması

Bu yöntemde araştırmacı üzerinde çalıştığı veri hakkında bilgi sahibidir. Bizim örneğimizde YillikGelir için kabul edilebilir aralık 50000 ile 500000 arasında olduğu biliniyorsa bu satırlar aşağıdaki gibi filtrelenebilir.

egitimVerisi = mlContext.Data.FilterByColumn(egitimVerisi, "YillikGelir", lowerBound: 50000 , upperBound: 500000);

Kartiller kullanıllarak

Kartil veya dörde bölen (quartile), verinin dağılımı büyükten küçüğe sıralandığında veriyi 4 eşit parçaya bölen 3 elemanın her birine verilen isimdir. Bu elemanlar Q1,Q2,Q3 diye isimlendirilir. Q2 aynı zamanda medyana (ortanca) denk gelmektedir. Hesaplamaları yaparken ilk ihtiyacımız olan bilgi çeyrekler açığı (Interquartile Range) olacaktır ve kendisi aşağıdaki formülle hesaplanmaktadır:

$$IQR=Q3-Q1$$

Daha sonra alt limit ve üst limit açıklığa göre şöyle hesaplanır:

$${Alt}=Q1-1.5({IQR})$$

$${Ust}=Q3+1.5({IQR})$$

Ortanca hesabı için daha önce "Ortalamalar" yazımda belirttiğim fonksiyonu kullanacağım. Kısaca hatırlayalım:

public static double Ortanca(IEnumerable<double> sayilar)
{
    var siralanmisSeri = sayilar.OrderBy(s => s).ToList();
    var ortaSira = (siralanmisSeri.Count - 1D) / 2D;
    return (siralanmisSeri[(int)ortaSira] + siralanmisSeri[(int)(ortaSira + 0.5D)]) / 2D;
}

Bu fonksiyon yardımıyla tüm kartilleri bulabiliriz. Uygulamanın son hali aşağıdaki gibi olacaktır:

var gelirler = veriler.Where(x => !double.IsNaN(x.YillikGelir))
                      .Select(x => x.YillikGelir);

var q2 = Ortanca(gelirler);
var q1 = Ortanca(gelirler.Where(x => x < q2));
var q3 = Ortanca(gelirler.Where(x => x > q2));

var iqr = q3 - q1;
var altLimit = q1 - 1.5 * iqr;
var ustLimit = q3 + 1.5 * iqr;

egitimVerisi = mlContext.Data.FilterByColumn(egitimVerisi, "YillikGelir", lowerBound: altLimit, upperBound: ustLimit);

Yukarıdaki yöntem kartilleri kullanarak sapan veri bulmak için oldukça yeterlidir. Fakat IQR hesaplamak için tek bir formül yoktur. Başlıcaları Tukey, Moore ve McCabe, Mendenhall ve Sincich, Freund ve Perles tarafından olmak üzere kartillerin hesaplanması için farklı yöntemler önerilmektedir.

Diğer yöntemler

Literatürde sapan değerleri bulmak için çok sayıda yöntem bulunmaktadır. Umarım farklı yazılarda buna değinebilirim. Araştırmak isterseniz bunların başlıcaları şunlardır:

Dixon Test, Rosner Test, Grubbs Test, Discodance Test, Walsh's Outlier Test, Hampel Method, Generalized ESD, DBScan, Z-Score, Isolation Forest

Mükerrer Veri

Nedir ?

Birbiriyle tamamen aynı olan veriler genellikle öğrenme süreçlerini kötü etkilerler. Genellikle diyorum çünkü bazı analizlerde verinin tekrar etmesini istiyor olabiliriz. Biz istemediğimiz durumları düşünelim.

Verimizde 2. satır ile 5. satır bire bir aynı olsun:

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise E 60000
2 K Lisans K 100000
3 S Y.Lisans E 300000
4 S Lisans E 200000
5 K Lisans K 100000

Çok basit düşünelim, bu tekrarlı veri sebebiyle lisans eğitim durumu için yıllık gelir ortalaması almak istersek sonuç 100000 değerine yaklaşacaktır. Yukarıdaki örnekte eğitim durumu lisans olanlar için ortalama yıllık gelir 150000 olması gerekirken 133333 çıkacaktır.

Kurtulmak

Tekrar eden veriler farklı niteliklerin değerlerini kontrol etmeyi gerektireceği için bunların hızlı tespit edilmesi gerekecektir. Veriler bir veritabanından geliyorsa ilgili sorguya "Distinct" sözcüğü eklemek genellike yeterli olacaktır. Fakat durum C# nesneleri olduğunda bu durumda IEquatable arayüzü ile hangi niteliklerin karşılaştırılması gerektiği belirtilmelidir.

public sealed class Kisi : IEquatable<Kisi>
{
    public int Id { get; set; }
    public string SacRengi { get; set; }
    public string EgitimDurumu { get; set; }

    public string Cinsiyet { get; set; }
    public double YillikGelir { get; set; }

    public Kisi()
    {

    }
    public Kisi(int id, string sacRengi, string egitimDurumu, string cinsiyet, double yillikGelir)
    {
        Id = id;
        SacRengi = sacRengi;
        EgitimDurumu = egitimDurumu;
        Cinsiyet = cinsiyet;
        YillikGelir = yillikGelir;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Kisi);
    }

    public bool Equals(Kisi other)
    {
        return other != null &&
               SacRengi == other.SacRengi &&
               EgitimDurumu == other.EgitimDurumu &&
               Cinsiyet == other.Cinsiyet &&
               YillikGelir == other.YillikGelir;
    }

    public static bool operator ==(Kisi kisi1, Kisi kisi2)
    {
        return EqualityComparer<Kisi>.Default.Equals(kisi1, kisi2);
    }

    public static bool operator !=(Kisi kisi1, Kisi kisi2)
    {
        return !(kisi1 == kisi2);
    }
}

Bu işlemin ardından LINQ ile "Distinct" metodunu kullanabiliriz:

veriler = veriler.Distinct().ToArray();

Uyumsuz Veri

Nedir

Bir nitelik değerinin türünün beklenenden farklı olması veya beklenen aralık içerisinde olmamasıdır.

Aşağıdaki tabloyu inceleyin:

Id SacRengi EgitimDurumu Cinsiyet YillikGelir
1 E Lise H 60000
2 K Lisans K Lisans
3 S Y.Lisans E 300000
4 S Lisans E 200000

Verilerde 1. satıda cinsiyet için anlamsız bir "H" değeri varken ve 2. satırda sayısal bir tür beklenirken metin türünden bir veri girişi olmuştur. Bu tip hatalı işlemler için "eksik veri" kısmında yaptığımız işlemler uygulanabilir.

Sonuç

Bir makine öğrenmesi işlemine geçmeden önce doğru veri ile çalışmak önemlidir. Yukarıda bunların başlıcalarının ne olduğunu ve ML.net dünyasında nasıl çözülebileceğine dair örnekler vermeye çalıştım. Umarım faydalı olmuştur.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir