整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

录取查询页面怎么做?

录取查询页面怎么做?

生录取工作开始后,负责招生的老师需要完成一系列任务,其中包括确定招生录取名单和及时向考生公布录取情况。为了快速完成录取查询工作的发布,招生老师们可以采取以下步骤:

1. 整理录取名单:招生老师们首先需要整理好录取名单,确保其中包含了所有被录取的考生的相关信息,如姓名、学号、专业等。他们可以通过学校的招生系统或数据库来获取这些信息。

2. 准备查询页面:招生老师们可以使用前面提到的创建录取查询页面的步骤来准备一个查询页面。在该页面上,他们可以设置一个查询表单,要求考生输入自己的相关信息,如姓名、学号等,然后点击查询按钮。这个页面可以利用易查分轻松实现,不需要任何费用,只需把录取名单制作成Excel表格,导入易查分,就可以生成功能强大的录取查询页面,考生输入查询条件,即可自己查录取信息。


3. 导入录取名单:招生老师们可以将整理好的录取名单导入到查询页面的后端数据库中。这样,当考生输入查询条件后,系统可以根据录取名单进行查询,快速返回相应的录取情况。

4. 测试查询功能:在发布查询页面之前,招生老师们应该进行一次测试,确保查询功能正常运行。他们可以模拟考生输入不同的查询条件,检查系统是否能够正确地返回相应的录取情况。

5. 发布查询页面:一旦测试通过,招生老师们可以将查询页面链接发布给考生。他们可以通过学校的官方网站、招生宣传渠道或邮件等方式,将查询页面的链接提供给考生和家长。

6. 监控查询情况:在查询页面发布后,招生老师们应该密切关注查询情况。他们可以设置一些监控机制,如记录查询次数和查询结果等,以便及时发现和解决可能出现的问题。

通过以上步骤,招生老师们可以快速完成录取查询工作的发布。这样,考生和家长可以方便地查询自己的录取情况,同时也减轻了招生老师们的工作负担。


除此之外,老师们还可以通过以下传统的制作录取查询页面的步骤,来制作一个录取查询页面:

1. 设计页面布局:确定页面的整体结构和布局,包括标题、查询表单和查询结果显示区域等。考虑使用响应式设计,以适应不同设备的屏幕大小。

2. 创建查询表单:在页面上添加一个查询表单,包括学生的相关信息,如姓名、学号、身份证号等。可以使用HTML表单元素,如输入框、下拉菜单和复选框等,来收集用户输入的查询条件。

3. 验证查询条件:在后端或前端进行查询条件的验证,确保输入的信息格式正确和完整。可以使用正则表达式或其他验证方法来检查输入的学号、身份证号等是否符合规定的格式。

4. 进行查询操作:在后端处理查询请求,根据用户输入的条件,在数据库中进行查询操作。根据查询结果,可以返回相应的录取信息或错误提示。

5. 显示查询结果:将查询结果显示在页面上,可以使用表格或列表等形式展示。如果查询结果为空,可以显示相应的提示信息。

6. 添加样式和交互效果:为查询页面添加适当的样式和交互效果,以提升用户体验。可以使用CSS来美化页面,如设置背景颜色、字体样式和按钮样式等。可以使用JavaScript或其他前端框架,实现一些交互效果,如实时搜索和动态加载查询结果等。

7. 测试和优化:进行页面的测试,确保查询功能正常运行,并检查页面在不同浏览器和设备上的兼容性。根据用户的反馈和测试结果,进行必要的优化和改进。

8. 部署和发布:将查询页面部署到服务器上,并将查询页面的链接提供给学生和家长。确保服务器的稳定性和安全性,以保护学生信息的机密性。

以上是创建一个录取查询页面的一般步骤,具体实现可以根据需求和技术要求进行调整和修改。

施 RAG 是一个挑战,特别是在有效解析和理解非结构化文档中的表格时。这对于扫描文档或图像格式的文档尤其困难。这些挑战至少有三个方面:

  • 扫描文档或图像文档的复杂性,例如其不同的结构、包含的非文本元素以及手写和打印内容的结合,给自动准确提取表格信息带来了挑战。不准确的解析会损坏表结构,并且使用不完整的表进行嵌入不仅无法捕获表的语义信息,而且还很容易破坏RAG结果。
  • 如何提取表格标题并将其有效链接到各自的表格。
  • 如何设计索引结构来有效存储表的语义信息。

本文首先介绍RAG中管理表的关键技术。然后,在提出和实施新的解决方案之前,它会审查一些现有的开源解决方案。

关键技术

表解析

该模块的主要功能是从非结构化文档或图像中准确提取表结构。

附加功能:最好能提取对应的表格标题,方便开发者将表格标题与表格关联起来。

根据我目前的理解,有以下几种方法,如图1所示:

图 1:表解析器。图片由作者提供。

(a) 利用多模态 LLM(例如GPT-4V)来识别表格并从每个 PDF 页面中提取信息。

  • 输入:图像格式的 PDF 页面
  • 输出:JSON 或其他格式的表。如果多模态 LLM 无法提取表数据,它应该总结图像并返回摘要。

(b)利用专业的表检测模型,如Table Transformer,来识别表结构。

  • 输入:PDF 页面作为图像
  • 输出:表格作为图像

(c)使用开源框架,例如非结构化框架和其他框架,它们也采用对象检测模型(本文详细介绍了非结构化的表检测过程)。这些框架允许对整个文档进行全面解析,并从解析结果中提取与表格相关的内容。

  • 输入:PDF或图像格式的文档
  • 输出:纯文本或HTML格式的表格,从整个文档的解析结果得到

