TowardsDataScience 2023 博客中文翻译(五十六)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

构建一个本地运行的语音助手

原文:towardsdatascience.com/build-a-locally-running-voice-assistant-2f2ead904fe9?source=collection_archive---------1-----------------------#2023-12-29

向 LLM 提问而不泄露私人信息

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Sébastien Gilbert

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 12 月 29 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者生成,并由 openart.ai 协助

我不得不承认,我最初对大型语言模型(LLM)生成实际有效的代码片段的能力持怀疑态度。我带着最坏的预期尝试了,结果却感到惊喜。像与任何聊天机器人互动一样,问题的格式很重要,但随着时间的推移,你会学会如何明确你需要帮助的问题的边界。

我习惯了在编写代码时有一个始终可用的在线聊天机器人服务,但当我的雇主发布了禁止员工使用它的公司政策时,我不得不寻找其他方案。我可以回到以前的谷歌习惯,但我决定构建一个本地运行的 LLM 服务,这样我可以在不泄露公司外部信息的情况下提问。感谢HuggingFace上的开源 LLM 提供以及chainlit 项目,我能够组建一个满足编码辅助需求的服务。

下一步是添加一些语音交互。尽管语音不适合编码辅助(你希望看到生成的代码片段,而不是听到它们),但在某些情况下,你需要在创意项目上获得灵感。讲故事的感觉为体验增添了价值。另一方面,你可能不愿意使用在线服务,因为你希望保持工作的隐私。

在这个项目中,我将带你了解构建一个允许你通过语音与开源 LLM 交互的助手的步骤。所有组件都在你的计算机上本地运行。

架构

架构包括三个独立的组件:

  • 一个唤醒词检测服务

  • 一个语音助手服务

  • 一个聊天服务

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

三个组件的流程图。图片由作者提供。

这三个组件是独立的项目,每个项目都有自己的 github 仓库。让我们逐一了解每个组件及其如何交互。

聊天服务

聊天服务运行开源 LLM,名为HuggingFaceH4/zephyr-7b-alpha。该服务通过 POST 调用接收提示,将提示传递给 LLM,并将输出作为调用响应返回。

你可以在这里找到代码。

在…/chat_service/server/中,将chat_server_config.xml.example重命名为chat_server_config.xml

然后你可以使用以下命令启动聊天服务器:

python .\chat_server.py

当服务第一次运行时,由于从HuggingFace网站下载大型文件并存储在本地缓存目录中,启动需要几分钟时间。

你会从终端收到服务正在运行的确认:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

聊天服务运行的确认。图片由作者提供。

如果你想测试与 LLM 的交互,请前往…/chat_service/chainlit_interface/。

app_config.xml.example重命名为app_config.xml。使用以下命令启动网络聊天服务:

.\start_interface.sh

浏览到本地地址localhost:8000

你应该能够通过文本接口与本地运行的 LLM 进行交互:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

与本地运行的 LLM 的文本交互。图片由作者提供。

语音助手服务

语音助手服务是进行语音转文本和文本转语音转换的地方。你可以在这里找到代码。

转到…/voice_assistant/server/。

voice_assistant_service_config.xml.example重命名为voice_assistant_service_config.xml

助理通过播放问候语开始,以表明它正在监听用户。问候语的文本在voice_assistant_config.xml中配置,位于*<welcome_message>*元素下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

voice_assistant_config.xml 文件。图片由作者提供。

允许程序将文本转换为你可以通过音频输出设备听到的语音的文本转语音引擎是pyttsx3。根据我的经验,这个引擎用英语和法语说话都非常自然。与其他依赖 API 调用的软件包不同,它是在本地运行的。

一个名为facebook/seamless-m4t-v2-large的模型执行语音转文本推断。voice_assistant_service.py首次运行时会下载模型权重。

**voice_assistant_service.main()**中的主要循环执行以下任务:

  • 从麦克风获取句子。使用语音转文本模型将其转换为文本。

  • 检查用户是否说出了配置文件中的*<end_of_conversation_text>*元素定义的消息。在这种情况下,谈话结束,程序在播放再见消息后终止。

  • 检查句子是否是胡言乱语。语音转文本引擎即使我什么也没说,通常也会输出有效的英语句子。偶尔,这些不必要的输出会重复出现。例如,胡言乱语的句子有时会以“[”或“i’m going to”开头。我在配置文件的*<gibberish_prefix_list>*元素中收集了通常与胡言乱语句子相关的前缀列表(这个列表可能会根据其他语音转文本模型而有所变化)。每当音频输入以列表中的一个前缀开头时,句子就会被忽略。

  • 如果句子看起来不是胡言乱语,请向聊天服务发送请求。播放响应。

**voice_assistant_service.main()**中的主要循环。代码由作者提供。

唤醒词服务

最后一个组件是一个持续监听用户麦克风的服务。当用户说出唤醒词时,系统调用会启动语音助手服务。唤醒词服务运行的模型比语音助手服务模型要小。因此,持续运行唤醒词服务是有意义的,而语音助手服务则只有在需要时才会启动。

你可以在这里找到唤醒词服务的代码。

克隆项目后,转到…/wakeword_service/server。

wakeword_service_gui_config.xml.example重命名为wakeword_service_gui_config.xml

command.bat.example重命名为command.bat。你需要编辑command.bat,使虚拟环境激活和对voice_assistant_service.py的调用与您的目录结构相匹配。

你可以通过以下调用来启动服务:

python gui.py

唤醒词检测服务的核心是openwakeword项目。在几个唤醒词模型中,我选择了“hey jarvis”模型。我发现简单地说“Jarvis?”就会触发检测。

每当检测到唤醒词时,都会调用一个命令文件,正如配置文件的*<command_on_wakeword>*元素中所指定的。在我们的情况下,command.bat文件激活虚拟环境并启动语音助手服务。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

唤醒词检测服务 GUI 的配置文件。图片来源于作者。

如果你想将唤醒词检测服务用于其他用途,你可以编辑command.bat文件,以启动你想要的任何程序。

当检测到唤醒词时,唤醒词检测服务会发出两声哔声以指示已触发。这一快速反馈告诉用户,他们必须等待,因为文本到语音和语音到文本模型正在加载到计算机的 GPU 中,这可能需要几秒钟。当两个模型都加载完毕后,语音助手服务会播放问候语,交谈可以开始。对话结束后,语音助手服务终止,唤醒词检测服务会发出另一声两声哔声信号。

当三个组件安装并测试完成后,我们可以启动聊天和唤醒词检测服务。唤醒词检测服务将不断监听麦克风。当检测到唤醒词(“Jarvis?”)时,它将启动语音助手服务。然后你可以大声提问并收到语音回答。当检测到对话结束短语(“谢谢,再见”)时,语音助手服务结束。

与助手的交互示例

这是与语音助手交互的一个示例:

“Jarvis?” [哔—哔]

[在语音助手模型加载期间的几秒钟沉默]

“你好!我能帮你什么?”

“土星的环由什么组成?”

“土星的环主要由冰粒组成,其中大部分是水冰。然而,环中也有少量其他冰类,例如氨冰和岩石碎片。环的确切成分因距离土星的远近而异,内圈的密度较高,颗粒较小,而外圈则分布较广,含有较大的冰粒。”

“告诉我电影‘回到未来’的情节。”

“电影《回到未来》讲述了高中生马蒂·麦克弗莱的故事,他意外地被传送回 1955 年,乘坐由他那古怪的科学家朋友布朗博士创造的时光旅行德洛林车。马蒂必须在修复对现在造成的损害的同时应对过去的挑战,并防止他的青少年母亲爱上他的青少年父亲。在这个过程中,马蒂结识了年轻版的布朗博士,并帮助他克服个人悲剧。这部电影探讨了命运、家庭和改变过去的后果等主题。”

“谢谢,再见”

“再见” [嘟嘟声]

结论

在这个项目中,我们实现了一个由唤醒词检测服务、语音助手服务和聊天服务组成的语音助手系统。这些组件都是独立的模块,可以在其他项目中重复使用。

在我开发这个语音助手的过程中,有许多惊喜,其中最让我印象深刻的是语音转文本的质量。如果你像我一样,可能会遇到过无法准确转录简单命令的自动语音识别系统,比如*“调低音量”*!我原本以为语音转文本会是整个流程的主要障碍。在尝试了几个不令人满意的模型后,我最终选择了facebook/seamless-m4t-v2-large,对其结果的质量印象深刻。我甚至可以用法语说一句话,神经网络会自动将其翻译成英语。这简直令人惊叹!

我希望你能尝试这个有趣的项目,并告诉我你是如何使用它的!

使用 Google Cloud Recommendation AI 构建推荐系统

原文:towardsdatascience.com/build-a-recommender-system-using-google-cloud-c0929f0c3080?source=collection_archive---------0-----------------------#2023-06-21

使用 Google Cloud Recommendation AI 实现高度先进的推荐系统

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Muffaddal Qutbuddin

·

关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 6 月 21 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来源 pixabay

想象一下谷歌自己的机器学习工程师在为你的网站和应用程序实施推荐系统。借助 Google Cloud Recommendation AI,你可以利用正在用于驱动 YouTube、Google 广告和其他 Google 产品的推荐系统,为我们提供个性化服务。

在本文中,我将指导您如何实施 Google Cloud Recommendation AI,以便为您的客户提供个性化体验。

Google Cloud Recommendation AI 的好处?

Noon、IKEA、KINGUIN 以及许多其他公司使用 Google Cloud Recommendation AI 提供跨渠道的高性能推荐。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Muffaddal 关于 Google Cloud Recommendation AI 对某些企业的初步影响

在工作量和资源方面,构建和部署推荐系统可能是一个复杂且耗时的任务。它通常涉及数据工程师、数据科学家和机器学习工程师的协作,以创建和运营一个全面的推荐系统。然而,使用 Google Cloud Recommendation AI 的情况却大不相同。

通过利用 Google Cloud Recommendation AI,实施过程变得显著简化且高效。这项完全托管的服务使您能够快速提供个性化体验,节省宝贵的时间和资源。实施 Google Cloud Recommendation AI 可以在以下几个方面带来明显的改进:

  1. 转化率: 通过向用户提供量身定制的产品或内容推荐,您可以提升他们的购物或浏览体验,从而增加转化的可能性。

  2. 客户满意度: 个性化推荐满足了个别用户的具体偏好和需求,带来了更高的客户满意度和参与度。

  3. 收入生成: 通过提高转化率和客户满意度,实施 Google Cloud Recommendation AI 可以对您的收入流产生积极影响。

  4. 时间节省: 作为一项完全托管的服务,Google Cloud Recommendation AI 减少了广泛开发和维护工作的需求,使您能够专注于业务的其他关键方面。

  5. 可扩展性: Google Cloud Recommendation AI 旨在处理大量数据,并且能够随着业务增长无缝扩展,确保一致的性能和准确性。

通过利用 Google Cloud Recommendation AI 的能力,您可以释放显著改善客户体验、转化率、收入和运营效率的潜力。

想要实施推荐 AI?让我们聊聊

Google Cloud Recommendation AI 可以在个性化方面实施的领域

Google Cloud Recommendation AI 提供了一系列机器学习模型和配置,可以在各种业务环境中有效地利用,以提升用户体验并推动更高的投资回报。以下是 Google Cloud Recommendation AI 可以应用的一些关键领域:

  1. 个性化推荐:在主页上实施“为您推荐”或“推荐给您”部分,根据用户的个人偏好和行为提供量身定制的建议。

  2. 商品推荐:在产品详情页展示“您可能喜欢的其他商品”部分,推荐用户高度可能感兴趣或购买的商品。

  3. 购物车推荐:利用“常一起购买”部分,建议用户购买与所选商品常一起购买的其他产品,从而提升用户的购物体验。

  4. 相似商品推荐:在产品页面展示“相似商品”部分,展示与当前浏览商品具有相似属性的商品,尤其在浏览的商品缺货时非常有用。

  5. 重复购买推荐:在不同页面(如主页、产品详情、购物车)实施“再次购买”功能,建议用户基于之前的互动记录,推荐可能会再次购买的商品。

  6. 销售推荐:利用“促销”部分展示打折商品,鼓励用户探索并进行购买。

此外,Google Cloud 推荐 AI 还支持客观优化,使企业能够根据特定目标改进其模型。例如,内容管理平台如 Medium 可能会优化产品浏览量,而电子商务网站则专注于转化优化。

你还可以结合多个目标来最大化个性化的好处。例如,可以使用优化参与度的模型来促进新用户的探索,而针对返回用户的转化和收入优化则可以发挥作用。

不要忘记电子邮件个性化。Google Cloud 推荐 AI 驱动的模型也可以与电子邮件结合使用,以提升业务目标。例如,我们可以在用户购买商品后,通过电子邮件进行追加销售或交叉销售。通过“为您推荐”模型进行精准定位,增加用户互动。

结合以上提到的所有内容,我们可以在用户在我们平台上的整个生命周期内完全改变他们的体验,从而增加 LTV 和业务收入。

如何实现 Google Cloud 推荐 AI?

推荐 AI 需要两个方面来驱动机器学习模型。一是用户活动,二是用户进行活动的产品详情。

活动或操作作为事件发送,商品详情作为目录存储在推荐 AI 中。一旦我们获得了所需数量的事件和目录详情,我们可以训练我们的机器学习模型,为用户提供基于用户过去行为和属性的个性化列表。

让我们逐一详细了解这些内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 Google Cloud 推荐 AI 的数据管道,由 Muffaddal 提供

1- 导入产品详情

目录可以有许多字段和属性,但 idnametitlecategories 是必需字段,必须提供。

点击这里了解有关目录所有可用字段的更多详情

假设我们的产品详细信息已经存在于 BigQuery 中,我们将利用 BigQuery 和 Recommendation AI 的集成来导入目录数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 的产品目录导入过程,由 Muffaddal 提供

Google Cloud Recommendation AI 需要一个特定的 BigQuery 表模式。因此,我们必须创建一个符合要求格式的表,并将目录数据插入其中。

假设我们的目录表在 BigQuery 中具有以下字段

  1. name: 产品名称

  2. id: 产品的 id

  3. category: 产品的分配类别

  4. description: 产品的描述

  5. url: 网站上产品的 URL

  6. image_link: 产品的公开访问图片链接

  7. city: 产品可用的城市

根据上述产品目录详细信息,我们需要为 BigQuery 表创建模式。从这里获取模式,并根据我们的数据进行更新。

我们可以为目录设置许多字段。数据越多越好。为了演示的目的,我将使用最常见的字段。请注意,可为空的字段是可选的。

type 字段是我们决定产品是变体还是主产品的地方。本文中我将使用 PRIMARY点击这里了解更多详情

一旦我们的表准备好,我们可以使用以下查询将目录数据从主表插入到此表中。

insert into `recommendersystem.product_data` 

(
  name,id,type,primaryProductId,collectionMemberIds,gtin,categories,title,brands,description,languageCode,attributes,  tags,    
  priceInfo,rating,expireTime,ttl,availableTime,availability,availableQuantity,fulfillmentInfo, uri, images,audience,colorInfo,sizes,materials,patterns,conditions,retrievableFields,publishTime,promotions
)

SELECT 
   name,
   cast(id as string) as id,
   "PRIMARY" as type, 
   cast(id as string) as primaryProductId,
    null as collectionMemberIds,
    null as gtin,
   array [categories] as categories,
   name as title,
   array[title] as brands,
   ifnull(description,name) as description,
    null as languageCode,
   [
      struct(
      'product_location' as key,  STRUCT(array[ifnull(city,"empty")] as text,  cast(null as ARRAY<FLOAT64>) as numbers, true as searchable, true as indexable ) as value
      )]
   as attributes,
   ARRAY_CONCAT(
      [ifnull(location,"empty")],
      [ifnull(categories,"empty")]      
    ) as tags,
    null as  priceInfo,
    null as  rating,
    null as  expireTime,
    null as  ttl,
    null as  availableTime,
    null as  availability,
    null as  availableQuantity,
    null as  fulfillmentInfo,
   url,
   array[struct(image_url) as uri, null as height, null as width)] as images,
   null as audience,
   null as colorInfo,
   null as sizes,
   null as materials,
   null as patterns,
   null as conditions,
   null as retrievableFields,
   null as publishTime,
   null as promotions
