文档结构化场景(文档比对)

Posted by mjTree on July 30, 2024

更新于:2024-08-15 16:00

一、业务场景介绍

概念介绍

文本比对又称为文本比较或差异分析,是识别和展示两个文本文件之间差异的过程。这种技术在软件开发(代码审查)、法律文件修订、学术研究等多个领域都有应用,而本节要说的文档比对则是熟悉文本比对的范畴。

背景来源

在日常合同审核和对比工作中,密集的人力校正工作往往耗费大量资源,导致对比效率低下,管理风险较高。随着业务量的快速增长,合同对比工作量逐年增加。合同签署过程中,通常涉及条款的多次磋商更改以及异地签署等情况。各类合同的法律条款通常较为严谨,客户通常会自行打印并盖章。但是,目前的合同审核工作依然由法务人员进行人工校验比对,逐页、逐行、逐字比对,耗费大量人力资源,导致效率低下。人工审核对法务人员的精力投入要求较高,容易受审核人员的业务素养、体力、精神状态等因素影响而出现错误。一旦审核疏漏,可能导致巨大损失。

为了解决这一问题,合同比对系统利用机器视觉智能识别输入的两份合同,并自动标注前后合同的差异,实现计算机替代人工肉眼审核比对。该系统能够解决合同比对工作中的时间成本高、人力成本高和风险高等难题,显著提高工作效率,减少人为错误。

业务价值

1. 效率增强:利用先进的自动识别和比对技术,系统迅速准确地标出合同差异,极大提高了审核流程的速度。
2. 风险管理优化:软件的辅助比对与专业人工审核相结合,形成了一道坚固的错误防范屏障,降低了业务风险并避免了可能的重大损失。
3. 人力资源升级:释放员工从重复性体力劳动中,赋予他们机会去执行更具创造性和战略性的任务,为企业带来更高的价值增长。

这种转变不仅优化了工作流程,还为员工提供了更广阔的发展空间,同时确保了企业运营的稳健性和竞争力。

二、文本语义分析内容差异

基于文档结构化的解析结果以及上下文语义分析技术进行特定文本内容的定位与分析,通过文本分词、语义匹配等方式进行文本内容的比对,输出文本差异性分析结果。最终实现跟踪项目建设内容、项目实施内容在项目不同阶段中的变化,检测项目内容变化可能带来的项目建设结果所带来的风险。

文本内容差异性分析能够解决传统文本比对仅从字面差异进行比对,无法从语义上等其他方内容进行差异分析。对于各行业领域文档的比对,为了能够给业务人员带来实用、有效的产出,则必须引入业务知识、语义信息、语义匹配等多维度内容。因此智能化的文本比对需要具备:

1. 文档结构化:文档包含了丰富的语义信息,主要以标题、目录、段落、页眉、页脚、表格为主。基于文档的结构化信息,可以更高效地对文档比对(基于段落搜索文档相同的部分,可剔除页眉页脚比对,可对表格数据进行结构化比对)。
2. 不同层次的文本语义匹配:区别于简单的文本字面比对,智能化的文档比对需要具备语义匹配的能力,如“NLP”和“自然语言处理”。语义匹配也涉及到不同层次或粒度,如词语级别、语句级别、段落级别等。具体实现需要中文分词、word embedding、同义词词典、基于BERT的句子/段落相似度计算。
3. 表格/段落元素比对:在有些业务场景下,主要关注两篇文档的具体差异,而对段落顺序差异可以不考虑。这需要比对算法可以自动检测匹配的段落并调整成一致的顺序,再进行比对。另一种常见的场景是两篇文档有一部分基本相同,需要在这一部分中进行比对。同样地,这种情况也需要先匹配相似段落。

三、算法实现路线

基于文档结构化的信息,使用哈希方法给文档中的元素(表格、段落、标题、目录等)构建一个唯一的哈希码,再结合元素的类型、内容、坐标、字体信息等计算出最相似的(段落/标题/表格)文本元素对。下面将介绍三种序列比对算法,常文本差异性比较,代码均由chat-gpt-4o协助生成。

1. LCS

