开放域实体抽取泛用工具

https://github.com/magicdict/FDDC

更新时间 2018年7月16日 By 带着兔子去旅行

开发这个工具的起源是天池大数据竞赛,FDDC2018金融算法挑战赛02-A股上市公司公告信息抽取。这个比赛是针对金融公告开展的信息抽取比赛。在参赛过程中,萌生出一个念头,是否能够开发出一个泛用的信息抽取工具呢?

信息抽取是NLP里的一个实用内容。该工具的目标是打造一个泛用的自动信息抽取工具。使得没有任何基础的用户,可以通过简单的步骤提取文档(PDF,HTML,TXT)中的信息。该工具使用C#(.Net Core)开发,所以可以跨平台运行。(Python在做大的工程的时候有诸多不便,所以没有使用python语言)

工具原理采用的是开放域实体抽取的方法:

使用各种方法尽可能抽取实体,然后对于候选内容进行置信度分析打分。

基本环境

  • .NetCore2.1
  • LTP组件:哈工大LTP3.3.2版
  • PDF转TXT工具 pdfminer
  • 分词系统:结巴分词

ltp工具:哈工大LTP工具(ltp.ai)提供的ltp工具,最新版为3.3.4.该工具在windows,max,centos上,srl的训练可能无法正常完成。(dp,ner阶段没有问题)所以这里使用了3.3.2版本。ltp工具的SRL结果中包含了DP和NER的内容,但是暂时保留DP和NER中间XML文件。

pdfminer:请注意处理中文的时候需要额外的步骤,具体方法不再赘述。部分PDF可能无法正确转换,原因CaseByCase。

结巴分词:某些地名,例如"大连",会被误判。这里使用地名辅助字典的方式做纠正。ltp工具没有这个问题。ltp工具和结巴分词功能虽然重复,但是暂时还不能移除结巴分词。

前期准备

  • 使用pdfminer将PDF文件转化为Txt文件
  • 使用哈工大LTP工具,将Txt文件转换为NER,DP,SRL的XML文件

期待文件夹结构

  • html(存放HTML文件目录)
  • pdf(存放PDF文件目录)
  • txt(存放TXT文件目录)
  • dp(存放LTP的DP结果XML目录)
  • ner(存放LTP的NER结果XML目录)
  • srl(存放LTP的SRL结果XML目录)

训练(词语统计)

  • 分析待提取信息自身的特征
  • 分析待提取信息周围语境的特征(LTP工具)
  • 构建置信度体系

词语自身属性

  • 长度
  • 包含词数
  • 首词词性(POS)
  • 词尾

语境

  • 该关键字在 :(中文冒号)之后的场景下,:(中文冒号)前面的内容
  • 包含该关键字的句子中,该关键字的前置动词
  • 包含该关键字的句子中,该关键字是否在角色标识中存在

训练结果例:

协议书(5.180388%)[56]
协议(11.84089%)[128]
合同(58.55689%)[633]
合同书(2.960222%)[32]
买卖合同(3.792784%)[41]
承包合同(12.0259%)[130]
意向书(0.2775208%)[3]
补充协议(1.110083%)[12]
项目(0.2775208%)[3]
书(0.9250694%)[10]
议案(0.2775208%)[3]
)(0.8325624%)[9]

(更多规则持续加入中,同时对于相关度低的规则也会剔除)

这里暂时使用频率最高的前5位作为抽取依据。同时为了保证正确率,部分特征的占比必须超过某个阈值。

以下是中文冒号的一个例子,要求前导词占比在40%以上。

(例如前导词A可以正确抽取10个关键字,前导词B可以抽取5个关键字,前导词C可以抽取15个关键字。则前导词A的占比为33%)

        e.LeadingColonKeyWordList = ContractTraning.ContractNameLeadingDict
.Where((x) => { return x.Value >= 40; }) //阈值40%以上
.Select((x) => { return x.Key + ":"; }).ToArray();

表格

对于大量表格中的关键字,工具也提供了表格统计的功能。主要是统计一下该关键字的表头标题信息。

同时由于表格中的原始数据可能需要通过参照表格标题才能进行比对的情况,这里支持变换器。

    /// <summary>
