Найти в Дзене
Prizrak Developer

Машинное обучение на C#: обзор ML.NET и интеграция с Python

Введение В эпоху расцвета искусственного интеллекта и машинного обучения C# разработчики долгое время оставались в стороне от этого бума, вынужденные использовать Python для ML-задач. Однако с появлением ML.NET ситуация кардинально изменилась. ML.NET — это кроссплатформенный open-source фреймворк машинного обучения от Microsoft, который позволяет .NET разработчикам создавать, обучать и развертывать ML-модели, не покидая привычную экосистему. При этом фреймворк не пытается полностью заменить Python, а предлагает гибридный подход, где сильные стороны обеих платформ используются максимально эффективно: C# для продакшн-систем и высоконагруженных приложений, Python для исследований и сложных алгоритмов. 1.1. Архитектура и основные компоненты ML.NET 1.1.1. Установка и первая модель // Установка: dotnet add package Microsoft.ML using Microsoft.ML; using Microsoft.ML.Data; public class HousingData { [LoadColumn(0)] public float Size { get; set; } [LoadColumn(1)] public float Bedrooms { get; se
Оглавление

Введение

В эпоху расцвета искусственного интеллекта и машинного обучения C# разработчики долгое время оставались в стороне от этого бума, вынужденные использовать Python для ML-задач. Однако с появлением ML.NET ситуация кардинально изменилась. ML.NET — это кроссплатформенный open-source фреймворк машинного обучения от Microsoft, который позволяет .NET разработчикам создавать, обучать и развертывать ML-модели, не покидая привычную экосистему. При этом фреймворк не пытается полностью заменить Python, а предлагает гибридный подход, где сильные стороны обеих платформ используются максимально эффективно: C# для продакшн-систем и высоконагруженных приложений, Python для исследований и сложных алгоритмов.

Основы ML.NET: от данных к предсказаниям

1.1. Архитектура и основные компоненты ML.NET

1.1.1. Установка и первая модель

// Установка: dotnet add package Microsoft.ML using Microsoft.ML; using Microsoft.ML.Data; public class HousingData { [LoadColumn(0)] public float Size { get; set; } [LoadColumn(1)] public float Bedrooms { get; set; } [LoadColumn(2)] public float YearBuilt { get; set; } [LoadColumn(3)] public float Price { get; set; } } public class PricePrediction { [ColumnName("Score")] public float PredictedPrice { get; set; } } public class HousingPricePredictor { private readonly MLContext _mlContext; private ITransformer _model; private PredictionEngine _predictionEngine; public HousingPricePredictor() { // Инициализация MLContext - центральный объект ML.NET _mlContext = new MLContext(seed: 42); } public void TrainModel(string trainDataPath) { // 1. Загрузка данных IDataView trainingData = _mlContext.Data .LoadFromTextFile ( path: trainDataPath, hasHeader: true, separatorChar: ','); // 2. Определение конвейера обработки данных var dataProcessPipeline = _mlContext.Transforms .CopyColumns("Label", "Price") .Append(_mlContext.Transforms.Concatenate( "Features", "Size", "Bedrooms", "YearBuilt")) .Append(_mlContext.Transforms.NormalizeMinMax("Features")); // 3. Выбор алгоритма обучения var trainer = _mlContext.Regression.Trainers.Sdca( labelColumnName: "Label", featureColumnName: "Features"); var trainingPipeline = dataProcessPipeline.Append(trainer); // 4. Обучение модели _model = trainingPipeline.Fit(trainingData); // 5. Создание движка для предсказаний _predictionEngine = _mlContext.Model .CreatePredictionEngine (_model); } public float PredictPrice(HousingData input) { var prediction = _predictionEngine.Predict(input); return prediction.PredictedPrice; } public void SaveModel(string modelPath) { _mlContext.Model.Save(_model, trainingData.Schema, modelPath); } public RegressionMetrics EvaluateModel(string testDataPath) { IDataView testData = _mlContext.Data .LoadFromTextFile (testDataPath, hasHeader: true); IDataView predictions = _model.Transform(testData); return _mlContext.Regression.Evaluate(predictions, "Label", "Score"); } } // Использование var predictor = new HousingPricePredictor(); predictor.TrainModel("housing-train.csv"); var metrics = predictor.EvaluateModel("housing-test.csv"); Console.WriteLine($"R-Squared: {metrics.RSquared:F4}"); Console.WriteLine($"RMS Error: {metrics.RootMeanSquaredError:F2}"); Console.WriteLine($"MAE: {metrics.MeanAbsoluteError:F2}"); var newHouse = new HousingData { Size = 1800, Bedrooms = 3, YearBuilt = 1995 }; var predictedPrice = predictor.PredictPrice(newHouse); Console.WriteLine($"Predicted price: ${predictedPrice:F2}");