最长公共子序列(Longest Common Subsequence,简称LCS)算法是一种用于寻找两个或多个序列中最长公共子序列的动态规划算法。

def lcs_matrix(s1, s2):
    """构建LCS矩阵"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp


def find_operations(s1, s2):
    """回溯LCS矩阵,找出转换操作"""
    dp = lcs_matrix(s1, s2)
    operations = []
    i, j = len(s1), len(s2)
    while i > 0 and j > 0:
        if s1[i - 1] == s2[j - 1]:
            operations.append(f"Move '{s1[i - 1]}'")
            i -= 1
            j -= 1
        elif dp[i - 1][j] >= dp[i][j - 1]:
            operations.append(f"Delete '{s1[i - 1]}'")
            i -= 1
        else:
            operations.append(f"Insert '{s2[j - 1]}'")
            j -= 1
    """处理剩余的删除操作"""
    while i > 0:
        operations.append(f"Delete '{s1[i - 1]}'")
        i -= 1
    """处理剩余的新增操作"""
    while j > 0:
        operations.append(f"Insert '{s2[j - 1]}'")
        j -= 1
    return operations[::-1]


def calculate_similarity(s1, s2):
	"""基于编辑距离的相似度计算"""
    operations = find_operations(s1, s2)
    edit_distance = len([op for op in operations if 'Move' not in op])
    max_length = max(len(s1), len(s2))
    similarity = 1 - edit_distance / max_length
    return similarity


s1 = "123456789qwertyuio"
s2 = "213456789qwertyuio"
similarity = calculate_similarity(s1, s2)
print(f"Similarity: {similarity:.2f}")

2. Needleman Wunsch

Needleman-Wunsch(NW)算法是一种全局的序列比对算法,广泛应用于生物信息学领域,用于计算两个生物序列之间的相似性。

def needleman_wunsch(s1, s2, match=1, mismatch=-1, gap=-1):
    """构建Needleman-Wunsch矩阵"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        dp[i][0] = i * gap
    for j in range(1, n + 1):
        dp[0][j] = j * gap
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                score = match
            else:
                score = mismatch
            dp[i][j] = max(dp[i - 1][j - 1] + score, dp[i - 1][j] + gap, dp[i][j - 1] + gap)
    return dp


def find_operations_nw(s1, s2, match=1, mismatch=-1, gap=-1):
    """回溯Needleman-Wunsch矩阵,找出转换操作"""
    dp = needleman_wunsch(s1, s2, match, mismatch, gap)
    operations = []
    i, j = len(s1), len(s2)
    while i > 0 and j > 0:
        if s1[i - 1] == s2[j - 1]:
            operations.append(f"Move '{s1[i - 1]}'")
            i -= 1
            j -= 1
        elif dp[i][j] == dp[i - 1][j - 1] + (match if s1[i - 1] == s2[j - 1] else mismatch):
            operations.append(f"Replace '{s1[i - 1]}' with '{s2[j - 1]}'")
            i -= 1
            j -= 1
        elif dp[i][j] == dp[i - 1][j] + gap:
            operations.append(f"Delete '{s1[i - 1]}'")
            i -= 1
        else:
            operations.append(f"Insert '{s2[j - 1]}'")
            j -= 1
    """处理剩余的删除操作"""
    while i > 0:
        operations.append(f"Delete '{s1[i - 1]}'")
        i -= 1
    """处理剩余的新增操作"""
    while j > 0:
        operations.append(f"Insert '{s2[j - 1]}'")
        j -= 1
    return operations[::-1]


def calculate_similarity_nw(s1, s2, match=1, mismatch=-1, gap=-1):
    operations = find_operations_nw(s1, s2, match, mismatch, gap)
    edit_distance = len([op for op in operations if 'Move' not in op])
    max_length = max(len(s1), len(s2))
    similarity = 1 - edit_distance / max_length
    return similarity


s1 = "123456789qwertyuio"
s2 = "213456789qwertyuio"
similarity = calculate_similarity_nw(s1, s2)
for op in find_operations_nw(s1, s2):
    print(op)
print(f"Similarity: {similarity:.2f}")


3. myers