(d) 使用 Nougat、Donut等端到端模型来解析整个文档并提取与表相关的内容。此方法不需要 OCR 模型。

  • 输入:PDF或图像格式的文档
  • 输出:LaTeX或JSON格式的表格,从整个文档的解析结果获得

值得一提的是,无论采用何种方法提取表格信息,都应包含表格标题。这是因为在大多数情况下,表格标题是文档或论文作者对表格的简要描述,可以很大程度上概括整个表格。

在上述四种方法中,方法(d)可以轻松检索表格标题。这对开发人员来说是有益的,因为它允许他们将表格标题与表格关联起来。这将在下面的实验中进一步解释。

指数结构

根据索引的结构,解决方案大致可以分为以下几类:

(e) 仅以图像格式索引表。

(f)仅索引纯文本或 JSON 格式的表。

(g)仅索引 LaTeX 格式的表。

(h)只索引表的摘要。

(i)从小到大或文档摘要索引结构,如图2所示。

  • 小块的内容可以是来自表的每一行的信息或表的摘要。
  • 大块的内容可以是图像格式、纯文本格式或LaTeX格式的表格。

图 2:从小到大索引(上)和文档摘要索引(中和下)的结构。图片由作者提供。

如上所述,表格摘要通常是使用 LLM 生成的:

  • 输入:图像格式、文本格式或LaTeX格式的表格
  • 输出:表格摘要

不需要表解析、索引或 RAG 的算法

有些算法不需要表解析。

(j).将相关图像(PDF页面)和用户的查询发送到VQA模型(如DAN等)或多模态LLM,并返回答案。

  • 待索引内容:图像格式的文档
  • 发送到VQA模型或多模态LLM的内容:查询+图片形式的对应页面

(k)将相关文本格式的PDF页面和用户的查询发送给LLM,然后返回答案。

  • 要索引的内容:文本格式的文档
  • 发送给LLM的内容:查询+文本格式对应页面

(l)相关图像(PDF页面)、文本块和用户查询发送到多模态LLM(如GPT-4V等)并直接返回答案。

  • 要索引的内容:图像格式的文档和文本格式的文档块
  • 发送到多模态LLM的内容:查询+文档对应的图像形式+对应的文本块

另外,下面是一些不需要索引的方法,如图3和图4所示:

图 3:类别 (m)。图片由作者提供。

(m)首先,应用类别(a)至(d)中的一种方法,将文档中的所有表格解析为图像形式。然后直接将所有表格图像和用户的查询发送到多模态LLM(如GPT-4V等)并返回答案。

  • 要索引的内容:无
  • 发送到多模式LLM的内容:查询+所有解析的表(图像格式)

图 4:类别 (n)。图片由作者提供。

(n)使用(m)提取的图像格式的表格,然后使用OCR模型识别表格中的所有文本,然后将表格中的所有文本和用户的查询直接发送给LLM并直接返回答案。

  • 要索引的内容:无
  • 发送给LLM的内容:用户查询+所有表内容(文本格式)

值得注意的是,有些方法不依赖于 RAG 过程:

  • 第一种方法不使用 LLM,在特定数据集上进行训练,并使模型(例如类似 BERT 的转换器)能够更好地支持表理解任务,例如TAPAS。
  • 第二种方法使用LLM,采用预训练、微调方法或提示,使LLM能够执行表理解任务,例如GPT4Table。

现有的开源解决方案

上一节对 RAG 中表的关键技术进行了总结和分类。在提出本文实现的解决方案之前,让我们先探索一些开源解决方案。

LlamaIndex 提出了四种方法,前三种使用多模态模型。

  1. 检索相关图像(PDF 页面)并将其发送到 GPT-4V 以响应查询。
  2. 将每个PDF页面视为一个图像,让GPT-4V对每个页面进行图像推理。为图像推理构建文本向量存储索引。针对图像推理向量存储查询答案。
  3. 使用 Table Transformer 从检索到的图像中裁剪表格信息,然后将这些裁剪后的图像发送到 GPT-4V 以进行查询响应。
  4. 对裁剪后的表格图像应用 OCR 并将数据发送到 GPT4/GPT-3.5 以回答查询。

按照本文的分类:

  • 第一种方法与本文中的类别(j)类似,不需要表解析。然而,结果表明,即使答案在图像中,也无法产生正确的答案。
  • 第二种方法涉及表解析,对应类别(a)。索引内容是表格内容或摘要,基于 GPT-4V 返回的结果,可能对应于类别 (f) 或 (h)。此方法的缺点是 GPT-4V 识别表格并从图像中提取其内容的能力不稳定,特别是当图像包含表格、文本和其他图像的混合时(这在 PDF 格式中很常见)。
  • 第三种方法与类别 (m)类似,不需要索引。
  • 第四种方法与category (n)类似,也不需要索引。其结果表明,由于无法从图像中提取表格信息,因此产生了错误的答案。

经过测试,发现第三种方法总体效果最好。然而,根据我的测试,第三种方法很难检测表格,更不用说正确地将表格标题与表格合并了。

Langchain也提出了一些解决方案,半结构化RAG的关键技术包括:

  • 表解析使用非结构化,属于类别 (c)
  • 索引方式为文档摘要索引,属于类别(i),小块内容:表格摘要,大块内容:原始表格内容(文本格式)。

如图5所示:

图 5:Langchain 的半结构化 RAG。来源:半结构化 RAG

