Je voulais ajouter de l’analyse de sentiment dans une app Blazor sans appeler une API externe, payer des tokens ou monter un service Python à côté. ML.NET permet de faire ça avec quelques packages NuGet et zéro infrastructure. Le modèle s’entraîne et roule directement dans le processus .NET.
À la fin de cet article, vous aurez une app Blazor fonctionnelle qui prédit si un texte est positif ou négatif.
Un exemple complet et fonctionnel est disponible ici : mongeon/code-examples · blazor-mlnet-sentiment.
C’est quoi ML.NET ?
ML.NET est un framework d’apprentissage automatique open source pour .NET. Il supporte la classification, la régression, le clustering et la détection d’anomalies, tout ça en C#. Pas de Python, pas de service externe. Ça roule dans votre processus, ce qui simplifie le déploiement : vous livrez votre app, c’est tout.
Créer le projet
On crée une nouvelle application web Blazor avec interactivité côté serveur :
dotnet new blazor -o BlazorMLNET --interactivity Server
cd BlazorMLNET
On ajoute les packages ML.NET :
dotnet add package Microsoft.ML --version 5.0.0
dotnet add package Microsoft.ML.FastTree --version 5.0.0
FastTree nous donne un bon algorithme de classification binaire pour l’analyse de sentiment.
Les modèles de données
Créez un dossier Models et ajoutez un fichier SentimentModel.cs :
using Microsoft.ML.Data;
namespace BlazorMLNET.Models
{
public class SentimentData
{
[LoadColumn(0)]
public string SentimentText { get; set; } = string.Empty;
[LoadColumn(1), ColumnName("Label")]
public bool Sentiment { get; set; }
}
public class SentimentPrediction
{
[ColumnName("PredictedLabel")]
public bool Prediction { get; set; }
public float Probability { get; set; }
public float Score { get; set; }
}
}
SentimentData est l’entrée. SentimentPrediction est ce que le modèle retourne : une prédiction, une probabilité et un score brut.
Le service ML
Ajoutez MLService.cs dans le même dossier Models. Ce service gère l’entraînement, le chargement et la prédiction :
using Microsoft.ML;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using System;
using System.Threading.Tasks;
namespace BlazorMLNET.Models
{
public class MLService
{
// Pas thread-safe : correct pour une démo, utilisez PredictionEnginePool en production
private PredictionEngine<SentimentData, SentimentPrediction>? _predictionEngine;
private readonly string _modelPath;
public MLService(IWebHostEnvironment env)
{
_modelPath = Path.Combine(env.WebRootPath, "sentiment_model.zip");
}
public async Task TrainAndSaveModelAsync()
{
await Task.Run(() =>
{
var mlContext = new MLContext();
// Petit jeu de données pour la démo. En production, on utilise des milliers d'exemples
var sampleData = new[]
{
new SentimentData { SentimentText = "J'adore ML.NET !", Sentiment = true },
new SentimentData { SentimentText = "C'est un excellent produit.", Sentiment = true },
new SentimentData { SentimentText = "Je le recommanderais à mes amis.", Sentiment = true },
new SentimentData { SentimentText = "La documentation est claire et utile.", Sentiment = true },
new SentimentData { SentimentText = "L'API est facile à utiliser.", Sentiment = true },
new SentimentData { SentimentText = "Je n'aime pas du tout cela.", Sentiment = false },
new SentimentData { SentimentText = "Ce produit est terrible.", Sentiment = false },
new SentimentData { SentimentText = "J'ai eu une mauvaise expérience avec ceci.", Sentiment = false },
new SentimentData { SentimentText = "Le service client n'était pas utile.", Sentiment = false },
new SentimentData { SentimentText = "Je suis très déçu.", Sentiment = false },
};
var dataView = mlContext.Data.LoadFromEnumerable(sampleData);
var dataSplit = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);
var pipeline = mlContext.Transforms.Text.FeaturizeText("Features", "SentimentText")
.Append(mlContext.BinaryClassification.Trainers.FastTree(numberOfLeaves: 50, numberOfTrees: 50));
Console.WriteLine("Entraînement du modèle...");
var model = pipeline.Fit(dataSplit.TrainSet);
var predictions = model.Transform(dataSplit.TestSet);
var metrics = mlContext.BinaryClassification.Evaluate(predictions);
Console.WriteLine($"Précision : {metrics.Accuracy:P2}, F1 : {metrics.F1Score:P2}");
mlContext.Model.Save(model, dataView.Schema, _modelPath);
_predictionEngine = mlContext.Model.CreatePredictionEngine<SentimentData, SentimentPrediction>(model);
});
}
public bool LoadModel()
{
var mlContext = new MLContext();
if (File.Exists(_modelPath))
{
var model = mlContext.Model.Load(_modelPath, out var _);
_predictionEngine = mlContext.Model.CreatePredictionEngine<SentimentData, SentimentPrediction>(model);
return true;
}
return false;
}
public SentimentPrediction PredictSentiment(string text)
{
if (_predictionEngine == null)
{
LoadModel();
if (_predictionEngine == null)
throw new InvalidOperationException("Le modèle n'est pas chargé. Entraînez-le d'abord.");
}
return _predictionEngine.Predict(new SentimentData { SentimentText = text });
}
}
}
Le service essaie de charger un modèle sauvegardé à la première prédiction. S’il n’y a pas de fichier modèle, PredictSentiment lance une exception, donc il faut entraîner le modèle d’abord. Dans le composant Razor ci-dessous, OnInitializedAsync s’en charge : il tente de charger le modèle, et l’entraîne si le fichier est absent. Dans une vraie app, on entraînerait le modèle hors-ligne avec un jeu de données beaucoup plus gros.
Le composant Razor
Créez SentimentAnalysis.razor dans le dossier Components/Pages :
@page "/sentiment"
@using BlazorMLNET.Models
@inject MLService MLService
<div class="container mt-5">
<h1>Analyse de sentiment avec ML.NET</h1>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Analyseur de sentiment</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="sentimentText">Entrez le texte à analyser :</label>
<textarea
id="sentimentText"
class="form-control"
rows="5"
@bind="InputText"
placeholder="Tapez du texte ici..."></textarea>
</div>
<button class="btn btn-primary mt-3" @onclick="AnalyzeSentiment">
Analyser le sentiment
</button>
@if (isAnalyzing)
{
<div class="mt-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<span class="ms-2">Analyse en cours...</span>
</div>
}
@if (showResult)
{
<div class="mt-4">
<div class="card @(isPredictionPositive ? "border-success" : "border-danger")">
<div class="card-header @(isPredictionPositive ? "bg-success" : "bg-danger") text-white">
<h4 class="mb-0">Résultat</h4>
</div>
<div class="card-body">
<h5 class="card-title">
Sentiment : @(isPredictionPositive ? "Positif" : "Négatif")
</h5>
<p class="card-text">
<strong>Confiance :</strong> @(confidenceScore.ToString("P2"))
</p>
<div class="progress mt-3">
<div class="progress-bar @(isPredictionPositive ? "bg-success" : "bg-danger")"
role="progressbar"
style="width: @(confidenceScore * 100)%"
aria-valuenow="@(confidenceScore * 100)"
aria-valuemin="0"
aria-valuemax="100">
@(confidenceScore.ToString("P0"))
</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
@code {
private string InputText { get; set; } = "";
private bool isAnalyzing = false;
private bool showResult = false;
private bool isPredictionPositive = false;
private float confidenceScore = 0;
protected override async Task OnInitializedAsync()
{
if (!MLService.LoadModel())
await MLService.TrainAndSaveModelAsync();
}
private async Task AnalyzeSentiment()
{
if (string.IsNullOrWhiteSpace(InputText))
return;
isAnalyzing = true;
showResult = false;
try
{
var prediction = await Task.Run(() => MLService.PredictSentiment(InputText));
showResult = true;
isPredictionPositive = prediction.Prediction;
confidenceScore = prediction.Probability;
}
catch (Exception ex)
{
Console.WriteLine($"Erreur : {ex.Message}");
}
finally
{
isAnalyzing = false;
}
}
}
Brancher le tout
Enregistrez le service dans Program.cs. Le template Blazor a déjà l’appel AddRazorComponents, il suffit d’ajouter le singleton MLService :
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton<MLService>();
Et plus bas, assurez-vous d’avoir :
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
Ajoutez un lien dans Components/Layout/NavMenu.razor :
<div class="nav-item px-3">
<NavLink class="nav-link" href="sentiment">
<span class="oi oi-list-rich" aria-hidden="true"></span> Analyse de sentiment
</NavLink>
</div>
Lancez l’app :
dotnet run
Naviguez vers /sentiment et essayez quelques phrases. Le modèle s’entraîne au premier chargement s’il n’y a pas de fichier sauvegardé.
À garder en tête
- Cachez votre PredictionEngine. En créer un est coûteux. Le service ci-dessus le garde en champ statique. Correct pour une démo, mais regardez
PredictionEnginePoolpour la production. - Chargez les modèles hors du thread UI. L’approche
OnInitializedAsyncfonctionne ici, mais pour des modèles plus gros vous voudrez un chargement en arrière-plan. - La taille du modèle compte pour WASM. Avec Blazor Server c’est pas un problème puisque le modèle reste sur le serveur. Pour WebAssembly, le modèle est envoyé au navigateur. Surveillez la taille du fichier.
- Entraînez avec de vraies données. Le jeu de 10 exemples ci-dessus sert juste à faire tourner quelque chose. Un vrai modèle a besoin de milliers d’exemples étiquetés.
Ressources
- Documentation ML.NET
- Référence API ML.NET
- Documentation Blazor
- Model Builder ML.NET
- Référentiel GitHub ML.NET
- Tutoriels ML.NET
ML.NET ne remplacera pas un modèle transformer fine-tuné, mais pour des tâches de classification simples à l’intérieur d’une app .NET, c’est dur à battre. Pas de clé API, pas de latence, pas de facture à la fin du mois. Bonne prédiction !
Cet article a été rédigé avec l’aide de l’IA et révisé par moi.