简介
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文件,如:
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
作者期望通过:
- 仅仅position信息
- position + 文本信息
- 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的样例如图:
是不是像模像样了?
通过GCN生成边索引的局限性
因为edge_index是通过GCN的KNNGraph得到的,表格中边的覆盖率取决于K的设定,默认值为6,但是对于较大的表,邻近单元格可能都不在边索引映射中,使用时一定要小心。。。
GCN训练的效果
经过对GFTE代码的复现之后,通过GCN进行训练,发现每次训练几乎所有预测都集中在最多分类的边上,比如预测为0的边有10000条,预测为1的边有300条,那么预测值就都是0,这导致了看起来准确率很高,但是完全不可用的情况。
不清楚是否是对GCN理解不深刻,但是对GFTE的复现是如此。
有待之后进一步确认