rag评估的指标

忠诚度Faithfulness

Faithfulness:衡量生成答案与给定上下文之间的事实一致性。忠实度得分是基于答案和检索到的上下文 计算出来的,答案的评分范围在0到1之间,分数越高越好。

image-20250711154457878

计算方式:将大模型给出的答案进行切片,检索给出的上下文,计算这些切片是否在上下文中

image-20250711155257239

答案相关性Answerrelevance

Answerrelevance:答案相关性的评估指标旨在评估生成的答案与给定提示的相关程度。如果答案不完 整或包含冗余信息,则会被赋予较低的分数。这个指标使用问题和答案来计算,其值介于0到1之间,得 分越高表明答案的相关性越好

image-20250711155128553

计算方式:根据答案生成多个问题,然后计算生成的答案与原答案的余弦相似度,再取平均

image-20250711155407518

上下文精确度ContextPrecision

ContextPrecision:上下文精确度衡量上下文中所有相关的真实信息是否被排在了较高的位置。理想情 况下,所有相关的信息块都应该出现在排名的最前面。这个指标是根据问题和上下文来计算的,数值范 围在0到1之间,分数越高表示精确度越好。 $$ \text{Context Precision} = \frac{\sum_{k=1}^{K} (\text{rel}(k) \times \frac{\text{Precision@k}}{\text{Ideal Precision@k}})}{\text{Total Relevant Documents}} $$

  • K:检索返回的文档总数(如 top-5)
  • rel(k):第 k 个文档是否相关(相关=1,无关=0)
  • Precision@k:前 k 个文档的精确率(相关文档数 / k)
  • Ideal Precision@k:理想情况下前 k 个文档的精确率(假设所有相关文档都排在最前面)

上下文召回率ContextRecall

ContextRecall:用来衡量检索到的上下文与被视为事实真相的标注答案的一致性程度。它根据事实真相 和检索到的上下文来计算,数值范围在0到1之间,数值越高表示性能越好。 为了从事实真相的答案中估计上下文召回率,需要分析答案中的每个句子是否可以归因于检索到的 上下文。在理想情况下,事实真相答案中的所有句子都应该能够对应到检索到的上下文中。 $$ \text{Context Recall} = \frac{|\{\text{返回的相关文档}\} \cap \{\text{标准相关文档}\}|}{|\{\text{标准相关文档}\}|} $$ 计算方式:上下文是否包括了标准答案的内容

检索性能的评估

平均倒数排名(Mean Reciprocal Rank, MRR)

平均倒数排名(Mean Reciprocal Rank, MRR) 是一种常用于评估信息检索系统、推荐系统或问答系统性能的评价指标。它特别适用于“每个查询只有一个正确答案”或“我们只关心第一个正确结果”的场景。

  • 倒数排名(Reciprocal Rank, RR):对于一个查询,如果第一个正确答案出现在排序结果的第 $ k $ 位,那么它的倒数排名为: $$ RR = \frac{1}{k} $$ 如果没有正确答案,则 $ RR = 0 $。

  • 平均倒数排名(MRR):对多个查询的倒数排名取平均值: $$ MRR = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i} $$ 其中:

    • $ |Q| $ 是查询的总数,
    • $ _i $ 是第 $ i $ 个查询中第一个正确答案的排名(位置)。
image-20250818093714324

平均精确率均值(Mean Average Precision, MAP)

MAP(Mean Average Precision) 是对多个查询或样本的 平均精确率(Average Precision, AP) 取平均,用来衡量排序结果的相关性质量。它综合考虑了:

  • 排序中相关结果的数量(召回)
  • 相关结果在排序中的位置(越靠前越好)

平均精确率(Average Precision, AP)

AP 是对一个查询而言的,衡量该查询下所有相关文档在排序中的整体表现。

直观理解:AP 是“在每个相关文档被检索到时”的精确率的平均值。

公式定义: $$ AP = \frac{\sum_{k=1}^{n} (P(k) \times \text{rel}(k))}{\text{总相关文档数}} $$

其中: - $ P(k) $:在第 $ k $ 个位置的精确率(即前 k 个结果中有多少是相关的) - $ (k) $:第 $ k $ 个文档是否相关(1 表示相关,0 表示不相关)

也就是说,只在相关文档出现的位置计算并累加精确率,最后除以总相关文档数。

平均精确率均值(MAP)

将所有查询的 AP 求平均:

$$ MAP = \frac{1}{|Q|} \sum_{i=1}^{|Q|} AP_i $$

其中: - $ |Q| $:查询总数 - $ AP_i $:第 $ i $ 个查询的平均精确率

image-20250818094149682

归一化折损累积增益(Normalized Discounted Cumulative Gain, nDCG)

nDCG 的核心思想是: 1. 高相关性的文档更有价值 2. 排在前面的结果比排在后面的价值更高(位置越靠前,权重越大) 3. 将系统的得分与“理想排序”对比,进行归一化,便于跨查询比较

1.累积增益(Cumulative Gain, CG)

CG 是前 $ k $ 个结果的相关性评分之和,不考虑位置

$$ CG@k = \sum_{i=1}^{k} rel_i $$

其中 $ rel_i $ 是第 $ i $ 个文档的相关性评分。

❌ 缺点:CG 不关心排序顺序。无论相关文档排第1还是第10,CG 都一样。

  1. 折损累积增益(Discounted Cumulative Gain, DCG)

DCG 引入“位置折损”:越靠后的结果,其贡献被“打折”。

常用公式(两种形式,第二种更常见):

$$ DCG@k = \sum_{i=1}^{k} \frac{rel_i}{\log_2(i+1)} \quad \text{或} $$

$$ DCG@k = rel_1 + \sum_{i=2}^{k} \frac{rel_i}{\log_2(i)} \quad \text{(更常用)} $$

💡 解释:第1个位置不打折,第2个位置除以 $ _2(2) = 1 $,第3个位置除以 $ _2(3) $,相当于打了约 63% 的折扣。

这样,相关文档越早出现,DCG 越高

  1. 理想折损累积增益(Ideal DCG, IDCG)

IDCG 是在理想排序下(所有相关文档按相关性从高到低排列)的 DCG 值。

IDCG@k = 将前 k 个最相关文档按最优顺序排列时的 DCG

IDCG 是当前查询下 DCG 的理论最大值。

  1. 归一化折损累积增益(nDCG@k)

$$ nDCG@k = \frac{DCG@k}{IDCG@k} $$

✅ nDCG 的取值范围是 [0, 1]: - 1.0:排序完全理想 - 接近 1:排序质量高 - 接近 0:排序很差

image-20250818095351579

利用RAGAS评估rag性能

learn-rag-langchain/RAGAS-langchian.ipynb at main · zxj-2023/learn-rag-langchain

检索器 1.Contextprecision(上下文精确度):评估检索质量。 2.Context Recall(上下文召回率):衡量检索的完整性。 生成器 1.Faithfulness(忠实度):衡量生成答案中的幻觉情况。 2.AnswerRelevance(答案相关性):衡量答案对问题的直接性(紧扣问题的核心)。

最终的RAGAS得分是以上各个指标得分的调和平均值。简而言之,这些指标用来综合评估 -个系统整体的性能。

RAG的构建

创建RAG文本分割、Embedding model 、 向量库存储Chroma

我们主要使用 RecursiveCharacterTextSplitter 切割文本,通过OpenAIEmbeddings()进行文本编码,存储到 VectorStore

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings
embeddings_model = DashScopeEmbeddings(
model="text-embedding-v2",
dashscope_api_key=openai.api_key,
)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500)
#进行文本分割,生成更小、更易于处理的文档块
docs = text_splitter.split_documents(paper_docs)

vectorstore = Chroma.from_documents(docs, embeddings_model)

Chroma 向量数据库默认情况下是内存存储,这意味着数据在程序运行结束后不会保留。 但是,Chroma 也支持持久化存储,您可以指定一个路径将数据保存到磁盘上。这样,即使程序关闭,数据也会被保留,并在下次启动时自动加载。

检索器的构建

现在我们可以利用 Chroma 向量库的 .as_retriever() 方式进行检索,需要控制的主要参数为 k

1
base_retriever = vectorstore.as_retriever(search_kwargs={"k" : 3})
  • ectorstore.as_retriever() : 这个方法的作用是将一个向量数据库实例( vectorstore )转换为 LangChain 中的一个检索器( Retriever )对象。检索器是 LangChain 中负责根据用户查询从数据源中获取相关文档的核心组件。
  • “k” : 这个键表示要检索的“最相似”文档的数量。在这里, “k” : 3 意味着当检索器接收到一个查询时,它将从向量存储中返回与该查询最相似的 3 个文档。这在 RAG(检索增强生成)系统中非常常见,用于限制传递给大型语言模型的上下文信息量,以提高效率和相关性。

检索器的作用 检索器(Retriever)是一个核心组件,其主要作用是从一个数据源(如向量数据库、文档加载器等)中根据给定的查询(query)检索出相关的文档或信息。

prompt的构建

我们需要利用LLMContext 生成一系列的问题的answer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain import PromptTemplate

template = """You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know.

Question: {question}

Context: {context}

Answer:
"""

prompt = PromptTemplate(
template=template,
input_variables=["context","question"]
)

print(prompt)

生成answer,利用LLM

利用 Runnable 定义一个 chain 实现rag全流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model_name="qwen-plus-2025-04-28",
temperature=0,
api_key="",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
#RunnablePassthrough将输入数据原封不动地传递到输出
#StrOutputParser() 它被用作 RAG 链的最后一步,确保最终的答案以字符串形式输出。
rag_chain = (
{"context": base_retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)

创建 RAGAs 所需的数据

question Answer contexts ground_truths

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
# Ragas 数据集格式要求  ['question', 'answer', 'contexts', 'ground_truths']
'''
{
"question": [], <-- 问题基于Context的
"answer": [], <-- 答案基于LLM生成的
"contexts": [], <-- context
"ground_truths": [] <-- 标准答案
}
'''

from datasets import Dataset
#构建问题与标准答案(黄金数据集)
questions = ["What is faithfulness ?",
"How many pages are included in the WikiEval dataset, and which years do they cover information from?",
"Why is evaluating Retrieval Augmented Generation (RAG) systems challenging?",
]
ground_truths = ["Faithfulness refers to the idea that the answer should be grounded in the given context.",
" To construct the dataset, we first selected 50 Wikipedia pages covering events that have happened since the start of 2022.",
"Evaluating RAG architectures is, however, challenging because there are several dimensions to consider: the ability of the retrieval system to identify relevant and focused context passages, the ability of the LLM to exploit such passages in a faithful way, or the quality of the generation itself."]
answers = []
contexts = []

# 生成答案
for query in questions:
answers.append(rag_chain.invoke(query))
contexts.append([docs.page_content for docs in base_retriever.get_relevant_documents(query)])

# 构建数据
data = {
"user_input": questions,
"response": answers,
"retrieved_contexts": contexts,
"reference": ground_truths
}
dataset = Dataset.from_dict(data)

使用RAGAs 进行评估

1
2
3
#将评估数据转换成 Ragas 框架专用的格式 。
from ragas import EvaluationDataset
evaluation_dataset = EvaluationDataset.from_list(dataset)

我们可以使用一组常用的RAG评估指标,在收集的数据集上评估我们的RAG系统。您可以选择任何模型作为评估用LLM来进行评估。 ragas默认使用openai的api

1
2
from ragas.llms import LangchainLLMWrapper
evaluator_llm = LangchainLLMWrapper(llm)

调用

1
2
3
4
from ragas.metrics import LLMContextRecall, Faithfulness, FactualCorrectness
from ragas import evaluate
result = evaluate(dataset=evaluation_dataset,metrics=[LLMContextRecall(), Faithfulness(), FactualCorrectness()],llm=evaluator_llm)
result
image-20250713164037571

查看结果

1
2
3
4
5
import pandas as pd
pd.set_option("display.max_colwidth", None)

df = result.to_pandas()
df

参考资料

RAG系统效果难评?2025年必备的RAG评估框架与工具详解 - 知乎

如何利用RAGAs评估RAG系统的好坏_哔哩哔哩_bilibili

ragas中文文档Evaluate a simple RAG - Ragas

人工智能 - RAG系统的7个检索指标:信息检索任务准确性评估指南 - deephub - SegmentFault 思否

分块策略

以下是 RAG 应用程序的典型工作流程:

6878b8fa-5e74-45a1-9a89-5aab92889126_2366x990

主流主要有五种分块策略:

https___substack-post-media.s3.amazonaws.com_public_images_92c70184-ba0f-4877-9a55-e4add0e311ad_870x1116

Fixed-size chunking 固定大小的分块

98c422a0-f0e2-457c-a256-4476a56a601f_943x232

将文本以固定长度分块,overlap为每个块的重合程度

1
2
3
4
5
6
7
8
9
10
11
text = "大家好,我是果粒奶优有果粒,欢迎关注我,让我们一起探索AI。"
from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
separator="",#按字切分
chunk_size=5,
chunk_overlap=1,
length_function=len,#以长度计算
is_separator_regex=False,#不视为正则表达式
)
text_splitter.split_text(text)

