0%

项目背景

在视频内容分析领域,我们常常需要从长视频中快速定位特定场景或画面。传统方法依赖于人工逐帧查看或基于元数据的简单搜索,效率低下且无法应对复杂语义查询。CLIP(Contrastive Language-Image Pretraining)模型的出现为这一领域带来了新的可能性,它能够理解图像与文本之间的语义关联。

笔者基于HuggingFace的CLIP模型开发了智能视频帧匹配系统,该系统可实现:

  1. 自动解析视频文件(支持MP4/AVI/MOV等常见格式)
  2. 实时计算文本描述与视频帧的语义相似度
  3. 智能定位最佳匹配帧并支持可视化预览
  4. 一键保存关键帧为JPG图片

开发思路

本地模型加载模块

考虑到实际部署时的网络稳定性问题,系统采用本地化模型加载方案:

  • 使用transformers库加载预训练的CLIP模型(clip-vit-base-patch32)
  • 设置本地模型缓存路径(E:\clip),包含完整的模型文件:
    1
    2
    LOCAL_MODEL_DIR = r"E:\clip"
    model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
  • 异常处理机制:模型加载失败时,提示缺失文件清单

多模态编码模块

文本编码

文本编码是将用户输入的英文描述转化为高维语义向量的过程。CLIP模型使用Transformer架构对文本进行编码,具体步骤如下:

  • 文本预处理:将用户输入的英文描述(如 “a waitress standing in front of a restaurant”)进行分词和标准化处理。
  • Tokenization:将文本转换为模型可理解的Token序列,每个Token对应一个唯一的ID。
  • 特征提取:通过多层Transformer编码器,将Token序列映射为固定长度的语义向量(本项目中维数为512)。
1
2
inputs = processor(text=texts, return_tensors="pt", padding=True).to(device)
text_features = model.get_text_features(**inputs)

图像编码

图像编码是将视频帧转化为视觉特征向量的过程。CLIP模型使用Vision Transformer(ViT)架构对图像进行编码,具体步骤如下:

  • 图像预处理:将视频帧从BGR格式转换为RGB格式,并调整大小为模型输入要求。
  • 分块处理:将图像划分为多个小块(Patch),每个小块作为一个输入单元。
  • 特征提取:通过多层Transformer编码器,将图像块序列映射为固定长度的视觉特征向量(本项目中维数为512)。
1
2
inputs = processor(images=[pil_image], return_tensors="pt", padding=True).to(device)
image_features = model.get_image_features(**inputs)

相似度计算

相似度是衡量文本描述与视频帧之间语义匹配程度的关键步骤。CLIP模型通过以下方式实现:

  • 向量对齐:将文本特征向量和图像特征向量映射到同一语义空间。
  • 余弦相似度:计算两个向量的余弦相似度,公式如下:
    alt text
  • 匹配分数:相似度值范围在[-1, 1]之间,值越接近1表示匹配度越高。

源码

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
import torch
import numpy as np
from PIL import Image
import cv2
from transformers import CLIPModel, CLIPProcessor

LOCAL_MODEL_DIR = input("请输入你的CLIP模型路径:")

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")


try:
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
except Exception as e:
print(f"加载模型失败,请检查本地文件是否完整: {e}")
print(
"必须包含以下文件: config.json, pytorch_model.bin, preprocessor_config.json, tokenizer_config.json, vocab.json, merges.txt")
exit()

video_path = input("请输入你的视频文件路径:")
output_frame_path =input("请输入你的最佳匹配帧保存路径:")

user_text = input("请输入一段英文描述(例如:a waitress standing in front of a restaurant):")
if not user_text.strip():
print("输入不能为空!")
exit()

texts = [user_text]

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频文件: {video_path}")
exit()

best_match_score = -1
best_match_frame = None
best_match_text = ""


frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break

frame_count += 1
print(f"Processing frame {frame_count}...")

frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)

try:
inputs = processor(
text=texts,
images=[pil_image],
return_tensors="pt",
padding=True
).to(device)
except RuntimeError as e:
print(f"预处理失败: {e}")
continue

with torch.no_grad():
try:
outputs = model(**inputs)
except RuntimeError as e:
print(f"推理错误: {e}")
continue

logits_per_image = outputs.logits_per_image.cuda().numpy()

if len(texts) == 1:
current_match_score = logits_per_image[0][0]
else:
probs = np.exp(logits_per_image) / np.sum(np.exp(logits_per_image), axis=1, keepdims=True)
current_match_score = probs[0][0]

current_match_text = texts[0]

if current_match_score > best_match_score:
best_match_score = current_match_score
best_match_frame = frame
best_match_text = current_match_text
print(f"更新最佳匹配: '{best_match_text}' (分数: {best_match_score:.4f})")

cap.release()

