Nedir?

Makine öğrenmesi ve veri madenciliği denildiğinde akla gelen ilk iki dil Python ve R olmaktadır. Aslına bakarsanız .net ile bu işler zaten Accord.net gibi kütüphaneler yarıdımıyla yapılabilmektedir. Fakat bu kütüphaneler olaya biraz akademik düzeyde yaklaştıklarından ve pazarlama stratejilerinde doğrudan uzmanları hedefledikleri için pek bilinirlikleri yoktur. Dolayısıyla, konuya yeni başlayacaklar için, önerecek kütüphane olmamasından dolayı C#'ın tavsiye edilmemesi bence gayet normal bir durum olmuştur. ML.net buradaki açığı doldurmak üzere biraz geç olsa da gelen bir resmi bir kütüphanedir. ML.net, C# ve F# dilleri ile Windows, Linux ve macOS işletim sistemlerinde makine öğrenmesi, yapay zekâ ve veri madenciliği çalışmaları yapabilmemizi sağlamaktadır. Kütüphane konuya geç dahil olduğundan Python,R gibi çok zengin kaynaklara sahip olmadığı akıldan çıkmamalıdır.

Microsoft'un makine öğrenmesi üzerine bir çok ürünü bulunmaktadır. Bunların başlıcaları SQL Server ML Services, Microsoft ML Server, Windows ML ve Azure ML Studio'dur. ML.net tüm bunlardan farklı olarak ücretsiz, .net hedefli, platform bağımsız ve açık kaynaktır.

ML.net az önce bahsettiğim Accord.net gibi epey yol almış Light GBM, CNTK ve TensorFlow gibi kütüphaneler ile birlikte çalışabilmektedir. Umarım her birine tek tek değinebileceğim yazılar hazırlayabilirim. Özellikle GPU kullanan örnekler epey eğlenceli olacak diye düşünüyorum.

Proje açık kaynak olarak geliştiriliyor ve isterseniz siz de paylaşımda bulunabilirsiniz. Projenin reposuna https://github.com/dotnet/machinelearning adresinden ulaşabilirsiniz.

Başlarken

ML.net ile ilgili resmî web adresi https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet dir. Bu adreste kısayolunu bulacağınız http://mbmlbook.com/ Model-Based Machine Learning e-kitabı ücretsiz olarak dağıtılmaktadır.

