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的复现是如此。
有待之后进一步确认

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容