添加你自己的 .NET 模块:情感分析
引言
本文将介绍如何通过改编现有代码和添加适配器来为 CodeProject.AI 服务器创建 .NET 模块。这将使 CodeProject.AI 服务器能够通过 HTTP 端点公开原始代码的功能。该过程与将自己的 Python 模块添加到 CodeProject.AI 类似,只是我们将使用 .NET 而不是 Python。
再次:确保您已阅读将新模块添加到 CodeProject.AI,然后再开始。
选择一个模块
您可以从头开始编写自己的模块,或者使用当前可用的众多开源 AI 项目之一。那里有成千上万的项目,对于本文,我选择了DotNet Samples 存储库中的TextClassificationTF 示例。该项目接收一些文本并返回文本是积极还是消极的情感。
将模块添加到 CodeProject.AI
首先,请确保您已从 GitHub 克隆(或下载)了CodeProject.AI 代码。
要将我们选择的模块添加到 CodeProject.AI,我们将需要执行以下操作(稍后我们将详细介绍每个步骤):
-
在 CodeProject.AI 解决方案中创建一个新项目来存放我们的模块。
-
在项目中创建一个 modulesettings.json 文件,用于配置 CodeProject.AI 服务器为此模块公开的端点。CodeProject.AI 服务器将在启动时发现此文件,配置定义的端点,并启动模块的可执行文件。
-
将您希望添加的模块的代码复制到您刚刚创建的项目中。为了整洁起见,最好在项目中创建一个子文件夹,其中包含其他模块的整个代码。
目标是确保易于包含更新。您使用的代码无疑会随着时间的推移而更新。能够获取更新后的代码并将其直接放入同一个子文件夹中,这很不错。即时升级。或多或少需要一些调整。
-
添加对原始代码引用的任何 NuGet 包的引用。
-
如果需要,请重构复制的代码以供通用使用。这通常是必需的,因为您找到的许多代码片段可能包含硬编码的值或位置,可能被设计为作为 API、命令行调用,或者位于 Web 应用程序控制器深处。
您可能需要进行一些(希望是小)更改,以公开适配器可以调用的函数,或者用通过环境变量提供的值替换硬编码的值。再次:您需要进行的更改越少越好,但有些可能是不可避免的。
-
创建一个 CodeProject.AI 适配器,通过派生一个类来处理从 CodeProject.AI 服务器接收的请求并返回响应。您将派生自一个抽象基类,并且只需提供一个处理请求的方法。所有服务器/模块通信和错误处理的样板代码都已为您处理。
-
对项目
Program.cs
文件进行少量修改,以配置程序运行上述代码。 -
测试。可以使用 Postman 等工具或编写一个简单的网页来调用CodeProject.AI 服务器上的新端点来进行测试。
创建模块项目
当您用 .NET 6 编写 CodeProject.AI 模块时,您创建的内容会轮询 CodeProject.AI 服务器以从为该模块创建的队列中获取命令。最简单的方法是在src/modules
下的文件夹中创建一个Worker Service项目。CodeProject.AI 服务器会扫描此文件夹中的目录以获取模块元数据,这使得服务器能够启动模块。
执行此操作的步骤如下:
-
在解决方案资源管理器中右键单击src/modules文件夹
- 选择
添加
->新建项目
- 选择 C# 的 **Worker Service** 项目模板
- 点击
下一步
- 选择
这将打开项目配置
对话框
- 将
项目名称
设置为SentimentAnalysis - 将
位置
设置为 CodeProject.AI 解决方案副本中的src\modules目录。 - 点击
下一步
。
这将打开其他信息
对话框
我们无需在此处更改任何内容,因此请点击创建
。这将创建具有以下结构的项:
创建 modulesettings.json 文件
modulesettings.json
文件配置了模块的
- 是否应启动它
- 如何启动它
- 它运行的平台
- CodeProject.AI 服务器将为此模块公开的端点。在这种情况下,我们将
- 公开
https://:32168/v1/text/sentiment
- 使用 HTTP POST 方法
- 发送一个表单变量
text
,其中包含要分析的文本 - 并期望一个 JSON 载荷响应,其中包含
- 一个布尔值
success
属性,指示操作是否成功完成 - 一个布尔值
is_positive
属性,指示输入文本是否为积极情感 - 一个浮点值
positive_probability
,表示输入文本具有积极情感的概率,其中 0.5 为中性。
- 一个布尔值
- 公开
在我们的案例中,modulesettings.json
文件将如下所示:
{
"Modules": {
"SentimentAnalysis": {
"Name": "Sentiment Analysis",
"Version": "1.1",
// Publishing info
"Description": "Provides an analysis of the sentiment of a piece of text. Positive or negative?",
"Platforms": [ "windows", "macos" ],
"License": "CC-BY-4.0",
"LicenseUrl": "https://github.com/dotnet/samples/blob/main/LICENSE",
// Launch instructions
"AutoStart": true,
"FilePath": "SentimentAnalysis.exe",
"Runtime": "execute",
"RuntimeLocation": "Shared", // Can be Local or Shared. .NET so moot point here
// Which server version is compatible with each version of this module.
"ModuleReleases": [
{ "ModuleVersion": "1.1", "ServerVersionRange": [ "2.1", "" ], "ReleaseDate": "2023-03-20" }
],
"RouteMaps": [
{
"Name": "Sentiment Analysis",
"Path": "text/sentiment",
"Method": "POST",
"Command": "sentiment",
"Description": "Determines if the supplied text has a positive or negative sentiment",
"Inputs": [
{
"Name": "text",
"Type": "Text",
"Description": "The text to be analyzed."
}
],
"Outputs": [
{
"Name": "success",
"Type": "Boolean",
"Description": "True if successful."
},
{
"Name": "is_positive",
"Type": "Boolean",
"Description": "Whether the input text had a positive sentiment."
},
{
"Name": "positive_probability",
"Type": "Float",
"Description": "The probability the input text has a positive sentiment."
},
{
"Name": "inferenceMs",
"Type": "Integer",
"Description": "The time (ms) to perform the AI inference."
},
{
"Name": "processMs",
"Type": "Integer",
"Description": "The time (ms) to process the image (includes inference and text manipulation operations)."
},
{
"Name": "analysisRoundTripMs",
"Type": "Integer",
"Description": "The time (ms) for the round trip to the analysis module and back."
}
]
}
]
}
}
}
modulesettings.development.json
此文件会覆盖modulesettings.json
文件中针对开发环境的一些值。在这种情况下,可执行文件的位置将在bin\debug\net7.0
目录中找到,而不是模块的根文件夹,因此我们强制工作目录为模块的目录,并更新要执行的文件的位置,相对于模块的文件夹。
{
"Modules": {
"SentimentAnalysis": {
"FilePath": "bin\\debug\\net7.0\\SentimentAnalysis.exe"
}
}
}
从示例代码复制代码和资源
情感分析代码使用的数据和模型包含在示例存储库的sentiment_model
文件夹中。将此文件夹复制到新模块项目中。
使用数据和模型的所有代码都包含在示例代码的Program.cs
文件中。要复制此代码
- 创建一个新的类文件
TextClassifier.cs
- 用示例代码
Program.cs
文件中的Program
类的内容替换此文件中的TextClassifier
类的内容。我们将在下一步进行修复。
包含其他 NuGet 和项目依赖项
为了构建此项目,必须包含一些依赖项
- 使用 Microsoft 的 ML.NET 框架及其对 TensorFlow 模型的支持所需的 NuGet 包。
Microsoft.ML
Microsoft.ML.SampleUtils
Microsoft.ML.TensorFlow
SciScharp.TensorFlow.Redist
- 使用 CodeProject.AI .NET SDK 的项目
CodeProject.AI.AnalsisLayer.SDK
重构示例代码以供我们使用
来自示例代码的代码旨在作为特定的示例,其中包含硬编码的输入和大量Console.WriteLine
语句,以显示代码操作的大量详细信息。我们通过以下方式更新了代码:
- 将 main() 方法转换为 TextClassifier 类构造函数
- 将一些变量设为字段
- 更改 PredictSentiment 方法以接受参数而不是使用硬编码值。
由于更改的实际细节不是我们本文的目标,因此此处不显示代码。可以在存储库中的代码中看到这些更改的结果。
创建请求处理器类
倒数第二个编码步骤是创建后台工作程序,该工作程序将从 CodeProject.AI 服务器检索请求,处理请求,然后返回结果。使用更新后的 SDK,其中大部分已被封装在抽象类ModuleWorkerBase
中。我们只需创建一个新的类文件SentimentAnalysisWorker.cs
,在该文件中
- 创建一个响应类
SentimentAnalysisResponse
,派生自BackendSuccessResponse
,它定义了模块响应的结构。 - 覆盖
SentimentAnalysisWorker.ProcessRequest
方法,并 - 创建
SentimentAnalysisWorker
构造函数以初始化模块特有的功能。
完成的 SentimentAnalysisWorker.cs 文件是
using CodeProject.AI.SDK;
namespace SentimentAnalysis
{
class SentimentAnalysisResponse : BackendSuccessResponse
{
/// <summary>
/// Gets or set a value indicating whether the text is positive.
/// </summary>
public bool? is_positive { get; set; }
/// <summary>
/// Gets or sets the probability of being positive.
/// </summary>
public float? positive_probability { get; set; }
}
public class SentimentAnalysisWorker : ModuleWorkerBase
{
private readonly TextClassifier _textClassifier;
/// <summary>
/// Initializes a new instance of the SentimentAnalysisWorker.
/// </summary>
/// <param name="logger">The Logger.</param>
/// <param name="textClassifier">The TextClassifier.</param>
/// <param name="configuration">The app configuration values.</param>
public SentimentAnalysisWorker(ILogger<SentimentAnalysisWorker> logger,
TextClassifier textClassifier,
IConfiguration configuration)
: base(logger, configuration)
{
_textClassifier = textClassifier;
}
/// <summary>
/// The work happens here.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>The response.</returns>
public override BackendResponseBase ProcessRequest(BackendRequest request)
{
string text = request.payload.GetValue("text");
if (text is null)
return new BackendErrorResponse($"{ModuleName} missing 'text' parameter.");
Stopwatch sw = Stopwatch.StartNew();
var result = _textClassifier.PredictSentiment(text);
long inferenceMs = sw.ElapsedMilliseconds;
if (result is null)
return new BackendErrorResponse($"{ModuleName} PredictSentiment returned null.");
var response = new SentimentAnalysisResponse
{
is_positive = result?.Prediction?[1] > 0.5f,
positive_probability = result?.Prediction?[1],
processMs = inferenceMs,
inferenceMs = inferenceMs
};
return response;
}
}
}
将所有内容连接起来
将所有内容连接起来非常简单。在 Program.cs 文件中
- 更改行
to
- 通过添加以下行将 TextClassifier 添加到 DI 容器
正好在前一行之前。文件应如下所示:
您将希望使 SentimentAnalysis 成为前端项目的构建依赖项,以便在构建CodeProject.AI 服务器时构建它。
进行测试。
为了测试这一点,我创建了一个简单的test.html
页面,它接收一些文本,将其发送到CodeProject.AI 服务器,然后处理并显示结果。它尽可能地简单,以展示新功能的易用性。
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Sentiment Analysis Module Test</title>
<script type="text/javascript">
function doAnalysis(textToSend) {
var formData = new FormData();
formData.append('text', textToSend);
fetch('https://:32168/v1/text/sentiment', {
method: 'POST',
body: formData,
cache: "no-cache"
})
.then(response => {
if (!response.ok) {
result.innerText = `Http error! Status : ${response.status}`;
}
return response.json().then(data => {
var resultHTML = data.is_positive
? `<p>The text sentiment was positive with a probablity of ${data.positive_probability}</p>`
: `<p>The text sentiment was negative with a probablity of ${1.0 - data.positive_probability}</p>`;
result.innerHTML = resultHTML;
});
});
}
</script>
</head>
<body>
<h1>Sentiment Analysis Module Test</h1>
<form method="post" action="" enctype="multipart/form-data" id="myform">
<div>
<label for="textToAnalyze">Text to analyze:</label>
<div>
<textarea id="textToAnalyze" name="textToAnalyze" rows="8" cols="80" style="border:solid thin black"></textarea>
</div>
</div>
<div>
<button type="button" onclick="doAnalysis(textToAnalyze.value)">Submit</button>
</div>
<br />
<div>
<label for="result">Result</label>
<div id="result" name="result" style="border:solid thin black"></div>
</div>
</form>
</body>
</html>
要观看此操作,请在 Debugger 中以 Debug 配置运行Frontend
项目(CodeProject.AI Server),然后使用您选择的浏览器打开test.html
文件。在文本框中复制一些文本并按提交。我使用了亚马逊评论中的文本。您应该会看到与此类似的内容
附加奖励 - 在 Windows 上 XCOPY 部署模块。
.NET 6 模块不像 Python 模块那样依赖于任何运行时安装或虚拟环境的设置。CodeProject.AI 服务器安装程序已经安装了 .NET 6 运行时。因此,可以将其版本的构建版 bin 部署到现有的 CodeProject.AI 服务器 Windows 安装中。执行此操作的步骤是:
- 以
Release
模式构建模块项目。 - 在
c:\Program Files\CodeProject\AI\modules
目录中创建一个文件夹。此文件夹应与modulesettings.json
文件中的模块 ID 同名。在此示例中,即“SentimentAnalysis”。 - 将项目 bin\Release\net7.0 目录的内容复制到上一步创建的目录。
- 使用
Service
应用程序,重新启动CodeProject.AI 服务器服务。现在将在 modulesettings.json 文件中定义的端点上公开新模块。
总结
将新模块添加到 CodeProject.AI 通常并不难。您需要:
- 选择一个自包含的模块,并且可以公开作为方法调用的功能。
- 在 AnalysisServices 文件夹中创建一个项目来容纳您的项目,并复制代码
- 创建一个适配器,它将充当 CodeProject.AI 服务器和您的代码之间的接口
- 确保您拥有模型和依赖项
- 创建一个 modulesettings.json 文件来向服务器描述如何启动模块
- 对您要添加的模块进行任何必要的少量更改,使其能够与您的适配器一起工作,并修改 Program.cs 文件以启动一切
您的模块现在是 CodeProject.AI 生态系统的一部分,任何使用该服务器的客户端都可以无缝访问您的新模块。恭喜!