站点图标 AI技术聚合

基于知识图谱的智能问答

基于知识图谱的智能问答

  • 1.问答系统的简单介绍
    • 1.1 问答系统的目标
    • 1.2问答系统框架
  • 2. 项目介绍
    • 2.1数据集介绍
    • 2.2 问题分类
    • 2.3 技术方案
      • 2.3.1 数据准备
      • 2.3.2 数据导入neo4j
  • 3 模型
    • 3.1 JointBERT(分类、实体识别)
      • 3.1.1 数据集构造
      • 3.1.2 模型
    • 3.2 GraphSAGE(图网络完成属性预测)
      • 3.2.1 数据集构造
      • 3.2.3 模型
  • 4. KBQA
  • 5. 问题与总结

1.问答系统的简单介绍

1.1 问答系统的目标

  1. 将数据导入到neo4j数据库
  2. 完成图神经网络并嵌入到问答系统
  3. 给定一个自然语言的问题

    完成三元组的提取
    能回答任意问题的智能系统

    收益小于9968614的公司有哪些
    王青多大
    沈阳惠天热电股份有限公司有可能失信吗

1.2问答系统框架

问答系统

  • 1- 问题分析模块
    问题分析模块主要负责对用户的问题进行处理,如 王青多大
    问题分类:分析出这个问题是由实体查询属性
    ⽣成查询关键词(提问关键词,扩展关键词,…)
    确定提问答案类型(PER, LOC, ORG, TIM, NUM, …)
    确定提问的句法、命名实体、语义表示等等。
  • 2- 问题回答模块(信息检索)
    总的⽬标:如何根据问题的分析结果去缩⼩答案可能存在的范围。这个过程包含
    2.1.信息抽取
    ⽅法描述:从问句中提取关键词语,⽤信息检索的⽅法找出包含候选答案的段落或句⼦,然后基于问答
    类型⽤信息抽取的⽅法从备选答案集中提取出最佳答案。
    检索过程:段落或者句⼦级排序,利⽤不同类型关键词的加权组合
    答案抽取过程:根据问答类型从排序后的段落或句⼦中抽取答案
    2.2.模式匹配
    基本思想:对于某些提问类型(某⼈的出⽣⽇期、原名、别称等),问句和包含答案的句⼦之间存在⼀
    定的答案模式,该⽅法在信息检索的基础上根据这种模式找出答案。因此如何⾃动获取某些类型提问的
    尽可能多的答案模式是其中的关键技术。
    举个例⼦:
    问题⽂本:1968年的今天,加州斯坦福研究所的道格·恩格勒巴特发明了世界上第⼀只⿏标。最初恩格
    勒巴特给⿏标 起的名字叫“显示位置纵横显示器”最初的⿏标不过是⼀个⽊盒拖着⼀根绳⼦。但⼀个“⿏
    标”(Mouse)的⼩名却⻛靡天下。就是这⽆名⿏辈,竟让世界发⽣了天翻地覆的变化。……
    如下给出了⼀个发明家(INVENTOR)型问题的的答案模式及其答案匹配过程:
    问题:⿏标是谁发明的?
    先由问题解析问题类型< Question Type>和问题词< Q_tag>,这个⽅法略过。
    问题类型< Question Type>:发明家(INVENTOR)
    问题词< Q_tag>:⿏标
    我们要求的是答案< A>,然后根据候选句的评分,进⾏排序,返回前n句中匹配到的答案项。
    1.0 < A> 发明 < Q_tag>
    1.0 < Q_tag> 被 < A> 发明
    0.83 < A> 的 发明 < Q_tag>
    0.79 < A> < Q_tag> 的 发明者
    0.50 < A> (< Q_tag>)
    ……
    匹配到的模式:1.0 < A> 发明 < Q_tag>
    匹配到的答案:道格·恩格勒巴特(如上⽂本⿊体加粗⽂字)
  • 2.3- 问答
    给定⾃然语⾔问题,通过对问题进⾏语义理解和解析,利⽤知识库进⾏查询、推理得出答案。 其特点
    是,回答的答案是知识库中的实体。 KBQA问答是我们本项⽬的重点,具体的下个章节论述。
  • 3- 答案⽣成模块
    总的⽬标:如何从可能存在答案的信息块中抽取答案。
    ⼀般搜索引擎返回的是⼀堆⽹⻚,⽽问答系统需要返回的是简短的答案。这样,通过信息检索模块
    搜索出来的相关⽂档就要提交给答案抽取模块来提炼答案。答案可以是⼀句话,或者是⼏句话,也
    可以是⼏个词或者短语。对于那些问时间地点的问题,就可以⽤很短的语句来回答,⽽对于询问原
    因、事件的问题就需要较⻓的语句才能回答。⽐如对于问题“9.11事件的是怎么回事?”就不可能⽤
    ⼀句话就能回答的。所以答案的抽取还需要依据问题的类型。
    最后是⽤户⾏为反馈,就是怎么样根据⽤户的结果去指导我们去做更好对话模型的理解。

