Skip to content

Commit 125d05a

Browse files
luisquintanillaCESARDELATORRE
authored andcommitted
Model Builder Sentiment Analysis Razor Pages Sample (dotnet#609)
* Model Builder Sentiment Analysis Razor Pages Sample * Renaming Razor Pages project folder * Adding README and sample image * Updating link to image * Updated csproj files with MLVersion placeholders * Update samples/modelbuilder/BinaryClassification_Sentiment_Razor/README.md Co-Authored-By: Justin Ormont <[email protected]> * Updated sample to use labels instead of raw scores * Updated based on feedback * Removed file and hard-coded sample for prediction instead * Updating training code * Updated layout * Updated csproj file
1 parent e047118 commit 125d05a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+40213
-0
lines changed
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
3+
# Sentiment Analysis: Razor Pages sample optimized for scalability and performance when running/scoring an ML.NET model built with Model Builder (Using the new '.NET Core Integration Package for ML.NET')
4+
5+
6+
| ML.NET version | Status | App Type | Data type | Scenario | ML Task | Algorithms |
7+
|----------------|-------------------------------|-------------|-----------|---------------------|---------------------------|-----------------------------|
8+
| v1.3.1 | Up-to-date | Razor Pages | Single data sample | Sentiment Analysis | Binary classification | Linear Classification |
9+
10+
# Goal
11+
12+
The goal is to be able to make **SENTIMENT ANALYSIS** prediction/detection of what the user is writing in a very **UI interactive app** in the client side and running an **ML.NET model** (Sentiment analysis based on binary-classification) built with Model Builder in the server side.
13+
14+
Here's a screenshot of the app while running:
15+
16+
![Blazor App running](./Images/web-app.png)
17+
18+
From ML.NET perspective, the goal is to **optimize the ML.NET model executions in the server** by sharing the ML.NET objects used for predictions across Http requests and being able to implement very simple code to be used by the user when predicting, like the following line of code that you could write on any ASP.NET Core handler method or custom service class:
19+
20+
```csharp
21+
ModelOutput prediction = _predictionEnginePool.Predict(input);
22+
```
23+
24+
As simple as a single line. The object _predictionEnginePool will be injected in the controller's constructor or into you custom class.
25+
26+
Internally, it is optimized so the object dependencies are cached and shared across Http requests with minimum overhead when creating those objects.
27+
28+
# Architecture
29+
30+
For this sample we chose to run the ML.NET model in the server side, so the model is protected within the service.
31+
32+
Running the model in the server side allows you to protect the model plus the following benefits:
33+
34+
- The service could be consumed by other remote apps
35+
36+
# Problem
37+
38+
The problem running/scoring an ML.NET model in multi-threaded applications comes when you want to do single predictions with the PredictionEngine object and you want to cache that object (i.e. as Singleton) so it is being reused by multiple Http requests (therefore it would be accessed by multiple threads). That's is a problem because **the Prediction Engine is not thread-safe** ([ML.NET issue, Nov 2018](https://github.com/dotnet/machinelearning/issues/1718)).
39+
40+
# Solution
41+
42+
## Use the new '.NET Core Integration Package' that implements Object Pooling of PredictionEngine objects for you
43+
44+
**'.NET Core Integration Package' NuGet**
45+
46+
Package name: **Microsoft.Extensions.ML**
47+
48+
Package version: 0.15.1-Preview
49+
50+
Basically, with this component, you inject/use the PredictionEngine object pooling in a single line in your Startup.cs, like the following:
51+
52+
```csharp
53+
services.AddPredictionEnginePool<ModelInput, ModelOutput>()
54+
.FromFile(_modelPath);
55+
```
56+
57+
Then you just need to call the Predict() function from the injected PredictionEnginePool, like the following code you can implement on any controller:
58+
59+
```csharp
60+
//Predict sentiment
61+
ModelOutput prediction = _predictionEnginePool.Predict(input);
62+
```
63+
64+
It is that simple.
65+
66+
For a much more detailed explanation of a PredictionEngine Object Pool comparable to the implementation done in the new '.NET Core Integration Package', including design diagrams, read the following blog post:
67+
68+
**Detailed Blog Post** for further documentation:
69+
70+
[How to optimize and run ML.NET models on scalable ASP.NET Core WebAPIs or web apps](https://devblogs.microsoft.com/cesardelatorre/how-to-optimize-and-run-ml-net-models-on-scalable-asp-net-core-webapis-or-web-apps/)
71+
72+
NOTE: YOU DON'T NEED TO MAKE THAT IMPLEMENTATION EXPLAINED IN THE BLOG POST.
73+
PRECISELY THAT IS IMPLEMENTED FOR YOU IN THE '.NET INTEGRATION PACKAGE'.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.28307.779
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SentimentRazor", "SentimentRazor\SentimentRazor.csproj", "{C5B15F34-91FA-4D93-A553-DF5242B4DA00}"
7+
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SentimentRazorML.Model", "SentimentRazorML.Model\SentimentRazorML.Model.csproj", "{D3DD62B8-209B-4191-928A-266232702E8E}"
9+
EndProject
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SentimentRazorML.ConsoleApp", "SentimentRazorML.ConsoleApp\SentimentRazorML.ConsoleApp.csproj", "{2D4C061F-A8E1-4242-9373-3AD534914730}"
11+
EndProject
12+
Global
13+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14+
Debug|Any CPU = Debug|Any CPU
15+
Release|Any CPU = Release|Any CPU
16+
EndGlobalSection
17+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
18+
{C5B15F34-91FA-4D93-A553-DF5242B4DA00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19+
{C5B15F34-91FA-4D93-A553-DF5242B4DA00}.Debug|Any CPU.Build.0 = Debug|Any CPU
20+
{C5B15F34-91FA-4D93-A553-DF5242B4DA00}.Release|Any CPU.ActiveCfg = Release|Any CPU
21+
{C5B15F34-91FA-4D93-A553-DF5242B4DA00}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{D3DD62B8-209B-4191-928A-266232702E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{D3DD62B8-209B-4191-928A-266232702E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{D3DD62B8-209B-4191-928A-266232702E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{D3DD62B8-209B-4191-928A-266232702E8E}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{2D4C061F-A8E1-4242-9373-3AD534914730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{2D4C061F-A8E1-4242-9373-3AD534914730}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{2D4C061F-A8E1-4242-9373-3AD534914730}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{2D4C061F-A8E1-4242-9373-3AD534914730}.Release|Any CPU.Build.0 = Release|Any CPU
30+
EndGlobalSection
31+
GlobalSection(SolutionProperties) = preSolution
32+
HideSolutionNode = FALSE
33+
EndGlobalSection
34+
GlobalSection(ExtensibilityGlobals) = postSolution
35+
SolutionGuid = {17515FD9-FD1E-4F18-AEBA-C76830108EFE}
36+
EndGlobalSection
37+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@page
2+
@model ErrorModel
3+
@{
4+
ViewData["Title"] = "Error";
5+
}
6+
7+
<h1 class="text-danger">Error.</h1>
8+
<h2 class="text-danger">An error occurred while processing your request.</h2>
9+
10+
@if (Model.ShowRequestId)
11+
{
12+
<p>
13+
<strong>Request ID:</strong> <code>@Model.RequestId</code>
14+
</p>
15+
}
16+
17+
<h3>Development Mode</h3>
18+
<p>
19+
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
20+
</p>
21+
<p>
22+
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
23+
It can result in displaying sensitive information from exceptions to end users.
24+
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
25+
and restarting the app.
26+
</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.RazorPages;
8+
9+
namespace SentimentAnalysisRazorPages.Pages
10+
{
11+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
12+
public class ErrorModel : PageModel
13+
{
14+
public string RequestId { get; set; }
15+
16+
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
17+
18+
public void OnGet()
19+
{
20+
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@page
2+
@model IndexModel
3+
@{
4+
ViewData["Title"] = "Home page";
5+
}
6+
7+
<div class="text-center">
8+
<h2>Live Sentiment</h2>
9+
10+
<p><textarea id="Message" cols="45" placeholder="Type any text like a short review"></textarea></p>
11+
12+
<div class="sentiment">
13+
<h4>Your sentiment is...</h4>
14+
<p>😡 😐 😍</p>
15+
16+
<div class="marker">
17+
<div id="markerPosition" style="left: 45%;">
18+
<div>▲</div>
19+
<label id="markerValue">Neutral</label>
20+
</div>
21+
</div>
22+
</div>
23+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.AspNetCore.Mvc.RazorPages;
7+
using Microsoft.Extensions.ML;
8+
using SentimentRazorML.Model.DataModels;
9+
10+
namespace SentimentAnalysisRazorPages.Pages
11+
{
12+
public class IndexModel : PageModel
13+
{
14+
private readonly PredictionEnginePool<ModelInput, ModelOutput> _predictionEnginePool;
15+
16+
public IndexModel(PredictionEnginePool<ModelInput, ModelOutput> predictionEnginePool)
17+
{
18+
_predictionEnginePool = predictionEnginePool;
19+
}
20+
21+
public void OnGet()
22+
{
23+
24+
}
25+
26+
public IActionResult OnGetAnalyzeSentiment([FromQuery] string text)
27+
{
28+
var input = new ModelInput { Comment = text };
29+
var prediction = _predictionEnginePool.Predict(input);
30+
var sentiment = Convert.ToBoolean(prediction.Prediction) ? "Positive" : "Negative";
31+
return Content(sentiment);
32+
33+
//return Content(percentage.ToString("0.0"));
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@page
2+
@model PrivacyModel
3+
@{
4+
ViewData["Title"] = "Privacy Policy";
5+
}
6+
<h1>@ViewData["Title"]</h1>
7+
8+
<p>Use this page to detail your site's privacy policy.</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.AspNetCore.Mvc.RazorPages;
7+
8+
namespace SentimentAnalysisRazorPages.Pages
9+
{
10+
public class PrivacyModel : PageModel
11+
{
12+
public void OnGet()
13+
{
14+
}
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@using Microsoft.AspNetCore.Http.Features
2+
3+
@{
4+
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
5+
var showBanner = !consentFeature?.CanTrack ?? false;
6+
var cookieString = consentFeature?.CreateConsentCookie();
7+
}
8+
9+
@if (showBanner)
10+
{
11+
<div id="cookieConsent" class="alert alert-info alert-dismissible fade show" role="alert">
12+
Use this space to summarize your privacy and cookie use policy. <a asp-page="/Privacy">Learn More</a>.
13+
<button type="button" class="accept-policy close" data-dismiss="alert" aria-label="Close" data-cookie-string="@cookieString">
14+
<span aria-hidden="true">Accept</span>
15+
</button>
16+
</div>
17+
<script>
18+
(function () {
19+
var button = document.querySelector("#cookieConsent button[data-cookie-string]");
20+
button.addEventListener("click", function (event) {
21+
document.cookie = button.dataset.cookieString;
22+
}, false);
23+
})();
24+
</script>
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>@ViewData["Title"] - SentimentRazor</title>
7+
8+
<environment include="Development">
9+
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
10+
</environment>
11+
<environment exclude="Development">
12+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
13+
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
14+
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
15+
crossorigin="anonymous"
16+
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/>
17+
</environment>
18+
<link rel="stylesheet" href="~/css/site.css" />
19+
</head>
20+
<body>
21+
<header>
22+
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
23+
<div class="container">
24+
<a class="navbar-brand" asp-area="" asp-page="/Index">SentimentRazor</a>
25+
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
26+
aria-expanded="false" aria-label="Toggle navigation">
27+
<span class="navbar-toggler-icon"></span>
28+
</button>
29+
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
30+
<ul class="navbar-nav flex-grow-1">
31+
<li class="nav-item">
32+
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
33+
</li>
34+
<li class="nav-item">
35+
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
36+
</li>
37+
</ul>
38+
</div>
39+
</div>
40+
</nav>
41+
</header>
42+
<div class="container">
43+
<partial name="_CookieConsentPartial" />
44+
<main role="main" class="pb-3">
45+
@RenderBody()
46+
</main>
47+
</div>
48+
49+
<footer class="border-top footer text-muted">
50+
<div class="container">
51+
&copy; 2019 - SentimentRazor - <a asp-area="" asp-page="/Privacy">Privacy</a>
52+
</div>
53+
</footer>
54+
55+
<environment include="Development">
56+
<script src="~/lib/jquery/dist/jquery.js"></script>
57+
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
58+
</environment>
59+
<environment exclude="Development">
60+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
61+
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
62+
asp-fallback-test="window.jQuery"
63+
crossorigin="anonymous"
64+
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
65+
</script>
66+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"
67+
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
68+
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
69+
crossorigin="anonymous"
70+
integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">
71+
</script>
72+
</environment>
73+
<script src="~/js/site.js" asp-append-version="true"></script>
74+
75+
@RenderSection("Scripts", required: false)
76+
</body>
77+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<environment include="Development">
2+
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
3+
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
4+
</environment>
5+
<environment exclude="Development">
6+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"
7+
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
8+
asp-fallback-test="window.jQuery && window.jQuery.validator"
9+
crossorigin="anonymous"
10+
integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
11+
</script>
12+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
13+
asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
14+
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
15+
crossorigin="anonymous"
16+
integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
17+
</script>
18+
</environment>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@using SentimentAnalysisRazorPages
2+
@namespace SentimentAnalysisRazorPages.Pages
3+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@{
2+
Layout = "_Layout";
3+
}

0 commit comments

Comments
 (0)