半结构化和多模态 RAG提出了三种解决方案,其架构如图 6 所示。

图 6:Langchain 的半结构化和多模式 RAG。来源:半结构化和多模式 RAG。

选项 1与本文的类别 (l)类似。它涉及使用多模态嵌入(例如CLIP)来嵌入图像和文本,使用相似性搜索检索两者,并将原始图像和块传递到多模态 LLM 进行答案合成。

选项 2利用多模态 LLM(例如GPT-4V、LLaVA或FUYU-8b)从图像生成文本摘要。然后,嵌入和检索文本,并将文本块传递给 LLM 进行答案合成。

  • 表解析使用非结构化,即类别 (d)
  • 索引结构为文档摘要索引(catogery(i)),小块内容:表格摘要,大块内容:文本格式的表格

选项 3使用多模态 LLM(例如GPT-4V、LLaVA或FUYU-8b)从图像生成文本摘要,然后参考原始图像(类别 (i))嵌入和检索图像摘要,然后传递原始图像将图像和文本块传输到多模式法学硕士以进行答案合成。

建议的解决方案

本文对关键技术和现有解决方案进行了总结、分类和讨论。基于此,我们提出了以下解决方案,如图 7 所示。为了简单起见,省略了一些 RAG 模块,例如重新排名和查询重写。

图 7:本文建议的解决方案。图片由作者提供。

  • 表解析:使用Nougat(类别(d))。根据我的测试,它的表检测比非结构化(类别(c))更有效。另外,Nougat可以很好地提取表格标题,非常方便与表格关联。
  • 文档摘要索引结构(类别(i)):小块的内容包括表格摘要,大块的内容包括LaTeX格式的相应表格和文本格式的表格标题。我们使用多向量检索器来实现它。
  • 表格摘要获取方法:将表格和表格标题发送给LLM进行摘要。

这种方法的优点是能够高效地解析表格,同时综合考虑表格汇总和表格之间的关系。它还消除了对多模态LLM的需求,从而节省了成本。

Nougat的原理

Nougat是基于Donut架构开发的。它通过网络隐式识别文本,无需任何 OCR 相关输入或模块,如图 8 所示。

图 8:遵循Donut 的端到端架构。 Swin Transformer 编码器获取文档图像并将其转换为潜在嵌入,随后以自回归方式将其转换为标记序列。资料来源:Nougat:学术文档的神经光学理解。

Nougat 解析公式的能力令人印象深刻。它在解析表方面也很出色。它可以方便地关联表格标题,如图 9 所示:

图9:Nougat运行结果,结果文件为Mathpix Markdown格式(通过vscode插件打开),表格为LaTeX格式。

在我对十几篇论文的测试中,我发现表格标题总是固定在表格后面的行上。这种一致性表明这并非偶然。因此,我们有兴趣了解Nougat是如何实现这种效果的。

鉴于它是一个缺乏中间结果的端到端模型,它可能严重依赖于其训练数据。

根据格式化训练数据的代码,对于 table 来说,紧接着的一行\end{table}caption_parts,这看起来与提供的训练数据的格式一致:

def format_element(
    element: Element, keep_refs: bool=False, latex_env: bool=False
) -> List[str]:
    """
    Formats a given Element into a list of formatted strings.

    Args:
        element (Element): The element to be formatted.
        keep_refs (bool, optional): Whether to keep references in the formatting. Default is False.
        latex_env (bool, optional): Whether to use LaTeX environment formatting. Default is False.

    Returns:
        List[str]: A list of formatted strings representing the formatted element.
    """
    ...
    ...
    if isinstance(element, Table):
        parts=[
            "[TABLE%s]\n\\begin{table}\n"
            % (str(uuid4())[:5] if element.id is None else ":" + str(element.id))
        ]
        parts.extend(format_children(element, keep_refs, latex_env))
        caption_parts=format_element(element.caption, keep_refs, latex_env)
        remove_trailing_whitespace(caption_parts)
        parts.append("\\end{table}\n")
        if len(caption_parts) > 0:
            parts.extend(caption_parts + ["\n"])
        parts.append("[ENDTABLE]\n\n")
        return parts
    ...
    ...

Nougat的优点和缺点

优点:

  • Nougat 可以准确地将公式和表格等对以前的解析工具来说具有挑战性的部分解析为 LaTeX 源代码。
  • Nougat的解析结果是类似于markdown的半结构化文档。
  • 轻松获取表格标题并将其与表格方便地关联起来。

缺点:

  • Nougat的解析速度较慢,这对于大规模部署来说可能是一个挑战。
  • 由于 Nougat 接受过科学论文方面的培训,因此它擅长处理类似结构的文档。对于非拉丁文本文档,其性能会下降。
  • Nougat 模型一次仅在科学论文的一页上进行训练,缺乏对其他页面的了解。这可能会导致解析的内容出现一些不一致。因此,如果识别效果不好,可以考虑将PDF分成单独的页面,然后一页一页地进行解析。
  • 解析两列论文中的表格不如单列论文中的有效。

代码实现

首先,安装相关Python包

pip install langchain
pip install chromadb
pip install nougat-ocr

安装完成后,我们可以查看Python包的版本:

langchain                                 0.1 .12
 langchain-community                       0.0 .28
 langchain-core                            0.1 .31
 langchain-openai                          0.0 .8
 langchain-text-splitters                  0.0 .1

 chroma-hnswlib                            0.7 .3
 chromadb                                  0.4 .24

 nougat-ocr                                0.1 .17

