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

使用自然语言处理自动化文档的语义映射

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2014年8月13日

CPOL

10分钟阅读

viewsIcon

30508

利用AlchemyAPI,我们创建了关键词和句子关系的视觉化图表,以便用户能够快速高效地提取有意义的概念。

源代码和程序运行

本文的源代码托管在GitHub上: https://github.com/cliftonm/nlpvisualizer

要运行该程序,您需要从AlchemyAPI注册页面获取API密钥。免费账户每天允许您执行1000次查询。您可以将密钥直接放入源代码中,或者像我一样,在bin\debug文件夹中创建一个名为“alchemyapikey.txt”的文件,并将您的密钥复制到该文件的第一行。

使用程序

基本操作

  1. 在URL文本框中输入URL,然后点击“处理”。
  2. 一旦显示了关键词,您就可以点击关键词列表来显示包含该关键词的句子,并更新该关键词的选定可视化图表。
  3. 如果存在多个句子,请双击RichTextBox中的一个句子,以将可视化图表的范围缩小到该单一句子。
  4. 使用“上一句”和“下一句”按钮在句子之间导航。

可视化

  1. 右键单击并拖动以移动整个可视化表面。
  2. 使用鼠标滚轮进行缩放(由于鼠标滚轮事件与窗体上获得焦点的控件之间的荒谬关系,这有点麻烦)。此功能仅在“关键词导向图”可视化中可用。或者(如果您没有鼠标滚轮),左键单击并向上/向右拖动或向下/向左拖动可分别进行放大/缩小。
  3. 在“相邻句子关键词”可视化中,双击一个关键词以选择该关键词,以便在文本和可视化图中导航。
  4. 在“关键词导向图”可视化中,双击一个节点(蓝色圆圈)以选择该关键词,以便在文本和可视化图中导航。

引言

自然语言处理(NLP)旨在使计算机能够从人类或自然语言输入中提取意义。在我审查三种NLP的文章中,我们看到这些服务提取实体、关键词、主题、事件、主题和概念。除主题和概念外,结果本质上是关键词或短语。“字符串”通常与相关性或强度、计数或频率以及/或情感值相关联。我使用了其中一个NLP提供商AlchemyAPI的功能,在另一篇文章中,提供了对RSS feed的一些过滤功能,使用户能够基于提取的字符串和附加值创建过滤器。

概念中的意义

尽管如此,我还是对结果感到不满意。我的第一个问题是概念提取。在分析一篇关于“三分社会秩序与教育改革”1的简短出版物时,AlchemyAPI的“概念”提取是非常高级的。

  • 社会学
  • 教育
  • 灵魂
  • 人生的意义
  • 宗教
  • 人类
  • 学校
  • 生活

Semantria的“主题”也是如此。

  • 经济组织
  • 社会有机体
  • 人性
  • 经济体系
  • 资产阶级世界观

然而,鉴于文档中的这句话

“相反,社会有机体的精神文化器官应该遵循其自身独立管理的指示,将那些具有适当天赋的人培养到一定的水平,而国家和经济生活应该根据精神文化领域的劳动成果来组织自己。”

我之前审查过的任何NLP都没有确定这句话与“精英治国”2的概念有关。

关系中的意义

我的第二个担忧是,意义与关键词或概念之间的关系密切相关。本文讨论了从单个文档内的关键词中提取关系意义的两种方法,创建一种语义思维导图或概念图。这两种方法使用了两种不同类型的可视化图表——一种是简单的“相邻句子中的关键词”可视化,另一种是句子中选定关键词和相关关键词出现时它们之间关系的力导向图3(FDG)。GDF的读取方式将在稍后详细解释。FDG代码最初由Bradley Smith在他的2010年7月2日博客文章4中编写和发布——我对该代码进行了一些小的修改,以改进处理并渲染文本节点。

可视化

有两种可视化:相邻句子关键词和关键词关联。在这些示例中,我使用了关于美国国父的维基百科页面5

相邻句子关键词可视化

在包含关键词“国家事务”的句子中

上一句包含关键词“受过良好教育的人”,下一句包含关键词“美国革命”和“大陆军”。

这个可视化实际上很有用。虽然不应解释为具有任何因果关系,但可以解释为具有概念关系。以上面的关键词关系为例,这三句话一起是

  1. 他们几乎都是富裕的、在社区中有领导地位的受过良好教育的人。
  2. 许多人也在国家事务中很突出。
  3. 几乎每个人都参加了美国革命;至少有29人曾在陆军服役,其中大多数担任指挥职务。

