65.9K
CodeProject 正在变化。 阅读更多。
Home

语义网与自然语言处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (14投票s)

2014年7月16日

CPOL

21分钟阅读

viewsIcon

42129

在高级编程环境中,使用AlchemyAPI处理和筛选RSS订阅源

运行此应用程序...

  1. 您需要向AlchemyAPI注册以获取API密钥,并且此密钥必须放置在bin\Debug(或bin\Release)文件夹中的“alchemyapikey.txt”文件中。
  2. https://github.com/cliftonm/HOPE 下载代码
  3. 检出“semantic-feed-reader”分支。与本文相关的bug修复将在此分支上进行。
  4. 启动HOPE后,加载名为“NewFeedReaderTabbed”的小程序
  5. 各种显示表单可能会消失在HOPE应用程序主窗口后面——移动/调整主窗口大小以将其移开。此外,表单最初会相互重叠显示。根据需要排列它们,然后保存小程序——主窗口和显示表单的大小和位置会持久保存。
  6. 如果您对其他API感兴趣,例如C++、Android、Java、Ruby等,请访问该页面。

引言

显而易见,"云端"存在大量信息,并且每毫秒都在增长。其中一些是相当静态的,比如维基百科页面、新闻文章或博客,而另一些则非常动态,比如股票行情、天气和推文。再显而易见的是,从可用性角度来看,集成化地处理这些信息,使得呈现给用户的只是对用户有意义的内容的手段根本不存在,或者如果存在,也仅限于“Google,请根据这些类别筛选我的新闻项目”。但如果,例如,我想在有人撰写关于Visual Studio 14(或您阅读本文时处于CTP状态的任何版本VS)的博客时收到提醒,那么,祝您好运。

我们可以看看语义网