1.2. Типы задач и алгоритмы в ML.NET

1.2.1. Классификация

// Бинарная классификация: спам/не спам public class EmailData { [LoadColumn(0)] public string Subject { get; set; } [LoadColumn(1)] public string Body { get; set; } [LoadColumn(2)] public bool IsSpam { get; set; } } public class SpamPrediction { [ColumnName("PredictedLabel")] public bool IsSpam { get; set; } [ColumnName("Probability")] public float Probability { get; set; } [ColumnName("Score")] public float Score { get; set; } } public class SpamClassifier { private readonly MLContext _mlContext; public ITransformer TrainBinaryClassification(string dataPath) { var data = _mlContext.Data .LoadFromTextFile (dataPath, hasHeader: true, separatorChar: '\t'); // Разделение данных на train/test var trainTestSplit = _mlContext.Data.TrainTestSplit(data, testFraction: 0.2); // Конвейер обработки текста var dataProcessPipeline = _mlContext.Transforms.Text .FeaturizeText("Features", nameof(EmailData.Subject)) .Append(_mlContext.Transforms.Text.FeaturizeText( "BodyFeatures", nameof(EmailData.Body))) .Append(_mlContext.Transforms.Concatenate( "Features", "Features", "BodyFeatures")) .Append(_mlContext.Transforms.NormalizeMinMax("Features")); // Выбор алгоритма var trainer = _mlContext.BinaryClassification.Trainers .AveragedPerceptron( labelColumnName: nameof(EmailData.IsSpam), featureColumnName: "Features", numberOfIterations: 10); var trainingPipeline = dataProcessPipeline.Append(trainer); var model = trainingPipeline.Fit(trainTestSplit.TrainSet); // Оценка модели var predictions = model.Transform(trainTestSplit.TestSet); var metrics = _mlContext.BinaryClassification .Evaluate(predictions, nameof(EmailData.IsSpam)); Console.WriteLine($"Accuracy: {metrics.Accuracy:P2}"); Console.WriteLine($"AUC: {metrics.AreaUnderRocCurve:F4}"); Console.WriteLine($"F1 Score: {metrics.F1Score:F4}"); return model; } } // Мультиклассовая классификация public class IrisData { [LoadColumn(0)] public float SepalLength { get; set; } [LoadColumn(1)] public float SepalWidth { get; set; } [LoadColumn(2)] public float PetalLength { get; set; } [LoadColumn(3)] public float PetalWidth { get; set; } [LoadColumn(4)] public string Label { get; set; } } public class IrisClassifier { public ITransformer TrainMulticlassClassification() { var mlContext = new MLContext(); var data = mlContext.Data.LoadFromTextFile ( "iris.data", separatorChar: ','); var pipeline = mlContext.Transforms .Conversion.MapValueToKey("Label") .Append(mlContext.Transforms.Concatenate( "Features", nameof(IrisData.SepalLength), nameof(IrisData.SepalWidth), nameof(IrisData.PetalLength), nameof(IrisData.PetalWidth))) .Append(mlContext.Transforms.NormalizeMinMax("Features")) .Append(mlContext.MulticlassClassification.Trainers .SdcaMaximumEntropy("Label", "Features")) .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel")); var model = pipeline.Fit(data); // Кросс-валидация var crossValResults = mlContext.MulticlassClassification .CrossValidate(data, pipeline, numberOfFolds: 5); var avgAccuracy = crossValResults .Average(r => r.Metrics.MicroAccuracy); Console.WriteLine($"Average accuracy from cross-validation: {avgAccuracy:P2}"); return model; } }

1.2.2. Кластеризация и рекомендательные системы

