【问题标题】:Data structure for parse tree in natural language C/C++自然语言 C/C++ 解析树的数据结构
【发布时间】:2014-01-22 13:52:24
【问题描述】:

我想在 C/C++ 的数据结构中存储句子。例如这句"This uploads files to a remote machine."表示为:

(TOP
  (S
    (NP (DT This))
    (VP
      (VBZ uploads)
      (NP (NNS files))
      (PP (TO to) (NP (DT a) (JJ remote) (NN machine))))
    (. .)))

喜欢here 有没有一种简单的方法可以在 C/C++ 中做到这一点?我正在手动构建树(不使用解析器)。

【问题讨论】:

    标签: c++ c boost data-structures tree


    【解决方案1】:

    http://opennlp.apache.org/ 中提到的解析器非常复杂。它将一个句子分成名词、动词、介词等。如果你试图用 c/c++ 重写它,这是一项艰巨的任务。

    最好使用解析器并将输出读入 c/c++ 数据结构。

    假设你有解析器的输出,那么输出的格式就相当简单了。结构是这样的:

    struct SentencePart {
      SType type;
      // If the type is a basic word type (e.g. NN, JJ, etc)
      char* word;      
      // If the type is a complex sub-sentence.
      struct SentencePart* sentence_part;
    };
    

    您可以创建类型的枚举(TOP、S、VP、NP 等)。然后您可以读取输入并根据您扫描的类型创建结构。

    这是一种非常简单的方法,可能还有其他方法。

    【讨论】:

    • 我没有尝试重写解析器。谢谢。
    【解决方案2】:

    扩展 Trenin 的回答,我将使用目录样式树,其中兄弟是坐标部分,子部分是从属部分:

    typedef struct Token Token;
    
    struct Token {
        const char *type;   /* Type of token, cold be an enum */
        const char *data;   /* associated word */
        Token *next;        /* next coordinate token */
        Token *child;       /* eldest subordinate token */
    };
    

    然后您可以设计一种基于级别的方法将您的标记插入到该树中:

    root = token_new_level(0, "TOP", NULL);
    
    token_new_level(1,          "S", NULL);
    token_new_level(  2,        "NP", NULL);
    token_new_level(    3,      "DT", "this");
    token_new_level(  2,        "VP", NULL);
    token_new_level(    3,      "VPZ", "uploads");
    token_new_level(    3,      "NP", NULL);
    token_new_level(      4,    "NNS", "files");
    token_new_level(    3,      "PP", NULL);
    token_new_level(      4,    "TO", "to");
    token_new_level(      4,    "NP", NULL);
    token_new_level(        5,  "DT", "a");
    token_new_level(        5,  "JJ", "remote");
    token_new_level(        5,  "NN", "machine");
    token_new_level(  2,        ".", ".");
    

    产生:

    OP
        S
            NP
                DT this
            VP
                VPZ uploads
                NP
                    NNS files
                PP
                    TO to
                    NP
                        DT a
                        JJ remote
                        NN machine
            . .
    

    作为树或平面表示:

     (TOP (S (NP (DT this)) (VP (VPZ uploads) (NP (NNS files)) 
          (PP (TO to) (NP (DT a) (JJ remote) (NN machine)))) (. .)))
    

    名词短语NP和动词短语VP通过next并列连接。名词短语NP和动词短语VP从属于句子S,但只有NP存储为S的直接child

    只有没有子代的标记才会附加单词,因此您可以使用 C 中的联合或 C++ 中的两个不同类,例如 PhraseWord,它们都继承自 Token 来优化模型。

    【讨论】:

    • 感谢您的回复
    【解决方案3】:

    您基本上是在使用 S 表达式。 编辑 显然,我错过了部分问题。但是,以下技术很容易扩展到其他种类的树。

    我喜欢使用递归 Boost 变体来处理这些问题:

    using s_expr = boost::make_recursive_variant<std::string, std::vector<boost::recursive_variant_> >::type;
    using s_list = std::vector<s_expr>;
    

    当然,部分原因可能是因为我使用 Boost Spirit 简单地解析了这些 AST。所以,这是我的演示程序,展示了它是如何使用的。

    Live on Coliru

    测试程序展示了如何解析您展示的示例以及如何在代码中构造等效的 AST。请注意,断言证明两者都导致完全相同的表达式树:

    int main()
    {
        s_expr parsed = parse_s_expr(
                "(TOP\n"
                "  (S\n"
                "    (NP (DT This))\n"
                "    (VP\n"
                "      (VBZ uploads)\n"
                "      (NP (NNS files))\n"
                "      (PP (TO to) (NP (DT a) (JJ remote) (NN machine))))\n"
                "    (. .)"
                ")"
                ")");
    
        std::cout << "parsed: " << parsed           << "\n";
    
        // conversely, just build one:
        const s_expr in_code(s_list { 
            "TOP",
            s_list { "S",
                s_list { "NP", s_list { "DT", "This", } },
                s_list { "VP",
                    s_list { "VBZ", "uploads" },
                        s_list { "NP", s_list { "NNS", "files" } },
                        s_list { "PP", s_list { "TO", "to" }, s_list { "NP", s_list { "DT", "a" }, s_list { "JJ", "remote" }, s_list { "NN", "machine" } } } },
                    s_list { ".", "." }
            }
        });
    
        // both AST trees are exactly equivalent:
        assert(in_code == parsed);
    }
    

    输出(如 the coliru link 所示)是:

    parsed: ( TOP ( S ( NP ( DT This ) ) ( VP ( VBZ uploads ) ( NP ( NNS files ) ) ( PP ( TO to ) ( NP ( DT a ) ( JJ remote ) ( NN machine ) ) ) ) ( . . ) ) )

    这是完整的程序。请注意,实现解析器占用了全部 35 行代码 :) 并且非常灵活和高效,感谢 Spirit)

    完整的演示程序

    #define BOOST_SPIRIT_DEBUG
    #include <boost/spirit/include/qi.hpp>
    #include <boost/variant.hpp>
    #include <stdexcept>
    
    namespace qi    = boost::spirit::qi;
    namespace phx   = boost::phoenix;
    
    using s_expr = boost::make_recursive_variant<std::string, std::vector<boost::recursive_variant_> >::type;
    using s_list = std::vector<s_expr>;
    
    template <typename It, typename Skipper = qi::space_type>
        struct parser : qi::grammar<It, s_expr(), Skipper>
    {
        parser() : parser::base_type(expr)
        {
            using namespace qi;
    
            value = lexeme [ +(graph - '(' - ')') ];
            list  = '(' >> *expr >> ')';
            expr  = list | value;
    
            BOOST_SPIRIT_DEBUG_NODES((expr)(value)(list));
        }
    
      private:
        qi::rule<It, s_expr(),      Skipper> expr;
        qi::rule<It, std::string(), Skipper> value;
        qi::rule<It, s_list(),      Skipper> list;
    };
    
    s_expr parse_s_expr(const std::string& input)
    {
        typedef std::string::const_iterator It;
    
        static const parser<It, qi::space_type> p;
    
        It f(begin(input)), l(end(input));
        s_expr data;
    
        if (!qi::phrase_parse(f,l,p,qi::space,data))
            throw std::runtime_error("parse failed: '" + std::string(f,l) + "'");
    
        return data;
    }
    
    namespace std { // a hack for easy debug printing
        static inline std::ostream& operator<<(std::ostream& os, s_list const& l) {
            os << "( "; std::copy(l.begin(), l.end(), std::ostream_iterator<s_expr>(os, " "));
            return os << ")";
        }
    }
    
    int main()
    {
        s_expr parsed = parse_s_expr(
                "(TOP\n"
                "  (S\n"
                "    (NP (DT This))\n"
                "    (VP\n"
                "      (VBZ uploads)\n"
                "      (NP (NNS files))\n"
                "      (PP (TO to) (NP (DT a) (JJ remote) (NN machine))))\n"
                "    (. .)"
                ")"
                ")");
    
        std::cout << "parsed: " << parsed           << "\n";
    
        // conversely, just build one:
        const s_expr in_code(s_list { 
            "TOP",
            s_list { "S",
                s_list { "NP", s_list { "DT", "This", } },
                s_list { "VP",
                    s_list { "VBZ", "uploads" },
                        s_list { "NP", s_list { "NNS", "files" } },
                        s_list { "PP", s_list { "TO", "to" }, s_list { "NP", s_list { "DT", "a" }, s_list { "JJ", "remote" }, s_list { "NN", "machine" } } } },
                    s_list { ".", "." }
            }
        });
    
        // both AST trees are exactly equivalent:
        assert(in_code == parsed);
    }
    

    【讨论】:

    • 在重新阅读问题时,我似乎错过了关于 SNPVP 等节点类型的相当大的一点。我希望你仍然觉得我的回答很有启发性
    • 感谢您的回复
    猜你喜欢
    • 2011-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-12
    • 2018-06-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多