GCN常用数据集- SciTSR介绍

简介

SciTSR是一个多种格式PDF表格的数据集。
TSR对应英文单词为:Table Structure Recognition,即表格结构识别。
共有15000个样例,其中12000训练数据 (2885张复杂表格),3000(716张复杂表格)测试数据。
Github链接为:https://github.com/Academic-Hammer/SciTSR

文件夹结构如下:

SciTSR
├── SciTSR-COMP.list
├── test
│   ├── chunk
│   ├── img
│   ├── pdf
│   └── structure
└── train
    ├── chunk
    ├── img
    ├── pdf
    ├── rel
    └── structure

所有文件夹的对应数据的文件名相同,扩展名不同而已。
比如文件:0704.2596v1.2在各个文件夹均有同名文件。

训练集相比测试集多了rel文件夹,因为rel文件夹中的数据就是需要预测的表格中各个单元格的同行或同列的关系。

pdf文件夹

顾名思义,即原始表格对应的PDF文件,如:


2020-12-08 17_15_42-Start.png

chunk文件夹

存储表格各个单元格的位置以及文本信息
数据格式:json
数据对应表格顺序:从左往右,从上往下
pos中的顺序为:xmin, xmax, ymin, ymax
如:

{
    "pos": [
        41.35900115966797,
        61.00923538208008,
        574.2490234375,
        579.2303466796875
    ],
    "text": "code"
},
{
    "pos": [
        90.00299835205078,
        149.44332885742188,
        572.7540283203125,
        581.053466796875
    ],
    "text": "computing S d"
},
{
    "pos": [
        170.58999633789062,
        186.80360412597656,
        572.7540283203125,
        581.053466796875
    ],
    "text": "|S d |"
},
{
    "pos": [
        198.75900268554688,
        253.30320739746094,
        574.2490234375,
        579.2303466796875
    ],
    "text": "fulliteration"
}

img文件夹

即对pdf文件内容截图

structure文件夹

与chunk数据的索引一一对应的结果数据,即y
其中id的值即为chunk数据中的索引
start_row, end_row, start_col, end_col都是给定的结果
示例:

{
    "id": 0,
    "tex": "code",
    "content": [
        "code"
    ],
    "start_row": 0,
    "end_row": 0,
    "start_col": 0,
    "end_col": 0
},
{
    "id": 1,
    "tex": "computing ${\\cal S}_d$",
    "content": [
        "computing",
        "Sd"
    ],
    "start_row": 0,
    "end_row": 0,
    "start_col": 1,
    "end_col": 1
},
{
    "id": 2,
    "tex": "$|{\\cal S}_d|$",
    "content": [
        "|Sd|"
    ],
    "start_row": 0,
    "end_row": 0,
    "start_col": 2,
    "end_col": 2
},
{
    "id": 10,
    "tex": "$[105,7,77]_5$",
    "content": [
        "[105,",
        "7,",
        "77]5"
    ],
    "start_row": 1,
    "end_row": 1,
    "start_col": 0,
    "end_col": 0
},

rel文件夹

经过structure的行列索引,计算出来的单元格之间的行列关系,同一行为1:0,同一列为2:0,严格来说这个是输入图卷积网络真正的y,示例为:

0   1   1:0
0   10  2:0
1   2   1:0
1   11  2:0
2   3   1:0
2   12  2:0
3   4   1:0
3   13  2:0
4   5   1:0
4   14  2:0
5   15  2:0
7   17  2:0
9   19  2:0
10  11  1:0
10  20  2:0
11  12  1:0
11  21  2:0
12  13  1:0
12  22  2:0
13  14  1:0
13  23  2:0
14  15  1:0
14  24  2:0
15  16  1:0
15  25  2:0
16  17  1:0
16  26  2:0
17  18  1:0
17  27  2:0
18  19  1:0
18  28  2:0
19  29  2:0
20  21  1:0
20  30  2:0
21  22  1:0
21  31  2:0
22  23  1:0

应用该训练集的GCN实例:GFTE

Github地址为:https://github.com/Irene323/GFTE
作者期望通过:

  1. 仅仅position信息
  2. position + 文本信息
  3. position + 文本信息 + 图像特征
    这三种方式分别训练GCN
    这是一个很好的用例,且有使用SciTSR的示范代码

获取训练与验证数据

获取训练与验证数据的代码位于:GFTE-pos/GFTE-pos/dataset0.py
单元格之间的关系,首先是根据chunk的坐标信息,通过geometric.transforms的KNNGraph,得到边索引,即edge_index。
因为是最近邻,所以超参的K设定很重要,因为这个直接决定了表格中的邻近单元格有没有被描边,我是设置为10。
得到edge_index之后,根据structure的数据,得到点与点之间的关系,同一行为1,同一列为2,否则为0。