// Кластеризация с K-Means public class CustomerData { [LoadColumn(0)] public float Age { get; set; } [LoadColumn(1)] public float AnnualIncome { get; set; } [LoadColumn(2)] public float SpendingScore { get; set; } } public class CustomerClustering { public ITransformer PerformClustering(string dataPath) { var mlContext = new MLContext(); var data = mlContext.Data.LoadFromTextFile ( dataPath, hasHeader: true, separatorChar: ','); var pipeline = mlContext.Transforms .Concatenate("Features", nameof(CustomerData.Age), nameof(CustomerData.AnnualIncome), nameof(CustomerData.SpendingScore)) .Append(mlContext.Transforms.NormalizeMinMax("Features")) .Append(mlContext.Clustering.Trainers.KMeans( featureColumnName: "Features", numberOfClusters: 5)); var model = pipeline.Fit(data); // Визуализация кластеров var predictions = model.Transform(data); var clusters = mlContext.Data .CreateEnumerable (predictions, reuseRowObject: false) .ToList(); // Метрики кластеризации var metrics = mlContext.Clustering.Evaluate( predictions, "Features", "Score", "Label"); Console.WriteLine($"Average Distance: {metrics.AverageDistance:F4}"); Console.WriteLine($"Davies Bouldin Index: {metrics.DaviesBouldinIndex:F4}"); return model; } } // Рекомендательная система public class MovieRating { [LoadColumn(0)] public uint UserId { get; set; } [LoadColumn(1)] public uint MovieId { get; set; } [LoadColumn(2)] public float Label { get; set; } } public class MovieRecommender { public ITransformer TrainRecommendationModel(string ratingsPath, string moviesPath) { var mlContext = new MLContext(); var ratings = mlContext.Data.LoadFromTextFile ( ratingsPath, hasHeader: true, separatorChar: ','); // Матричная факторизация var options = new MatrixFactorizationTrainer.Options { MatrixColumnIndexColumnName = nameof(MovieRating.UserId), MatrixRowIndexColumnName = nameof(MovieRating.MovieId), LabelColumnName = nameof(MovieRating.Label), NumberOfIterations = 20, ApproximationRank = 100, LearningRate = 0.01 }; var pipeline = mlContext.Recommendation().Trainers .MatrixFactorization(options); var model = pipeline.Fit(ratings); // Тестирование рекомендаций var predictionEngine = mlContext.Model .CreatePredictionEngine (model); var testRating = new MovieRating { UserId = 1, MovieId = 10 }; var prediction = predictionEngine.Predict(testRating); Console.WriteLine($"Predicted rating for user {testRating.UserId}, " + $"movie {testRating.MovieId}: {prediction.Score:F2}"); return model; } }

Продвинутые возможности ML.NET

2.1. AutoML: автоматический подбор моделей

2.1.1. Использование AutoML для регрессии

