前言
之前7月中旬,我曾在微博上說準備做“20個LLM大型項目的源碼解讀”
針對這個事,目前的最新情況是
- 已經做了的:LLaMA、Alpaca、ChatGLM-6B、deepspeedchat、transformer、langchain、langchain-chatglm知識庫
- 準備做的:chatpaper、deepspeed、Megatron-LM
- 再往后則:BERT、GPT、pytorch、chatdoctor、baichuan、BLOOM/BELLE、Chinese LLaMA、PEFT BLIP2 llama.cpp
總之,夠未來半年忙了。為加快這個事情的進度,本文解讀兩個關于學術論文的GPT(由于我司每周都有好幾個或為申博、或為評職稱、或為畢業而報名論文1V1發表輔導的,比如中文期刊、EI會議、ei期刊/SCI等等,所以對這個方向一直都是高度關注,我司也在做類似的LLM產品,敬請期待)
- 一個是chatpaper:https://github.com/kaixindelele/ChatPaper
- 一個是gpt_academic:https://github.com/binary-husky/gpt_academic
我把這兩個項目的結構做了拆解/解析,且基本把原有代碼的每一行都補上了注釋,如果大家對任何一行代碼有疑問,可以隨時在本文評論區留言,我會及時做補充說明
第一部分 ChatPaper:論文對話、總結、翻譯
ChatPaper的自身定位是全流程加速科研:論文總結+專業級翻譯+潤色+審稿+審稿回復,因為論文更多是PDF的格式,故針對PDF的對話、總結、翻譯,便不可避免的涉及到PDF的解析
1.1?ChatPaper/ChatReviewerAndResponse
1.1.1 對PDF的解析:ChatReviewerAndResponse/get_paper.py
// 待更
1.1.2 論文審查:ChatReviewerAndResponse/chat_reviewer.py
使用OpenAI的GPT模型進行論文審查的腳本。它首先定義了一個Reviewer類來處理審查工作,然后在if __name__ == '__main__':語句下使用argparse處理命令行參數,并調用chat_reviewer_main函數來開始審查過程
- 導入模塊:與第一段代碼相似,但新增了一些庫,如jieba、tenacity等
- 命名元組定義:用于保存與論文審稿相關的參數
ReviewerParams = namedtuple("ReviewerParams",["paper_path","file_format","research_fields","language"], )
- 判斷文本中是否包含中文:
def contains_chinese(text):for ch in text:if u'\u4e00' <= ch <= u'\u9fff':return Truereturn False
- 插入句子到文本
主要功能是在給定文本的每隔一定數量的單詞或中文字符后插入一個指定的句子。如果文本行包含中文字符,則使用jieba分詞工具來切分中文,否則使用空格來切分:def insert_sentence(text, sentence, interval):# 將輸入文本按換行符分割成行lines = text.split('\n')# 初始化一個新的行列表new_lines = []# 遍歷每一行for line in lines:# 檢查行中是否包含中文字符if contains_chinese(line):# 如果是中文,使用jieba分詞工具進行分詞words = list(jieba.cut(line))# 定義分隔符為空字符(對于中文分詞)separator = ''else:# 如果不包含中文,按空格分割行words = line.split()# 定義分隔符為空格(對于英文或其他非中文語言)separator = ' '# 初始化一個新的單詞列表new_words = []# 初始化一個計數器count = 0# 遍歷當前行的每一個單詞for word in words:# 將當前單詞添加到新的單詞列表new_words.append(word)# 計數器增加count += 1# 檢查是否達到了插入句子的間隔if count % interval == 0:# 在達到指定間隔時,將要插入的句子添加到新的單詞列表new_words.append(sentence)# 將新的單詞列表連接起來,并添加到新的行列表new_lines.append(separator.join(new_words))# 將新的行列表連接起來,返回結果return '\n'.join(new_lines)
- 論文審稿類:定義了一個Reviewer類,包含以下功能:
? 第一階段審稿:先是基于論文標題和摘要,選擇要審稿的部分
然后分別實現兩個函數# 定義Reviewer類 class Reviewer:# 初始化方法,設置屬性def __init__(self, args=None):if args.language == 'en':self.language = 'English'elif args.language == 'zh':self.language = 'Chinese'else:self.language = 'Chinese' # 創建一個ConfigParser對象self.config = configparser.ConfigParser()# 讀取配置文件self.config.read('apikey.ini')# 獲取某個鍵對應的值 self.chat_api_list = self.config.get('OpenAI', 'OPENAI_API_KEYS')[1:-1].replace('\'', '').split(',')self.chat_api_list = [api.strip() for api in self.chat_api_list if len(api) > 5]self.cur_api = 0self.file_format = args.file_format self.max_token_num = 4096self.encoding = tiktoken.get_encoding("gpt2")def validateTitle(self, title):# 修正論文的路徑格式rstr = r"[\/\\\:\*\?\"\<\>\|]" # '/ \ : * ? " < > |'new_title = re.sub(rstr, "_", title) # 替換為下劃線return new_title
一個stage_1,主要功能是為了與GPT-3模型進行對話,獲取模型對于文章的兩個最關鍵部分的選擇意見
一個chat_review,主要功能是調用GPT-3模型進行論文審稿,對輸入的文章文本進行審查,并按照預定格式生成審稿意見def stage_1(self, paper):# 初始化一個空列表,用于存儲生成的HTML內容htmls = []# 初始化一個空字符串,用于存儲文章的標題和摘要text = ''# 添加文章的標題text += 'Title: ' + paper.title + '. '# 添加文章的摘要text += 'Abstract: ' + paper.section_texts['Abstract']# 計算文本的token數量text_token = len(self.encoding.encode(text))# 判斷token數量是否超過最大token限制的一半減去800if text_token > self.max_token_num/2 - 800:input_text_index = int(len(text)*((self.max_token_num/2)-800)/text_token)# 如果超出,則截取文本以滿足長度要求text = text[:input_text_index]# 設置OpenAI API的密鑰openai.api_key = self.chat_api_list[self.cur_api]# 更新當前使用的API索引self.cur_api += 1# 如果當前API索引超過API列表的長度,則重置為0self.cur_api = 0 if self.cur_api >= len(self.chat_api_list)-1 else self.cur_api# 創建與GPT-3的對話消息messages = [{"role": "system","content": f"You are a professional reviewer in the field of {args.research_fields}. "f"I will give you a paper. You need to review this paper and discuss the novelty and originality of ideas, correctness, clarity, the significance of results, potential impact and quality of the presentation. "f"Due to the length limitations, I am only allowed to provide you the abstract, introduction, conclusion and at most two sections of this paper."f"Now I will give you the title and abstract and the headings of potential sections. "f"You need to reply at most two headings. Then I will further provide you the full information, includes aforementioned sections and at most two sections you called for.\n\n"f"Title: {paper.title}\n\n"f"Abstract: {paper.section_texts['Abstract']}\n\n"f"Potential Sections: {paper.section_names[2:-1]}\n\n"f"Follow the following format to output your choice of sections:"f"{{chosen section 1}}, {{chosen section 2}}\n\n"},{"role": "user", "content": text},]# 調用OpenAI API與GPT-3進行對話response = openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=messages,)# 初始化一個空字符串,用于存儲模型的回復result = ''# 遍歷模型的回復,將其添加到結果字符串中for choice in response.choices:result += choice.message.content# 打印模型的回復print(result)# 返回模型的回復,將其分割為多個部分return result.split(',')
def chat_review(self, text):# 設置OpenAI API的密鑰openai.api_key = self.chat_api_list[self.cur_api]# 更新當前使用的API密鑰索引self.cur_api += 1# 如果當前API密鑰索引超過API密鑰列表的長度,則將其重置為0self.cur_api = 0 if self.cur_api >= len(self.chat_api_list)-1 else self.cur_api# 定義用于審稿提示的token數量review_prompt_token = 1000# 計算輸入文本的token數量text_token = len(self.encoding.encode(text))# 計算輸入文本的截取位置input_text_index = int(len(text)*(self.max_token_num-review_prompt_token)/text_token)# 截取文本并添加前綴input_text = "This is the paper for your review:" + text[:input_text_index]# 從'ReviewFormat.txt'文件中讀取審稿格式with open('ReviewFormat.txt', 'r') as file:review_format = file.read()# 創建與GPT-3的對話消息messages=[{"role": "system", "content": "You are a professional reviewer in the field of "+args.research_fields+". Now I will give you a paper. You need to give a complete review opinion according to the following requirements and format:"+ review_format +" Please answer in {}.".format(self.language)},{"role": "user", "content": input_text},]# 調用OpenAI API與GPT-3進行對話response = openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=messages,)# 初始化一個空字符串,用于存儲模型的回復result = ''# 遍歷模型的回復,將其添加到結果字符串中for choice in response.choices:result += choice.message.content# 在結果中插入特定的句子,警告不允許復制result = insert_sentence(result, '**Generated by ChatGPT, no copying allowed!**', 15)# 追加倫理聲明result += "\n\n?倫理聲明/Ethics statement:\n--禁止直接復制生成的評論用于任何論文審稿工作!\n--Direct copying of generated comments for any paper review work is prohibited!"# 打印分隔符和結果print("********"*10)print(result)print("********"*10)# 打印相關的token使用信息和響應時間print("prompt_token_used:", response.usage.prompt_tokens)print("completion_token_used:", response.usage.completion_tokens)print("total_token_used:", response.usage.total_tokens)print("response_time:", response.response_ms/1000.0, 's')# 返回模型生成的審稿意見return result
? 使用ChatGPT進行審稿,且有tenacity重試機制和更多的功能,其中review_by_chatgpt?調用了上面所示的兩個函數,一個stage_1,一個chat_review
def review_by_chatgpt(self, paper_list):# 創建一個空列表用于存儲每篇文章審稿后的HTML格式內容htmls = []# 遍歷paper_list中的每一篇文章for paper_index, paper in enumerate(paper_list):# 使用第一階段審稿方法選擇文章的關鍵部分sections_of_interest = self.stage_1(paper)# 初始化一個空字符串用于提取文章的主要部分text = ''# 添加文章的標題text += 'Title:' + paper.title + '. '# 添加文章的摘要text += 'Abstract: ' + paper.section_texts['Abstract']# 查找并添加“Introduction”部分intro_title = next((item for item in paper.section_names if 'ntroduction' in item.lower()), None)if intro_title is not None:text += 'Introduction: ' + paper.section_texts[intro_title]# 同樣地,查找并添加“Conclusion”部分conclusion_title = next((item for item in paper.section_names if 'onclusion' in item), None)if conclusion_title is not None:text += 'Conclusion: ' + paper.section_texts[conclusion_title]# 遍歷sections_of_interest,添加其他感興趣的部分for heading in sections_of_interest:if heading in paper.section_names:text += heading + ': ' + paper.section_texts[heading]# 使用ChatGPT進行審稿,并得到審稿內容chat_review_text = self.chat_review(text=text)# 將審稿的文章編號和內容添加到htmls列表中htmls.append('## Paper:' + str(paper_index+1))htmls.append('\n\n\n')htmls.append(chat_review_text)# 獲取當前日期和時間,并轉換為字符串格式date_str = str(datetime.datetime.now())[:13].replace(' ', '-')try:# 創建輸出文件夾export_path = os.path.join('./', 'output_file')os.makedirs(export_path)except:# 如果文件夾已存在,則不執行任何操作pass# 如果是第一篇文章,則寫模式為'w',否則為'a'mode = 'w' if paper_index == 0 else 'a'# 根據文章標題和日期生成文件名file_name = os.path.join(export_path, date_str+'-'+self.validateTitle(paper.title)+"."+self.file_format)# 將審稿內容導出為Markdown格式并保存self.export_to_markdown("\n".join(htmls), file_name=file_name, mode=mode)# 清空htmls列表,為下一篇文章做準備htmls = []
- 主程序部分:
定義了一個chat_reviewer_main?函數,該函數創建了一個Reviewer對象,并對指定路徑中的PDF文件進行審稿
主程序中定義了命令行參數解析,并調用了chat_reviewer_main?函數def chat_reviewer_main(args): reviewer1 = Reviewer(args=args)# 開始判斷是路徑還是文件: paper_list = [] if args.paper_path.endswith(".pdf"):paper_list.append(Paper(path=args.paper_path)) else:for root, dirs, files in os.walk(args.paper_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))) [print(paper_index, paper_name.path.split('\\')[-1]) for paper_index, paper_name in enumerate(paper_list)]reviewer1.review_by_chatgpt(paper_list=paper_list)
在主程序中增加了審稿時間的計算功能if __name__ == '__main__': parser = argparse.ArgumentParser()parser.add_argument("--paper_path", type=str, default='', help="path of papers")parser.add_argument("--file_format", type=str, default='txt', help="output file format")parser.add_argument("--research_fields", type=str, default='computer science, artificial intelligence and reinforcement learning', help="the research fields of paper")parser.add_argument("--language", type=str, default='en', help="output lauguage, en or zh")reviewer_args = ReviewerParams(**vars(parser.parse_args()))start_time = time.time()chat_reviewer_main(args=reviewer_args)print("review time:", time.time() - start_time)
// 待更