基于统计的中文目标机构名识别(缩水简化版)

xiaoxiao2021-02-28  70

最近研究基础建设的一些招中标信息,如何在一篇中标公告中识别出中标单位的名称。尝试了很多方法,感觉要么不太理想,要么方法太难了一时半会儿搞不定。

有一些比较高大上的机构名识别方法,例如基于角色标注的机构名识别,里面的核心思想,大概就是先分词并标注词性,然后经过一系列统计算法,利用词性序列识别机构名,召唤率和正确率看上去都挺不错的。不过实现起来比较复杂,领导又要求尽快见成果,只能先用简单的方法凑合着了,一些更好的方法以后再来尝试吧。

这里所说的简单方法,就是基于统计的机构名识别了。

首先,我们来看一篇典型的中标公告:

嗯,这篇算是比较中规中矩的中标公告了,中标人就是箭头所指的公司,当然,中标公告也有可能没有表格,而即使有表格,行列所对应的内容也是千奇百怪,这一篇只是N中情况中的一种。

之后的问题就比较直接了,如何提取中标公司?

首先,提取到公司名称,再根据上下文内容判别哪一家是中标公司?比较高大上的科学方法或许会这么干,但!我们现在是用简单的、缩水的办法,既然要简单那就简单到底吧。这里我没有用分词,而是直接将html片段去掉了html标签,空格和阿拉伯数字。因为在大规模的中标公司提取中,以上三者的存在会对程序的工作造成重大影响。然后我人工选取了其中的中标单位名称,就像这样:

项目名称奉节县年脱贫村撤并村通畅工程红土乡野茶庄平黄山路项目招标公告编号招标人奉节县红土乡人民政府联系电话招标代理机构河南创达建设工程管理有限公司重庆分公司标段第一中标候选人 重庆远华建筑有限公司 第二中标候选人重庆渝奥工程项目管理有限公司第三中标候选人四川锦泰达建筑工程有限公司拟中标人重庆远华建筑有限公司中标金额万元工商注册号L组织机构代码投诉受理部门奉节县交通委员会联系电话奉节县发改委联系电话奉节县公共资源交易管理办公室联系电话招标人奉节县红土乡人民政府年月日单位公章招标代理机构河南创达建设工程管理有限公司重庆分公司年月日单位公章

咳咳,中间的两个空格就是我的人工操作了。如此往复,我坚持了两天,人工标注了几百条数据。

这样,就初步形成了一个初步的熟预料库(额,虽然只有几百条不成规模,但先这样吧,再继续手工我就要吐了)。

接下来,可以训练样本了,我这里由于没有分词,因此我自己制定了一个边界标准,左侧边界是出现在中标单位前的六个字,右侧边界是中标单位本身最后的两个字。如上面的例子,左侧边界就是“一中标候选人”,右侧边界是“公司”。当然我这里之所以选择6个字为标准,是测试了3、4、5、6、7这五个数字之后得到的,当选取6的时候,匹配的精度较高而代价较小。

代码如下:

