跳到内容


添加你自己的 .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,我们将需要执行以下操作(稍后我们将详细介绍每个步骤):

  1. 在 CodeProject.AI 解决方案中创建一个新项目来存放我们的模块。

  2. 在项目中创建一个 modulesettings.json 文件,用于配置 CodeProject.AI 服务器为此模块公开的端点。CodeProject.AI 服务器将在启动时发现此文件,配置定义的端点,并启动模块的可执行文件。

  3. 将您希望添加的模块的代码复制到您刚刚创建的项目中。为了整洁起见,最好在项目中创建一个子文件夹,其中包含其他模块的整个代码。

目标是确保易于包含更新。您使用的代码无疑会随着时间的推移而更新。能够获取更新后的代码并将其直接放入同一个子文件夹中,这很不错。即时升级。或多或少需要一些调整。

  1. 添加对原始代码引用的任何 NuGet 包的引用。

  2. 如果需要,请重构复制的代码以供通用使用。这通常是必需的,因为您找到的许多代码片段可能包含硬编码的值或位置,可能被设计为作为 API、命令行调用,或者位于 Web 应用程序控制器深处。

    您可能需要进行一些(希望是小)更改,以公开适配器可以调用的函数,或者用通过环境变量提供的值替换硬编码的值。再次:您需要进行的更改越少越好,但有些可能是不可避免的。

  3. 创建一个 CodeProject.AI 适配器,通过派生一个类来处理从 CodeProject.AI 服务器接收的请求并返回响应。您将派生自一个抽象基类,并且只需提供一个处理请求的方法。所有服务器/模块通信和错误处理的样板代码都已为您处理。

  4. 对项目Program.cs文件进行少量修改,以配置程序运行上述代码。

  5. 测试。可以使用 Postman 等工具或编写一个简单的网页来调用CodeProject.AI 服务器上的新端点来进行测试。

创建模块项目

当您用 .NET 6 编写 CodeProject.AI 模块时,您创建的内容会轮询 CodeProject.AI 服务器以从为该模块创建的队列中获取命令。最简单的方法是在src/modules下的文件夹中创建一个Worker Service项目。CodeProject.AI 服务器会扫描此文件夹中的目录以获取模块元数据,这使得服务器能够启动模块。

执行此操作的步骤如下:

  • 在解决方案资源管理器中右键单击src/modules文件夹

    • 选择添加 -> 新建项目
    • 选择 C# 的 **Worker Service** 项目模板
    • 点击下一步

这将打开项目配置对话框

Configuration Dialog

  • 项目名称设置为SentimentAnalysis
  • 位置设置为 CodeProject.AI 解决方案副本中的src\modules目录。
  • 点击下一步

这将打开其他信息对话框

Additional Information Dialog

我们无需在此处更改任何内容,因此请点击创建。这将创建具有以下结构的项:

Project Structure

创建 modulesettings.json 文件

modulesettings.json 文件配置了模块的

  • 是否应启动它
  • 如何启动它
  • 它运行的平台
  • CodeProject.AI 服务器将为此模块公开的端点。在这种情况下,我们将
    • 公开 https://:32168/v1/text/sentiment
    • 使用 HTTP POST 方法
    • 发送一个表单变量text,其中包含要分析的文本
    • 并期望一个 JSON 载荷响应,其中包含
      • 一个布尔值success属性,指示操作是否成功完成
      • 一个布尔值is_positive属性,指示输入文本是否为积极情感
      • 一个浮点值positive_probability,表示输入文本具有积极情感的概率,其中 0.5 为中性。

在我们的案例中,modulesettings.json 文件将如下所示:

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目录中找到,而不是模块的根文件夹,因此我们强制工作目录为模块的目录,并更新要执行的文件的位置,相对于模块的文件夹。

JSON
{
  "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 文件是

C#
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 文件中

  • 更改行
    C#
    services.AddHostedService<Worker>();
    
    to
    C#
    services.AddHostedService<SentimentAnalysisWorker>();
    
  • 通过添加以下行将 TextClassifier 添加到 DI 容器
    C#
    services.AddSingleton<TextClassifier>();
    
    正好在前一行之前。文件应如下所示:
    C#
    using SentimentAnalysis;
    
    IHost host = Host.CreateDefaultBuilder(args)
        .ConfigureServices(services =>
        {
            services.AddSingleton<TextClassifier>();
            services.AddHostedService<SentimentAnalysisWorker>();
        })
        .Build();
    
    await host.RunAsync();
    

您将希望使 SentimentAnalysis 成为前端项目的构建依赖项,以便在构建CodeProject.AI 服务器时构建它。

进行测试。

为了测试这一点,我创建了一个简单的test.html页面,它接收一些文本,将其发送到CodeProject.AI 服务器,然后处理并显示结果。它尽可能地简单,以展示新功能的易用性。

HTML
<!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 通常并不难。您需要:

  1. 选择一个自包含的模块,并且可以公开作为方法调用的功能。
  2. 在 AnalysisServices 文件夹中创建一个项目来容纳您的项目,并复制代码
  3. 创建一个适配器,它将充当 CodeProject.AI 服务器和您的代码之间的接口
  4. 确保您拥有模型和依赖项
  5. 创建一个 modulesettings.json 文件来向服务器描述如何启动模块
  6. 对您要添加的模块进行任何必要的少量更改,使其能够与您的适配器一起工作,并修改 Program.cs 文件以启动一切

您的模块现在是 CodeProject.AI 生态系统的一部分,任何使用该服务器的客户端都可以无缝访问您的新模块。恭喜!


© . All rights reserved.