Kütüphanenin resmi adresinde ( https://dotnet.microsoft.com/learn/machinelearning-ai/ml-dotnet-get-started-tutorial ) bir giriş eğitimi mevcut. Buradaki örnekleri takip etmenizi öneririm. Ben biraz farklı bir örnek olması adına değişik bir problemi ele almayı tercih ettim.

Örnek Projemiz

Projeyi açmadan

Projemizi trendleri takip ederek .net core üzerinde bir console uygulaması olarak geliştireceğiz. Bunun için eğer .net core sizde yüklü değilse https://dotnet.microsoft.com/download adresinden “Build Apps” kısmından en son sürüm SDK paketini indirmeniz gerekecek. Ben bu çalışmayı yaparken 2.2.101 sürümü ile çalıştım.

Daha önce .net core yüklemişseniz ama sizdeki SDK sürümünü bilmiyorsanız WIN+X tuşuna basarak veya başlat düğmesine sağ tıklayarak “Power Shell” konsolunu açın ve aşağıdaki komutu girin:


dotnet --list-sdks

Komutunuzun çıktısı aşağıdaki gibi olmalı:

2.1.2 [C:\\Program Files\\dotnet\\sdk]
2.1.101 [C:\\Program Files\\dotnet\\sdk]
2.1.202 [C:\\Program Files\\dotnet\\sdk]
2.1.402 [C:\\Program Files\\dotnet\\sdk]
2.1.403 [C:\\Program Files\\dotnet\\sdk]
2.1.500 [C:\\Program Files\\dotnet\\sdk]
2.1.502 [C:\\Program Files\\dotnet\\sdk]
2.1.600-preview-009426 [C:\\Program Files\\dotnet\\sdk]
2.2.100 [C:\\Program Files\\dotnet\\sdk]
2.2.101 [C:\\Program Files\\dotnet\\sdk]

Ben elimdeki en güncel sürüm olan 2.2.101 ile çalışacağım. Buradan sonra projemizi istediğimiz şekilde oluşturabiliriz. İstersek Power Shell ya da komut satırından devam edebiliriz. İstersek de Visual Studio, Visual Studio Code veya başka bir IDE/editör kullanabiliriz.

Projeyi açalım

Power Shell veya komut satırı kullanarak projemizi açmak için beğendiğimiz bir klasöre gelelim.

Visual Studio ile ilerlemek isterseniz yeni bir .net core console projesi oluşturmanız yeterli olacaktır.

´Mlnet1´ adında bir klasör oluşturdum ve içinde aşağıdaki komutu çalıştırdım.


dotnet new console

Bu komutun çıktısı aşağıdaki gibi oldu:

PS C:\\cihan\\mlnet1\> dotnet new console

The template "Console Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on C:\\cihan\\mlnet1\\mlnet1.csproj...
Restoring packages for C:\\cihan\\mlnet1\\mlnet1.csproj...
Generating MSBuild file C:\\cihan\\mlnet1\\obj\\mlnet1.csproj.nuget.g.props.
Generating MSBuild file C:\\cihan\\mlnet1\\obj\\mlnet1.csproj.nuget.g.targets.
Restore completed in 279,98 ms for C:\\cihan\\mlnet1\\mlnet1.csproj.
Restore succeeded.

Artık ilgili dizinde bir C# projem var. Sıra geldi bu proje ml.net’i eklemeye.
Bunun için aşağıdaki komutu çalıştırıyorum:


dotnet add package Microsoft.ML

Bu komut nuget aracılığıyla ilgili paketi bulup sonra projeme ekliyor. Visual Studio kullanarak da nuget paketini projenize ekleyebilirsiniz. Komutu çalıştırırken özellikle sürüm numarası belirtmediğim için en güncel stabil sürüm indirildi. Ben örneği hazırlarken güncel sürüm 0.9.0 idi. Eğer problem yaşarsanız benimle aynı sürümde çalışmayı deneyebilirsiniz. Bunun için --version 0.9.0 parametresi vermeniz yeterli olacaktır.

Dikkat: Bu örnekler 0.9 sürümüne uygun yazılmıştır. ML.net 0.10, 0.11 ile bir çok metodun adı ve kullanımı değişmiştir. Versiyon 1.0 çıktığında yazılarda toplu bir güncelleme yapacağım.

Bu kadar konsol macerası yeter diyerek aşağıdaki komutla Visual Studio’mun açılmasını sağlıyorum:


start mlnet1.csproj

Eğer ki siz Visual Studio Code kullanmak isterseniz bu durumda aşağıdaki komutu çalıştırabilirsiniz:


code .

Kod yazmaya geçmeden önce son bir eksiğimiz kaldı. Üzerinde çalışacağımız veri.

Verinin Temini

Projemiz için https://www.kaggle.com/uciml/mushroom-classification adresindeki veriyi sınıflandırma için kullanacağız. Bu veri farklı mantarların çeşitli niteliklerini ve bu niteliklerdeki mantarın zehirli olup olmadığını içeriyor. Amacımız bu listeden yapılacak bir öğrenme ile nitelikleri verilen mantarın yenilebilir mi yoksa zehirli mi olduğunu tahmin etmek. Veriyi mushrooms.csv adıyla proje dizinimize kopyalayalım. Bu dosyanın uygulama derlendiği zaman oluşacak program dosyalarının yanına kopyalanması gerekiyor. Bunu başarmak için eğer Visual Studio kullanıyorsanız, dosyaya tıklayın ve properties penceresinden “Copy to Output Directory” seçeneğini “Always” yapın. Ya da mlnet1.csproj dosyasını bir metin editörü ile açıp aşağıdaki düğümü ekleyin:

<ItemGroup>
<None Update="mushrooms.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

Ve kod yazmaya hazırız.

Kodlar

Program.cs üzerinde Main metodu üzerinde çalışarak basit bir örnek yapacağız. İlk yapmamız gereken kullanacağımız namepaceleri eklemek olacak.

using Microsoft.ML;
using Microsoft.ML.Core.Data;
using Microsoft.ML.Data;
using System;
using System.IO;

Öğrenme işi tamamlandığında elde edeceğimiz sınıflandırıcıyı test edebilmek için verisetindeki satırları simgeleyecek C# sınıfımı oluşturuyorum, ben bu sınıfı oluştururken https://toolslick.com/generation/code/class-from-csv adresindeki araçtan faydalandım.

public class Mushroom
{
public string @class { get; set; }
public string cap_shape { get; set; }
public string cap_surface { get; set; }
public string cap_color { get; set; }
public string bruises { get; set; }
public string odor { get; set; }
public string gill_attachment { get; set; }
public string gill_spacing { get; set; }
public string gill_size { get; set; }
public string gill_color { get; set; }
public string stalk_shape { get; set; }
public string stalk_root { get; set; }
public string stalk_surface_above_ring { get; set; }
public string stalk_surface_below_ring { get; set; }
public string stalk_color_above_ring { get; set; }
public string stalk_color_below_ring { get; set; }
public string veil_type { get; set; }
public string veil_color { get; set; }
public string ring_number { get; set; }
public string ring_type { get; set; }
public string spore_print_color { get; set; }
public string population { get; set; }
public string habitat { get; set; }
}

Kullandığımız verisetinde Class niteliği “e” olduğunda “edible”, “p” olduğunda “poisonous” anlamına gelmektedir ve bizim için en önemli nitelik bu. Diğer property'lerin anlamları tek tek Kaggle'da açıklanmış durumda. Class sözcüğünü c# da yeni bir sınıf oluşturmak için kullandığımız için bu niteliği oluştururken @ işaretini kullanmalıyız.

Oluşturacağımız sınıflandırıcıya işlemesi için vereceğimiz Mushroom sınıfını oluşturduktan sonra bir de bu sınıflandırıcının döneceği yanıt için bir nesne oluşturmamız gerekiyor. O da aşağıdaki gibi olacak:

public class MushroomPrediction
{
[ColumnName("PredictedLabel")]
public bool IsEdible;
[ColumnName("Score")]
public float Score;
}

Burada attribute’lar yardımıyla neyin neye denk geldiğini ML.net’e söylemiş oluyoruz. IsEdible mantarın yenilebilir olup olmadığını, score ise tahminin oranını belirtiyor olacak.

Akabinde “main” metodu içinde ML.net ile yeni bir “context” oluşturmamız gerekiyor. Bunu Entity Framework’deki context yapısına benzetebilirsiniz. Context’i oluşturmak için kodumuz aşağıdaki gibi olacak:

var mlContext = new MLContext();

Daha sonra csv dosyasını okuyacağız. Neyse ki bunun için ML.net kendi içerisinde de bir yapı sunuyor.

Öncelikle verinin niteliklerini ML.net’in tanıdığı türlere dönüştürmek gerekiyor. Bunun için aşağıdaki dönüşüm tablosu işimizi görecektir.

Enum C# Karşılığı
U1 byte
U2 ushort
U4 uint
U8 ulong
U16 Microsoft.ML.Data.RowId
UG Microsoft.ML.Data.RowId
I1 sbyte
I2 short
I4 int
I8 long
R4 float
Num float
R8 double
TX string
TXT string
Text string
BL bool
Bool bool
TS TimeSpan
TimeSpan TimeSpan
DT DateTime
DZ DateTimeOffset

Öncelikle kolonlarımı ardından CreateTextReader ile okuma işlemini yapacak nesneyi oluşturuyoruz.

var columns = new[]
{
new TextLoader.Column("class", DataKind.Text, 0),
new TextLoader.Column("cap_shape", DataKind.Text, 1),
new TextLoader.Column("cap_surface", DataKind.Text, 2),
new TextLoader.Column("cap_color", DataKind.Text, 3),
new TextLoader.Column("bruises", DataKind.Text, 4),
new TextLoader.Column("odor", DataKind.Text, 5),
new TextLoader.Column("gill_attachment", DataKind.Text, 6),
new TextLoader.Column("gill_spacing", DataKind.Text, 7),
new TextLoader.Column("gill_size", DataKind.Text, 8),
new TextLoader.Column("gill_color", DataKind.Text, 9),
new TextLoader.Column("stalk_shape", DataKind.Text, 10),
new TextLoader.Column("stalk_root", DataKind.Text, 11),
new TextLoader.Column("stalk_surface_above_ring", DataKind.Text, 12),
new TextLoader.Column("stalk_surface_below_ring", DataKind.Text, 13),
new TextLoader.Column("stalk_color_above_ring", DataKind.Text, 14),
new TextLoader.Column("stalk_color_below_ring", DataKind.Text, 15),
new TextLoader.Column("veil_type", DataKind.Text, 16),
new TextLoader.Column("veil_color", DataKind.Text, 17),
new TextLoader.Column("ring_number", DataKind.Text, 18),
new TextLoader.Column("ring_type", DataKind.Text, 19),
new TextLoader.Column("spore_print_color", DataKind.Text, 20),
new TextLoader.Column("population", DataKind.Text, 21),
new TextLoader.Column("habitat", DataKind.Text, 22),
};
TextLoader reader = mlContext.Data.CreateTextReader(new TextLoader.Arguments()
{
Separator = ",",
HasHeader = true,
Column = columns

});

!! Not. ML.net 0.10 ile birlikte CreateTextReader metodunun adı ReadFromTextFile olarak değişmiştir. Yazıyı komple güncelleyecek zamanım olana kadar buradan belirtmek istedim.

!! ÖNEMLİ NOT 2 ML.net 0.11 ile tekrar bir isim değişikliği ile metodun adı LoadFromEnumerable olarak değişti.

Kolonları oluştururken kolon adının dosyadaki başlık ile birebir aynı olmasına gerek yok. İstediğiniz adı verebilirsiniz. Fakat kolon sıra numarasını ve türü doğru yazdığınızdan emin olmalısınız.

Elimizdeki veri oldukça temiz olduğu için tüm kolonların türü “Text” türünde. CreateTextReader’ın sık kullanılacak 2 adet argümanı var. Separator dosyanın her bir satırında hücrelerin hangi karakter ile ayrıldığını tutuyor. HasHeader ise dosyanın ilk satırında başlıkların olup olmadığını soruyor.

Eğer ML.net örneklerini incelemişseniz kolonların doğrudan CreateTextReader içinde verildiğini görebilirsiniz. Bu tamamen keyfi bir konu, ben iki sebeple bundan kaçınıyorum. İlki bir metoda parametre olarak verdiğim şey yeni bir sınıf örneği ise okumayı kolaylaştırmak için. İkincisi ise karışık ML.net işlemlerinde doğrudan kolon nesnesine ihtiyaç olabiliyor bu durumda ilgili kolona kolayca columns[5] diyerek ulaşabiliyorum.

CreateTextReader adı üzerinde yalnızca okuma işlemini yapacak bir nesne oluşturuyor.

Okuma işlemi bu nesneyi kullanacağımız zaman yapılacak.

Elimizdeki veride 8124 kayıt var. Fakat bunların hepsini eğitim için kullanırsak bu durumda eğitimin ne kadar başarılı olduğunu bilemeyiz. Bu sebeple elimizdeki verinin bir kısmını eğitim bir kısmını da kontrol amaçlı ayırmalıyız. Bu ayırma işlemi için çok fazla yöntem var. Bunlara umarım farklı bir yazıda değinme fırsatım olur. Normalde bunun için %20 oranı önerilse de bu örnek için verinin %30 kadarının test için kullanabiliriz. Seed’e sabit değer vererek verilerin bu sayıya göre rastgele alınmasını ve hep aynı verilerin seçilmesini sağlıyoruz.

Seed kavramına daha önce rastgele sayılar yazımda değinmiştim.

var fullData = reader.Read(dataPath);

(var trainingDataView, var testDataView) =
mlContext.BinaryClassification.TrainTestSplit(fullData, 0.3, seed: 5555);

İlk satırda veriler okunup fullData isimli bir değişkene aktarılıyor. Ardından bir tuple decontruction işlemi ile trainingDataView, testDataView değişkenleri oluşturuluyor.

Tuple deconstruction'a daha önce C# 7 yazımda değinmiştim.

Sıra geldi pipeline’ı oluşturmaya,

var pipeline =
mlContext.Transforms.Conversion.ValueMap(new[] { "e".AsMemory(), "p".AsMemory()
}, new[] { true, false }, ("class", "class"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("cap_shape"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("cap_shape"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("cap_surface"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("cap_color"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("bruises"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("odor"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("gill_attachment"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("gill_spacing"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("gill_size"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("gill_color"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_shape"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_root"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_surface_above_ring"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_surface_below_ring"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_color_above_ring"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("stalk_color_below_ring"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("veil_type"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("veil_color"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("ring_number"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("ring_type"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("spore_print_color"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("population"))
.Append(mlContext.Transforms.Categorical.OneHotEncoding("habitat"))
.Append(mlContext.Transforms.Concatenate("Features", "cap_shape",
"cap_surface",
"cap_color",
"bruises",
"odor",
"gill_attachment",
"gill_spacing",
"gill_size",
"gill_color",
"stalk_shape",
"stalk_root",
"stalk_surface_above_ring",
"stalk_surface_below_ring",
"stalk_color_above_ring",
"stalk_color_below_ring",
"veil_type",
"veil_color",
"ring_number",
"ring_type",
"spore_print_color",
"population",
"habitat"));

Yukarıdaki kodda oldukça fazla satır olsa da aslında 3 farklı işlem yapıyoruz.

“mlContext.Transforms.Conversion.ValueMap” ilk dizide verilen değerler ile ikinci dizide verilen değerleri eşlemeye yarar. İlk dizide “e”,”p” değerleri sırayla ikinci dizideki true ve false değerlerine eşitlenmektedir. String değerlerin ise Memory<char> türünden olması gerekiyor, bunun için .AsMemory() extension methodunu kullanabilirsiniz.

AsMemory de ne diyorsanız, buradaki yazı ilginizi çekebilir.

OneHotEncoding’i ise kategorik türden nitelikleri sayısal hale çevirmek için kullanıyoruz. Kullanacağımız sınıflandırıcı için string geçerli bir tür olmadığı için bir dönüşüm işlemi gereklidir. One hot adı ise değeri oluşturan bit dizisi içerinde yalnızca bir adet 1 olmasından gelmektedir (00100 , 01000, 1000 gibi).
Concatenate metodu ise özellikleri tek bir yere toplamaktadır.

Gelelim en önemli kısıma:

var trainer = mlContext.BinaryClassification.Trainers.FastTree(labelColumn:
"class", featureColumn: "Features");
var trainingPipeline = pipeline.Append(trainer);
var trainedModel = trainingPipeline.Fit(trainingDataView);
using (var fs = new FileStream("model.zip", FileMode.Create, FileAccess.Write,
FileShare.Write))
{
mlContext.Model.Save(trainedModel, fs);
}
var predictions = trainedModel.Transform(testDataView);
var metrics = mlContext.BinaryClassification.Evaluate(predictions, "class",
"Score");
Console.WriteLine($"*Accuracy: {metrics.Accuracy:P2}");
Console.WriteLine($"*F1Score: {metrics.F1Score:P2}");

FastTree sınıflandırıcısına etiketimizi ve niteliklerimizi belirttik. Ardından eğitim işlemini başlatıyoruz. Eğitim tamamlanınca ortaya çıkan model’i daha sonra kullanabilmek için kaydediyoruz. Peşinden test verilerimizi kullanarak modelimizi ölçüyoruz. Peşinden doğruluk (Accuracy) ve F1Score değerlerine bakıyoruz. Bu örnekte her ikisi de mükemmel şekilde %100 çıkıyor olmalı.

Son olarak, kaydettiğimiz modeli yeni bir context açarak kendi verimiz ile çalıştıralım:

var context2 = new MLContext();

ITransformer loadedModel;

using (var stream = File.OpenRead("model.zip"))
{
loadedModel = context2.Model.Load(stream);
}

var classifier = loadedModel.CreatePredictionEngine<Mushroom,MushroomPrediction>(mlContext);
var result = classifier.Predict(new Mushroom
{
cap_shape = "x",
cap_surface = "s",
cap_color = "y",
bruises = "t",
odor = "a",
gill_attachment = "f",
gill_spacing = "c",
gill_size = "b",
gill_color = "k",
stalk_shape = "e",
stalk_root = "c",
stalk_surface_above_ring = "s",
stalk_surface_below_ring = "s",
stalk_color_above_ring = "w",
stalk_color_below_ring = "w",
veil_type = "p",
veil_color = "w",
ring_number = "o",
ring_type = "p",
spore_print_color = "n",
population = "n",
habitat = "g",
});
Console.WriteLine(result.IsEdible);
Console.WriteLine(result.Score);

Yeni bir context açıp, bu context’e model olarak kaydettiğimiz dosyayı yüklüyoruz. Arkasından bir tanımlayıcı fonksiyon oluşturuyoruz. Csv dosyasındaki kolon isimleriyle C# sınıfındaki property adları eşleşen nesnemizi kullanarak testimizi yapıyoruz. Böylece ML.net de ilk kodumuzu bitirmiş oluyoruz.

Yorum

%100 başarı, kulağa çok güzel geliyor. Fakat çok temiz bir veri ile çalıştığımızı yazıda da belirtmiştim. Gerçek örneklerde çoğu durumda 70-80 elde etmek büyük başarı sayılmaktadır. Verimizde ise neredeyse hiç analiz yapmadık. Örneğin, mantarın örtü rengi (veil_color) belki de zehirli olup olmadığına dair bir bilgi vermiyor. Ama biz bunu sınıflandırcıya dahil edip işlemci gücümüzden harcadık. Belki verilerimiz içinde bir birini tekrar eden satırlar vardı. Elimizdeki veride zehirli ve yenilebilir mantarların oranına bakmadık. Başarımın neden bu kadar yüksek çıktığını çok fazla irdelemedik, belki de test verilerimizi seçme yöntemimizde sıkıntı vardı. Bundan sonraki yazılarda bu tip analizlere de bakıyor olacağız. Yine kullandığımız FastTree algoritması gibi ML.net'in sağladığı algoritmaların detaylarından bahsetmek isterim. Okuduğunuz için teşekkürler.

Örnek kodların tamamına aşağıdaki linkten ulaşabilirsiniz :

https://github.com/cihanyakar/MLNetMushroom