using Microsoft.ML.AutoML; public class AutoMLRegression { public ITransformer RunAutoML(string trainDataPath, string testDataPath) { var mlContext = new MLContext(); // Загрузка данных IDataView trainData = mlContext.Data .LoadFromTextFile (trainDataPath, hasHeader: true); IDataView testData = mlContext.Data .LoadFromTextFile (testDataPath, hasHeader: true); // Настройка эксперимента AutoML var experimentSettings = new RegressionExperimentSettings { MaxExperimentTimeInSeconds = 60 * 10, // 10 минут OptimizingMetric = RegressionMetric.RSquared, CacheDirectory = null, CacheBeforeTrainer = AutoMLCacheOption.On }; // Создание эксперимента var experiment = mlContext.Auto() .CreateRegressionExperiment(experimentSettings); // Запуск эксперимента Console.WriteLine("Starting AutoML experiment..."); var experimentResult = experiment.Execute( trainData: trainData, validationData: testData, labelColumnName: nameof(HousingData.Price)); // Лучшая модель var bestRun = experimentResult.BestRun; Console.WriteLine($"Best model: {bestRun.TrainerName}"); Console.WriteLine($"Best R-Squared: {bestRun.ValidationMetrics.RSquared:F4}"); Console.WriteLine($"Training time: {bestRun.RuntimeInSeconds:F1}s"); // Отображение всех попыток foreach (var run in experimentResult.RunDetails .OrderByDescending(r => r.ValidationMetrics?.RSquared ?? 0)) { if (run.ValidationMetrics != null) { Console.WriteLine($"{run.TrainerName,-30} " + $"R²: {run.ValidationMetrics.RSquared:F4} " + $"Time: {run.RuntimeInSeconds:F1}s"); } } return bestRun.Model; } } // Классификация с AutoML public class AutoMLClassification { public ITransformer RunAutoMLBinary(string dataPath) { var mlContext = new MLContext(); var data = mlContext.Data.LoadFromTextFile ( dataPath, hasHeader: true, separatorChar: '\t'); var trainTestData = mlContext.Data.TrainTestSplit(data, testFraction: 0.2); var experimentSettings = new BinaryClassificationExperimentSettings { MaxExperimentTimeInSeconds = 300, // 5 минут OptimizingMetric = BinaryClassificationMetric.Accuracy, CacheDirectory = "./automl_cache", Trainers = { BinaryClassificationTrainer.FastForest, BinaryClassificationTrainer.LightGbm, BinaryClassificationTrainer.AveragedPerceptron } }; var experiment = mlContext.Auto() .CreateBinaryClassificationExperiment(experimentSettings); var result = experiment.Execute( trainTestData.TrainSet, trainTestData.TestSet, nameof(EmailData.IsSpam)); // Сохранение лучшей модели mlContext.Model.Save(result.BestRun.Model, trainTestData.TrainSet.Schema, "best_model.zip"); // Загрузка модели для использования var loadedModel = mlContext.Model.Load("best_model.zip", out _); return loadedModel; } }

2.2. ONNX интеграция и готовые модели

2.2.1. Использование предобученных ONNX моделей

using Microsoft.ML.Transforms.Onnx; public class OnnxModelInference { public void RunImageClassification(string imagePath) { var mlContext = new MLContext(); // Загрузка ONNX модели (например, ResNet50) var onnxModelPath = "resnet50.onnx"; var pipeline = mlContext.Transforms .LoadImages("image", "images", nameof(ImageInput.ImagePath)) .Append(mlContext.Transforms.ResizeImages( "image", ImageNetSettings.imageWidth, ImageNetSettings.imageHeight, "image")) .Append(mlContext.Transforms.ExtractPixels("input", "image")) .Append(mlContext.Transforms.ApplyOnnxModel( modelFile: onnxModelPath, outputColumnNames: new[] { "output" }, inputColumnNames: new[] { "input" })) .Append(mlContext.Transforms.CopyColumns( "PredictedLabel", "output")); var emptyData = mlContext.Data.LoadFromEnumerable(new List ()); var model = pipeline.Fit(emptyData); // Создание prediction engine var predictionEngine = mlContext.Model .CreatePredictionEngine (model); // Классификация изображения var imageInput = new ImageInput { ImagePath = imagePath }; var prediction = predictionEngine.Predict(imageInput); Console.WriteLine($"Predicted class: {prediction.PredictedLabel}"); Console.WriteLine($"Confidence: {prediction.Probability:P2}"); } } public class ImageInput { public string ImagePath { get; set; } } public class ImagePrediction { [ColumnName("PredictedLabel")] public string PredictedLabel { get; set; } [ColumnName("Probability")] public float Probability { get; set; } [ColumnName("Score")] public float[] Score { get; set; } } // Экспорт ML.NET модели в ONNX public class ModelExporter { public void ExportToOnnx(ITransformer model, IDataView data, string outputPath) { var mlContext = new MLContext(); using var stream = File.Create(outputPath); mlContext.Model.ConvertToOnnx( model: model, inputData: data, outputStream: stream, inputSchema: data.Schema); Console.WriteLine($"Model exported to {outputPath}"); } public void ExportWithOnnxRuntime(string modelPath, string onnxPath) { // Конвертация через ML.NET var mlContext = new MLContext(); // Загрузка модели ML.NET var mlModel = mlContext.Model.Load(modelPath, out var schema); // Создание тестовых данных var testData = mlContext.Data.LoadFromEnumerable( new List { new HousingData() }); // Экспорт в ONNX ExportToOnnx(mlModel, testData, onnxPath); } }

Интеграция C# с Python для машинного обучения

3.1. Python.NET: прямой вызов Python из C#

3.1.1. Установка и базовое использование Python.NET

// Установка: dotnet add package Python.Runtime.NETStandard using Python.Runtime; public class PythonIntegration { private IntPtr _pythonThread; public void InitializePython(string pythonPath) { // Установка пути к Python Runtime.PythonDLL = Path.Combine(pythonPath, "python39.dll"); // Инициализация Python runtime PythonEngine.Initialize(); // Получение GIL (Global Interpreter Lock) _pythonThread = PythonEngine.BeginAllowThreads(); } public dynamic CallPythonScript(string scriptPath, params object[] args) { using (Py.GIL()) // Захват GIL для работы с Python { // Загрузка Python скрипта dynamic sys = Py.Import("sys"); sys.path.append(Path.GetDirectoryName(scriptPath)); string moduleName = Path.GetFileNameWithoutExtension(scriptPath); dynamic module = Py.Import(moduleName); // Вызов функции return module.main(args); } } public double[] UseNumPyForCalculations(double[] data) { using (Py.GIL()) { dynamic np = Py.Import("numpy"); // Конвертация C# массива в numpy array var pyArray = np.array(data); // Использование numpy функций var mean = np.mean(pyArray); var std = np.std(pyArray); var normalized = (pyArray - mean) / std; // Конвертация обратно в C# массив return normalized.As (); } } public void TrainScikitLearnModel(double[][] X, double[] y) { using (Py.GIL()) { dynamic sklearn = Py.Import("sklearn"); dynamic model = sklearn.ensemble.RandomForestClassifier( n_estimators: 100, random_state: 42); // Конвертация данных var pyX = ToPython(X); var pyY = ToPython(y); // Обучение модели model.fit(pyX, pyY); // Сохранение модели dynamic joblib = Py.Import("joblib"); joblib.dump(model, "python_model.pkl"); // Использование модели в C# var predictions = model.predict(pyX); var csharpPredictions = predictions.As (); } } private PyObject ToPython(double[][] array) { using (Py.GIL()) { dynamic np = Py.Import("numpy"); return np.array(array); } } private PyObject ToPython(double[] array) { using (Py.GIL()) { dynamic np = Py.Import("numpy"); return np.array(array); } } public void Shutdown() { PythonEngine.EndAllowThreads(_pythonThread); PythonEngine.Shutdown(); } } // Пример использования var pythonIntegration = new PythonIntegration(); pythonIntegration.InitializePython(@"C:\Python39"); // Вызов Python скрипта var result = pythonIntegration.CallPythonScript( @"C:\scripts\data_processing.py", "input.csv", "output.csv"); // Использование numpy var data = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0 }; var normalized = pythonIntegration.UseNumPyForCalculations(data);

3.2. ML.NET + TensorFlow.NET: глубокое обучение на C#

3.2.1. Использование TensorFlow.NET

// Установка: dotnet add package TensorFlow.NET // dotnet add package SciSharp.TensorFlow.Redist using Tensorflow; using static Tensorflow.Binding; public class TensorFlowModel { public void CreateAndTrainModel() { // Инициализация TensorFlow tf.enable_eager_execution(); // Загрузка данных MNIST var ((trainData, trainLabels), (testData, testLabels)) = keras.datasets.mnist.load_data(); // Препроцессинг trainData = trainData / 255.0f; testData = testData / 255.0f; // Создание модели var model = keras.Sequential(new List { keras.layers.Flatten(input_shape: (28, 28)), keras.layers.Dense(128, activation: "relu"), keras.layers.Dropout(0.2), keras.layers.Dense(10, activation: "softmax") }); // Компиляция модели model.compile( optimizer: keras.optimizers.Adam(0.001), loss: keras.losses.SparseCategoricalCrossentropy(), metrics: new[] { "accuracy" }); // Обучение model.fit( trainData, trainLabels, epochs: 5, validation_data: (testData, testLabels)); // Сохранение модели model.save("mnist_model.h5"); // Загрузка и использование модели var loadedModel = keras.models.load_model("mnist_model.h5"); // Предсказание var predictions = loadedModel.predict(testData); var predictedClasses = predictions.argmax().numpy(); } public void TransferLearningWithTensorFlow() { // Загрузка предобученной модели var baseModel = keras.applications.MobileNetV2( input_shape: (224, 224, 3), include_top: false, weights: "imagenet"); // Заморозка базовой модели baseModel.trainable = false; // Добавление новых слоев var model = keras.Sequential(new List { baseModel, keras.layers.GlobalAveragePooling2D(), keras.layers.Dense(256, activation: "relu"), keras.layers.Dropout(0.5), keras.layers.Dense(10, activation: "softmax") }); model.compile( optimizer: keras.optimizers.Adam(0.0001), loss: keras.losses.CategoricalCrossentropy(), metrics: new[] { "accuracy" }); // Генератор данных для аугментации var dataGenerator = keras.preprocessing.image.ImageDataGenerator( rescale: 1.0f / 255, rotation_range: 20, width_shift_range: 0.2, height_shift_range: 0.2, horizontal_flip: true); } }

3.3. Гибридный пайплайн: Python для обучения, C# для инференса

3.3.1. Обучение в Python, использование в C#

# train_model.py - обучение модели в Python import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split import joblib import json # Загрузка и подготовка данных data = pd.read_csv('housing_data.csv') X = data[['Size', 'Bedrooms', 'YearBuilt']] y = data['Price'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # Обучение модели model = RandomForestRegressor(n_estimators=100, random_state=42) model.fit(X_train, y_train) # Сохранение модели joblib.dump(model, 'model.pkl') # Сохранение метаданных metadata = { 'features': X.columns.tolist(), 'feature_types': { 'Size': 'float', 'Bedrooms': 'float', 'YearBuilt': 'float' }, 'target': 'Price', 'model_type': 'RandomForestRegressor', 'performance': { 'train_score': model.score(X_train, y_train), 'test_score': model.score(X_test, y_test) } } with open('model_metadata.json', 'w') as f: json.dump(metadata, f, indent=2) print("Model trained and saved successfully")

3.3.2. Загрузка и использование Python-модели в C#

public class PythonTrainedModel { private dynamic _pythonModel; private dynamic _scaler; private Dictionary _metadata; public void LoadPythonModel(string modelPath, string metadataPath) { using (Py.GIL()) { // Загрузка joblib dynamic joblib = Py.Import("joblib"); // Загрузка модели и скейлера _pythonModel = joblib.load(Path.Combine(modelPath, "model.pkl")); _scaler = joblib.load(Path.Combine(modelPath, "scaler.pkl")); // Загрузка метаданных string metadataJson = File.ReadAllText(metadataPath); _metadata = JsonSerializer.Deserialize >(metadataJson); } } public double Predict(double[] features) { using (Py.GIL()) { // Конвертация features в Python объект dynamic np = Py.Import("numpy"); var pyFeatures = np.array(features).reshape(1, -1); // Масштабирование признаков var scaledFeatures = _scaler.transform(pyFeatures); // Предсказание var prediction = _pythonModel.predict(scaledFeatures); // Конвертация в C# тип return prediction[0].As (); } } public double[] PredictBatch(double[][] featuresBatch) { using (Py.GIL()) { dynamic np = Py.Import("numpy"); var pyBatch = np.array(featuresBatch); var scaledBatch = _scaler.transform(pyBatch); var predictions = _pythonModel.predict(scaledBatch); return predictions.As (); } } public ModelMetrics GetMetrics() { using (Py.GIL()) { if (_pythonModel != null) { // Получение feature importance var importances = _pythonModel.feature_importances_; var featureNames = _metadata["features"].Split(','); var importanceDict = new Dictionary (); for (int i = 0; i (); } return new ModelMetrics { FeatureImportance = importanceDict, ModelType = _metadata["model_type"], TrainingDate = DateTime.Parse(_metadata["training_date"]) }; } return null; } } } // Сервис для управления гибридным пайплайном public class HybridMLService { private readonly PythonIntegration _python; private readonly MLContext _mlContext; private PythonTrainedModel _pythonModel; private ITransformer _mlNetModel; public HybridMLService() { _python = new PythonIntegration(); _python.InitializePython(@"C:\Python39"); _mlContext = new MLContext(); } public async Task TrainHybridModel(string dataPath) { // Шаг 1: Предобработка данных в Python Console.WriteLine("Preprocessing data with pandas..."); await Task.Run(() => _python.CallPythonScript( @"scripts\preprocess.py", dataPath, "processed_data.csv")); // Шаг 2: Обучение сложной модели в Python Console.WriteLine("Training model with scikit-learn..."); await Task.Run(() => _python.CallPythonScript( @"scripts\train_model.py", "processed_data.csv", "python_model.pkl")); // Шаг 3: Загрузка Python модели в C# _pythonModel = new PythonTrainedModel(); _pythonModel.LoadPythonModel("python_model.pkl", "model_metadata.json"); // Шаг 4: Создание упрощенной модели в ML.NET для продакшена Console.WriteLine("Creating production model with ML.NET..."); await CreateSimplifiedMlNetModel("processed_data.csv"); // Шаг 5: Сравнение производительности await CompareModelPerformance("test_data.csv"); } private async Task CreateSimplifiedMlNetModel(string processedDataPath) { await Task.Run(() => { var data = _mlContext.Data.LoadFromTextFile ( processedDataPath, hasHeader: true); var pipeline = _mlContext.Transforms .Concatenate("Features", "Size", "Bedrooms", "YearBuilt") .Append(_mlContext.Regression.Trainers.FastTree( labelColumnName: "Price", featureColumnName: "Features")); _mlNetModel = pipeline.Fit(data); _mlContext.Model.Save(_mlNetModel, data.Schema, "mlnet_model.zip"); }); } private async Task CompareModelPerformance(string testDataPath) { var testData = File.ReadAllLines(testDataPath) .Skip(1) .Select(line => line.Split(',')) .Select(parts => new HousingData { Size = float.Parse(parts[0]), Bedrooms = float.Parse(parts[1]), YearBuilt = float.Parse(parts[2]), Price = float.Parse(parts[3]) }) .ToList(); var pythonPredictions = new List (); var mlnetPredictions = new List (); var actualPrices = new List (); foreach (var item in testData) { // Python модель var pythonPred = await Task.Run(() => _pythonModel.Predict( new[] { item.Size, item.Bedrooms, item.YearBuilt })); pythonPredictions.Add(pythonPred); // ML.NET модель var mlnetPred = PredictWithMlNet(item); mlnetPredictions.Add(mlnetPred); actualPrices.Add(item.Price); } // Расчет метрик var pythonMetrics = CalculateMetrics(actualPrices, pythonPredictions); var mlnetMetrics = CalculateMetrics(actualPrices, mlnetPredictions); Console.WriteLine("Performance Comparison:"); Console.WriteLine($"Python Model - RMSE: {pythonMetrics.RMSE:F2}, " + $"R²: {pythonMetrics.RSquared:F4}"); Console.WriteLine($"ML.NET Model - RMSE: {mlnetMetrics.RMSE:F2}, " + $"R²: {mlnetMetrics.RSquared:F4}"); Console.WriteLine($"Inference Time - Python: {pythonMetrics.InferenceTimeMs}ms, " + $"ML.NET: {mlnetMetrics.InferenceTimeMs}ms"); } private double PredictWithMlNet(HousingData data) { var predictionEngine = _mlContext.Model .CreatePredictionEngine (_mlNetModel); return predictionEngine.Predict(data).PredictedPrice; } }

Продакшн-развертывание и мониторинг

4.1. Docker-контейнеризация ML-моделей

4.1.1. Dockerfile для ML.NET приложения

# Dockerfile для ML.NET приложения с Python интеграцией FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build # Установка Python RUN apt-get update && apt-get install -y \ python3.9 \ python3-pip \ && rm -rf /var/lib/apt/lists/* # Установка Python пакетов COPY requirements.txt . RUN pip3 install -r requirements.txt WORKDIR /src COPY . . # Сборка .NET приложения RUN dotnet restore "MLApp.csproj" RUN dotnet publish "MLApp.csproj" -c Release -o /app/publish # Финальный образ FROM mcr.microsoft.com/dotnet/aspnet:6.0 RUN apt-get update && apt-get install -y python3.9 WORKDIR /app COPY --from=build /app/publish . COPY --from=build /usr/local/lib/python3.9/dist-packages /usr/local/lib/python3.9/dist-packages COPY --from=build /usr/local/bin /usr/local/bin # Python модель COPY models/python_model.pkl ./models/ COPY models/model_metadata.json ./models/ # ML.NET модель COPY models/mlmodel.zip ./models/ EXPOSE 80 ENTRYPOINT ["dotnet", "MLApp.dll"]

4.2. REST API для ML-моделей

4.2.1. ASP.NET Core Web API для обслуживания моделей

[ApiController] [Route("api/[controller]")] public class PredictController : ControllerBase { private readonly PredictionEngine _mlNetEngine; private readonly PythonTrainedModel _pythonModel; private readonly ILogger _logger; private readonly MetricsCollector _metrics; public PredictController( MLContext mlContext, PythonTrainedModel pythonModel, ILogger logger, MetricsCollector metrics) { // Загрузка ML.NET модели var mlModel = mlContext.Model.Load("models/mlmodel.zip", out _); _mlNetEngine = mlContext.Model .CreatePredictionEngine (mlModel); _pythonModel = pythonModel; _logger = logger; _metrics = metrics; } [HttpPost("mlnet")] public async Task > PredictMlNet( [FromBody] HousingData input) { var stopwatch = Stopwatch.StartNew(); try { var prediction = _mlNetEngine.Predict(input); stopwatch.Stop(); _metrics.RecordPrediction("mlnet", stopwatch.ElapsedMilliseconds, true); return Ok(new PredictionResponse { Model = "ML.NET", PredictedPrice = prediction.PredictedPrice, InferenceTimeMs = stopwatch.ElapsedMilliseconds, Timestamp = DateTime.UtcNow }); } catch (Exception ex) { _logger.LogError(ex, "ML.NET prediction failed"); _metrics.RecordPrediction("mlnet", stopwatch.ElapsedMilliseconds, false); return StatusCode(500, "Prediction failed"); } } [HttpPost("python")] public async Task > PredictPython( [FromBody] HousingData input) { var stopwatch = Stopwatch.StartNew(); try { var features = new[] { input.Size, input.Bedrooms, input.YearBuilt }; var predictedPrice = await Task.Run(() => _pythonModel.Predict(features)); stopwatch.Stop(); _metrics.RecordPrediction("python", stopwatch.ElapsedMilliseconds, true); return Ok(new PredictionResponse { Model = "Python", PredictedPrice = (float)predictedPrice, InferenceTimeMs = stopwatch.ElapsedMilliseconds, Timestamp = DateTime.UtcNow }); } catch (Exception ex) { _logger.LogError(ex, "Python prediction failed"); _metrics.RecordPrediction("python", stopwatch.ElapsedMilliseconds, false); return StatusCode(500, "Prediction failed"); } } [HttpPost("ensemble")] public async Task > PredictEnsemble( [FromBody] HousingData input) { var tasks = new[] { Task.Run(() => PredictMlNetInternal(input)), Task.Run(() => PredictPythonInternal(input)) }; await Task.WhenAll(tasks); var mlNetResult = tasks[0].Result; var pythonResult = tasks[1].Result; // Усреднение предсказаний var ensemblePrice = (mlNetResult.PredictedPrice + pythonResult.PredictedPrice) / 2; return Ok(new EnsembleResponse { MlNetPrediction = mlNetResult, PythonPrediction = pythonResult, EnsemblePrediction = ensemblePrice, FinalPrediction = ensemblePrice // или более сложная логика }); } [HttpGet("metrics")] public ActionResult GetMetrics() { var metrics = _metrics.GetCurrentMetrics(); return Ok(metrics); } [HttpPost("retrain")] public async Task RetrainModel([FromBody] RetrainRequest request) { if (!ModelState.IsValid) return BadRequest(ModelState); _logger.LogInformation("Starting model retraining with {DataPoints} data points", request.NewData?.Count ?? 0); // Асинхронное переобучение await Task.Run(() => RetrainModelInternal(request)); return Accepted(new { Message = "Retraining started", JobId = Guid.NewGuid() }); } } public class MetricsCollector { private readonly ConcurrentDictionary _metrics = new ConcurrentDictionary (); public void RecordPrediction(string modelName, long inferenceTimeMs, bool success) { var metrics = _metrics.GetOrAdd(modelName, _ => new ModelMetrics()); lock (metrics) { metrics.TotalPredictions++; metrics.TotalInferenceTimeMs += inferenceTimeMs; if (success) metrics.SuccessfulPredictions++; else metrics.FailedPredictions++; metrics.AverageInferenceTimeMs = metrics.TotalInferenceTimeMs / metrics.TotalPredictions; metrics.LastPredictionTime = DateTime.UtcNow; // Обновление гистограммы времени ответа UpdateResponseTimeHistogram(metrics, inferenceTimeMs); } } private void UpdateResponseTimeHistogram(ModelMetrics metrics, long inferenceTimeMs) { if (inferenceTimeMs 500ms"]++; } public Dictionary GetCurrentMetrics() { return _metrics.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); } }

4.3. Мониторинг и алертинг

public class ModelMonitoringService : BackgroundService { private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly TimeSpan _monitoringInterval = TimeSpan.FromMinutes(5); public ModelMonitoringService( IServiceProvider services, ILogger logger, IConfiguration configuration) { _services = services; _logger = logger; _configuration = configuration; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { using var scope = _services.CreateScope(); var metricsCollector = scope.ServiceProvider .GetRequiredService (); var modelDriftDetector = scope.ServiceProvider .GetRequiredService (); // Проверка метрик производительности await CheckPerformanceMetrics(metricsCollector); // Обнаружение дрифта модели await CheckModelDrift(modelDriftDetector); // Проверка доступности моделей await CheckModelAvailability(); } catch (Exception ex) { _logger.LogError(ex, "Error in model monitoring"); } await Task.Delay(_monitoringInterval, stoppingToken); } } private async Task CheckPerformanceMetrics(MetricsCollector metricsCollector) { var metrics = metricsCollector.GetCurrentMetrics(); foreach (var (modelName, modelMetrics) in metrics) { // Проверка средней скорости предсказания if (modelMetrics.AverageInferenceTimeMs > 1000) { _logger.LogWarning( $"Model {modelName} has high average inference time: " + $"{modelMetrics.AverageInferenceTimeMs}ms"); await SendAlertAsync( $"High inference time for {modelName}", $"Average: {modelMetrics.AverageInferenceTimeMs}ms"); } // Проверка rate of failures var failureRate = (double)modelMetrics.FailedPredictions / modelMetrics.TotalPredictions; if (failureRate > 0.05) // 5% failures threshold { _logger.LogError( $"Model {modelName} has high failure rate: {failureRate:P2}"); await SendAlertAsync( $"High failure rate for {modelName}", $"Failure rate: {failureRate:P2}"); } // Проверка распределения времени ответа var slowPredictions = modelMetrics.ResponseTimeHistogram .Where(kvp => kvp.Key == ">500ms") .Sum(kvp => kvp.Value); var slowPercentage = (double)slowPredictions / modelMetrics.TotalPredictions; if (slowPercentage > 0.01) // 1% медленных предсказаний { _logger.LogWarning( $"Model {modelName} has {slowPercentage:P2} slow predictions"); } } } private async Task CheckModelDrift(ModelDriftDetector driftDetector) { var driftResult = await driftDetector.CheckForDriftAsync(); if (driftResult.HasDrift) { _logger.LogWarning( $"Model drift detected: {driftResult.DriftScore:F4} " + $"(threshold: {driftResult.DriftThreshold:F4})"); await SendAlertAsync( "Model drift detected", $"Drift score: {driftResult.DriftScore:F4}"); // Автоматическое переобучение при сильном дрифте if (driftResult.DriftScore > driftResult.DriftThreshold * 2) { _logger.LogInformation("Triggering automatic retraining due to strong drift"); await TriggerRetrainingAsync(); } } } private async Task SendAlertAsync(string title, string message) { // Интеграция с системами алертинга (Slack, Email, SMS, etc.) // Пример для Slack using var httpClient = new HttpClient(); var slackMessage = new { text = $"🚨 *{title}*", attachments = new[] { new { text = message, color = "danger", ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() } } }; var webhookUrl = _configuration["Slack:WebhookUrl"]; if (!string.IsNullOrEmpty(webhookUrl)) { await httpClient.PostAsJsonAsync(webhookUrl, slackMessage); } } }

Заключение

Машинное обучение на C# с использованием ML.NET и интеграцией с Python представляет собой мощную комбинацию, которая позволяет .NET разработчикам эффективно решать ML-задачи, не покидая привычную экосистему.

Машинное обучение на C# больше не является нишевой технологией — это полноценная альтернатива Python-стеку, особенно для enterprise-разработчиков, которым необходимы типизация, производительность и интеграция с существующей .NET инфраструктурой.