不过标签获取的计算方程,原代码仅仅是获取是否同一行的标签,并没有是否同列的信息。
原代码如下:

    def cal_label(self,data,tbpos): # 根据构造的图,计算边的标注。
        edges = data.edge_index  # [2, 边的个数] 无向图的边是对称的,即有2条。
        y = []
        for i in range(edges.size()[1]):
            y.append(self.if_same_row(edges[0,i], edges[1,i],tbpos))
        return y
            
    def if_same_row(self,si,ti,tbpos):
        ss,se = tbpos[si][0], tbpos[si][1]
        ts,te = tbpos[ti][0], tbpos[ti][1]
        if (ss>=ts and se<=te):
            return 1
        if (ts>=ss and te<=se):
            return 1
        return 0

我们加入相应的修改,使得其不仅能获取是否同一行信息,也能获取是否同一列的信息:

    def cal_label(self,data,tbpos): # 根据构造的图,计算边的标注。
        edges = data.edge_index  # [2, 边的个数] 无向图的边是对称的,即有2条。
        y = []
        for i in range(edges.size()[1]):
            # 相同列的y值为2,相同行的y值为1,否则为0
            if self.if_same_col(edges[0,i], edges[1,i],tbpos):
                y.append(2)
            elif self.if_same_row(edges[0,i], edges[1,i],tbpos):
                y.append(1)
            else:
                y.append(0)
#           y.append(self.if_same_row(edges[0,i], edges[1,i],tbpos))
        return y
            
    def if_same_row(self,si,ti,tbpos):
        ss,se = tbpos[si][0], tbpos[si][1]
        ts,te = tbpos[ti][0], tbpos[ti][1]
        if (ss>=ts and se<=te):
            return 1
        if (ts>=ss and te<=se):
            return 1
        return 0
    
    def if_same_col(self,si,ti,tbpos):
        ss,se = tbpos[si][2], tbpos[si][3]
        ts,te = tbpos[ti][2], tbpos[ti][3]
        if (ss>=ts and se<=te):
            return 1
        if (ts>=ss and te<=se):
            return 1
        return 0

根据SciTSR给出的label信息,我们或许可以得出如下结论:仅仅相邻的行,才判断是否位于同一列,亦即:相邻边的两个点:
点1结束行索引-点2开始行索引的绝对值为1,或者点2结束行索引-点1开始行索引的绝对值为1,才进入判断是否是同一列的判断
是否是同一行的索引可以进行类推。
代码示例如下:

    def cal_label(self,data,tbpos): # 根据构造的图,计算边的标注。
        edges = data.edge_index  # [2, 边的个数] 无向图的边是对称的,即有2条。
        y = []
        for i in range(edges.size()[1]):
            # 相同列的y值为2,相同行的y值为1,否则为0
            if self.if_same_col(edges[0,i], edges[1,i],tbpos):
                y.append(2)
            elif self.if_same_row(edges[0,i], edges[1,i],tbpos):
                y.append(1)
            else:
                y.append(0)
#           y.append(self.if_same_row(edges[0,i], edges[1,i],tbpos))
        return y
            
    def if_same_row(self,si,ti,tbpos):
        # 是否需要考虑两个单元格是邻近列的关系
        # 0:开始行,1:结束行,2:开始列,3:结束列
        if abs(tbpos[si][3] - tbpos[ti][2]) == 1 or \
                abs(tbpos[ti][3] - tbpos[si][2]) == 1:
            ss,se = tbpos[si][0], tbpos[si][1]
            ts,te = tbpos[ti][0], tbpos[ti][1]
            if (ss>=ts and se<=te):
                return 1
            if (ts>=ss and te<=se):
                return 1
        return 0
    
    def if_same_col(self,si,ti,tbpos):
        # 是否需要考虑两个单元格是邻近行的关系
        # 0:开始行,1:结束行,2:开始列,3:结束列
        if abs(tbpos[si][1] - tbpos[ti][0]) == 1 or \
                abs(tbpos[ti][1] - tbpos[si][0]) == 1:
            ss,se = tbpos[si][2], tbpos[si][3]
            ts,te = tbpos[ti][2], tbpos[ti][3]
            if (ss>=ts and se<=te):
                return 1
            if (ts>=ss and te<=se):
                return 1
        return 0

