diff --git a/.gitignore b/.gitignore index 7e7cc84..851e5e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ## ## Get latest from `dotnet new gitignore` +.models_cache .zed *.png cache diff --git a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs index f4dc459..8bc658f 100644 --- a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs +++ b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs @@ -141,69 +141,102 @@ public partial class AnalyzerService var sampleRecords = _context.SampleRecords .Where(r => r.BrandId == brand.Id); - _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); - if (sampleRecords.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); - cancellationToken.ThrowIfCancellationRequested(); - - if (separator.Features.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных признаков для сепаратора: \"{separator.Type}\""); - if (separator.Features.Count() % FEATURES_LENGTH != 0) - throw new InvalidDataException($"Неожидаемое количество калибровочных признаков для сепаратора: \"{separator.Type}\""); - var separatorDataSet = separator.Features.Chunk(FEATURES_LENGTH) - .Select(v => new ClusterizationData - { - Label = "separator", - Features = v.ToArray() - }) - .Shuffle(); - - _logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count()); - if (separatorDataSet.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\""); - cancellationToken.ThrowIfCancellationRequested(); - - foreach (var sampleRecord in sampleRecords) + var dataSb = new System.Text.StringBuilder(); + dataSb.AppendLine(brand.Id.ToString()); + dataSb.AppendLine(brand.EditDateTime.ToString()); + foreach (var record in sampleRecords.OrderBy(r => r.Id)) { - if (sampleRecord.Features.Count() == 0) - _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); - if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) - _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + dataSb.AppendLine(record.Id.ToString()); + dataSb.AppendLine(record.EditDateTime.ToString()); } + dataSb.AppendLine(separator.Id.ToString()); + dataSb.AppendLine(separator.EditDateTime.ToString()); + var dataHashCode = Convert.ToHexString( + System.Security.Cryptography.MD5.HashData( + System.Text.Encoding.UTF8.GetBytes(dataSb.ToString()) + ) + ); - var sampleDataSet = sampleRecords - .AsEnumerable() - .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) - .SelectMany(r => r.Features.Chunk(FEATURES_LENGTH)) - .Select(f => new ClusterizationData + _logger.LogInformation("Хэш калибровочных записей: {}", dataHashCode); + + Directory.CreateDirectory(".models_cache"); + var cachedModelPath = Path.Combine(".models_cache", dataHashCode) + ".zip"; + + if (File.Exists(cachedModelPath)) + { + _logger.LogInformation("Модель найдена к кэше"); + var model = _ml.Model.Load(cachedModelPath, out _); + _clusterizationEngine = _ml.Model.CreatePredictionEngine(model); + } + else + { + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); + if (sampleRecords.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); + + if (separator.Features.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных признаков для сепаратора: \"{separator.Type}\""); + if (separator.Features.Count() % FEATURES_LENGTH != 0) + throw new InvalidDataException($"Неожидаемое количество калибровочных признаков для сепаратора: \"{separator.Type}\""); + var separatorDataSet = separator.Features.Chunk(FEATURES_LENGTH) + .Select(v => new ClusterizationData + { + Label = "separator", + Features = v.ToArray() + }) + .Shuffle(); + + _logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count()); + if (separatorDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\""); + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var sampleRecord in sampleRecords) { - Label = "sample", - Features = f.ToArray() - }) - .Shuffle(); + if (sampleRecord.Features.Count() == 0) + _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) + _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + } - _logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count()); - if (separatorDataSet.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); - cancellationToken.ThrowIfCancellationRequested(); + var sampleDataSet = sampleRecords + .AsEnumerable() + .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) + .SelectMany(r => r.Features.Chunk(FEATURES_LENGTH)) + .Select(f => new ClusterizationData + { + Label = "sample", + Features = f.ToArray() + }) + .Shuffle(); - var trainDataSet = Enumerable.Concat(separatorDataSet, sampleDataSet); - _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); - var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count()); + if (separatorDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); - _logger.LogInformation("Создание конвейера"); - var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label") - .Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy()) - .Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel")); - cancellationToken.ThrowIfCancellationRequested(); + var trainDataSet = Enumerable.Concat(separatorDataSet, sampleDataSet); + _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); + var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); + cancellationToken.ThrowIfCancellationRequested(); - _logger.LogInformation("Обучение модели"); - var model = pipeline.Fit(trainData); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Создание конвейера"); + var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label") + .Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy()) + .Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel")); + cancellationToken.ThrowIfCancellationRequested(); - _clusterizationEngine = _ml.Model.CreatePredictionEngine(model); - model.Dispose(); + _logger.LogInformation("Обучение модели"); + var model = pipeline.Fit(trainData); + cancellationToken.ThrowIfCancellationRequested(); + + _ml.Model.Save(model, trainData.Schema, cachedModelPath); + + _clusterizationEngine = _ml.Model.CreatePredictionEngine(model); + + model.Dispose(); + } } private async Task InitializeMlRegressionEngine(BrandRecord brand, CancellationToken cancellationToken) { @@ -217,79 +250,110 @@ public partial class AnalyzerService var sampleRecords = _context.SampleRecords .Where(r => r.BrandId == brand.Id); - _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); - if (sampleRecords.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); - cancellationToken.ThrowIfCancellationRequested(); - - foreach (var sampleRecord in sampleRecords) + var dataSb = new System.Text.StringBuilder(); + dataSb.AppendLine(brand.Id.ToString()); + dataSb.AppendLine(brand.EditDateTime.ToString()); + foreach (var record in sampleRecords.OrderBy(r => r.Id)) { - if (sampleRecord.Features.Count() == 0) - _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); - if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) - _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + dataSb.AppendLine(record.Id.ToString()); + dataSb.AppendLine(record.EditDateTime.ToString()); } - - _logger.LogInformation("Подготовка данных"); - var trainDataSet = sampleRecords - .AsEnumerable() - .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) - .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0) - .SelectMany(r => - r.Features - .Chunk(FEATURES_LENGTH) - .Select(f => - new RegressionData - { - Value = (float)(r.MeasuredContent >= 0 ? r.MeasuredContent : r.MixtureActualRate), - Features = f.ToArray() - } - ) + var dataHashCode = Convert.ToHexString( + System.Security.Cryptography.MD5.HashData( + System.Text.Encoding.UTF8.GetBytes(dataSb.ToString()) ) - .Shuffle() - .ToList(); + ); - _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); - if (trainDataSet.Count() == 0) - throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Хэш калибровочных записей: {}", dataHashCode); - int rowCount = trainDataSet.Count(); - double[][] featuresArray = trainDataSet.Select(d => d.Features.Select(f => (double)f).ToArray()).ToArray(); - Matrix matrix = DenseMatrix.OfRowArrays(featuresArray); - for (int i = 0; i < IMAGES_COUNT; i++) + Directory.CreateDirectory(".models_cache"); + var cachedModelPath = Path.Combine(".models_cache", dataHashCode) + ".zip"; + + if (File.Exists(cachedModelPath)) { - matrix = matrix.RemoveColumn(i * 3); - matrix = matrix.RemoveColumn(i * 3); - matrix = matrix.RemoveColumn(i * 3); + _logger.LogInformation("Модель найдена к кэше"); + var model = _ml.Model.Load(cachedModelPath, out _); + _regressionEngine = _ml.Model.CreatePredictionEngine(model); } - cancellationToken.ThrowIfCancellationRequested(); + else + { + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); + if (sampleRecords.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); - Vector meanVector = matrix.ColumnSums() / matrix.RowCount; - Matrix covarianceMatrix = MatrixCovariance(matrix, meanVector); - Matrix invCovarianceMatrix = covarianceMatrix.Inverse(); - trainDataSet = trainDataSet - .Where((data, index) => + foreach (var sampleRecord in sampleRecords) { - var row = matrix.Row(index); - double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); + if (sampleRecord.Features.Count() == 0) + _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) + _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + } - return distance <= FILTERING_THRESHOLD; - }) - .ToList(); - _logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count()); - var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Подготовка данных"); + var trainDataSet = sampleRecords + .AsEnumerable() + .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) + .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0) + .SelectMany(r => + r.Features + .Chunk(FEATURES_LENGTH) + .Select(f => + new RegressionData + { + Value = (float)(r.MeasuredContent >= 0 ? r.MeasuredContent : r.MixtureActualRate), + Features = f.ToArray() + } + ) + ) + .Shuffle() + .ToList(); - _logger.LogInformation("Создание конвейера"); - var pipeline = _ml.Regression.Trainers.FastTree(); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); - _logger.LogInformation("Обучение модели"); - var model = pipeline.Fit(trainData); - _regressionEngine = _ml.Model.CreatePredictionEngine(model); + if (trainDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); - model.Dispose(); + int rowCount = trainDataSet.Count(); + double[][] featuresArray = trainDataSet.Select(d => d.Features.Select(f => (double)f).ToArray()).ToArray(); + Matrix matrix = DenseMatrix.OfRowArrays(featuresArray); + for (int i = 0; i < IMAGES_COUNT; i++) + { + matrix = matrix.RemoveColumn(i * 3); + matrix = matrix.RemoveColumn(i * 3); + matrix = matrix.RemoveColumn(i * 3); + } + cancellationToken.ThrowIfCancellationRequested(); + + Vector meanVector = matrix.ColumnSums() / matrix.RowCount; + Matrix covarianceMatrix = MatrixCovariance(matrix, meanVector); + Matrix invCovarianceMatrix = covarianceMatrix.Inverse(); + trainDataSet = trainDataSet + .Where((data, index) => + { + var row = matrix.Row(index); + double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); + + return distance <= FILTERING_THRESHOLD; + }) + .ToList(); + _logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count()); + var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); + cancellationToken.ThrowIfCancellationRequested(); + + _logger.LogInformation("Создание конвейера"); + var pipeline = _ml.Regression.Trainers.FastTree(); + cancellationToken.ThrowIfCancellationRequested(); + + _logger.LogInformation("Обучение модели"); + var model = pipeline.Fit(trainData); + _ml.Model.Save(model, trainData.Schema, cachedModelPath); + + _regressionEngine = _ml.Model.CreatePredictionEngine(model); + + model.Dispose(); + } } public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable contours, Mat result)?> Analyze(IEnumerable imagesData)