package com.test; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Administrator */ public class Test2 { /** * 读取训练文件 * * @param filePath * @return */ public String readTxtFile(String filePath) { String temp = ""; String encoding = "GBK"; File file = new File(filePath); if (file.isFile() && file.exists()) { InputStreamReader read = null; try { //判断文件是否存在 read = new InputStreamReader(new FileInputStream(file), encoding); //考虑到编码格式 BufferedReader bufferedReader = new BufferedReader(read); String lineTxt = null; while ((lineTxt = bufferedReader.readLine()) != null) { temp = lineTxt.trim(); } read.close(); } catch (Exception ex) { Logger.getLogger(Test2.class.getName()).log(Level.SEVERE, null, ex); } finally { try { read.close(); } catch (IOException ex) { Logger.getLogger(Test2.class.getName()).log(Level.SEVERE, null, ex); } } } else { System.out.println("找不到指定的文件"); } return temp; } /** * 输出上下文片段 * * @param temp * @param filename */ public void WriteIn(String temp, String filename) { FileWriter fw = null; try { //如果文件存在,则追加内容;如果文件不存在,则创建文件 File f = new File(filename); fw = new FileWriter(f, true); } catch (IOException e) { e.printStackTrace(); } PrintWriter pw = new PrintWriter(fw); pw.println(temp); pw.flush(); try { fw.flush(); pw.close(); fw.close(); } catch (IOException e) { e.printStackTrace(); } } /** * 截取上下文片段 * * @param paths 文件路径 * @param size 左侧边界选取的字数 */ public void getDetail(List<String> paths, int size) { Map befores = new TreeMap(); Map afters = new TreeMap(); for (String path : paths) { HashMap<String, String> map = new HashMap<>(); String info = readTxtFile(path); String[] temp = info.split(" "); if (temp.length == 1) { continue; } //获取上文片段 String before = temp[0]; before = before.substring(before.lastIndexOf("") - size, before.lastIndexOf("")); if (befores.get(before) == null) { befores.put(before, 1); } else { int count = (int) befores.get(before); befores.put(before, count + 1); } //获取下文片段 String after = temp[1]; after = after.substring(after.lastIndexOf("") - 2, after.lastIndexOf("")); if (afters.get(after) == null) { afters.put(after, 1); } else { int count = (int) afters.get(after); afters.put(after, count + 1); } } ArrayList<Map.Entry<String, Integer>> befores_result = sortMap(befores); ArrayList<Map.Entry<String, Integer>> afters_result = sortMap(afters); befores_result.stream().forEach((bmap) -> { String key = bmap.getKey(); int value = bmap.getValue(); WriteIn(key + "," + String.valueOf(value), "C:\\Users\\Administrator\\Desktop\\trainresult\\上文" + String.valueOf(size) + ".txt"); }); afters_result.stream().forEach((amap) -> { String key = amap.getKey(); int value = amap.getValue(); WriteIn(key + "," + String.valueOf(value), "C:\\Users\\Administrator\\Desktop\\trainresult\\下文" + String.valueOf(size) + ".txt"); }); } /** * 排序,将出现频率由小到大排 * * @param map * @return */ public static ArrayList<Map.Entry<String, Integer>> sortMap(Map map) { List<Map.Entry<String, Integer>> entries = new ArrayList<>(map.entrySet()); Collections.sort(entries, (Map.Entry<String, Integer> obj1, Map.Entry<String, Integer> obj2) -> obj2.getValue() - obj1.getValue()); return (ArrayList<Entry<String, Integer>>) entries; } /** * 获取某个目录下的全部文件名 * * @param path * @return */ public List<String> getFileName(String path) { List<String> list = new ArrayList<>(); File f = new File(path); if (!f.exists()) { System.out.println(path + " not exists"); return list; } File fa[] = f.listFiles(); for (File fs : fa) { if (fs.isDirectory()) { System.out.println(fs.getName() + " [目录]"); } else { System.out.println(fs.getName()); list.add(path + "\\" + fs.getName()); } } return list; } /** * 得出上下文 * * @param args */ public static void main(String args[]) { Test2 t = new Test2(); List<String> filenames = t.getFileName("C:\\Users\\Administrator\\Desktop\\trained"); for (int count = 6; count <= 6; count++) { t.getDetail(filenames, count); } } } 其中getDetails是用来截取上下文片段以及排序并输出它们的,训练结果如下:

上文:                           下文:

上下文内容后面的数字代表它们在样本中出现的次数,出现次数越高,匹配优先度越高。当然,低匹配度的结果我们也会保留,用于辅助矫正最终结果,核心代码如下:

/** * 从中标文件中取得公司名字 * * @param info 去掉空格、阿拉伯数字、html标签之后的网页片段 * @param befores 上文 * @param afters 下文 * @return */ public String getCompanyName(String info, List<String> befores, List<String> afters) { List<String> temps = new ArrayList<>(); List<String> tbefores = new ArrayList<>(); List<String> tafters = new ArrayList<>(); befores.stream().filter((before) -> (info.contains(before.split(",")[0]))).forEach((before) -> { tbefores.add(before); }); afters.stream().filter((after) -> (info.contains(after.split(",")[0]))).forEach((after) -> { tafters.add(after); }); tafters.stream().forEach((String tafter) -> { tbefores.stream().map((tbefore) -> matchString(info, tbefore.split(",")[0], tafter.split(",")[0])).forEach((companynames) -> { companynames.stream().forEach((companyname) -> { temps.add(companyname); }); }); }); if (temps.isEmpty()) { return ""; } String companyname = getMinCompanyName(temps); return companyname; } /** * 矫正公司名字 * * @param names * @return */ public String getMinCompanyName(List<String> names) { String temp = names.get(0); for (String name : names) { if (temp.contains(name)) { temp = name; } } return temp; } /** * 卡公司名称的长度 * * @param temp * @param before * @param after * @return */ public List<String> matchString(String temp, String before, String after) { List<String> list = new ArrayList<>(); Pattern p = Pattern.compile(before + "[一-龥]{3,18}" + after); Matcher m = p.matcher(temp); while (m.find()) { String str = m.group().replace(before, ""); if (!str.contains("废标") && !str.contains("流标")) { list.add(str); } } p = Pattern.compile(before + "[一-龥]{3,6}" + after); m = p.matcher(temp); while (m.find()) { String str = m.group().replace(before, ""); if (!str.contains("废标") && !str.contains("流标")) { list.add(str); } } p = Pattern.compile(before + "[一-龥]{7,10}" + after); m = p.matcher(temp); while (m.find()) { String str = m.group().replace(before, ""); if (!str.contains("废标") && !str.contains("流标")) { list.add(str); } } p = Pattern.compile(before + "[一-龥]{11,14}" + after); m = p.matcher(temp); while (m.find()) { String str = m.group().replace(before, ""); if (!str.contains("废标") && !str.contains("流标")) { list.add(str); } } p = Pattern.compile(before + "[一-龥]{15,18}" + after); m = p.matcher(temp); while (m.find()) { String str = m.group().replace(before, ""); if (!str.contains("废标") && !str.contains("流标")) { list.add(str); } } return list; } 其中getCompanyName利用上下文内容获取中标公司名字并进行矫正,另外两个函数都是被它所调用的。

好了,进行到这里,可以看成果了,我按天数测试了很多批中标数据,统计了召回率,大概维持在65%~80%的一个区间;正确率要更高些,我没有详细统计,大概测试了一下,估计90%以上。以下是成果截图:

怎么样?虽然用的方法很low,过程也比较吐血(手工标注了两天啊!!!),但结果还是不错的,至少应急应付领导可以了。

至于更科学的方法,以后再慢慢研究了。

先继续学习文本挖掘吧~~~

转载请注明原文地址: https://www.6miu.com/read-41835.html

最新回复(0)