Myers算法是用于计算两个序列(如字符串或数组)之间的差异的算法(动态规划算法),可以高效地找到两个序列之间的最小编辑距离,即通过插入、删除或替换操作将一个序列转换为另一个序列所需的最小操作数。

def myers_diff(source, target):
    m, n = len(source), len(target)
    D = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        D[i][0] = i
    for j in range(1, n + 1):
        D[0][j] = j
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if source[i - 1] == target[j - 1]:
                D[i][j] = D[i - 1][j - 1]
            else:
                D[i][j] = min(D[i - 1][j] + 1, D[i][j - 1] + 1, D[i - 1][j - 1] + 1)
    """回溯编辑操作"""
    i, j = m, n
    operations = []
    while i > 0 or j > 0:
        if i > 0 and j > 0 and D[i][j] == D[i - 1][j - 1] and source[i - 1] == target[j - 1]:
            operations.append(f"Keep '{source[i - 1]}'")
            i -= 1
            j -= 1
        elif i > 0 and D[i][j] == D[i - 1][j] + 1:
            operations.append(f"Delete '{source[i - 1]}'")
            i -= 1
        elif j > 0 and D[i][j] == D[i][j - 1] + 1:
            operations.append(f"Insert '{target[j - 1]}'")
            j -= 1
        else:
            operations.append(f"Replace '{source[i - 1]}' with '{target[j - 1]}'")
            i -= 1
            j -= 1
    operations.reverse()
    edit_distance = D[m][n]
    similarity = 1 - edit_distance / max(m, n)
    return operations, similarity


source = "abcde"
target = "acbed"
operations, similarity = myers_diff(source, target)
print("Operations:", operations)
print("Similarity:", similarity)

上面计算文本相似度采用了编辑距离公式计算,实际过程中需要使用其他方式来计算相似度。

  1. 其他的相似度计算公式,找出全部的公共子串长度,然后乘于2,再除于源字符串与目标字符串的长度和。
  2. 快速计算相似度,就是找出每个字符在源字符串和目标字符串中出现的较小次数之和,然后乘于2,再除于源字符串与目标字符串的长度和。
  3. 当文本是扫描件的时候,因样本问题或模型问题出现字符识别错误,需要忽略一些单字符差异再计算相似度。

四、比对产品调研

国内厂商有(排名不分先后):科大讯飞、合合、庖丁、达观、法狗狗、易道博识、秀合同、犀语、译图智讯、唯你(账e捷)、幂律智能、智合同等等。ToB 和 ToC 都有,需要体验的可自行网络查询相应产品申请账号。技术路线有基于文档结构化再进行比对,有的则是直接读取文本信息直接比对,具体需实际体验感知。

由于比对的技术门槛较低,国内做比对产品的厂商有很多。如果自己想做文档比对的话,就需要有强硬的技术支持,如文档结构化技术和字符识别技术,提高比对精度(特别是银行和证劵公司的扫描件类型)以及泛化度(研报等类型的复杂版面文件)。除了技术之外,拓展更多格式的文件有利于提升产品在项目竞标时的竞争力,因为各家比对功能主要支持的文档格式为 doc、docx、pdf 这些常见的办公文件,其他格式相对较少。最后需要从产品自身的角度考虑(使用门槛降低、减少用户学习成本以及提高用户体验感等)以及对购买产品的用户,对用户来说更重要的是比对效果和结果展示交互。

对于比对效果的提升,除了上面提到的技术(文档结构化和字符识别)要持续优化,可以尝试把比对的方向场景化、垂直化,从某一类的文档类型着手。比如常见的合同,这类的文档类型版式相对比较固定,不容易出现缺线表格,而且页眉页脚比较规范,文档解析从文档类型下手会相对容易些,然后后期拓展更多的文档类型来提升产品可用性。对于比对结果,在扫描件的比对场景中,易出现样本质量差导致字符识别效果不理想,出现大量差异提示(极度不友好),此时比对算法是否要侧重召回;比对结果在前端上展示的差异出现大范围跨页场景;客户购买比对产品后需要获取比对结果在自己客户端展示(部署定制成本);交互方面能否条件过滤差异,人工审核完是否二次检查等等。