设置环境并导入:

import os
os.environ["OPENAI_API_KEY"]="YOUR_OPEN_AI_KEY"

import subprocess
import uuid

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_core.runnables import RunnablePassthrough

下载《Attention Is All You Need to》论文YOUR_PDF_PATH,运行nougat解析PDF文件,从解析结果中获取latex格式的表格和文本格式的表格标题。第一次执行将下载必要的模型文件。

def june_run_nougat(file_path, output_dir):
    # Run Nougat and store results as Mathpix Markdown
    cmd=["nougat", file_path, "-o", output_dir, "-m", "0.1.0-base", "--no-skipping"]
    res=subprocess.run(cmd) 
    if res.returncode !=0:
        print("Error when running nougat.")
        return res.returncode
    else:
        print("Operation Completed!")
        return 0

def june_get_tables_from_mmd(mmd_path):
    f=open(mmd_path)
    lines=f.readlines()
    res=[]
    tmp=[]
    flag=""
    for line in lines:
        if line=="\\begin{table}\n":
            flag="BEGINTABLE"
        elif line=="\\end{table}\n":
            flag="ENDTABLE"
        
        if flag=="BEGINTABLE":
            tmp.append(line)
        elif flag=="ENDTABLE":
            tmp.append(line)
            flag="CAPTION"
        elif flag=="CAPTION":
            tmp.append(line)
            flag="MARKDOWN"
            print('-' * 100)
            print(''.join(tmp))
            res.append(''.join(tmp))
            tmp=[]

    return res

file_path="YOUR_PDF_PATH"
output_dir="YOUR_OUTPUT_DIR_PATH"

if june_run_nougat(file_path, output_dir)==1:
    import sys
    sys.exit(1)

mmd_path=output_dir + '/' + os.path.splitext(file_path)[0].split('/')[-1] + ".mmd" 
tables=june_get_tables_from_mmd(mmd_path)

该函数june_get_tables_from_mmd用于从图 10 所示的文件中提取从 t\begin{table}到 的所有内容\end{table},包括 后面的行。\end{table}mmd

图10:Nougat运行结果,结果文件为Mathpix Markdown格式(通过vscode插件打开),表格为latex格式。The function of june_get_tables_from_mmd is to extract the table information in the red box图片由作者提供。

值得注意的是,目前还没有找到官方文档规定表格标题必须放在表格下方或者表格以 开头\begin{table}和结尾\end{table}。因此,june_get_tables_from_mmd是启发式的。

以下是解析 PDF 中表格的结果:

Operation Completed!
----------------------------------------------------------------------------------------------------
\begin{table}
\begin{tabular}{l c c c} \hline \hline Layer Type & Complexity per Layer & Sequential Operations & Maximum Path Length \\ \hline Self-Attention & \(O(n^{2}\cdot d)\) & \(O(1)\) & \(O(1)\) \\ Recurrent & \(O(n\cdot d^{2})\) & \(O(n)\) & \(O(n)\) \\ Convolutional & \(O(k\cdot n\cdot d^{2})\) & \(O(1)\) & \(O(log_{k}(n))\) \\ Self-Attention (restricted) & \(O(r\cdot n\cdot d)\) & \(O(1)\) & \(O(n/r)\) \\ \hline \hline \end{tabular}
\end{table}
Table 1: Maximum path lengths, per-layer complexity and minimum number of sequential operations for different layer types. \(n\) is the sequence length, \(d\) is the representation dimension, \(k\) is the kernel size of convolutions and \(r\) the size of the neighborhood in restricted self-attention.

----------------------------------------------------------------------------------------------------
\begin{table}
\begin{tabular}{l c c c c} \hline \hline \multirow{2}{*}{Model} & \multicolumn{2}{c}{BLEU} & \multicolumn{2}{c}{Training Cost (FLOPs)} \\ \cline{2-5}  & EN-DE & EN-FR & EN-DE & EN-FR \\ \hline ByteNet [18] & 23.75 & & & \\ Deep-Att + PosUnk [39] & & 39.2 & & \(1.0\cdot 10^{20}\) \\ GNMT + RL [38] & 24.6 & 39.92 & \(2.3\cdot 10^{19}\) & \(1.4\cdot 10^{20}\) \\ ConvS2S [9] & 25.16 & 40.46 & \(9.6\cdot 10^{18}\) & \(1.5\cdot 10^{20}\) \\ MoE [32] & 26.03 & 40.56 & \(2.0\cdot 10^{19}\) & \(1.2\cdot 10^{20}\) \\ \hline Deep-Att + PosUnk Ensemble [39] & & 40.4 & & \(8.0\cdot 10^{20}\) \\ GNMT + RL Ensemble [38] & 26.30 & 41.16 & \(1.8\cdot 10^{20}\) & \(1.1\cdot 10^{21}\) \\ ConvS2S Ensemble [9] & 26.36 & **41.29** & \(7.7\cdot 10^{19}\) & \(1.2\cdot 10^{21}\) \\ \hline Transformer (base model) & 27.3 & 38.1 & & \(\mathbf{3.3\cdot 10^{18}}\) \\ Transformer (big) & **28.4** & **41.8** & & \(2.3\cdot 10^{19}\) \\ \hline \hline \end{tabular}
\end{table}
Table 2: The Transformer achieves better BLEU scores than previous state-of-the-art models on the English-to-German and English-to-French newstest2014 tests at a fraction of the training cost.