2. 项目介绍

2.1数据集介绍

数据中包含公司的主键、名称、分红方式、所处行业、债券类型等,也包含公司主要职位的人物名称,还有公司与公司之间的关系。

经过分析之后,我们建立的图谱中内容如下

  • 实体:公司、人物、行业、分红方式、违规类型、债券类型。
  • 公司的属性:名称、收益。
  • 人物的属性:名称、年龄。
  • 其他实体的属性只有:名称。
  • 公司与公司之间的关系有:供应商、客户、公担保。
  • 人物与公司之间的关系有:监事、董事。
  • 公司与行业的关系:属于。
  • 公司与分红方式的关系:属于。
  • 其他类似。
  • 将属性公司的所处行业属性按照关系来处理,便于之后进行查询。

2.2 问题分类

KBQA问题可以分为事实类问题、是非类问题、对比类问题、原因方法类问题等。我们这里只回答事实类问题。

  • 事实类问题又分为:查询实体、查询属性、查询关系。
  • 查询实体:收益大于1000的公司有哪些;王青多大
  • 查询属性:沈阳惠天热电股份有限公司的分红方式什么
  • 查询关系:沈阳惠天热电股份有限公司的供应商; 沈阳惠天热电股份有限公司的供应商的供应商
  • 引入图神经网络完成属性预测:沈阳惠天热电股份有限公司有可能失信吗

其中查询关系又分为一跳和多跳。

项目目标1:能够识别这4类问题,给出正确答案。
项目目标2:引入召回、排序功能,彻底解决名称不匹配的可能性
项目目标3:引入生成式聊天机器人

2.3 技术方案

  • 1 输入问句
  • 2 通过联合模型JointBERT完成问句分类,实体识别,属性识别
  • 3 如果需要预测,则调用图神经网络完成属性预测
  • 4 输出答案

我们已知关系有:董事、理事、违规类型、供应商、客户等9种。

对于一跳关系查询:秦皇岛兴龙房地产集团有限公司的董事,这个处理和查询属性类似:使用AC自动机匹配得到关系,使用模板:match (s:company)-[p:{p}]->(o) where s.name=’{subject}’ return o.name 查询。

对于多跳关系查询:秦皇岛兴龙房地产集团有限公司的供应商的分红方式。这就要求先找到 ”秦皇岛兴龙房地产集团有限公司的供应商“查询得到答案: 重庆广建装饰股份有限公司。然后将问题替换为”重庆广建装饰股份有限公司的分红方式“。对于该问题要先分类,然后再按照不同类型问题的模板去查询。当前对于这个问题应该属于第三类查询关系,并且是一跳关系。按照模板处理。

2.3.1 数据准备


原始数据存在excel中,是以属性的方式存储的。将其拆分为公司、人物、行业等excel。

2.3.2 数据导入neo4j