Semantic chunking 语义分块

https___substack-post-media.s3.amazonaws.com_public_images_a6ad83a6-2879-4c77-9e49-393f16577aef_1066x288

先将文本分段,然后为每个段进行嵌入,若两个段有较高的余弦相似度,则合并成一个块,一直合并到余弦相似度显著下降,再从新的块开始

需要设定阈值来确定余弦相似度是否显著下降,这因文档而异。

image-20250710150106274

具体实现思路:利用滑动窗口,从第一句往后移动滑动窗口,如图,emed1与emed2相差sen3,计算出来的distance决定sen3是否加入chunk1,以此类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#利用langchain调用
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.embeddings import DashScopeEmbeddings
embeddings_model = DashScopeEmbeddings(
model="text-embedding-v2",
dashscope_api_key="",
)
semantic_chunk=SemanticChunker(
embeddings=embeddings_model,#嵌入模型
breakpoint_threshold_type="percentile",#定义如何计算语义断点阈值
breakpoint_threshold_amount=95,#设定阈值
#min_chunk_size=500#限制生成块最小的字符数,避免生成无意义的块
sentence_split_regex=r'[。!?.\n]',#语句切分
)

LangChain 搭配 QWen 踩坑-阿里云开发者社区

使用OpenAIEmbeddings配置embedding模型,需要设置一个关键参数

check_embedding_ctx_length = False 的作用是:

关闭 langchain_openai 在调用嵌入模型前对输入文本长度的检查与自动截断/分段逻辑。

但 DashScope 的 text-embedding-v4 接口:

  • 对输入格式要求更严格(必须是字符串或字符串列表,不能是分段后的复杂结构)。
  • 不接受 langchain_openai 默认生成的分段后的列表嵌套结构
1
2
3
4
5
6
7
8
9
10
from langchain_openai import OpenAIEmbeddings,  OpenAI
embeddings = OpenAIEmbeddings(
api_key="sk-",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
model="text-embedding-v4",
check_embedding_ctx_length = False,
dimensions=1536
)
result=embeddings.embed_query("Hello, world!")
print(len(result))

源代码理解见最后

Recursive chunking 递归分块

f4009caa-34fc-48d6-8102-3d0f6f2c1386_1066x316

先依据大的段落进行分块,再对每个块进行处理,若符合chunk-size的限制,则不会再分

结果可能如下

b0e40cc1-996f-48f4-9306-781b112536e4_984x428

首先,我们定义两个块(紫色的两个段落。接下来,第1段进一步拆分为更小的块。

1
2
3
4
5
6
7
8
from langchain_text_splitters import RecursiveCharacterTextSplitter

recursive_splitter_chinese = RecursiveCharacterTextSplitter(
chunk_size=50,
chunk_overlap=10,
length_function=len,
separators=["\n\n", "。", ",", " ", ""]#中文的分隔符,可以用逗号句号
)

Document structure-based chunking 基于文档结构的分块

e8febecd-ee68-42ff-ab06-41a0a3a43cd3_1102x306

根据文档的固有结构进行分块,如markdown的一级标题二级标题等

langchain.text_splitter中有两个用于md文档分块的类,MarkdownTextSplitterMarkdownHeaderTextSplitter

二者区别主要在:前者继承于RecursiveCharacterTextSplitter递归分块,它会尝试沿着 Markdown 格式的标题进行分割,但其核心仍然是基于字符的递归分割;后者专注于 基于 Markdown 标题的结构化分割 ,并能将标题信息作为元数据保留,更适合需要保持 Markdown 文档层级结构的应用场景。

需要注意的是MarkdownHeaderTextSplitter 本身不直接提供限制块内容长度的参数,但可以通过与 RecursiveCharacterTextSplitter 等其他文本分割器结合使用来有效控制块的大小。

1
2
3
4
5
6
7
8
9
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_document)

存储结构类似如下

1
2
3
[Document(metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}, page_content='Hi this is Jim  \nHi this is Joe'),
Document(metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}, page_content='Hi this is Lance'),
Document(metadata={'Header 1': 'Foo', 'Header 2': 'Baz'}, page_content='Hi this is Molly')]

LLM-based chunking 基于 LLM 的分块

4d1b6d60-8956-4030-8525-d899ee61a9d5_1140x198

利用大模型进行分块

langchain没有提供官方的类实现LLM-based chunking

但是我在找到了别人实现的agentic_chunkerRetrievalTutorials/tutorials/LevelsOfTextSplitting/agentic_chunker.py at main · FullStackRetrieval-com/RetrievalTutorials,可供参考

后记:agentic chunk大概的思路为先进行初步分段,按照长度或递归,然后让大模型生成这一段的概要,将段与段合并生成块,但是测试下来,一个文档的内容同质化很严重,基本上都分到一块里了,而且这个主要还是提示词工程,分块并不系统,看个乐吧

chunks-strategy-/agentic_chunker.py at main · zxj-2023/chunks-strategy-代码稍作更新,弃用了部分库

embedding

之前对chunking和embedding的理解不够清晰,chunking是对文本进行分块,由于大多数文本嵌入模型对输入文本长度有严格限制,如果不分块则无法embedding,从而无法更好的进行向量化或者更好地储存在知识库中,提升retriever性能;embedding则是将文本映射到向量空间,为了更好的相似度计算

语义分块的源代码实战

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
'''
将文本划分成单句,可以按照标点符号划分
'''
single_sentences_list = re.split(r'(?<=[。!?])', essay)
# 移除可能存在的空字符串
single_sentences_list = [s.strip() for s in single_sentences_list if s.strip()]

'''
我们需要为单个句子拼接更多的句子,但是 `list` 添加比较困难。因此将其转换为字典列(`List[dict]`)
{ 'sentence' : XXX , 'index' : 0}
'''
sentences = [{'sentence': x, 'index' : i} for i, x in enumerate(single_sentences_list)]

#利用滑动窗口分段
def combine_sentences(sentences, buffer_size=1):
combined_sentences = [
' '.join(sentences[j]['sentence'] for j in range(max(i - buffer_size, 0), min(i + buffer_size + 1, len(sentences))))
for i in range(len(sentences))
]
# 更新原始字典列表,添加组合后的句子
for i, combined_sentence in enumerate(combined_sentences):
sentences[i]['combined_sentence'] = combined_sentence

return sentences

sentences = combine_sentences(sentences)

'''
接下来使用**embedding model**对**sentences** 进行编码
'''
from langchain_community.embeddings import DashScopeEmbeddings
embeddings_model = DashScopeEmbeddings(
model="text-embedding-v2",
dashscope_api_key="",

)
# 提取所有组合后的句子用于 embedding
combined_sentences_to_embed = [x['combined_sentence'] for x in sentences]

# 对句子进行 embedding
embeddings = embeddings_model.embed_documents(combined_sentences_to_embed)
print(f"成功对 {len(embeddings)} 个句子进行了 embedding。")

#将embedding添加到sentence中
for i, sentence in enumerate(sentences):
sentence['combined_sentence_embedding'] = embeddings[i]
'''
接下来需要根据余弦相似度进行切分
通过计算两个向量的夹角余弦值来衡量相似性
'''
import numpy as np
def cosine_similarity(vec1, vec2):
"""Calculate the cosine similarity between two vectors."""
dot_product = np.dot(vec1, vec2)
norm_vec1 = np.linalg.norm(vec1)
norm_vec2 = np.linalg.norm(vec2)
return dot_product / (norm_vec1 * norm_vec2)
#遍历,计算余弦相似度
def calculate_cosine_distances(sentences):
distances = []
for i in range(len(sentences) - 1):
embedding_current = sentences[i]['combined_sentence_embedding']
embedding_next = sentences[i + 1]['combined_sentence_embedding']
# Calculate cosine similarity
similarity = cosine_similarity(embedding_current, embedding_next)
# Convert to cosine distance
distance = 1 - similarity
distances.append(distance)
# Store distance in the dictionary
sentences[i]['distance_to_next'] = distance
return distances, sentences

distances, sentences = calculate_cosine_distances(sentences)

#根据阈值划分
breakpoint_percentile_threshold = 95
breakpoint_distance_threshold = np.percentile(distances, breakpoint_percentile_threshold)
print("距离的第95个百分位阈值是:", breakpoint_distance_threshold)
# 找到所有距离大于阈值的点的索引,这些索引就是我们的切分点
indices_above_thresh = [i for i, x in enumerate(distances) if x > breakpoint_distance_threshold]

# 初始化块的起始句子索引。我们将根据之前计算出的语义分割点(`indices_above_thresh`)来切分句子列表。
start_index = 0

# 创建一个列表,用于存储最终组合成的、具有语义连贯性的文本块。
chunks = []

# 遍历所有识别出的语义分割点(这些是句子列表 `sentences` 中的索引)。
for index in indices_above_thresh:
# 确定当前文本块的结束点,即当前的分割点索引。
end_index = index

# 从原始句子列表(`sentences`)中切片,提取从上一个分割点到当前分割点之间的所有句子。
# `end_index + 1` 是为了在切片时包含结束索引指向的那个句子。
group = sentences[start_index:end_index + 1]

# 将切分出的句子组(`group`)中的所有 'sentence' 字段的值合并成一个单独的字符串,句子之间用空格隔开。
combined_text = ' '.join([d['sentence'] for d in group])

# 将合并后的文本块添加到 `chunks` 列表中。
chunks.append(combined_text)

# 更新下一个文本块的起始索引,设置为当前分割点的下一个位置,为处理下一个块做准备。
start_index = index + 1

# 处理最后一个文本块。
# 循环结束后,如果 `start_index` 仍然小于句子总数,说明从最后一个分割点到文本末尾还有剩余的句子。
if start_index < len(sentences):
# 将这些剩余的句子合并成最后一个文本块。
combined_text = ' '.join([d['sentence'] for d in sentences[start_index:]])
chunks.append(combined_text)

# 此时,`chunks` 列表包含了所有根据语义距离切分和重组后的文本块。
for i, chunk in enumerate(chunks):
buffer = 200
print (f"Chunk #{i}")
print (chunk[:buffer].strip())
print ("...")
print (chunk[-buffer:].strip())
print ("\n")

参考资料

RAG 的 5 种分块策略 — 5 Chunking Strategies For RAG