----------------------------------------------------------------------------------------------------
\begin{table}
\begin{tabular}{c|c c c c c c c c|c c c c} \hline \hline  & \(N\) & \(d_{\text{model}}\) & \(d_{\text{ff}}\) & \(h\) & \(d_{k}\) & \(d_{v}\) & \(P_{drop}\) & \(\epsilon_{ls}\) & train steps & PPL & BLEU & params \\ \hline base & 6 & 512 & 2048 & 8 & 64 & 64 & 0.1 & 0.1 & 100K & 4.92 & 25.8 & 65 \\ \hline \multirow{4}{*}{(A)} & \multicolumn{1}{c}{} & & 1 & 512 & 512 & & & & 5.29 & 24.9 & \\  & & & & 4 & 128 & 128 & & & & 5.00 & 25.5 & \\  & & & & 16 & 32 & 32 & & & & 4.91 & 25.8 & \\  & & & & 32 & 16 & 16 & & & & 5.01 & 25.4 & \\ \hline (B) & \multicolumn{1}{c}{} & & \multicolumn{1}{c}{} & & 16 & & & & & 5.16 & 25.1 & 58 \\  & & & & & 32 & & & & & 5.01 & 25.4 & 60 \\ \hline \multirow{4}{*}{(C)} & 2 & \multicolumn{1}{c}{} & & & & & & & & 6.11 & 23.7 & 36 \\  & 4 & & & & & & & & 5.19 & 25.3 & 50 \\  & 8 & & & & & & & & 4.88 & 25.5 & 80 \\  & & 256 & & 32 & 32 & & & & 5.75 & 24.5 & 28 \\  & 1024 & & 128 & 128 & & & & 4.66 & 26.0 & 168 \\  & & 1024 & & & & & & 5.12 & 25.4 & 53 \\  & & 4096 & & & & & & 4.75 & 26.2 & 90 \\ \hline \multirow{4}{*}{(D)} & \multicolumn{1}{c}{} & & & & & 0.0 & & 5.77 & 24.6 & \\  & & & & & & 0.2 & & 4.95 & 25.5 & \\  & & & & & & & 0.0 & 4.67 & 25.3 & \\  & & & & & & & 0.2 & 5.47 & 25.7 & \\ \hline (E) & \multicolumn{1}{c}{} & \multicolumn{1}{c}{} & & \multicolumn{1}{c}{} & & & & & 4.92 & 25.7 & \\ \hline big & 6 & 1024 & 4096 & 16 & & 0.3 & 300K & **4.33** & **26.4** & 213 \\ \hline \hline \end{tabular}
\end{table}
Table 3: Variations on the Transformer architecture. Unlisted values are identical to those of the base model. All metrics are on the English-to-German translation development set, newstest2013. Listed perplexities are per-wordpiece, according to our byte-pair encoding, and should not be compared to per-word perplexities.

----------------------------------------------------------------------------------------------------
\begin{table}
\begin{tabular}{c|c|c} \hline
**Parser** & **Training** & **WSJ 23 F1** \\ \hline Vinyals \& Kaiser et al. (2014) [37] & WSJ only, discriminative & 88.3 \\ Petrov et al. (2006) [29] & WSJ only, discriminative & 90.4 \\ Zhu et al. (2013) [40] & WSJ only, discriminative & 90.4 \\ Dyer et al. (2016) [8] & WSJ only, discriminative & 91.7 \\ \hline Transformer (4 layers) & WSJ only, discriminative & 91.3 \\ \hline Zhu et al. (2013) [40] & semi-supervised & 91.3 \\ Huang \& Harper (2009) [14] & semi-supervised & 91.3 \\ McClosky et al. (2006) [26] & semi-supervised & 92.1 \\ Vinyals \& Kaiser el al. (2014) [37] & semi-supervised & 92.1 \\ \hline Transformer (4 layers) & semi-supervised & 92.7 \\ \hline Luong et al. (2015) [23] & multi-task & 93.0 \\ Dyer et al. (2016) [8] & generative & 93.3 \\ \hline \end{tabular}
\end{table}
Table 4: The Transformer generalizes well to English constituency parsing (Results are on Section 23 of WSJ)* [5] Kyunghyun Cho, Bart van Merrienboer, Caglar Gulcehre, Fethi Bougares, Holger Schwenk, and Yoshua Bengio. Learning phrase representations using rnn encoder-decoder for statistical machine translation. _CoRR_, abs/1406.1078, 2014.

然后用LLM总结一下表格:

# Prompt
prompt_text="""You are an assistant tasked with summarizing tables and text. \ 
Give a concise summary of the table or text. The table is formatted in LaTeX, and its caption is in plain text format: {element}  """
prompt=ChatPromptTemplate.from_template(prompt_text)

# Summary chain
model=ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
summarize_chain={"element": lambda x: x} | prompt | model | StrOutputParser()
# Get table summaries
table_summaries=summarize_chain.batch(tables, {"max_concurrency": 5})
print(table_summaries)

以下是Attention Is All You Need中找到的四个表的摘要,如图 11 所示:

图 11: Attention Is All You Need中找到的四个表的表摘要。

使用Multi-Vector Retriever构建文档摘要索引结构。

# The vectorstore to use to index the child chunks
vectorstore=Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())

# The storage layer for the parent documents
store=InMemoryStore()
id_key="doc_id"

# The retriever (empty to start)
retriever=MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
    search_kwargs={"k": 1} # Solving Number of requested results 4 is greater than number of elements in index..., updating n_results=1
)