/// 增发对象训练
/// </summary>
public static void TrainingIncreaseTarget()
{
var TargetTool = new TableAnlayzeTool();
var IncreaseNumberTool = new TableAnlayzeTool();
IncreaseNumberTool.Transform = NumberUtility.NormalizerStockNumber;
var IncreaseMoneyTool = new TableAnlayzeTool();
IncreaseMoneyTool.Transform = MoneyUtility.Format;
TraningDataset.InitIncreaseStock();
var PreviewId = String.Empty;
var PreviewRoot = new HTMLEngine.MyRootHtmlNode();
foreach (var increase in TraningDataset.IncreaseStockList)
{
if (!PreviewId.Equals(increase.id))
{
var htmlfile = Program.DocBase + @"\FDDC_announcements_round1_train_20180518\定增\html\" + increase.id + ".html";
PreviewRoot = new HTMLEngine().Anlayze(htmlfile, "");
PreviewId = increase.id;
}
TargetTool.PutTrainingItem(PreviewRoot, increase.PublishTarget);
IncreaseNumberTool.PutTrainingItem(PreviewRoot, increase.IncreaseNumber);
IncreaseMoneyTool.PutTrainingItem(PreviewRoot, increase.IncreaseMoney);
}
TargetTool.WriteTop(10);
} 增发对象
17% 00237 发行对象
16% 00223 发行对象名称
11% 00156 股东名称
09% 00132 认购对象
07% 00096 投资者名称
06% 00085 名称
04% 00061 认购对象名称
04% 00055 获配投资者名称
02% 00035 询价对象名称
02% 00029 配售对象名称
增发数量
30% 00370 获配股数(股)
19% 00234 配售股数(股)
13% 00158 认购股数(股)
10% 00126 持股数量(股)
03% 00045 认购数量(股)
02% 00028 持股总数(股)
02% 00024 配售数量(股)
01% 00019 持股数(股)
01% 00015 获配数量(股)
00% 00011 总股本比例
00% 00011 获配股数(万股)
00% 00011 认购股数(万股)
增发金额
35% 00257 获配金额(元)
21% 00155 认购金额(元)
17% 00125 配售金额(元)
08% 00062 配售金额(元)
02% 00018 认购金额(万元)
02% 00017 认购金额(人民币元)
01% 00014 发行前
01% 00014 申购金额(万元)
01% 00011 获配金额(元)
01% 00008 追加认购金额(元)

除了统计标题之外,还可以通过某个标题下面出现的内容。

下面的例子是看一下增减持方式有哪些:

    /// <summary>
/// 增减持训练
/// </summary>
/// <param name="TraningCnt">训练条数</param>
public static void Traning(int TraningCnt = int.MaxValue)
{
var ChangeMethodTool = new TableAnlayzeTool();
var PreviewId = String.Empty;
var PreviewRoot = new HTMLEngine.MyRootHtmlNode();
int Cnt = 0;
foreach (var stockchange in TraningDataset.StockChangeList)
{
if (!PreviewId.Equals(stockchange.id))
{
var htmlfile = Program.DocBase + @"\FDDC_announcements_round1_train_20180518\增减持\html\" + stockchange.id + ".html";
PreviewRoot = new HTMLEngine().Anlayze(htmlfile, "");
PreviewId = stockchange.id;
Cnt++; if (Cnt == TraningCnt) break;
}
ChangeMethodTool.PutValueTrainingItem(PreviewRoot, new string[]{"减持方式","增持方式"}.ToList());
}
Program.Training.WriteLine("增减持方式");
ChangeMethodTool.WriteTop(10);
} 增减持方式
33% 09277 集中竞价交易
24% 06771 集中竞价
21% 05940 大宗交易
08% 02468 竞价交易
01% 00464 集中竞价减持
01% 00365 减持方式
01% 00303 <null>
01% 00289 二级市场竞价
00% 00258 合计
00% 00196 竞价减持

抽取

采用各种方法抽取数据,务必使得所有数据都抽取出来。根据训练结果从候选值里面获得置信度最大的数据。抽取手段如下:

  • 具有明确先导词
  • NER实体标识
  • 具体语境

表格抽取工具(内容系)

代码内置表头规则系的表抽取工具,对于表格可以设定如下抽取规则:

  • Content:匹配内容
  • IsContentEq:内容匹配规则(包含或者相等)
    /// <summary>