通过鼓励在网页中包含语义内容,语义网旨在将当前由非结构化和半结构化文档主导的网页转换为“数据之网”。 (http://en.wikipedia.org/wiki/Semantic_Web)

但是,这项运动的普及速度慢得令人沮丧,而且可能无法提供足够的语义信息来使其真正有用。

这让我们转向了“自然语言处理”,简称NLP

“使计算机能够从人类或自然语言输入中提取意义” (http://en.wikipedia.org/wiki/Natural_language_processing)。

使用NLP,我们可以提取内容的实际语义。本文探讨的是将一个NLP服务(AlchemyAPI)与网页抓取(AlchemyAPI的一个功能)集成,以提取并持久化所述语义。给定一组基本功能,一旦从内容中提取出语义,就可以进一步开发许多其他功能(例如跟踪/报告趋势)。这些附加功能可能会在未来的文章中探讨。本文将具体介绍以下内容:

  1. 使用SyndicationFeed类获取订阅源项目
  2. 使用AlchemyAPI的NLP提取语义
  3. 持久化订阅项链接及其关联含义
  4. 提供一个简单的用户界面,用于探索订阅项及其相关的语义

RSS是一种特定的利基工具,对于非订阅源内容,需要能够使用其他工具。我不想开发一个与RSS紧密绑定的庞大应用程序,因此我将展示一种D-Tier方法(分布式、多维、动态)来构建此应用程序,并选择高级编程环境(HOPE)作为平台。这可以用于包含其他内容获取方式,使用其他NLP处理器(如OpenCalaisSemantria),以及开发用于以其他独特有趣方式处理语义的组件。如果您不熟悉我之前关于HOPE的文章,请阅读介绍性文章。因此,我将把关于HOPE开发的讨论与本文的主要主题交织在一起。

炼金API

AlchemyAPI 是众多 NLP 之一。出于我的特定目的,它们之所以吸引人,原因如下:

  • 快速响应——在我查看过的服务中,它们的响应时间最快
  • 免费选项——NLP提供商可能很昂贵!虽然OpenCalais是免费的,但AlchemyAPI提供更丰富的分析,并且每天免费提供1000次事务。
  • 内置网页抓取工具——我当然不想自己编写一个,所以这个功能至关重要。AlchemyAPI的网页抓取工具看起来相当不错。其他一些服务要么与昂贵的选项捆绑。OpenCalais与SemanticProxy相关联,但演示存在问题(内存不足),我还没有尝试过编程接口。
  • 无痛API——AlchemyAPI提供的.NET API使用起来非常简单,并且XML格式可以直接解析为.NET DataSet 对象。在我对Semantria和OpenCalais的评估中,情况绝非如此——我在.NET OpenCalais API中遇到了错误,Semantria API的复杂性令人沮丧,尽管Semantria在指导我解决问题方面提供了很大帮助。我将在另一篇文章中发布对这三个NLP服务的完整评论。

基于上述标准,选择相当明确。

为什么选择高级编程环境?

我为什么要用HOPE框架编写这篇文章?有几个原因:

  • 我想继续推广和扩展这个框架的功能
  • 我想避免一个庞大的应用程序。NLP可以应用于RSS订阅源以外的许多事物,我想要一个允许我即插即用,并且真正“玩转”不同配置、NLP提供商等来提取语义的平台。HOPE正是为此类乐高式构建而设计的。
  • NLP结果的可视化是一个未知的领域。虽然我只使用枯燥的数据表列表,但在NLP结果方面,有一个丰富的可视化领域值得探索。再次强调,HOPE是一个出色的框架,可以插入不同的可视化工具并与之交互。
  • 在我看来,编写同步、单线程的单体应用程序是死胡同,而HOPE代表了一种非常有趣的替代方案,用于创建分布式、动态和多维应用程序,促进非确定性UI和行为:由**用户**而不是**开发人员**决定小程序行为和可视化。
  • 它很有趣,而且很简单。

仍然感兴趣?那么让我们从订阅阅读器开始,接着是可视化工具,然后是使用NLP解析订阅内容。

订阅阅读器接收器

(一个接收器,带有准备好处理的订阅项。)

在HOPE中,行为由自主接收器编写。我们可以从一个非常简单的接收器开始,它加载获取订阅项并发出它们。

RSSFeedItem 语义结构

我们需要定义一个feed item的协议,这在XML中完成。

<SemanticTypeStruct DeclType="RSSFeedItem">
  <Attributes>
    <NativeType Name="FeedName" ImplementingType="string"/>
    <NativeType Name="Title" ImplementingType="string"/>
    <SemanticElement Name="URL"/> <!-- the link -->
    <NativeType Name="Description" ImplementingType="string"/>
    <NativeType Name="Authors" ImplementingType="string"/>
    <NativeType Name="Categories" ImplementingType="string"/>
    <NativeType Name="PubDate" ImplementingType="DateTime"/>
  </Attributes>
</SemanticTypeStruct>

如果您是HOPE的新手,其中一个基本概念是所有数据本身都是语义的,这在第一次尝试中有利有弊,并且总是一个有趣的决定点:类型应该总是语义元素还是可以是原生类型?我将把这个问题留待另一个讨论。

接收器实现

这里有三点值得注意:

  • 有一个配置UI,用户可以指定订阅源名称和URL。
  • 请注意用户可配置的属性如何使用 `UserConfigurableProperty` 属性进行修饰,以便序列化程序知道在保存/加载小程序时要持久化什么。
  • 订阅源是异步加载的,当任务完成时,订阅源项目会被发出。
public class FeedReader : BaseReceptor
{
  public override string Name { get { return "Feed Reader"; } }
  public override bool IsEdgeReceptor { get { return true; } }
  public override string ConfigurationUI { get { return "FeedReaderConfig.xml"; } }

  [UserConfigurableProperty("Feed URL:")]
  public string FeedUrl { get; set; }

  [UserConfigurableProperty("Feed Name:")]
  public string FeedName {get;set;}

  protected SyndicationFeed feed;

  public FeedReader(IReceptorSystem rsys)
    : base(rsys)
  {
    AddEmitProtocol("RSSFeedItem");
  }

  /// <summary>
  /// If specified, immmediately acquire the feed and start emitting feed items.
  /// </summary>
  public override void EndSystemInit()
  {
    base.EndSystemInit();
    AcquireFeed();
  }

  /// <summary>
  /// When the user configuration fields have been updated, re-acquire the feed.
  /// </summary>
  public override void UserConfigurationUpdated()
  {
    base.UserConfigurationUpdated();
    AcquireFeed();
  }

  /// <summary>
  /// Acquire the feed and emit the feed items. 
  /// </summary>
  protected async void AcquireFeed()
  {
    if (!String.IsNullOrEmpty(FeedUrl))
    {
      try
      {
        SyndicationFeed feed = await GetFeedAsync(FeedUrl);
        EmitFeedItems(feed);
      }
      catch (Exception ex)
      {
        EmitException("Feed Reader Receptor", ex);
      }
    }
  }


  /// <summary>
  /// Acquire the feed asynchronously.
  /// </summary>
  protected async Task<SyndicationFeed> GetFeedAsync(string feedUrl)
  {
    SyndicationFeed feed = await Task.Run(() =>
    {
      XmlReader xr = XmlReader.Create(feedUrl);
      SyndicationFeed sfeed = SyndicationFeed.Load(xr);
      xr.Close();

      return sfeed;
    });

    return feed;
  }

  /// <summary>
  /// Emits only new feed items for display.
  /// </summary>
  protected void EmitFeedItems(SyndicationFeed feed)
  {
    feed.Items.ForEach(item =>
    {
      CreateCarrier("RSSFeedItem", signal =>
        {
          signal.FeedName = FeedName;
          signal.Title = item.Title.Text;
          signal.URL.Value = item.Links[0].Uri.ToString();
          signal.Description = item.Summary.Text;
          signal.Authors = String.Join(", ", item.Authors.Select(a => a.Name).ToArray());
          signal.Categories = String.Join(", ", item.Categories.Select(c => c.Name).ToArray());
          signal.PubDate = item.PublishDate.LocalDateTime;
        });
    });
  }
}

订阅源阅读器用户配置

一个非常简单的用户界面用于配置订阅源(请注意,此配置在保存HOPE小程序时会持久化)。由于用户界面是在XML中定义的,因此可以轻松地自定义以适应其他外观——这种可定制性是HOPE的独特优势。使用的解析器是我大约10年前编写的MycroXaml的派生版本。

这里的关键点是控件属性与接收器实例属性的显式绑定。

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:r="Clifton.Receptor, Clifton.Receptor"
  xmlns:def="def"
  xmlns:ref="ref">
  <wf:Form Text="Feed Reader Configuration" Size="480, 190" StartPosition="CenterScreen" ShowInTaskbar="false" MinimizeBox="false" MaximizeBox="false">
    <wf:Controls>
      <wf:Label Text="Feed Name:" Location="20, 23" Size="70, 15"/>
      <wf:TextBox def:Name="tbFeedName" Location="92, 20" Size="150, 20"/>
      <wf:Label Text="Feed URL:" Location="20, 48" Size="70, 15"/>
      <wf:TextBox def:Name="tbFeedUrl" Location="92, 45" Size="250, 20"/>
      <wf:CheckBox def:Name="ckEnabled" Text="Enabled?" Location="20, 120" Size="80, 25"/>
      <wf:Button Text="Save" Location="360, 10" Size="80, 25" Click="OnReceptorConfigOK"/>
      <wf:Button Text="Cancel" Location="360, 40" Size="80, 25" Click="OnReceptorConfigCancel"/>
    </wf:Controls>
    <r:PropertyControlMap def:Name="ControlMap">
      <r:Entries>
        <r:PropertyControlEntry PropertyName="FeedUrl" ControlName="tbFeedUrl" ControlPropertyName="Text"/>
        <r:PropertyControlEntry PropertyName="FeedName" ControlName="tbFeedName" ControlPropertyName="Text"/>
      </r:Entries>
    </r:PropertyControlMap>
  </wf:Form>
</MycroXaml>

受体与载体

异步函数返回后,我们注意到有几个载体(每个订阅源中列出的项都有一个)等待处理。我们可以将鼠标悬停在其中一个载体(黄色三角形)上检查它们的信号,这会在属性网格中显示信号。

订阅项查看器

接下来,我们需要一种方式来查看订阅源。我将不编写特定的订阅源阅读器查看器,而是实现一个通用的“载体查看器”,它将在`DataGridView`控件中显示载体信号。作为一个通用的接收器,它也将对其他应用程序有用。我们唯一需要配置的是查看器应该监听的协议(语义结构)。

配置订阅项查看器

与订阅阅读器一样,我们有一个小的XML文件(未显示),允许我们指定要监视的协议。在我们的例子中,它是“RSSFeedItem”。

代码

代码再次非常简单,只是增加了如果用户更改协议,则删除旧协议的功能。

public class CarrierListViewer : BaseReceptor
{
  public override string Name { get { return "Carrier List Viewer"; } }
  public override bool IsEdgeReceptor { get { return true; } }
  public override string ConfigurationUI { get { return "CarrierListViewerConfig.xml"; } }

  [UserConfigurableProperty("Protocol Name:")]
  public string ProtocolName { get; set; }

  protected string oldProtocol;
  protected DataView dvSignals;
  protected DataGridView dgvSignals;
  protected Form form;

  public CarrierListViewer(IReceptorSystem rsys)
    : base(rsys)
  {
  }

  public override void Initialize()
  {
    base.Initialize();
    InitializeUI();
  }

  public override void EndSystemInit()
  {
    base.EndSystemInit();
    CreateViewerTable();
    ListenForProtocol();
  }

  /// <summary>
  /// Instantiate the UI.
  /// </summary>
  protected void InitializeUI()
  {
    // Setup the UI:
    MycroParser mp = new MycroParser();
    form = mp.Load<Form>("CarrierListViewer.xml", this);
    dgvSignals = (DataGridView)mp.ObjectCollection["dgvRecords"];
    form.Show();
  }

  /// <summary>
  /// When the user configuration fields have been updated, reset the protocol we are listening for.
  /// </summary>
  public override void UserConfigurationUpdated()
  {
    base.UserConfigurationUpdated();
    CreateViewerTable();
    ListenForProtocol();
  }

  /// <summary>
  /// Create the table and column definitions for the protocol.
  /// </summary>
  protected void CreateViewerTable()
  {
    if (!String.IsNullOrEmpty(ProtocolName))
    {
      DataTable dt = new DataTable();
      ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(ProtocolName);
      st.AllTypes.ForEach(t =>
      {
        dt.Columns.Add(new DataColumn(t.Name));
      });

    dvSignals = new DataView(dt);
    dgvSignals.DataSource = dvSignals;
    }
  }

  /// <summary>
  /// Remove the old protocol (if it exists) and start listening to the new.
  /// </summary>
  protected void ListenForProtocol()
  {
    if (!String.IsNullOrEmpty(oldProtocol))
    {
      RemoveReceiveProtocol(oldProtocol);
    }

    oldProtocol = ProtocolName;
    AddReceiveProtocol(ProtocolName, (Action<dynamic>)((signal) => ShowSignal(signal)));
  }

  /// <summary>
  /// Add a record to the existing view showing the signal's content.
  /// </summary>
  /// <param name="signal"></param>
  protected void ShowSignal(dynamic signal)
  {
    try
    {
      DataTable dt = dvSignals.Table;
      DataRow row = dt.NewRow();
      ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(ProtocolName);

      st.AllTypes.ForEach(t =>
        {
          object val = t.GetValue(rsys.SemanticTypeSystem, signal);
          row[t.Name] = val;
        });

      dt.Rows.Add(row);
    }
    catch (Exception ex)
    {
      EmitException("Carrier List Viewer Receptor", ex);
    }
  }
}

显示订阅项

我们现在可以将载体列表查看器拖放到表面上,双击它以配置协议,我们立即注意到它现在已作为订阅源阅读器接收器发出的内容的接收器连接起来。

一个小的XML文件声明了UI(同样,可以轻松配置为其他呈现或第三方控件)

<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:def="def"
  xmlns:ref="ref">
  <wf:Form Text="List Viewer" Size="500, 300" StartPosition="CenterScreen" ShowInTaskbar="false" MinimizeBox="false" MaximizeBox="false">
    <wf:Controls>
      <wf:DataGridView def:Name="dgvRecords" Dock="Fill"
        AllowUserToAddRows="false"
        AllowUserToDeleteRows="false"
        ReadOnly="true"
        SelectionMode="FullRowSelect"
        RowHeadersVisible="False"/>
    </wf:Controls>
  </wf:Form>
</MycroXaml>

以下是Code Project文章订阅源的一个结果

配置订阅源阅读器(膜的介绍)

让我们在这里暂停一下,看看现在我们可以用HOPE做些什么。例如,我们可以创建多个订阅源阅读器,所有阅读器都输入到一个列表查看器中。

以下是一个示例列表

但假设您只想要一个用于Code Project的列表。我们可以使用HOPE的新功能“膜”来做到这一点。虽然我暂时不打算详细介绍膜,但您可以在膜计算中了解其概念。这个概念的概述是:载体(协议及其信号)包含在膜中,并且只有在膜被配置为对该协议可渗透时才能穿透膜(进入或移出)。因此,我们可以使用膜来创建“计算孤岛”:

导致单独的订阅项列表

处理语义类型

我们可以为查看器添加的另一项功能是,当用户双击一行时,能够发出语义类型。请记住,当我们定义RSSFeedItem语义类型时,URL本身就是一种语义类型。

<SemanticElement Name="URL"/>

我们可以查找所有语义类型属性并发出它们,让其他接收器对它们进行处理。我们检查查看器监听的协议中是否存在语义元素,并将它们添加到发射器列表中。

// Add other semantic type emitters:
RemoveEmitProtocols();
ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(ProtocolName);
st.SemanticElements.ForEach(se => AddEmitProtocol(se.Name));

然后,当我们双击时,接收器会遍历它所代表的协议的语义元素,并发出信号为该语义元素值的载体

/// <summary>
/// Emit a semantic protocol with the value in the selected row and the column determined by the semantic element name.
/// </summary>
protected void OnCellContentDoubleClick(object sender, DataGridViewCellEventArgs e)
{
  ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(ProtocolName);

  st.SemanticElements.ForEach(se =>
  {
     CreateCarrier(se.Name, signal => se.SetValue(rsys.SemanticTypeSystem, signal, dvSignals[e.RowIndex][se.Name].ToString()));
  });
}

APOD网页抓取器文章中,我创建了一个简单的接收器,它监听语义类型“URL”并使用该URL启动浏览器,因此我们可以在这里重新使用该接收器。

请注意,我们只需要一个URL接收器。每个膜都对URL协议可渗透。

这允许URL协议渗透出膜,从而将载体列表查看器(在运行时将自身配置为发出URL协议)连接到URL接收器。现在我们有两个独立的订阅项列表,并且可以通过双击任一列表中的项来在浏览器中访问订阅项。

协议语义子元素

HOPE的一个新功能是能够在其父载体的语义元素上创建载体。例如,因为协议RSSFeedItem包含语义元素“URL”,所以当发出“RSSFeedItem”信号时,也会为语义元素“URL”创建第二个载体。当启用HOPE的此行为时,您可以在当前订阅阅读器小程序中立即看到效果。

请注意从订阅阅读器接收器直接到URL接收器的额外路径。此功能是实验性的,但绝对有用且探索载体协议信号的行为非常有趣。事实上,按照上述配置实现,这具有在浏览器中打开每个订阅项页面的有趣效果。然而,这不是我们想要的,所以我们将围绕订阅阅读器创建一个子膜,以防止URL渗透膜并被URL接收器接收。

对于围绕 Feed Reader Receptor 的每个膜,我们将其配置为只允许 RSSFeedItem 协议渗透膜。

这为我们提供了所需的行为——只有载体列表查看器发出的“URL”协议才会被URL接收器接收。

将自然语言处理应用于订阅项

为协议中的语义元素创建载体的功能可以被NLP利用,我们确实希望处理每个订阅项的URL。如前所述,我正在使用AlchemyAPI作为NLP服务。请注意,我将右侧的两个订阅阅读器组合成一个子膜,以及Alchemy接收器如何与订阅阅读器接收器关联,因为Alchemy接收器正在监听“URL”协议。

请注意,由于我们将订阅阅读器配置为两个独立的“系统”,因此不可能只有一个Alchemy Receptor——这将需要允许URL协议渗透订阅阅读器膜,这将使我们回到前面描述的问题。然而,这真的是一个问题吗?事实上,不一定,特别是如果您考虑分布式系统以及利用异步行为的优势。此外,如果多个实例确实是一个问题,那么HOPE框架在某个时候可能会允许您指定逻辑接收器,这将支持底层实现中的单个实例(或更多)。

炼金术API接收器代码

Alchemy API 在其或多或少的默认配置中提供了来自 NLP 的三个结果:实体、关键字和概念,每个都有独特的属性,如下图所示,该截图来自比较三种 NLP 服务的文章。

主要要点

  • AlchemyAPI 允许我们直接传入 URL,因为它内置了内容抓取工具。这为我们省去了大量工作,无需自行提取内容(一项艰巨的任务)或使用第三方服务。
  • 要获取实体、关键词和概念,我们需要进行三次单独的调用。请注意我是如何将返回条目的限制(默认值为 50)增加到最大值 250 的。
  • 请注意,我有一个“TEST”编译器条件,因为我不想在测试整个小程序时访问AlchemyAPI,也不想等待AlchemyAPI返回数据所需的4或5秒。测试数据集是预先获取并序列化的。
  • AlchemyAPI 返回一个格式非常漂亮的 XML 文档,可以直接读取到 .NET DataSet 中。我忽略了该 DataSet 中的一些信息,您可能希望进行探索。

这是Alchemy Receptor的完整代码

public class Alchemy : BaseReceptor
{
  public override string Name { get { return "Alchemy"; } }
  public override bool IsEdgeReceptor { get { return true; } }

  protected AlchemyAPI.AlchemyAPI alchemyObj;

  public Alchemy(IReceptorSystem rsys)
    : base(rsys)
  {
    AddEmitProtocol("AlchemyEntity");
    AddEmitProtocol("AlchemyKeyword");
    AddEmitProtocol("AlchemyConcept");

    AddReceiveProtocol("URL",
      // cast is required to resolve Func vs. Action in parameter list.
      (Action<dynamic>)(signal => ParseUrl(signal)));
  }

  public override void Initialize()
  {
    base.Initialize();
    InitializeAlchemy();
  }

  protected void InitializeAlchemy()
  {
    alchemyObj = new AlchemyAPI.AlchemyAPI();
    alchemyObj.LoadAPIKey("alchemyapikey.txt");
  }

  /// <summary>
  /// Calls the AlchemyAPI to parse the URL. The results are 
  /// emitted to an NLP Viewer receptor and to the database for
  /// later querying.
  /// </summary>
  /// <param name="signal"></param>
  protected async void ParseUrl(dynamic signal)
  {
    string url = signal.Value;

    DataSet dsEntities = await Task.Run(() => { return GetEntities(url); });
    DataSet dsKeywords = await Task.Run(() => { return GetKeywords(url); });
    DataSet dsConcepts = await Task.Run(() => { return GetConcepts(url); });

    dsEntities.Tables["entity"].IfNotNull(t => Emit("AlchemyEntity", t));
    dsKeywords.Tables["keyword"].IfNotNull(t => Emit("AlchemyKeyword", t));
    dsConcepts.Tables["concept"].IfNotNull(t => Emit("AlchemyConcept", t));
  }

  protected void Emit(string protocol, DataTable data)
  {
    data.ForEach(row =>
      {
        CreateCarrierIfReceiver(protocol, signal =>
          {
            // Use the protocol as the driver of the fields we want to emit.
            ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(protocol);
            st.AllTypes.ForEach(se =>
              {
                object val = row[se.Name];

                if (val != null && val != DBNull.Value)
                {
                  se.SetValue(rsys.SemanticTypeSystem, signal, val);
                }
              });
          });
      });
  }

  protected DataSet GetEntities(string url)
  {
    DataSet dsEntities = new DataSet();
#if TEST
    // Using previously captured dataset
    dsEntities.ReadXml("alchemyEntityTestResponse.xml");
#else
    try
    {
      AlchemyAPI_EntityParams eparams = new AlchemyAPI_EntityParams();
      eparams.setMaxRetrieve(250);
      string xml = alchemyObj.URLGetRankedNamedEntities(url, eparams);
      TextReader tr = new StringReader(xml);
      XmlReader xr = XmlReader.Create(tr);
      dsEntities.ReadXml(xr);
      xr.Close();
      tr.Close();
    }
    catch(Exception ex)
    {
      EmitException("Alchemy Receptor", ex);
    }
#endif
    return dsEntities;
  }

  protected DataSet GetKeywords(string url)
  {
    DataSet dsKeywords = new DataSet();

#if TEST
    // Using previously captured dataset
    dsKeywords.ReadXml("alchemyKeywordsTestResponse.xml");
#else
    try
    {
      AlchemyAPI_KeywordParams eparams = new AlchemyAPI_KeywordParams();
      eparams.setMaxRetrieve(250);
      string xml = alchemyObj.URLGetRankedKeywords(url);
      TextReader tr = new StringReader(xml);
      XmlReader xr = XmlReader.Create(tr);
      dsKeywords.ReadXml(xr);
      xr.Close();
      tr.Close();
    }
    catch(Exception ex)
    {
      EmitException("Alchemy Receptor", ex);
    }
#endif
    return dsKeywords;
  }

  protected DataSet GetConcepts(string url)
  {
    DataSet dsConcepts = new DataSet();

#if TEST
    // Using previously captured dataset
    dsConcepts.ReadXml("alchemyConceptsTestResponse.xml");
#else
    try
    {
      AlchemyAPI_ConceptParams eparams = new AlchemyAPI_ConceptParams();
      eparams.setMaxRetrieve(250);
      string xml = alchemyObj.URLGetRankedConcepts(url);
      TextReader tr = new StringReader(xml);
      XmlReader xr = XmlReader.Create(tr);
      dsConcepts.ReadXml(xr);
      xr.Close();
      tr.Close();
    }
    catch(Exception ex)
    {
      EmitException("Alchemy Receptor", ex);
    }
#endif
    return dsConcepts;
  }
}

为了显示结果,我们将放置载体列表查看器接收器,列出所有订阅源的NLP结果

为此,我们需要允许 AlchemyEntity、AlchemyKeyword 和 AlchemyConcept 协议渗透这些膜。

当我们对围绕Alchemy Receptor的两个膜都这样做时,可视化工具会向我们显示Alchemy receptor正在发出Carrier List Viewer Receptor感兴趣的协议。截屏底部每个Carrier List Viewer receptor都已配置为接收相应的协议。

当然,我们不一定需要看到所有三种类型(实体、关键词、概念)——这完全取决于您希望如何配置小程序。您会注意到上面我使用了三个独立的列表查看器,每个类别一个。稍后我将使用选项卡式列表查看器来管理所有这些信息。

炼金API

本节专门讨论AlchemyAPI服务。本文并未涵盖AlchemyAPI提供的所有内容——仅涉及最常见的功能。具体来说,“情感”和“关系”未作介绍,但您可以在AlchemyAPI网站上阅读更多相关信息。

给定一个文档或URL,您可以将语义含义提取为三个类别:实体、关键词和概念。

实体

AlchemyAPI返回每个实体的以下信息

text: 这是实体名称(或更具体地说,是名词)

类型:AlchemyAPI试图确定实体类型,其中包括城市、公司、大陆、国家、犯罪、学位、设施、领域术语、地理特征、假日、职称、人物、操作系统、组织、平面媒体、产品、区域、运动、州或县以及技术等标签。完整列表可在此处找到。

count:这是实体出现的次数。这个计数(在我审查过的所有NLP中都很常见)利用了一个称为“照应解析”的共指功能:“在句子‘莎莉来了,但没人看见她’中,代词‘’是照应的,指代‘莎莉’。”(摘自维基百科)

relevance: 关联度分数介于0.0到1.0之间,其中1.0表示最相关。根据AlchemyAPI的API布道师Steve Herschleb的说法:"每个关键词的关联度分数衡量了每个提取关键词的总体重要性。实际计算分数涉及一些相当复杂的统计学,但算法包括词语在文本中的位置、周围的其他词语、使用次数等。" (来自 Quora 网站的来源)

关键词

关键词由关键词文本和相关性组成。“关键词是您内容中的重要主题,可用于索引数据、生成标签云或进行搜索。AlchemyAPI的关键词提取API能够查找文本中的关键词并对其进行排名。然后可以确定每个提取关键词的情感。”来源)请注意,我在此小程序中未演示情感——执行情感分析是一个单独的调用,算作一次“事务”。

概念

概念是AlchemyAPI的一个有趣功能:"

“AlchemyAPI 采用复杂的文本分析技术对文档进行概念标记,方式类似于人类识别概念。概念标记API能够通过理解概念之间的关系进行高层次的抽象,并且可以识别文本中不一定直接引用的概念。例如,如果一篇文章提到 CERN 和希格斯玻色子,它会将大型强子对撞机标记为一个概念,即使该术语没有在页面中明确提及。通过使用概念标记,您可以对内容进行比基本关键词识别更高层次的分析。” (来源)

AlchemyAPI概念的一个有趣之处在于其数据链接。您可以在此处阅读更多关于链接数据的资料。从上面的截图中,您可以看到来自DBpediaFreebaseopencyc的三个链接数据结果。根据内容,AlchemyAPI还会链接到其他几个知识库。

AlchemyAPI 异常

AlchemyAPI的异常处理相当糟糕——它实际上没有报告服务器产生的错误,而这些错误肯定是结果XML的一部分。

一个简单的修改提供了更有意义的结果(在AlchemyAPI.cs中,从第955行开始)

// OLD:
/*
if (status.InnerText != "OK")
{
  System.ApplicationException ex = new System.ApplicationException ("Error making API call.");

  throw ex;
}*/

// MTC 7/14/2014
// Much better, as it gives me the error message from the server.
if (status.InnerText != "OK")
{
  string errorMessage = "Error making API call.";

  try
  {
    XmlNode statusInfo = root.SelectSingleNode("/results/statusInfo");
    errorMessage = statusInfo.InnerText;
  }
  catch
  {
    // some problem with the statusInfo. Return the generic message.
  }

  System.ApplicationException ex = new System.ApplicationException(errorMessage);

  throw ex;
}

令人高兴的是,这个修复将很快被整合到AlchemyAPI提供的API中。

缓存内容

理想情况下,我们不希望重复抓取相同的页面,因此目前(因为我不想在本文中添加整个持久化部分),我添加了一个简单的缓存机制,以避免超出每日1000次事务的限制。

/// <summary>
/// Return true if cached and populate the refenced DataSet parameter.
/// </summary>
protected bool Cached(string prefix, string url, ref DataSet ds)
{
  string urlHash = url.GetHashCode().ToString();
  string fn = prefix + "-" + urlHash + ".xml";

  bool cached = File.Exists(fn);

  if (cached)
  {
    ds.ReadXml(fn);
  }

  return cached;
}

/// <summary>
/// Cache the dataset.
/// </summary>
protected void Cache(string prefix, string url, DataSet ds)
{
  string urlHash = url.GetHashCode().ToString();
  string fn = prefix + "-" + urlHash + ".xml";
  ds.WriteXml(fn);
}

这只是一个临时措施,真正的数据持久化到数据库将在第二部分中介绍。

内容限制大小

您可能还会收到“内容超出大小限制”的错误。一旦我知道确切的限制,我将更新本文。

检索限制

AlchemyAPI 检索的实体、关键词和概念的默认数量是 50。您可以将此限制增加到最大 250,就像我在 Alchemy Receptor 中所做的那样,例如对于实体:

AlchemyAPI_EntityParams eparams = new AlchemyAPI_EntityParams();
eparams.setMaxRetrieve(250);
string xml = alchemyObj.URLGetRankedNamedEntities(url, eparams);

这是一个重要的参数,需要进行实验,因为我不确定增加这个限制有多大用处。例如,在处理维基百科上关于计算机科学的这个页面时,AlchemyAPI 提取了 147 个实体。这与没有默认限制的 OpenCalais(155 个实体)相当。相比之下,Semantria 默认提取 5 个实体,最大检索量为 50 个。

更多与受体相关的内容

为了实现本文的主要目标——根据NLP结果筛选订阅源,我们需要添加一些额外的行为,其中第一个就是简单的选项卡式列表查看器接收器,它将使所有这些列表的管理变得更容易。

选项卡式列表查看器接收器

我不会展示代码(它与上面的Carrier List Viewer Receptor非常相似),而是只介绍配置和使用方法。

配置

将选项卡式列表查看器接收器拖放到表面后,我们双击它并配置所需的选项卡及其列出的协议。敏锐的读者可能会意识到,这对于RSSFeedItem协议不起作用——无法区分来自不同RSS订阅源的订阅项。这只能通过限定信号数据来实现,在这种情况下使用订阅源名称。此功能目前尚未实现,因为它需要以通用方式完成。

接线

一旦协议被定义,我们就可以看到它是如何连接的。

结果

NLP结果现在以选项卡式列表形式显示,而不是离散的列表形式。

将URL与NLP结果关联

NLP结果本身并没有多大用处。我们需要将URL与每个结果关联起来,我们可以通过将语义元素添加到Alchemy协议中来实现。

<SemanticElement Name="URL"/>

当然,还要将该属性分配给Alchemy Receptor发出的每个结果记录。

signal.URL.Value = url;  // .Value because this is a semantic element and Value drills into the implementing native type.

立即注意现在发生了什么

因为Alchemy协议现在包含语义元素“URL”,所以列表查看器接收器和URL接收器现在自动神奇地连接起来(嗯,它是在几行代码中实现的,如上图所示在单个列表查看器中),这样,当用户双击选项卡式查看器中的条目时,它会发出所有已知的语义元素,其中“URL”是其中之一(目前是唯一的)。再次,敏锐的读者会说,“但是Linked Data内容中的URL呢,比如DBpedia?”这是一个非常好的问题,本文没有解决。

顺便提一句,HOPE架构的美妙之处体现在上述行为中:系统的能力同样(如果不是更多的话)由协议的语义定义——你的语义越丰富,就可以创建出更多有趣的、与这些语义协同工作的行为。

过滤接收器

我们终于来到了问题的关键——根据NLP结果过滤订阅源。为了使其稍微复杂一些,我将使用NCalc表达式评估器,这样我们就可以做一些有趣的事情,例如不仅通过关键词,而且通过相关性阈值来过滤实体或概念。我们将尽可能通用地完成此操作。首先,过滤后的协议将按原样发出,但必须使用不同的语义协议来避免未过滤和过滤结果之间的歧义。在某种程度上,这可以被视为HOPE架构中的一个潜在缺陷,但这个问题在发布/订阅系统中很常见,而发布/订阅系统是HOPE的一个方面。我们将在未来的某个时候研究这个问题。

使用 NCalc 非常简单。Filter Receptor 的这段代码片段演示了如何在 NCalc 中设置“变量”并创建自定义函数“contains”。

protected void FilterSignal(string protocol, dynamic signal, List<string> filters)
{
  filters.ForEach(filter =>
  {
    try
    {
      Expression exp = new Expression(filter);

      // Assign the types in the semantic structure as variables.
      ISemanticTypeStruct st = rsys.SemanticTypeSystem.GetSemanticTypeStruct(protocol);

      st.AllTypes.ForEach(t =>
      {
        exp.Parameters[t.Name] = t.GetValue(rsys.SemanticTypeSystem, signal);
      });

      // Allow parsing of additional functions.
      exp.EvaluateFunction += OnEvaluateFunction;

      object result = exp.Evaluate();

      if (result is bool)
      {
        if ((bool)result)
        {
          // Copy the input signal to the Filtered[protocol] signal for emission.
          CreateCarrier("Filtered" + protocol, outSignal =>
          {
            st.AllTypes.ForEach(t =>
            {
              t.SetValue(rsys.SemanticTypeSystem, outSignal, t.GetValue(rsys.SemanticTypeSystem, signal));
            });
          });
        }
      }
    }
    catch (Exception ex)
    {
      EmitException(ex.Message + " with filter " + filter);
    }
  });
}

protected void OnEvaluateFunction(string name, FunctionArgs args)
{
  if (name.ToLower() == "contains")
  {
    string v1 = args.Parameters[0].Evaluate().ToString().ToLower();
    string v2 = args.Parameters[1].Evaluate().ToString().ToLower();

    args.Result = v1.Contains(v2);
  }
}

配置

上面的截图展示了过滤协议的示例配置。当然,可以添加更多针对相同协议(或其他协议)的过滤器。

我们以选项卡式列表视图显示过滤后的列表,配置如下:

接线

膜再次被用于确保协议以受控方式接收和发出

我们现在可以查看未过滤和过滤后的订阅项(请注意,我更改了上面截图中的过滤条件)。

结论

自然语言处理是一种解析“大数据”的独特方式,提供适用于潜在复杂机器处理的语义含义,从而生成专门为人类量身定制的信息。然而,如此崇高的目标只能通过开发将这些信息处理成真正“有意义”的算法来实现。我在这里演示的只是一个非常初步的过程,比关键词过滤好不了多少,但希望它能激发人们使用这些服务进一步发展这些想法!

虽然我将NLP的讨论与高级编程环境框架纠缠在一起,但我个人希望这也能激励其他人开发超越简单列表的处理接收器和可视化。

此演示仍有更多工作要做,这将是第二部分的重点:将NLP数据持久化到数据库,查询,并提高可用性,例如显示订阅项是新的还是已读。

语义网和自然语言处理 - CodeProject - 代码之家
© . All rights reserved.