if best_match_frame is not None:
print(f"\n最佳匹配描述: '{best_match_text}' (分数: {best_match_score:.4f})")
cv2.imshow("Best Matching Frame", best_match_frame)
cv2.waitKey(0)
cv2.destroyAllWindows()

save_frame = input("是否保存最佳匹配帧?(y/n): ").strip().lower()
if save_frame == 'y':
cv2.imwrite(output_frame_path, best_match_frame)
print(f"最佳匹配帧已保存到: {output_frame_path}")
else:
print("未找到匹配的帧。")

使用说明

环境要求:

  • 请安装Python 3.8+
  • 安装依赖库:torch, transformers,opencv-python,pillow
  • 如希望利用显卡进行加速计算,请安装CUDA和CUDNN

准备步骤:

  • 下载CLIP模型文件至本地目录
  • 准备一个您要处理的视频,并确保视频文件路径不包含中文
  • 准备一个保存最佳匹配帧的路径

输出结果示例

笔者以本人所在学院的宣传片为例,其中有一段是游泳的片段,笔者尝试利用该程序找到游泳的片段。

首先按照要求输入CLIP模型的地址、视频地址和导出图片的地址,然后系统开始运行。
alt text

等到处理至游泳片段后,匹配分数不断刷新。
alt text

程序运行完毕,最匹配的那一帧会自动弹出。
alt text
alt text

键盘输入y之后,图片自动保存在设置的路径中。
alt text

技术优势

  • 多模态对齐:CLIP模型通过对比学习实现了文本和图像在语义空间的对齐。
  • 高效计算:利用GPU加速,实时处理视频帧并计算匹配分数。
  • 语义理解:能够捕捉复杂的语义关系,而不仅仅是简单的关键词匹配。

备注

本项目是笔者在学习了一点大模型技术后,利用不到半天时间开发的小型应用。这只是使用多模态大模型的一个小练习,且市场上已经有一些不错的产品实现了本项目的功能(如百度网盘自带的“云一朵”助理)。

这篇文档会先在笔者的个人博客上发布,稍后会整理并发布在我的Github上,欢迎读者在Github社区提交issue讨论改进方案。

前言

AI 领域的科研近几年呈现井喷式的增长,相比于理科/传统工科,AI 领域的科研不需要太多太深的基础知识,且不需要庞大的 Team 就能做出些成果。经常有朋友问我科研入门都需要掌握哪些知识,正逢近日师兄希望完善一下培养新人的 doc,我也打算另开一篇 blog 记录下做研究需要补充的基础知识。

总的来说,做 AI Research 大约需要的基础有这些:数学基础,AI基础,编程基础,工具基础,英语基础。本文的重点是工具基础这部分。顺带说一下其他部分:

数学基础高数、概率论、CSAI。因方向而异,除去理论机器学习、大模型机理等理论性较强的方向,需要的基础其实并不多,能看懂 paper 中的公式推导即可,比如 VAE, Diffusion 原始 paper 中的推导。另外,一些其他的数学知识也是必要的,比如 PAC/SVD/插值法等等,这就体现出 CSAI 这门课的作用了。至于复变,除了傅里叶变换在传统的信号处理中常用之外,其他的貌似没有什么太大作用(当然可能是笔者水平不够,所看的 paper 没有用过这些)。

AI基础深度学习+方向相关的基础知识。深度学习可以参考 CS231N 这门课(+作业)。著名的喵喵学长多次讲过,传统的机器学习算法似乎对科研帮助并不大,这点我还是很赞同的,因此不必按照课程设置把机器学习、AI概论等等课学完再去学 Deep Learning,直接学 DL 完全没有问题,有问题请教 AI 即可。此外,根据研究方向的不同,所需的基础知识也有差异,比如 Robot Learning 就需要额外学一些 Robotics 和 3D Vision 的基础知识。

编程基础Python, Pytorch。Python 编程和 Pytorch 的使用同样重要,这部分内容可以搜一些现有的教程,如李沐在B站上的视频,等等。完成标准是可以手写一个 Transformer 之类的较大的 Project (或者手动实现一下 nn.MultiheadAttention)。算法知识其实并不太必要(笔者算法水平很菜呜),重要的是能看明白程序是怎么运行的,参数是怎么调用的,当我需要修改某个模块的时候代码应该怎么改(这才是最实用的技能)。另外,学会和 Cursor/Copilot 一起工作也很重要,特别是发现他们的错误。

英语基础不能太差就行,比如六级 550+ 这样。一开始看 paper 肯定是看不太懂的,于是查词典,看了几篇之后专业词汇都懂了之后便能做到独立看 paper, 且阅读速度并不会比中文慢多少。

下面进入正题,详细介绍一下工具基础。

Linux 服务器

大部分的深度学习训练应该是在课题组的服务器/租用的服务器上完成的,因此需要掌握如何使用组内的服务器。