FROM `product.product_details`

数据在我们的新表中可用后,我们准备将其导入 Recommendation AI。

Google Cloud 的零售 AI 数据标签中,点击左上角的导入以导入数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

向 Google Cloud Recommendation AI 导入目录详细信息,由 Muffaddal 提供

将出现一个面板,如下图所示。选择导入类型中的“产品目录”和数据源中的“BigQuery”。提供 BigQuery 表路径,选择一个分支并点击导入。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 的目录导入面板,由 Muffaddal 提供

等待几分钟以查看 Retail AI 数据表中的目录详细信息。

2- 导入历史事件

接下来,我们需要将用户的历史数据导入 Recommendation AI。此步骤是可选的,但这样做有助于构建更好的机器学习模型。

类似于目录,我们需要在 BigQuery 表中拥有符合要求格式的事件。

以下是 Google Cloud Recommendation AI 接受的事件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以发送到 Google Cloud Recommendation AI 的事件,由 Muffaddal 提供

在所有这些事件中,home-page-viewdetail-page-viewedadd-to-cartpurchase-complete对于全面激活 AI 模型是必要的。

每个事件都有一个特定的表模式,用于数据导入。你可以在这里详细查看

1- home-page-viewdetail-page-viewedadd-to-cart的表模式如下

[
    {
        "name": "eventType",
        "type": "STRING",
        "mode": "REQUIRED"
    },
    {
        "name": "visitorId",
        "type": "STRING",
        "mode": "REQUIRED"
    },
    {
        "name": "eventTime",
        "type": "STRING",
        "mode": "REQUIRED"
    },
 {
   "name": "productDetails",
   "type": "RECORD",
   "mode": "REPEATED",
   "fields": [
     {
       "name": "product",
       "type": "RECORD",
       "mode": "REQUIRED",
       "fields": [
         {
           "name": "id",
           "type": "STRING",
           "mode": "REQUIRED"
         }
       ]
     },
     {
       "name": "quantity",
       "type": "INTEGER",
       "mode": "REQUIRED"
     }
   ]
 },
    {
        "name": "attributes",
        "type": "RECORD",
        "mode": "NULLABLE",
        "fields": [
            {
                "name": "deviceType",
                "type": "RECORD",
                "mode": "NULLABLE",
                "fields": [
                    {
                        "name": "text",
                        "type": "STRING",
                        "mode": "REPEATED"
                    }
                ]
            }
        ]
    }
]

2- purchase-complete事件的模式如下

[
 {
   "name": "eventType",
   "type": "STRING",
   "mode": "REQUIRED"
 },
 {
   "name": "visitorId",
   "type": "STRING",
   "mode": "REQUIRED"
 },
 {
   "name": "eventTime",
   "type": "STRING",
   "mode": "REQUIRED"
 },
 {
   "name": "productDetails",
   "type": "RECORD",
   "mode": "REPEATED",
   "fields": [
     {
       "name": "product",
       "type": "RECORD",
       "mode": "REQUIRED",
       "fields": [
         {
           "name": "id",
           "type": "STRING",
           "mode": "REQUIRED"
         }
       ]
     },
     {
       "name": "quantity",
       "type": "INTEGER",
       "mode": "REQUIRED"
     }
   ]
 },
 {
   "name": "purchaseTransaction",
   "type": "RECORD",
   "mode": "REQUIRED",
   "fields": [
     {
       "name": "revenue",
       "type": "FLOAT",
       "mode": "REQUIRED"
     },
     {
       "name": "currencyCode",
       "type": "STRING",
       "mode": "REQUIRED"
     }
   ]
 },
 {
    "name": "attributes",
    "type": "RECORD",
    "mode": "NULLABLE",
    "fields": [
        {
            "name": "deviceType",
            "type": "RECORD",
            "mode": "NULLABLE",
            "fields": [
                {
                    "name": "text",
                    "type": "STRING",
                    "mode": "REPEATED"
                }
            ]
        },
        {
            "name": "cityName",
            "type": "RECORD",
            "mode": "NULLABLE",
            "fields": [
                {
                    "name": "text",
                    "type": "STRING",
                    "mode": "REPEATED"
                }
            ]
        }
    ]
}

]

一旦你创建了表,可以使用以下查询将数据插入到新表中。

1- home-page-viewed的 SQL 插入查询如下

insert into `recommendersystem.user_event_home_page_view` (eventType, visitorId,eventTime,attributes)
  SELECT 
  'home-page-view' as eventType,
  visitorId,
  eventTime,
  struct(
      struct([deviceType] as text)as deviceType,
      struct([city] as text)as cityName 
  )
    as attributes,
  from (
    select *, 
 deviceType from  `recommendersystem.user_event_history` 

2- detail-page-viewedadd-to-cart的 SQL 插入查询为

insert into `recommendersystem.user_event_add_to_cart` (eventType, visitorId,eventTime,productDetails,attributes)
SELECT 
'add-to-cart' as eventType,
visitorId,
eventTime,
[
    struct( struct(product_id  as id) as product , 1 as quantity) 
] as productDetails,
 struct(
     struct([deviceType] as text)as deviceType,
     struct([city] as text)as cityName 

 )
   as attributes,
from (
  select *, 
 deviceType from  `recommendersystem.user_event_history` 
-- limit 100
)

3- purchase-complete事件的 SQL 插入查询为

insert into `recommendersystem.user_event_purchase_complete` (eventType, visitorId,eventTime,productDetails,purchaseTransaction,attributes)
SELECT 
'purchase-complete' as eventType,
cast( visitorId as string) as visitorId,
eventTime,
[
    struct( struct(product_id  as id) as product , 1 as quantity) 
] as productDetails,
struct(safe_cast(revenue as float64) as revenue, 'USD' as currencyCode) as purchaseTransaction,
 struct(
     struct([deviceType] as text)as deviceType,
     struct([city] as text)as cityName 

 )
   as attributes
from (
  select *, 
deviceType from  `recommendersystem.user_purchase_event` 
)

注:访客 ID 和用户 ID 可以相同,也可以不同。这取决于用户是否需要在使用你的产品之前登录。

注:Google Cloud 推荐 AI 也支持 Google Analytics 4 原始数据。如果你有这些数据,则无需进行转换,可以直接导入。

要导入历史数据,请像以前一样转到零售 AI 的数据标签页,然后点击顶部的导入。

在导入面板中,选择导入类型为用户事件,选择以下选项,然后点击导入。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将历史事件导入 Google Cloud Recommendation AI,由 Muffaddal 提供

我们已经成功将用户事件导入到 Google Cloud Recommendation AI 中。

3- 发送实时事件

接下来,我们需要向用户发送实时事件,以便 AI 模型可以重新训练并在用户与平台互动的过程中提升推荐效果。

有三种方法将用户事件发送到 Google Cloud Recommendation AI。使用 javascript pixel使用 APIGTM

本文将使用 API,因为它可以覆盖所有情况,无论网站或应用的性质如何。

这是发送home-page-viewed事件的 curl 调用

curl -X POST \
     -H "Authorization: Bearer ya29.a0AasRrdaM8jq4J0FBtJpsdhu87ZJcPlr4-8NqkDdGmLYtQ7P-riTx5man4k2maqWGNIsL1007a4BClSsfVbgHyjycaKn_5bviofo5XCbvCeO5-kkepnb_RUgy6prxRX7X8pi2PFHxX-kbmSmQgeEoILQQnK_aYMtTagSFzkUXC12Q2A5VmlfXR5cvSW-a80XxGOikpEf1jHuwusQx2EftBITnhPaYvg6Xi08qzvAEnmKgYicqCqY5o9d9ixav1jm6bd0r7A" \
     -H "Content-Type: application/json; charset=utf-8" \
     -H "X-Goog-User-Project: test-prod "
     --data "{
         'eventType': 'home-page-view',
         'visitorId': '12',
         'eventTime': '2021-09-28T03:33:33.000001Z',
         'attributionToken': 'ABC',
         'attributes': {
            'city_name': {
              'text': ['karachi']
            },
            'device_type': {
              'text': ['iOS']
            },
         },
         'userInfo': {
           'userId': '123',
         }
}"\
"https://retail.googleapis.com/v2/projects/test-prod/locations/global/catalogs/default_catalog/userEvents:write"

detail-page-view的 Curl 调用