# Add tables
table_ids=[str(uuid.uuid4()) for _ in tables]
summary_tables=[
    Document(page_content=s, metadata={id_key: table_ids[i]})
    for i, s in enumerate(table_summaries)
]
retriever.vectorstore.add_documents(summary_tables)
retriever.docstore.mset(list(zip(table_ids, tables)))

一切准备就绪,构建一个简单的RAG管道,并执行查询:

# Prompt template
template="""Answer the question based only on the following context, which can include text and tables, there is a table in LaTeX format and a table caption in plain text format:
{context}
Question: {question}
"""
prompt=ChatPromptTemplate.from_template(template)

# LLM
model=ChatOpenAI(temperature=0, model="gpt-3.5-turbo")


# Simple RAG pipeline
chain=(
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)


print(chain.invoke("when layer type is Self-Attention, what is the Complexity per Layer?"))  # Query about table 1

print(chain.invoke("Which parser performs worst for BLEU EN-DE"))  # Query about table 2

print(chain.invoke("Which parser performs best for WSJ 23 F1"))  # Query about table 4

执行结果如下,说明几个问题都得到了准确的解答,如图12所示:

图 12:三个查询的结果。第一行对应Attention Is All You Need中的查询表1 ,第二行对应表2,第三行对应表4。

整体代码如下:

import os
os.environ["OPENAI_API_KEY"]="YOUR_OPEN_AI_KEY"

import subprocess
import uuid

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_core.runnables import RunnablePassthrough


def june_run_nougat(file_path, output_dir):
    # Run Nougat and store results as Mathpix Markdown
    cmd=["nougat", file_path, "-o", output_dir, "-m", "0.1.0-base", "--no-skipping"]
    res=subprocess.run(cmd) 
    if res.returncode !=0:
        print("Error when running nougat.")
        return res.returncode
    else:
        print("Operation Completed!")
        return 0

def june_get_tables_from_mmd(mmd_path):
    f=open(mmd_path)
    lines=f.readlines()
    res=[]
    tmp=[]
    flag=""
    for line in lines:
        if line=="\\begin{table}\n":
            flag="BEGINTABLE"
        elif line=="\\end{table}\n":
            flag="ENDTABLE"
        
        if flag=="BEGINTABLE":
            tmp.append(line)
        elif flag=="ENDTABLE":
            tmp.append(line)
            flag="CAPTION"
        elif flag=="CAPTION":
            tmp.append(line)
            flag="MARKDOWN"
            print('-' * 100)
            print(''.join(tmp))
            res.append(''.join(tmp))
            tmp=[]

    return res

file_path="YOUR_PDF_PATH"
output_dir="YOUR_OUTPUT_DIR_PATH"

if june_run_nougat(file_path, output_dir)==1:
    import sys
    sys.exit(1)

mmd_path=output_dir + '/' + os.path.splitext(file_path)[0].split('/')[-1] + ".mmd" 
tables=june_get_tables_from_mmd(mmd_path)


# Prompt
prompt_text="""You are an assistant tasked with summarizing tables and text. \ 
Give a concise summary of the table or text. The table is formatted in LaTeX, and its caption is in plain text format: {element}  """
prompt=ChatPromptTemplate.from_template(prompt_text)

# Summary chain
model=ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
summarize_chain={"element": lambda x: x} | prompt | model | StrOutputParser()
# Get table summaries
table_summaries=summarize_chain.batch(tables, {"max_concurrency": 5})
print(table_summaries)

# The vectorstore to use to index the child chunks
vectorstore=Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())

# The storage layer for the parent documents
store=InMemoryStore()
id_key="doc_id"

# The retriever (empty to start)
retriever=MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
    search_kwargs={"k": 1} # Solving Number of requested results 4 is greater than number of elements in index..., updating n_results=1
)

# Add tables
table_ids=[str(uuid.uuid4()) for _ in tables]
summary_tables=[
    Document(page_content=s, metadata={id_key: table_ids[i]})
    for i, s in enumerate(table_summaries)
]
retriever.vectorstore.add_documents(summary_tables)
retriever.docstore.mset(list(zip(table_ids, tables)))


# Prompt template
template="""Answer the question based only on the following context, which can include text and tables, there is a table in LaTeX format and a table caption in plain text format:
{context}
Question: {question}
"""
prompt=ChatPromptTemplate.from_template(template)

# LLM
model=ChatOpenAI(temperature=0, model="gpt-3.5-turbo")

