镜像(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或弹性网络。

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

笔记

IMG_20250627_175733
IMG_20250627_175736
IMG_20250627_175740
IMG_20250627_175743
IMG_20250627_175748
IMG_20250627_175752
IMG_20250627_175755
IMG_20250627_175759

第一章

时钟频率(f) :单位时间内完成的时钟周期数,单位为赫兹(Hz)。 例如:800MHz 表示每秒完成 800×106 个周期。

时钟周期(T) :完成一个时钟周期所需的时间,单位为秒(s)。 例如:800MHz 的时钟周期为 T=800×1061​s=1.25ns (纳秒)。

CPICycles Per Instruction,每条指令所需的时钟周期数)是衡量计算机体系结构性能的关键指标之一,用于描述CPU执行一条指令平均需要多少个时钟周期。它直接影响程序的执行速度和系统性能。

  • CPI 表示每条指令执行所需的平均时钟周期数,计算公式为: $$ \text{CPI} = \frac{\text{总时钟周期数}}{\text{总指令数}} $$
  • 执行时间 与 CPI 的关系: 执行时间 = 指令数 × CPI × 时钟周期时间 其中,时钟周期时间 = 1 / 时钟频率。

MIPS(Million Instructions Per Second) 是衡量计算机处理器性能的一个经典指标,表示 每秒执行的百万条指令数,用于量化 CPU 的指令处理能力。其核心思想是:数值越大,性能越强,但需注意其局限性。

  • MIPS = 指令数 / (执行时间 × 10⁶)
  • 或通过 时钟频率CPI(Cycles Per Instruction) 计算:
    $$ \text{MIPS} = \frac{\text{时钟频率(Hz)}}{\text{CPI} \times 10^6} $$

举例
- 若 CPU 主频为 2 GHz(2×10⁹ Hz),平均 CPI=4,则:
$$ \text{MIPS} = \frac{2 \times 10^9}{4 \times 10^6} = 500 \text{ MIPS} $$

数量级:

G,吉,十的九次方

n,纳,十的负九次方

m(milli,毫)的数量级是 10−3 (千分之一)

第二章

补码

1. 补码的定义

补码(Two’s Complement)是计算机中表示有符号整数的标准方法,其核心作用是将减法运算转化为加法运算,从而简化硬件设计。

2. 如何求一个数的补码?

8位二进制 为例: - 正数:补码 = 原码(符号位为0,其余位直接表示数值)。
例如:+5 的补码是 00000101

  • 负数:补码 = 原码的符号位不变,其余位取反(反码),然后末位加1。
    例如:求 -5 的补码:
    1. 原码:10000101(符号位为1,其余位为5的二进制)。
    2. 取反(符号位保留):11111010(反码)。
    3. 加1:11111010 + 1 = 11111011(补码)。

3. 数学原理:模运算

补码的本质是基于 模(Modulo)运算
- 对于 n位二进制数,其模为 2n
- 负数的补码表示为:
x ≡ 2n − x (mod 2n) 例如,8位二进制数的模是 28 = 256,因此:
−5 的补码 = 256 − 5 = 251,二进制表示为 11111011

4. 为什么“取反 + 1”有效?

  • 取反:相当于将数值部分取反(即 x → (2n − 1 − 1 − x))。
  • 加1:最终得到 2n − x,即补码的数学定义。

-5 为例(8位): 1. 原码:10000101(符号位为1,数值部分为5)。 2. 取反:11111010(数值部分取反,符号位保留)。 3. 加1:11111010 + 1 = 11111011,即 251 = 256 − 5

5. 补码的优势

  • 唯一零表示:补码中只有 一个零00000000),而原码和反码存在 +0-0 的问题。
  • 加减统一:所有加减运算均通过加法器完成,无需单独的减法器。
    例如:5 - 3 = 5 + (-3),直接通过补码相加即可。
  • 溢出自动处理:超过范围的高位会自然丢弃(模运算特性)。

6. 特殊情况:最小负数

对于 n位补码,能表示的范围是:
[−2n − 1, 2n − 1 − 1] - 例如,8位补码范围是:-12810000000)到 +12701111111)。 - 最小负数(-128) 没有对应的正数(因为 +128 超出范围),其补码直接定义为 10000000,无法通过“取反 + 1”从原码推导(因为原码中不存在 +128)。