/// 表抽取规则(内容系)
/// </summary>
public struct TableSearchContentRule
{
/// <summary>
/// 匹配内容
/// </summary>
public List<String> Content;
/// <summary>
/// 是否相等模式
/// </summary>
public bool IsContentEq;
}

下面是一个表格抽取的例子:

        var rule = new TableSearchContentRule();
rule.Content = new string[] { "集中竞价交易", "竞价交易", "大宗交易", "约定式购回" }.ToList();
rule.IsContentEq = true;
var result = HTMLTable.GetMultiRowsByContentRule(root,rule);

表格抽取工具(表头规则系)

代码内置表头规则系的表抽取工具,对于表格可以设定如下抽取规则:

  • SuperTitle:层叠表头的情况下,父表头文字
  • IsSuperTitleEq:父表头文字匹配规则(包含或者相等)
  • Title:表头文字
  • IsTitleEq:表头文字匹配规则(包含或者相等)
  • IsRequire:在行单位抽取时,该项目是否为必须项目
  • ExcludeTitle:表标题不能包含的文字
  • Normalize:抽取内容预处理器
  /// <summary>
/// 表抽取规则
/// </summary>
public struct TableSearchTitleRule
{
public string Name;
/// <summary>
/// 父标题
/// </summary>
public List<String> SuperTitle;
/// <summary>
/// 是否必须一致
/// </summary>
public bool IsSuperTitleEq;
/// <summary>
/// 标题
/// </summary>
public List<String> Title;
/// <summary>
/// 是否必须一致
/// </summary>
public bool IsTitleEq;
/// <summary>
/// 是否必须
/// </summary>
public bool IsRequire;
/// <summary>
/// 表标题不能包含的文字
/// </summary>
public List<String> ExcludeTitle;
/// <summary>
/// 抽取内容预处理器
/// </summary>
public Func<String, String, String> Normalize;
}

下面是一个表格抽取的例子:

增持前 (合并表头) 增持后 (合并表头)
持股数 持股比例 持股数 持股比例

这里我们想抽取持股比例和持股数,但是希望抽取的是增持后的部分,所以需要使用SuperTitle的规则了。

        var HoldList = new List<struHoldAfter>();
var StockHolderRule = new TableSearchRule();
StockHolderRule.Name = "股东全称";
StockHolderRule.Title = new string[] { "股东名称", "名称", "增持主体", "增持人", "减持主体", "减持人" }.ToList();
StockHolderRule.IsTitleEq = true;
StockHolderRule.IsRequire = true; var HoldNumberAfterChangeRule = new TableSearchRule();
HoldNumberAfterChangeRule.Name = "变动后持股数";
HoldNumberAfterChangeRule.IsRequire = true;
HoldNumberAfterChangeRule.SuperTitle = new string[] { "减持后", "增持后" }.ToList();
HoldNumberAfterChangeRule.IsSuperTitleEq = false;
HoldNumberAfterChangeRule.Title = new string[] {
"持股股数","持股股数",
"持股数量","持股数量",
"持股总数","持股总数","股数"
}.ToList();
HoldNumberAfterChangeRule.IsTitleEq = false; var HoldPercentAfterChangeRule = new TableSearchRule();
HoldPercentAfterChangeRule.Name = "变动后持股数比例";
HoldPercentAfterChangeRule.IsRequire = true;
HoldPercentAfterChangeRule.SuperTitle = HoldNumberAfterChangeRule.SuperTitle;
HoldPercentAfterChangeRule.IsSuperTitleEq = false;
HoldPercentAfterChangeRule.Title = new string[] { "比例" }.ToList();
HoldPercentAfterChangeRule.IsTitleEq = false; var Rules = new List<TableSearchRule>();
Rules.Add(StockHolderRule);
Rules.Add(HoldNumberAfterChangeRule);
Rules.Add(HoldPercentAfterChangeRule);
var result = HTMLTable.GetMultiInfoByTitleRules(root, Rules, false);

EntityProperty对象