一个人可以快速确定这里的概念是“这些人(在这种情况下,是费城制宪会议的代表,由检查前面的句子确定)受过良好教育,在国家事务中很有声望,并且几乎都参加了美国革命或在大陆军服役,而且大多数人都担任过指挥职务。”当处理复杂的文档时,关键词的邻近性可以帮助您快速从周围文本中创建一个概念,这个概念可能在文本的整体复杂性中被忽略了。

还请注意,双击可视化中的关键词会显示包含该关键词的所有句子,并更新可视化。例如,当双击“受过良好教育的人”时,程序会显示

关键词导向图

第二个可视化是关键词关联的导向图。为了解释这一点,让我们从一个简单的例子开始,使用句子“许多人也在国家事务中很突出”。

这张图显示,这个句子只有一个关键词,那就是“国家事务”。由于这个关键词没有出现在其他句子中,所以没有进一步的链接。

现在让我们看看文本前面一点的这句话。

“作为浸信会的避难所,罗德岛在会议上的缺席在一定程度上解释了出席会议的人中没有浸信会成员。”

通过点击关键词“浸信会成员”也可以找到这句话。

这里有一个更复杂的图。从“作为避难所……”这句话开始,我们看到它有两个关键词。

  1. 浸信会成员
  2. 罗德岛

“浸信会成员”未包含在任何其他句子中,因此没有子节点。然而,“罗德岛”包含在一个或多个句子中,具有另外两个关键词。

  1. 代表
  2. 公约代表

关键词“代表”出现在一个或多个包含关键词的句子中。

  1. 美国宣言
  2. 制宪会议
  3. 大群体
  4. 国父
  5. United States

这些图会变得复杂,正如起始文本所示。

美利坚合众国的国父是政治领袖和政治家,他们通过签署《美利坚合众国独立宣言》、参加美国革命战争和建立《美利坚合众国宪法》来参与美国革命。

有两个常量,目前在UI中未暴露,它们限制了导向图的深度和广度。

const int FDG_DEPTH_LIMIT = 3;
const int FDG_NODE_KEYWORD_LIMIT = 5;

关键词关联导向图是一种非常有趣的方式,用于绘制句子中出现的概念之间的关系。人们可以快速发现基于关键词如何相互关联来调查概念的附加路径,我发现这有助于构建一个文本讨论内容的更广阔图景。因此,例如,虽然相邻关键词通常保持在一个紧密的思维过程中,但关键词关联图允许人们探索围绕中心主题的更松散关联的概念。

双击可视化中的关键词节点(蓝色圆圈)会显示包含该关键词的所有句子,并更新可视化。

相关性加权

关键词字体大小反映了关键词的相关性(由AlchemyAPI确定)。因此,例如,由于关键词“美国”具有最高的相关性(0.92971),它以大字体显示。相关性范围从0到1,通过将相关性(减去最小相关性)乘以16并将该值添加到基本字体大小8来调整字体。

font = new Font(
  FontFamily.GenericSansSerif, 
  (float)(8.0 + (Program.app.keywordRelevanceMap[keyword] - Program.app.minRelevance) * FONT_WEIGHT_MULTIPLIER));

代码

虽然代码本身并不复杂,但我将在此讨论基本流程。

文档分析

该程序从主窗体上输入的URL分析网页(而不是您自己输入的文档文本)。您可能会发现某些页面会显示“内容超出”的错误消息,因为AlchemyAPI处理的内容存在大小限制。

处理包含三个部分。

  1. 使用AlchemyAPI的URLGetText方法获取抓取的内容。
  2. 使用AlchemyAPI的TextGetRankedKeywords方法从该内容中获取关键词。
  3. 执行关键词-句子关系查找映射预处理。
/// <summary>
/// Analyze the document, extracting the text the keywords, and create the keyword-sentence maps.
/// </summary>
protected async void Process(object sender, EventArgs args)
{
  btnProcess.Enabled = false;
  ClearAllGrids();
  string url = tbUrl.Text;
  sbStatus.Text = "Acquiring page content...";
  try
  {
    pageText = await Task.Run(() => GetUrlText(url));
    pageSentences = ParseOutSentences(pageText);

    sbStatus.Text = "Acquiring keywords from AlchemyAPI...";
    dsKeywords = GetKeywords(url, pageText);

    sbStatus.Text = "Processing results...";
    dvKeywords = new DataView(dsKeywords.Tables["keyword"]);
    CreateSortedKeywordList(dvKeywords);
    CreateSentenceKeywordMaps(dvKeywords);
    CreateKeywordRelevanceMap(dvKeywords); // Must be done before assigning the data source.
    sbStatus.Text = "Ready";
    dgvKeywords.DataSource = dvKeywords;
    lblAlchemyKeywords.Text = String.Format("Keywords: {0}", dvKeywords.Count);
    btnProcess.Enabled = true;
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message, "Processing Error", MessageBoxButtons.OK);
  }
  finally
  {
    sbStatus.Text = "Ready";
    btnProcess.Enabled = true;
  }
}