首先,服务器可以理解为是操作系统为 Linux 且性能极强(无论是GPU 还是 CPU)的一台电脑。所以需要先学习一下 Linux 系统的基本知识。Linux 系统的内容十分丰富,笔者只列与 AI Rsearch 密切相关的几点。

1. 文件系统

  • 文件复制

    1
    cp /home/nailong/jitui.txt /home/baobaolong/

    表示将 jitui.txt 文件复制到 /home/baobaolong/ 目录下。复制整个文件夹需加 -r 参数:

    1
    cp -r /home/nailong/project /home/baobaolong/
  • 文件移动(剪切)
    1
    2
    mv jitui.txt /home/baobaolong/
    mv old_name.txt new_name.txt # 也可以用于重命名
  • 文件删除
    1
    2
    3
    4
    5
    6
    # 删除单个文件:
    rm jitui.txt
    # 删除文件夹及其所有内容:
    rm -r folder_name/
    # 强制删除(不提示):
    rm -rf folder_name/
    警告:rm -rf 命令非常危险,误用可能导致系统崩溃,务必小心。

2. 权限管理与运行程序

3. 视监你的机器:nvidia-smi 和 htop

nvidia-smi 和 watch -n 0.1 nvidia-smi

这个命令用来查看模型训练的 GPU 使用情况,如果希望动态查看,可以使用 watch -n 0.1 nvidia-smi (0.1 是我设置的刷新时间间隔)。这里以笔者的输出举例,分析怎么看这里面的数据。
alt text

左边表示从 0 到 7 是 8 张 NVIDIA 4090 显卡,说明这是一台八卡 4090 服务器。

最左边是风扇和温度,反映了这张卡近一段时间的工作强度,中间是目前的显存占用,右边是目前的 GPU 利用率。一般而言,良好的训练状态下 GPU 利用率应该在 70% 以上波动,最好能一直 80% 以上。如果 GPU 利用率不高(经常三四十,甚至 0% )则要考虑是不是数据预处理太慢,CPU 和 GPU 速度不同步导致 GPU 空闲,这时可以试图增加数据预处理模块,或者提高 num_workers 到 8 或者 16,都可能解决这个问题。

显存的占用和 batch_size 与模型的大小都有关,一般而言,在模型大小固定的情况下,根据显卡的显存总量,尽量提高 batch_size 有助于稳定训练和提高效果。

htop 和 top

这两个命令是监控 CPU 状态的,区别是 htop 比较新,top 是个很老的命令了。
alt text
同样以一台服务器为例(摄像时机器正处空闲时段,没有跑什么训练或测试任务)展示 htop 的输出,通过这个输出,我们很方便判断系统此时的负载,有没有哪个线程卡死之类的问题。比如发现训练缓慢,可以通过查询 STATE 判断是否有卡 I/O 的情况。具体的细节,一位台湾的工程师写过 blog 详细讲解怎么看 htop 面板,很实用。

bash
复制
编辑
ls -l
输出示例:

csharp
复制
编辑
-rw-r—r— 1 user group 1234 Jul 22 12:00 jitui.txt
修改权限

bash
复制
编辑
chmod +x script.sh # 增加执行权限
运行程序

运行 Python 脚本:

bash
复制
编辑
python script.py
运行 Shell 脚本(先确保可执行):

bash
复制
编辑
chmod +x run.sh
./run.sh

  1. 包管理与环境配置(基础)
    使用 pip 安装库

bash
复制
编辑
pip install numpy
使用 conda 创建虚拟环境

bash
复制
编辑
conda create -n myenv python=3.10
conda activate myenv
查看当前已安装的包

bash
复制
编辑
pip list

  1. GPU 与进程管理(简要)
    查看 GPU 使用情况(NVIDIA 显卡)

bash
复制
编辑
nvidia-smi
查看进程状态

bash
复制
编辑
top
推荐工具:htop

bash
复制
编辑
sudo apt install htop
htop
杀死进程

bash
复制
编辑
kill
kill -9 # 强制杀死

  1. 远程连接与文件传输
    通过 SSH 连接远程服务器

bash
复制
编辑
ssh username@server_ip
从本地复制文件到服务器

bash
复制
编辑
scp localfile.txt username@server_ip:/home/username/
从服务器复制文件到本地

bash
复制
编辑
scp username@server_ip:/home/username/file.txt .
如需继续阅读 Git 基础、Shell 脚本、远程开发 VSCode 配置、Docker 使用等内容,请阅读下一节。

go
复制
编辑

如你需要我输出为 .md 文件或继续补充后续章节(如 Git、Docker、调试工具等),可以随时告诉我。

需要注意的两点:组内服务器常为共用,一般需建一个以自己名字命名的文件夹存储自己的代码。并且更为重要的是,服务器通常将代码和数据分开存储,数据存储在数据盘以加快读写速度,必须把自己训练的数据放在数据盘(否则可能导致系统盘爆满影响他人使用,笔者一开始不知道就放错了哈哈)。

项目背景