如果期望更激进一些,只有从左往右的邻近单元格才标注为同一行:1,或者从下往下的邻近单元格才标注为同一列,则按照如下方式计算标签数据:

    def cal_label(self,data,tbpos): # 根据构造的图,计算边的标注。
        edges = data.edge_index  # [2, 边的个数] 无向图的边是对称的,即有2条。
        y = []
        for i in range(edges.size()[1]):
            # 相同列的y值为2,相同行的y值为1,否则为0
            if self.if_same_col(edges[0,i], edges[1,i],tbpos):
                y.append(2)
            elif self.if_same_row(edges[0,i], edges[1,i],tbpos):
                y.append(1)
            else:
                y.append(0)
#           y.append(self.if_same_row(edges[0,i], edges[1,i],tbpos))
        return y
            
    def if_same_row(self,si,ti,tbpos):
        # 是否需要考虑两个单元格是邻近列的关系
        # 0:开始行,1:结束行,2:开始列,3:结束列
        # 考虑从左往右的方向,取邻近关系
        if tbpos[ti][2] - tbpos[si][3] == 1:
            ss,se = tbpos[si][0], tbpos[si][1]
            ts,te = tbpos[ti][0], tbpos[ti][1]
            if (ss>=ts and se<=te):
                return 1
            if (ts>=ss and te<=se):
                return 1
        return 0
    
    def if_same_col(self,si,ti,tbpos):
        # 是否需要考虑两个单元格是邻近行的关系
        # 0:开始行,1:结束行,2:开始列,3:结束列
        # 考虑从上往下的方向,取邻近关系
        if tbpos[ti][0] - tbpos[si][1] == 1:
            ss,se = tbpos[si][2], tbpos[si][3]
            ts,te = tbpos[ti][2], tbpos[ti][3]
            if (ss>=ts and se<=te):
                return 1
            if (ts>=ss and te<=se):
                return 1
        return 0

制作边关系数据

我们从代码可以得出一个结论,图片名称.rel的数据,是经过structure数据计算而来的。
当得到边索引edge_index, 以及初步计算的边关系y之后,即可生成所需的relation数据:

    def cal_relation(self, edges, y):
        relations = []
        for i, rel in enumerate(y):
            if rel == 1 or rel == 2:
                if edges[0][i] < edges[1][i]:
                    relation = '{0}\t{1}\t{2}:0\n'.format(edges[0][i], edges[1][i], rel)
                else:
                    relation = '{0}\t{1}\t{2}:0\n'.format(edges[1][i], edges[0][i], rel)
                if relation not in relations:
                    relations.append(relation)
        return relations

制作HTML数据