curl -X POST \
     -H "Authorization: Bearer ya29.a0ARrdaMsd84J0FBtZdJp2jhu87ZJcPlr4-8NqkDdGmLYtQ7P-riTx5man4k2maqWGNIsL1007a4BClSsfVbgHyjycaKn_5bviofo5XCbvCeO5-kkepnb_RUgy6prxRX7X8pi2PFHxX-kbmSmQgeEoILQQnK_aYMtTagSFzkUXC12Q2A5VmlfXR5cvSW-a80XxGOikpEf1jHuwusQx2EftBITnhPaYvg6Xi08qzvAEnmKgYicqCqY5o9d9ixav1jm6bd0r7A" \
     -H "Content-Type: application/json; charset=utf-8" \
     -H "X-Goog-User-Project: test-prod "
     --data "{
         'eventType': 'detail-page-view',
         'visitorId': '123',
         'eventTime': '2021-09-28T03:33:33.000001Z',
         'attributionToken': 'ABC',
         'attributes': {
            'city_name': {
              'text': ['karachi']
            },
            'device_type': {
              'text': ['iOS']
            },
         },
         'productDetails': [{
           'product': {
             'id': '2806'
           }
          }],
         'userInfo': {
           'userId': '123',
         }
}"\
"https://retail.googleapis.com/v2/projects/test-prod/locations/global/catalogs/default_catalog/userEvents:write"

add-to-cart的 Curl 调用

curl -X POST \
     -H "Authorization: Bearer ya29.sdrdaM8jq4J0FBtZdJp2jsdshu87ZJcPlr4-8NqkDdGmLYtQ7P-riTx5man4k2maqWGNIsL1007a4BClSsfVbgHyjycaKn_5bviofo5XCbvCeO5-kkepnb_RUgy6prxRX7X8pi2PFHxX-kbmSmQgeEoILQQnK_aYMtTagSFzkUXC12Q2A5VmlfXR5cvSW-a80XxGOikpEf1jHuwusQx2EftBITnhPaYvg6Xi08qzvAEnmKgYicqCqY5o9d9ixav1jm6bd0r7A" \
     -H "Content-Type: application/json; charset=utf-8" \
     -H "X-Goog-User-Project: test-prod"
     --data "{
         'eventType': 'add-to-cart',
         'visitorId': '123',
         'eventTime': '2021-09-28T03:33:33.000001Z',
         'attributionToken': 'ABC',
         'attributes': {
            'city_name': {
              'text': ['karachi']
            },
            'device_type': {
              'text': ['iOS']
            },
         },
         'productDetails': [{
           'product': {
             'id': '2806'
           },
     'quantity':1
          }],
         'userInfo': {
           'userId': '123',
         }
}"\
"https://retail.googleapis.com/v2/projects/test-prod/locations/global/catalogs/default_catalog/userEvents:write"

purchase-complete的 Curl 调用

curl -X POST \
     -H "Authorization: Bearer ya29.a0ARrsddaM8jJ0FBtZdJsdjhu87ZJcPlr4-8NqkDdGmLYtQ7P-riTx5man4k2maqWGNIsL1007a4BClSsfVbgHyjycaKn_5bviofo5XCbvCeO5-kkepnb_RUgy6prxRX7X8pi2PFHxX-kbmSmQgeEoILQQnK_aYMtTagSFzkUXC12Q2A5VmlfXR5cvSW-a80XxGOikpEf1jHuwusQx2EftBITnhPaYvg6Xi08qzvAEnmKgYicqCqY5o9d9ixav1jm6bd0r7A" \
     -H "Content-Type: application/json; charset=utf-8" \
     -H "X-Goog-User-Project: test-prod "
     --data "{
         'eventType': 'purchase-complete',
         'visitorId': '123',
         'eventTime': '2021-09-28T03:33:33.000001Z',
         'attributionToken': 'ABC',
         'attributes': {
            'city_name': {
              'text': ['karachi']
            },
            'device_type': {
              'text': ['iOS']
            },
         },
         'productDetails': [{
           'product': {
             'id': '2806'
           },
     'quantity':'1'
          }],
          'purchaseTransaction':{
              "id": 'transacion-id-here',
              "revenue": 'orderPrice-here',
              "currencyCode": 'USD',
               "quantity":'1'
            }
         'userInfo': {
           'userId': '123',
         }
}"\
"https://retail.googleapis.com/v2/projects/test-prod/locations/global/catalogs/default_catalog/userEvents:write"

你需要使用 Google Cloud 生成授权令牌以发送事件。

你可以在 Retail AI 的事件标签页中查看实时事件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Retail AI 用户事件,由 Muffaddal 提供

注意:如果你在导入目录之前或期间记录了用户事件,请 重新加入任何事件 这些事件是在目录导入完成之前记录的。

最后,我们只差一步就能创建第一个推荐模型。

构建推荐模型

Google Cloud Recommendation AI 支持以下机器学习模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 支持的 AI 模型,由 Muffaddal 提供

对于这篇文章,我们将使用 Recommended For You 模型。你可以 点击这里查看有关可用模型的更多细节

进入模型选项卡,点击“创建模型”开始配置你的 ML 模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 中的模型创建,由 Muffaddal 提供

接下来,在模型类型中选择 Recommended For You 模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 中的 AI 模型选择,由 Muffaddal 提供

我们的目标是提高购买量,因此我们希望我们的 ML 模型优化转换率。选择 conversion rate (CVR) 作为模型目标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 中的模型目标,由 Muffaddal 提供

设置调优频率为 every three month,并按属性值过滤为 auto

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 模型的调优和选项卡设置,由 Muffaddal 提供

然后点击“创建”按钮。这将开始模型训练。等待一两天,AI 模型将准备就绪。时间取决于训练所需的数据量。

服务

一旦 AI 模型创建完成,就该配置服务,以便我们可以调用 AI 模型并获取个性化列表。

进入服务配置,点击顶部的“创建服务”按钮。选择下图所示的推荐设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 中的模型服务配置,由 Muffaddal 提供

命名你的服务并点击“继续”。

选择我们创建的 recommended for you 模型。这将把我们的模型附加到这个服务配置中。

偏好选项卡是你决定模型行为的地方。我们可以使用自动设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Google Cloud Recommendation AI 的偏好设置,由 Muffaddal 提供

点击“创建”按钮。注意配置的 ID,因为它将用于调用模型 API。

获取推荐

以下是从我们创建的模型获取推荐的 curl 调用。

curl -X POST \
    -H "Authorization: Bearer ya29.aARrdaM9Bm57OTsdsIQAzGT15GwYzZpVfssffknWPNJ8gpKRk6IHSFmGqs1nBpAlaRRg2fQvtJgtUDGsuIc-h-j0RMLkAPy7FjxQ4tQbYZl62ba-4q4oRx-oY2KwYDA-pEQW77SACo2a8hS1zEUZHyyHCO3V-PycSBetJeldjib5VYo969D1PFVF33WSSRLPIP9uBcTW9ABoYthSOioTePlaICbwV1p8dlXesnCH8PdPNuKPxJJI3rzrnIghKXUKSQb4E-mc" \
    -H "Content-Type: application/json; charset=utf-8" \
    -H "X-Goog-User-Project: test-prod"\

    --data  '{

     "pageSize":100,

          "userEvent": {
              "eventType": "home-page-view",
              "visitorId": "123",
              "userInfo": {
                  "userId": "123"
              },
              "experimentIds": "123"
            }
          }' \
https://retail.googleapis.com/v2/projects/test-prod/locations/global/catalogs/default_catalog/placements/<your serving id here>:predict

传递准确的用户 ID、服务 ID、项目 ID 和授权令牌,你将获得所提供用户 ID 的个性化列表。

与你的开发人员分享以上代码,他应该能够在你的网站和应用程序上提供个性化的部分。

实验

我强烈建议你在首次启动推荐系统时进行 A/B 测试。这将帮助你了解提供个性化体验带来的实际价值。

最终思考

这篇文章应该足够帮助你轻松构建一个完全可扩展的推荐系统。但请记住,使用 Google Cloud Recommendation AI 还有更多细节需要了解。

例如,在设置多个模型时,归因令牌至关重要,保持目录更新也很重要,以便向用户提供最新的产品,同时还需考虑 Google Cloud 的定价问题。还有许多其他方面。

如果你需要我在 Google Cloud Recommendation AI 方面的帮助,随时联系我。

结论

如果你正在寻找一种强大的方法来构建个性化推荐系统,Google Cloud Recommendation AI 是你的解决方案。

Google Cloud Recommendation AI 不仅允许你使用 Google 自有的机器学习模型,还能让你跳过整个推荐系统架构设计的过程。它能迅速为你提供推荐的价值。

及时实现 Google Cloud Recommendation AI,以提高用户参与度、留存率和业务收入。

类似阅读

## 你可能喜欢的类似产品

推荐系统用于在产品详细页面上向用户推荐类似的项目。

towardsdatascience.com ## 使用 BigQuery ML 进行 RFM 分析

使用 RFM 分析在 BigQuery ML 中进行用户细分,并在 Data Studio 中进行可视化。

towardsdatascience.com

用 Go 构建 REST API:时间序列的数据分析

原文:towardsdatascience.com/build-a-rest-api-with-go-data-analytics-for-time-series-8d3f0747665c

一个关于使用 Go、Gin 和 Gorm 进行 CRUD 操作和统计分析的逐步示例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Davide Burba

·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 11 月 7 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

“学习 Go”,作者 Giulia Roggia。经许可使用。

  • 介绍

  • 模型与数据库

  • 统计

  • 处理程序

  • 用法

介绍

本文介绍了一个使用 Go 构建的 REST API 示例,用于执行 CRUD(创建、读取、更新、删除)操作并计算时间序列数据的统计信息。

本文中使用的完整代码可以在 这里找到

为什么选择 Go?

Go 是构建 REST API 的常见选择,原因有很多。尽管它是高效的编译语言,但其语法简单且可读性强。它使得实现并发处理变得容易。它提供了功能丰富的标准库,并且拥有一个出色的库和工具生态系统。

在这个例子中,我们使用了两个流行的 Go 库:

  • Gin: 一个提供创建 Web 应用工具的 Web 框架。

  • Gorm: 一个功能全面的 ORM(对象关系映射),用于与数据库交互。

文件夹结构

我们为每个“服务”创建一个文件夹。在 Go 中,每个文件夹对应一个包,每个文件都可以访问同一包中其他文件定义的元素。以下是项目中使用的文件夹结构:

├── database
│   └── database.go
├── models
│   └── models.go
├── handlers
│   ├── routes.go
│   ├── stats.go
│   ├── timeseries.go
│   └── timeseriesvalues.go
├── stats
│   └── stats.go
├── go.mod
├── go.sum
└── main.go

模型与数据库

首先在 models.go 中定义 ORM 模型,以表示时间序列数据。我们使用了两个模型,一个用于标识序列,另一个用于存储其值。每个值通过外键与时间序列相关联。

type TimeSeries struct {
 ID   int    `gorm:"primaryKey"`
 Name string `gorm:"not null"`
}

type TimeSeriesValue struct {
 ID           int `gorm:"primaryKey"`
 Time         time.Time
 Value        float64
 TimeSeriesID int `gorm:"not null"`
}

我们还定义了两个用于时间序列值的 getter,这在计算统计数据时将非常有用。

func (v TimeSeriesValue) GetTime() time.Time { return v.Time }
func (v TimeSeriesValue) GetValue() float64  { return v.Value }

database.go中,我们定义了一个获取数据库的函数(在示例中使用SQLite),并应用迁移以创建每个模型的 SQL 表(如果尚不存在)。

func GetDatabase(dbFile string) (*gorm.DB, error) {
 return gorm.Open(sqlite.Open(dbFile), &gorm.Config{})
}

func AutoMigrate(db *gorm.DB) {
 db.AutoMigrate(&models.TimeSeries{}, &models.TimeSeriesValue{})
}

统计量

注意:如果你只对 CRUD 部分感兴趣,可以跳过这一部分。

由于统计量的计算是一个常见任务,为了提高代码的可重用性,我们将其开发为一个“独立”包,这意味着它不依赖于项目中定义的其他包。为此,我们定义一个TsValue接口:统计函数的输入是一个值的切片,每个值必须定义接口方法GetTimeGetValue