创建了多个关键词、句子索引和相关性值之间的“映射”,以方便可视化选定的关键词。

/// <summary>
/// Create the sentence-keyword map (list of keywords in each sentence.)
/// Create the keyword-sentence map (list of sentence indices for each keyword.)
/// </summary>
/// <param name="dvKeywords"></param>
protected void CreateSentenceKeywordMaps(DataView dvKeywords)
{
  sentenceKeywordMap.Clear();
  keywordSentenceMap.Clear();

  // For each sentence, get all the keywords in that sentence.
  pageSentences.ForEachWithIndex((s, idx) =>
  {
    List<string> keywordsInSentence = new List<string>();
    sentenceKeywordMap[idx] = keywordsInSentence;
    string sl = s.ToLower();

    // For each of the returned keywords in the view...
    dvKeywords.ForEach(row =>
    {
      string keyword = row[0].ToString();

      if (sl.Contains(keyword.ToLower()))
      {
        // Add keyword to sentence-keyword map.
        keywordsInSentence.Add(keyword);

        // Add sentence to keyword-sentence map.
        List<int> sentences;
        
        if (!keywordSentenceMap.TryGetValue(keyword, out sentences))
        {
          // No entry for this keyword yet, so create the sentence indices list.
          sentences = new List<int>();
          keywordSentenceMap[keyword] = sentences;
        }

      sentences.AddIfUnique(idx);
      }
    });
  });
}

RichTextBox显示

当选择一个关键词时,将显示包含该关键词的句子,并高亮显示该关键词。

/// <summary>
/// When a keyword is selected from the grid or the visualizator, we update RTB
/// to display the sentences containing the keyword and also the keyword relationship visualization.
/// </summary>
public void ShowKeywordSelection(string keyword)
{
  textboxEventsEnabled = false;
  ShowSentences(keyword);
  textboxEventsEnabled = true;
  rtbSentences.SelectionStart = 0;
  surface.NewKeyword(keyword);
  UpdateKeywordVisualization();
}

这是通过解析句子中的选定关键词来完成的,并在遇到每个关键词的出现时构建RichTextBox中的文本。

/// <summary>
/// Build the text, checking for keyword occurrence and if found, colorizing the keyword.
/// </summary>
protected void ShowSentences(string keyword)
{
  rtbSentences.Clear();
  displayedSentenceIndices.Clear();

  pageSentences.ForEachWithIndex((sentence, sidx) =>
  {
    string s = sentence.ToLower();
    int idx = s.IndexOf(keyword.ToLower());
    bool found = idx >= 0;
    int start = 0;

    while (idx >= 0)
    {
      // Remember the index of this sentence, but we don't want duplicates.
      if (!displayedSentenceIndices.Contains(sidx))
      {
        displayedSentenceIndices.Add(sidx);
      }

      // Use master sentence to preserve casing.
      string substr = sentence.Substring(start, idx);
      rtbSentences.AppendText(substr);
      rtbSentences.AppendText(keyword, Color.Red);

      // Get remainder.
      s = s.Substring(idx + keyword.Length);
      start += idx + keyword.Length; // for master sentence.
      idx = s.IndexOf(keyword.ToLower());
    }

    if (found)
    {
      // Append the remainder.
      rtbSentences.AppendText(s);
      rtbSentences.AppendText("\n\n");
    }
  });
}

相邻句子关键词可视化

生成相邻句子关键词可视化图的代码首先绘制前一个关键词,然后绘制后一个关键词,最后绘制当前关键词,使当前关键词显示在连接线的上方。

protected void DrawNeighboringSentenceKeywords(Graphics gr)
{
  try
  {
    // Get location of keyword in the center of the
    Point ctr = new Point(Size.Width / 2, Size.Height / 2);

    keywordLocationMap.Clear();
    DrawPreviousKeywords(gr, ctr);
    DrawNextKeywords(gr, ctr);
    DrawKeyword(gr, keyword); // Last, so that text appears above lines.
  }
  catch (Exception ex)
  {
    System.Diagnostics.Debug.WriteLine(ex.Message);
  }
}

当用户点击关键词列表中的关键词或将包含该关键词的句子过滤到单个句子时,前一个和后一个关键词就会被预先确定。

protected void UpdateKeywordVisualization()
{
  List<SentenceInfo> prevKeywords = GetPreviousSentencesKeywords();
  List<SentenceInfo> nextKeywords = GetNextSentencesKeywords();
  surface.PreviousKeywords(prevKeywords);
  surface.NextKeywords(nextKeywords);

  if (directedGraph)
  {
    UpdateDirectedGraph();
  }

  surface.Invalidate(true);
}

