[開源]開放域實體抽取泛用工具 NetCore2.1

開放域實體抽取泛用工具

https://github.com/magicdict/FDDChtml

更新時間 2018年7月16日 By 帶着兔子去旅行python

開發這個工具的起源是天池大數據競賽,FDDC2018金融算法挑戰賽02-A股上市公司公告信息抽取。這個比賽是針對金融公告開展的信息抽取比賽。在參勝過程中,萌生出一個念頭,是否可以開發出一個泛用的信息抽取工具呢?git

信息抽取是NLP裏的一個實用內容。該工具的目標是打造一個泛用的自動信息抽取工具。使得沒有任何基礎的用戶,能夠經過簡單的步驟提取文檔(PDF,HTML,TXT)中的信息。該工具使用C#(.Net Core)開發,因此能夠跨平臺運行。(Python在作大的工程的時候有諸多不便,因此沒有使用python語言)github

工具原理採用的是開放域實體抽取的方法:
使用各類方法儘量抽取實體,而後對於候選內容進行置信度分析打分。正則表達式

開放域實體抽取的方法

基本環境

  • .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。windows

結巴分詞:某些地名,例如"大連",會被誤判。這裏使用地名輔助字典的方式作糾正。ltp工具沒有這個問題。ltp工具和結巴分詞功能雖然重複,可是暫時還不能移除結巴分詞。centos

前期準備

  • 使用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)
  • 詞尾

語境

  • 該關鍵字在 :(中文冒號)以後的場景下,:(中文冒號)前面的內容
  • 包含該關鍵字的句子中,該關鍵字的前置動詞
  • 包含該關鍵字的句子中,該關鍵字是否在角色標識中存在

訓練結果例:ide

協議書(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宋老師 的幫助和指導
相關文章
相關標籤/搜索