既然我们得到边关系了,那么可以根据这个信息,进一步绘制HTML,因为HTML结构是树状的,所以tr与td的构建可以通过递归完成:

    def construct_html(self, relation_file: str):
        try:
            chunkfn = os.path.join(self.root_path,
                                   "chunk",
                                   os.path.splitext(os.path.basename(relation_file))[0] + ".chunk")
            relationfn = os.path.join(self.root_path,
                                      "rel",
                                      os.path.splitext(os.path.basename(relation_file))[0] + ".rel")
            if not os.path.exists(chunkfn) or not os.path.exists(relationfn):
                print("can't find chunk file.")
                return
            html_folder = os.path.join(self.root_path, 'html')
            if not os.path.exists(html_folder):
                os.makedirs(html_folder)
            html_file = os.path.join(html_folder,
                                     '{0}.html'.format(
                                         os.path.splitext(os.path.basename(relation_file))[0]))
            if not self.is_rewrite_file and os.path.exists(html_file):
                return
            with open(chunkfn, 'r', encoding='utf-8') as f:
                chunks = json.load(f)['chunks']
            with open(relationfn, 'r', encoding='utf-8') as f:
                relations = [line.strip() for line in f.readlines()]
                relation_dict = {}
                for relation in relations:
                    splits = relation.split('\t')
                    if len(splits) == 3:
                        start = int(splits[0])
                        end = int(splits[1])
                        direction = int(splits[2].split(':')[0])
                        if relation_dict.get(start) is None:
                            relation_dict[start] = [(end, direction)]
                        else:
                            relation_dict[start].append((end, direction))
                html_body = r"""<html><body><table border="1px"><tbody>{0}</tbody></table>"""
                html_end = r"""
                            <style>
                            table,table tr th, table tr td { border:1px solid #0094ff; }
                            table {border-collapse: collapse;} 
                            </style>
                            </body></html>"""
                tr_list = []
                # 先确定行列表
                nodes = list(relation_dict.keys())
                if nodes[0] != 0:
                    print('The first node is not in graph!')
                    return
                flat_list = []
                self.get_tr_header_list(relation_dict, row_header=0, tr_list=tr_list, flat_list=flat_list)
                if len(tr_list) > 0:
                    for index, td_list in enumerate(tr_list):
                        self.get_td_list(relation_dict, td_list[-1], td_list, flat_list)
                    # print(tr_list)
                    row_string_list = []
                    for tr in tr_list:
                        row_string = r'<tr>{0}</tr>'
                        td_string_list = []
                        for td in tr:
                            if td < len(chunks):
                                td_string = r'<td>{0}</td>'.format(chunks[td].get('text', ''))
                                td_string_list.append(td_string)
                        row_string_list.append(row_string.format(''.join(td_string_list)))
                    html = html_body.format(''.join(row_string_list)) + html_end

                    try:
                        html_obj = BeautifulSoup(html, 'lxml')
                        html_txt = html_obj.prettify()
                        html_txt = re.sub(r'( ){2,}', ' ', html_txt)
                        with open(html_file, 'w', encoding='utf-8', errors='ignore') as wf:
                            wf.write(html_txt)
                    except Exception as e:
                        with open(html_file, 'w', encoding='utf-8', errors='ignore') as wf:
                            wf.write(html)
        except Exception as e:
            print(relation_file)
            raise Exception(e)

    def get_td_list(self, relation_dict: dict, current_td: int, td_list: list, flat_list: list):
        if len(td_list) > 0 and current_td == td_list[-1]:
            colspan = 0
            children = relation_dict.get(current_td, None)
            if children is not None:
                next_td = None
                for child in children:
                    if child[1] == 2:
                        colspan += 1
                        if colspan > 1:
                            td_list.append(current_td)
                            flat_list.append(current_td)
                for child in children:
                    if child[1] == 1:
                        next_td = child[0]
                        # 如果单元格跨行,则通过此方式,得到应该指向的成员
                        if next_td not in flat_list:
                            td_list.append(next_td)
                            flat_list.append(next_td)
                            break
                if next_td is not None and relation_dict.get(next_td, None) is not None:
                    self.get_td_list(relation_dict, next_td, td_list, flat_list)

    def get_tr_header_list(self, relation_dict: dict, row_header: int, tr_list: list, flat_list: list):
        if len(tr_list) == 0:
            tr_list.append([row_header])
            flat_list.append(row_header)
        children = relation_dict.get(row_header, None)
        if children is not None:
            rowspan = 0
            for child in children:
                if child[1] == 1:
                    rowspan += 1
                    if rowspan > 1:
                        tr_list.append([row_header])
                        flat_list.append(row_header)
            next_row_header = None
            for child in children:
                if child[1] == 2:
                    next_row_header = child[0]
                    tr_list.append([next_row_header])
                    flat_list.append(next_row_header)
                    break
            if next_row_header is not None:
                self.get_tr_header_list(relation_dict, next_row_header, tr_list, flat_list)

得到HTML的样例如图:


2020-12-24 15_14_28-Search.png

是不是像模像样了?

通过GCN生成边索引的局限性

因为edge_index是通过GCN的KNNGraph得到的,表格中边的覆盖率取决于K的设定,默认值为6,但是对于较大的表,邻近单元格可能都不在边索引映射中,使用时一定要小心。。。

GCN训练的效果

经过对GFTE代码的复现之后,通过GCN进行训练,发现每次训练几乎所有预测都集中在最多分类的边上,比如预测为0的边有10000条,预测为1的边有300条,那么预测值就都是0,这导致了看起来准确率很高,但是完全不可用的情况。
不清楚是否是对GCN理解不深刻,但是对GFTE的复现是如此。
有待之后进一步确认

推荐阅读更多精彩内容

  • 目录 一、概述 二、关键词,非保留关键字和保留关键字 三、创建/删除/修改/使用数据库3.1、 创建数据库3.2、...
    三分清醒阅读 611评论 0 1
  • 前端开发面试题 <a name='preface'>前言</a> 只看问题点这里 看全部问题和答案点这里 本文由我...
    自you是敏感词阅读 548评论 0 3
  • <a name='html'>HTML</a> Doctype作用?标准模式与兼容模式各有什么区别? (1)、<...
    clark124阅读 2,755评论 1 18
  • 在线阅读 http://interview.poetries.top[http://interview.poetr...
    程序员小月阅读 100,881评论 23 444
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 5,390评论 16 21
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 7,964评论 0 9
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 1,488评论 1 1
  • 在妖界我有个名头叫胡百晓,无论是何事,只要找到胡百晓即可有解决的办法。因为是只狐狸大家以讹传讹叫我“倾城百晓”,...
    猫九0110阅读 1,426评论 2 3