#######################
#  导入节点
######################
def import_company():
    # 组件-公司名-有无失信行为
    df = pd.read_csv('company_data/公司.csv')
    # 提取组件和公司名
    eid = df['eid'].values
    name = df['companyname'].values

    nodes = []
    data = list(zip(eid, name))
    for eid, name in tqdm(data):
        # 加入了额外属性, 收入profit
        # 这个例子中是随机数
        profit = np.random.randint(100000, 100000000, 1)[0]
        # 创建节点, 并加入属性,如再加入公司人数
        # node = Node('company', name=name, profit=int(profit), eid=eid, people_num=1000)
        node = Node('company', name=name, profit=int(profit), eid=eid)
        nodes.append(node)

    graph.create(Subgraph(nodes))

3 模型

3.1 JointBERT(分类、实体识别)

采用JointBERT的思想:
使用[CLS]完成意图识别、使用last_hidden_state完成实体识别、使用[CLS]+[SEP]完成属性识别

3.1.1 数据集构造

创建意图识别(4类)、实体识别(BIO标记法)、属性识别(多分类)的数据集

问题BIO意图属性
收益小于5684719的公司有哪些O O O O O O O O O B-LOC I-LOC O O O0profit
景明琪多大B-LOC I-LOC I-LOC O O1age
诸城外贸有限责任公司的债券类型是啥B-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC O O O O O O O2other
中铁高新工业股份有限公司有可能失信吗B-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC I-LOC O O O O O O3other

3.1.2 模型

class JointBERT(BertPreTrainedModel):
    def __init__(self, config, args, intent_label_lst, slot_label_lst, attribute_label_lst):
        super(JointBERT, self).__init__(config)
        self.args = args
        self.num_intent_labels = len(intent_label_lst)
        self.num_slot_labels = len(slot_label_lst)
        self.num_attribute_labels = len(attribute_label_lst)
        self.bert = BertModel(config=config)     # Load pretrained bert

        self.intent_classifier = IntentClassifier(config.hidden_size, self.num_intent_labels, args.dropout_rate)
        self.slot_classifier = SlotClassifier(config.hidden_size, self.num_slot_labels, args.dropout_rate)
        self.attribute_classifier = ObjectClassifier(config.hidden_size, self.num_attribute_labels, args.dropout_rate)

        if args.use_crf:
            self.crf = CRF(num_tags=self.num_slot_labels, batch_first=True)

    def forward(self, input_ids, attention_mask, token_type_ids, intent_label_ids, attribute_label_ids=None, slot_labels_ids=None):
        outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)  # sequence_output, pooled_output, (hidden_states), (attentions)
        # last_hidden_state: [batch_size, seq_len, hidden_size]
        sequence_output = outputs[0]   # [last_hidden_state]
        # pooler_output: [batch_size, hidden_size]
        pooled_output = outputs[1]     # [CLS]

        slot_logits = self.slot_classifier(sequence_output)    # [batch_size, seq_len, hidden_size] --> [batch_size, seq_len, num_slot]
        intent_logits = self.intent_classifier(pooled_output)  # [batch_size, hidden_size] --> [batch_size, num_intent]

        # total_loss = intent_loss + self.args.slot_loss_coef * slot_loss
        total_loss = 0
        # 1. Intent Softmax
        if intent_label_ids is not None:
            if self.num_intent_labels == 1:
                intent_loss_fct = nn.MSELoss()
                intent_loss = intent_loss_fct(intent_logits.view(-1), intent_label_ids.view(-1))
            else:
                intent_loss_fct = nn.CrossEntropyLoss()
                intent_loss = intent_loss_fct(intent_logits.view(-1, self.num_intent_labels), intent_label_ids.view(-1))
            total_loss += intent_loss

        # 2. Slot Softmax
        if slot_labels_ids is not None:
            if self.args.use_crf:
                slot_loss = self.crf(slot_logits, slot_labels_ids, mask=attention_mask.byte(), reduction='mean')
                slot_loss = -1 * slot_loss  # negative log-likelihood
            else:
                slot_loss_fct = nn.CrossEntropyLoss(ignore_index=self.args.ignore_index)
                # Only keep active parts of the loss
                if attention_mask is not None:
                    active_loss = attention_mask.view(-1) == 1
                    active_logits = slot_logits.view(-1, self.num_slot_labels)[active_loss]
                    active_labels = slot_labels_ids.view(-1)[active_loss]
                    slot_loss = slot_loss_fct(active_logits, active_labels)
                else:
                    slot_loss = slot_loss_fct(slot_logits.view(-1, self.num_slot_labels), slot_labels_ids.view(-1))
            total_loss += self.args.slot_loss_coef * slot_loss

        # 3. Object
        # 为了导入subject 的信息,先做一次预测,求取出 subject token  ---> word_embedding  ---> plus to sequence_output
        if attribute_label_ids is not None:
            # slot_preds_ids: [batch_size, seq_len, num_slot_labels]
            slot_preds_ids = slot_logits.detach().cpu().numpy()
            slot_preds_ids = np.argmax(slot_preds_ids, axis=2)
            # 将slot_preds_ids 转换为 subject_preds_ids
            subject_preds_ids = slots_ids_2_subject_ids(slot_preds_ids)
            # subject_input_index = [[input_ids[batch][start:end+1] for start, end in line] for batch, line in enumerate(subject_preds_ids)]

        attribute_logits = self.attribute_classifier(sequence_output)

        # compute Object loss
        if attribute_label_ids is not None:
            if self.num_attribute_labels == 1:
                attribute_loss_fct = nn.MSELoss()
                attribute_loss = attribute_loss_fct(attribute_logits.view(-1), attribute_label_ids.view(-1))
            else:
                attribute_loss_fct = nn.CrossEntropyLoss()
                attribute_loss = attribute_loss_fct(attribute_logits.view(-1, self.num_attribute_labels), attribute_label_ids.view(-1))
            total_loss += attribute_loss


        outputs = ((intent_logits, slot_logits, attribute_logits),) + outputs[2:]  # add hidden states and attention if they are here

        outputs = (total_loss,) + outputs

        # outputs: (total_loss), ( (last_hidden_state, pooler_output), (outputs[2:]) )
        return outputs  # (loss), logits, (hidden_states), (attentions) # Logits is a tuple of intent and slot logits