一文读懂 Qwen3 最新开源的 Embedding 和 Rerank 模型优势!_qwen-rerank-CSDN博客

一站帮你选择RAG中的文本切分策略_哔哩哔哩_bilibili

LangChain 语义文本拆分指南:基于语义相似度的智能分块技术实战_langchain 语义分割-CSDN博客

镜像(Image)

镜像可以被看作是一个轻量级、可执行的独立软件包,包含了运行某个应用所需的所有代码、库、环境变量和配置文件。它是容器的静态模板,在创建容器时用作基础。

只读:镜像本身是只读的,无法修改。

可重用:镜像是可以多次重用的,你可以基于相同的镜像创建多个容器。

容器(Container)

与虚拟机通过操作系统实现隔离不同,容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统,这里的运行时环境指的是程序运行依赖的各种库以及配置。

容器更加的轻量级且占用的资源更少,与操作系统动辄几G的内存占用相比,容器技术只需数M空间,因此我们可以在同样规格的硬件上大量部署容器,这是虚拟机所不能比拟的,而且不同于操作系统数分钟的启动时间容器几乎瞬时启动,容器技术为打包服务栈提供了一种更加高效的方式

镜像与容器的关系

镜像是静态的:它只包含应用和运行环境,不能进行任何运行时的操作。你可以把它看作是软件的安装包

容器是动态的:它是在镜像的基础上创建的,可以运行、执行代码、修改文件系统等。你可以把它看作是镜像的运行实例

docker

docker将程序以及程序所有的依赖都打包到docker container,这样你的程序可以在任何环境都会有一致的表现

此外docker的另一个好处就是快速部署,这是当前互联网公司最常见的一个应用场景,一个原因在于容器启动速度非常快,另一个原因在于只要确保一个容器中的程序正确运行,那么你就能确信无论在生产环境部署多少都能正确运行。

每一种容器都是一个完整的运行环境,容器之间互相隔离。

简单来说,docker将程序打包部署,方便了软件的部署,避免了环境冲突等问题

常用命令

查看所有容器(包括停止的容器):docker ps -a

在Docker中运行容器:docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

  • [OPTIONS]:可选参数,用于配置容器的各种选项,如端口映射、容器名称等。
  • IMAGE:要运行的镜像名称或ID。
  • [COMMAND] [ARG...]:可选的命令和参数,用于在容器内执行特定的命令。

停止正在运行的容器:docker stop [OPTIONS] CONTAINER [CONTAINER...]

启动已停止的容器:docker start [OPTIONS] CONTAINER [CONTAINER...]

删除已停止的容器或镜像:docker rm [OPTIONS] CONTAINER [CONTAINER...] docker rmi [OPTIONS] IMAGE [IMAGE...]

  • docker rm:删除容器的命令。
  • docker rmi:删除镜像的命令。

从Docker仓库中拉取现有的镜像:docker pull [OPTIONS] NAME[:TAG|@DIGEST]

  • docker pull:拉取镜像的命令。
  • [OPTIONS]:可选参数,用于配置拉取过程,如认证信息等。
  • NAME[:TAG|@DIGEST]:要拉取的镜像名称、标签或摘要。

docker部分指令

linux安装dockersudo apt-get update && sudo apt-get install docker.io

查看 Docker 版本信息docker version

查看镜像docker images

查看所有的容器docker ps -a

systemctlsystemd 系统和服务管理器的核心工具,用于管理系统和服务的状态及配置。

mysql-client 是 MySQL 数据库的命令行客户端工具。它允许你通过命令行连接和操作 MySQL 数据库服务器,比如执行 SQL 查询、管理数据库和用户等。

常用命令格式如下:mysql -h 主机地址 -P 端口号 -u 用户名 -p

你可以在终端输入以下命令来检查是否已安装 mysql-clientmysql --version

可以使用以下命令安装:sudo apt-get update sudo apt-get install mysql-client

sudo apt-get update 这个命令的作用是更新本地软件包列表

停止并删除容器docker stop fastapi docker rm fastapi

Linux修改镜像源如何查看docker配置的镜像仓库_查看docker镜像地址-CSDN博客

常见参数

基础参数:

