更新:非常抱歉,经过交流讨论,发现一处我的代码错误,Merge 模型中最前面应该加上对应的 SFCN,我写了 SFCN 但是却没加入到 Merge 模型中。
更新:我自己用 tf 实现的 SPLERGE 代码已开源在 github 和 gitee,因为数据很少,模型效果是 over-fitting 的,仅供学习。
SPLERGE
之前有写过 2019 年 ICDAR 表格结构识别最佳论文 Deep Splitting and Merging for Table Structure Decomposition 的介绍,也有一个野生 torch 版本的项目实现,我自己用 tf/keras 写过 split 阶段的复现,整理了一下。merge 部分的在下篇中写。
针对 split 阶段,整个过程不外乎可以分为数据准备、模型搭建、模型训练和模型评估,如果模型评估结果不尽人意或者希望能更上一层,就要复盘整个过程中哪里还有改进点。
数据准备
使用的数据就是 ICDAR 2013 年和 2019 年的表格数据(提取码: vwwf),其中的每份数据都包含对应的 pdf、csv、xls、reg.xml 和 str.xml 几个文件。
在结构识别任务中,以表格图片信息作为输入,输出预测的横纵分隔线,所以只需要用 pdf、reg 和 str 三个文件信息就可以了。每个 pdf 包含一页或多页,在其中一页或多页中存在一个或多个表格,首先将每个 pdf 文件的每一页都转为图片,再根据 reg 文件中的信息裁剪获得表格区域图片,str 文件中包含每个字块的信息,结合这几部分信息就可以得到 split 的训练数据。
数据量不是很多,一共 248 个表格, 这里是将表格图片和对应行列的分隔信息写进了 tfrecords 用来训练。
备注:
(1)原始表格数据中表格位置和字块位置的纵向坐标都是上下倒置的,需要转换一下。
(2)有一处错误,icdar2013-competition-dataset-with-gt/competition-dataset-us/us-018-reg.xml 文件中 26ß(经过查看,应该是260)。
(3)有关 str 文件中 merge 相关的信息(虽然现在重点在说 split 部分,但还是先记录下来),标注标准不太一致。对于不跨行或者不跨列的说明,部分写法只写 start 不写end,部分写法是 start 和 end 都指向一行/列,需要注意下。
问题:
(1)有关 pdf 转 png,安装 fitz 和 PyMuPDF,以往装包还真没遇到过 pip 版本问题的,这是第一次= =,升级 pip 就好了。
(2)一直以为 os.path.join(s1, s2) 可以解决所有的路径拼接问题,偶然发现 s2 必须不能以斜杠开头,否则并不会把两个路径拼接出来。
模型搭建
原论文介绍的很清楚,一共三个大组件分别是 SFCN、RPN 和 CPN,其中 RPN 和 CPN 某种意义上对称,都由 5 个大体相同的 Block 组成。
# SFCN
class SFCN(tf.keras.Model):
def __init__(self):
super().__init__()
cnn = tf.keras.Sequential()
dilation = [1, 1, 2]
for i in range(3):
cnn.add(tf.keras.layers.Conv2D(filters=18, kernel_size=7, padding='same', kernel_initializer=kernel_init, activation='relu', dilation_rate=dilation[i]))
self.cnn = cnn
def call(self, input):
output = self.cnn(input)
return output
# Block
class Block(tf.keras.Model):
def __init__(self, block_num, mode=None):
super().__init__()
self.block_num = block_num # start from index 1
self.mode = mode
self.conv1 = tf.keras.layers.Conv2D(filters=6, kernel_size=3, padding='same', kernel_initializer=kernel_init, activation='relu', dilation_rate=2)
self.conv2 = tf.keras.layers.Conv2D(filters=6, kernel_size=3, padding='same', kernel_initializer=kernel_init, activation='relu', dilation_rate=3)