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

Введение

В эпоху расцвета искусственного интеллекта и машинного обучения 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<HousingData, PricePrediction> _predictionEngine;
    
    public HousingPricePredictor()
    {
        // Инициализация MLContext - центральный объект ML.NET
        _mlContext = new MLContext(seed: 42);
    }
    
    public void TrainModel(string trainDataPath)
    {
        // 1. Загрузка данных
        IDataView trainingData = _mlContext.Data
            .LoadFromTextFile<HousingData>(
                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<HousingData, PricePrediction>(_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<HousingData>(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<EmailData>(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<IrisData>(
            "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<CustomerData>(
            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<ClusterPrediction>(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<MovieRating>(
            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<MovieRating, MovieRatingPrediction>(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<HousingData>(trainDataPath, hasHeader: true);
        IDataView testData = mlContext.Data
            .LoadFromTextFile<HousingData>(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<EmailData>(
            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<ImageInput>());
        var model = pipeline.Fit(emptyData);
        
        // Создание prediction engine
        var predictionEngine = mlContext.Model
            .CreatePredictionEngine<ImageInput, ImagePrediction>(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<HousingData> { 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<double[]>();
        }
    }
    
    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<double[]>();
        }
    }
    
    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<ILayer>
        {
            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<ILayer>
        {
            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<string, string> _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<Dictionary<string, string>>(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<double>();
        }
    }
    
    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<double[]>();
        }
    }
    
    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<string, double>();
                for (int i = 0; i < featureNames.Length; i++)
                {
                    importanceDict[featureNames[i]] = importances[i].As<double>();
                }
                
                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<HousingData>(
                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<double>();
        var mlnetPredictions = new List<double>();
        var actualPrices = new List<double>();
        
        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<HousingData, PricePrediction>(_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<HousingData, PricePrediction> _mlNetEngine;
    private readonly PythonTrainedModel _pythonModel;
    private readonly ILogger<PredictController> _logger;
    private readonly MetricsCollector _metrics;
    
    public PredictController(
        MLContext mlContext,
        PythonTrainedModel pythonModel,
        ILogger<PredictController> logger,
        MetricsCollector metrics)
    {
        // Загрузка ML.NET модели
        var mlModel = mlContext.Model.Load("models/mlmodel.zip", out _);
        _mlNetEngine = mlContext.Model
            .CreatePredictionEngine<HousingData, PricePrediction>(mlModel);
        
        _pythonModel = pythonModel;
        _logger = logger;
        _metrics = metrics;
    }
    
    [HttpPost("mlnet")]
    public async Task<ActionResult<PredictionResponse>> 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<ActionResult<PredictionResponse>> 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<ActionResult<EnsembleResponse>> 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<ModelMetrics> GetMetrics()
    {
        var metrics = _metrics.GetCurrentMetrics();
        return Ok(metrics);
    }
    
    [HttpPost("retrain")]
    public async Task<IActionResult> 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<string, ModelMetrics> _metrics 
        = new ConcurrentDictionary<string, ModelMetrics>();
    
    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 < 10) metrics.ResponseTimeHistogram["0-10ms"]++;
        else if (inferenceTimeMs < 50) metrics.ResponseTimeHistogram["10-50ms"]++;
        else if (inferenceTimeMs < 100) metrics.ResponseTimeHistogram["50-100ms"]++;
        else if (inferenceTimeMs < 500) metrics.ResponseTimeHistogram["100-500ms"]++;
        else metrics.ResponseTimeHistogram[">500ms"]++;
    }
    
    public Dictionary<string, ModelMetrics> GetCurrentMetrics()
    {
        return _metrics.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone());
    }
}

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

public class ModelMonitoringService : BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly ILogger<ModelMonitoringService> _logger;
    private readonly IConfiguration _configuration;
    private readonly TimeSpan _monitoringInterval = TimeSpan.FromMinutes(5);
    
    public ModelMonitoringService(
        IServiceProvider services,
        ILogger<ModelMonitoringService> 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<MetricsCollector>();
                var modelDriftDetector = scope.ServiceProvider
                    .GetRequiredService<ModelDriftDetector>();
                
                // Проверка метрик производительности
                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 инфраструктурой.