// Interface for a data point in a time series
type TsValue interface {
 GetTime() time.Time
 GetValue() float64
}

现在我们为每个统计量定义一个函数:

  • Count:系列的长度。
func Count(values []TsValue) float64 {
 return float64(len(values))
}
  • Min:最小值(以及Max,其功能类似)。
func Min(values []TsValue) float64 {
 if len(values) == 0 {
  return math.NaN()
 }
 min := values[0].GetValue()
 for _, value := range values {
  if value.GetValue() < min {
   min = value.GetValue()
  }
 }
 return min
}
  • Mean:系列的均值。
func Mean(values []TsValue) float64 {
 count := Count(values)
 if count == 0 {
  return math.NaN()
 }
 sum := 0.0
 for _, value := range values {
  sum += value.GetValue()
 }
 return sum / count
}
  • StandardDeviation:系列的标准差。
func StandardDeviation(values []TsValue) float64 {
 count := Count(values)
 if count == 0 {
  return math.NaN()
 }
 mean := Mean(values)
 sumSquare := 0.0
 for _, value := range values {
  sumSquare += math.Pow(value.GetValue()-mean, 2)
 }
 return math.Sqrt(sumSquare / count)
}

我们可以将统计量集中在一个函数中:

func ComputeStatistics(values []TsValue) map[string]float64 {
 result := make(map[string]float64)

 result["Count"] = Count(values)
 result["Min"] = Min(values)
 result["Max"] = Max(values)
 result["Mean"] = Mean(values)
 result["StdDev"] = StandardDeviation(values)

 return result
}

请注意,函数ComputeStatistics是顺序计算统计量的。如果每个系列中的值很多,这可能会变得耗时。如果是这样,我们可以使用 goroutines 并发计算统计量。

让我们定义一个并发版本ComputeStatisticsConcurrent,其执行以下步骤:

  1. 创建一个通道来收集结果,一个同步 goroutines 的等待机制,以及一个包含要计算的统计量的映射。

  2. 设置一个计数器来计算要计算的统计量数量。

  3. 为每个统计函数启动一个 goroutine。

  4. 设置一个特殊的 goroutine,以便在所有统计量计算完成后(即计数器归零时)关闭通道。

  5. 从通道中收集结果。收集在通道关闭时停止。

func ComputeStatisticsConcurrent(values []TsValue) map[string]float64 {
 result := make(map[string]float64)
 // 1) Create a WaitGroup to sync, a Channel to collect results, and
 // a map with the statistics to compute.
 var wg sync.WaitGroup
 ch := make(chan struct {
  string
  float64
 })
 statsToCompute := map[string]func([]TsValue) float64{
  "Count":             Count,
  "Min":               Min,
  "Max":               Max,
  "Mean":              Mean,
  "StandardDeviation": StandardDeviation,
 }

 // 2) Set how many stats to compute.
 wg.Add(len(statsToCompute))

 // 3) Compute each stat in a separate goroutine.
 for statName, statFunc := range statsToCompute {
  go func(name string, operation func([]TsValue) float64) {
   defer wg.Done()
   value := operation(values)
   ch <- struct {
    string
    float64
   }{name, value}
  }(statName, statFunc)
 }

 // 4) Set a goroutine to close the channel once all the
 // stats are computed.
 go func() {
  wg.Wait()
  close(ch)
 }()

 // 5) Collect the results from the channel.
 for stat := range ch {
  result[stat.string] = stat.float64
 }

 return result
}

注意:如果你想在不使用大数据的情况下测试性能提升,可以在每个统计函数内部运行time.Sleep进行模拟。

处理程序

现在让我们定义 REST API 的端点。因为:

  • 端点逻辑依赖于数据库。

  • 我们不想使用全局变量。

  • 端点函数必须只接受 Gin 上下文指针作为输入,该指针包含请求信息。

我们创建一个数据库包装器,并将端点方法分配给它:

// Create a new type that embeds database.Database to assign new
// methods to it
type wrapDB struct {
 DB *gorm.DB
}

我们定义一个函数来设置所有端点。每个端点指定一个路径,并指定一个接受 Gin 上下文指针的函数。

func SetEndpoints(r *gin.Engine, db *gorm.DB) {

 wrapdb := &wrapDB{DB: db}

 // timeseries CRUD
 r.GET("/timeseries", wrapdb.listTimeSeries)
 r.GET("/timeseries/:tsid", wrapdb.getTimeSeries)
 r.POST("/timeseries", wrapdb.postTimeSeries)
 r.PUT("/timeseries/:tsid", wrapdb.putTimeSeries)
 r.DELETE("/timeseries/:tsid", wrapdb.deleteTimeSeries)

 // timeseries values CRUD
 r.GET("/timeseries/:tsid/values", wrapdb.listTimeSeriesValues)
 r.GET("/timeseries/:tsid/values/:valueid", wrapdb.getTimeSeriesValue)
 r.POST("/timeseries/:tsid/values", wrapdb.postTimeSeriesValues)
 r.PUT("/timeseries/:tsid/values/:valueid", wrapdb.putTimeSeriesValue)
 r.DELETE("/timeseries/:tsid/values/:valueid", wrapdb.deleteTimeSeriesValue)

 // statistics
 r.GET("/timeseries/:tsid/statistics", wrapdb.getTimeSeriesStats)
}

处理程序:系列

让我们从定义时间序列端点开始。下面是列出所有可用时间序列的实现。当调用DB.Find时,时间序列存储在tseriesList切片中。

func (db *wrapDB) listTimeSeries(c *gin.Context) {
 var tseriesList []models.TimeSeries
 if err := db.DB.Find(&tseriesList).Error; err != nil {
  c.JSON(500, gin.H{"error": "Failed to retrieve timeseries"})
  return
 }
 c.JSON(200, tseriesList)
}

在开发TimeSeries的基础 CRUD 端点之前,让我们定义一个辅助函数来从请求中获取系列 ID 并执行一些检查。

func (db *wrapDB) checkTimeSeriesID(c *gin.Context) (int, error) {
 id := c.Param("tsid")
 // Check if the time series exists can be converted to
 timeSeriesID, err := strconv.Atoi(id)
 if err != nil {
  c.JSON(400, gin.H{"error": "Invalid time series ID"})
  return timeSeriesID, err
 }

 // Check if the time series exists in the TimeSeries table
 var timeSeries models.TimeSeries
 if err := db.DB.First(&timeSeries, timeSeriesID).Error; err != nil {
  c.JSON(404, gin.H{"error": "Time series not found"})
  return timeSeriesID, err
 }
 return timeSeriesID, nil
}

我们现在可以定义TimeSeries的基本 CRUD 操作:

  • 创建:POST 一个新的系列。
func (db *wrapDB) postTimeSeries(c *gin.Context) {
 var tseries models.TimeSeries
 c.BindJSON(&tseries)
 if err := db.DB.Create(&tseries).Error; err != nil {
  c.JSON(500, gin.H{"error": "Failed to create timeseries"})
  return
 }
 c.JSON(201, tseries)
}
  • 读取:GET 一个现有系列。
func (db *wrapDB) getTimeSeries(c *gin.Context) {
 timeSeriesID, err := db.checkTimeSeriesID(c)
 if err != nil {
  return
 }
 var tseries models.TimeSeries
 if err := db.DB.Where("id = ?", timeSeriesID).First(&tseries).Error; err != nil {
  c.JSON(404, gin.H{"error": "Failed to retrieve timeseries"})
  return
 }
 c.JSON(200, tseries)
}
  • 更新:PUT 一个现有系列。
func (db *wrapDB) putTimeSeries(c *gin.Context) {
 timeSeriesID, err := db.checkTimeSeriesID(c)
 if err != nil {
  return
 }
 var tseries models.TimeSeries
 if err = db.DB.Where("id = ?", timeSeriesID).First(&tseries).Error; err != nil {
  c.JSON(404, gin.H{"error": "Time series not found"})
  return
 }
 c.BindJSON(&tseries)
 if err = db.DB.Save(&tseries).Error; err != nil {
  c.JSON(500, gin.H{"error": "Error while saving"})
 }
 c.JSON(200, tseries)
}
  • 删除:删除一个系列及其值。为了在运行时出现错误时进行回滚,我们在数据库事务中执行这两个操作。
// Delete a time series and its values
func (db *wrapDB) deleteTimeSeries(c *gin.Context) {
 timeSeriesID, err := db.checkTimeSeriesID(c)
 if err != nil {
  return
 }
 var tseries models.TimeSeries

 // Delete in a transaction
 db.DB.Transaction(func(tx *gorm.DB) error {

  // Delete values
  if err := tx.Where("time_series_id = ?", timeSeriesID).
   Delete(&models.TimeSeriesValue{}).Error; err != nil {
   c.JSON(500, gin.H{"error": "Deleting the timeseries failed."})
   return err
  }

  // Delete timeseries
  if err := tx.Where("id = ?", timeSeriesID).
   Delete(&tseries).Error; err != nil {
   c.JSON(500, gin.H{"error": "Deleting the timeseries failed."})
   return err
  }

  c.JSON(200, gin.H{"id #" + strconv.Itoa(timeSeriesID): "deleted"})
  return nil
 })

}

处理程序:值

我们现在可以定义 TimeSeriesValues 的端点。由于它们与我们刚刚定义的端点类似,我们仅展示 Create 方法的实现,它与系列方法不同,因为我们允许同时发布多个值。

func (db *wrapDB) postTimeSeriesValues(c *gin.Context) {
 timeSeriesID, err := db.checkTimeSeriesID(c)
 if err != nil {
  return
 }

 // Bind the request body to a slice of TimeSeriesValue
 var timeSeriesValues []models.TimeSeriesValue
 if err := c.ShouldBindJSON(&timeSeriesValues); err != nil {
  c.JSON(400, gin.H{"error": "Invalid request data"})
  return
 }
 // Set the TimeSeriesID for the posted values
 for i := range timeSeriesValues {
  timeSeriesValues[i].TimeSeriesID = timeSeriesID
 }

 // Create the values in the database
 if err := db.DB.Create(&timeSeriesValues).Error; err != nil {
  c.JSON(500, gin.H{"error": "Failed to create time series values"})
  return
 }
 c.JSON(201, gin.H{"message": "Time series values created"})
}

处理程序:统计信息

我们需要定义的最后一个处理程序是计算给定系列的统计信息。根据其 ID,我们查询值并使用之前实现的 ComputeStatisticsConcurrent 函数计算统计信息。

func (db *wrapDB) getTimeSeriesStats(c *gin.Context) {
 timeSeriesID, err := db.checkTimeSeriesID(c)
 if err != nil {
  return
 }
 // Query the database.Database to retrieve specific fields of time series values
 var tsValues []models.TimeSeriesValue
 if err := db.DB.Where("time_series_id = ?", timeSeriesID).
  Select("id, time, value").Find(&tsValues).Error; err != nil {
  c.JSON(500, gin.H{"error": "Failed to retrieve time series values"})
  return
 }
 // Convert the slice of TimeSeriesValue to a slice of TsValue interfaces
 values := []stats.TsValue{}
 for _, v := range tsValues {
  values = append(values, v)
 }
 c.JSON(200, serializeMap(stats.ComputeStatisticsConcurrent(values)))
}

在返回统计信息之前,我们将 serializeMap 函数应用于输出。这是为了将 NaN 值转换为 nil,以便使输出可以进行 JSON 序列化。注意,映射中的输出值类型是空接口 interface{},它可以容纳任何类型的值。

// SerializeMap serializes a map from string to float64,
// returning null for NaN values
func serializeMap(data map[string]float64) map[string]interface{} {
 serializedData := make(map[string]interface{})
 for key, value := range data {
  if math.IsNaN(value) {
   serializedData[key] = nil
  } else {
   serializedData[key] = value
  }
 }
 return serializedData
}

使用方法

