赞
踩
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
本文的目的是做一个深度学习模型,可以对PDF论文、期刊(现只限英文)进行总结。在科研和生活中,假如需要长期关注某个课题组的论文;或者有大量的文献,一篇篇的去看又太耗费时间,那么可以使用这个模型,让模型对大量的论文进行初步的总结,再从中有侧重的挑取自己感兴趣或者研究方向相符的论文进行精读。
本文的相关代码和思路来自于github上的chatpaper,只不过chatpaper是将pdf的读取内容通过ChatGPT的api传递给ChatGPT,让gpt来进行总结,假如我们没有gpt账户,或者账户下没有余额,或者觉得成本过高,可以使用huggingface上开源的模型,即博主使用的这个方法。
这是一个比较复杂的问题,如果想要准确的对一篇文章进行一个总结,那么对文章的“摘要”部分的提取就要相对精准一些。但是文章PDF的内容和排版是不同的,不同期刊使用的格式也是千差万别。仅使用一种方法很难对“摘要”部分进行准确的提取。比如:
文章出自nature communition。这类文章没有明确的标注“Abstract”和“Introduction”,但是我们人自己去读,一眼就能知道第一段是文章的摘要部分。但是通过相关的api去读取,怎么用一个通用的方法去读就是一个问题。
再比如:
文章出自IEEE。这类文章有相关描述性的文字,告知读者这部分的内容是什么,那么提取的难度就会小一些。
本方法是通过chatpaper的源码学习到的思路。即是对PDF进行分块处理。通过读取相关的关键字来将论文所需要内容提取出来。
首先是
def chat_paper_main(args):
# 创建一个Reader对象,并调用show_info方法
if args.sort == 'Relevance':
sort = arxiv.SortCriterion.Relevance
elif args.sort == 'LastUpdatedDate':
sort = arxiv.SortCriterion.LastUpdatedDate
else:
sort = arxiv.SortCriterion.Relevance
这里因为我们不需要使用ChatGPT的API因此我将reader对象部分就删去了。
if args.pdf_path:
# 开始判断是路径还是文件:
paper_list = []
if args.pdf_path.endswith(".pdf"):
paper_list.append(Paper(path=args.pdf_path))
else:
for root, dirs, files in os.walk(args.pdf_path):
print("root:", root, "dirs:", dirs, 'files:', files) # 当前目录路径
for filename in files:
# 如果找到PDF文件,则将其复制到目标文件夹中
if filename.endswith(".pdf"):
paper_list.append(Paper(path=os.path.join(root, filename)))
print("------------------paper_num: {}------------------".format(len(paper_list)))
首先是根据传入的参数是否是一个pdf文件,因为这个项目是允许传入单独的pdf,或者是总结一个文件夹下所有的pdf。因此这段代码得逻辑就非常好理解了,判断args参数的结尾字符是不是pdf,如果不是,那说明用户给的是一个文件夹路径,则读取文件夹下所有结尾是pdf的文件,并使用Paper类来处理这些文件,显然这个Paper类就是处理PDF数据的关键了。
class Paper: def __init__(self, path, title='', url='', abs='', authers=[]): # 初始化函数,根据pdf路径初始化Paper对象 self.url = url # 文章链接 self.path = path # pdf路径 self.section_names = [] # 段落标题 self.section_texts = {} # 段落内容 self.abs = abs self.title_page = 0 if title == '': self.pdf = fitz.open(self.path) # pdf文档 self.title = self.get_title() self.parse_pdf() else: self.title = title self.authers = authers self.roman_num = ["I", "II", 'III', "IV", "V", "VI", "VII", "VIII", "IIX", "IX", "X"] self.digit_num = [str(d + 1) for d in range(10)] self.first_image = ''
打开Paper类,查看这段源码,首先title和url以及abs和authers都是空的,这些都是需要我们通过读取pdf然后去给它们一一赋值。
self.pdf = fitz.open(self.path)
这行代码即是使用fitz库区读取pdf。读出来的内容如下所示:
这个类在后面的方法中会用到。
self.title = self.get_title()
这行代码则是利用self里的get_title方法来读取title,以下是get_title方法的代码:
def get_title(self): doc = self.pdf # 打开pdf文件 max_font_size = 0 # 初始化最大字体大小为0 max_string = "" # 初始化最大字体大小对应的字符串为空 max_font_sizes = [0] for page_index, page in enumerate(doc): # 遍历每一页 text = page.get_text("dict") # 获取页面上的文本信息 blocks = text["blocks"] # 获取文本块列表 for block in blocks: # 遍历每个文本块 if block["type"] == 0 and len(block['lines']): # 如果是文字类型 if len(block["lines"][0]["spans"]): font_size = block["lines"][0]["spans"][0]["size"] # 获取第一行第一段文字的字体大小 max_font_sizes.append(font_size) if font_size > max_font_size: # 如果字体大小大于当前最大值 max_font_size = font_size # 更新最大值 max_string = block["lines"][0]["spans"][0]["text"] # 更新最大值对应的字符串 max_font_sizes.sort() print("max_font_sizes", max_font_sizes[-10:]) cur_title = '' for page_index, page in enumerate(doc): # 遍历每一页 text = page.get_text("dict") # 获取页面上的文本信息 blocks = text["blocks"] # 获取文本块列表 for block in blocks: # 遍历每个文本块 if block["type"] == 0 and len(block['lines']): # 如果是文字类型 if len(block["lines"][0]["spans"]): cur_string = block["lines"][0]["spans"][0]["text"] # 更新最大值对应的字符串 font_flags = block["lines"][0]["spans"][0]["flags"] # 获取第一行第一段文字的字体特征 font_size = block["lines"][0]["spans"][0]["size"] # 获取第一行第一段文字的字体大小 # print(font_size) if abs(font_size - max_font_sizes[-1]) < 0.3 or abs(font_size - max_font_sizes[-2]) < 0.3: # print("The string is bold.", max_string, "font_size:", font_size, "font_flags:", font_flags) if len(cur_string) > 4 and "arXiv" not in cur_string: # print("The string is bold.", max_string, "font_size:", font_size, "font_flags:", font_flags) if cur_title == '': cur_title += cur_string else: cur_title += ' ' + cur_string self.title_page = page_index # break title = cur_title.replace('\n', ' ') return title
这里的注释相对比较详细,这里只简单的说一下这段代码得逻辑,是通过什么样的一个方法来准确提取到标题信息的。
简单的说就是通过现成的库读取每页的内容,然后对其进行分块,block的内容如下:
每一个子模块的内容如下:
从中读出的内容可以看出,每个子模块,把number数,文档块的坐标位置(bbox),字体的font,块内的字体信息等等诸多信息全部以一个字典的形式表示出来了。
因此就有了后面,根据字体大小的逻辑来判断是否是标题。
当然还做了一些细微的操作,比如比较字体大小,然后如果某个文本块的字体大小非常接近最大字体大小(在这个代码中,差异小于0.3),则假定这个文本块是标题的一部分。最后将整个文本块的内容拼接起来。即找到了所需要文章标题。这就是大概得思路了。
def parse_pdf(self):
self.pdf = fitz.open(self.path) # pdf文档
self.text_list = [page.get_text() for page in self.pdf]
self.all_text = ' '.join(self.text_list)
self.section_page_dict = self._get_all_page_index() # 段落与页码的对应字典
print("section_page_dict", self.section_page_dict)
self.section_text_dict = self._get_all_page() # 段落与内容的对应字典
self.section_text_dict.update({"title": self.title})
self.section_text_dict.update({"paper_info": self.get_paper_info()})
self.pdf.close()
def _get_all_page_index(self): # 定义需要寻找的章节名称列表 section_list = ["Abstract", 'Introduction', 'Related Work', 'Background', "Preliminary", "Problem Formulation", 'Methods', 'Methodology', "Method", 'Approach', 'Approaches', # exp "Materials and Methods", "Experiment Settings", 'Experiment', "Experimental Results", "Evaluation", "Experiments", "Results", 'Findings', 'Data Analysis', "Discussion", "Results and Discussion", "Conclusion", 'References'] # 初始化一个字典来存储找到的章节和它们在文档中出现的页码 section_page_dict = {} # 遍历每一页文档 for page_index, page in enumerate(self.pdf): # 获取当前页面的文本内容 cur_text = page.get_text() # 遍历需要寻找的章节名称列表 for section_name in section_list: # 将章节名称转换成大写形式 section_name_upper = section_name.upper() # 如果当前页面包含"Abstract"这个关键词 if "Abstract" == section_name and section_name in cur_text: # 将"Abstract"和它所在的页码加入字典中 section_page_dict[section_name] = page_index # 如果当前页面包含章节名称,则将章节名称和它所在的页码加入字典中 else: if section_name + '\n' in cur_text: section_page_dict[section_name] = page_index elif section_name_upper + '\n' in cur_text: section_page_dict[section_name] = page_index # 返回所有找到的章节名称及它们在文档中出现的页码 return section_page_dict
首先是这段代码,作用是将对应的出现abstract等关键字的页码记录下来,记录成一个字典。具体还有很多很细的操作,这个函数的核心逻辑是根据每个章节的起始页码和结束页码,提取和拼接文本。其具体操作是:遍历 self.section_page_dict(假定为章节名与起始页码的映射字典),使用 sec_index 和 sec_name 分别表示当前章节的索引和名称,如果 sec_index 小于等于0且存在摘要 (self.abs),则跳过当前迭代。
从 self.section_page_dict 中获取当前章节的起始页码,确定章节的结束页码。如果当前章节不是最后一个章节,则结束页码为下一章节的起始页;否则为文本列表的长度。如果章节只包含一页面,则特殊处理以提取章节标题之间的文本。否则,遍历从起始页到结束页的范围,拼接这些页面的文本。
如果是章节的第一页,仅提取章节标题之后的文本。如果是章节的最后一页,仅提取到下一章节标题之前的文本。(特殊处理方式:替换文本中的 ‘-\n’(可能是由于PDF格式造成的断行)和 ‘\n’(换行符)
总结:简单的说就是找到关键字,看下一个关键字是不是和这个关键字在同一页,如果不是则是上一个关键字的页码-1。
这么操作有一定的误差,比如论文的排版的下一个关键字在第3页,但是第三页上也存在着上一个关键字的部分内容,则这一部分内容不会被提取到。
根据上述方法最后读取出来的内容应如下所示,可以看出,得到的结果还是相对准确的。但是对于各种各样的PDF论文格式并不能一概而论。
由于现在GPT的API除了需要账户也需要进行充值,每条消息大概0.013刀,因此这里采用了一个代替GPT的summary模型。由于时间关系和工作量关系,我们直接使用huggingface上的mT5-multilingual-XLSum的开源模型,可以总结各种语言的概要。
使用方法也非常简单,只需要管道调用即可。
summarizer = pipeline("summarization", model=eng_cache_dir, tokenizer=eng_cache_dir, config=eng_cache_dir) #将paper里的Abstract和Discussion提取出来,拼接在一起。如果没有Discussion,就只选Abstract。如果没有Abstract,就只选Discussion。 for paper in paper_list: if paper.section_text_dict.get('Discussion') and paper.section_text_dict.get('Abstract'): paper.abstract_and_discussion = paper.section_text_dict.get('Abstract') + paper.section_text_dict.get('Discussion') elif paper.section_text_dict.get('Discussion') and not paper.section_text_dict.get('Abstract'): paper.abstract_and_discussion = paper.section_text_dict.get('Discussion') elif paper.section_text_dict.get('Abstract') and not paper.section_text_dict.get('Discussion'): paper.abstract_and_discussion = paper.section_text_dict.get('Abstract') else: paper.abstract_and_discussion = 'Abstract not found.' # 生成摘要 summary = summarizer(paper.abstract_and_discussion, max_length=230, min_length=100, do_sample=False) # 打印标题 print(paper.title) # 打印摘要 print(summary[0]['summary_text'])
这里还可以指定总结文本的长度,再次不在赘述。
至此,一个简单的pdf总结的模型就做好了。后续还会添加诸多功能。比如说,自动从网站上下载一周的最新论文,比如说根据喜好进行推送,对输入的关键字进行推送,直接推送论文结果等等。如果有后续开发我会同步更新
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。