3.2 GraphSAGE(图网络完成属性预测)

仅可预测公司实体是否可能存在失信

3.2.1 数据集构造

eidcompanynameindustryassignviolationsbonddishonesty_y
06fc37e2-8bf5-4e36-adc2-387f6a96a4ad江苏联赢激光有限公司不分配信息披露虚假或严重误导性陈述1
003ffe3f-d012-40b6-9cd6-ba94cf235ae2重庆江骏房地产开发有限公司寿险公司业绩预测结果不准确或不及时企业债0

3.2.3 模型


GraphSAGE 是Graph SAmple and aggreGatE的缩写,其运行流程如上图所示,可以分为三个步骤

对图中每个顶点邻居顶点进行采样
根据聚合函数从聚合邻居顶点蕴含的信息
得到图中各顶点的向量表示供下游任务

class GraphSAGE_MODEL(torch.nn.Module):
    def __init__(self, args, in_channels=31, hidden_channels=64):
        super(GraphSAGE_MODEL, self).__init__()
        # in_channels   # 初始特征的维度
        # out_channels  # 分类
        self.args = args
        self.out_channels = args.num_classes

        self.conv1 = SAGEConv(in_channels, hidden_channels)
        self.conv2 = SAGEConv(hidden_channels, hidden_channels)
        self.conv3 = SAGEConv(hidden_channels, hidden_channels)
        self.lin = torch.nn.Linear(3 * hidden_channels, self.out_channels)

    # 每层的聚合方式
    def set_aggr(self, aggr):
        self.conv1.aggr = aggr
        self.conv2.aggr = aggr
        self.conv3.aggr = aggr

    def forward(self, data):
        x0, edge_index, edge_weight = data.x, data.edge_index, data.edge_attr
        x1 = F.relu(self.conv1(x0, edge_index, edge_weight))
        x1 = F.dropout(x1, p=0.2, training=self.training)
        x2 = F.relu(self.conv2(x1, edge_index, edge_weight))
        x2 = F.dropout(x2, p=0.2, training=self.training)
        x3 = F.relu(self.conv3(x2, edge_index, edge_weight))
        x3 = F.dropout(x3, p=0.2, training=self.training)
        x = torch.cat([x1, x2, x3], dim=-1)
        x = self.lin(x)
        return x

