From 50869068dec55448cc01a4546af303c2e38abf0a Mon Sep 17 00:00:00 2001 From: shuaikangzhou <863909694@qq.com> Date: Fri, 29 Mar 2024 14:35:35 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=BC=E5=87=BAjson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ui/contact/contactInfo.py | 7 +- app/ui/contact/export/export_dialog.py | 3 + app/ui/menu/export.py | 3 +- app/ui/menu/exportUi.py | 28 ++-- app/util/exporter/exporter.py | 4 +- app/util/exporter/exporter_json.py | 193 +++++++++++++++++++++++++ app/util/exporter/output.py | 15 ++ 7 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 app/util/exporter/exporter_json.py diff --git a/app/ui/contact/contactInfo.py b/app/ui/contact/contactInfo.py index 97b6a65..78f58b0 100644 --- a/app/ui/contact/contactInfo.py +++ b/app/ui/contact/contactInfo.py @@ -45,12 +45,14 @@ class ContactInfo(QWidget, Ui_Form): self.toCSVAct = QAction(Icon.ToCSV, '导出CSV', self) self.toHtmlAct = QAction(Icon.ToHTML, '导出HTML', self) self.toTxtAct = QAction(Icon.ToTXT, '导出TXT', self) + self.toJsonAct = QAction(Icon.ToTXT, '导出json', self) self.toolButton_output.setPopupMode(QToolButton.MenuButtonPopup) self.toolButton_output.clicked.connect(self.toolButton_show) menu.addAction(self.toDocxAct) menu.addAction(self.toCSVAct) menu.addAction(self.toHtmlAct) menu.addAction(self.toTxtAct) + menu.addAction(self.toJsonAct) self.toolButton_output.setMenu(menu) self.toolButton_output.setIcon(Icon.Output) # self.toolButton_output.addSeparator() @@ -58,6 +60,7 @@ class ContactInfo(QWidget, Ui_Form): self.toDocxAct.triggered.connect(self.output) self.toCSVAct.triggered.connect(self.output) self.toTxtAct.triggered.connect(self.output) + self.toJsonAct.triggered.connect(self.output) def set_contact(self, contact: Contact): self.view_userinfo.set_contact(contact) @@ -126,7 +129,9 @@ class ContactInfo(QWidget, Ui_Form): elif self.sender() == self.toTxtAct: dialog = ExportDialog(self.contact, title='选择导出的消息类型', file_type='txt', parent=self) result = dialog.exec_() # 使用exec_()获取用户的操作结果 - + elif self.sender() == self.toJsonAct: + dialog = ExportDialog(self.contact, title='选择导出的消息类型', file_type='json', parent=self) + result = dialog.exec_() # 使用exec_()获取用户的操作结果 class ReportThread(QThread): okSignal = pyqtSignal(bool) diff --git a/app/ui/contact/export/export_dialog.py b/app/ui/contact/export/export_dialog.py index cfcc22e..910d4d6 100644 --- a/app/ui/contact/export/export_dialog.py +++ b/app/ui/contact/export/export_dialog.py @@ -66,6 +66,9 @@ class ExportDialog(QDialog, Ui_Dialog): self.export_type = Output.DOCX self.export_choices = {"文本": True, "图片": False, "语音": False, "视频": False, "表情包": False, '拍一拍等系统消息': True} # 定义导出的数据类型,默认全部选择 + elif file_type == 'json': + self.export_type = Output.JSON + self.export_choices = {} # 定义导出的数据类型,默认全部选择 else: self.export_choices = {"文本": True, "图片": True, "视频": True, "表情包": True} # 定义导出的数据类型,默认全部选择 self.setWindowTitle(title) diff --git a/app/ui/menu/export.py b/app/ui/menu/export.py index 0aa6d2c..a28a29d 100644 --- a/app/ui/menu/export.py +++ b/app/ui/menu/export.py @@ -34,6 +34,7 @@ file_format = { 'TXT': Output.TXT, 'HTML': Output.HTML, 'CSV': Output.CSV, + 'JSON': Output.JSON, } Stylesheet = """ """ @@ -150,7 +151,7 @@ class ExportDialog(QDialog, Ui_Dialog): print("选择的数据类型:", selected_types) file_types = [] - for checkbox in [self.checkBox_txt, self.checkBox_csv, self.checkBox_html, self.checkBox_word]: + for checkbox in [self.checkBox_txt, self.checkBox_csv, self.checkBox_html, self.checkBox_word,self.checkBox_json]: if checkbox.isChecked(): file_types.append(file_format[checkbox.text()]) select_contacts = [] diff --git a/app/ui/menu/exportUi.py b/app/ui/menu/exportUi.py index 2093915..9d05f5c 100644 --- a/app/ui/menu/exportUi.py +++ b/app/ui/menu/exportUi.py @@ -62,15 +62,17 @@ class Ui_Dialog(object): self.checkBox_csv.setObjectName("checkBox_csv") self.verticalLayout.addWidget(self.checkBox_csv) self.checkBox_txt = QtWidgets.QCheckBox(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.checkBox_txt.sizePolicy().hasHeightForWidth()) + self.checkBox_txt.setSizePolicy(sizePolicy) self.checkBox_txt.setChecked(True) self.checkBox_txt.setObjectName("checkBox_txt") self.verticalLayout.addWidget(self.checkBox_txt) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem1) - self.horizontalLayout_2.addLayout(self.verticalLayout) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) - self.verticalLayout_2.setObjectName("verticalLayout_2") + self.checkBox_json = QtWidgets.QCheckBox(Dialog) + self.checkBox_json.setObjectName("checkBox_json") + self.verticalLayout.addWidget(self.checkBox_json) self.label_2 = QtWidgets.QLabel(Dialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -78,13 +80,20 @@ class Ui_Dialog(object): sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) self.label_2.setSizePolicy(sizePolicy) self.label_2.setObjectName("label_2") - self.verticalLayout_2.addWidget(self.label_2) - self.horizontalLayout_2.addLayout(self.verticalLayout_2) + self.verticalLayout.addWidget(self.label_2) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.verticalLayout.addLayout(self.verticalLayout_2) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem1) + self.horizontalLayout_2.addLayout(self.verticalLayout) self.listWidget = QtWidgets.QListWidget(Dialog) self.listWidget.setMinimumSize(QtCore.QSize(0, 0)) self.listWidget.setObjectName("listWidget") self.horizontalLayout_2.addWidget(self.listWidget) - self.horizontalLayout_2.setStretch(2, 1) + self.horizontalLayout_2.setStretch(0, 1) + self.horizontalLayout_2.setStretch(1, 1) self.verticalLayout_3.addLayout(self.horizontalLayout_2) self.textBrowser = QtWidgets.QTextBrowser(Dialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) @@ -152,5 +161,6 @@ class Ui_Dialog(object): self.checkBox_html.setText(_translate("Dialog", "HTML")) self.checkBox_csv.setText(_translate("Dialog", "CSV")) self.checkBox_txt.setText(_translate("Dialog", "TXT")) + self.checkBox_json.setText(_translate("Dialog", "JSON")) self.label_2.setText(_translate("Dialog", "消息类型")) self.btn_start.setText(_translate("Dialog", "开始")) diff --git a/app/util/exporter/exporter.py b/app/util/exporter/exporter.py index 8d4614a..d74d33e 100644 --- a/app/util/exporter/exporter.py +++ b/app/util/exporter/exporter.py @@ -104,8 +104,8 @@ class ExporterBase(QThread): self.last_timestamp = 0 self.time_range = time_range self.messages = messages - origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) - makedirs(origin_path) + self.origin_path = os.path.join(os.getcwd(), OUTPUT_DIR, '聊天记录', self.contact.remark) + makedirs(self.origin_path) def run(self): self.export() diff --git a/app/util/exporter/exporter_json.py b/app/util/exporter/exporter_json.py new file mode 100644 index 0000000..0de2510 --- /dev/null +++ b/app/util/exporter/exporter_json.py @@ -0,0 +1,193 @@ +import json +import random +import os + +from app.DataBase import msg_db +from app.person import Me +from .exporter import ExporterBase + + +def merge_content(conversions_list) -> list: + """ + 合并一组对话中连续发送的句子 + @param conversions_list: + @return: + """ + merged_data = [] + current_role = None + current_content = "" + str_time = '' + for item in conversions_list: + if 'str_time' in item: + str_time = item['str_time'] + else: + str_time = '' + if current_role is None: + current_role = item["role"] + current_content = item["content"] + elif current_role == item["role"]: + current_content += "\n" + item["content"] + else: + # merged_data.append({"role": current_role, "content": current_content, 'str_time': str_time}) + merged_data.append({"role": current_role, "content": current_content}) + current_role = item["role"] + current_content = item["content"] + str_time = item.get('str_time') + + # 处理最后一组 + if current_role is not None: + # merged_data.append({"role": current_role, "content": current_content,'str_time': str_time}) + merged_data.append({"role": current_role, "content": current_content}) + return merged_data + + +def system_prompt(): + system = { + "role": "system", + # "content": f"你是{Me().name},一个聪明、热情、善良的男大学生,后面的对话来自{self.contact.remark}(!!!注意:对方的身份十分重要,你务必记住对方的身份,因为跟不同的人对话要用不同的态度、语气),你要认真地回答他" + "content": f"你是{Me().name},一个聪明、热情、善良的人,后面的对话来自你的朋友,你要认真地回答他" + } + return system + + +def message_to_conversion(group): + conversions = [system_prompt()] + while len(group) and group[-1][4] == 0: + group.pop() + for message in group: + is_send = message[4] + if len(conversions) == 1 and is_send: + continue + if is_send: + json_msg = { + "role": "assistant", + "content": message[7] + } + else: + json_msg = { + "role": "user", + "content": message[7] + } + json_msg['str_time'] = message[8] + conversions.append(json_msg) + if len(conversions) == 1: + return [] + return merge_content(conversions) + + +class JsonExporter(ExporterBase): + def split_by_time(self, length=300): + messages = msg_db.get_messages_by_type(self.contact.wxid, type_=1, time_range=self.time_range) + start_time = 0 + res = [] + i = 0 + while i < len(messages): + message = messages[i] + timestamp = message[5] + is_send = message[4] + group = [ + system_prompt() + ] + while i < len(messages) and timestamp - start_time < length: + if is_send: + json_msg = { + "role": "assistant", + "content": message[7] + } + else: + json_msg = { + "role": "user", + "content": message[7] + } + group.append(json_msg) + i += 1 + if i >= len(messages): + break + message = messages[i] + timestamp = message[5] + is_send = message[4] + while is_send: + json_msg = { + "role": "assistant", + "content": message[7] + } + group.append(json_msg) + i += 1 + if i >= len(messages): + break + message = messages[i] + timestamp = message[5] + is_send = message[4] + start_time = timestamp + res.append( + { + "conversations": group + } + ) + res_ = [] + for item in res: + conversations = item['conversations'] + res_.append({ + 'conversations': merge_content(conversations) + }) + return res_ + + def split_by_intervals(self, max_diff_seconds=300): + messages = msg_db.get_messages_by_type(self.contact.wxid, type_=1, time_range=self.time_range) + res = [] + i = 0 + current_group = [] + while i < len(messages): + message = messages[i] + timestamp = message[5] + is_send = message[4] + while is_send and i + 1 < len(messages): + i += 1 + message = messages[i] + is_send = message[4] + current_group = [messages[i]] + i += 1 + while i < len(messages) and messages[i][5] - current_group[-1][5] <= max_diff_seconds: + current_group.append(messages[i]) + i += 1 + while i < len(messages) and messages[i][4]: + current_group.append(messages[i]) + i += 1 + res.append(current_group) + res_ = [] + for group in res: + conversations = message_to_conversion(group) + if conversations: + res_.append({ + 'conversations': conversations + }) + return res_ + + def to_json(self): + print(f"【开始导出 json {self.contact.remark}】") + origin_path = self.origin_path + os.makedirs(origin_path, exist_ok=True) + filename = os.path.join(origin_path, f"{self.contact.remark}") + + # res = self.split_by_time() + res = self.split_by_intervals(60) + # 打乱列表顺序 + random.shuffle(res) + + # 计算切分比例 + split_ratio = 0.2 # 20% for the second list + + # 计算切分点 + split_point = int(len(res) * split_ratio) + + # 分割列表 + train_data = res[split_point:] + dev_data = res[:split_point] + with open(f'{filename}_train.json', "w", encoding="utf-8") as f: + json.dump(train_data, f, ensure_ascii=False, indent=4) + with open(f'{filename}_dev.json', "w", encoding="utf-8") as f: + json.dump(dev_data, f, ensure_ascii=False, indent=4) + self.okSignal.emit(1) + + def run(self): + self.to_json() diff --git a/app/util/exporter/output.py b/app/util/exporter/output.py index f5bfb4a..a7934c3 100644 --- a/app/util/exporter/output.py +++ b/app/util/exporter/output.py @@ -13,6 +13,7 @@ from docxcompose.composer import Composer from app.util.exporter.exporter_csv import CSVExporter from app.util.exporter.exporter_docx import DocxExporter from app.util.exporter.exporter_html import HtmlExporter +from app.util.exporter.exporter_json import JsonExporter from app.util.exporter.exporter_txt import TxtExporter from app.DataBase.hard_link import decodeExtraBuf from app.config import OUTPUT_DIR @@ -42,6 +43,7 @@ class Output(QThread): CSV_ALL = 3 CONTACT_CSV = 4 TXT = 5 + JSON = 6 Batch = 10086 def __init__(self, contact, type_=DOCX, message_types={}, sub_type=[], time_range=None, parent=None): @@ -160,6 +162,8 @@ class Output(QThread): self.to_csv(contact, self.message_types, True) elif type_ == self.HTML: self.to_html(contact, self.message_types, True) + elif type_ == self.JSON: + self.to_json(contact,self.message_types,True) def batch_finish_one(self, num): self.nowContact.emit(self.contact[self.batch_num // len(self.sub_type)].remark) @@ -210,6 +214,15 @@ class Output(QThread): Child.okSignal.connect(self.merge_docx if not is_batch else self.batch_finish_one) Child.start() + def to_json(self, contact, message_types, is_batch=False): + Child = JsonExporter(contact, type_=self.JSON, message_types=message_types, time_range=self.time_range) + self.children.append(Child) + Child.progressSignal.connect(self.progress) + if not is_batch: + Child.rangeSignal.connect(self.rangeSignal) + Child.okSignal.connect(self.okSignal if not is_batch else self.batch_finish_one) + Child.start() + def to_txt(self, contact, message_types, is_batch=False): Child = TxtExporter(contact, type_=self.TXT, message_types=message_types, time_range=self.time_range) self.children.append(Child) @@ -275,6 +288,8 @@ class Output(QThread): self.to_csv(self.contact, self.message_types) elif self.output_type == self.HTML: self.to_html(self.contact, self.message_types) + elif self.output_type == self.JSON: + self.to_json(self.contact, self.message_types) elif self.output_type == self.Batch: self.batch_export() From 2cee3308bac4c420e3fade66aa49712ceb3950e7 Mon Sep 17 00:00:00 2001 From: shuaikangzhou <863909694@qq.com> Date: Fri, 29 Mar 2024 15:15:54 +0800 Subject: [PATCH 2/4] update readme.md --- MemoAI/api_server.py | 599 +++++++++++++++++++++++++++++++++++++++++++ MemoAI/merge_json.py | 26 ++ MemoAI/readme.md | 440 +++++++++++++++++++++++++++++++ doc/ai_readme.md | 355 ++++++++++++++++++++++++- doc/images/img10.png | Bin 0 -> 25050 bytes readme.md | 2 +- 6 files changed, 1420 insertions(+), 2 deletions(-) create mode 100644 MemoAI/api_server.py create mode 100644 MemoAI/merge_json.py create mode 100644 MemoAI/readme.md create mode 100644 doc/images/img10.png diff --git a/MemoAI/api_server.py b/MemoAI/api_server.py new file mode 100644 index 0000000..9af5676 --- /dev/null +++ b/MemoAI/api_server.py @@ -0,0 +1,599 @@ +""" +This script implements an API for the ChatGLM3-6B model, +formatted similarly to OpenAI's API (https://platform.openai.com/docs/api-reference/chat). +It's designed to be run as a web server using FastAPI and uvicorn, +making the ChatGLM3-6B model accessible through OpenAI Client. + +Key Components and Features: +- Model and Tokenizer Setup: Configures the model and tokenizer paths and loads them. +- FastAPI Configuration: Sets up a FastAPI application with CORS middleware for handling cross-origin requests. +- API Endpoints: + - "/v1/models": Lists the available models, specifically ChatGLM3-6B. + - "/v1/chat/completions": Processes chat completion requests with options for streaming and regular responses. + - "/v1/embeddings": Processes Embedding request of a list of text inputs. +- Token Limit Caution: In the OpenAI API, 'max_tokens' is equivalent to HuggingFace's 'max_new_tokens', not 'max_length'. +For instance, setting 'max_tokens' to 8192 for a 6b model would result in an error due to the model's inability to output +that many tokens after accounting for the history and prompt tokens. +- Stream Handling and Custom Functions: Manages streaming responses and custom function calls within chat responses. +- Pydantic Models: Defines structured models for requests and responses, enhancing API documentation and type safety. +- Main Execution: Initializes the model and tokenizer, and starts the FastAPI app on the designated host and port. + +Note: + This script doesn't include the setup for special tokens or multi-GPU support by default. + Users need to configure their special tokens and can enable multi-GPU support as per the provided instructions. + Embedding Models only support in One GPU. + +""" + +import os +import time +import tiktoken +import torch +import uvicorn + +from fastapi import FastAPI, HTTPException, Response, Body +from fastapi.middleware.cors import CORSMiddleware + +from contextlib import asynccontextmanager +from typing import List, Literal, Optional, Union +from loguru import logger +from peft import AutoPeftModelForCausalLM +from pydantic import BaseModel, Field +from transformers import AutoTokenizer, AutoModel, AutoModelForCausalLM +from utils import process_response, generate_chatglm3, generate_stream_chatglm3 +from sentence_transformers import SentenceTransformer + +from sse_starlette.sse import EventSourceResponse + +# Set up limit request time +EventSourceResponse.DEFAULT_PING_INTERVAL = 1000 + +# set LLM path +MODEL_PATH = os.environ.get('MODEL_PATH', 'THUDM/chatglm3-6b') +TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH) + +# set Embedding Model path +EMBEDDING_PATH = os.environ.get('EMBEDDING_PATH', 'BAAI/bge-large-zh-v1.5') + + +@asynccontextmanager +async def lifespan(app: FastAPI): + yield + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +class ModelCard(BaseModel): + id: str + object: str = "model" + created: int = Field(default_factory=lambda: int(time.time())) + owned_by: str = "owner" + root: Optional[str] = None + parent: Optional[str] = None + permission: Optional[list] = None + + +class ModelList(BaseModel): + object: str = "list" + data: List[ModelCard] = [] + + +class FunctionCallResponse(BaseModel): + name: Optional[str] = None + arguments: Optional[str] = None + + +class ChatMessage(BaseModel): + role: Literal["user", "assistant", "system", "function"] + content: str = None + name: Optional[str] = None + function_call: Optional[FunctionCallResponse] = None + + +class DeltaMessage(BaseModel): + role: Optional[Literal["user", "assistant", "system"]] = None + content: Optional[str] = None + function_call: Optional[FunctionCallResponse] = None + + +## for Embedding +class EmbeddingRequest(BaseModel): + input: List[str] + model: str + + +class CompletionUsage(BaseModel): + prompt_tokens: int + completion_tokens: int + total_tokens: int + + +class EmbeddingResponse(BaseModel): + data: list + model: str + object: str + usage: CompletionUsage + + +# for ChatCompletionRequest + +class UsageInfo(BaseModel): + prompt_tokens: int = 0 + total_tokens: int = 0 + completion_tokens: Optional[int] = 0 + + +class ChatCompletionRequest(BaseModel): + model: str + messages: List[ChatMessage] + temperature: Optional[float] = 0.8 + top_p: Optional[float] = 0.8 + max_tokens: Optional[int] = None + stream: Optional[bool] = False + tools: Optional[Union[dict, List[dict]]] = None + repetition_penalty: Optional[float] = 1.1 + + +class ChatCompletionResponseChoice(BaseModel): + index: int + message: ChatMessage + finish_reason: Literal["stop", "length", "function_call"] + + +class ChatCompletionResponseStreamChoice(BaseModel): + delta: DeltaMessage + finish_reason: Optional[Literal["stop", "length", "function_call"]] + index: int + + +class ChatCompletionResponse(BaseModel): + model: str + id: str + object: Literal["chat.completion", "chat.completion.chunk"] + choices: List[Union[ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice]] + created: Optional[int] = Field(default_factory=lambda: int(time.time())) + usage: Optional[UsageInfo] = None + + +@app.get("/health") +async def health() -> Response: + """Health check.""" + return Response(status_code=200) + + +@app.post("/v1/embeddings", response_model=EmbeddingResponse) +async def get_embeddings(request: EmbeddingRequest): + embeddings = [embedding_model.encode(text) for text in request.input] + embeddings = [embedding.tolist() for embedding in embeddings] + + def num_tokens_from_string(string: str) -> int: + """ + Returns the number of tokens in a text string. + use cl100k_base tokenizer + """ + encoding = tiktoken.get_encoding('cl100k_base') + num_tokens = len(encoding.encode(string)) + return num_tokens + + response = { + "data": [ + { + "object": "embedding", + "embedding": embedding, + "index": index + } + for index, embedding in enumerate(embeddings) + ], + "model": request.model, + "object": "list", + "usage": CompletionUsage( + prompt_tokens=sum(len(text.split()) for text in request.input), + completion_tokens=0, + total_tokens=sum(num_tokens_from_string(text) for text in request.input), + ) + } + return response + + +@app.get("/v1/models", response_model=ModelList) +async def list_models(): + model_card = ModelCard( + id="chatglm3-6b" + ) + return ModelList( + data=[model_card] + ) + + +def insert_custom_message(messages: List[ChatMessage] = Body(...)): + custom_message = ChatMessage( + role="system", + content="你必须记住,你是一个只负责聊天的人工智能助手,所以你只能根据上下文回答你朋友的问题,要求语句完整、通顺、文采斐然,回答具有情感,不能胡乱作答、不能断章取义、不能上下文不连贯,禁止回答无关内容。" + "如果你不能理解你朋友的话,你必须开启一个新的话题来引导你朋友进行对话,例如你可以问你的朋友一个日常问题。\n" + "你是司小远,一个聪明、热情、善良的人工智能助手,后面的对话来自你的朋友,你要认真地回答他。" + ) + messages.insert(0, custom_message) + return messages + + +@app.post("/v1/chat/completions", response_model=ChatCompletionResponse) +async def create_chat_completion(request: ChatCompletionRequest): + global model, tokenizer + + if len(request.messages) < 1 or request.messages[-1].role == "assistant": + raise HTTPException(status_code=400, detail="Invalid request") + messages = request.messages + if request.messages and request.messages[0].role == 'system': + messages = request.messages + else: + if request.messages: + messages = insert_custom_message(request.messages) + else: + messages = request.messages + print(type(request.messages), request.messages) + gen_params = dict( + messages=messages, + temperature=request.temperature, + top_p=request.top_p, + max_tokens=request.max_tokens or 1024, + echo=False, + stream=request.stream, + repetition_penalty=request.repetition_penalty, + tools=request.tools, + ) + logger.debug(f"==== request ====\n{gen_params}") + + if request.stream: + + # Use the stream mode to read the first few characters, if it is not a function call, direct stram output + predict_stream_generator = predict_stream(request.model, gen_params) + # return EventSourceResponse(predict_stream_generator, media_type="text/event-stream") + output = next(predict_stream_generator) + print(output) + # logger.debug(f"First result output:\n{output}") + if not contains_custom_function(output): + return EventSourceResponse(predict_stream_generator, media_type="text/event-stream") + + # Obtain the result directly at one time and determine whether tools needs to be called. + # logger.debug(f"First result output:\n{output}") + + function_call = None + if output and request.tools: + try: + function_call = process_response(output, use_tool=True) + except: + logger.warning("Failed to parse tool call") + + # CallFunction + if isinstance(function_call, dict): + function_call = FunctionCallResponse(**function_call) + + """ + In this demo, we did not register any tools. + You can use the tools that have been implemented in our `tools_using_demo` and implement your own streaming tool implementation here. + Similar to the following method: + function_args = json.loads(function_call.arguments) + tool_response = dispatch_tool(tool_name: str, tool_params: dict) + """ + tool_response = "" + + if not gen_params.get("messages"): + gen_params["messages"] = [] + + gen_params["messages"].append(ChatMessage( + role="assistant", + content=output, + )) + gen_params["messages"].append(ChatMessage( + role="function", + name=function_call.name, + content=tool_response, + )) + + # Streaming output of results after function calls + generate = predict(request.model, gen_params) + return EventSourceResponse(generate, media_type="text/event-stream") + + else: + # Handled to avoid exceptions in the above parsing function process. + generate = parse_output_text(request.model, output) + return EventSourceResponse(generate, media_type="text/event-stream") + + # Here is the handling of stream = False + response = generate_chatglm3(model, tokenizer, gen_params) + + # Remove the first newline character + if response["text"].startswith("\n"): + response["text"] = response["text"][1:] + response["text"] = response["text"].strip() + + usage = UsageInfo() + function_call, finish_reason = None, "stop" + if request.tools: + try: + function_call = process_response(response["text"], use_tool=True) + except: + logger.warning("Failed to parse tool call, maybe the response is not a tool call or have been answered.") + + if isinstance(function_call, dict): + finish_reason = "function_call" + function_call = FunctionCallResponse(**function_call) + + message = ChatMessage( + role="assistant", + content=response["text"], + function_call=function_call if isinstance(function_call, FunctionCallResponse) else None, + ) + + logger.debug(f"==== message ====\n{message}") + + choice_data = ChatCompletionResponseChoice( + index=0, + message=message, + finish_reason=finish_reason, + ) + task_usage = UsageInfo.model_validate(response["usage"]) + for usage_key, usage_value in task_usage.model_dump().items(): + setattr(usage, usage_key, getattr(usage, usage_key) + usage_value) + + return ChatCompletionResponse( + model=request.model, + id="", # for open_source model, id is empty + choices=[choice_data], + object="chat.completion", + usage=usage + ) + + +async def predict(model_id: str, params: dict): + global model, tokenizer + + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=DeltaMessage(role="assistant"), + finish_reason=None + ) + chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk") + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + + previous_text = "" + for new_response in generate_stream_chatglm3(model, tokenizer, params): + decoded_unicode = new_response["text"] + delta_text = decoded_unicode[len(previous_text):] + previous_text = decoded_unicode + + finish_reason = new_response["finish_reason"] + if len(delta_text) == 0 and finish_reason != "function_call": + continue + + function_call = None + if finish_reason == "function_call": + try: + function_call = process_response(decoded_unicode, use_tool=True) + except: + logger.warning( + "Failed to parse tool call, maybe the response is not a tool call or have been answered.") + + if isinstance(function_call, dict): + function_call = FunctionCallResponse(**function_call) + + delta = DeltaMessage( + content=delta_text, + role="assistant", + function_call=function_call if isinstance(function_call, FunctionCallResponse) else None, + ) + + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=delta, + finish_reason=finish_reason + ) + chunk = ChatCompletionResponse( + model=model_id, + id="", + choices=[choice_data], + object="chat.completion.chunk" + ) + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=DeltaMessage(), + finish_reason="stop" + ) + chunk = ChatCompletionResponse( + model=model_id, + id="", + choices=[choice_data], + object="chat.completion.chunk" + ) + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + yield '[DONE]' + + +def predict_stream(model_id, gen_params): + """ + The function call is compatible with stream mode output. + + The first seven characters are determined. + If not a function call, the stream output is directly generated. + Otherwise, the complete character content of the function call is returned. + + :param model_id: + :param gen_params: + :return: + """ + output = "" + is_function_call = False + has_send_first_chunk = False + print('参数') + print(model_id,gen_params) + for new_response in generate_stream_chatglm3(model, tokenizer, gen_params): + decoded_unicode = new_response["text"] + delta_text = decoded_unicode[len(output):] + output = decoded_unicode + + # When it is not a function call and the character length is> 7, + # try to judge whether it is a function call according to the special function prefix + if not is_function_call: + + # Determine whether a function is called + is_function_call = contains_custom_function(output) + if is_function_call: + continue + + # Non-function call, direct stream output + finish_reason = new_response["finish_reason"] + + # Send an empty string first to avoid truncation by subsequent next() operations. + if not has_send_first_chunk: + message = DeltaMessage( + content="", + role="assistant", + function_call=None, + ) + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=message, + finish_reason=finish_reason + ) + chunk = ChatCompletionResponse( + model=model_id, + id="", + choices=[choice_data], + created=int(time.time()), + object="chat.completion.chunk" + ) + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + + send_msg = delta_text if has_send_first_chunk else output + has_send_first_chunk = True + message = DeltaMessage( + content=send_msg, + role="assistant", + function_call=None, + ) + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=message, + finish_reason=finish_reason + ) + chunk = ChatCompletionResponse( + model=model_id, + id="", + choices=[choice_data], + created=int(time.time()), + object="chat.completion.chunk" + ) + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + + if is_function_call: + yield output + else: + yield '[DONE]' + + +async def parse_output_text(model_id: str, value: str): + """ + Directly output the text content of value + + :param model_id: + :param value: + :return: + """ + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=DeltaMessage(role="assistant", content=value), + finish_reason=None + ) + chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk") + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + + choice_data = ChatCompletionResponseStreamChoice( + index=0, + delta=DeltaMessage(), + finish_reason="stop" + ) + chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk") + yield "{}".format(chunk.model_dump_json(exclude_unset=True)) + yield '[DONE]' + + +def contains_custom_function(value: str) -> bool: + """ + Determine whether 'function_call' according to a special function prefix. + + For example, the functions defined in "tools_using_demo/tool_register.py" are all "get_xxx" and start with "get_" + + [Note] This is not a rigorous judgment method, only for reference. + + :param value: + :return: + """ + return value and 'get_' in value + + +from pathlib import Path +from typing import Annotated, Union + +import typer +from peft import AutoPeftModelForCausalLM, PeftModelForCausalLM +from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + PreTrainedModel, + PreTrainedTokenizer, + PreTrainedTokenizerFast, +) + +ModelType = Union[PreTrainedModel, PeftModelForCausalLM] +TokenizerType = Union[PreTrainedTokenizer, PreTrainedTokenizerFast] + + +def _resolve_path(path: Union[str, Path]) -> Path: + return Path(path).expanduser().resolve() + + +def load_model_and_tokenizer( + model_dir: Union[str, Path], trust_remote_code: bool = True +) -> tuple[ModelType, TokenizerType]: + model_dir = _resolve_path(model_dir) + if (model_dir / 'adapter_config.json').exists(): + model = AutoPeftModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model.peft_config['default'].base_model_name_or_path + else: + model = AutoModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model_dir + tokenizer = AutoTokenizer.from_pretrained( + tokenizer_dir, trust_remote_code=trust_remote_code + ) + return model, tokenizer + + +if __name__ == "__main__": + # Load LLM + # tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH, trust_remote_code=True) + # model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True, device_map="auto").eval() + # 填微调之后的保存路径 + model, tokenizer = load_model_and_tokenizer( + r'E:\Project\Python\ChatGLM3\finetune_demo\output03-24\checkpoint-224000' + ) + # load Embedding + embedding_model = SentenceTransformer(EMBEDDING_PATH, device="cuda") + uvicorn.run(app, host='0.0.0.0', port=8002, workers=1) diff --git a/MemoAI/merge_json.py b/MemoAI/merge_json.py new file mode 100644 index 0000000..af22374 --- /dev/null +++ b/MemoAI/merge_json.py @@ -0,0 +1,26 @@ +import json +import os + +data_dir = r'E:\Project\Python\MemoTrace\data\聊天记录' + +dev_res = [] +train_res = [] + +for filepath, dirnames, filenames in os.walk(data_dir): + for filename in filenames: + if filename.endswith('.json'): + print(filename, filepath) + filepath_ = os.path.join(filepath, filename) + with open(filepath_, 'r', encoding='utf-8') as f: + data = json.load(f) + if data: + if filename.endswith('train.json'): + train_res += data + else: + dev_res += data + +with open('train.json', 'w', encoding='utf-8') as f: + json.dump(train_res, f, ensure_ascii=False, indent=4) + +with open('dev.json', 'w', encoding='utf-8') as f: + json.dump(dev_res, f, ensure_ascii=False, indent=4) diff --git a/MemoAI/readme.md b/MemoAI/readme.md new file mode 100644 index 0000000..bfa16fc --- /dev/null +++ b/MemoAI/readme.md @@ -0,0 +1,440 @@ +# 大模型训练指南 + +## 一、导出聊天记录 + +导出json格式的聊天记录。 + +![img.png](../doc/images/img10.png) + +如果你想合并多个联系人的数据,可以直接运行下面的代码合并 + +```python +import json +import os + +data_dir = r'E:\Project\Python\MemoTrace\data\聊天记录' + +dev_res = [] +train_res = [] + +for filepath, dirnames, filenames in os.walk(data_dir): + for filename in filenames: + if filename.endswith('.json'): + print(filename, filepath) + filepath_ = os.path.join(filepath, filename) + with open(filepath_, 'r', encoding='utf-8') as f: + data = json.load(f) + if data: + if filename.endswith('train.json'): + train_res += data + else: + dev_res += data + +with open('train.json', 'w', encoding='utf-8') as f: + json.dump(train_res, f, ensure_ascii=False, indent=4) + +with open('dev.json', 'w', encoding='utf-8') as f: + json.dump(dev_res, f, ensure_ascii=False, indent=4) + +``` + +你现在应该有两个文件,dev.json(验证集)和train.json(训练集) + +## 二、下载ChatGLM3-68模型 + +下载地址:[https://github.com/THUDM/ChatGLM3](https://github.com/THUDM/ChatGLM3) + +## 使用方式 + +### 环境安装 + +首先需要下载本仓库: + +```shell +git clone https://github.com/THUDM/ChatGLM3 +cd ChatGLM3 +``` + +然后使用 pip 安装依赖: + +``` +pip install -r requirements.txt +``` + ++ 为了保证 `torch` 的版本正确,请严格按照 [官方文档](https://pytorch.org/get-started/locally/) 的说明安装。 ++ **如果遇到问题,请参照ChatGLM3项目的解决方案,不要在本项目中提问** + +## 三、ChatGLM3-6B 微调 + +本目录提供 ChatGLM3-6B 模型的微调示例,包括全量微调和 P-Tuning v2。格式上,提供多轮对话微调样例和输入输出格式微调样例。 + +如果将模型下载到了本地,本文和代码中的 `THUDM/chatglm3-6b` 字段均应替换为相应地址以从本地加载模型。 + +运行示例需要 `python>=3.10`,除基础的 `torch` 依赖外,示例代码运行还需要依赖。 + + +```bash +pip install -r requirements.txt +``` + +## 测试硬件标准 + +我们仅提供了单机多卡/多机多卡的运行示例,因此您需要至少一台具有多个 GPU 的机器。本仓库中的**默认配置文件**中,我们记录了显存的占用情况: + ++ SFT 全量微调: 4张显卡平均分配,每张显卡占用 `48346MiB` 显存。 ++ P-TuningV2 微调: 1张显卡,占用 `18426MiB` 显存。 ++ LORA 微调: 1张显卡,占用 `14082MiB` 显存。 + +> 请注意,该结果仅供参考,对于不同的参数,显存占用可能会有所不同。请结合你的硬件情况进行调整。 + +> 请注意,我们仅仅使用英伟达 Hopper(代表显卡:H100) 和 Ampère(代表显卡:A100) 架构和系列显卡做过测试。如果您使用其他架构的显卡,可能会出现 +> 1. 未知的训练问题 / 显存占用与上述有误差。 +> 2. 架构过低而不支持某些特性。 +> 3. 推理效果问题。 + > 以上三种情况为社区曾经遇到过的问题,虽然概率极地,如果您遇到了以上问题,可以尝试在社区中解决。 + +## 多轮对话格式 + +多轮对话微调示例采用 ChatGLM3 对话格式约定,对不同角色添加不同 `loss_mask` 从而在一遍计算中为多轮回复计算 `loss`。 + +对于数据文件,样例采用如下格式 + +如果您仅希望微调模型的对话能力,而非工具能力,您应该按照以下格式整理数据。 + +```json +[ + { + "conversations": [ + { + "role": "system", + "content": "" + }, + { + "role": "user", + "content": "" + }, + { + "role": "assistant", + "content": "" + }, + // ... Muti Turn + { + "role": "user", + "content": "" + }, + { + "role": "assistant", + "content": "" + } + ] + } + // ... +] +``` + +**请注意,这种方法在微调的step较多的情况下会影响到模型的工具调用功能** + +- `system` 角色为可选角色,但若存在 `system` 角色,其必须出现在 `user` + 角色之前,且一个完整的对话数据(无论单轮或者多轮对话)只能出现一次 `system` 角色。 + +## 数据集格式示例 + +这里以 AdvertiseGen 数据集为例, +您可以从 [Google Drive](https://drive.google.com/file/d/13_vf0xRTQsyneRKdD1bZIr93vBGOczrk/view?usp=sharing) +或者 [Tsinghua Cloud](https://cloud.tsinghua.edu.cn/f/b3f119a008264b1cabd1/?dl=1) 下载 AdvertiseGen 数据集。 +将解压后的 AdvertiseGen 目录放到 `data` 目录下并自行转换为如下格式数据集。 + +> 请注意,现在的微调代码中加入了验证集,因此,对于一组完整的微调数据集,必须包含训练数据集和验证数据集,测试数据集可以不填写。或者直接用验证数据集代替。 + +``` +{"conversations": [{"role": "user", "content": "类型#裙*裙长#半身裙"}, {"role": "assistant", "content": "这款百搭时尚的仙女半身裙,整体设计非常的飘逸随性,穿上之后每个女孩子都能瞬间变成小仙女啦。料子非常的轻盈,透气性也很好,穿到夏天也很舒适。"}]} +``` + +## 配置文件 + +微调配置文件位于 `config` 目录下,包括以下文件: + +1. `ds_zereo_2 / ds_zereo_3.json`: deepspeed 配置文件。 +2. `lora.yaml / ptuning.yaml / sft.yaml`: 模型不同方式的配置文件,包括模型参数、优化器参数、训练参数等。 部分重要参数解释如下: + + data_config 部分 + + train_file: 训练数据集的文件路径。 + + val_file: 验证数据集的文件路径。 + + test_file: 测试数据集的文件路径。 + + num_proc: 在加载数据时使用的进程数量。 + + max_input_length: 输入序列的最大长度。 + + max_output_length: 输出序列的最大长度。 + + training_args 部分 + + output_dir: 用于保存模型和其他输出的目录。 + + max_steps: 训练的最大步数。 + + per_device_train_batch_size: 每个设备(如 GPU)的训练批次大小。 + + dataloader_num_workers: 加载数据时使用的工作线程数量。 + + remove_unused_columns: 是否移除数据中未使用的列。 + + save_strategy: 模型保存策略(例如,每隔多少步保存一次)。 + + save_steps: 每隔多少步保存一次模型。 + + log_level: 日志级别(如 info)。 + + logging_strategy: 日志记录策略。 + + logging_steps: 每隔多少步记录一次日志。 + + per_device_eval_batch_size: 每个设备的评估批次大小。 + + evaluation_strategy: 评估策略(例如,每隔多少步进行一次评估)。 + + eval_steps: 每隔多少步进行一次评估。 + + predict_with_generate: 是否使用生成模式进行预测。 + + generation_config 部分 + + max_new_tokens: 生成的最大新 token 数量。 + + peft_config 部分 + + peft_type: 使用的参数有效调整类型(如 LORA)。 + + task_type: 任务类型,这里是因果语言模型(CAUSAL_LM)。 + + Lora 参数: + + r: LoRA 的秩。 + + lora_alpha: LoRA 的缩放因子。 + + lora_dropout: 在 LoRA 层使用的 dropout 概率 + + P-TuningV2 参数: + + num_virtual_tokens: 虚拟 token 的数量。 + +## 开始微调 + +通过以下代码执行 **单机多卡/多机多卡** 运行,这是使用 `deepspeed` 作为加速方案的,您需要安装 `deepspeed`。 + +```angular2html +cd finetune_demo +OMP_NUM_THREADS=1 torchrun --standalone --nnodes=1 --nproc_per_node=8 finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml configs/ds_zero_2.json +``` + +通过以下代码执行 **单机单卡** 运行。 + +```angular2html +cd finetune_demo +python finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml +``` + +## 从保存点进行微调 + +如果按照上述方式进行训练,每次微调都会从头开始,如果你想从训练一半的模型开始微调,你可以加入第四个参数,这个参数有两种传入方式: + +1. `yes`, 自动从最后一个保存的 Checkpoint开始训练 +2. `XX`, 断点号数字 例 `600` 则从序号600 Checkpoint开始训练 + +例如,这就是一个从最后一个保存点继续微调的示例代码 + +```angular2html +cd finetune_demo +python finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml yes +``` + +## 使用微调后的模型 + +### 在 inference_hf.py 中验证微调后的模型 + +您可以在 `finetune_demo/inference_hf.py` 中使用我们的微调后的模型,仅需要一行代码就能简单的进行测试。 + +```angular2html +python inference_hf.py your_finetune_path --prompt your prompt +``` + +这样,得到的回答就微调后的回答了。 + +### 在本仓库的其他 demo 或者外部仓库使用微调后的模型 + +您可以在任何一个 demo 内使用我们的 `lora` 和 全参微调的模型。这需要你自己按照以下教程进行修改代码。 + +1. 使用`finetune_demo/inference_hf.py`中读入模型的方式替换 demo 中读入模型的方式。 + +> 请注意,对于 LORA 和 P-TuningV2 我们没有合并训练后的模型,而是在`adapter_config.json` +> 中记录了微调型的路径,如果你的原始模型位置发生更改,则你应该修改`adapter_config.json`中`base_model_name_or_path`的路径。 + +```python +def load_model_and_tokenizer( + model_dir: Union[str, Path], trust_remote_code: bool = True +) -> tuple[ModelType, TokenizerType]: + model_dir = _resolve_path(model_dir) + if (model_dir / 'adapter_config.json').exists(): + model = AutoPeftModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model.peft_config['default'].base_model_name_or_path + else: + model = AutoModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model_dir + tokenizer = AutoTokenizer.from_pretrained( + tokenizer_dir, trust_remote_code=trust_remote_code + ) + return model, tokenizer +``` + +2. 读取微调的模型,请注意,你应该使用微调模型的位置,例如,若你的模型位置为`/path/to/finetune_adapter_model` + ,原始模型地址为`path/to/base_model`,则你应该使用`/path/to/finetune_adapter_model`作为`model_dir`。 +3. 完成上述操作后,就能正常使用微调的模型了,其他的调用方式没有变化。 + +### 提示 + +1. 微调代码在开始训练前,会先打印首条训练数据的预处理信息(默认已经注释,可以解除注释),显示为 + +```log +Sanity +Check >> >> >> >> >> >> > +'[gMASK]': 64790 -> -100 +'sop': 64792 -> -100 +'<|system|>': 64794 -> -100 +'': 30910 -> -100 +'\n': 13 -> -100 +'Answer': 20115 -> -100 +'the': 267 -> -100 +'following': 1762 -> -100 +... +'know': 683 -> -100 +'the': 267 -> -100 +'response': 3010 -> -100 +'details': 3296 -> -100 +'.': 30930 -> -100 +'<|assistant|>': 64796 -> -100 +'': 30910 -> 30910 +'\n': 13 -> 13 +'I': 307 -> 307 +'need': 720 -> 720 +'to': 289 -> 289 +'use': 792 -> 792 +... +<< << << << << << < Sanity +Check +``` + +字样,每行依次表示一个 detokenized string, token_id 和 target_id。其中,`target_id`为`token_id`在模型词表中的索引,`-100`表示该 +token 不参与 `loss` 计算。 + +2. `_prepare_model_for_training` 的作用是遍历模型的所有可训练参数,并确保它们的数据类型为`torch.float32`。 + 这在某些情况下是必要的,因为混合精度训练或其他操作可能会更改模型参数的数据类型。该代码默打开,可以注释,但是如果使用 + `half` 格式训练出现问题,可以切换回这个代码,显存可能增加。 +3. 在我们的[Huggingface模型代码](https://huggingface.co/THUDM/chatglm3-6b/blob/main/modeling_chatglm.py)中,有以下内容: + ```python + if self.gradient_checkpointing and self.training: + layer_ret = torch.utils.checkpoint.checkpoint( + layer, + hidden_states, + attention_mask, + rotary_pos_emb, + kv_caches[index], + use_cache, + use_reentrant=False + ) + ``` + 这可能导致训练的时候显存增加,因此,如果您的显存不足,可以尝试将``` use_reentrant``` 修改为`True`。 +4. 微调后的模型可以使用任何支持 `peft` 载入的模型加速框架,在这里,我们没有提供demo。 +5. 本仓库的微调数据集格式与 API 微调数据集格式有一定区别 + + ZhipuAI API 微调数据集中的 `messages` 字段在本仓库为 `conversation` 字段。 + + ZhipuAI API 中的微调文件为 `jsonl`, 在本仓库,需要简单的将文件名改为 `json`。 + +> 以上内容来自ChatGLM3项目 + +## 微调示例 + +配置文件 + +```yaml +data_config: + train_file: train.json + val_file: dev.json + test_file: dev.json + num_proc: 10 +max_input_length: 512 +max_output_length: 128 +training_args: + # see `transformers.Seq2SeqTrainingArguments` + output_dir: ./output03-24 + max_steps: 100000 + # settings for data loading + per_device_train_batch_size: 4 + dataloader_num_workers: 10 + remove_unused_columns: false + # settings for saving checkpoints + save_strategy: steps + save_steps: 2000 + # settings for logging + log_level: info + logging_strategy: steps + logging_steps: 10 + # settings for evaluation + per_device_eval_batch_size: 4 + evaluation_strategy: steps + eval_steps: 5200 + # settings for optimizer + # adam_epsilon: 1e-6 + # uncomment the following line to detect nan or inf values + # debug: underflow_overflow + predict_with_generate: yes + # see `transformers.GenerationConfig` + generation_config: + max_new_tokens: 256 + # set your absolute deepspeed path here + #deepspeed: ds_zero_2.json + # set to true if train with cpu. + use_cpu: false +peft_config: + peft_type: LORA + task_type: CAUSAL_LM + r: 8 + lora_alpha: 32 + lora_dropout: 0.1 +``` + +硬件配置:4090 24G、64G内存、CPU 14700KF 20核28线程 + +你需要根据你的硬件配置修改上述参数,各个参数含义上面有说 + +微调命令(需要指定数据集路径和ChatGLM3基础大模型的路径) + +```shell +python finetune_hf.py data/ E:\\Project\\Python\\Langchain-Chatchat\\chatglm3-6b configs/lora.yaml yes +``` + +## 部署 + +api_server.py修改微调保存路径 +```python +model, tokenizer = load_model_and_tokenizer( + r'E:\Project\Python\ChatGLM3\finetune_demo\output03-24\checkpoint-224000' + ) +``` + +直接运行即可 + +```shell +python api_server.py +``` + +调用示例 + +```python +from openai import OpenAI + +base_url = "http://127.0.0.1:8002/v1/" +client = OpenAI(api_key="EMPTY", base_url=base_url) + +def simple_chat(use_stream=True): + messages = [ + { + "role": "user", + "content": "你好啊" + } + ] + response = client.chat.completions.create( + model="chatglm3-6b", + messages=messages, + stream=use_stream, + max_tokens=256, + temperature=0.8, + presence_penalty=1.1, + top_p=0.8) + if response: + if use_stream: + for chunk in response: + print(chunk.choices[0].delta.content, end='') + else: + content = response.choices[0].message.content + print(content) + else: + print("Error:", response.status_code) + +if __name__ == "__main__": + simple_chat(use_stream=True) +``` \ No newline at end of file diff --git a/doc/ai_readme.md b/doc/ai_readme.md index e3c98a9..0ee47e4 100644 --- a/doc/ai_readme.md +++ b/doc/ai_readme.md @@ -1,3 +1,356 @@ # 大模型训练指南 -这个人很懒,什么都没写 \ No newline at end of file +## 一、导出聊天记录 + +导出json格式的聊天记录。 + +![img.png](images/img10.png) + +你现在应该有两个文件,dev.json(验证集)和train.json(训练集) + +## 二、下载ChatGLM3-68模型 + +下载地址:[https://github.com/THUDM/ChatGLM3](https://github.com/THUDM/ChatGLM3) + +## 使用方式 + +### 环境安装 + +首先需要下载本仓库: + +```shell +git clone https://github.com/THUDM/ChatGLM3 +cd ChatGLM3 +``` + +然后使用 pip 安装依赖: + +``` +pip install -r requirements.txt +``` + ++ 为了保证 `torch` 的版本正确,请严格按照 [官方文档](https://pytorch.org/get-started/locally/) 的说明安装。 ++ **如果遇到问题,请参照ChatGLM3项目的解决方案,不要在本项目中提问** + +## 三、ChatGLM3-6B 微调 + +本目录提供 ChatGLM3-6B 模型的微调示例,包括全量微调和 P-Tuning v2。格式上,提供多轮对话微调样例和输入输出格式微调样例。 + +如果将模型下载到了本地,本文和代码中的 `THUDM/chatglm3-6b` 字段均应替换为相应地址以从本地加载模型。 + +运行示例需要 `python>=3.10`,除基础的 `torch` 依赖外,示例代码运行还需要依赖。 + + +```bash +pip install -r requirements.txt +``` + +## 测试硬件标准 + +我们仅提供了单机多卡/多机多卡的运行示例,因此您需要至少一台具有多个 GPU 的机器。本仓库中的**默认配置文件**中,我们记录了显存的占用情况: + ++ SFT 全量微调: 4张显卡平均分配,每张显卡占用 `48346MiB` 显存。 ++ P-TuningV2 微调: 1张显卡,占用 `18426MiB` 显存。 ++ LORA 微调: 1张显卡,占用 `14082MiB` 显存。 + +> 请注意,该结果仅供参考,对于不同的参数,显存占用可能会有所不同。请结合你的硬件情况进行调整。 + +> 请注意,我们仅仅使用英伟达 Hopper(代表显卡:H100) 和 Ampère(代表显卡:A100) 架构和系列显卡做过测试。如果您使用其他架构的显卡,可能会出现 +> 1. 未知的训练问题 / 显存占用与上述有误差。 +> 2. 架构过低而不支持某些特性。 +> 3. 推理效果问题。 + > 以上三种情况为社区曾经遇到过的问题,虽然概率极地,如果您遇到了以上问题,可以尝试在社区中解决。 + +## 多轮对话格式 + +多轮对话微调示例采用 ChatGLM3 对话格式约定,对不同角色添加不同 `loss_mask` 从而在一遍计算中为多轮回复计算 `loss`。 + +对于数据文件,样例采用如下格式 + +如果您仅希望微调模型的对话能力,而非工具能力,您应该按照以下格式整理数据。 + +```json +[ + { + "conversations": [ + { + "role": "system", + "content": "" + }, + { + "role": "user", + "content": "" + }, + { + "role": "assistant", + "content": "" + }, + // ... Muti Turn + { + "role": "user", + "content": "" + }, + { + "role": "assistant", + "content": "" + } + ] + } + // ... +] +``` + +**请注意,这种方法在微调的step较多的情况下会影响到模型的工具调用功能** + +- `system` 角色为可选角色,但若存在 `system` 角色,其必须出现在 `user` + 角色之前,且一个完整的对话数据(无论单轮或者多轮对话)只能出现一次 `system` 角色。 + +## 数据集格式示例 + +这里以 AdvertiseGen 数据集为例, +您可以从 [Google Drive](https://drive.google.com/file/d/13_vf0xRTQsyneRKdD1bZIr93vBGOczrk/view?usp=sharing) +或者 [Tsinghua Cloud](https://cloud.tsinghua.edu.cn/f/b3f119a008264b1cabd1/?dl=1) 下载 AdvertiseGen 数据集。 +将解压后的 AdvertiseGen 目录放到 `data` 目录下并自行转换为如下格式数据集。 + +> 请注意,现在的微调代码中加入了验证集,因此,对于一组完整的微调数据集,必须包含训练数据集和验证数据集,测试数据集可以不填写。或者直接用验证数据集代替。 + +``` +{"conversations": [{"role": "user", "content": "类型#裙*裙长#半身裙"}, {"role": "assistant", "content": "这款百搭时尚的仙女半身裙,整体设计非常的飘逸随性,穿上之后每个女孩子都能瞬间变成小仙女啦。料子非常的轻盈,透气性也很好,穿到夏天也很舒适。"}]} +``` + +## 配置文件 + +微调配置文件位于 `config` 目录下,包括以下文件: + +1. `ds_zereo_2 / ds_zereo_3.json`: deepspeed 配置文件。 +2. `lora.yaml / ptuning.yaml / sft.yaml`: 模型不同方式的配置文件,包括模型参数、优化器参数、训练参数等。 部分重要参数解释如下: + + data_config 部分 + + train_file: 训练数据集的文件路径。 + + val_file: 验证数据集的文件路径。 + + test_file: 测试数据集的文件路径。 + + num_proc: 在加载数据时使用的进程数量。 + + max_input_length: 输入序列的最大长度。 + + max_output_length: 输出序列的最大长度。 + + training_args 部分 + + output_dir: 用于保存模型和其他输出的目录。 + + max_steps: 训练的最大步数。 + + per_device_train_batch_size: 每个设备(如 GPU)的训练批次大小。 + + dataloader_num_workers: 加载数据时使用的工作线程数量。 + + remove_unused_columns: 是否移除数据中未使用的列。 + + save_strategy: 模型保存策略(例如,每隔多少步保存一次)。 + + save_steps: 每隔多少步保存一次模型。 + + log_level: 日志级别(如 info)。 + + logging_strategy: 日志记录策略。 + + logging_steps: 每隔多少步记录一次日志。 + + per_device_eval_batch_size: 每个设备的评估批次大小。 + + evaluation_strategy: 评估策略(例如,每隔多少步进行一次评估)。 + + eval_steps: 每隔多少步进行一次评估。 + + predict_with_generate: 是否使用生成模式进行预测。 + + generation_config 部分 + + max_new_tokens: 生成的最大新 token 数量。 + + peft_config 部分 + + peft_type: 使用的参数有效调整类型(如 LORA)。 + + task_type: 任务类型,这里是因果语言模型(CAUSAL_LM)。 + + Lora 参数: + + r: LoRA 的秩。 + + lora_alpha: LoRA 的缩放因子。 + + lora_dropout: 在 LoRA 层使用的 dropout 概率 + + P-TuningV2 参数: + + num_virtual_tokens: 虚拟 token 的数量。 + +## 开始微调 + +通过以下代码执行 **单机多卡/多机多卡** 运行,这是使用 `deepspeed` 作为加速方案的,您需要安装 `deepspeed`。 + +```angular2html +cd finetune_demo +OMP_NUM_THREADS=1 torchrun --standalone --nnodes=1 --nproc_per_node=8 finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml configs/ds_zero_2.json +``` + +通过以下代码执行 **单机单卡** 运行。 + +```angular2html +cd finetune_demo +python finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml +``` + +## 从保存点进行微调 + +如果按照上述方式进行训练,每次微调都会从头开始,如果你想从训练一半的模型开始微调,你可以加入第四个参数,这个参数有两种传入方式: + +1. `yes`, 自动从最后一个保存的 Checkpoint开始训练 +2. `XX`, 断点号数字 例 `600` 则从序号600 Checkpoint开始训练 + +例如,这就是一个从最后一个保存点继续微调的示例代码 + +```angular2html +cd finetune_demo +python finetune_hf.py data/AdvertiseGen/ THUDM/chatglm3-6b configs/lora.yaml yes +``` + +## 使用微调后的模型 + +### 在 inference_hf.py 中验证微调后的模型 + +您可以在 `finetune_demo/inference_hf.py` 中使用我们的微调后的模型,仅需要一行代码就能简单的进行测试。 + +```angular2html +python inference_hf.py your_finetune_path --prompt your prompt +``` + +这样,得到的回答就微调后的回答了。 + +### 在本仓库的其他 demo 或者外部仓库使用微调后的模型 + +您可以在任何一个 demo 内使用我们的 `lora` 和 全参微调的模型。这需要你自己按照以下教程进行修改代码。 + +1. 使用`finetune_demo/inference_hf.py`中读入模型的方式替换 demo 中读入模型的方式。 + +> 请注意,对于 LORA 和 P-TuningV2 我们没有合并训练后的模型,而是在`adapter_config.json` +> 中记录了微调型的路径,如果你的原始模型位置发生更改,则你应该修改`adapter_config.json`中`base_model_name_or_path`的路径。 + +```python +def load_model_and_tokenizer( + model_dir: Union[str, Path], trust_remote_code: bool = True +) -> tuple[ModelType, TokenizerType]: + model_dir = _resolve_path(model_dir) + if (model_dir / 'adapter_config.json').exists(): + model = AutoPeftModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model.peft_config['default'].base_model_name_or_path + else: + model = AutoModelForCausalLM.from_pretrained( + model_dir, trust_remote_code=trust_remote_code, device_map='auto' + ) + tokenizer_dir = model_dir + tokenizer = AutoTokenizer.from_pretrained( + tokenizer_dir, trust_remote_code=trust_remote_code + ) + return model, tokenizer +``` + +2. 读取微调的模型,请注意,你应该使用微调模型的位置,例如,若你的模型位置为`/path/to/finetune_adapter_model` + ,原始模型地址为`path/to/base_model`,则你应该使用`/path/to/finetune_adapter_model`作为`model_dir`。 +3. 完成上述操作后,就能正常使用微调的模型了,其他的调用方式没有变化。 + +### 提示 + +1. 微调代码在开始训练前,会先打印首条训练数据的预处理信息(默认已经注释,可以解除注释),显示为 + +```log +Sanity +Check >> >> >> >> >> >> > +'[gMASK]': 64790 -> -100 +'sop': 64792 -> -100 +'<|system|>': 64794 -> -100 +'': 30910 -> -100 +'\n': 13 -> -100 +'Answer': 20115 -> -100 +'the': 267 -> -100 +'following': 1762 -> -100 +... +'know': 683 -> -100 +'the': 267 -> -100 +'response': 3010 -> -100 +'details': 3296 -> -100 +'.': 30930 -> -100 +'<|assistant|>': 64796 -> -100 +'': 30910 -> 30910 +'\n': 13 -> 13 +'I': 307 -> 307 +'need': 720 -> 720 +'to': 289 -> 289 +'use': 792 -> 792 +... +<< << << << << << < Sanity +Check +``` + +字样,每行依次表示一个 detokenized string, token_id 和 target_id。其中,`target_id`为`token_id`在模型词表中的索引,`-100`表示该 +token 不参与 `loss` 计算。 + +2. `_prepare_model_for_training` 的作用是遍历模型的所有可训练参数,并确保它们的数据类型为`torch.float32`。 + 这在某些情况下是必要的,因为混合精度训练或其他操作可能会更改模型参数的数据类型。该代码默打开,可以注释,但是如果使用 + `half` 格式训练出现问题,可以切换回这个代码,显存可能增加。 +3. 在我们的[Huggingface模型代码](https://huggingface.co/THUDM/chatglm3-6b/blob/main/modeling_chatglm.py)中,有以下内容: + ```python + if self.gradient_checkpointing and self.training: + layer_ret = torch.utils.checkpoint.checkpoint( + layer, + hidden_states, + attention_mask, + rotary_pos_emb, + kv_caches[index], + use_cache, + use_reentrant=False + ) + ``` + 这可能导致训练的时候显存增加,因此,如果您的显存不足,可以尝试将``` use_reentrant``` 修改为`True`。 +4. 微调后的模型可以使用任何支持 `peft` 载入的模型加速框架,在这里,我们没有提供demo。 +5. 本仓库的微调数据集格式与 API 微调数据集格式有一定区别 + + ZhipuAI API 微调数据集中的 `messages` 字段在本仓库为 `conversation` 字段。 + + ZhipuAI API 中的微调文件为 `jsonl`, 在本仓库,需要简单的将文件名改为 `json`。 + +> 以上内容来自ChatGLM3项目 + +## 微调示例 + +配置文件 + +```yaml +data_config: + train_file: train.json + val_file: dev.json + test_file: dev.json + num_proc: 10 +max_input_length: 512 +max_output_length: 128 +training_args: + # see `transformers.Seq2SeqTrainingArguments` + output_dir: ./output03-24 + max_steps: 100000 + # settings for data loading + per_device_train_batch_size: 4 + dataloader_num_workers: 10 + remove_unused_columns: false + # settings for saving checkpoints + save_strategy: steps + save_steps: 2000 + # settings for logging + log_level: info + logging_strategy: steps + logging_steps: 10 + # settings for evaluation + per_device_eval_batch_size: 4 + evaluation_strategy: steps + eval_steps: 5200 + # settings for optimizer + # adam_epsilon: 1e-6 + # uncomment the following line to detect nan or inf values + # debug: underflow_overflow + predict_with_generate: yes + # see `transformers.GenerationConfig` + generation_config: + max_new_tokens: 256 + # set your absolute deepspeed path here + #deepspeed: ds_zero_2.json + # set to true if train with cpu. + use_cpu: false +peft_config: + peft_type: LORA + task_type: CAUSAL_LM + r: 8 + lora_alpha: 32 + lora_dropout: 0.1 +``` + +硬件配置:4090 24G、64G内存、CPU 14700KF 20核28线程 + +你需要根据你的硬件配置修改上述参数,各个参数含义上面有说 + +微调命令(需要指定数据集路径和ChatGLM3基础大模型的路径) + +```shell +python finetune_hf.py data/ E:\\Project\\Python\\Langchain-Chatchat\\chatglm3-6b configs/lora.yaml yes +``` diff --git a/doc/images/img10.png b/doc/images/img10.png new file mode 100644 index 0000000000000000000000000000000000000000..f23b61a08b69fa951d48a3b7a797e2e610b4851a GIT binary patch literal 25050 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 literal 0 HcmV?d00001 diff --git a/readme.md b/readme.md index 2aefe72..f36b2e7 100644 --- a/readme.md +++ b/readme.md @@ -113,7 +113,7 @@ [详见开发者手册](./doc/开发者手册.md) -[AI聊天](./doc/ai_readme.md) +[AI聊天](./MemoAI/readme.md) ## PC端使用过程中部分问题解决(可参考) From 667e66379b93cfd6aaa3cdfc472c7bdb1594c91d Mon Sep 17 00:00:00 2001 From: shuaikangzhou <863909694@qq.com> Date: Fri, 29 Mar 2024 15:21:31 +0800 Subject: [PATCH 3/4] update readme.md --- MemoAI/img/img.png | Bin 0 -> 39430 bytes MemoAI/img/img2.png | Bin 0 -> 24698 bytes MemoAI/readme.md | 14 ++++++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 MemoAI/img/img.png create mode 100644 MemoAI/img/img2.png diff --git a/MemoAI/img/img.png b/MemoAI/img/img.png new file mode 100644 index 0000000000000000000000000000000000000000..fa28704bee178ca7cce2387905a03c29f799c80c GIT binary patch literal 39430 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 literal 0 HcmV?d00001 diff --git a/MemoAI/img/img2.png b/MemoAI/img/img2.png new file mode 100644 index 0000000000000000000000000000000000000000..70f390aefb5b78d5af2378718fcecc6c8e649062 GIT binary patch literal 24698 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 literal 0 HcmV?d00001 diff --git a/MemoAI/readme.md b/MemoAI/readme.md index bfa16fc..e54398e 100644 --- a/MemoAI/readme.md +++ b/MemoAI/readme.md @@ -330,6 +330,8 @@ token 不参与 `loss` 计算。 配置文件 +config/lora.yaml + ```yaml data_config: train_file: train.json @@ -389,7 +391,7 @@ python finetune_hf.py data/ E:\\Project\\Python\\Langchain-Chatchat\\chatglm3- ## 部署 -api_server.py修改微调保存路径 +在api_server.py修改微调保存路径 ```python model, tokenizer = load_model_and_tokenizer( r'E:\Project\Python\ChatGLM3\finetune_demo\output03-24\checkpoint-224000' @@ -437,4 +439,12 @@ def simple_chat(use_stream=True): if __name__ == "__main__": simple_chat(use_stream=True) -``` \ No newline at end of file +``` + +## 体验地址 + +[https://chat/memotrace.cn/](https://chat/memotrace.cn/) + +![img.png](img/img.png) + +![img.png](img/img2.png) \ No newline at end of file From 4fed8cea469d212533458e66454ae63b57a652c5 Mon Sep 17 00:00:00 2001 From: shuaikangzhou <863909694@qq.com> Date: Fri, 29 Mar 2024 15:23:50 +0800 Subject: [PATCH 4/4] update readme.md --- MemoAI/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemoAI/readme.md b/MemoAI/readme.md index e54398e..6a209c3 100644 --- a/MemoAI/readme.md +++ b/MemoAI/readme.md @@ -404,7 +404,7 @@ model, tokenizer = load_model_and_tokenizer( python api_server.py ``` -调用示例 +调用示例(你可以在任意一个支持ChatGPT的应用中使用它): ```python from openai import OpenAI