使应用程序可用的最后一段代码是 main 函数,用于运行 Web 服务器。我们提供了三个可选的命令行参数:

  • db:指定数据库文件的名称(默认:timeseries.db

  • proxy:设置服务器的受信任代理(默认:127.0.0.1

  • port:定义服务器监听的端口号(默认:8080

func main() {
 // Define flags for command-line arguments
 dbName := flag.String("db", "timeseries.db", "Database name")
 trustedProxy := flag.String("proxy", "127.0.0.1", "Trusted proxy")
 port := flag.String("port", "8080", "Port number")
 flag.Parse()

 // Initialize the database
 db, err := database.GetDatabase(*dbName)
 if err != nil {
  panic("failed to connect to the database")
 }
 database.AutoMigrate(db)

 // Set the trusted proxies
 router := gin.Default()
 router.SetTrustedProxies([]string{*trustedProxy})

 // Set endpoints and run the server
 handlers.SetEndpoints(router, db)
 router.Run(fmt.Sprintf(":%s", *port))

}

构建

你现在可以通过以下方式运行 API:

  1. 使用 go mod init 初始化一个新的 Go 模块。这将生成一个带有模块名称和 Go 版本的 go.mod 文件。

  2. 使用 go mod tidy 跟踪依赖项。这将把项目依赖项添加到 go.mod 文件中,并创建一个包含所有依赖项校验和的 go.sum 文件(用于确保其完整性)。

  3. 使用 go build 构建程序并运行生成的可执行文件。这也可以通过 go run main.go 一步完成。

go.modgo.sum 文件已在 项目仓库 中提供。你可以在 这里 阅读更多关于 go 命令的信息。

我们还可以使用 Docker 运行 API。在下面显示的 Dockerfile 中,首先,我们使用 Go 官方镜像构建可执行文件,然后将其复制到一个最小的镜像中,这样得到一个约 20Mb 的小文件。

# Stage 1: Build the Go application

FROM golang:1.21-alpine3.18 AS build
WORKDIR /app
COPY . .
RUN apk add build-base
# Download (and cache) all dependencies.
RUN go mod download
# Build the Go app with static linking
RUN go build -ldflags="-w -s" -o tsapi

# Stage 2: Create a minimal image for running the application

FROM alpine:3.18
WORKDIR /app
COPY --from=build /app/tsapi .
EXPOSE 8080
CMD ["./tsapi"]

我们可以按照以下步骤使用 Docker 构建和执行 API:

# Build the docker image
docker build -t go-timeseries-api .
# Run the docker image
docker run -p 8080:8080 go-timeseries-api

示例用法

应用程序启动并运行后,我们可以通过 API 调用来测试它。下面我们展示了一个使用 curl 的示例。我们创建一个时间序列,发布其值,计算统计信息,最后删除它。

# Create a time series
curl -i -X POST http://localhost:8080/timeseries -d '{ "Name": "My time series"}'
> {"ID":1,"Name":"My time series"}

# Create time series values
curl -i -X POST http://localhost:8080/timeseries/1/values -d '[{"Time": "2023-10-28T12:00:00Z", "Value": 10.0},{"Time": "2023-10-28T12:15:00Z", "Value": 20.5}]'
> {"message":"Time series values created"}

# Get statistics
curl -i -GET http://localhost:8080/timeseries/1/statistics
> {"Count":2,"Max":20.5,"Mean":15.25,"Min":10,"StandardDeviation":5.25}

# Delete a time series
curl -i -X DELETE http://localhost:8080/timeseries/1
> {"id #1":"deleted"}

关于学习 Go 的说明

这是我在 Go 中的第一个项目。因此,如果你有任何反馈,我将非常感激!

我来自 Python 背景,我特别喜欢 Go 的速度和轻量性,并发现它比 C++ 学起来容易多了。以下是一些帮助我入门的资源:

本文中使用的完整代码可以在 这里 找到。

喜欢这篇文章吗? 查看我的其他文章 并关注我以获取更多内容! 点击这里 以无限阅读文章并以零额外费用支持我 ❤️

使用 Node.js 构建语音转文本网页应用

原文:towardsdatascience.com/build-a-speech-to-text-web-app-using-node-js-210f8c054d79

我们来构建一个使用 OpenAI 的 Whisper 模型来转录和翻译音频的网页应用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Kumar Shubham

·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 3 月 27 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 AltumCode 提供,来源于 Unsplash

大家好!希望你们一切都好。今天,我们将使用 Node.js 和 OpenAI 的 API 构建一个语音转文本网页应用。我们将使用 OpenAI 的 API 来使用其 Whisper 模型,该模型允许我们上传 mp3 格式的音频文件,并提供其文字稿。它甚至可以将其他语言的音频翻译成英文文本,实在令人惊叹。

首先,我们将设置一个新的 Node.js 项目,以便我们可以开始构建我们的应用。因此,我们将创建一个文件夹,在其中构建我们的项目,并使用命令行进入该文件夹,然后我们可以使用以下命令来设置一个新的 Node.js 项目:

npm init

运行此命令后,它将询问一些问题,例如应用程序的名称、入口点等。我们现在可以将其保留为默认设置。之后,你会看到它创建了一个package.json文件。该文件包含有关我们应用程序的信息以及我们为该应用程序安装了哪些包。

接下来的一步是将必要的 Node 模块,即包,安装到我们的应用中,以便我们可以开始构建应用。我们可以通过运行以下命令来完成:

npm install express multer openai cors --save

我们安装这四个包,并使用--save将这些包添加到package.json文件中。这使得克隆仓库的人只需运行一次npm install命令即可轻松安装所有所需的包。

我们还希望在应用程序中使用 nodemon 包,以帮助我们在检测到代码更改时自动刷新和重新加载服务器,这样我们就不需要在每次更改后手动重新启动服务器。因此,我们将其添加为开发依赖项,因为它只用于开发中的帮助,我们不会直接在代码中使用它。我们可以使用以下命令安装它:

npm install --save-dev nodemon

现在我们拥有了开始开发所需的所有包。正如我们在package.json文件中看到的,里面列出了我们安装的所有模块和包,以及一些关于应用程序的细节。package.json文件应如下所示:

{
  "name": "speechtext",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "².8.5",
    "express": "⁴.18.2",
    "multer": "¹.4.5-lts.1",
    "openai": "³.2.1"
  },
  "devDependencies": {
    "nodemon": "².0.22"
  }
}

所以,如我们所见,index.js是写在main中,表示index.js文件是我们应用程序的入口点。如果你还记得,这在我们运行npm init命令时的设置过程中有所要求。如果你将其留作默认设置,你将拥有相同的入口点;否则,你将拥有当时定义的入口点。

现在,我们将创建一个名为index.js的新文件。你可以根据自己定义的入口点命名文件。我们考虑使用index.js

index.js

所以,我们现在将开始构建index.js文件。我们将从将所需模块导入到我们的应用程序开始。对于索引文件,我们需要expresscors。因此,我们首先需要这两个模块:

const express = require('express');
const cors = require('cors');

接下来,我们将创建一个新的express应用程序实例。同时,我们将设置应用程序以使用 cors,处理 json 数据,并使public文件夹包含静态文件,这些文件可以由客户端或前端访问。

const app = express();
app.use(express.static('public'));
app.use(express.json());
app.use(cors());

接下来,我们希望有一个单独的文件来定义 API。我们将创建一个名为routes的文件夹,并在其中创建一个名为api.js的文件,在该文件中定义应用程序所需的 GET 和 POST API。为了让应用程序知道这一点,我们将添加这一行代码,以定义基本 URL 和所有 API 定义文件的位置。这是一个中间件,帮助我们设置应用程序的路由。

app.use('/', require('./routes/api'));

接下来,我们使用一个错误处理的中间件函数,该函数将用于处理应用程序中发生的任何错误。

app.use(function(err,req,res,next){
    res.status(422).send({error: err.message});
});

最后,我们设置应用程序以监听指定端口号的传入请求,我们可以通过使用环境变量来设置,或者直接定义。

app.listen(process.env.PORT || 4000, function(){
    console.log('Ready to Go!');
});

我们为我们的应用程序使用了 4000 端口。我们还在其中添加了一个简单的console.log,当应用程序准备好接收请求时,它会在控制台上打印一条消息。

完整的index.js文件:

const express = require('express');
const cors = require('cors');

const app = express();
app.use(express.static('public'));
app.use(express.json());
app.use(cors());

app.use('/', require('./routes/api'));

app.use(function(err,req,res,next){
    res.status(422).send({error: err.message});
});

app.listen(process.env.PORT || 4000, function(){
    console.log('Ready to Go!');
});

接下来,我们将转到api.js文件,该文件位于routes文件夹内。

api.js

所以,我们现在将开始构建api.js文件。我们将通过将所需模块导入文件来开始。我们将导入expressmulteropenai库。

const express = require("express");
const multer = require("multer");
const { Configuration, OpenAIApi } = require("openai");

Multer 是一个中间件,我们使用它来处理 multipart/form-data,因为我们将处理音频文件的上传。

openai 中,我们需要 ConfigurationOpenAIApi 模块,用于向 Whisper 模型发送 API 请求。

然后,我们将设置 express 路由器,并创建一个 multer 中间件的实例。

const router = express.Router();
const upload = multer();

接下来,我们将配置 OpenAI 并创建一个新的配置实例。我们需要一个 OpenAI 密钥,我们必须在这里将其作为 API 密钥。你可以从 这里 获取密钥。

const configuration = new Configuration({
    apiKey: process.env.OPENAI_KEY,
});

现在,我们创建一个异步函数,该函数接受一个包含歌曲数据的缓冲区,并返回当我们调用 OpenAI 的 Whisper 模型 API 时收到的响应。

async function transcribe(buffer) {
    const openai = new OpenAIApi(configuration);
    const response = await openai.createTranscription(
        buffer, // The audio file to transcribe.
        "whisper-1", // The model to use for transcription.
        undefined, // The prompt to use for transcription.
        'json', // The format of the transcription.
        1, // Temperature
        'en' // Language
    )
    return response;
}

如上所示,我们首先通过使用在代码中之前定义的配置创建 OpenAI 类的新实例。然后我们调用 OpenAI 的 createTranscription 函数,并在其中使用 await 关键字,以便我们在继续之前等待响应。

我们在函数中传递所需的参数,其中包括包含歌曲数据的缓冲区,以及用于转录的模型,在我们的情况下是 whisper-1。然后我们将 prompt 留空。如果愿意,你也可以提供一个 prompt,这将帮助模型通过提供与 prompt 类似的风格来更好地转录音频。我们将接收的数据类型定义为 json,将温度设置为 1,并定义我们希望输出的语言。

接下来,我们将定义 GET 请求。我们使用 sendFile 发送一个包含我们表单的 HTML 文件,用户可以在其中上传音频文件。我们稍后将构建这些 HTML 文件。我们将它提供在基本 URL 上。

router.get("/", (req, res) => {
    res.sendFile(path.join(__dirname, "../public", "index.html"));
});

接下来,我们定义 POST 请求,它将处理音频文件的上传。我们使用 multer 中间件来管理文件上传部分。然后我们从音频文件创建一个缓冲区,该缓冲区将包含可以发送到 OpenAI API 的音频文件数据。我们使用上传音频文件的原始名称为缓冲区设置一个名称。

然后我们调用 transcribe 函数,一旦获得响应,我们将 JSON 数据发送回客户端。我们将转录文本和音频文件名返回到前端。我们还有一个 catch 方法来处理任何错误。

router.post("/", upload.any('file'), (req, res) => {
    audio_file = req.files[0];
    buffer = audio_file.buffer;
    buffer.name = audio_file.originalname;
    const response = transcribe(buffer);
    response.then((data) => {
        res.send({ 
            type: "POST", 
            transcription: data.data.text,
            audioFileName: buffer.name
        });
    }).catch((err) => {
        res.send({ type: "POST", message: err });
    });
});

最后,我们导出 router 模块,这样其他文件就可以导入它们。

module.exports = router;

所以,api.js 文件的完整代码是:

const express = require("express");
const multer = require("multer");
const { Configuration, OpenAIApi } = require("openai");

const router = express.Router();
const upload = multer();

const configuration = new Configuration({
    apiKey: process.env.OPENAI_KEY,
});

async function transcribe(buffer) {
    const openai = new OpenAIApi(configuration);
    const response = await openai.createTranscription(
        buffer, // The audio file to transcribe.
        "whisper-1", // The model to use for transcription.
        undefined, // The prompt to use for transcription.
        'json', // The format of the transcription.
        1, // Temperature
        'en' // Language
    )
    return response;
}

router.get("/", (req, res) => {
    res.sendFile(path.join(__dirname, "../public", "index.html"));
});

router.post("/", upload.any('file'), (req, res) => {
    audio_file = req.files[0];
    buffer = audio_file.buffer;
    buffer.name = audio_file.originalname;
    const response = transcribe(buffer);
    response.then((data) => {
        res.send({ 
            type: "POST", 
            transcription: data.data.text,
            audioFileName: buffer.name
        });
    }).catch((err) => {
        res.send({ type: "POST", message: err });
    });
});

module.exports = router;

现在,我们已经完成了所有的后端部分。接下来我们将编写 HTML 文件和一些前端 JavaScript 代码,以处理表单提交和数据保存在 本地存储 以及从本地存储中检索数据。

我们创建一个 public 文件夹,在其中我们将创建两个 HTML 文件——index.htmltranscribe.html

我们将从 index.html 文件开始:

index.html

在这个文件中,我们将构建一个页面,展示上传音频文件的表单。我们将使用 Bootstrap CSS,通过 CDN 引入它。我们还在 HTML 文件的末尾通过 CDN 引入 Bootstrap JS。

然后,我们创建了一个简单的卡片,要求用户上传音频文件。我确保提交的文件格式为.mp3,因为这是 OpenAI 的 API 唯一接受的格式。我们显示一个按钮,点击后提交表单。

然后,我们有了处理表单提交的 JavaScript 代码。所以,首先,我们通过阻止表单提交事件的默认行为来停止页面刷新。然后,我们获取表单数据,即音频文件,并将其作为 POST 请求发送到后台。接着,我们等待响应并将其存储在数据变量中。

如果数据中有可用的转录文本,我们将转录文本和音频文件名存储在本地存储中,以便在需要显示转录文本的下一页访问它们。虽然有多种方法可以传递信息,比如通过 URI 传递信息,但在这里我们使用本地存储来实现。

保存数据到本地存储后,我们更改窗口位置以加载transcribe.html文件。

<!DOCTYPE html>
<html>
<head>
 <title>Speech to Text</title>
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
</head>

<body style="background-color: #f2f2f2;">
 <div class="container mt-5">
  <div class="row justify-content-center">
   <div class="col-md-6">
    <div class="card">
     <div class="card-header">
      Upload Audio File
     </div>
     <div class="card-body">
      <form id="transcription-form" enctype="multipart/form-data">
          <div class="form-group">
           <label for="file-upload"><b>Select file:</b></label>
           <input id="file-upload" type="file" name="file" class="form-control-file" accept=".mp3" style="margin-bottom: 20px">
          </div>
          <input type="submit" value="Transcribe" class="btn btn-primary"></input>
      </form>
     </div>
    </div>
   </div>
  </div>
 </div>

 <script>
        document.getElementById("transcription-form").addEventListener("submit", async function (event) {
            event.preventDefault();

            const formData = new FormData(event.target);
            const response = await fetch("/", {
                method: "POST",
                body: formData,
            });
            const data = await response.json();

            if (data.transcription) {
                localStorage.setItem("transcription", data.transcription);
                localStorage.setItem("audioFileName", data.audioFileName);
                window.location.href = "/transcribe.html";
            } 
   else {
                console.error("Error:", data.message);
            }
        });
    </script>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
</body>
</html>

所以上述代码构建了index.html文件,该文件将向用户显示一个表单,用户可以通过这个表单上传音频文件。

这是其中一个截图的效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

音频上传页面 — index.html

接下来,我们将构建transcribe.html文件。

transcribe.html

在这个文件中,我们将显示用户上传的音频文件的转录文本。因此,我们将再次使用 Bootstrap CSS 和 JS,通过 CDN 引入这些资源。

然后,我们定义了一些自定义 CSS 来美化元素,使其看起来更好。我们在一个容器中显示音频文件名和该音频文件的转录文本。

在此页面底部的 JavaScript 代码中,我们从本地存储中获取音频文件名和转录文本,并使用 id 将这些数据推送到相应的 HTML 元素中。

<!DOCTYPE html>
<html>
<head>
 <title>Transcription</title>
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">

 <style>
  h1 {
   margin-top: 20px;
   margin-bottom: 10px;
   font-size: 2.5rem;
   font-weight: bold;
   color: #333;
  }

  p {
   font-size: 1.2rem;
   color: #333;
   margin-bottom: 30px;
  }

  .container {
   margin-top: 50px;
   margin-bottom: 50px;
   max-width: 600px;
   padding: 30px;
   background-color: #fff;
   box-shadow: 0 0 10px rgba(0,0,0,0.2);
   border-radius: 5px;
  }
 </style>
</head>

<body style="background-color: #f2f2f2;">
 <div class="container">
  <h1>Audio File:</h1>
  <p id="audioFileName"></p>

  <h1>Transcription:</h1>
  <p id="transcription"></p>
 </div>

 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>

 <script>
     const audioFileName = localStorage.getItem("audioFileName");
     const transcription = localStorage.getItem("transcription");
     document.getElementById("audioFileName").innerHTML = audioFileName;
     document.getElementById("transcription").innerHTML = transcription;
 </script>
</body>
</html>

我尝试了两个不同的小音频文件,一个是英语录制的,另一个是印地语录制的。虽然第二个音频文件是用印地语录制的,但我希望看到英语的输出,从而测试其翻译能力。它在转录两个音频文件时都非常准确。不过,在多次运行中,有时会产生不正确的模糊转录,但许多时候,转录结果大多是正确的。

我在下面附上了转录的屏幕截图。这些截图并不完全正确,但我认为它们的转录准确率大约为 85%–90%。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

英语音频文件的转录

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

印地语音频文件的英语转录

因此,我们已经成功使用 OpenAI 的 API 和 Node.js 构建了一个语音转文本的 Web 应用程序。希望你喜欢构建这个应用,并从这篇文章中学到了一些新东西。你还可以调整参数进行实验,并比较结果,以更好地了解在不同场景下什么效果更好。

感谢阅读本文。接下来你必须阅读的一些文章包括:

[## 使用 Next.js 构建一个简单的 Todo 应用

让我们使用 Next.js 构建一个简单的 Todo 应用,这将教会你 CRUD(创建、读取、更新和…

如何使用 Next.js 构建一个简单的 Todo 应用 [## 如何在 Django 中实现多用户类型

学习如何在 Django 中实现多用户类型,处理认证,并根据用户类型进行重定向。

如何在 Django 中实现多用户类型 [## 使用 React 构建一个简单的 Todo 应用

让我们使用 React 构建一个简单的 Todo 应用,这将教会你 CRUD(创建、读取、更新和…

使用 React 构建一个简单的 Todo 应用 [## 使用 Django 构建一个社交媒体网站 — 项目设置(第一部分)

在第一部分中,我们专注于设置项目和安装所需组件,通过设置密码…

构建一个社交媒体网站使用 Django — 项目设置(第一部分)

使用 LangChain 和 GPT-3 构建一个透明的文档问答机器人

原文:towardsdatascience.com/build-a-transparent-question-answering-bot-for-your-documents-with-langchain-and-gpt-3-7f6a71f379f8

开发一个信息丰富的问答机器人指南,并显示所使用的来源

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Konstantin Rink

·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 7 月 22 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Justin Ha拍摄,来源于Unsplash

问答系统在分析大量数据或文档时可以大有帮助。然而,模型用来生成答案的来源(即文档的部分内容)通常不会在最终答案中显示。

理解回应的背景和来源不仅对寻求准确信息的用户有价值,也对希望持续改进其 QA 机器人(问答机器人的开发者)有帮助。通过回答中包含的来源,开发者可以深入了解模型的决策过程,从而促进迭代改进和微调。

本文展示了如何使用 LangChain 和 GPT-3(text-davinci-003)创建一个透明的问答机器人,通过两个示例展示了生成答案时所使用的来源。

在第一个示例中,你将学习如何创建一个透明的 QA 机器人,利用你的网站内容回答问题。在第二个示例中,我们将探讨使用来自不同 YouTube 视频的转录文本,包括带有和不带有时间戳的文本。

处理数据并创建向量存储

在我们能够利用像 GPT-3 这样的语言模型的能力之前,我们需要将文档(例如网站内容或 YouTube 转录文本)以正确的格式(首先分块,然后生成嵌入)处理,并将其存储在向量存储中。下图 1 展示了从左到右的处理流程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1. 数据处理和向量存储创建的流程图(图像来源于作者)。

网站内容示例

在这个示例中,我们将处理网络门户It’s FOSS的内容,该门户专注于开源技术,特别是 Linux。

首先,我们需要获取一个所有待处理的文章列表并存储在我们的向量存储中。下面的代码读取了sitemap-posts.xml文件,该文件包含了所有文章的链接列表。

import xmltodict
import requests

r = requests.get("https://news.itsfoss.com/sitemap-posts.xml")
xml = r.text
rss = xmltodict.parse(xml)

article_links = [entry["loc"] for entry in rss["urlset"]["url"]]

在本文撰写时,列表中包含了超过 969 个文章链接。

有了链接列表,我们现在可以编写一个小的辅助函数,称为extract_content,它使用BeautifulSoup从文章页面中提取包含相关内容的特定元素。

from bs4 import BeautifulSoup
from tqdm.notebook import tqdm

def extract_content(url):
    html = requests.get(url).text
    soup = BeautifulSoup(html, features="html.parser")

    elements = [
        soup.select_one(".c-topper__headline"),
        soup.select_one(".c-topper__standfirst"),
        soup.select_one(".c-content"),
    ]

    text = "".join([element.get_text() for element in elements])

    return text

articles = []
# Limited the list of > 900 articles to 10 for this example
for url in tqdm(article_links[0:10], desc="Extracting article content"):
    articles.append({"source": url, "content": extract_content(url)})

最后一步,我们遍历链接列表,并将我们的辅助函数extract_content应用于每个 URL。为了演示目的,我将列表限制为 10 个元素。如果你想爬取所有文章,只需从article_links[0:10]中删除[0:10]

articles列表现在包含每篇文章的字典,字典中包括"source"(文章链接)和"content"(文章内容)。文章的链接将在最终答案中作为来源显示。

由于 GPT-3 有一个令牌限制(4,096 个令牌),因此将长文章分成是有意义的。这些块将被组合在一起并发送给 GPT-3。

下面的代码将文章内容拆分成几个块。

from langchain.text_splitter import RecursiveCharacterTextSplitter

rec_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, 
                                              chunk_overlap=150)

web_docs, meta = [], []

for article in tqdm(articles, desc="Splitting articles into chunks"):
    splits = rec_splitter.split_text(article["content"])
    web_docs.extend(splits)
    meta.extend([{"source": article["source"]}] * len(splits))

我们在这里使用RecursiveCharacterTextSplitter,因为它旨在尽可能长时间地将语义相关的内容保持在一起

一旦完成,我们只需执行以下命令,将文章及其来源存储到我们的向量存储中。

import os
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS

os.environ["OPENAI_API_KEY"] = "YOUR KEY"

article_store = FAISS.from_texts(
    texts=web_docs, embedding=OpenAIEmbeddings(), metadatas=meta
)

在这个示例中,我们使用FAISS作为向量存储,并使用OpenAIEmbeddings作为我们的嵌入模型。当然,也可以探索其他向量存储选项,例如Chroma,并尝试 Hugging Face 的嵌入模型解决方案。

注意:你还可以通过运行article_store.save_local("your_name")来保存你的向量存储,这样你就不必每次使用时都重新创建它。更多详情见这里

如果你不想处理 YouTube 转录内容,你可以跳过下面的部分继续到下一节“运行透明问答”。

YouTube 转录示例

转录内容可以通过两种不同且独立的方式处理。第一个选项演示了如何处理 YouTube 转录内容,同时保留视频链接作为来源(例如,youtu.be/XYZ。)。

第二部分做了同样的事情,但说明了如何保留链接包括时间戳(例如,https://youtu.be/XYZ&t=60)以获取更详细的信息。

对于这两种方法,使用了来自频道StatQuest的以下 YouTube 视频的转录:

YouTube 转录示例(不带时间戳)

第一部分非常直接。下面的代码利用了 LangChain 的 DocumentLoader YoutubeLoader,它集成了youtube-transcript-apipytube

from langchain.document_loaders import YoutubeLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
import os

os.environ["OPENAI_API_KEY"] = "YOUR KEY"

yt_ids = [
    "OtD8wVaFm6E",  # XGBoost Part 1 (of 4): Regression
    "8b1JEDvenQU",  # XGBoost Part 2 (of 4): Classification
    "ZVFeW798-2I",  # XGBoost Part 3 (of 4): Mathematical Details
    "oRrKeUCEbq8",  # XGBoost Part 4 (of 4): Crazy Cool Optimizations
]

yt_docs = []

for yt_id in tqdm(yt_ids, desc="Retrieving transcripts"):
    splitter = CharacterTextSplitter(chunk_size=1500, chunk_overlap=150, 
                                     separator=" ")
    yt_loader = YoutubeLoader(yt_id, add_video_info=True)
    yt_docs.extend(yt_loader.load_and_split(splitter))

为了避免与令牌限制发生冲突,我们使用CharacterTextSplitter将数据拆分为几个块。add_video_info设置为True,以接收视频的标题和作者信息

返回的分块转录是文档对象。在创建嵌入并将其存储在向量存储中之前,我们通过添加有关标题作者视频链接的信息来操作或扩展它们的元数据。

# Manipulate / extend source attribute
for doc in yt_docs:
    doc.metadata["source"] = (
        doc.metadata["title"]
        + " ["
        + doc.metadata["author"]
        + "] "
        + "https://youtu.be/"
        + doc.metadata["source"]
    )

# Vector store
yt_store = FAISS.from_documents(yt_docs, OpenAIEmbeddings())

YouTube 转录示例(带时间戳)

第二种方法稍微复杂一些。在这里,我们使用名为youtube-transcript-api的不同包来检索转录。输出是一个包含文本、开始时间和持续时间的字典列表。我们需要切换到不同的包,因为YoutubeLoader包不返回时间戳。

示例可以在这里看到:

[
 {'text': "gonna talk about XG boost part 1 we're",
  'start': 14.19,
  'duration': 6.21},
 {'text': 'gonna talk about XG boost trees and how',
  'start': 17.91,
  'duration': 6.66},
...
]

从每个文本条目创建文档对象没有多大意义,因为条目太短(例如,上述示例中每个条目只有 8 个单词),不便于后续使用。在向量存储中搜索时,只返回有限数量的匹配文档(例如,4 个),信息内容不足。

因此,我们需要首先将文本条目聚合或合并成适当的文本块。下面的代码片段包含一个自定义助手函数。

# Create transcript df
def create_transcript_df(yt_transcript: list, yt_id: str):
    return (
        pd.DataFrame(yt_transcript)
        .assign(start_dt=lambda x: pd.to_datetime(x["start"], unit="s"))
        .set_index("start_dt")
        .resample("3min")
        .agg({"text": " ".join})
        .reset_index()
        .assign(start_dt=lambda x: x["start_dt"].dt.minute * 60)
        .assign(
            source=lambda x: "https://youtu.be/"
            + yt_id
            + "&t="
            + x["start_dt"].astype("str")
        )
        .drop(columns=["start_dt"])
    )

该助手应用重采样来调整时间维度为 3 分钟步长。换句话说,它将转录合并为 3 分钟的文本部分。有了这个功能,我们现在可以开始获取和处理转录。

from youtube_transcript_api import YouTubeTranscriptApi

yt_ids = [
    "OtD8wVaFm6E",  # XGBoost Part 1 (of 4): Regression
    "8b1JEDvenQU",  # XGBoost Part 2 (of 4): Classification
    "ZVFeW798-2I",  # XGBoost Part 3 (of 4): Mathematical Details
    "oRrKeUCEbq8",  # XGBoost Part 4 (of 4): Crazy Cool Optimizations
]
transcript_dfs = []
for yt_id in tqdm(yt_ids, desc="Fetching transcription"):
    yt_transcript = YouTubeTranscriptApi.get_transcript(yt_id)
    transcript_dfs.append(create_transcript_df(yt_transcript, yt_id))

transcripts_df = pd.concat(transcript_dfs).reset_index(drop=True)

结果的摘录可以在下图中看到。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2. 转录数据框的摘录(图片由作者提供)。

由于合并的 3 分钟部分现在可能会导致令牌限制问题,我们需要在生成嵌入并将其存储在向量存储中之前,使用分割器再次处理它们。

from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
import os

os.environ["OPENAI_API_KEY"] = "YOUR KEY"

text_splitter = CharacterTextSplitter(separator=" ", chunk_size=1500, 
                                      chunk_overlap=150)

yt_docs, yt_meta = [], []

for index, row in tqdm(transcripts_df.iterrows(), total=len(transcripts_df)):
    splits = text_splitter.split_text(row["text"])
    yt_docs.extend(splits)
    yt_meta.extend([{"source": row["source"]}] * len(splits))
    print(f"Split {row['source']} into {len(splits)} chunks")

yt_ts_store = FAISS.from_texts(yt_docs, OpenAIEmbeddings(), metadatas=yt_meta)

运行透明问答

有了填充的向量存储后,我们现在可以专注于透明问答。下面的图给出了该过程的概述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3. 透明问答过程概述(图片由作者提供)。

我们首先定义一个问题,然后由嵌入模型或 API转换为嵌入。向量存储利用这个问题嵌入来搜索存储中的’n’(默认:4)相似文档或片段。随后,将每个文档或片段的内容与提示组合并发送到 GPT-3。

GPT-3 返回的结果然后与另一个提示组合,在最后一步再次发送回 GPT-3以获取最终答案,包括来源。

网站内容示例

在使用RetrievalQAWithSourcesChain之前,我们确保通过实施记忆使我们的机器人记住之前的对话。这增强了与用户的上下文相关互动。

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(
    memory_key="chat_history",
    input_key="question",
    output_key="answer",
    return_messages=True,
)

为了将之前的聊天记录整合到使用的提示中,我们需要修改现有的模板。

from langchain import PromptTemplate

template = """You are a chatbot having a conversation with a human.
Given the following extracted parts of a long document and a question, 
create a final answer.
{context}
{chat_history}
Human: {question}
Chatbot:"""

question_prompt = PromptTemplate(
    input_variables=["chat_history", "question", "context"], template=template
)

之后,我们可以利用 RetrievalQAWithSourcesChain 来提问。在这个示例中,我们设置k=4,这意味着我们将查询向量存储以获取 4 个最相似的文档。

from langchain.chains import RetrievalQAWithSourcesChain

article_chain = RetrievalQAWithSourcesChain.from_llm(
    llm=OpenAI(temperature=0.0),
    retriever=article_store.as_retriever(k=4),
    memory=memory,
    question_prompt=question_prompt,
)

result = article_chain({"question": "What is Skiff?"}, 
                        return_only_outputs=True)

结果以字典形式返回:

{'question': 'What is Skiff?',
 'answer':   'Skiff is a privacy-focused email service with unique 
              functionalities such as the ability to manage multiple 
              sessions, appearance tweaks, dark mode, white theme, 
              two layouts, supporting imports from Gmail, Outlook, 
              Proton Mail, and more, creating and managing aliases, 
              and connecting a crypto wallet from Coinbase, BitKeep, 
              Brave, and others to send/receive email utilizing Web3\. 
              It also includes Pages to create/store documents securely, 
              the ability to use Skiff's server or IPFS (decentralized 
              technology) for file storage, and Skiff Pages, 
              Encrypted Cloud Storage With IPFS Support.\n',
 'sources': 'https://news.itsfoss.com/skiff-mail-review/'}

我们可以观察到,结果包含了回答给定问题所用的来源。为了生成这个最终答案,API 被调用了5 次:4 次提取最相似的 4 个片段中的相关信息,额外 1 次生成最终答案。

我们还可以提出引用之前问题的问题。

article_chain(
    {"question": "What are its functionalities?"},
    return_only_outputs=True,
)

结果将如下所示。

{
'answer': "Skiff offers a range of functionalities, 
including Web3 integration, IPFS decentralized storage, 
creating and managing aliases, connecting crypto wallets, 
getting credits to upgrade your account, importing from Gmail, 
Outlook, Proton Mail, and more, Pages to create/store documents securely, 
encrypted cloud storage with IPFS support, and the ability to use 
Skiff's server or IPFS (decentralized technology) for file storage.\n",
 'sources': 'https://news.itsfoss.com/anytype-open-beta/, 
             https://news.itsfoss.com/skiff-mail-review/'
}

请记住,对于这些问题,API 也被调用了 5 次。

YouTube 转录示例(有时间戳和没有时间戳)

YouTube 转录示例的代码与网站的代码非常相似。首先,我们初始化 ConversationBufferMemory,并创建一个自定义问题提示模板。

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(
    memory_key="chat_history",
    input_key="question",
    output_key="answer",
    return_messages=True,
)

template = """You are a chatbot having a conversation with a human.
    Given the following extracted parts of a long document and a question, 
    create a final answer.
    {context}
    {chat_history}
    Human: {question}
    Chatbot:"""

question_prompt = PromptTemplate(
    input_variables=["chat_history", "question", "context"], template=template
)

然后我们创建带有来源的 QA 链。

# Use yt_store for YouTube transcripts without timestamps or
# yt_ts_store with timestamps as sources.
yt_chain = RetrievalQAWithSourcesChain.from_llm(
    llm=OpenAI(temperature=0.0),
    retriever=yt_store.as_retriever(k=4),
    memory=memory,
    question_prompt=question_prompt,
)

让我们问一个问题。

result = yt_chain(
    {
        "question": "What is the difference in building a tree for a 
                     regression case compared to a classification case?"
    },
    return_only_outputs=True
)

不带时间戳的示例结果:

{'answer': ' The main difference between building a tree for a regression case 
              and a classification case is that in a regression case, the goal 
              is to predict a continuous value, while in a classification case,
              the goal is to predict a discrete value. In a regression case, 
              the tree is built by splitting the data into subsets based on 
              the value of a certain feature, while in a classification case, 
              the tree is built by splitting the data into subsets based on 
              the value of a certain feature and the class label. 
              Additionally, in a regression case, 
              the weights are all equal to one, 
              while in a classification case, the weights are the previous 
              probability times one minus the previous probability.\n',
 'sources': 'XGBoost Part 2 (of 4): Classification [StatQuest with Josh Starmer] https://youtu.be/8b1JEDvenQU, 
             XGBoost Part 3 (of 4): Mathematical Details [StatQuest with Josh Starmer] https://youtu.be/ZVFeW798-2I, 
             XGBoost Part 4 (of 4): Crazy Cool Optimizations [StatQuest with Josh Starmer] https://youtu.be/oRrKeUCEbq8'
}

带时间戳的示例结果:

{'answer': 'The difference in building a tree for a regression case compared 
            to a classification case is that in a regression case, the goal 
            is to predict a continuous value, while in a classification case, 
            the goal is to predict a probability that the drug will be 
            effective. Additionally, the numerator for classification is the 
            same as the numerator for regression, but the denominator 
            contains a regularization parameter. The denominator for 
            classification is different from the denominator for regression, 
            and is the sum for each observation of the previously predicted 
            probability times 1 minus the previously predicted probability. 
            The only difference between building a tree for a regression case 
            and a classification case is the loss function.\n',
 'sources': 'https://youtu.be/ZVFeW798-2I&t=0 
             https://youtu.be/8b1JEDvenQU&t=180
             https://youtu.be/OtD8wVaFm6E&t=0'
}

结论

LangChain 的 RetrievalQAWithSourcesChain 和 GPT-3 的结合非常适合提高问答的透明度。正如过程图所示(图 3),获取最终答案需要多次调用 OpenAI。

根据你使用服务的情况和需要处理的类似文档数量,调用次数可能会增加,导致更高的费用。这确实值得关注。不过,对于你的爱好项目来说,这不应该太关键。为了更好地关注费用和发送的提示,可以考虑使用 PromptlayerTruLens

Colab 笔记本可以在这里找到:

资料来源

所有者或创作者已经提前询问是否允许我将他们的内容/数据用作本文的示例。

  • It’s FOSS., “It’s FOSS”, itsfoss.com/

  • StatQuest. “XGBoost 第一部分(共 4 部分):回归” YouTube, Joshua Starmer, 2019 年 12 月 16 日, youtu.be/OtD8wVaFm6E

  • StatQuest. “XGBoost 第二部分(共 4 部分):分类” YouTube, Joshua Starmer, 2020 年 1 月 13 日, youtu.be/8b1JEDvenQU

  • StatQuest. “XGBoost 第三部分(共 4 部分):数学细节” YouTube, Joshua Starmer, 2020 年 2 月 10 日, youtu.be/ZVFeW798-2I

  • StatQuest. “XGBoost 第四部分(共 4 部分):疯狂炫酷的优化” YouTube, Joshua Starmer, 2020 年 3 月 2 日, youtu.be/oRrKeUCEbq8

用 Jupyter 和 Mercury 构建 Web 应用

原文:towardsdatascience.com/build-a-web-app-with-jupyter-and-mercury-9d59661441b7

教程

Mercury 提供了一种简单的方法,将 Jupyter Notebooks 转换为互动式 Web 应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Alan Jones

·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 5 月 9 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我在提及代码开发还是 CO2 排放?照片由 Etienne Girardet 供稿于 Unsplash

如果没有 Jupyter Notebooks,我们将会处于何地?它们无疑是数据科学社区的基石之一。

它们非常适合原型设计和/或逐步构建和展示数据科学应用,但在演示方面不太好。

如果你想将你的工作结果展示给利益相关者,但不想展示所有聪明的代码,那么你可以用 Streamlit 或 Dash 重新编码所有内容,或者用 Flask 或 Django 构建一个 Web 应用。

但还有一种替代方案。

Mercury 是一个可以将你的 Notebook 转换为完全互动的 Web 应用的系统。你可以添加滑块和选择框等控件,制作一个完全互动的应用程序。

这完全是 100% Python 和相当简单,尽管结果比起其他工具可能少了些复杂性,但如果你寻找的是一个简单的解决方案,值得一看。

一个示例应用

我们将创建的应用将如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

你可以看到它由两部分组成:一个包含文本和输入控件的小侧边栏——一个滑块和一个选择框;以及一个包含笔记本代码输出的主窗口。所有 Mercury 应用都遵循这种模式,即小侧边栏包含控件,而主窗口显示代码输出。

你看到的地图是一个显示国家 CO2 排放量随时间变化的分级图。侧边栏中的滑块用于选择正确的年份。

在主窗口向下滚动,你将看到一条折线图,展示了某个大陆随时间变化的排放输出。大陆可以从侧边栏的选择框中选择。

每当输入组件的值发生变化时,代码会重新运行,并显示图表的新版本。

Mercury V.2

我在大约一年前首次查看了 Mercury,并喜欢从你已经用 Jupyter 编写的代码创建 Web 应用程序的简单性。

在过去的一年里有很多分心的事物(PyScript、Streamlit 以及一些大家似乎都在谈论的 AI 应用程序),但我终于重新审视了 Mercury,并且有了一些变化。

主要的是,版本 2 具有支持与应用程序交互的组件。这些替代了版本 1 中有些笨重的 YAML 定义,并且与版本 1 不同,它们可以在 Jupyter Notebook 以及应用程序中操作。

在这篇文章中,我将使用开源版本构建一个应用程序,这意味着你的应用程序将带有 Mercury 品牌(也提供商业许可证)。

首先,编写一个 Notebook

我们将逐步构建 Notebook 单元格,然后将其转换为 Web 应用程序。

但首先,安装 Mercury 库。

pip install mercury

我将在最后提供源代码和数据的链接,但如果你想跟随教程,你需要创建一个新的 Jupyter Notebook 并安装以下库:

import mercury as mr
import plotly.express as px
import pandas as pd

这,当然,是你的第一个单元格。

下一个完全关注于 Mercury,并且在 Notebook 中不会输出任何重要内容。

# set Application parameters
app = mr.App(title="CO2 Emissions control panel",
        description="Graphs showing the emissions over more than 100 years",
        show_code=False,
        show_prompt=False,
        continuous_update=True,
        static_notebook=False,
        show_sidebar=True,
        full_screen=True,
        allow_download=False)

此单元格在 Mercury 创建应用程序的 Web 版本时设置一些信息。前两个字段是不言而喻的,这些数据会在 Mercury 版本中显示。如果你愿意接受其余字段的默认值,那么这些就是你需要包括的唯一字段。

作为参考,这里列出了所有参数及其用途。

  • title (string) — 应用程序的标题

  • description (string) — 应用程序的描述

  • show_code (boolean) — 设置为True以在应用程序中显示 Notebook 代码。默认值为False

  • show_prompt (boolean) — 如果你希望在应用程序中显示 Notebook 提示,请将此值设置为Trueshow_code也需要设置为True。默认值为False

  • continuous_update (boolean) — 默认值为True,意味着在输入组件值变化后,应用程序会重新运行。如果设置为False,将显示一个运行按钮,用于用更新后的组件数据重新运行应用程序。

  • static_notebook (boolean) — 设置为True时,应用程序在组件值变化时不会重新运行,Notebook 是静态的。默认值为False,即为交互式应用程序。

  • show_sidebar (boolean) — 应用程序由主显示和侧边栏组成,默认情况下在启动时显示。用户在应用程序运行时可以随时打开或关闭侧边栏。

  • full_screen (布尔值)——默认值为True,表示笔记本全宽显示。设置为False将笔记本宽度限制为 1140px。

  • allow_download (布尔值)——默认情况下有一个下载按钮,可以将结果导出为 PDF 或 HTML。你可以通过设置allow_download=False来隐藏这个按钮。

现在,我们需要下载一些数据。这里是下一个单元格。

# Get the data
def get_countries_data(): 
    url = "./data/countries_df.csv"
    return pd.read_csv(url)
def get_continent_data():
    url = './data/continents_df.csv'
    return pd.read_csv(url)
def get_world_data():
    url = './data/world_df.csv'
    return pd.read_csv(url)
def get_group_data():
    url = './data/income_types_df.csv'
    return pd.read_csv(url)

df_countries= get_countries_data()
df_continents= get_continent_data()
df_world = get_world_data()
df_groups = get_group_data()

我们从本地目录加载数据到 Pandas 数据框中。这些数据记录了几十年来的二氧化碳排放情况,按国家、洲和收入组分类。

每个数据框看起来类似于下面的表格。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

它们包含以下列:

  • 实体:国家、大洲、收入组或‘世界’

  • 代码:ISO 国家代码(如果它是国家)

  • 年份

  • 年度二氧化碳排放量:来自燃烧化石燃料和工业过程

  • 包括土地利用变化的年度二氧化碳排放量:前一列和下一列的总和

  • 土地利用变化导致的年度二氧化碳排放量

这是公开可用的数据,你可以从多个来源下载,例如,我们的世界数据¹。

这里的数据比我们实际需要的稍多——我们将在这个练习中使用前两个数据框,并仅使用部分列。

小部件

有许多可用于输入和输出的小部件。我们只会查看其中的几个,但完整列表可以在Mercury 文档中找到。

这是包含选择年份滑块的小部件的单元格。

mr.Note(text="__Select a year to see the global emissions in the map__")

ymax = df_countries['Year'].max()
ymin = df_countries['Year'].min()

yearSlider = mr.Slider(value=2000, min=ymin, max=ymax, label="Year", step=1)

mr.Note(text="---")

在第一行,我们看到一个输出小部件mr.Note,它简单地显示 Markdown 文本。这里我们用它来提示用户从滑块中选择一个年份。然后我们计算数据中的年份范围,并使用这些值来显示滑块。

滑块小部件的参数比较自解释:我们从默认值开始,然后是最大值和最小值,标签,最后是步长值。

从小部件返回的值实际上是一个小部件对象。要从该对象中获取值,我们必须使用.value属性,如下所示。

最后一行是另一条 Markdown 字符串——一条将滑块与下方的小部件分开的线。

在 Jupyter 中,输出位于单元格下方,通常,你会看到文本和一个完全可操作的简单滑块,值会按预期变化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

笔记本中的滑块小部件——作者提供的图片

当我们将其转换为应用程序时,滑块将位于侧边栏中。

下一个单元格显示地图。我们首先从滑块对象中获取年份值,然后计算二氧化碳排放范围的最大值和最小值。然后,我们使用 Plotly 计算分级地图并显示它。

# Get the year and find the maximum and minimum values to set the range for the choropleth
year = yearSlider.value
max = df_countries['Annual CO₂ emissions'].max()
min = df_countries['Annual CO₂ emissions'].min()

fig1 = px.choropleth(df_countries[df_countries['Year']==year], 
                    locations="Code",
                    color="Annual CO₂ emissions",
                    hover_name="Entity",
                    range_color=(min,max),
                    color_continuous_scale=px.colors.sequential.Blues,
                    width=1000, height=500,
                    title = f"Annual CO2 Emission for {year}")

fig1.show()

地图看起来如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

另一个小部件

按照之前类似的模式。首先是一个指令,然后我们从数据框的Entity列中计算出所有大陆的唯一列表。

这次我们使用选择小部件mr.Select

该功能接受一个默认值、一个可供选择的列表和一个标签作为参数。

mr.Note(text=f"__Select a continent to see the global emissions over the entire period__")

continents = df_continents['Entity'].unique()

select = mr.Select(value="Europe", choices=continents, label="")
selected_continent = select.value

单元格的输出如下图所示。但同样,在应用程序中,它将出现在侧边栏中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

笔记本中的选择框小部件——由作者提供的图片

然后我们使用选择框中的值来绘制适当的折线图。我们通过使用该值过滤数据框,以便仅返回所选大陆的数据。然后使用这些数据绘制图表。

df = df_continents[df_continents['Entity'] == selected_continent]

fig2 = px.line(df,"Year","Annual CO₂ emissions", 
               width=1000, height=500,
               title=f"Annual CO₂ emissions for {selected_continent}")

fig2.show()

图表看起来像下面的图片。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者提供的图片

这可能听起来很明显,但如果你运行整个笔记本,你将得到与小部件的默认值对应的结果。要更改小部件值并获得具有新值的相应图表,你需要逐个单元格运行笔记本。

还有其他小部件,如输入文本或数字值的字段、按钮和复选框——详情请参见文档

这就是 Jupyter Notebook 的所有内容。现在来创建一个网络应用程序。

创建一个网络应用程序

从命令提示符导航到包含笔记本的目录并输入:

mercury run

这将拾取该目录中的所有笔记本,并运行一个类似下面的网页,其中包含每个笔记本的面板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者提供的图片

在我的情况下,只有一个笔记本——我们刚刚看到的那个——当你点击它时,它将运行应用程序。

提醒你它是这样的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者提供的图片

你可以看到,小部件比笔记本中显示的要更具吸引力,并且它们出现在侧边栏中。

应用程序是完全交互式的:当你更改小部件值时,小部件下方的所有笔记本单元都会重新运行。因此,改变滑块会更新地图,选择框的选择更改时,图表会重新绘制。

我不得不承认这里有点作弊。Mercury 复制了一些演示应用程序,并创建了一个名为welcome.md的文件,其中包含欢迎信息。虽然这些对初学者很有用,但为了清晰和简单,我已经删除了笔记本文件夹中的笔记本,并从welcome.md中删除了所有内容。

结果

结果是一个简单但功能齐全的应用程序。

缺点是 Mercury 应用程序始终具有相同的布局——一个包含小部件的侧边栏和一个显示代码结果的主窗口。此外,主窗口有一个简单的线性布局,无法更改。

这并不是很灵活,虽然对许多目的来说足够,但你可以通过像 Streamlit 或 Dash 这样的替代应用构建工具创建更复杂的布局。

总的来说,如果你乐意在 Jupyter Notebooks 中编写 Python,那么 Mercury 的学习曲线几乎可以忽略不计。而且,尽管结果不如其他应用构建器那样灵活,但转换为应用的过程和在终端窗口中输入一个命令一样简单。

当然,我们的应用正在我们本地机器上的专用服务器上运行。在网上发布则是另一回事,我会在以后查看发布选项。

与此同时,感谢阅读,希望这对你有帮助。本文的代码和数据可以在我的网页上找到,此外还有一个演示应用的链接和如何在 Mercury Cloud 上发布应用的说明。你也可以在那里看到我其他工作的链接,并且可以注册我的偶尔新闻通讯。

说明

[1] 我们的数据世界 是一个信息宝库,其使命是发布*“研究和数据以应对世界上最大的难题”*。他们的所有工作都在创作共用 BY 许可协议下提供。

免责声明:我与 Mercury 没有关联。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值