4. KBQA

主要包含内容:

  • 调用训练好的ner_bert 模型(文本分类、实体提取、属性名提取),完成文本分类
    • 属性查实体
              # 属性 查 实体
              op, num = get_op(text)
              cypher = f'match (n:{subject_type}) where n.{attribute}{op}{num} return n.name'
              print(cypher)
              res = graph.run(cypher).to_ndarray()
      
    • 实体查属性
              # 实体 查 属性
              cypher = f'''match (n:{subject_type}) where n.name='{subject}' return n.{attribute}'''
              print(cypher)
              res = graph.run(cypher).to_ndarray()
      
    • 实体、关系查询实体(多跳问答)
              # 实体、关系 查询实体
              # 实体 subject
              # 关系抽取  这里 可用 spo 方法, 同时抽取 ner 和 relation
              predicate = []
              for end_index, original_value in ac_relation.iter(text):
                  start_index = end_index - len(original_value) + 1
                  print('关系:', (start_index, end_index, original_value))
                  assert text[start_index:start_index + len(original_value)] == original_value
                  predicate.append(original_value)
              for i, p in enumerate(predicate):
                  cypher = f'''match (s:company)-[p:`{p}`]->(o) where s.name='{subject}' return o.name'''
                  print(cypher)
                  res = graph.run(cypher).to_ndarray()
                  object = res[0][0]
                  if i == len(predicate) - 1:
                      break
                  new_index = text.index(p) + len(p)
                  new_question = object + str(text[new_index:])
                  print('new question:', new_question)
                  res = kbqa(new_question)
                  break
      
    • 预测是否可能失信
      • 调用图神经网络
                # 预测是否可能失信
                assert subject_type == "company", "Subject not a company if you want to pre_dishonesty"
                from utils import pre_dishonesty
                res = pre_dishonesty(args, subject)
        

5. 问题与总结

1、仅限于提问已经存在与知识库的问题
1、提取的实体(slot_pred –> subject)来自输入语句txt, 有可能导致和数据库中实体名称不能匹配,如公司名字应该说全名
2、三任务联合训练在简单数据集(如本项目,以公司为主题的数据集)上能达到很好的效果,这是因为数据集简单,提问简单,但是如果是复杂数据集,其可能存在其他的问题
3、GNN模块存在较大的问题
  a) 数据集的制作存在问题,OneHot编码的方式简单的使用了pd.get_dummies,考虑能否往embedding反向转变
  b) 准确率有待提高

结果

text:收益小于9968614的公司有哪些
question type: 0
match (n:company) where n.profit<9968614.0 return n.name
[[‘广东欧昊集团有限公司’]
[‘浙江天工建设集团有限公司’]
…]

text:绍兴仁昌酱园有限公司的收入
question type: 1
match (n:company) where n.name=‘绍兴仁昌酱园有限公司’ return n.profit
[[16399117]]

text:沈阳惠天热电股份有限公司的分红方式什么
question type: 2
match (s:company)-[p:分红方式]->(o) where s.name=‘沈阳惠天热电股份有限公司’ return o.name
[[‘现金分红’]]

text:沈阳惠天热电股份有限公司有可能失信吗
question type: 3
the node: 沈阳惠天热电股份有限公司, –> 0

展望
1、引入召回、排序功能,彻底解决名称不匹配的可能性
2、引入生成式聊天机器人

文章出处登录后可见!

已经登录?立即刷新
退出移动版