在实际的科研场景中,我们常常需要阅读大量的文献,这其中有一个文献的筛选过程:我们希望快速地知道这篇文章讲了哪些内容,从而判断这篇文章与自己的方向是否契合,决定是否要精读。虽然,阅读文章的摘要就能做到这一点,但是摘要有时不能覆盖全文每一个部分的主题,提供的信息可能不足,而且纯英文的摘要对于刚刚入门该领域的科研人员并不友好。

笔者基于阿里通义大模型 (qwen-turbo-2024-11-01) 开发了这样一种自动化文献处理系统,它可以实现大批量纯英文文献的阅读,并以pdf格式输出文章的概要,概要有条理且内容清晰,总长度不超过2页A4纸。

科研人员可以大批量下载某一特定方向的论文,然后先经过本系统处理为中文概要进行阅读,再选择其中合适的论文用一些大模型论文阅读插件阅读(或自行阅读),从而在一定程度上提高了科研工作者的论文阅读效率。

开发思路

PDF文本提取模块 (extract_useful_text_from_pdf)

第一步是解析PDF,由于大模型对PDF的解析较慢且准确率不高,笔者决定先自行解析PDF为文本,再向大模型中输入文本。笔者采用PyPDF2库实现PDF解析,通过逐页处理、首尾行剔除策略去除页眉页脚。该模块处理单页时保留段落结构,输出文本已去除冗余格式字符,为后续处理提供干净输入。

文本分块模块 (split_into_chunks)

在论文阅读任务中,文献的长度往往很长,tokenize之后很可能超出了大模型的输入限制(或者因为上下文长度限制导致输出内容过少)。因此,笔者先使用cl100k_base模型对论文进行文本分块,再将分块之后的token整合之后传给通义模型。基于tiktokencl100k_base编码器实现智能分块,具备以下特性:

  • 段落完整性保护:以自然段落为最小单位,避免分块时拆分逻辑语义。
  • 动态分块策略:设置8000token的阈值(为API预留空间),对超长段落实施二次分割。
  • 容错机制:通过正则表达式兼容不同换行符格式,确保分割稳定性。

摘要生成模块(summarize_document)

该模块整合了分块处理和AI摘要技术:

  • 分块摘要:采用迭代式请求策略,每块生成带层级结构的Markdown摘要。
  • 双重校验机制:首轮生成后自动检测摘要长度,超限时执行二次精炼。
  • 异常处理:对API调用失败的分块记录错误日志,保证流程持续运行;同时可根据API调用失败的错误日志查阅阿里官方的大模型产品文档,以寻求支持。

格式转换模块(convert_markdown_to_pdf)

基于pandoc实现文档格式转换,核心特性包括:

  • 中文排版优化:通过xelatex引擎配置微软雅黑字体,确保中文PDF正确渲染
  • 临时文件管理:采用写入-转换-清理模式,避免产生冗余文件
  • 路径验证:自动检测输出目录有效性,异常时触发系统告警

源码

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import os
import subprocess
import PyPDF2
import re
import tiktoken
from openai import OpenAI