# Simple RAG pipeline
chain=(
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

print(chain.invoke("when layer type is Self-Attention, what is the Complexity per Layer?"))  # Query about table 1

print(chain.invoke("Which parser performs worst for BLEU EN-DE"))  # Query about table 2

print(chain.invoke("Which parser performs best for WSJ 23 F1"))  # Query about table 4

结论

本文讨论了RAG流程中表处理的关键技术和现有解决方案,并提出了解决方案及其实现。

我们在本文中使用 nougat 来解析表格。但是,如果有更快、更有效的解析工具可用,我们会考虑替换 nougat。我们对待工具的态度是先有正确的想法,然后找到工具来实现,而不是依赖于某种工具。

在本文中,我们将所有表格内容输入到LLM中。然而,在实际场景中,我们应该考虑表超出LLM上下文长度的情况。我们可以通过使用有效的分块方法来解决这个问题。

人负责的项目主要采用阿里云数据库 MySQL,最近频繁出现慢 SQL 告警,执行时间最长的竟然高达 5 分钟。

图片来自 Pexels

导出日志后分析,主要原因竟然是没有命中索引和没有分页处理。其实这是非常低级的错误,我不禁后背一凉,团队成员的技术水平亟待提高啊。

改造这些 SQL 的过程中,总结了一些经验分享给大家,如果有错误欢迎批评指正。

MySQL 性能

①最大数据量

抛开数据量和并发数,谈性能都是耍流氓。MySQL 没有限制单表最大记录数,它取决于操作系统对文件大小的限制。

《阿里巴巴 Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐分库分表。

性能由综合因素决定,抛开业务复杂度,影响程度依次是硬件配置、MySQL 配置、数据表设计、索引优化。500 万这个值仅供参考,并非铁律。

我曾经操作过超过 4 亿行数据的单表,分页查询最新的 20 条记录耗时 0.6 秒,SQL 语句大致是:

select field_1,field_2 from table where id < #{prePageMinId} order by id desc limit 20 

prePageMinId 是上一页数据记录的最小 ID。虽然当时查询速度还凑合,随着数据不断增长,有朝一日必定不堪重负。

分库分表是个周期长而风险高的大活儿,应该尽可能在当前结构上优化,比如升级硬件、迁移历史数据等等,实在没辙了再分。对分库分表感兴趣的同学可以阅读分库分表的基本思想。

②最大并发数

并发数是指同一时刻数据库能处理多少个请求,由 max_connections 和 max_user_connections 决定。

max_connections 是指 MySQL 实例的最大连接数,上限值是 16384,max_user_connections 是指每个数据库用户的最大连接数。

MySQL 会为每个连接提供缓冲区,意味着消耗更多的内存。如果连接数设置太高硬件吃不消,太低又不能充分利用硬件。

一般要求两者比值超过 10%,计算方法如下:

max_used_connections / max_connections * 100% = 3/100 *100% ≈ 3% 

查看最大连接数与响应最大连接数:

show variables like '%max_connections%'; 
show variables like '%max_user_connections%'; 

在配置文件 my.cnf 中修改最大连接数:

[mysqld] 
max_connections=100 
max_used_connections=20 

③查询耗时 0.5 秒

建议将单次查询耗时控制在 0.5 秒以内,0.5 秒是个经验值,源于用户体验的 3 秒原则。如果用户的操作 3 秒内没有响应,将会厌烦甚至退出。

响应时间=客户端 UI 渲染耗时+网络请求耗时+应用程序处理耗时+查询数据库耗时,0.5 秒就是留给数据库 1/6 的处理时间。

④实施原则

相比 NoSQL 数据库,MySQL 是个娇气脆弱的家伙。它就像体育课上的女同学,一点纠纷就和同学闹别扭(扩容难),跑两步就气喘吁吁(容量小并发低),常常身体不适要请假(SQL 约束太多)。

如今大家都会搞点分布式,应用程序扩容比数据库要容易得多,所以实施原则是数据库少干活,应用程序多干活:

  • 充分利用但不滥用索引,须知索引也消耗磁盘和 CPU。
  • 不推荐使用数据库函数格式化数据,交给应用程序处理。
  • 不推荐使用外键约束,用应用程序保证数据准确性。
  • 写多读少的场景,不推荐使用唯一索引,用应用程序保证唯一性。
  • 适当冗余字段,尝试创建中间表,用应用程序计算中间结果,用空间换时间。
  • 不允许执行极度耗时的事务,配合应用程序拆分成更小的事务。
  • 预估重要数据表(比如订单表)的负载和数据增长态势,提前优化。

数据表设计

①数据类型

数据类型的选择原则,更简单或者占用空间更小:

  • 如果长度能够满足,整型尽量使用 tinyint、smallint、medium_int 而非 int。
  • 如果字符串长度确定,采用 char 类型。
  • 如果 varchar 能够满足,不采用 text 类型。
  • 精度要求较高的使用 decimal 类型,也可以使用 BIGINT,比如精确两位小数就乘以 100 后保存。
  • 尽量采用 timestamp 而非 datetime。


相比 datetime,timestamp 占用更少的空间,以 UTC 的格式储存自动转换时区。

②避免空值

MySQL 中字段为 NULL 时依然占用空间,会使索引、索引统计更加复杂。从 NULL 值更新到非 NULL 无法做到原地更新,容易发生索引分裂影响性能。

因此尽可能将 NULL 值用有意义的值代替,也能避免 SQL 语句里面包含 is not null 的判断。

③Text 类型优化

由于 Text 字段储存大量数据,表容量会很早涨上去,影响其他字段的查询性能。建议抽取出来放在子表里,用业务主键关联。

索引优化

索引分类如下:

  • 普通索引:最基本的索引。
  • 组合索引:多个字段上建立的索引,能够加速复合查询条件的检索。
  • 唯一索引:与普通索引类似,但索引列的值必须唯一,允许有空值。
  • 组合唯一索引:列值的组合必须唯一。
  • 主键索引:特殊的唯一索引,用于唯一标识数据表中的某一条记录,不允许有空值,一般用 primary key 约束。
  • 全文索引:用于海量文本的查询,MySQL 5.6 之后的 InnoDB 和 MyISAM 均支持全文索引。由于查询精度以及扩展性不佳,更多的企业选择 Elasticsearch。

索引优化原则:

  • 分页查询很重要,如果查询数据量超过 30%,MySQL 不会使用索引。
  • 单表索引数不超过 5 个、单个索引字段数不超过 5 个。
  • 字符串可使用前缀索引,前缀长度控制在 5-8 个字符。
  • 字段唯一性太低,增加索引没有意义,如:是否删除、性别。
  • 合理使用覆盖索引,如下所示:
select login_name, nick_name from member where login_name = ? 

login_name, nick_name 两个字段建立组合索引,比 login_name 简单索引要更快。

SQL 优化

①分批处理

博主小时候看到鱼塘挖开小口子放水,水面有各种漂浮物。浮萍和树叶总能顺利通过出水口,而树枝会挡住其他物体通过,有时还会卡住,需要人工清理。

MySQL 就是鱼塘,最大并发数和网络带宽就是出水口,用户 SQL 就是漂浮物。

不带分页参数的查询或者影响大量数据的 update 和 delete 操作,都是树枝,我们要把它打散分批处理,下面举例说明。

业务描述:更新用户所有已过期的优惠券为不可用状态。

SQL 语句:

update status=0 FROM `coupon` WHERE expire_date <= #{currentDate} and status=1; 

如果大量优惠券需要更新为不可用状态,执行这条 SQL 可能会堵死其他 SQL,分批处理伪代码如下:

int pageNo=1; 
int PAGE_SIZE=100; 
while(true) { 
    List<Integer> batchIdList=queryList('select id FROM `coupon` WHERE expire_date <=#{currentDate} and status=1 limit #{(pageNo-1) * PAGE_SIZE},#{PAGE_SIZE}'); 
    if (CollectionUtils.isEmpty(batchIdList)) { 
        return; 
    } 
    update('update status=0 FROM `coupon` where status=1 and id in #{batchIdList}') 
    pageNo ++; 
} 

②操作符 <> 优化

通常 <> 操作符无法使用索引,举例如下,查询金额不为 100 元的订单:

select id from orders where amount  != 100; 

如果金额为 100 的订单极少,这种数据分布严重不均的情况下,有可能使用索引。

鉴于这种不确定性,采用 union 聚合搜索结果,改写方法如下:

(select id from orders where amount > 100) 
 union all 
(select id from orders where amount < 100 and amount > 0) 

③OR 优化

在 Innodb 引擎下 OR 无法使用组合索引,比如:

select id,product_name from orders where mobile_no = '13421800407' or user_id = 100; 

OR 无法命中 mobile_no + user_id 的组合索引,可采用 union,如下所示:

(select id,product_name from orders where mobile_no='13421800407') 
 union 
(select id,product_name from orders where user_id=100); 

此时 id 和 product_name 字段都有索引,查询才最高效。

④IN 优化

IN 适合主表大子表小,EXIST 适合主表小子表大。由于查询优化器的不断升级,很多场景这两者性能差不多一样了。

尝试改为 Join 查询,举例如下:

select id from orders where user_id in (select id from user where level = 'VIP'); 

采用 Join 如下所示:

select o.id from orders o left join user u on o.user_id = u.id where u.level = 'VIP'; 

⑤不做列运算

通常在查询条件列运算会导致索引失效,如下所示,查询当日订单:

select id from order where date_format(create_time,'%Y-%m-%d') = '2019-07-01'; 

date_format 函数会导致这个查询无法使用索引,改写后:

select id from order where create_time between '2019-07-01 00:00:00' and '2019-07-01 23:59:59'; 

⑥避免Select All

如果不查询表中所有的列,避免使用 SELECT *,它会进行全表扫描,不能有效利用索引。

⑦Like 优化

Like 用于模糊查询,举个例子(field 已建立索引):

SELECT column FROM table WHERE field like '%keyword%'; 

这个查询未命中索引,换成下面的写法:

SELECT column FROM table WHERE field like 'keyword%'; 

去除了前面的 % 查询将会命中索引,但是产品经理一定要前后模糊匹配呢?全文索引 fulltext 可以尝试一下,但 Elasticsearch 才是终极武器。

⑧Join 优化

Join 的实现是采用 Nested Loop Join 算法,就是通过驱动表的结果集作为基础数据,通过该结数据作为过滤条件到下一个表中循环查询数据,然后合并结果。

如果有多个 Join,则将前面的结果集作为循环数据,再次到后一个表中查询数据。

驱动表和被驱动表尽可能增加查询条件,满足 ON 的条件而少用 Where,用小结果集驱动大结果集。

被驱动表的 Join 字段上加上索引,无法建立索引的时候,设置足够的 Join Buffer Size。

禁止 Join 连接三个以上的表,尝试增加冗余字段。

⑨Limit 优化

Limit 用于分页查询时越往后翻性能越差,解决的原则:缩小扫描范围,如下所示:

select * from orders order by id desc limit 100000,10  
耗时0.4秒 
select * from orders order by id desc limit 1000000,10 
耗时5.2秒 

先筛选出 ID 缩小查询范围,写法如下:

select * from orders where id > (select id from orders order by id desc  limit 1000000, 1) order by id desc limit 0,10 
耗时0.5秒 

如果查询条件仅有主键 ID,写法如下:

select id from orders where id between 1000000 and 1000010 order by id desc 
耗时0.3秒 

如果以上方案依然很慢呢?只好用游标了,感兴趣的朋友阅读 JDBC 使用游标实现分页查询的方法。

其他数据库

作为一名后端开发人员,务必精通作为存储核心的 MySQL 或 SQL Server,也要积极关注 NoSQL 数据库,他们已经足够成熟并被广泛采用,能解决特定场景下的性能瓶颈。

作者:编码砖家

编辑:陶家龙

出处:www.cnblogs.com/xiaoyangjia/p/11267191.html