移码(Offset Binary)详解

1. 移码的定义

移码是一种带偏移量的编码方式,主要用于表示浮点数的阶码(Exponent)。其核心思想是将真值(实际数值)加上一个固定的偏移量(Bias),使得所有数值映射到非负数范围,从而简化比较和运算。

公式
移码 = 真值 + 偏移量

2. 移码的核心作用

  • 简化比较
    移码将负数范围映射到正数范围,使得可以直接通过无符号整数比较来判断阶码的大小。
    • 例如:
      在浮点数中,阶码 −3+2 的移码分别为 125130(偏移量为127),直接比较 125 < 130 即可得出 −3 < +2
  • 消除负数表示
    移码将负数转换为正数表示,避免了补码中负数符号位的影响。

3. 偏移量的选择

偏移量通常为 2n − 12n − 1 − 1n 为位数): - 单精度浮点数(32位):偏移量为 127(即 27 − 1)。
- 双精度浮点数(64位):偏移量为 1023(即 210 − 1)。

4. 移码与补码的关系

  • 符号位取反
    移码可以看作是补码的符号位取反。例如:
    • 补码 10000000−128)的移码为 00000000−128 + 128 = 0)。
    • 补码 000000000)的移码为 100000000 + 128 = 128)。
  • 本质区别
    • 补码:用于定点数的加减运算,支持负数和正数的统一处理。
    • 移码:用于浮点数阶码的表示,便于直接比较大小。

5. 移码的应用场景

  • IEEE 754浮点数标准
    移码用于表示浮点数的阶码(Exponent),使得阶码可以直接按无符号整数比较。
    • 单精度(32位)
      阶码占8位,偏移量为127。
      真值 E 的移码为 E + 127
    • 双精度(64位)
      阶码占11位,偏移量为1023。
      真值 E 的移码为 E + 1023

浮点数表示

【CSAPP-深入理解计算机系统】2-4.浮点数(上)_哔哩哔哩_bilibili

【计算机知识】定点数与浮点数(2)浮点数法表示方法!_哔哩哔哩_bilibili

进制转换

【计算机基础】进制转换(3) 小数部分如何进行转换?_哔哩哔哩_bilibili

整数加减

浮点数加减

浮点数加减法运算 白中英计算机组成原理期末速成_哔哩哔哩_bilibili

(自用)计算机组成原理 题型三 浮点数加减法运算题_哔哩哔哩_bilibili

浮点运算(浮点数加减运算)计算机组成原理(看了包会)_哔哩哔哩_bilibili

ieee

计算机组成原理期末复习(5分钟):IEEE754浮点数加减计算!_哔哩哔哩_bilibili

位数

short 16位

第三章 程序的转换与机器级表示

结构体与联合体

【CSAPP-深入理解计算机系统】3-9.结构体与联合体_哔哩哔哩_bilibili

数组的分配和访问

【CSAPP-深入理解计算机系统】3-8.数组的分配和访问_哔哩哔哩_bilibili

过程调用

C程序在内存中的栈_哔哩哔哩_bilibili

【CSAPP-深入理解计算机系统】3-7. 过程(函数调用)_哔哩哔哩_bilibili

AT&T格式

AT&T格式是汇编语言中的一种语法风格,主要用于x86/x64架构的汇编代码编写。它与Intel格式并列为最常见的两种汇编语法,两者在语法细节上有显著差异。以下是AT&T格式的核心特点、示例及常见用途:

主要特点