def extract_useful_text_from_pdf(pdf_path):
all_text = []
try:
with open(pdf_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
for page in reader.pages:
text = page.extract_text()
if text:
lines = text.splitlines()
if len(lines) > 2:
lines = lines[1:-1]
page_text = "\n".join(lines)
all_text.append(page_text)
except Exception as e:
print(f"读取PDF出错:{e}")
return "\n".join(all_text)

def split_into_chunks(text, max_tokens=8000):
tokenizer = tiktoken.get_encoding("cl100k_base")

paragraphs = re.split(r'\n\s*\n|\r\n\s*\r\n', text.strip())

chunks = []
current_chunk = []
current_token_count = 0

for para in paragraphs:
if not para.strip():
continue

para_tokens = len(tokenizer.encode(para))

if para_tokens > max_tokens * 0.8:
sub_paras = re.split(r'(?<=[。!?;]) +', para)
for sub_para in sub_paras:
sub_tokens = len(tokenizer.encode(sub_para))
if current_token_count + sub_tokens > max_tokens:
chunks.append("\n\n".join(current_chunk))
current_chunk = [sub_para]
current_token_count = sub_tokens
else:
current_chunk.append(sub_para)
current_token_count += sub_tokens
else:
if current_token_count + para_tokens > max_tokens:
chunks.append("\n\n".join(current_chunk))
current_chunk = [para]
current_token_count = para_tokens
else:
current_chunk.append(para)
current_token_count += para_tokens

if current_chunk:
chunks.append("\n\n".join(current_chunk))

return chunks

def summarize_document(text):
CHUNK_MAX_TOKENS = 8000
SUMMARY_MAX_TOKENS = 8192

chunks = split_into_chunks(text, max_tokens=CHUNK_MAX_TOKENS)

all_summaries = []
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

for i, chunk in enumerate(chunks):
try:
prompt = (
f"请阅读以下文献的第{i + 1}部分,用中文返回文献框架的markdown源代码,并且每个标题下要把文章中这部分内容做一个简单的概括。 请一定注意:\n"
"1.除了markdown源代码之外,任何内容都不要输出。\n"
"2.输出语言必须为中文。【非常重要!】\n"
"3.用多级的逻辑结构(一级标题、二级标题甚至三级标题)来总结原文的框架,尽可能丰满你的框架\n" + text
)

response = client.chat.completions.create(
model="qwen-turbo-2024-11-01",
messages=[
{"role": "system",
"content": f"你是一个{area}领域的文献分析助手,擅长从长文档中提取结构化框架并生成技术性摘要。"},
{"role": "user", "content": prompt}
],
temperature=0.4,
top_p=0.7,
max_tokens=SUMMARY_MAX_TOKENS,
frequency_penalty=0.5
)

summary = response.choices[0].message.content.strip()
all_summaries.append(summary)

except Exception as e:
print(f"第{i + 1}部分处理失败:{e}")
all_summaries.append(f"## 第{i + 1}部分摘要生成失败\n")

final_summary = "\n\n".join(all_summaries)

if len(final_summary) > SUMMARY_MAX_TOKENS * 2:
try:
response = client.chat.completions.create(
model="qwen-turbo-2024-11-01",
messages=[
{"role": "system", "content": "你是一个摘要精炼专家,擅长将多个章节摘要整合成连贯的完整文档摘要"},
{"role": "user", "content": f"请将以下分块摘要整合为完整的文献框架,你只能输出一段完整的markdown代码,其他的都不要输出:\n{final_summary}"}
],
temperature=0.3,
top_p=0.8,
max_tokens=SUMMARY_MAX_TOKENS
)
return response.choices[0].message.content.strip()
except:
return final_summary

return final_summary

def clean_markdown(md_text):
if md_text.startswith("```") and md_text.rstrip().endswith("```"):
lines = md_text.splitlines()
return "\n".join(lines[1:-1])
return md_text

def convert_markdown_to_pdf(markdown_text, output_path):
temp_md_path = r"D:\TestForEssayReader\Temp\temp.md"

markdown_text = clean_markdown(markdown_text)

try:

with open(temp_md_path, "w", encoding="utf-8") as f:
f.write(markdown_text)
print(f"临时Markdown文件已创建: {os.path.abspath(temp_md_path)}")


subprocess.run(
[
pandoc,
temp_md_path,
"-f", "markdown",
"-o", output_path,
"--pdf-engine=xelatex",
"-V", "CJKmainfont=Microsoft YaHei"
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True,
encoding = 'utf-8',
)
print(f"PDF成功生成于:{os.path.abspath(output_path)}")

except subprocess.CalledProcessError as e:
print(f"转换失败,错误详情:\n{e.stderr}")
raise
except Exception as e:
print(f"操作出错:{str(e)}")
raise
finally:
if os.path.exists(temp_md_path):
os.remove(temp_md_path)


def find_pandoc():
try:
subprocess.run(["pandoc", "--version"], check=True, stdout=subprocess.PIPE)
return "pandoc"
except FileNotFoundError:
common_paths = [
r"C:\Program Files\Pandoc\pandoc.exe",
r"C:\Users\{}\AppData\Local\Pandoc\pandoc.exe".format(os.getenv("USERNAME"))
]
for path in common_paths:
if os.path.exists(path):
return path
return None

def main():
area = input("请输入你的专业:")
YOUR_API_KEY = input('请输入你的API_KEY(请在“ https://bailian.console.aliyun.com/?apiKey=1#/api-key ” 申请阿里API):')
essay_folder = input("请输入你原始文献文件夹的路径:")
summary_folder = input("请输入你希望保存摘要的文件夹路径:")

os.makedirs(summary_folder, exist_ok=True)

for filename in os.listdir(essay_folder):
if filename.lower().endswith(".pdf"):
pdf_path = os.path.join(essay_folder, filename)
print(f"正在处理:{pdf_path}")

text = extract_useful_text_from_pdf(pdf_path)
if not text.strip():
print("未提取到有效文本,跳过该文件。")
continue
markdown_summary = summarize_document(text)

output_pdf = os.path.join(summary_folder, f"summary_{os.path.splitext(filename)[0]}.pdf")
convert_markdown_to_pdf(markdown_summary, output_pdf)
print(f"生成摘要文件:{output_pdf}")

if __name__ == "__main__":
main()

使用说明

  • 请安装python环境,并安装os,tiktoken,re,openai,subprocess,PyPDF2软件包。
  • 请安装pandoc,并配置系统环境变量
  • 请申请阿里的API,网址:https://bailian.console.aliyun.com/?apiKey=1#/api-key
  • 请创建一个文件夹,其中存放您要处理的文献原始文件(pdf格式)

输出结果示例

python终端,可看到程序运行结果如下:
alt text
在设定的概要输出路径下,可看到概要文件如下:
alt text
点开后内容如下,层次分明、结构清晰,便于科研工作者迅速了解文献内容。
alt text

备注

本项目是笔者在学习了一点大模型技术后,利用半天时间开发的小型应用。在使用过程中,可能会在整合多个chunk的时候出现格式问题,导致生成的PDF中保留了一部分Markdown源码,这个问题会慢慢解决。当然也可能会有其他的问题,笔者尚未发现。
这篇文档会先在笔者的个人博客上发布,稍后会整理并发布在我的Github上,欢迎读者在自己科研时使用这个小程序,通过Github社区与我交流。

期末成绩陆陆续续出来得差不多了,我也终于找到时间浅浅总结一下这学期的收获了。

刚从高中模式中逃脱出来的人大概是逃不开高中的思维,身边有不少紧紧盯着成绩的朋友(当然我也算是,毕竟是第一次大学的考试,陋习难改)。若是但从分数上看,除了线性代数寄的很惨之外,其他倒也能接受。充满智慧的教务老师们,为了“降低压力和内卷”,把成绩分布和排位功能给取消了,我也尚不清楚我这均分是个什么水平。

只不过,若但从期末考试的卷子上看这一学期的收获,也未免过于肤浅了,毕竟如果要评论一下这学期的一些课:

(1)思政、国防、毛概,这三门是每门一天速通的。据此总结了经验:摆烂一学期,考前速通也能90+(当然更高我也不追求)。

(2)高数和线代的内容,大部分高中竞赛时期就已经掌握得很熟练了,而且XJTU考试题的难度相比华科实在是简单,再往上跟USTC、PKU、THU更没法比。

(3)婴语,主要是学术写作的内容,学到了很多学术写作的格式和规范,收获很多,也很喜欢楚老师。但其实一周一次两小时的婴语课,也提高不了多少真正的英语水平,考个四级就老实了。

(4)选修课更是水的一批,这学期选了一门生态学、一门材料学的课。感觉选这些“推荐的好课”确实是上课水、不点名、给分高,但是1门课就要白白浪费32个小时(毕竟上课写作业也没啥效率,如果不逃课,那课堂的时间确实很浪费)。上完之后有一种强烈的空虚感,应该再也不会选这种课了。

那这学期到底收获了什么呢?

第一,学会了编程,算是小白入门的第一课。编程从0学起,而且基本上是让GPT教我、帮我debug,靠着PTA上的作业题学会的。这让我深刻体会到了“大不了自学”的含义。作为高考选拔进入,曾经有自认为不错的物理竞赛背景的同学,我选择CS类的专业,本身并没有什么基础优势,编程都是现学的,算法更是一窍不通。这学期从科学上网、学会github、再到学习一部分pytorch、搭建个人博客,虽然水平远比不上少班的大佬们,但总算是学习了一点点技术,这应该是未来工作/研究中比较实用的部分。

第二,认识了很多新朋友,社交扩大了一些。很不幸,高中同学没有来XJTU的,以前认识的朋友进入XJTU的也不多,再加上我是后选拔进班,和其他同学军训和住宿都不在一起,social十分困难。我也得强行转变一下MBTI,变得E一些,主动加了不少朋友,也很有幸能认识到很多少班的大佬们。非常感谢大佬们不吝赐教,这学期给了我很多宝贵的指导和帮助。

第三,AI学组运行平稳起步,顺利走上正轨。当时本着想为同学们做些事情的想法,再加上自己时间比较宽裕,就接下了24级学组负责人的职位。我接下工作后才发现,学组各方面制度和架构其实很不完善,创立时间也不长。怎么把学组建设好、运营好,让学组中各位大佬团结起来、发挥聪明才智、创造集体价值,是摆在我面前的一个重要问题。从一开始准备见面会,分配工作,到后来一项一项推进工作,产出一份一份资料,学组的每一项工作都倾注着我们的付出,凝聚着所有人的努力,我也当然为此投入了大量的时间和精力。

第四,身体素质相比有了很大提高。荒废了半年多的长跑慢慢拾起,从一开始的两公里六分配,用两个月的时间一点一点提高到五公里四分半的水平。在无数个零下黑夜的田径场上,音乐、脚步和呼吸共同律动,我似乎又找回五年前那个1500米银牌的自己了。相比去年那个一周跑几趟医院的我,这学期去医院的次数明显少了很多(或许也是因为大部分问题都能自己诊断然后吃药解决)。然而不幸的是,期末周的来临,让我有半个多月找不到跑步的时间,连续一天一门速通的压力也让我身体扛不住了,期末考完一回家就直接急诊科输液去了。

看起来很棒,所以下学期有什么打算呢?

这个问题我想了很久,因为计划的制定是从最核心的价值观出发的。所以,这必须要回答这样的问题:我看重什么呢?这学期最后一次心理咨询的时候,老师抛给我一个这样的问题,她希望我寒假的时候好好思考一下。

我追求的是90分提高到95分,然后获得4.3的绩点的那种满足吗?还是门门课都要争取上90,最后拿奖学金?如果为了成绩/科研天天晚上学到1点以后睡觉,身体素质下降,这是我想要的吗?

首先,我想我来XJTU是为了学知识和技术的,上课的主要意义是学知识,成绩只是学习成果的一个反映,大概比保研线高一些,维持在安全能保研的水平就够了,我认为自己没有能力也没有心力去争前几名。既然上课的主要意义是学知识,那么我宁愿选一些上课水平高但是给分低的老师,或是内容我感兴趣/内容有价值但给分低的课,而不会去选那些给分高但是上完之后没什么收获的课。当然,一些必修课如果像后者一样,不如翘之,然后从网上找替代品学习。

至于奖学金,我倒认为争取的必要性不大,可以随缘处之,有机会就拿,拿不成就算了。如果为了这几千块钱牺牲掉一些更为重要的价值,那真是捡了芝麻丢了西瓜,没有抓住主要矛盾。

其次,身体素质上我希望能继续提升一些。跑半马和爬雪山是我一直以来的两个梦想,希望下个学期能至少实现一个。跑半马需要月跑量100km以上,至少训练半年,注重10km/15km这样的长距离跑步训练(这学期的5km训练是远远不够的)。爬雪山也需要半年以上的准备时间,长跑训练、负重爬楼训练、肌肉耐力训练都要加上,而且还需要一些训练营培训一些冰雪技巧(不过这倒不难,身体素质是最硬的一道关)。另外,规律的作息也是身体素质的一个保障。希望下个学期还能保证12点半以前入睡,然后8点左右起来的充足睡眠(听起来不像是上大学的,但我这个学期确实如此)。身体和事业是同等重要的,如果工作真的需要我熬夜处理,那我认为应该放弃一部分工作来保证睡眠。

还有科研的问题,身边有不少大一就进组的同学,但是盲从跟风并不是我想要的,我更希望的是能有机会投入到自己真正感兴趣的方向中去。毕竟有兴趣才会有动力,工作起来有激情就不会感到疲惫,若是单纯以论文成果为导向,恐怕我没法承受这种强度的枯燥工作。另外,目前自己的水平也十分有限,提高能力是根本,基础知识应该格外重视,不能急于进组科研,更不能急于追求成果。

最后,希望学组的工作能继续推进一些,希望能推动更多同学加入到资料编写、学业互助的工作中来,让学组创造更大的社会价值,也在学组的工作中不断积累经验,提高自己的专业能力、管理能力。

这学期的故事就到这里了,下个学期会更好,一定会。

Introduction

欢迎您访问 HzmSailor 的个人博客!我是西安交通大学人工智能学院的本科生。

博客用于整理人工智能本科专业学习笔记,AI方向论文摘要和个人日志等。(然而目前只有日志)

欢迎邮件联系我! E-mail: zimuhan276@gmail.com

谨以此文纪念我的物竞和我的2023年。

“有一种事物很恐怖,叫断断续续的梦终有消散的时分,令人愀然的结局终将迎接光暗相逢的伊始。”读到两年之前梁老师写下的这段文字,倏然之间,心里为之一颤。朦胧魔幻的梦终于走到消逝的尽头,而我在此刻,将倾尽真心,为它庄重送别。

我不知用何种语言才能确切表达这样的一种玄妙的旅途,它如同毅然踏上不知终点站的列车,并义无反顾地驰车奔涌向前,绕过蜿蜒的小路,越过奇崛的峰峦,或踏过万壑绵延的沟野,或奔驰于一马平川的山原。若让我平心而论,去谈谈这种旅途的得与失时,我却只会轻描淡写地讲:是这样的旅途,让我看到不一样的天地,探寻无人品悟过的快乐,也或许能找到自己灵魂之所向、精神之所归。这种体验无论结果的胜与负,也无关过程的得与失。这不应只是享受名利之雍华,享受利得之喜悦,享受得志之光辉的投机博利,而是寻求本心,探索自我的一次勇敢的尝试。

我记得旅途的开始,是2021年7月11日的清晨。暴雨刚过,积水许多。蓝色的清晨,我坐三号线来到彼时遥远的二中,坐在了实验楼310第一排的最左侧。我特意选的这个位置,因为听说陈路加就坐在这里考进了清华,也希望坐在这里,能为自己带来一些好的运气。刚开始看高中物理学,虽然比斌神看的慢不少,但也从未因此着急,倒是看一些有一些的收获。慢慢的,程书、难集、题选、国培……写满的刷题本堆叠成山,刷完的试卷装满了纸箱,我的实力也在一点一点增强。

我还记得一次一次去中山宾馆的培训。冬日的早晨,漆黑一片,只有和平路高架上橙黄的灯光把车影拉的很长。集训是极其有规律的:刷题,讲题,再刷题,再讲题……但从未因此感到无聊,课间弹个钢镚,或是弹个尺子,哈哈一笑,这些简单的游戏总让人解压。我们还曾有规矩:考的好的要请大家饮料或者雪糕,这多少会给水平稍弱的朋友一些安慰。

我还记得一次一次外出的学习培训,远方的风景总令人心旷神怡。

伫立在橘子洲头,望湘江日落,看淡淡的靛青色天空中露出血红的微光。这是何等精妙的自然景色!一半是天青色,一半是血红色,仿佛是从天空的中间劈开一条极细的裂缝,肆意地播撒下鲜活又有生机的落日余晖。“万物皆有裂痕,那是光照进来的地方。”

缓缓骑行于厦门雾气腾腾的海滩,看潮水一层一层涌上岸边,又一层一层褪去。漫步沙滩上,留下的脚印一深一浅,还有某人调皮写的一个“毸”字(乐)。那轻拂面颊的晚风,那稍有些咸味的空气,和着那日在海边无穷无际的遐思,最终成为了弥足珍贵的回忆。

最难忘却的便是夏日的北大。澄明清澈的未名湖,静静矗立的博雅塔,激情澎湃的物院教授,执着高深的课题研究,北大的湖光塔影、科学精神、人文关怀,在我心里深深埋下了理想的种子,烛照苍白疲惫的备考时光。

然而,美好的时日总是短暂的,临近复赛,考试的压力与日俱增:春联落榜的惊愕与恐惧、集训糟糕的压力与焦虑……忽然某一日,我有些怀疑:这是否是我真心热爱的旅途?这漫漫旅途的终点是物理的探索,还是功名的成就?或是,这只是一场虚幻缥缈的梦境,它终将在时间长河里慢慢消散?我想起曾经的那些疯狂与残忍:我想起冷落一旁的高考课本,我想起被迫分离的最好朋友,我想起为之着迷的文学著作……仿佛是走入了无底之洞,仿佛是陷入了精神牢笼,我已无能为力,无法逃避,无路可退。我只想早日结束这一切痛苦和挣扎,回到一个正常高中生的世界。

复赛结束,与其他落榜者以头抢地、如丧考妣般的痛苦不同,我感到一种异乎常人的解脱与释放。悟已往之不谏,知来者之可追。我兴高采烈地收拾起高考课本,去教学楼里落座。我迅速融入了高考课的节奏,慢慢拾起曾经抛弃的知识碎片。“一切向前看”,我总用这五个字劝勉自己。或是遮盖过去的某些疮疤。

虽然退役许多时日了,我仍时常去实验楼看看,看看下一届追梦物竞的年轻人,这是一种陌生的熟悉感受。看他们一人一个小书架,摆满曾经熟悉又亲切的竞赛书;看他们的训练计划表,又想起当年我们的两天三考;看他们分组阅卷、讲题,如同我们当年的讨论课一样……

向之所欣,俯仰之间,已为陈迹!当我坐在卓越考场上,再次翻开熟悉的物竞试题时,已无言以对。我淡淡地望着旁边的同龄人,他们有人竞赛落榜后孤注一掷,为卓越奋战到底,我敬佩他们的勇毅与坚持;我静静地看着身边的年轻人,他们年少有为,物竞之路上仍有无穷的未来可以探索,我羡慕他们前途似海、来日方长。我也默默地注视手里的签字笔,它是我拥有的一切。因为,笔随心动,路随心生。

和牧佬喜欢陈奕迅的《葡萄成熟时》不同,我最爱的是他的《苦瓜》。

就像我一直听香夭从未沾湿眼角   
仔细地看神坛里木纹什麼精巧也不觉   
却在某萧瑟晚秋深夜 忽尔明了了 而黄叶便碎落   
真想不到当初我们也讨厌吃苦瓜   
今天竟吃得出那睿智愈来愈记挂 

苦瓜,曾经吃出的只是苦涩味道,而在有所经历,褪去曾经的青涩与稚嫩后,才能吃出其中清甜与睿智。品味苦瓜,也是在回首自己走过的路。曾经那些所谓踟蹰徘徊、忧郁不前,或是愀然揪心、怅然入梦的往事,已成过往云烟,随风飘散。明悟之际,也只能淡然道:“真想不到当初我们也讨厌吃苦瓜”。

回首向来萧瑟处,归去,也无风雨也无晴。人生的经历无法复刻,也无法重来。曾经那些兀兀穷年不知其意的探寻与迷茫,却在一萧瑟深秋夜里忽然明了了。

黄叶碎落了,那些曾经看不开也解不出、挣不开也逃不过的事,也忽然,随黄叶飘落了。

(2023年12月31日夜,于北京)