-d--detach 后台运行容器(detached mode)
--name <name> 为容器指定名称(如--name my_container
--rm 容器停止后自动删除(适用于临时容器)

端口映射:

-p <主机端口>:<容器端口> 映射主机端口到容器端口(如-p 80:80
-p <主机IP>:<主机端口>:<容器端口> 指定主机IP绑定(如-p 127.0.0.1:8080:80
-P--publish-all 自动映射所有暴露的端口(随机分配主机端口)

卷挂载:

-v <主机路径>:<容器路径> 挂载主机目录到容器(如-v //app
-v <卷名>:<容器路径> 使用命名卷(如-v my_volume:/data

环境变量:

-e <KEY=VALUE> 设置环境变量(如-e DEBUG=true
--env-file <文件名> 从文件加载环境变量(每行KEY=VALUE

网络配置:

--network <网络名> 指定容器使用的网络(如--network bridge或自定义网络)
--network host 使用主机网络(共享主机网络命名空间)

拯救被wsl占用的内存

以笔者的情况来说,我的wsl中只有一些必备的开发环境,项目源代码 和 docker。前两者显然没啥可操作的空间,所以只有一个靶子 —— docker。

首先,我们可以进入wsl,通过以下命令,看看 Docker 的磁盘使用情况和资源总量。

1
docker system df 

大家都知道,docker运行一段时间后,可能会产生一些无用的镜像文件。要清理无用的 Docker 镜像,则可以运行以下命令:

1
docker image prune 

该命令可以删除所有未被任何容器使用的镜像。如果想清理所有已停止的容器和未使用的镜像:

1
docker system prune -a

执行完后咱们可以再运行第一个命令查看磁盘使用情况,大概率能看到释放了一部分磁盘空间。如果确实长时间为清理过,很大可能可释放几十G。

然而这时候我们退出wsl回到win10, 你可能会看到磁盘空间几乎没啥变化。这是因为wsl还需要我们手动释放这部分空间,即压缩磁盘。

修改docker存储镜像位置

windows

image-20250709095041821
image-20250709092539404
image-20250709092605183

Linux

修改Docker默认镜像和容器存储位置(超详细!!!)_docker更改存储位置-CSDN博客

修改镜像源

查看可用的镜像源DockerHub加速器可用性监控

国内能用的Docker镜像源【2025最新持续更新】_docker 镜像-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.1ms.run",
"https://ccr.ccs.tencentyun.com",
"https://hub.xdark.top",
"https://hub.fast360.xyz",
"https://docker-0.unsee.tech",
"https://docker.xuanyuan.me",
"https://docker.tbedu.top",
"https://docker.hlmirror.com",
"https://doublezonline.cloud",
"https://docker.melikeme.cn",
"https://image.cloudlayer.icu",
"https://dislabaiot.xyz",
"https://freeno.xyz",
"https://docker.kejilion.pro"
]

如何使用vscode进入远程服务器的docker容器内部调试代码

安装一下插件

9a3be11a07b5a0866d09d1bcbbaae4dc
image-20250716165555700

build,pull与run

docker build:从源代码构建镜像

  • 作用:根据你提供的 Dockerfile(一个包含构建镜像所需指令的文本文件)以及上下文(通常是包含 Dockerfile 的目录及其子目录),创建一个新的 Docker 镜像。

docker pull:从注册中心下载镜像

  • 作用:从 Docker 注册中心(默认是 Docker Hub,也可以是私有注册中心如 Harbor, GitLab Registry, AWS ECR 等)下载一个已经构建好的 Docker 镜像到你的本地机器。

docker run:创建并启动容器

  • 作用:基于一个本地已有的镜像(无论这个镜像是你刚 build 出来的,还是 pull 下来的,或是之前就存在的),创建一个新的容器实例,并按照指定的命令(或镜像默认的命令)启动它。

docker的自定义网络

创建自定义桥接网络

1
docker network create mynet

启动服务容器(不映射宿主机端口也能被同网络容器访问)

1
docker run -d --name api --network demo-net fastapi-svc

列出所有网络(包括自定义网络)

1
2
3
4
docker network ls

#只列出自定义网络(过滤掉默认网络)
docker network ls --filter type=custom

查看某个自定义网络的详细信息

1
docker network inspect network_test 

Docker网络介绍_哔哩哔哩_bilibili

docker compose

Docker Compose基础与语法_哔哩哔哩_bilibili

参考文献

改变软件行业的技术!程序员、软件爱好者必须掌握的Docker,到底是什么?_哔哩哔哩_bilibili

什么是Docker?看这一篇干货文章就够了! - 知乎

Docker常用命令大全(非常详细)零基础入门到精通,收藏这一篇就够了-CSDN博客

40分钟的Docker实战攻略,一期视频精通Docker_哔哩哔哩_bilibili

docker部署

使用dockerfile构建镜像:

1
2
wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/docker/china/Dockerfile
docker build -t mineru-sglang:latest -f Dockerfile .

使用wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU @master/docker/china/Dockerfile -O Dockerfile将指定的 Dockerfile 下载到本地

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用官方的 sglang 镜像作为基础镜像
FROM lmsysorg/sglang:v0.4.9-cu126

# 安装 OpenCV 依赖库
RUN apt-get update && apt-get install -y libgl1 && apt-get clean && rm -rf /var/lib/apt/lists/*

# 安装 mineru Python 包
RUN python3 -m pip install -U 'mineru[core]' -i https://mirrors.aliyun.com/pypi/simple --break-system-packages

# 下载模型并配置
RUN /bin/bash -c "mineru-models-download -s modelscope -m all"

# 设置容器入口命令
ENTRYPOINT ["/bin/bash", "-c", "export MINERU_MODEL_SOURCE=local && exec \"$@\"", "--"]

SGLang(全称可能为 Serving Large Language Models with Golang )是由斯坦福大学研究团队开发的一个高效的大语言模型(LLM)推理服务框架 ,旨在通过优化模型推理过程,显著提升生成式AI服务的吞吐量和响应速度。

  • SGlang 版本v0.4.8.post1(SGlang 是一个用于大语言模型(LLM)推理和服务的高性能框架)。
  • CUDA 版本cu126 表示使用 CUDA 12.6 ,适用于 Turing/Ampere/Ada Lovelace/Hopper 架构的 GPU (如 RTX 30/40 系列、A100、H100)。

报错排查

之前由于默认dockerfile内容为FROM lmsysorg/sglang:v0.4.8.post1-cu126报错

1
lmsysorg/sglang:v0.4.8.post1-cu126: failed to resolve source metadata for docker.io/lmsysorg/sglang:v0.4.8.post1-cu126: unexpected status from HEAD request to https://yaj2teeh.mirror.aliyuncs.com/v2/lmsysorg/sglang/manifests/v0.4.8.post1-cu126?ns=docker.io: 403 Forbidden

之前以为是sglang版本问题,然后去dockerhub上查找,并通过docker pull sglang:v0.4.8.post1-cu126测试,是可以拉取的,最后认为原因还是网络问题

解决方法,更换了镜像源

镜像源配置

1
2
3
4
5
6
7
8
9
10
11
12
"registry-mirrors": [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://dockerhub.azk8s.cn",
"https://mirror.ccs.tencentyun.com",
"https://registry.cn-hangzhou.aliyuncs.com",
"https://docker.mirrors.ustc.edu.cn",
"https://docker.m.daocloud.io",
"https://noohub.ru",
"https://huecker.io",
"https://dockerhub.timeweb.cloud"
]

保姆级Docker安装+镜像加速 计算机系必备技能_哔哩哔哩_bilibili

为什么要指定基础镜像

  • 提供操作系统和依赖 基础镜像包含操作系统(如 Ubuntu、Alpine)、运行时环境(如 Python、Node.js)或框架(如 TensorFlow、PyTorch)等核心组件,后续所有操作(如安装依赖、拷贝文件)都基于此环境。
    • 例如:FROM python:3.9 提供了 Python 3.9 的运行环境,后续可以直接用 pip install 安装 Python 包。
  • 避免重复造轮子 如果直接从空镜像(scratch)开始,需要手动安装所有依赖,效率低下且容易出错。使用现有基础镜像可以复用已验证的环境配置。

确认支持的cuda版本

命令nvidia-smi

image-20250708160948192

CUDA Version 显示当前驱动支持的最高 CUDA 版本

问题:使用dockerfile直接部署,始终出现网络问题

解决方案

先修改了一下docker储存镜像的位置,太大了

先拉取基础镜像docker pull lmsysorg/sglang:v0.4.8.post1-cu126

再使用docker build -t mineru-sglang:latest -f Dockerfile .,可以直接跳过基础镜像的拉取

启动

官方启动命令

1
2
3
4
5
6
7
8
docker run -d \
--name sglang-server \ # 容器命名(便于管理)
--gpus all \ # 启用所有GPU
--shm-size 32g \ # 共享内存大小
-p 30000:30000 \ # 端口映射(主机端口:容器端口)
--ipc=host \ # 共享主机IPC命名空间
mineru-sglang:latest \
mineru-sglang-server --host 0.0.0.0 --port 30000
1
docker run -d --name sglang-server --gpus all --shm-size 32g -p 30000:30000 --ipc=host mineru-sglang:latest mineru-sglang-server --host 0.0.0.0 --port 30000

将mineru-sglang-server暴露到30000端口的作用

为了支持 vlm-sglang-client 后端模式,使得MinerU客户端可以通过网络连接到这个服务器,实现多个客户端可以同时连接到同一个服务器

使用docker exec -it sglang-server bash命令进入容器

使用docker desk

image-20250709152627120
1
2
3
4
5
docker run -d --name mineru-server --gpus all --shm-size 32g -p 30000:30000 -p 7860:7860 -p 8000:8000 --ipc=host \
-v "F:/project python/实习/mineru/demo/pdfs:/pdfs" \
-v "F:/project python/实习/mineru/output:/output" \
mineru-sglang:latest \
mineru-sglang-server --host 0.0.0.0 --port 30000

使用挂载卷启动

  • 将本地的PDF文件目录挂载到容器内的 /pdfs 目录
  • 将本地的输出目录挂载到容器内的 /output 目录
  • 把8000,和7860端口暴露,方便调用fastapi与gradio webui 可视化
image-20250710100047464
image-20250710103505453

调用

命令行调用sglang-server/client 模式

docker exec mineru-server mineru -p /pdfs/demo1.pdf -o /output -b vlm-sglang-client -u http://localhost:30000

这条命令在名为 mineru-server 的容器内执行 mineru 工具,处理 /pdfs 目录下的 demo1.pdf 文件,输出结果到 /output 目录,使用 vlm-sglang-client 后端,并连接到 http://localhost:30000 的SGLang服务器。

image-20250710103615272

fastapi调用与gradio webui 可视化

在完成docker的端口映射之后,运行mineru-api --host 0.0.0.0 --port 8080启动fastapi服务,

FastAPI服务的使用场景

FastAPI服务提供了一个/file_parse端点,用于处理PDF和图像文件的解析请求

微服务架构部署,FastAPI服务可以独立部署

服务提供了标准的HTTP API接口,允许客户端通过网络请求进行文档解析

运行mineru-gradio --server-name 0.0.0.0 --server-port 7860启动gradio webui服务

mineru-gradio --server-name 0.0.0.0 --server-port 7860 --enable-sglang-engine true

注意,模型下载需要配置环境变量

1
2
3
4
# 在容器内设置环境变量
export MINERU_MODEL_SOURCE=local
# 验证环境变量是否设置成功
echo $MINERU_MODEL_SOURCE
115678648318f55f1fe3a5baaeac2aaf

在调用过程中关于端口的问题与思考

调用过程中发现,在容器中使用mineru-api --host 127.0.0.1 --port 8000,宿主机无法访问http://127.0.0.1:8000/docs/,经过查询ai,命令改为mineru-api --host 0.0.0.0 --port 8000就可以正常访问,那么关键在于对这两个地址的理解

查看端口netstat -ano | findstr LISTENING

127.0.0.1与0.0.0.0

  • 127.0.0.1 (localhost) :仅表示本机回环地址,只能在 同一设备内 访问
  • 0.0.0.0 :表示监听所有可用的网络接口,允许 来自任何地址 的连接

当您在Docker容器内运行服务时:

  1. 使用127.0.0.1作为绑定地址 :

    • 服务只接受来自容器内部的连接
    • 即使您映射了端口,宿主机也无法访问该服务
    • 只有容器内的应用程序可以通过 127.0.0.1:端口 访问
  2. 使用0.0.0.0作为绑定地址 :

    • 服务接受来自任何网络接口的连接请求
    • 允许从容器外部(包括宿主机)访问该服务
    • 当您映射端口时(如 -p 8000:8000 ),宿主机可以通过 localhost:8000 或 127.0.0.1:8000 访问

为什么需要在容器内使用0.0.0.0

在Docker环境中,容器有自己独立的网络命名空间,这意味着容器内的 127.0.0.1 与宿主机的 127.0.0.1 是完全不同的两个环境。因此:

  • 当您在容器内使用 –host 0.0.0.0 启动服务时,该服务会监听容器的所有网络接口
  • 当您在宿主机上访问 127.0.0.1:映射端口 时,Docker会将请求转发到容器内监听在 0.0.0.0:容器端口 的服务

mineru相关知识

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Usage: mineru [OPTIONS]

Options:
-v, --version 显示版本并退出
-p, --path PATH 输入文件路径或目录(必填)
-o, --output PATH 输出目录(必填)
-m, --method [auto|txt|ocr] 解析方法:auto(默认)、txt、ocr(仅用于 pipeline 后端)
-b, --backend [pipeline|vlm-transformers|vlm-sglang-engine|vlm-sglang-client]
解析后端(默认为 pipeline)
-l, --lang [ch|ch_server|ch_lite|en|korean|japan|chinese_cht|ta|te|ka|latin|arabic|east_slavic|cyrillic|devanagari]
指定文档语言(可提升 OCR 准确率,仅用于 pipeline 后端)
-u, --url TEXT 当使用 sglang-client 时,需指定服务地址
-s, --start INTEGER 开始解析的页码(从 0 开始)
-e, --end INTEGER 结束解析的页码(从 0 开始)
-f, --formula BOOLEAN 是否启用公式解析(默认开启)
-t, --table BOOLEAN 是否启用表格解析(默认开启)
-d, --device TEXT 推理设备(如 cpu/cuda/cuda:0/npu/mps,仅 pipeline 后端)
--vram INTEGER 单进程最大 GPU 显存占用(GB)(仅 pipeline 后端)
--source [huggingface|modelscope|local]
模型来源,默认 huggingface
--help 显示帮助信息

后端的区别

pipeline (默认后端) :

  • 含义 : 这是 MinerU 的默认后端,它使用本地安装的 mineru 库来执行文档解析任务。它通常不依赖于外部的 VLM(视觉语言模型)服务,而是直接在本地处理 PDF 文件。
10d6d1a7f702c5806e29ee7b1c51283

vlm-transformers :

  • 含义 : 这个后端利用 Hugging Face transformers 库中提供的 VLM 模型进行文档分析。它会在本地加载并运行一个基于 transformers 的 VLM 模型来处理 PDF 中的视觉信息和文本内容。

VLM(Vision-Language Model,视觉语言模型)是一种结合计算机视觉和自然语言处理能力的多模态人工智能模型。

OCR 是 Optical Character Recognition(光学字符识别)的缩写。它是一种技术,用于将图像中的手写、打印或打字文本转换为机器编码的文本,使其可以被计算机编辑、搜索、存储和处理。

vlm-sglang-engine :

  • 含义 : 这个后端表示 MinerU 将直接集成并使用 SGLang 引擎进行 VLM 推理。SGLang 是一个高性能的推理引擎,旨在优化大型语言模型(LLM)和 VLM 的推理速度和效率。在这种模式下,SGLang 引擎作为 MinerU 进程的一部分运行。

vlm-sglang-client :

  • 含义 : 这个后端表示 MinerU 作为 SGLang 服务器的客户端。在这种模式下,MinerU 不会直接运行 VLM 模型,而是将 PDF 处理请求发送到一个独立的 SGLang 服务器(通过 -u 参数指定的 URL,例如 http://localhost:30000 )。SGLang 服务器负责执行实际的 VLM 推理,并将结果返回给 MinerU 客户端。

用场景 : 这是我们之前讨论的 Docker 容器部署场景中推荐的模式。它非常适合以下情况: - 资源隔离 : 将 VLM 推理的计算密集型任务从 MinerU 主进程中分离出来,允许独立扩展和管理 SGLang 服务器。 - 集中管理 : 可以在一个或多个 SGLang 服务器上集中管理 VLM 模型,供多个 MinerU 客户端共享使用。 - 性能优化 : SGLang 服务器可以针对 VLM 推理进行专门优化,提供更好的吞吐量和延迟。 - 灵活部署 : SGLang 服务器可以部署在不同的机器上,甚至作为微服务运行,提供更大的部署灵活性。

关于吞吐量的相关知识

“吞吐量”(throughput)*指的是系统在单位时间内能处理的 **Token 数量**,单位通常是 **tokens/秒**。这个指标衡量的是整体系统*处理并发请求的能力,而不仅仅是单个请求的速度。

SGLang 支持两种主要并行方式来提升吞吐量:

并行类型 作用 对吞吐量的影响
张量并行(TP) 把模型权重切分到多张卡上,减少单卡负载 提升单请求处理能力,但通信开销大
数据并行(DP) 把不同请求分发到不同卡上,并行处理多个请求 直接提升并发吞吐量,尤其适合高并发场景

mineru的并发测试和吞吐量测试

并发能力是测试mineru同时处理多个请求的能力,吞吐量是测试mineru处理文件时的tokens

明确要的是哪个吞吐量:一个是MinerU 内部推理引擎(如 vLLM/SGLang)的 token/s 输出,即 生成阶段(decode)的吞吐量,一个是你压测 /file_parse 接口时的 端到端 tokens/s

是否有缓存(kvcache)

测试场景:10页的pdf,50用户并发

工具:locust

1
2
3
4
5
6
7
8
locust \
-f locustfile.py \
--headless \
-u 50 \ # 并发用户数
-r 5 \ # 每秒启动用户数
--host=http://mineru-server:30000 \
--html=report.html \ # 自动生成 HTML 报告
--csv=result # 同时保存 csv(result_stats.csv / result_failures.csv)
1
locust -f locustfile.py --headless -u 100 -r 5 --host=http://mineru-server:30000 --html=report.html --csv=result

测试结果

image-20250805152017255

对于推理模型的吞吐量,在3个gpu开启数据并行的情况下,平均每秒单个gpu处理tokens为1500左右

image-20250805152149822

gpu状态如上:显存几乎打满 85–87 %,GPU 利用率 59–63 %,功耗 170–188 W / 350 W

image-20250805153516235

完整压测结果如上

重要指标:

指标 数值 通俗解释
平均响应时间 241 秒4 分钟 上传一个 PDF → 拿到解析结果,平均要等 4 分钟。
中位数 215 秒3.6 分钟 一半请求在 3.6 分钟内完成。
95% 用户 361 秒6 分钟 最慢的 5% 要等 6 分钟以上。
吞吐量 0.18 req/s 这台 MinerU 每分钟只能处理约11 个 PDF
image-20250806104340935
image-20250806110438932

参考资料

MinerU/projects/multi_gpu_v2/README_zh.md at be4f3de32b58ccf81c6a6dcb9d3e4998424cee6a · opendatalab/MinerU

部署服务器并运行

load镜像

1
docker load -i mineru-sglang-latest.tar
1
2
free -h
nvidia-smi
image-20250716092152823

每秒刷新watch -n1 nvidia-smi # 每秒刷新

查看Linux路径

1
pwd 

启动容器

1
2
3
4
5
docker run -d --name mineru-server --gpus all --shm-size 32g -p 30000:30000 -p 7860:7860 -p 8000:8000 --ipc=host \
-v "/aisys/repo_dev/xizhang/pdfs:/pdfs" \
-v "/aisys/repo_dev/xizhang/outputs:/output" \
mineru-sglang:latest \
mineru-sglang-server --host 0.0.0.0 --port 30000
1
docker run -d --name mineru-server --gpus all --shm-size 32g -p 30000:30000 --ipc=host -v "/aisys/repo_dev/xizhang/pdfs:/pdfs" -v "/aisys/repo_dev/xizhang/outputs:/output" mineru-sglang:latest mineru-sglang-server --host 0.0.0.0 --port 30000

调用

1
docker exec mineru-server mineru -p /pdfs/demo1.pdf -o /output -b vlm-sglang-client -u http://localhost:30000`

进入容器

1
docker exec -it mineru-server /bin/bash

使用pipline解析后端模式

1
mineru -p /pdfs/demo1.pdf -o /output --source local

使用sglang加速推理

1
CUDA_VISIBLE_DEVICES=1,2,3 mineru -p /pdfs/small_ocr.pdf -o /output -b vlm-sglang-engine --source local

vlm模式同样可以处理扫描件

使用ocr解析扫描件

1
mineru -p /pdfs/small_ocr.pdf -o /output --source local -m ocr

增加推理设备

1
mineru -p /pdfs/small_ocr.pdf -o /output --source local -m ocr -d cuda

通过在命令行的开头添加CUDA_VISIBLE_DEVICES 环境变量来指定可见的 GPU 设备。

1
CUDA_VISIBLE_DEVICES=1,2,3 mineru -p /pdfs/small_ocr.pdf -o /output --source local -m ocr

使用sglang加速模式的多GPU并行

数据并行(dp-size)和张量并行(tp-size)

MinerU支持通过sglang的多GPU并行模式来提升推理速度。您可以使用以下参数:

  • --dp-size: 数据并行,通过多卡同时处理多个输入来增加吞吐量
  • --tp-size: 张量并行,将模型分布到多张GPU上以扩展可用显存

如果您已经可以正常使用sglang对vlm模型进行加速推理,但仍然希望进一步提升推理速度,可以尝试以下参数:

  • 如果您有超过多张显卡,可以使用sglang的多卡并行模式来增加吞吐量:--dp-size 2
  • 同时您可以启用torch.compile来将推理速度加速约15%:--enable-torch-compile
1
CUDA_VISIBLE_DEVICES=1,2,3 mineru -p /pdfs -o /output -b vlm-sglang-engine --source local --dp-size 3 --enable-torch-compile

将python文件上传docker并运行

1
2
3
4
5
6
docker cp demo.py mineru-server:/demo.py

docker exec mineru-server python /demo.py

#删除
rm -i demo.py

添加自定义网络,修改挂载卷

1
docker run -d --name mineru-server --gpus all --shm-size 32g -p 30000:30000 --ipc=host -v /aisys/:/aisys/ --network network_test mineru-sglang:latest mineru-sglang-server --host 0.0.0.0 --port 30000

后面才知道,上面这个命令会自动启动sglang-server服务

1
2
docker run -d --name mineru-server --gpus all --shm-size 32g -p 30000:30000 --ipc=host -v /aisys/:/aisys/ --network network_test mineru-sglang:latest tail -f /dev/null
docker start mineru-server

使用上面这个命令启动容器,但不启动sglang-server服务,使用下面指令手动启动

1
2
docker exec -it mineru-server /bin/bash
MINERU_MODEL_SOURCE=local CUDA_VISIBLE_DEVICES=1,2,3 mineru-sglang-server --port 30000 --dp-size 3 --enable-torch-compile

启动服务后在另一个容器尝试访问

1
curl http://mineru-server:30000/get_model_info

在另一个容器使用服务

1
mineru -p /test -o / -b vlm-sglang-client -u http://mineru-server:30000

启动fastapi服务

1
MINERU_MODEL_SOURCE=local CUDA_VISIBLE_DEVICES=1,2,3 mineru-api --host 0.0.0.0 --port 30000 --dp-size 3 --enable-torch-compile

在另一个容器验证

1
curl http://mineru-server:30000/openapi.json
1
2
3
4
5
6
7
8
9
# 在容器内设置环境变量
export CUDA_VISIBLE_DEVICES=1,2,3
# 验证环境变量是否设置成功
echo $CUDA_VISIBLE_DEVICES

# 在容器内设置环境变量
export MINERU_MODEL_SOURCE=local
# 验证环境变量是否设置成功
echo $MINERU_MODEL_SOURCE

资料

MinerU 2.0部署-CSDN博客

https://github.com/opendatalab/MinerU?tab=readme-ov-file#local-deployment

https://deepwiki.com/opendatalab/MinerU

MinerU:PDF处理神器的Pipeline和VLM两种模式大揭秘_哔哩哔哩_bilibili

日志

7.16 10:46

已经将mineru部署至服务器,完成pdf和扫描件的提取测试,但是mineru是没办法直接提取doc与docx的,能不能直接对doc与docx进行文本分块,或者先转换成pdf再提取(官方给出的解决方案:通过独立部署的LibreOffice服务先行转换为PDF格式,再进行后续解析操作。)

7.17 11:04

完成mineru脚本的编写,仅输出提取的md与image;实现调用多块gpu;实现数据并行,通过多卡同时处理多个输入来增加吞吐量;使用sglang框架

7.18 14:37

发现mineru提取的markdown文档是不带多级标题的,只有一级标题,所以不考虑文档结构分块;语义分块要调用embedding模型,而且文档里有很多表格,我感觉效果不一定会好;后续我想法是使用递归分块,表格在md文档中以html表格格式存储的,大量冗余信息,我想先对其进行预处理,转换成md表格的形式吧(用“ |”存储),然后再分块,或许效果会好一些,正在进行

还有一个就是libreoffice是部署在哪里,我没有找到诶

7.19 10:26

完成表格预处理的脚本编写,完成对md文档的预处理;完成递归分块,后续完成存入es数据库

7.21

完成简单地将分块结果存入elasticsearch(mapping只有content字段,使用http请求存的,没有用langchain-elasticsearch)后面要试试langchain-elasticsearch,去看看字段的处理

7.22

完成langchain-elasticsearch的bm25的检索测试,接下来尝试使用服务器的embedding模型进行测试,阅读项目文件,理清思路,具体内容见多模态 - PDF表格图片&扫描件,存入es与文件处理字段处理部分已经理清了,成功存入服务器的es

7.23

今天在把之前做的所有工作进行整合,编写一个完整的代码,实现生产的流程,遇到的主要问题有两个:1.我还是没有很看懂之前对字段的存储,和字段的结构2.我没有太理解pdf分块的部分在哪里,是没有吗,我看doc的rewrite_word是有文本分块的

感觉需要你给我讲一下,不然我后面不大知道该怎么处理,那我明天去把语义分块和其他的检索方案试一下吧,整合代码先稍微延后

7.24

使用docker的自定义网络,实现容器之间的通信,从而可以在repo容器中调用mineru,其他容器都在network_test下,我把mineru也启动到里面

成功在repo容器访问到mineru容器,完成pdf分块数据的批量写入elasticsearch并可以成功检索,后续完成调用mineru的fastapi接口(还没编完),继续完成代码整合(已经完成大部分,主要差mineru的部分,和一些衔接的代码)

7.25

完成通过接口使用mineru批量处理pdf文件

7.28

完成了代码整合,可以完成pdf整个流程的处理

7.29

补充了libreoffice的代码,实现了将doc,docx转换成pdf,统一处理流程;完成了数据标记webhttps://traedemortu8-zxj2902065320-7643-junxi-zhangs-projects.vercel.app/data;后续计划:根据问题检索url,处理后存入zxj_test es数据库,再进行进一步检索产生数据集

7.30

完成扫描件的测试,文字公式图片皆可正常识别;根据问题完成文档的收集工作,共648份文档;目前正在进行将文档录入es数据库,目前存在问题,部分docx文件在转换成pdf的过程中会导致libreoffice卡死,原因暂未查明;解决方案:跳过这些文件,缺几个影响应该也不大

7.31

已完成621/648文件的存储

8.1

已完成数据集的建立,检索了140个问题,每个问题选取top16构建数据集;

8.4

完成了ElasticsearchRetriever支持的几种检索方式的测试,为后续评估做准备(混合搜索需付费,bm25的多字段搜索有三种模式且字段的权重可以调整,后续评估时调整进行测试);测试了agentic chunk(其主要思想是,先进行初步分段,按照长度或递归,然后让大模型生成这一段的概要,将段与段合并生成块),但是测试下来,我们这个一个文档的内容同质化很严重,基本上都分到一块里了,我猜测语义分块也是这种效果;了解学习如何并发测试和吞吐量测试,明天完成测试和文档的撰写工作

8.5

完成七月rag部分的复盘七月复盘.md;完成mineru的压测

8.7

继续学习transformer和模型微调

8.8

完成了transformer和Tokenizer相关知识的学习,周末完成对模型微调相关知识的学习,下周开始模型微调的实战与部署

学习内容概况:1. transformer相关:了解transformer架构;学习其中的注意力机制,大致了解了几种注意力机制的差异;了解了残差连接,层归一化,前馈神经网络,位置编码的相关知识;大致了解了BERT,T5,GPT模型的差异 2.tokenizer相关:两种分词方式BPE与WordPiece;了解了transformer中的分词流程

8.11

完成了微调的理论学习,正在进行实战,学习了大致包括以下内容:什么是全量微调,lora(qlora)的原理;了解微调工具与模型评估框架;如何构建微调数据集

8.12

完成使用unsloth对Qwen3-8B-unsloth-bnb-4bit模型的lora微调实战,包括如何构建微调数据集,数据集清洗等工作的了解,还有一小部分工作没做,就是训练的可视化,使用wandb或swanlab,还没有研究完

peft,目的:意图识别;提取字段

我想问一下,我们这个项目有去试拿公司文件去微调模型吗,我找了个项目,可以根据文档生成微调数据集,但还没试,后续需要的话,我可以试试生成一下,然后去微调实操一下

8.13

完成了微调过程的可视化记录,记录了训练过程中损失下降的情况;跑了一下数据集生成的开源项目,体验了一下:支持doc,docx,pdf文件的上传,上传后会进行分块处理(策略与大小皆可自行调整;pdf可以通过设置mineruapi,也可以自己配置视觉模型解析;doc文件处理思路为,先转化为md格式再进行分块)然后根据每块生成问题,在结合问题和问题所在块,传给大模型生成问题(就像检索出来的上下文一样),最后形成数据集

8.21

完成模型部署镜像搭建,支持vllm,sglang;生成9000份qa数据集

rag评估;论文;部署视觉模型,qwen3-8b

其他

vscode连接远程服务器

  1. 输入 ssh root@10.117.128.50
  2. 输入密码 think123@
使用旧版remotessh
image-20250708104452761

原因:可能因为内网,服务器那边没有进行更新,所以新版的remotessh无法连接

其他问题:可能由于上述原因,trae也无法连接,并且由于trae的远程连接插件无法更改版本,因此无法使用

虚拟环境创建
image-20250708104254347
1
2
3
4
5
6
7
8
9
10
python3 -m venv .venv

which python

#激活虚拟环境
.venv\Scripts\activate
source .venv/bin/activate

#停掉虚拟环境
deactivate

git连接远程仓库

初始化仓库:git init

1
2
3
4
5
6
7
8
# 添加所有文件到暂存区
git add .

# 提交更改
git commit -m "Initial commit"

#上传远程库
git push -u origin main
目标 命令
查看远程仓库 git remote -v
修改远程仓库地址 git remote set-url origin <新地址>
添加新远程仓库(不同名) git remote add upstream <新地址>
删除远程仓库 git remote remove origin
添加第一个远程仓库 git remote add origin
image-20250708151320868

当前存在连接超时问题,可能是服务器连接的原因

mineru部署情况

1a9954d6-31d7-404a-9419-cd9a87c9ee09

通过调整docker镜像源,可以拉取基础镜像了,但是遇到RUN apt-get update && apt-get install -y libgl1 && apt-get clean && rm -rf /var/lib/apt/lists/*第二部命令再次出现网络问题

当你来到双非,你会艳羡211的牌子,当你拼死考上211,你会发现985的头衔会处处卡死你。当你侥幸考上末流 985,你就会发现华五c9的光芒压的你喘不过气。若你真问鼎华五,抬头看,京城中双日凌空。或许你是真正的天才,清北中的佼佼者,他们告诉你,世界不止中国。当你最终成为这一世最不折不扣的天才,你会发现欧拉,黎曼,还有7岁因为想快点放学而创造求和公式的高斯, 早已在山顶等候多时。

有时候想想,这学上到多高才算高啊,大专上面有本科,本科上面有硕博,好不容易毕业了吧,副高,正高,青基,博导,在上面还有杰青,院士。唉,天下英雄,如过江之鲫,无穷尽也。之前没有感觉,自从上了大学,这种感受就像一团乌云一直萦绕在我心头,不禁反思人这一生究竟在追求什么。

大多数人追求的东西,无非三者:权钱学。

有的人梦想升官,但官外有官,权外有权,科级处级厅级,省部级已经算是人中龙凤,但上面还有副国级正国级,大多数人忙忙碌碌一生也就当个副处级,他们真的甘心吗,那种拼劲全力也无法跨过的鸿沟,最后只剩下无奈,遗憾和释然。

有人的渴望财富,赚到了十万就想赚百万,赚到了百万又开始想办法,想赚千万,亿,觉得自己有能力了,开始创业投资,拿着钱去炒股炒币最后赔了个精光,更有甚者权钱勾结,做些不法勾当,不都是为了满足自己的贪婪,但欲望无穷无尽,何时才能填满这个无底洞,更可怕的是,多少人的欲望和他的能力并不符合,自身没有那么大的能力却渴望一切,最后只会反噬,自食其果。

有的人钻研学识,中国的大多数人都是通过高考这一途径踏进学识的殿堂,那些在高中自命不凡的天才们,进入了高校才发现,自己只是芸芸众生的普通一员,以前的光辉也变的暗淡无光,即便是清北级别,已经是很多人可望而不可即的存在,在面对越来越难的知识,在面对更聪明的身边人,在发现自己再努力也无法达成目标时,也会学习释然这一门课。

我想起来看过的一个清华物理系同学的采访,有一句话我印象很深刻,古人会说少壮不努力,老大徒伤悲,但不会说少壮不成功,老大徒伤悲,或许我上面说的这些,都太注重结果了,以结果的好坏判定了过程的意义,这是不对的,在追求这些目标的过程中,我们努力了,拼搏了,奋斗了,其实那就足够了,不应该把结果失利的压力强加在自己身上。

唉,这些说起来容易,真正能做到不为结果所动哪里容易呢,只跟自己比较,不与他人攀比,处之泰然地面对任何困难与挑战。这就是我所追求的心境吧,我什么时候能做到这种地步,可能才是真正的长大了吧。

SVM支持向量机

作业

1

关于核化软间隔支持向量机,推导目标函数的原始问题转换为对偶问题的过程、KKT条件、预测函数。

原始问题

软间隔SVM的目标函数为: $$ \min_{\mathbf{w}, b, \xi} \frac{1}{2} \|\mathbf{w}\|^2 + C \sum_{i=1}^n \xi_i $$ 约束条件: yi(wTϕ(xi) + b) ≥ 1 − ξi,  ξi ≥ 0,  i = 1, …, n 其中 C > 0 是惩罚参数,ξi 是松弛变量,ϕ(⋅) 是特征映射。

转化为对偶问题

  1. 构造拉格朗日函数$$ \mathcal{L}(\mathbf{w}, b, \xi, \boldsymbol{\alpha}, \boldsymbol{\beta}) = \frac{1}{2} \|\mathbf{w}\|^2 + C \sum_{i=1}^n \xi_i - \sum_{i=1}^n \alpha_i \left[y_i (\mathbf{w}^T \phi(\mathbf{x}_i) + b) - 1 + \xi_i\right] - \sum_{i=1}^n \beta_i \xi_i $$ 其中 αi ≥ 0, βi ≥ 0 是拉格朗日乘子。

  2. 对原始变量求偏导并令其为零

  • w 求导: $$ \frac{\partial \mathcal{L}}{\partial \mathbf{w}} = \mathbf{w} - \sum_{i=1}^n \alpha_i y_i \phi(\mathbf{x}_i) = 0 \quad \Rightarrow \mathbf{w} = \sum_{i=1}^n \alpha_i y_i \phi(\mathbf{x}_i) $$
  • b 求导: $$ \frac{\partial \mathcal{L}}{\partial b} = -\sum_{i=1}^n \alpha_i y_i = 0 \quad \Rightarrow \sum_{i=1}^n \alpha_i y_i = 0 $$
  • ξi 求导: $$ \frac{\partial \mathcal{L}}{\partial \xi_i} = C - \alpha_i - \beta_i = 0 \quad \Rightarrow \beta_i = C - \alpha_i $$
  1. 代入拉格朗日函数消去原始变量: 将 wβi 代入 ,得到对偶目标函数: $$ \max_{\boldsymbol{\alpha}} \sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j \phi(\mathbf{x}_i)^T \phi(\mathbf{x}_j) $$ 约束条件: $$ 0 \leq \alpha_i \leq C, \quad \sum_{i=1}^n \alpha_i y_i = 0 $$

  2. 引入核函数: 用核函数 K(xi, xj) = ϕ(xi)Tϕ(xj) 替换内积,得到最终对偶问题: $$ \max_{\boldsymbol{\alpha}} \sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j K(\mathbf{x}_i, \mathbf{x}_j) $$ 约束条件不变。

KKT条件

  • 原始可行性yi(wTϕ(xi) + b) ≥ 1 − ξi,  ξi ≥ 0
  • 对偶可行性αi ≥ 0,  βi = C − αi ≥ 0
  • 互补松弛性αi[yi(wTϕ(xi) + b) − 1 + ξi] = 0,  βiξi = 0
  • 梯度为零条件:已通过偏导数消去原始变量。

预测函数

测试样本 x 的预测函数为: $$ f(\mathbf{x}) = \text{sign} \left( \sum_{i=1}^n \alpha_i y_i K(\mathbf{x}_i, \mathbf{x}) + b \right) $$ 其中 b 可通过任一支持向量(满足 0 < αi < C)计算: $$ b = y_i - \sum_{j=1}^n \alpha_j y_j K(\mathbf{x}_j, \mathbf{x}_i) $$

2

贪心问题

找零钱

用最少数量的钱币凑出目标金额 m 元。

核心思想 : 每次选择不超过剩余金额的最大面值 ,直到凑够目标金额。

步骤:

  1. 将钱币面值按从大到小排序。
  2. 对于当前剩余金额,不断减去最大可用面值,直到金额为 0。

贪心策略的适用性

仅当钱币面值满足以下条件时有效

  • 面值序列中每个元素都是前一个元素的因数(如 1, 2, 5, 10)。
  • 否则,贪心可能失败(例如面值 [1, 3, 4],目标 6:贪心选 4+1+1 需 3 枚,而最优解是 3+3 需 2 枚)。

代码问题分析

用户提供的代码是一个基于贪心策略的找零钱实现,但在硬币面值不满足贪心条件时可能无法得到最优解。以下是具体分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def findMinCoins(coins, amount):
coins.sort(reverse=True) # 降序排序
res = []
for i in range(len(coins)):
while amount >= coins[i]:
res.append(coins[i])
amount -= coins[i]
return res

coins = [1, 2, 5, 10, 50, 100]
amount = 1136
out = findMinCoins(coins, amount)
print(out)
print(f'钱币数量为{len(out)}.')

问题点:

  1. 贪心策略的局限性
    仅当硬币面值满足 每种面值是前一种面值的因数(如 [1, 2, 5, 10, 50, 100])时,贪心算法才能保证最优解。若面值不满足此条件(如 [1, 3, 4]),则可能失败。

  2. 未处理特殊情况

    • amount 无法被硬币组合凑出(如硬币为 [2, 5],目标 3),代码会返回非最优解或死循环。

改进方案

适用于任意硬币面值,确保最优解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def min_coins_dp(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0

for a in range(1, amount + 1):
for coin in coins:
if a >= coin and dp[a - coin] + 1 < dp[a]:
dp[a] = dp[a - coin] + 1

return dp[amount] if dp[amount] != float('inf') else -1

coins = [1, 2, 5, 10, 50, 100]
amount = 1136
print(min_coins_dp(coins, amount)) # 输出 16

最优装载问题

🧮 问题描述

给定一个集装箱重量列表 weights 和轮船的最大载重 W,目标是 尽可能多地装载集装箱(不考虑体积限制)。

✅ 算法思路

  1. 排序:将所有集装箱按重量从小到大排序。
  2. 贪心装载:依次尝试装载每个集装箱,若当前总重量加上该集装箱的重量不超过 W,则装载;否则停止。
  3. 返回结果:返回成功装载的集装箱数量。

🧾 Python 实现

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
def max_loaded_containers(weights, W):
"""
返回在总载重 W 下,最多可以装载的集装箱数量。

参数:
weights (list of int/float): 集装箱重量列表
W (int/float): 轮船的最大载重

返回:
int: 最多可以装载的集装箱数量
"""
# 按重量从小到大排序
weights.sort()

total_weight = 0
count = 0

for weight in weights:
if total_weight + weight <= W:
total_weight += weight
count += 1
else:
break

return count

# 示例测试
if __name__ == "__main__":
weights = [3, 5, 4, 1, 2]
W = 10
result = max_loaded_containers(weights, W)
print(f"最多可以装载 {result} 个集装箱")

活动选择问题(最大相容活动子集)

📌 问题描述

给定 $ n $ 个活动的集合 $ C = {1, 2, …, n} $,每个活动 $ i $ 都有起始时间 $ s_i $ 和结束时间 $ f_i $(满足 $ s_i < f_i $)。要求选择一个最大相容活动子集,使得被选中的活动之间时间互不重叠

两个活动 $ i $ 和 $ j $ 相容的条件为: si ≥ fj  或  sj ≥ fi

✅ 贪心策略与正确性

贪心策略
1. 按活动结束时间 $ f_i $ 从小到大排序。 2. 依次选择结束最早的活动,并跳过与其冲突的所有活动。

正确性证明(归纳法)

  • 基础情况:当只有一项活动时,显然选择它是最优的。
  • 归纳假设:对于前 $ k $ 个活动,该策略能得到最大相容子集。
  • 归纳步骤:考虑第 $ k+1 $ 个活动。若选择结束最早的活动 $ A $,则剩下的可用时间区间为 $ [f_A, +) $,此时在该区间内继续应用该策略,仍能得到最大子集。若不选 $ A $ 而选其他活动,则剩余时间更少,无法容纳更多活动。

🧾 Python 实现

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
def max_compatible_activities(activities):
"""
返回最大相容活动子集的数量及具体活动列表。

参数:
activities (list of tuples): 每个元素为 (s_i, f_i),表示活动的起始和结束时间

返回:
tuple: (最大活动数量, 相容活动列表)
"""
# 按结束时间从小到大排序
sorted_activities = sorted(activities, key=lambda x: x[1])

selected = []
last_end = -1 # 上一个选中的活动的结束时间

for activity in sorted_activities:
s, f = activity
if s >= last_end:
selected.append(activity)
last_end = f

return len(selected), selected

# 示例测试
if __name__ == "__main__":
activities = [
(1, 4), (3, 5), (0, 6), (5, 7),
(3, 8), (5, 9), (6, 10), (8, 11)
]
count, selected = max_compatible_activities(activities)
print(f"最大相容活动数: {count}")
print("所选活动:", selected)

使用堆优化的 Dijkstra 算法(Python 实现)

🧠 核心思想

  • 使用最小堆(优先队列)高效选择当前距离最小的节点,避免暴力遍历。
  • 每次从堆中取出当前最短路径的节点,进行松弛操作(Relaxation)。
  • 若发现堆中存在过时的路径记录,则跳过(因为已找到更优路径)。

📦 图的表示

使用邻接表(字典嵌套列表):

1
2
3
4
5
6
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}

🧾 Python 代码实现

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
49
50
51
import heapq

def dijkstra_with_heap(graph, start):
"""
使用堆优化的 Dijkstra 算法求单源最短路径。

参数:
graph (dict): 邻接表形式的图,格式为 {节点: [(邻接节点, 权重), ...]}
start (str/int): 起始节点

返回:
dict: 从起始节点到所有节点的最短路径长度
"""
# 初始化距离字典,所有节点初始距离为无穷大
distances = {node: float('inf') for node in graph}
distances[start] = 0 # 起始节点到自身的距离为 0

# 优先队列(最小堆),存储 (距离, 节点)
heap = [(0, start)]

while heap:
current_distance, current_node = heapq.heappop(heap)

# 如果当前弹出的距离大于记录的距离,说明该节点已被处理过,跳过
if current_distance > distances[current_node]:
continue

# 遍历当前节点的所有邻接边
for neighbor, weight in graph[current_node]:
distance = current_distance + weight

# 如果找到更短路径,更新距离并推入堆
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(heap, (distance, neighbor))

return distances

# 示例测试
if __name__ == "__main__":
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}
start_node = 'A'
shortest_paths = dijkstra_with_heap(graph, start_node)
print(f"从节点 {start_node} 出发的最短路径:")
for node, dist in shortest_paths.items():
print(f"{start_node}{node} : {dist}")

哈夫曼编码

2. 构建哈夫曼树的步骤

步骤 1:统计字符频率

假设输入字符串为 "BCCABBDDAECCBAAAEC",统计每个字符的出现次数:

1
A: 6, B: 4, C: 5, D: 2, E: 1

步骤 2:创建最小堆(优先队列)

  • 将每个字符及其频率构建成节点,并按频率升序排列。
  • 初始堆:[E(1), D(2), B(4), C(5), A(6)]

步骤 3:合并节点,构建哈夫曼树

  1. 取出两个频率最小的节点 E(1)D(2),合并为新节点 ED(3)
  2. 将新节点插入堆:[B(4), C(5), ED(3), A(6)] → 重新排序为 [ED(3), B(4), C(5), A(6)]
  3. 重复上述步骤,直到堆中只剩一个根节点(哈夫曼树)。

最终树结构示意图(频率越小越靠近叶子):

1
2
3
4
5
6
        (18)
/ \
(8) A(6)
/ \
(4) (4)
B C(5)

步骤 4:生成哈夫曼编码表

从根节点出发,左子树标记为 0,右子树标记为 1

1
2
3
4
5
A: 11
B: 00
C: 01
D: 100
E: 101

4. Python 实现哈夫曼编码

以下代码展示如何用 Python 构建哈夫曼树并生成编码表:

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
import heapq
from collections import Counter

class HuffmanNode:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None

def __lt__(self, other):
return self.freq < other.freq

def build_huffman_tree(text):
# 统计频率
frequency = Counter(text)
# 创建最小堆
heap = [HuffmanNode(char, freq) for char, freq in frequency.items()]
heapq.heapify(heap)

# 合并节点
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)

return heapq.heappop(heap) if heap else None

def build_huffman_codes(root):
codes = {}
def dfs(node, current_code):
if node:
if node.char is not None:
codes[node.char] = current_code
dfs(node.left, current_code + "0")
dfs(node.right, current_code + "1")
dfs(root, "")
return codes

# 示例
text = "BCCABBDDAECCBAAAEC"
root = build_huffman_tree(text)
codes = build_huffman_codes(root)
print("哈夫曼编码表:", codes)

输出示例:

1
哈夫曼编码表: {'B': '0', 'C': '10', 'A': '11', 'D': '110', 'E': '111'}

prim

以下是 朴素 Prim 算法 的实现与详解,适用于稠密图(如邻接矩阵存储的图):

Prim 算法核心思想

  1. 从任意顶点开始(如 start=0)。
  2. 维护一个集合 selected,记录已加入生成树的顶点。
  3. 每次从未选顶点中选择到当前生成树的最小权重边的顶点。
  4. 重复步骤 3,直到所有顶点加入生成树。

时间复杂度:O(V²),其中 V 是顶点数。

Python 实现

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
import sys

def prim(graph, start=0):
V = len(graph) # 顶点数量
selected = [False] * V # 标记顶点是否已加入生成树
key = [sys.maxsize] * V # 记录各顶点到生成树的最小权重
parent = [-1] * V # 记录最小生成树的父节点

key[start] = 0 # 起始顶点的权值设为0

for _ in range(V):
# 找到当前未选顶点中 key 最小的顶点 u
min_key = sys.maxsize
u = -1
for v in range(V):
if not selected[v] and key[v] < min_key:
min_key = key[v]
u = v
if u == -1:
break # 无连通顶点,生成树结束

selected[u] = True # 将 u 加入生成树

# 更新 u 的所有邻接顶点的 key 值
for v in range(V):
if graph[u][v] > 0 and not selected[v] and graph[u][v] < key[v]:
key[v] = graph[u][v]
parent[v] = u # 记录 v 的父节点为 u

return key, parent

# 示例:邻接矩阵表示的图
graph = [
[0, 2, 0, 6, 0],
[2, 0, 3, 8, 5],
[0, 3, 0, 0, 7],
[6, 8, 0, 0, 9],
[0, 5, 7, 9, 0]
]

key, parent = prim(graph)
print("最小生成树的总权重:", sum(key))
print("父节点数组:", parent)

kruskal

2. Kruskal 算法的核心思想

  1. 按权重从小到大排序所有边
  2. 依次选择边
    • 如果这条边的两个顶点不在同一个连通分量中(即不形成环),则将这条边加入生成树。
    • 否则跳过这条边。
  3. 重复步骤2,直到生成树中有 V-1 条边V 是顶点数)。

6. Python 实现示例

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
class UnionFind:
def __init__(self, size):
self.parent = list(range(size))

def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路径压缩
return self.parent[x]

def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX == rootY:
return False # 已在同一个集合
self.parent[rootY] = rootX # 合并
return True

def kruskal(n, edges):
# edges: [(权重, u, v), ...]
edges.sort()
uf = UnionFind(n)
mst = []
cost = 0

for weight, u, v in edges:
if uf.union(u, v):
mst.append((u, v))
cost += weight
if len(mst) == n - 1:
break # 已选够 n-1 条边
return mst, cost

# 示例
n = 5 # 顶点数(0~4)
edges = [
(1, 0, 1), # A(0)-B(1)
(2, 1, 2), # B(1)-C(2)
(3, 2, 3), # C(2)-D(3)
(4, 3, 4), # D(3)-E(4)
(5, 0, 4), # A(0)-E(4)
(6, 1, 3) # B(1)-D(3)
]
mst, total = kruskal(n, edges)
print("MST 边:", mst)
print("总权重:", total)

动态规划

完全背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def unbounded_knapsack_2d(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]

for i in range(1, n + 1):
for j in range(1, capacity + 1):
if weights[i-1] <= j:
dp[i][j] = max(
dp[i-1][j],
dp[i][j - weights[i-1]] + values[i-1]
)
else:
dp[i][j] = dp[i-1][j]

return dp[n][capacity]

# 示例
weights = [1, 2, 3]
values = [15, 20, 50]
capacity = 5
print(unbounded_knapsack_2d(weights, values, capacity)) # 输出 80

最优二叉搜索树

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
def optimal_bst(p, q, n):
# 初始化 dp 和 w 数组(大小为 (n+2) x (n+2),避免越界)
dp = [[0] * (n+2) for _ in range(n+2)]
w = [[0] * (n+2) for _ in range(n+2)]
root = [[0] * (n+2) for _ in range(n+2)]

# 初始化虚拟键的权重
for i in range(n+1):
w[i][i] = q[i]

# 填表顺序:链长从 1 到 n
for l in range(1, n+1): # l 为关键字数量
for i in range(n - l + 1):
j = i + l
w[i][j] = w[i][j-1] + p[j] + q[j]
dp[i][j] = float('inf')
# 枚举根节点 r(i < r ≤ j)
for r in range(i+1, j+1):
cost = dp[i][r-1] + dp[r][j]
if cost < dp[i][j]:
dp[i][j] = cost
root[i][j] = r

return dp[0][n], root

# 示例输入
p = [0, 0.15, 0.1, 0.05] # 关键字概率(从 k₁ 开始)
q = [0.05, 0.1, 0.05, 0.05] # 虚拟键概率(从 d₀ 开始)
n = 3 # 关键字数量
min_cost, root = optimal_bst(p, q, n)
print("最小期望搜索代价:", min_cost)

回溯法

八皇后

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
'''
https://leetcode.cn/problems/n-queens/
'''
n=4
ans=[]
path=[]
onpath=[False]*n#记录哪一列有皇后
diag1=[False]*(2*n-1)#记录主对角线是否有皇后
diag2=[False]*(2*n-1)#记录副对角线是否有皇后
def dfs(row,path:list):
if row==n:
#print(path)
chess=[]
# 生成棋盘
for i in range(n):
chess.append("."*path[i]+"Q"+"."*(n-path[i]-1))
ans.append(chess)
return
for col in range(n):
if isvalid(row,col):
path.append(col)#放置皇后
onpath[col]=diag1[row+col]=diag2[row-col+n-1]=True
dfs(row+1,path)#递归下一行
path.pop()#回溯,取消放置
onpath[col]=diag1[row+col]=diag2[row-col+n-1]=False

def isvalid(row,col):
if onpath[col] or diag1[row+col] or diag2[row-col+n-1]:
return False
return True
dfs(0,path)
print(ans)

高斯核(RBF核)中 σ² 的作用及其对模型的影响

高斯核(RBF核)的形式为: $$ K(x, x') = \exp\left(-\frac{\|x - x'\|^2}{2\sigma^2}\right) $$ 其中 $ |x - x’| $ 是两个样本点之间的欧氏距离,$ ^2 $ 是高斯核的方差参数,控制核函数的“宽度”或“局部性”。

1. σ² 的几何意义:核函数的“影响范围”

  • σ² 较小时
    分母较小,指数项中的 $ $ 会更大,导致指数函数值快速衰减。
    结果:只有当 $ x $ 和 $ x’ $ 非常接近时,核函数值才接近1;稍远一点的距离会导致核函数值迅速趋近于0。
    直观理解:模型只关注局部区域内的样本点,决策边界会围绕每个样本点“弯曲”,形成复杂的非线性形状。

  • σ² 较大时
    分母较大,指数项中的 $ $ 会更小,指数函数值衰减缓慢。
    结果:即使 $ x $ 和 $ x’ $ 相距较远,核函数值仍可能较大。
    直观理解:模型会考虑更大范围的样本点,决策边界更平滑,接近线性分隔。

2. σ² 如何影响模型的复杂度

  • σ² 小 → 局部敏感,高复杂度
    • 每个样本点的影响范围有限,模型需要“记住”每个局部区域的细节。
    • 决策边界会围绕每个样本点剧烈弯曲,甚至形成孤立的环形区域(如图1)。
    • 容易过拟合:模型过度适应训练数据的噪声和细节。
  • σ² 大 → 全局平滑,低复杂度
    • 样本点的影响范围扩大,模型倾向于用简单的全局模式区分数据。
    • 决策边界接近线性(如图2),可能无法捕捉数据中的非线性结构。
    • 容易欠拟合:模型无法拟合数据中的局部特征。

3. 数学与直观示例

假设两个样本点 $ x_1 $ 和 $ x_2 $ 距离为 $ d $,核函数值 $ K(x_1, x_2) $ 随 $ ^2 $ 的变化如下:

$ ^2 $ $ d = 1 $ $ d = 2 $ $ d = 3 $
$ ^2 = 0.1 $ $ (-5) $ $ (-20) ^{-9} $ $ (-45) ^{-20} $
$ ^2 = 1 $ $ (-0.5) $ $ (-2) $ $ (-4.5) $
$ ^2 = 10 $ $ (-0.05) $ $ (-0.2) $ $ (-0.45) $
  • σ² 小(如 0.1):距离稍大的样本点之间几乎无关联,模型仅依赖极邻近的点做决策。
  • σ² 大(如 10):即使距离较远的样本点仍有显著关联,模型决策边界更平滑。

为什么使用高斯核之前要归一化

在使用高斯核(RBF核)之前对数据进行归一化,是机器学习中至关重要的预处理步骤。其核心原因是高斯核对特征的尺度(scale)极度敏感,而归一化能消除特征间尺度差异带来的负面影响。以下是详细解释:

  1. 高斯核的本质依赖距离计算

高斯核的公式为: $$ K(x, x') = \exp\left(-\frac{\|x - x'\|^2}{2\sigma^2}\right) $$ 其中 x − x 是两个样本点之间的欧氏距离。
问题:欧氏距离的计算受特征尺度影响极大。例如: - 假设特征A的取值范围是 [0,1],特征B的取值范围是 [0,1000]。 - 此时特征B的差异会主导距离计算(如 $ (0.5)^2 + (500)^2 $),特征A的贡献几乎被忽略。

结果:模型决策边界会过度依赖尺度大的特征,导致性能下降。

  1. 归一化消除特征尺度差异

归一化(如标准化或最小-最大缩放)将所有特征调整到相似的数值范围(如 [0,1] 或均值为0、方差为1)。
效果: - 公平比较特征:每个特征对距离的贡献权重均衡。 - 防止“大尺度特征主导”:避免模型因某些特征数值过大而忽略其他重要特征。

示例
假设两个样本:
- 未归一化:$ x_1 = [1, 100], x_2 = [2, 200] $,距离为 $ 。 − [0, 1]): x_1 = [0.1, 0.1], x_2 = [0.2, 0.2] $,距离为 $ $。
此时两个特征的贡献比例从 1:100 变为 1:1。

  1. 高斯核参数 σ² 的有效性依赖归一化

高斯核的参数 σ²(或 γ = 1/σ²)决定了核函数的“局部性”(即模型关注局部还是全局模式)。
- 未归一化时:σ² 的选择必须同时适应不同尺度的特征,导致参数调优困难。 - 例如:若某特征尺度极大,需要极小的 σ² 才能捕捉其局部变化,但这可能使其他小尺度特征的核函数失效。 - 归一化后:所有特征尺度一致,σ² 的调参只需关注数据整体分布,而非单个特征的尺度。

SVM的Hinge损失函数

Hinge损失函数是支持向量机(SVM)中用于分类任务的核心损失函数,其核心思想是最大化分类间隔,同时惩罚分类错误或置信度不足的样本。以下是详细解析:

1. 数学定义

对于二分类问题,假设真实标签 $ y {+1, -1} $,模型输出 $ f(x) = w^T x + b  * *Hinge * *$ (y, f(x)) = (0, 1 - y f(x)) $$ - 关键含义: - 当 $ y f(x) $:样本被正确分类且置信度足够(位于间隔边界外),损失为0。 - 当 $ y f(x) < 1 $:样本位于间隔内或被错误分类,损失随 $ y f(x) $ 线性增长。

2. 几何意义:最大化间隔

Hinge损失的设计与SVM的硬间隔(Hard Margin)软间隔(Soft Margin)目标直接相关: - 硬间隔:要求所有样本严格满足 $ y_i (w^T x_i + b) $,即完全线性可分。 - 软间隔:允许部分样本违反间隔约束,通过Hinge损失将约束转化为优化目标: $$ \min_{w,b} \left( \frac{1}{2} \|w\|^2 + C \sum_{i=1}^n \max(0, 1 - y_i (w^T x_i + b)) \right) $$ - 第一项 $ |w|^2 $:最大化间隔(间隔宽度与 $ |w| $ 成反比)。 - 第二项 Hinge损失:惩罚违反间隔约束的样本,$ C $ 控制惩罚强度。

为什么树的数量增加不会导致过拟合?

核心原因:随机森林通过集成学习多样性机制抑制了单棵决策树的过拟合风险。具体来说:

  1. Bagging(自助聚合)机制
    每棵树的训练数据是通过有放回采样(Bootstrap)得到的子集,这意味着每棵树看到的数据略有不同,减少了对训练数据的“记忆”依赖。

  2. 特征随机选择
    每次分裂节点时,仅从随机选择的特征子集中挑选最优特征,进一步降低了各树之间的相关性。

  3. 投票/平均机制
    多棵树的预测结果通过投票(分类)或平均(回归)结合,高方差的个体树被平滑,整体模型的泛化能力增强。

  4. 收敛性保证
    随着树的数量增加,模型性能逐渐收敛到一个稳定值。即使继续增加树的数量,也不会显著提升训练集性能,更不会过拟合。

欧式距离的特性分析

欧式距离(Euclidean Distance)是衡量欧几里得空间中两点之间直线距离的常用方法,其公式为: $$ d(x, y) = \sqrt{\sum_{i=1}^n (x_i - y_i)^2} $$ 以下是对其特性的详细分析:

A. 旋转不变性

正确
- 定义:若坐标系旋转,两点间的欧式距离保持不变。
- 原因:旋转是刚性变换(rigid transformation),仅改变点的坐标表示,但不改变几何距离。
- 示例:在二维平面中,将坐标系旋转θ角度,两点 (x1, y1)(x2, y2) 的旋转后坐标分别为: (x1, y1) = (x1cos θ − y1sin θ, x1sin θ + y1cos θ) (x2, y2) = (x2cos θ − y2sin θ, x2sin θ + y2cos θ) 计算旋转后的距离仍等于原始距离。

B. 尺度缩放不变性

错误
- 定义:若对坐标轴进行非均匀或均匀缩放,欧式距离会发生变化。
- 反例:假设对某维特征缩放 k 倍(如将 xi 变为 kxi),则距离变为原来的 k 倍。
- 结论:欧式距离依赖于特征的绝对尺度,不具备缩放不变性。

C. 不受量纲影响的特性

错误
- 定义:若不同特征的量纲不同(如身高[m]与体重[kg]),欧式距离的计算会因量纲差异而失真。
- 反例
- 点A:(1.8m, 70kg),点B:(1.7m, 65kg)
- 若不标准化,身高差(0.1m)与体重差(5kg)的贡献会被直接相加,但两者量纲不同,结果无实际意义。
- 解决方法:需通过标准化(如Z-score归一化)消除量纲影响。

下列哪个不属于特征提取

答案:D. 主成分分析

解析:

在文本分类的特征选择中,常用的方法包括:

  • A. 卡方检验值:通过统计检验评估特征与类别的相关性,属于过滤式特征选择方法。
  • B. 互信息:基于信息论,衡量特征与类别的依赖关系,属于无监督或半监督的特征选择方法。
  • C. 信息增益:基于熵的指标,评估特征对分类的贡献,常用于决策树等算法中的特征选择。

D. 主成分分析(PCA) 是一种 降维技术,通过线性变换将高维数据映射到低维空间,其核心目标是保留数据的主要方差,而非直接选择原始特征。它属于 特征提取(Feature Extraction)而非传统意义上的 特征选择(Feature Selection)。因此,主成分分析不属于常用的文本分类特征选择算法。

### ridge回归和lasso回归

Ridge回归(岭回归)和Lasso回归(套索回归)是两种常用的正则化线性回归方法,主要用于解决线性回归中的过拟合问题特征选择问题。它们的核心思想是在损失函数中添加正则化项(惩罚项),从而限制模型参数的大小,提升模型的泛化能力。

1. Ridge回归(岭回归)

目标函数 $$ \min_{\mathbf{w}} \left\{ \sum_{i=1}^n (y_i - \mathbf{w}^T \mathbf{x}_i)^2 + \lambda \|\mathbf{w}\|_2^2 \right\} $$ - 第一项是普通线性回归的均方误差(MSE)。 - 第二项是L2正则化项(权重平方的和),λ ≥ 0 是正则化系数,控制惩罚强度。

特点

  • L2正则化:通过缩小权重系数(但不会完全置零)来减少模型复杂度。
  • 解决多重共线性:当特征之间存在高度相关性时,Ridge回归能稳定回归系数。
  • 唯一解:目标函数是凸函数,且严格凸,因此有唯一最优解。
  • 计算效率高:可以通过解析解(闭式解)求解: wRidge = (XTX + λI)−1XTy

应用场景

  • 特征维度较低,但存在多重共线性。
  • 需要保留所有特征,但希望抑制其影响(如基因数据分析)。

2. Lasso回归(套索回归)

目标函数 $$ \min_{\mathbf{w}} \left\{ \sum_{i=1}^n (y_i - \mathbf{w}^T \mathbf{x}_i)^2 + \lambda \|\mathbf{w}\|_1 \right\} $$ - 第一项是均方误差。 - 第二项是L1正则化项(权重绝对值的和),λ ≥ 0 是正则化系数。

特点

  • L1正则化:强制部分权重系数为零,实现特征选择。
  • 稀疏模型:适用于高维数据(如文本分类、基因数据),自动筛选关键特征。
  • 非唯一解:目标函数是凸函数,但可能有多个解(当特征高度相关时)。
  • 计算复杂度较高:通常需要迭代优化算法(如坐标下降法、近端梯度下降)。

应用场景

  • 特征维度极高(如万维以上),需降维。
  • 需要可解释性强的模型(如金融风控中的关键特征筛选)。

3. 总结

  • Ridge回归:适合特征较少且需要稳定系数的场景。
  • Lasso回归:适合高维数据和特征选择场景。
  • 实际选择
    • 如果特征数量远大于样本数量(p ≫ n),优先使用Lasso。
    • 如果特征间存在强相关性,优先使用Ridge或弹性网络。

通过调整正则化系数 λ,可以控制模型的复杂度与泛化能力。

0%