最终,给定句子索引,这是一个简单的查找并处理到SentenceInfo实例列表的过程。

protected List<SentenceInfo> GetKeywordsInSentence(int idx)
{
  List<SentenceInfo> ret = new List<SentenceInfo>();
  sentenceKeywordMap[idx].ForEach(k => ret.Add(new SentenceInfo() 
      { Keyword = k, Index = idx, Relevance = keywordRelevanceMap[k] }));

  return ret;
}

如果选定的关键词未出现在当前句子中,则可视化图将以空的方括号“[ ]”在中心渲染。

关键词导向图

如前所述,这是根据关键词在句子中的关联出现情况进行的递归搜索。该算法的深度和广度由两个常量限制。

const int FDG_DEPTH_LIMIT = 3;
const int FDG_NODE_KEYWORD_LIMIT = 5;

此外,在遍历过程中会省略重复的关键词。该算法从当前句子中的关键词开始,并对每个关键词递归到包含该关键词的其他句子。在这些句子中,关联的关键词决定了下一级递归。

protected void UpdateDirectedGraph()
{
  mDiagram.Clear();
  parsedKeywords.Clear();

  string ctrSentence = FirstThreeWords(pageSentences[displayedSentenceIndices[0]]);
  Node node = new TextNode(surface, ctrSentence);
  ((TextNode)node).Brush = surface.greenBrush;
  mDiagram.AddNode(node);

  // Get the keywords of all sentences for the current sentence or sentences containing the selected keyword.
  List<SentenceInfo> keywords = GetSentencesKeywords();
  keywords = keywords.RemoveDuplicates((si1, si2) => si1.Keyword.ToLower() == si2.Keyword.ToLower()).ToList();
  parsedKeywords.AddRange(keywords.Select(si => si.Keyword.ToLower()));
  AddKeywordsToGraphNode(node, keywords, 0);
  mDiagram.Arrange();
}

protected void AddKeywordsToGraphNode(Node node, List<SentenceInfo> keywords, int depth)
{
  if (depth < FDG_DEPTH_LIMIT)
  {
    int idx = 0;

    foreach(SentenceInfo si in keywords)
    {
      // Limit # of keywords we display.
      if (idx++ < FDG_NODE_KEYWORD_LIMIT)
      {
        Node child = new TextNode(surface, si.Keyword);
        node.AddChild(child);

        // Get all sentences indices containing this keyword:
        List<int> containingSentences = keywordSentenceMap[si.Keyword];

        // Now get the related keywords for each of those sentences. 
        List<SentenceInfo> relatedKeywords = new List<SentenceInfo>();

        containingSentences.ForEach(cs =>
        {
          // Get the unique and previously not processed keywords in the sentence.
          List<SentenceInfo> si3 = GetKeywordsInSentence(cs).Where(sik => !parsedKeywords.Contains(sik.Keyword.ToLower())).ToList();
          si3 = si3.RemoveDuplicates((si1, si2) => si1.Keyword.ToLower() == si2.Keyword.ToLower()).ToList();
          relatedKeywords.AddRange(si3);
          parsedKeywords.AddRange(si3.Select(sik=>sik.Keyword.ToLower()));
        });

        if (relatedKeywords.Count > 0)
        {
          AddKeywordsToGraphNode(child, relatedKeywords, depth + 1);
        }
      }
      else
      {
        break;
      }
    }
  }
}

我推荐您阅读Brad Smith关于力导向图的出色博客4,以进一步了解生成图的算法。

深入探索

作为一个研究工具,创建文档之间的关系也是有用的。这需要建立一个文档和提取的关键词/概念的数据库,以便像这里介绍的程序可以关联文档之间的关键词/概念,使您能够在一个文档范围之外研究一个概念。我可能会在某个时候添加此功能!!

结论

在实际应用中,我发现这个程序是一个非常有效的工具,可以专注于文章或博客中的特定点。逐句导航文档本身也很有用,因为它有助于减少整个文档的混乱。相邻句子关键词可视化有助于探索同一“思想”内的相关关键词,从而促进快速构建主要概念。使用关键词关联导向图,主要概念可以扩展到包含其他外围概念。以这种方式处理文档是相当有趣和富有启发性的。

参考文献

1. http://wn.rsarchive.org/Books/GA024/English/AP1985/GA024_c04.htmll

2. http://en.wikipedia.org/wiki/Meritocracy<

3. http://en.wikipedia.org/wiki/Force-directed_graph_drawing

4. http://www.brad-smith.info/blog/archives/129

5. http://en.wikipedia.org/wiki/Founding_Fathers_of_the_United_States

© . All rights reserved.