EntityProperty对象属性如下:

  • PropertyName:属性名称
  • PropertyType:属性类型(数字,金额,字符,日期)
  • MaxLength:最大长度
  • MinLength:最小长度
  • MaxLengthCheckPreprocess:最大长度判定前预处理器(不改变抽取内容)
  • LeadingColonKeyWordList:先导词(包含":")
  • LeadingColonKeyWordCandidatePreprocess:先导词预处理器(改变抽取内容
  • QuotationTrailingWordList:引号和书名号中的词语
  • DpKeyWordList:句法依存环境
  • ExternalStartEndStringFeature:普通的开始结尾词判定
  • CandidatePreprocess:一般候选词预处理器(改变抽取内容
  • struRegularExpressFeature:正则表达式特征检索条件
  • ExcludeContainsWordList:不能包含词语列表
  • ExcludeEqualsWordList:不能等于词语列表
  • Confidence:置信度对象
    /// <summary>
/// 获得合同名
/// </summary>
/// <returns></returns>
string GetContractName()
{
var e = new EntityProperty();
e.PropertyName = "合同名称";
e.PropertyType = EntityProperty.enmType.Normal;
e.MaxLength = ContractTraning.MaxContractNameLength;
e.MinLength = 5;
e.LeadingColonKeyWordList = new string[] { "合同名称:" };
e.QuotationTrailingWordList = new string[] { "协议书", "合同书", "确认书", "合同", "协议" };
e.QuotationTrailingWordList_IsSkipBracket = true; //暂时只能选True
var KeyList = new List<ExtractPropertyByDP.DPKeyWord>();
KeyList.Add(new ExtractPropertyByDP.DPKeyWord()
{
StartWord = new string[] { "签署", "签订" }, //通过SRL训练获得
StartDPValue = new string[] { LTPTrainingDP.核心关系, LTPTrainingDP.定中关系, LTPTrainingDP.并列关系 },
EndWord = new string[] { "补充协议", "合同书", "合同", "协议书", "协议", },
EndDPValue = new string[] { LTPTrainingDP.核心关系, LTPTrainingDP.定中关系, LTPTrainingDP.并列关系, LTPTrainingDP.动宾关系, LTPTrainingDP.主谓关系 }
});
e.DpKeyWordList = KeyList; var StartArray = new string[] { "签署了", "签订了" }; //通过语境训练获得
var EndArray = new string[] { "合同" };
e.ExternalStartEndStringFeature = Utility.GetStartEndStringArray(StartArray, EndArray);
e.ExternalStartEndStringFeatureCandidatePreprocess = (x) => { return x + "合同"; };
e.MaxLengthCheckPreprocess = str =>
{
return EntityWordAnlayzeTool.TrimEnglish(str);
};
//最高级别的置信度,特殊处理器
e.LeadingColonKeyWordCandidatePreprocess = str =>
{
var c = Normalizer.ClearTrailing(TrimJianCheng(str));
return c;
}; e.CandidatePreprocess = str =>
{
var c = Normalizer.ClearTrailing(TrimJianCheng(str));
var RightQMarkIdx = c.IndexOf("”");
if (!(RightQMarkIdx != -1 && RightQMarkIdx != c.Length - 1))
{
//对于"XXX"合同,有右边引号,但不是最后的时候,不用做
c = c.TrimStart("“".ToCharArray());
}
c = c.TrimStart("《".ToCharArray());
c = c.TrimEnd("》".ToCharArray()).TrimEnd("”".ToCharArray());
return c;
};
e.ExcludeContainsWordList = new string[] { "日常经营重大合同" };
//下面这个列表的根据不足
e.ExcludeEqualsWordList = new string[] { "合同", "重大合同", "项目合同", "终止协议", "经营合同", "特别重大合同", "相关项目合同" };
e.Extract(this); //是否所有的候选词里面包括(测试集无法使用)
var contractlist = TraningDataset.ContractList.Where((x) => { return x.id == this.Id; });
if (contractlist.Count() > 0)
{
var contract = contractlist.First();
var contractname = contract.ContractName;
if (!String.IsNullOrEmpty(contractname))
{
e.CheckIsCandidateContainsTarget(contractname);
}
}
//置信度
e.Confidence = ContractTraning.ContractES.GetStardardCI();
return e.EvaluateCI();
}

简单关键字抽取

对于一些及其简单的关键字抽取,例如,出现"现金认购",则将认购方法标记为"现金",则可以使用KeyWordMap属性即可。

    /// <summary>
/// 评估方式
/// </summary>
/// <param name="root"></param>
/// <returns></returns>
string getEvaluateMethod()
{
var p = new EntityProperty();
foreach (var method in ReOrganizationTraning.EvaluateMethodList)
{
p.KeyWordMap.Add(method, method);
}
p.Extract(this);
if (!Program.IsMultiThreadMode) Program.Logger.WriteLine("评估方式:" + string.Join("、", p.WordMapResult));
return string.Join("、", p.WordMapResult);
}

实体位置体系

在寻在实体的时候,尽可能的将找到的实体及其位置进行记录,下面的结构体则是一个实体的记录。

    /// <summary>
/// 位置和值
/// </summary>
public struct LocAndValue<T>
{
/// <summary>
/// HTML整体位置
/// </summary>
public int Loc;
/// <summary>
/// 开始位置
/// </summary>
public int StartIdx;
/// <summary>
/// 值
/// </summary>
public T Value;
/// <summary>
/// 类型
/// </summary>
public string Type;
}

下面则是一个实体位置的应用。公司里面放着所有公司实体的位置,标的则放着百分比 + “股权”字样的实体。通过位置信息,则可以将“公司”和“标的”成对发现。

    /// <summary>
/// 获得标的
/// </summary>
/// <returns></returns>
List<(string Target, string Comany)> getTargetList()
{
var rtn = new List<(string Target, string Comany)>(); var targetRegular = new ExtractProperyBase.struRegularExpressFeature()
{
RegularExpress = RegularTool.PercentExpress,
TrailingWordList = new string[] { "股权" }.ToList()
};
var targetLoc = ExtractPropertyByHTML.FindRegularExpressLoc(targetRegular, root); //所有公司名称
var CompanyList = new List<string>();
foreach (var companyname in companynamelist)
{
//注意,这里的companyname.WordIdx是分词之后的开始位置,不是位置信息!
if (!CompanyList.Contains(companyname.secFullName))
{
if (!string.IsNullOrEmpty(companyname.secFullName)) CompanyList.Add(companyname.secFullName);
}
if (!CompanyList.Contains(companyname.secShortName))
{
if (!string.IsNullOrEmpty(companyname.secShortName)) CompanyList.Add(companyname.secShortName);
}
} var targetlist = new List<string>(); foreach (var companyname in CompanyList)
{
var companyLoc = ExtractPropertyByHTML.FindWordLoc(companyname, root);
foreach (var company in companyLoc)
{
foreach (var target in targetLoc)
{
var EndIdx = company.StartIdx + company.Value.Length;
if (company.Loc == target.Loc && Math.Abs(target.StartIdx - EndIdx) < 2)
{
if (!targetlist.Contains(target.Value + ":" + company.Value))
{
rtn.Add((target.Value, company.Value));
targetlist.Add(target.Value + ":" + company.Value);
}
}
}
}
} return rtn;
}

参考文献

鸣谢

  • 感谢阿里巴巴组委会提供标注好的金融数据。
  • 感谢组委会@通联数据_梅洁,@梅童的及时答疑。
  • 感谢微信好友 邓少冬 潘昭鸣 NLP宋老师 的帮助和指导

[开源]开放域实体抽取泛用工具 NetCore2.1的更多相关文章

  1. Aleax prize (开放域聊天系统比赛)2018冠军论文阅读笔记

    Abstract Gunrock是一种社交机器人,旨在让用户参与开放域的对话.我们使用大规模的用户交互数据来迭代地改进了我们的机器人,使其更具能力和人性化.在2018年Alexa奖的半决赛期间,我们的 ...

  2. Android 开源控件与常用开发框架开发工具类

    Android的加载动画AVLoadingIndicatorView 项目地址: https://github.com/81813780/AVLoadingIndicatorView 首先,在 bui ...

  3. Laya微信小游戏的开放域

    版本2.1.1.1 现在Laya的开放域比较好用了. 新建开放域项目,里面直接有个排行榜的示例. 直接发布 得到较少的文件,复制这些文件,粘贴到主项目bin/openDataContext下. (op ...

  4. 开放神经网络交换(ONNX)工具

    开放神经网络交换(ONNX)工具 开放神经网络交换(ONNX)是一个开放的生态系统,它使人工智能开发人员能够在项目发展过程中选择正确的工具.ONNX为人工智能模型提供了一种开源格式,包括深度学习和传统 ...

  5. Syncthing -- 开源的云储存和同步服务工具

    Syncthing  -- an open-source file synchronization client/server application Syncthing是一个开源的云存储和同步服务工 ...

  6. puppet开源的软件自动化配置和部署工具——本质就是CS,服务端统一管理配置

    1.  概述 puppet是一个开源的软件自动化配置和部署工具,它使用简单且功能强大,正得到了越来越多地关注,现在很多大型IT公司均在使用puppet对集群中的软件进行管理和部署,如google利用p ...

  7. 开源一款超实用的 Dubbo 测试工具,已用半年,感觉很有feel~

    不知道你是否在工作中有遇到过类似情况: dubbo接口调试复杂,需要通过telnet命令或者通过consumer调用来触发. telnet语句参数格式复杂,每次编写都要小心谨慎,一旦出错又需重来. 复 ...

  8. 【开源】C#信息抽取系统【招募C#队友】

    FDDC2018金融算法挑战赛02-A股上市公司公告信息抽取 更新时间 2018年7月11日 By 带着兔子去旅行 信息抽取是NLP里的一个实用内容.该工具的目标是打造一个泛用的自动信息抽取工具.使得 ...

  9. 开源个.NetCore写的 - 并发请求工具PressureTool

    本篇和大家分享的是一个 并发请求工具,并发往往代表的就是压力,对于一些订单量比较多的公司这种情况很普遍,也因此出现了很多应对并发的解决方案如:分布式,队列,数据库锁等: 对于没有遇到过或者不可能线上来 ...

随机推荐

  1. P1233 木棍加工 dp LIS

    题目描述 一堆木头棍子共有n根,每根棍子的长度和宽度都是已知的.棍子可以被一台机器一个接一个地加工.机器处理一根棍子之前需要准备时间.准备时间是这样定义的: 第一根棍子的准备时间为1分钟: 如果刚处理 ...

  2. Python中元类

    元类(metaclass) 简单地说,元类就是一个能创建类的类,而类class 是由type创建的,class可以创建对象 type与object的关系详见:python中type和object 1. ...

  3. CLR Via第一 章 知识点整理(3)CLR执行程序集的IL代码

    在了解CLR运行之前让我们先简单了解一下IL 除了编译器编译的IL代码,IL也是一种汇编语言,也就是说我们可以直接编写IL代码,当然也有对应的IL编译器,值得一提的是对于面向CLR的其他语言,CLR只 ...

  4. Github入门操作实录

    到目前为止,我已经工作快5年了,这5年最大的感受就是,框架什么的并不难,只要知道api,就能用起来,一开始会遇到一点问题,但是天下的框架都大同小异,无非是jar包,配置文件,模板代码,jar包可以使用 ...

  5. atom那些事儿

    基于electron(Electron 的底层基于Chromium 和node.js)

  6. cf 443

    题目链接 A,对于每一位可以暴力输入,看输出是什么,然后就有2x2中对应方式,然后可以用3次运算搞了,好像网上在悬赏最多只用2次搞出来的. B,这个题可以先处理每个串内部的情况,再处理连接处的情况,代 ...

  7. Java笔记(十七) 异步任务执行服务

    异步任务执行服务 一.基本原理和概念 一)基本接口 1)Runnable和Callable:表示要执行的异步任务. 2)Executor和ExecutorService:表示执行服务. 3)Futur ...

  8. string method and regular expresions

    <!doctype html> <!DOCTYPE html> <html> <head> <meta charset="utf-8&q ...

  9. GitHub用法

    注意: 在push之前要先git pull origin融合代码使得本地代码版本更新,从而才能进行push!! 详细内容参见->这里 本篇内容转自->这里 作者:知乎用户链接:https: ...

  10. Windows10关机问题----只有“睡眠”、“更新并重启”、“更新并关机”,但是又不想更新,解决办法

    最近的一个问题,电脑关机的时候发现,只有“睡眠”.“更新并重启”.“更新并关机” 内心很是煎熬.... 尝试了N种方式,然后总结如下: 第一种方式:(表示自己window的系统用着挺好,力荐) 1.打 ...