
Learn natural language processing in C# with ML.NET: text classification, sentiment analysis, tokenization and TF-IDF with runnable code. Start building today.
Natural language processing (NLP) in C# used to mean calling a Python service or a cloud API. That is no longer the case. With ML.NET, Microsoft's open-source machine learning framework for .NET, you can train and run natural language processing in C# entirely inside your own application—no Python, no external runtime, and no per-request fees. This complete guide walks through the core NLP concepts, builds a working sentiment analysis model, shows multi-class text classification, and covers the best practices and pitfalls you will hit in production.
What Is Natural Language Processing in C# and Why ML.NET?
NLP is the branch of machine learning that teaches software to understand human text: classifying reviews as positive or negative, routing support tickets, detecting spam, extracting topics, and more. Under the hood, every NLP model does the same two things:
- Featurization – convert raw text into numbers (vectors) a model can learn from.
- Learning – fit an algorithm (logistic regression, gradient boosting, a neural network) to those vectors.
ML.NET is compelling for .NET developers for three reasons. First, it is native: models are trained and consumed with ordinary C# classes and run on the same CLR as your ASP.NET Core or desktop app. Second, it ships production-grade text transforms (FeaturizeText, n-grams, TF-IDF, stop-word removal) as composable pipeline steps. Third, since ML.NET 2.0 it includes a TextClassification trainer that fine-tunes a pretrained NAS-BERT transformer, giving you modern deep-learning accuracy with a few lines of code.
Setting Up ML.NET for NLP
Create a console project and add the packages:
dotnet new console -n NlpDemo
cd NlpDemo
dotnet add package Microsoft.ML
Every ML.NET program starts with an MLContext, the entry point for loading data, building pipelines, and evaluating models. Seed it for reproducible results while you experiment:
using Microsoft.ML;
using Microsoft.ML.Data;
var mlContext = new MLContext(seed: 42);
Defining Input and Output Schemas
ML.NET is strongly typed. You describe your training rows and prediction output as plain C# classes. LoadColumn maps a CSV/TSV column to a property; ColumnName lets the output match the names the trainer produces.
public class ReviewData
{
[LoadColumn(0)] public string Text { get; set; } = string.Empty;
[LoadColumn(1)] public bool Label { get; set; } // true = positive
}
public class ReviewPrediction
{
[ColumnName("PredictedLabel")] public bool IsPositive { get; set; }
public float Probability { get; set; }
public float Score { get; set; }
}
C# Sentiment Analysis: Your First NLP Model
Sentiment analysis is binary classification, which makes it the ideal first project. The example below trains on an in-memory list so it runs anywhere; in real projects you would call mlContext.Data.LoadFromTextFile<ReviewData>(path, separatorChar: '\t', hasHeader: true) against a dataset with thousands of rows.
using Microsoft.ML;
using Microsoft.ML.Data;
var mlContext = new MLContext(seed: 42);
var samples = new List<ReviewData>
{
new() { Text = "Absolutely loved this product, works perfectly", Label = true },
new() { Text = "Fantastic quality and fast shipping", Label = true },
new() { Text = "Great value, would buy again", Label = true },
new() { Text = "Exceeded my expectations in every way", Label = true },
new() { Text = "Terrible, broke after two days", Label = false },
new() { Text = "Waste of money, very disappointed", Label = false },
new() { Text = "Customer service was rude and unhelpful", Label = false },
new() { Text = "Cheap plastic, stopped working immediately", Label = false },
};
IDataView data = mlContext.Data.LoadFromEnumerable(samples);
var split = mlContext.Data.TrainTestSplit(data, testFraction: 0.25);
// 1. Featurize text -> numeric vector. 2. Train a binary classifier.
var pipeline = mlContext.Transforms.Text
.FeaturizeText("Features", nameof(ReviewData.Text))
.Append(mlContext.BinaryClassification.Trainers
.SdcaLogisticRegression(labelColumnName: "Label", featureColumnName: "Features"));
ITransformer model = pipeline.Fit(split.TrainSet);
// Evaluate
var predictions = model.Transform(split.TestSet);
var metrics = mlContext.BinaryClassification.Evaluate(predictions, labelColumnName: "Label");
Console.WriteLine($"Accuracy: {metrics.Accuracy:P2} AUC: {metrics.AreaUnderRocCurve:P2} F1: {metrics.F1Score:P2}");
// Predict a single sentence
var engine = mlContext.Model.CreatePredictionEngine<ReviewData, ReviewPrediction>(model);
var result = engine.Predict(new ReviewData { Text = "This is the best purchase I have made all year" });
Console.WriteLine($"Positive: {result.IsPositive} Probability: {result.Probability:P1}");
// Persist for later use in a web app or service
mlContext.Model.Save(model, data.Schema, "sentiment.zip");
Why this works: FeaturizeText is a bundle of transforms—text normalization (lower-casing, punctuation removal), tokenization into words, word and character n-gram counting, and TF-IDF-style weighting—that produces a sparse float vector. Logistic regression via SDCA (Stochastic Dual Coordinate Ascent) then learns which n-grams push a review toward positive or negative. It trains in milliseconds and is remarkably strong on short text.
Understanding Text Featurization in ML.NET
The one-line FeaturizeText is convenient, but knowing the individual steps lets you tune accuracy and diagnose problems. Here is the same featurization built explicitly:
using Microsoft.ML.Transforms.Text;
var explicitPipeline = mlContext.Transforms.Text
.NormalizeText("NormText", nameof(ReviewData.Text),
caseMode: TextNormalizingEstimator.CaseMode.Lower,
keepDiacritics: false, keepPunctuations: false, keepNumbers: true)
.Append(mlContext.Transforms.Text.TokenizeIntoWords("Tokens", "NormText"))
.Append(mlContext.Transforms.Text.RemoveDefaultStopWords("CleanTokens", "Tokens"))
.Append(mlContext.Transforms.Conversion.MapValueToKey("KeyTokens", "CleanTokens"))
.Append(mlContext.Transforms.Text.ProduceNgrams("Features", "KeyTokens",
ngramLength: 2, useAllLengths: true,
weighting: NgramExtractingEstimator.WeightingCriteria.TfIdf))
.Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression());
- NormalizeText reduces vocabulary size so "Great" and "great!" become one feature.
- TokenizeIntoWords splits on whitespace and punctuation.
- RemoveDefaultStopWords drops "the", "and", "is"—words that carry almost no signal. Be careful: for sentiment, "not" is a stop word in some lists yet flips meaning. Use
RemoveStopWordswith a custom list if that bites you. - ProduceNgrams with
ngramLength: 2captures phrases like "not good", which unigrams alone cannot distinguish from "good". - TF-IDF weighting boosts rare, informative words and downweights ones appearing in every document.
You can also plug in pretrained word embeddings (GloVe, fastText) with ApplyWordEmbedding, which maps each token to a dense semantic vector so the model generalizes to words it never saw in training.
Multi-Class Text Classification with ML.NET
Most business NLP is not binary. Support-ticket routing, intent detection, and news categorization all need multi-class classification. ML.NET handles it by mapping string labels to keys, training a one-versus-all or multinomial trainer, and mapping keys back to strings for output.
public class TicketData
{
[LoadColumn(0)] public string Text { get; set; } = string.Empty;
[LoadColumn(1)] public string Category { get; set; } = string.Empty;
}
public class TicketPrediction
{
[ColumnName("PredictedLabel")] public string Category { get; set; } = string.Empty;
public float[] Score { get; set; } = Array.Empty<float>();
}
var tickets = new List<TicketData>
{
new() { Text = "I was charged twice on my invoice", Category = "Billing" },
new() { Text = "Refund has not appeared on my card", Category = "Billing" },
new() { Text = "The app crashes when I open settings", Category = "Bug" },
new() { Text = "Login button does nothing on Android", Category = "Bug" },
new() { Text = "Please add dark mode", Category = "Feature" },
new() { Text = "It would be great to export to CSV", Category = "Feature" },
};
var ticketData = mlContext.Data.LoadFromEnumerable(tickets);
var multiPipeline = mlContext.Transforms.Conversion
.MapValueToKey("Label", nameof(TicketData.Category))
.Append(mlContext.Transforms.Text.FeaturizeText("Features", nameof(TicketData.Text)))
.Append(mlContext.MulticlassClassification.Trainers
.SdcaMaximumEntropy(labelColumnName: "Label", featureColumnName: "Features"))
.Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
var ticketModel = multiPipeline.Fit(ticketData);
var ticketEngine = mlContext.Model
.CreatePredictionEngine<TicketData, TicketPrediction>(ticketModel);
var p = ticketEngine.Predict(new TicketData { Text = "My subscription was billed at the wrong price" });
Console.WriteLine($"Routed to: {p.Category}");
Use mlContext.MulticlassClassification.Evaluate(...) and inspect MacroAccuracy and LogLoss. Macro accuracy averages per-class accuracy, so it exposes a model that only ever predicts the majority class—a common failure with imbalanced ticket data.
Advanced NLP in C#: Transformer-Based Text Classification
N-gram models plateau on nuanced text: sarcasm, long documents, domain jargon. ML.NET 2.0+ adds a TextClassification trainer that fine-tunes a pretrained NAS-BERT model via TorchSharp. Accuracy jumps significantly, at the cost of larger dependencies and slower training.
// dotnet add package Microsoft.ML.TorchSharp
// dotnet add package TorchSharp-cpu (or TorchSharp-cuda-windows for GPU)
using Microsoft.ML.TorchSharp;
var bertPipeline = mlContext.Transforms.Conversion
.MapValueToKey("Label", nameof(TicketData.Category))
.Append(mlContext.MulticlassClassification.Trainers.TextClassification(
labelColumnName: "Label",
sentence1ColumnName: nameof(TicketData.Text),
maxEpochs: 10,
batchSize: 16))
.Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
var bertModel = bertPipeline.Fit(ticketData);
Reach for this when you have a few thousand labelled examples and accuracy matters more than a 300 MB model footprint. Also note the CPU/GPU package: install TorchSharp-cpu for portability, or a CUDA package when a GPU is available.
Best Practices for Production NLP with ML.NET
- Never use PredictionEngine as a singleton in ASP.NET Core. It is not thread-safe. Add
Microsoft.Extensions.MLand registerservices.AddPredictionEnginePool<ReviewData, ReviewPrediction>().FromFile("sentiment.zip"), then injectPredictionEnginePoolinto controllers. The pool also supports hot-reloading a retrained model file. - Use real datasets and a held-out test set. The toy lists above demonstrate the API; accuracy numbers only mean something with hundreds or thousands of rows. Use
CrossValidatefor small datasets to get a stable estimate. - Automate model selection with AutoML.
Microsoft.ML.AutoMLtries featurizers and trainers for you:mlContext.Auto().CreateBinaryClassificationExperiment(60).Execute(train, "Label"). - Version your models. Save the model zip alongside the training data hash and metrics so you can reproduce and roll back.
- Clean input the same way at inference time. Because transforms live inside the saved pipeline, this happens automatically—one of ML.NET's biggest advantages over hand-rolled preprocessing.
- Watch class imbalance. If 95% of tickets are "Bug", accuracy is meaningless; track F1, macro accuracy, and the confusion matrix instead.
Common Pitfalls and How to Avoid Them
- Schema mismatches: the label column must be
boolfor binary trainers and a key type (viaMapValueToKey) for multi-class trainers. Astringlabel passed straight to a trainer throws a confusing schema exception. - Forgetting MapKeyToValue: without it, your multi-class predictions come back as key integers instead of category names.
- Overfitting on tiny data: 100% training accuracy with eight samples is not a good model. Check test metrics.
- Stop-word removal killing negation: verify "not", "no", and "never" survive preprocessing for sentiment tasks.
- Slow first prediction: model loading and JIT warm-up take time; load the model at startup, not on the first request.
- Locale issues: numeric CSV parsing is culture-sensitive in some environments; set
CultureInfo.InvariantCulturewhen loading data yourself.
Conclusion: Key Takeaways for Natural Language Processing in C#
Natural language processing in C# is fully practical today thanks to ML.NET. You define typed schemas, chain text transforms into a pipeline, train with a classic or transformer-based trainer, evaluate with meaningful metrics, and deploy the saved model in any .NET app. Key takeaways:
FeaturizeTextplusSdcaLogisticRegressiongets you a fast, solid sentiment analysis baseline in under 20 lines.- Understand the featurization steps—normalization, tokenization, n-grams, TF-IDF—so you can tune them when accuracy plateaus.
- Use
MapValueToKey/MapKeyToValuefor multi-class text classification and evaluate with macro accuracy. - Upgrade to the
TextClassificationtransformer trainer when you have enough labelled data and need state-of-the-art accuracy. - In production, use
PredictionEnginePool, real datasets, model versioning, and imbalance-aware metrics.
Start with the sentiment example above, swap in your own data, and you will have a working NLP model running natively in C# before the end of the day.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment