0%

最近刚接触到这个话题,想开一篇新博客来记录一下自己学习的过程。

首先我是看了两篇review了解了这个topic的主要任务:

另外同步阅读了huggingface的tutorial:https://huggingface.co/learn/computer-vision-course/unit8/3d-vision/nvs。这篇博客将NVS描述为这样一个任务:

generate views from new camera angles that are plausibly consistent with a set of images.

我们在对一个场景进行3D还原时,首先的输入是一系列相机在不同的视角拍摄的静态图片,通过这些图片我们对该场景下的人物以及物体进行3D建模,但相机个数是有限的,如何推算出某个没有相机的角度上的view,这就是NVS这个任务要做的事情。

很多方法在这个topic上提出来,大致可以分成两类:1)generate an intermediate three-dimensional representation, which is rendered from a new viewing direction. 比如PixelNeFRF 2)direclty generated new views without an intermediate 3D representaion, 比如Zero123

NeRF

NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis

2020年出的一篇文章,下面这句话就是它这个算法的精华:

Our algorithm represents a scene using a fully-connected (nonconvolutional) deep network, whose input is a single continuous 5D coordinate (spatial location (x, y, z) and viewing direction (θ, φ)) and whose output is the volume density and view-dependent emitted radiance at that spatial location

LLFF 数据集

在查看NeRF的github code时,发现作者使用了两个数据集,其中一个就是LLFF,本着学习的原则,先把LLFF数据集搞清楚。

LLFF全称为Local light Field Fusion,也是提出了一个NVS的算法。LLFF的主旨思想是:

present a simple and reliable method for view synthesis from a set of input images captured by a handheld camera in an irregular grid pattern.

简单说就是:该方法可以从一系列手持拍摄的静态图片生成一个scene,这个scene可以理解为一个3D的场景,可以用VR眼镜看的那种。

LLFF repo提供了非常详细的安装教程,令我比较感兴趣的是,它可以基于自己拍摄的一些静态图片生成一个scene。先来看看它的这份代码。

我们的输入是从一系列的images开始的,首先第一步

  1. recover cammera poses

这一步采用COLMAP 实现了一个 struture from emotion + Multi-View Stereo(MVS)的完整pipeline。这一步的输入是一系列的静态图像,输出的是这个场景下的 6-DoF camera poses和 near/far depth bounds。

Structure-from-Motion (SfM) is the process of reconstructing 3D structure from its projections into a series of images. The input is a set of overlapping images of the same object, taken from different viewpoints. The output is a 3-D reconstruction of the object, and the reconstructed intrinsic and extrinsic camera parameters of all images.

incremental-sfm

COLMAP使用的方法依赖于Structure-from-Motion Revisited这篇文章,它将SfM分成三个步骤:

  • feature detection and extraction

这一步好理解,特征抽取,利用一个apprearance descriptor f

  • feature matching and geometric verification

feature matching利用前一步的features找出这一系列图片中的same scene part。原文是这样写的:

The na ̈ıve approach tests every image pair for scene overlap; it searches for feature correspondences by finding the most similar feature in image I(a) for every feature in image I(b), using a similarity metric comparing the appearance fj of the features

简单理解就是根据上一步抽取的features去一一比对每一对image pair,寻找出每一对image pair中的相似feature,从而找出same scene part。

geometric verification 我觉得有点稍微难理解。上一步只是确认了每一张图片中在apperance上相似的scene part,但有可能不是指代的这个场景下的同一个object(Point), 所以就需要去verify上一步的match是否准确,怎么verify呢?通过projective geometry去预估transformation

Since matching is based solely on appearance, it is not guaranteed that corresponding features actually map to the same scene point

  • structure and motion reconstruction

Multi-View Stereo(MVS) takes the output of SfM to compute depth and/or normal infomation for every pixel in an image.Fusion of the depth and normal maps of multiple images in 3D then produces a dense point cloud of the scene.

实现脚本为imgs2poses.py, 看该源代码就是基于COLMAP来做的。试着运行该脚本,测试数据用repo内的download_data.sh下载的数据。运行完后可以看到如下输出:

image-20250114162420847

其中images是source images,testscene下除了images这个文件夹,其他都是COLMAP生成的。具体含义参考COLMAP的document

logs文件内容:

1
2
3
4
5
6
7
8
9
10
11
Need to run COLMAP
Features extracted
Features matched
Sparse map created
Finished running COLMAP, see data/testscene/colmap_output.txt for logs
Post-colmap
('Cameras', 5)
('Images #', 20)
('Points', (9906, 3), 'Visibility', (9906, 20))
('Depth stats', 13.732739125795911, 118.85217973695897, 30.413495856274356)
Done with imgs2poses

仔细分析一下imgs2poses.py,一共用COLMAP执行了三条terminal命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# extract features
feature_extractor_args = [
'colmap', 'feature_extractor',
'--database_path', os.path.join(basedir, 'database.db'),
'--image_path', os.path.join(basedir, 'images'),
'--ImageReader.single_camera', '1',
# '--SiftExtraction.use_gpu', '0',
]

# matching
exhaustive_matcher_args = [
'colmap', match_type,
'--database_path', os.path.join(basedir, 'database.db'),
]
# Sparse map create
mapper_args = [
'colmap', 'mapper',
'--database_path', os.path.join(basedir, 'database.db'),
'--image_path', os.path.join(basedir, 'images'),
'--output_path', os.path.join(basedir, 'sparse'), # --export_path changed to --output_path in colmap 3.6
'--Mapper.num_threads', '16',
'--Mapper.init_min_tri_angle', '4',
'--Mapper.multiple_models', '0',
'--Mapper.extract_colors', '0',
]

对照colmap cli的guidebook, 作者使用了前三个命令,dense部分没有继续生成。想要知道它output出来的这些.bin文件含义,需要搞明白database.db内有什么东西,它是feature extraction的产物。db文件可以用vscode的插件打开,它包含7个table:

image-20250115133036561

keypoints表格中,我下图截图的data部分里才是所有feature的信息,内有每一个feature所在的X,Y坐标。

COLMAP uses the convention that the upper left image corner has coordinate (0, 0) and the center of the upper left most pixel has coordinate (0.5, 0.5)

COLMAP在表示图像坐标时,采用了一种特定的坐标系统,其中图像的左上角被定义为坐标原点 (0, 0)。而“the center of the upper left most pixel has coordinate (0.5, 0.5)” 指的是图像中最左上角的像素的中心位置被赋予了坐标 (0.5, 0.5)。

image-20250115133949032

在这两张表格中,rows表示的数值是number of detected features per image, 如果rows=0, 那么这个image没有feature

在运行命令colmap exhaustive_matcher --database_path ./data/testscene/database.db后,db文件内的matchs这张表会出现值(之前没有),每一行会表示一张图片和另外一张图片的匹配结果,rows的值表示match上特征点的个数。

在这个全民皆Difussion Model的时代,已经有人忘却了曾经的王者GAN。这篇博客是自己记录学习生成模型,这里的生成模型仅局限于生成图片的模型,不是LLM这一类自然语言生成模型。启发点有两个: - 在斯坦福cs231n这节课里,GAN这一章节详细讲解了从VAE到GAN的发展脉络,并没有延申到Difussion Model。Difussion Model的内容放到cs236中去讲了。 - Lilian Blog曾写过一篇What are Diffusion Models?, 内扩展了两篇介绍: GAN 和 VAE。

Self-supervised Learning

自监督学习中最出彩的就是对比学习。

Contrastive Learning

参考:

The goal of contrastive representation learning is to learn such an embedding space in which similar sample pairs stay close to each other while dissimilar ones are far apart.

对比学习的本质思想是:将positive的sample和negtive的sample距离分割的越远。cs231n中的lecture12发展逻辑捋的很好,是因为需要一个更general的pretext task:

image-20240904095209899之前的pretext task都是基于"visual common sense",例如预测rotations,画面修复inpainting, 颜色填充colorization等, 造成的问题是"learned representations may not be general"。

在contrastive learning中有一个loss比较重要:infoNCE,是在Representation Learning with Contrastive Predictive Coding文章中提到的,这个loss就是为了上面的本质思想量身定制,如何将一个class的sample拉的更近,而不属于一个class的sample拉的更远呢?

image-20240904133326653

这里我贴一下chatgpt对于上述公式为什么可以作为损失函数的解释,比我自己组织的语言要好:

image-20240904133551625

那么我们有了基本思想和loss函数之后如何训练这个网络呢?A Simple Framework for Contrastive Learning of Visual Representations 这篇文章给我们介绍了一个SimCLR,可以重点阅读一下。文章内给出了算法伪代码:

image-20240904134006818

SimCLR也披露了自己的训练代码:https://github.com/google-research/simclr?tab=readme-ov-file

SimCLR属于Instance contrastive learning中的一种,包括he Kaiming团队所出的MOCO以及MOCO V2,与之相对应的是另外一种contrastive learning: Sequence contrastive learning, 代表为CPC(Representation Learning with Contrastive Predictive Coding)。其实很好理解,如果接触过image segmentation这个任务的话,会记得在图像分割这个任务里有两种segmentation,一种是instance segmentation,一种是semantic segmentation。contrastive learning中的instance对比学习就是如上面图示,猫的图片它是一个class,我们将猫的图片和狗的图片的距离变大,而让猫和猫的图片的”距离“变小。对于sequence对比学习,顾名思义就是加入了序列的影响:

image-20240904144210318

Generative Modeling

generative modeling被认为是自监督学习的一种,但他们俩的目的不一样,前者是希望建立一个模型,我们用这个模型可以生成一些diverse和realistic的图片,后者是希望通过自监督的representation learning去生成图片更好的features,用这些features去帮助下游任务拥有更好的performance。image-20241016103604877

强烈建议阅读NIPS 2016 Tutorial: Generative Adversarial Networks, 作者将拟合Pmodel的方式分成两种:

image-20241016103757996

其中Explicit Density中可以Tractable density的PixelRNN是我们所熟知的,我对于Tractable Density的理解就是可以求导并利用梯度下降去一点点降低loss的函数,那么对于FVBN来说,拟合Pmodel的方式就是:一张图片的概率等于所有这张图片上pixel的联合概率image-20241016105016392

而近似估算中的VAE则采用的是一种引入潜在变量z的方式来间接的建模数据,导致密度函数不可解,必须采用变分推断等近似方法。那么为什么要引入潜在变量z呢?

image-20241016111401857

今天新开一篇文章。前段时间一直忙于项目周期中琐碎的事情,没有好好总结和思考技术核心的东西。探索RAG的应用也有一段时间了,市面上的应用也看的不少,很多的应用包括langchain-chatchat,Dify, 都是整体搭建了一个最basic版本的RAG框架,至于很多细节点,并没有提供更精细化的实现,今天要写的这部分:PDF上传到知识库之后如何parse和chunking,直接影响后续retrival的表现,目前还没有很全面的review来总结这部分内容。这里就对我看到的一些技术做一些整理。

解析PDF文档的难点主要在于如何精确地捕捉页面的整体布局,并将包括表格标题, 段落以及图片在内的内容转译为文档的文字形式。这一过程涉及到多个技术点,布局的检测,图片中文字的抽取,表格中行与列的识别(如何正确将PDF中的表格识别成可用结构化形式表示的表格,也就是能还原出原表格来)。

目前解析PDF文档主要有三种主流方式:

  1. 基于规则的方法:这种方法根据文档的组织特性来确定每个部分的样式和内容,代表库:pypdf, 这种方法的适用性比较差,很难通过预设的规则覆盖所有PDF的情形。
  2. 基于深度学习模型的方法:一个流行的解决方案是结合了物体检测和OCR模型,代表 Chatdoc
  3. 基于多模态大模型的方法:通过这种方法可以解析PDF中复杂结构或者提取关键信息

基于规则的方法

基于规则的PDF处理库真的太多了,每一次看到一个应用使用的新的PDF解析器,都要重新看看怎么处理的。比如langchain-chatchat用的是pyMuPDF中的fitz包,见langchain-chatchat源代码,这一段处理特别粗糙,我贴上来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import fitz  # pyMuPDF里面的fitz包,不要与pip install fitz混淆
import numpy as np

ocr = get_ocr()
doc = fitz.open(filepath)
resp = ""

b_unit = tqdm.tqdm(
total=doc.page_count, desc="RapidOCRPDFLoader context page index: 0"
)
for i, page in enumerate(doc):
b_unit.set_description(
"RapidOCRPDFLoader context page index: {}".format(i)
)
b_unit.refresh()
text = page.get_text("")
resp += text + "\n"

img_list = page.get_image_info(xrefs=True)
for img in img_list:
if xref := img.get("xref"):
bbox = img["bbox"]
# 检查图片尺寸是否超过设定的阈值
if (bbox[2] - bbox[0]) / (page.rect.width) < PDF_OCR_THRESHOLD[
0
] or (bbox[3] - bbox[1]) / (
page.rect.height
) < PDF_OCR_THRESHOLD[1]:
continue
pix = fitz.Pixmap(doc, xref)
samples = pix.samples
if int(page.rotation) != 0: # 如果Page有旋转角度,则旋转图片
img_array = np.frombuffer(
pix.samples, dtype=np.uint8
).reshape(pix.height, pix.width, -1)
tmp_img = Image.fromarray(img_array)
ori_img = cv2.cvtColor(np.array(tmp_img), cv2.COLOR_RGB2BGR)
rot_img = rotate_img(img=ori_img, angle=360 - page.rotation)
img_array = cv2.cvtColor(rot_img, cv2.COLOR_RGB2BGR)
else:
img_array = np.frombuffer(
pix.samples, dtype=np.uint8
).reshape(pix.height, pix.width, -1)

result, _ = ocr(img_array)
if result:
ocr_result = [line[1] for line in result]
resp += "\n".join(ocr_result)

注意它对PDF中图片的处理,是将某一页中所有的图片存入一个img_list,然后遍历这个list,用ocr算法抠出里面的文字,将这些文字都放到该页text的末尾,很大程度上丧失了图片在PDF中应该表达的语义,不仅如此,我觉得还影响了retrival的performance,加入了噪声。

再来看看Dify怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pypdfium2
with blob.as_bytes_io() as file_path:
pdf_reader = pypdfium2.PdfDocument(file_path, autoclose=True)
try:
for page_number, page in enumerate(pdf_reader):
text_page = page.get_textpage()
content = text_page.get_text_range()
text_page.close()
page.close()
metadata = {"source": blob.source, "page": page_number}
yield Document(page_content=content, metadata=metadata)
finally:
pdf_reader.close()

换了一个pypdfium2的库,图片完全舍弃。我也去搜了有没有对两者的比较,见pypdf or pymupdf?,更有作者做了一个repo用于比较各种library的效果:传送门。结论是如果单论文本抽取的质量,pypdfium2是第一

对于RAG中PDF的处理,我觉得最理想的目标应该是,query能够link到PDF中的图片,不仅如此,这张图片也应该作为reference,目前市面上的reference只能link到相应文件的纯文本。远远不够精细,不过做起来确实有点困难,也需要一点耐心。

image-20240628100839214

更多其他工具的比较可参考A Benchmark and Evaluation for Text Extraction from PDF

image-20240628101040870

基于规则的方法有一个最大的缺点就是:它会将每一行视为由换行符“”分隔的序列,如果那一行确实是以句号为结尾,影响还稍微小一点,但是如果下一行还在表述这句话,语义上就完全断掉了。要知道在chunking的阶段,大部分的做法是以\n作分隔符的。

基于深度学习模型的方法

这种方法的又是是它能准确识别整个文档的布局,包括表格和段落。它甚至能理解表格内的结构。这意味着解析出来的表格能完整的解析成表格原本的样子。局限性就在于对象检测和OCR的识别两个阶段可能会耗时。这时候可以考虑采用GPU加速或者多进程和多线程的方式进行处理。

这里有几个开源的代表框架:

  • Unstructured:它是langchain官方推荐的方式。在启用infer_table_structure=True的hi_res策略下,表格的识别效果良好。fast策略下表现不佳
  • Layout-parser:如果需要识别复杂结构的PDF,建议使用最大的模型,虽然可能会稍慢一点
  • PP-StructureV2: 采用多种模型组合进行文档的分析,性能优于平均水平。这是百度的飞桨出品的文档智能模型。

另外有一些闭源付费的工具诸如ChatDoc和LLama Parse, 这些在现实的落地中存在一定阻碍,毕竟调用API是一定会存在数据上传到别人服务器的风险的,如果是处理非敏感数据,付费API可以做考虑。

这里对Unstructured处理PDF做一些探索。

Unstructured对PDF的处理首先是对layout进行检测(detectron2模型),然后使用tesseract实现OCR的功能,用table transformer处理table。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from unstructured.partition.pdf import partition_pdf
# Get elements
raw_pdf_elements = partition_pdf(
filename=test_file,
# Using pdf format to find embedded image blocks
extract_images_in_pdf=True,
# Use layout model (YOLOX) to get bounding boxes (for tables) and find titles
# Titles are any sub-section of the document
infer_table_structure=True,
# Post processing to aggregate text once we have the title
chunking_strategy="by_title",
# Chunking params to aggregate text blocks
# Attempt to create a new chunk 3800 chars
# Attempt to keep chunks > 2000 chars
# Hard max on chunks
max_characters=4000,
new_after_n_chars=3800,
combine_text_under_n_chars=2000,
)

tables = [el for el in raw_pdf_elements if el.category == 'Table']

print(tables[0].text)

print(tables[0].metadata.text_as_html)

上述的第二个输出:将表格转化为html格式,如果将其保存为html,它可以在浏览器中打开,打开后还是一个完整的表格。unstructured的好处就在于它保证了一个表格的完整性。官方给出的示例代码中就是将每一个element的text去做embedding,对于表格来说就是将表格的text去做embedding。但是我测试了两个例子发现,它把表格的标题以及表格的注解给弄到其他element里去了,这些文本本应该和表格一起,比如下面的表格:

image-20240628160939056

它抽取出来的表格element就只包含纯表格那部分,上面关于Table1的介绍都包含在了上面一个element里

1
2
3
4
5
6
7
8
9
3.5 Positional Encoding

Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the

5

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.
--------------------------------- 上面是另一个element的内容
Layer Type Self-Attention Recurrent Convolutional Self-Attention (restricted) Complexity per Layer O(n2 · d) O(n · d2) O(k · n · d2) O(r · n · d) Sequential Maximum Path Length Operations O(1) O(n) O(1) O(1) O(1) O(n) O(logk(n)) O(n/r)

基于多模态大模型的方法