特性 AT&T格式语法 对比Intel格式语法
寄存器 前缀 %(如 %eax 无前缀(如 eax
立即数 前缀 $(如 $0x10 直接使用数值(如 10
操作数顺序 源操作数在前,目标在后 目标在前,源在后
内存寻址 offset(base, index, scale) [base + index*scale + offset]
指令后缀 通过后缀标明操作数大小(如 l 表示32位) 无后缀,由操作数推断

寄存器种类

  • 8 个通用寄存器,其中
    • EAX, EBX, ECX, EDX 均为 32 位寄存器
    • AX, BX, CX, DX 均为 16 位寄存器
    • AH, BH, CH, DH 均为高 8 位寄存器
    • AL, BL, CL, DL 均为低 8 位寄存器
  • 2 个专用寄存器
  • 6 个段寄存器

操作数寻址方式

1. 基础内存寻址模式

(1) 直接寻址(Direct Addressing) cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc

  • 语法offset(AT&T格式)或 [offset](Intel格式)。
  • 用途:直接访问全局变量或静态数据。
  • 示例
    1
    movl var(%rip), %eax  # AT&T格式(RIP相对寻址,64位模式推荐)
    1
    mov eax, [var]        # Intel格式(32位模式)

(2) 寄存器间接寻址(Register Indirect Addressing)

  • 语法(base_register)[base_register]
  • 用途:指针解引用。
  • 示例
    1
    movl (%eax), %ebx     # 将EAX指向的内存值传入EBX
    1
    mov ebx, [eax]

(3) 基址寻址(Base Addressing)

  • 语法offset(base_register)[base_register + offset]
  • 用途:访问栈帧中的局部变量或结构体成员。
  • 示例
    1
    movl 8(%ebp), %ecx    # 从栈帧偏移8处读取数据到ECX
    1
    mov ecx, [ebp + 8]

(4) 变址寻址(Indexed Addressing)比例寻址

  • 语法array(, index_register, scale)[array + index_register*scale]

  • 用途:数组元素访问。

  • 示例

    1
    movl array(,%eax,4), %edx  # 数组array + EAX*4位置的值传入EDX(数组索引)
    1
    mov edx, [array + eax*4]

2. 组合寻址模式

(1) 基址 + 变址(Base + Index)

  • 语法(base_register, index_register)[base_register + index_register]

  • 用途:访问二维数组或动态分配的数组。

  • 示例

    1
    movl (%ebx, %esi), %edi  # 将EBX + ESI指向的内存值传入EDI
    1
    mov edi, [ebx + esi]

**(2) 基址 + 比例变址(Base + Index*Scale)**

  • 语法(base_register, index_register, scale)[base_register + index_register*scale]
  • 用途:按元素大小(scale)访问数组。
  • 示例
    1
    movl (%ebx, %esi, 4), %edi  # 将EBX + ESI*4指向的内存值传入EDI(4字节元素)
    1
    mov edi, [ebx + esi*4]

**(3) 基址 + 比例变址 + 偏移(Base + Index*Scale + Offset)**

  • 语法offset(base_register, index_register, scale)[base_register + index_register*scale + offset]
  • 用途:访问结构体数组或复杂数据结构。
  • 示例
    1
    movl 12(%ebx, %esi, 8), %edi  # 结构体数组中第ESI个元素的偏移12处数据传入EDI
    1
    mov edi, [ebx + esi*8 + 12]

3.总结

寻址模式 AT&T格式语法 Intel格式语法
直接寻址 var(%rip) [rip + var](64位)或 var
寄存器间接寻址 (%eax) [eax]
基址寻址 8(%ebp) [ebp + 8]
变址寻址 array(,%eax,4) [array + eax*4]
基址+比例变址 (%ebx, %esi, 4) [ebx + esi*4]
基址+比例变址+偏移 12(%ebx, %esi, 8) [ebx + esi*8 + 12]

指令后缀

在 AT&T 汇编格式中,指令后缀(如 bwlq)用于明确操作数的大小,确保汇编器正确生成机器码。判断后缀的核心规则是:根据操作数的大小选择对应的后缀,尤其是寄存器的位数或内存操作数的显式指定。以下是详细说明:

后缀与操作数大小的对应关系

后缀 操作数大小 示例寄存器/操作数
b byte(8位) %al, $0x10, 12(%ebp)(需显式指定)
w word(16位) %ax, %bx, 12(%ebp)(需显式指定)
l long(32位) %eax, %ebx, 12(%ebp)(需显式指定)
q quad(64位) %rax, %rbx, 12(%ebp)(需显式指定)

立即数默认为32位

判断“指针”与“临时变量

(1)%edx:临时变量

  • 特征:直接从寄存器 %edx 读取数据,不涉及内存地址的间接访问。
  • 对应C语言
    如果 %edx 存储的是某个局部变量或计算结果(如 temp = a + b),则对应临时变量

(2)(%ecx):指针

  • 特征%ecx 中存储的是内存地址,(%ecx) 表示解引用该地址(类似C语言的 *ptr)。
  • 对应C语言
    如果 %ecx 存储的是一个指针变量(如 int *ptr),则 (%ecx) 对应指针解引用

关键结论

操作数 类型 判断依据
%edx 临时变量 直接从寄存器读取数据,无间接内存访问(无括号)。
(%ecx) 指针 使用括号 (%ecx) 表示解引用内存地址(类似C语言的 *ptr)。

常见模式对比

汇编指令 C语言对应操作 解释
movl %eax, (%ebx) *ptr = temp; %ebx 是指针(存储地址),%eax 是临时变量。
movl (%ebx), %eax temp = *ptr; 从指针 ptr 读取值到临时变量 temp
movl $0x1, %eax temp = 1; %eax 是临时变量,直接赋值。

汇编语言中M的作用

在汇编语言中,M 通常表示 内存(Memory),用于指示操作数来自内存地址。在你的问题中,M[R[eax]] 的含义是:

M 的作用

  • M[地址] 表示从 内存地址为 地址 的位置读取数据
  • R[eax] 表示寄存器 EAX 的值(即 EAX 中存储的内容)。
  • 因此,M[R[eax]] 的含义是: > EAX 寄存器的值作为内存地址,从该地址读取数据

** AT&T 汇编中的等价写法**

在 AT&T 汇编语法中,M[R[eax]] 对应的写法是:

1
addl (%eax), %edx
- 含义: - (%eax):以 EAX 的值为内存地址,读取该地址的内容(默认是 4 字节,即 32 位)。 - addl:执行 32 位加法。 - %edx:目标寄存器,存储结果。

关键点总结

符号 含义 示例
R 寄存器(Register) R[eax]EAX 的值
M 内存(Memory) M[地址] → 从地址读取数据
() AT&T 汇编中表示内存寻址 (%eax) → 等价于 M[R[eax]]

常见AT&T格式汇编指令

指令类型 操作目的 影响标志位 典型用途
addl 加法 OF, SF, ZF, CF 数值运算、地址偏移
subl 减法 OF, SF, ZF, CF 数值运算、条件判断
orl 按位或 OF=0, SF, ZF, CF=0 位掩码操作
testl 按位与测试 OF=0, SF, ZF, CF=0 条件判断(如检查位是否设置)
imull 有符号乘法 OF, CF 数值运算
leal 地址计算 无影响 高效数组索引计算
decl 递减 OF, SF, ZF, CF 循环计数、边界检查

sall(Shift Arithmetic Left)—— 左移指令

功能

  • 作用 :将操作数的二进制位 向左移动 指定的位数,低位补0。
  • 效果 :相当于将操作数乘以 2n (n 为移动的位数)。

and(Logical AND)—— 逻辑与指令

功能

  • 作用 :对两个操作数进行 按位与运算 ,结果写入目标操作数。
  • 效果 :只有对应位都为1时,结果位才为1。

shrl逻辑右移指令 (Shift Right Logical),用于对操作数进行 无符号右移 ,即高位补 0,低位移出。

leal加载有效地址(Load Effective Address) 的指令,其功能是 计算内存地址并存储到目标寄存器 ,但 不会访问内存 。它常用于 地址计算高效算术运算

标志位

以下是 x86/x64 架构中常见的四个状态标志位(OF、SF、ZF、CF)的详细说明及其判断方法:

1. 标志位概述

标志 全称 含义
CF Carry Flag 无符号溢出标志:表示无符号数运算是否产生进位或借位。
ZF Zero Flag 零标志:表示运算结果是否为零。
SF Sign Flag 符号标志:表示运算结果的最高位(符号位)是否为1(负数)。
OF Overflow Flag 溢出标志:表示有符号数运算是否溢出(结果超出数据类型表示范围)。

2. 判断方法详解

(1) 进位标志(CF)

  • 用途:判断 无符号数运算 是否溢出。
  • 判断规则
    • 加法:若结果最高位(最高有效位)发生进位(超过数据类型的最大值),CF=1。
    • 减法:若结果需要借位(被减数 < 减数),CF=1。

(2) 零标志(ZF)

  • 用途:判断运算结果是否为零。
  • 判断规则
    • 结果为0 → ZF=1
    • 结果非0 → ZF=0

(3) 符号标志(SF)

  • 用途:表示运算结果的符号(正/负)。
  • 判断规则
    • 结果最高位为1(负数)→ SF=1
    • 结果最高位为0(正数)→ SF=0

(4) 溢出标志(OF)

  • 用途:判断 有符号数运算 是否溢出。
  • 判断规则
    • 溢出条件:两个正数相加结果为负,或两个负数相加结果为正 → OF=1。
    • 无溢出:其他情况 → OF=0。

栈帧布局和参数偏移计算规则

【CSAPP-深入理解计算机系统】3-3.栈与数据传送指令_哔哩哔哩_bilibili

1. 参数压栈顺序

C语言默认使用 cdecl 调用约定,参数从右到左压入栈中。例如,函数调用 operate(x, y, z, k) 的压栈顺序为:

1
2
3
4
5
push k;     // 第四个参数(最右边)
push z; // 第三个参数
push y; // 第二个参数
push x; // 第一个参数(最左边)
call operate;
栈中参数布局(高地址 → 低地址):
1
2
3
4
5
6
7
高地址
| k (参数4) | ← 栈顶(ESP)
| z (参数3) |
| y (参数2) |
| x (参数1) |
| 返回地址 |
低地址

2. 栈帧建立过程

进入函数 operate 后,通过以下指令建立栈帧:

1
2
pushl %ebp        ; 保存旧的EBP(栈帧基址)
movl %esp, %ebp ; 将当前栈顶(ESP)赋值给EBP,作为新栈帧的基址
此时栈帧布局如下:
1
2
3
4
5
6
7
8
高地址
| k (参数4) | ← EBP + 20
| z (参数3) | ← EBP + 16
| y (参数2) | ← EBP + 12
| x (参数1) | ← EBP + 8
| 返回地址 | ← EBP + 4
| 旧 EBP | ← EBP
低地址

3. 参数地址的计算逻辑

  • EBP + 4:返回地址(由 call 指令自动压栈)。
  • EBP + 8:第一个参数(x)。
  • EBP + 12:第二个参数(y)。
  • EBP + 16:第三个参数(z)。
  • EBP + 20:第四个参数(k)。

原因
1. 参数顺序:参数从右到左压栈,导致第一个参数(x)位于栈的最低地址(EBP + 8),而第四个参数(k)位于最高地址(EBP + 20)。
2. 偏移计算:每个参数占用4字节(32位系统中 int 和指针大小),因此偏移量依次递增4。
3. 栈帧基址EBP 指向旧的 EBP 值,其上方是返回地址(EBP + 4),再上方是参数。

汇编语言表示程序函数的过程调用

超硬核!408考研重点!汇编语言表示程序函数的过程调用!23王道计算机组成原理指令系统_哔哩哔哩_bilibili

反汇编

反汇编代码是将二进制机器码(如可执行文件、内存转储)转换为 人类可读的汇编指令 的结果。它是逆向工程、漏洞分析、调试等领域的核心工具。以下是详细说明:

1. 反汇编代码的定义

  • 本质:将机器码(二进制/十六进制)转换为对应的汇编指令。
  • 作用:帮助开发者理解程序逻辑、分析恶意软件、调试崩溃原因或研究编译器优化。

2. 反汇编代码的典型格式

反汇编代码通常包含以下部分: | 字段 | 说明 | 示例 | | ————————- | ————————————————– | —————————- | | 地址(Address) | 指令在内存中的地址(十六进制)。 | 0x804838c | | 机器码(Opcode) | 对应的原始十六进制机器码(机器指令的二进制表示)。 | 74 08 | | 汇编指令(Mnemonic) | 汇编助记符(如 mov, jmp, call)及操作数。 | je 0x8048396 | | 注释(Comment, 可选) | 开发者添加的注释(某些工具会自动生成符号信息)。 | ; if (eax == 0) goto label |

示例反汇编代码(AT&T格式)

1
2
3
4
0804838c <main>:
804838c: 74 08 je 8048396 <main+0xa>
804838e: b8 00 00 00 00 mov $0x0, %eax
8048393: e9 0e 00 00 00 jmp 80483a6 <main+0x1a>

大端小端

小端方式(Little-Endian) 是一种 数据在内存中的存储顺序,其核心特点是: > 数据的低位字节(LSB, Least Significant Byte)存储在内存的低地址处,高位字节(MSB, Most Significant Byte)存储在高地址处

1. 小端 vs 大端

特性 小端(Little-Endian) 大端(Big-Endian)
存储顺序 低位字节在前(低地址),高位在后 高位字节在前(低地址),低位在后
示例 0x12345678 → 存储为 78 56 34 12 0x12345678 → 存储为 12 34 56 78
常见平台 x86/x64 架构(Intel/AMD 处理器) ARM(部分模式)、网络协议(TCP/IP)

2. 小端方式的直观理解

示例:32位整数 0x12345678

  • 内存地址分配
    1
    2
    3
    4
    地址 →    0x1000    0x1001    0x1002    0x1003
    +---------+---------+---------+---------+
    | 0x78 | 0x56 | 0x34 | 0x12 |
    +---------+---------+---------+---------+
  • 解释
    • 数据的最低位字节 0x78 存储在最低地址 0x1000
    • 高位字节 0x12 存储在最高地址 0x1003

转移目标地址的计算

在 IA-32(x86)架构中,转移目标地址的计算依赖于 指令的长度相对偏移量(Displacement)。以下是详细分析:

1. 转移指令的基本原理

  • 相对跳转(Relative Jump):转移目标地址 = 下一条指令地址 + 偏移量
  • 偏移量:有符号的 8 位、16 位或 32 位整数,表示从 下一条指令地址 开始的偏移(正向或负向)。
  • 小端方式(Little-Endian):多字节偏移量需按小端方式存储(低位字节在前)。

2. 示例:call 指令的地址计算

(1) 已知条件

  • 指令地址0x804838ecall 指令的起始地址)。
  • 机器码E8 1E 00 00 00
    • E8call 的操作码。
    • 1E 00 00 00 是偏移量(小端方式存储)。

(2) 计算步骤

  1. 确定指令长度
    • call 指令占 5 字节(1 字节操作码 + 4 字节偏移量)。
  2. 计算下一条指令地址
    • 下一条指令地址 = 当前指令地址 + 指令长度
      = 0x804838e + 5 = 0x8048393
  3. 解析偏移量
    • 偏移量字段为 1E 00 00 00(小端方式)→ 转换为大端顺序为 0x0000001E(十进制 30)。
  4. 计算转移目标地址
    • 转移目标地址 = 下一条指令地址 + 偏移量
      = 0x8048393 + 0x1E = 0x80483B1

3. 核心公式 目标地址 = (当前指令地址 + 指令长度) + 偏移量 - 当前指令地址:指令的起始地址(如 0x804838e)。 - 指令长度:由操作码和操作数决定(如 call 占 5 字节)。 - 偏移量:从指令的操作数中提取并转换为有符号整数。

9. 其他指令示例

(1) je 指令

1
804838c:    74 08                   je     0x8048396
  • 当前地址0x804838c
  • 指令长度:2 字节。
  • 偏移量0x08(单字节,无需反转)。
  • 目标地址0x804838c + 2 + 0x08 = 0x8048396

(2) jmp 指令

1
80483a4:    E9 F6 FF FF FF          jmp    0x804839f
  • 当前地址0x80483a4
  • 指令长度:5 字节。
  • 偏移量F6 FF FF FF(小端)→ 补码为 -10(十进制)。
  • 目标地址0x80483a4 + 5 + (-10) = 0x804839f

计算下一条指令地址

下一条指令地址=当前指令地址+当前指令长度

第四章 程序的链接

重定位

【CSAPP-深入理解计算机系统】7-6. 重定位_哔哩哔哩_bilibili

其他

3分钟彻底理解链接器_哔哩哔哩_bilibili

计算机系统基础摘记——程序的链接_引入链接的好处是什么-CSDN博客

【CSAPP-深入理解计算机系统】7-5. 静态库的解析过程_哔哩哔哩_bilibili

其他

gdb调试

一分钟学会GDB程序调试_哔哩哔哩_bilibili

参考资料

深入理解计算机系统合集(周更中)_哔哩哔哩_bilibili

0%