feat: models caching

This commit is contained in:
2026-08-10 15:09:04 +03:00
parent 67718629ae
commit ec054d88c4
2 changed files with 179 additions and 114 deletions
+1
View File
@@ -3,6 +3,7 @@
##
## Get latest from `dotnet new gitignore`
.models_cache
.zed
*.png
cache
+178 -114
View File
@@ -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<ClusterizationData, ClusterizationPrediction>(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<ClusterizationData, ClusterizationPrediction>(model);
model.Dispose();
_logger.LogInformation("Обучение модели");
var model = pipeline.Fit(trainData);
cancellationToken.ThrowIfCancellationRequested();
_ml.Model.Save(model, trainData.Schema, cachedModelPath);
_clusterizationEngine = _ml.Model.CreatePredictionEngine<ClusterizationData, ClusterizationPrediction>(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<double> 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<RegressionData, RegressionPrediction>(model);
}
cancellationToken.ThrowIfCancellationRequested();
else
{
_logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count());
if (sampleRecords.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\"");
cancellationToken.ThrowIfCancellationRequested();
Vector<double> meanVector = matrix.ColumnSums() / matrix.RowCount;
Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector);
Matrix<double> 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<RegressionData, RegressionPrediction>(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<double> 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<double> meanVector = matrix.ColumnSums() / matrix.RowCount;
Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector);
Matrix<double> 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<RegressionData, RegressionPrediction>(model);
model.Dispose();
}
}
public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, Mat result)?> Analyze(IEnumerable<ImageData> imagesData)